();
+ return "(" + string.Join( ",", args.Zip( attr.TransformNames, ( pType, label ) => label != null ? $"{pType} {label}" : pType.Name ) ) + ")";
+ }
+
+ return prop.PropertyType.Name;
+
+ // Debug.Log( $"prop.PropertyType.FullName {prop.PropertyType.FullName}" );
+ // Debug.Log( $"prop.GetMethod.ReturnType.FullName {prop.GetMethod.ReturnType.FullName}" );
+ // if( prop.IsGenericMethod )
+ // Debug.Log( $"prop.GetMethod.GetGenericMethodDefinition().ReturnType {prop.GetMethod.GetGenericMethodDefinition().ReturnType}" );
+
+
+ // prop.GetMethod.gene
+
+ // if(prop.typ)
+
+ // return prop.GetMethod.IsGenericMethod ? prop.GetMethod.GetGenericMethodDefinition().ReturnType.FullName : type.FullName;
+ }
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/Editor/Codegen/StaticAccessInterfaceGenerator.cs.meta b/Editor/Codegen/StaticAccessInterfaceGenerator.cs.meta
new file mode 100644
index 0000000..c212dc3
--- /dev/null
+++ b/Editor/Codegen/StaticAccessInterfaceGenerator.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 33527b6429cb4500b0f22c7cfd7c5fff
+timeCreated: 1775855648
\ No newline at end of file
diff --git a/Editor/Mathfs.Editor.asmdef b/Editor/Mathfs.Editor.asmdef
new file mode 100644
index 0000000..63e841f
--- /dev/null
+++ b/Editor/Mathfs.Editor.asmdef
@@ -0,0 +1,19 @@
+{
+ "name": "Mathfs.Editor",
+ "rootNamespace": "",
+ "references": [
+ "GUID:6071c9f2ce0a4407c93af459fa416e54",
+ "GUID:d8b63aba1907145bea998dd612889d6b"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Editor/Mathfs.Editor.asmdef.meta b/Editor/Mathfs.Editor.asmdef.meta
new file mode 100644
index 0000000..378efcc
--- /dev/null
+++ b/Editor/Mathfs.Editor.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: e310b6bc7297442988d0ba2f3b166f3a
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Property Drawers.meta b/Editor/Property Drawers.meta
new file mode 100644
index 0000000..9e08f0b
--- /dev/null
+++ b/Editor/Property Drawers.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0c50cca08ed628843a5c7077b998eef6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Property Drawers/ClampedIntegerDrawers.cs b/Editor/Property Drawers/ClampedIntegerDrawers.cs
new file mode 100644
index 0000000..7f42d16
--- /dev/null
+++ b/Editor/Property Drawers/ClampedIntegerDrawers.cs
@@ -0,0 +1,81 @@
+namespace Freya {
+
+ using UnityEngine;
+ using UnityEditor;
+
+ public class ClampedIntegerDrawer : PropertyDrawer {
+ protected bool hasInitialized; // this flag is used to ensure it's valid when the inspector reveals it
+ }
+
+ [CustomPropertyDrawer( typeof(PositiveIntegerAttribute) )] public class PositiveIntegerDrawer : ClampedIntegerDrawer {
+ public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, false, false, true );
+ }
+
+ [CustomPropertyDrawer( typeof(NegativeIntegerAttribute) )] public class NegativeIntegerDrawer : ClampedIntegerDrawer {
+ public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, true, false, false );
+ }
+
+ [CustomPropertyDrawer( typeof(NonNegativeIntegerAttribute) )] public class NonNegativeIntegerDrawer : ClampedIntegerDrawer {
+ public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, false, true, true );
+ }
+
+ [CustomPropertyDrawer( typeof(NonPositiveIntegerAttribute) )] public class NonPositiveIntegerDrawer : ClampedIntegerDrawer {
+ public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, true, true, false );
+ }
+
+ [CustomPropertyDrawer( typeof(NonZeroIntegerAttribute) )] public class NonZeroIntegerDrawer : ClampedIntegerDrawer {
+ public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) => IntegerDrawerUtils.OnGUI( ref hasInitialized, rect, property, label, true, false, true );
+ }
+
+ static class IntegerDrawerUtils {
+
+ public static void OnGUI( ref bool hasInitialized, Rect rect, SerializedProperty property, GUIContent label, bool allowNegative, bool allowZero, bool allowPositive ) {
+ EditorGUI.BeginProperty( rect, label, property );
+
+ if( property.propertyType == SerializedPropertyType.Integer ) {
+ // PositiveIntegerAttribute range = attribute as PositiveIntegerAttribute;
+ using( EditorGUI.ChangeCheckScope chChk = new() ) {
+ int prevValue = property.intValue;
+
+ EditorGUI.PropertyField( rect, property, label );
+ if( chChk.changed || hasInitialized == false ) {
+ hasInitialized = true;
+ int newValue = property.intValue;
+ // special case, make it "skip" 0 when scrubbing
+ if( allowNegative && allowZero == false && allowPositive && newValue == 0 ) {
+ property.intValue = prevValue > 0 ? -1 : 1;
+ } else {
+ // other cases can clamp
+ int min = GetRangeMin( allowNegative, allowZero, allowPositive );
+ int max = GetRangeMax( allowNegative, allowZero, allowPositive );
+ if( newValue < min || newValue > max )
+ property.intValue = Mathf.Clamp( newValue, min, max );
+ }
+ }
+ }
+ } else {
+ EditorGUI.LabelField( rect, label.text, $"PositiveInteger only works on integer fields. Field is: {property.propertyType.ToString()}" );
+ }
+
+ EditorGUI.EndProperty();
+ }
+
+ static int GetRangeMin( bool allowNegative, bool allowZero, bool allowPositive ) {
+ if( allowNegative )
+ return int.MinValue;
+ if( allowZero )
+ return 0;
+ return 1;
+ }
+
+ static int GetRangeMax( bool allowNegative, bool allowZero, bool allowPositive ) {
+ if( allowPositive )
+ return int.MaxValue;
+ if( allowZero )
+ return 0;
+ return -1;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/Editor/Property Drawers/ClampedIntegerDrawers.cs.meta b/Editor/Property Drawers/ClampedIntegerDrawers.cs.meta
new file mode 100644
index 0000000..bfa43bf
--- /dev/null
+++ b/Editor/Property Drawers/ClampedIntegerDrawers.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b35a7699a7b00a441a973209a563f913
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Property Drawers/RationalDrawer.cs b/Editor/Property Drawers/RationalDrawer.cs
new file mode 100644
index 0000000..49dfa9d
--- /dev/null
+++ b/Editor/Property Drawers/RationalDrawer.cs
@@ -0,0 +1,57 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+
+namespace Freya {
+
+ [CustomPropertyDrawer( typeof(rat) )]
+ public class IngredientDrawer : PropertyDrawer {
+
+ bool hasInitialized;
+
+ // Draw the property inside the given rect
+ public override void OnGUI( Rect rect, SerializedProperty property, GUIContent label ) {
+ // Using BeginProperty / EndProperty on the parent property means that
+ // prefab override logic works on the entire property.
+ EditorGUI.BeginProperty( rect, label, property );
+
+ // Draw label
+ rect = EditorGUI.PrefixLabel( rect, GUIUtility.GetControlID( FocusType.Passive ), label );
+
+ // Don't make child fields be indented
+ int indent = EditorGUI.indentLevel;
+ EditorGUI.indentLevel = 0;
+
+ // Calculate rects
+ int slashWidth = 10;
+ int inputBoxWidth = Mathfs.FloorToInt( ( rect.width - slashWidth ) / 2 );
+ Rect rectNumerator = new Rect( rect.x, rect.y, inputBoxWidth, rect.height );
+ Rect rectDivision = new Rect( rect.x + inputBoxWidth, rect.y, slashWidth, rect.height );
+ Rect rectDenominator = new Rect( rect.x + inputBoxWidth + slashWidth, rect.y, inputBoxWidth, rect.height );
+
+ SerializedProperty numerator = property.FindPropertyRelative( "n" );
+ SerializedProperty denominator = property.FindPropertyRelative( "d" );
+
+ using( var chChk = new EditorGUI.ChangeCheckScope() ) {
+ EditorGUI.DelayedIntField( rectNumerator, numerator, GUIContent.none );
+ GUI.Label( rectDivision, "/" );
+ EditorGUI.DelayedIntField( rectDenominator, denominator, GUIContent.none );
+ if( chChk.changed || hasInitialized == false ) {
+ hasInitialized = true;
+ // validation, make sure we're not dividing by 0
+ if( denominator.intValue == 0 ) {
+ denominator.intValue = 1;
+ Debug.LogWarning( "Rational numbers cannot divide by 0. Setting denominator to 1", property.serializedObject.targetObject );
+ }
+ }
+ }
+
+ // Set indent back to what it was
+ EditorGUI.indentLevel = indent;
+
+ EditorGUI.EndProperty();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Editor/Property Drawers/RationalDrawer.cs.meta b/Editor/Property Drawers/RationalDrawer.cs.meta
new file mode 100644
index 0000000..2dc1257
--- /dev/null
+++ b/Editor/Property Drawers/RationalDrawer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a6c3402c7b9d5aa4b8518af9657a070c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Extensions.cs b/Extensions.cs
deleted file mode 100644
index 5262558..0000000
--- a/Extensions.cs
+++ /dev/null
@@ -1,513 +0,0 @@
-// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
-
-using System.Runtime.CompilerServices;
-using UnityEngine;
-
-
-namespace Freya {
-
- /// Various extensions for floats, vectors and colors
- public static class MathfsExtensions {
-
- const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;
-
- #region Vector rotation and angles
-
- /// Returns the angle of this vector, in radians
- /// The vector to get the angle of. It does not have to be normalized
- ///
- [MethodImpl( INLINE )] public static float Angle( this Vector2 v ) => Mathf.Atan2( v.y, v.x );
-
- /// Rotates the vector 90 degrees clockwise (negative Z axis rotation)
- [MethodImpl( INLINE )] public static Vector2 Rotate90CW( this Vector2 v ) => new Vector2( v.y, -v.x );
-
- /// Rotates the vector 90 degrees counter-clockwise (positive Z axis rotation)
- [MethodImpl( INLINE )] public static Vector2 Rotate90CCW( this Vector2 v ) => new Vector2( -v.y, v.x );
-
- /// Rotates the vector around pivot with the given angle (in radians)
- /// The vector to rotate
- /// The point to rotate around
- /// The angle to rotate by, in radians
- [MethodImpl( INLINE )] public static Vector2 RotateAround( this Vector2 v, Vector2 pivot, float angRad ) => Rotate( v - pivot, angRad ) + pivot;
-
- /// Rotates the vector around (0,0) with the given angle (in radians)
- /// The vector to rotate
- /// The angle to rotate by, in radians
- public static Vector2 Rotate( this Vector2 v, float angRad ) {
- float ca = Mathf.Cos( angRad );
- float sa = Mathf.Sin( angRad );
- return new Vector2( ca * v.x - sa * v.y, sa * v.x + ca * v.y );
- }
-
- /// Converts an angle in degrees to radians
- /// The angle, in degrees, to convert to radians
- [MethodImpl( INLINE )] public static float DegToRad( this float angDegrees ) => angDegrees * Mathfs.Deg2Rad;
-
- /// Converts an angle in radians to degrees
- /// The angle, in radians, to convert to degrees
- [MethodImpl( INLINE )] public static float RadToDeg( this float angRadians ) => angRadians * Mathfs.Rad2Deg;
-
- /// Extracts the quaternion components into a Vector4
- /// The quaternion to get the components of
- [MethodImpl( INLINE )] public static Vector4 ToVector4( this Quaternion q ) => new Vector4( q.x, q.y, q.z, q.w );
-
- #endregion
-
- #region Swizzling
-
- /// Returns X and Y as a Vector2, equivalent to new Vector2(v.x,v.y)
- [MethodImpl( INLINE )] public static Vector2 XY( this Vector2 v ) => new Vector2( v.y, v.x );
-
- /// Returns Y and X as a Vector2, equivalent to new Vector2(v.y,v.x)
- [MethodImpl( INLINE )] public static Vector2 YX( this Vector2 v ) => new Vector2( v.y, v.x );
-
- /// Returns X and Z as a Vector2, equivalent to new Vector2(v.x,v.z)
- [MethodImpl( INLINE )] public static Vector2 XZ( this Vector3 v ) => new Vector2( v.x, v.z );
-
- /// Returns this vector as a Vector3, slotting X into X, and Y into Z, and the input value y into Y.
- /// Equivalent to new Vector3(v.x,y,v.y)
- [MethodImpl( INLINE )] public static Vector3 XZtoXYZ( this Vector2 v, float y = 0 ) => new Vector3( v.x, y, v.y );
-
- /// Returns this vector as a Vector3, slotting X into X, and Y into Y, and the input value z into Z.
- /// Equivalent to new Vector3(v.x,v.y,z)
- [MethodImpl( INLINE )] public static Vector3 XYtoXYZ( this Vector2 v, float z = 0 ) => new Vector3( v.x, v.y, z );
-
- /// Sets X to 0
- [MethodImpl( INLINE )] public static Vector2 FlattenX( this Vector2 v ) => new Vector2( 0f, v.y );
-
- /// Sets Y to 0
- [MethodImpl( INLINE )] public static Vector2 FlattenY( this Vector2 v ) => new Vector2( v.x, 0f );
-
- /// Sets X to 0
- [MethodImpl( INLINE )] public static Vector3 FlattenX( this Vector3 v ) => new Vector3( 0f, v.y, v.z );
-
- /// Sets Y to 0
- [MethodImpl( INLINE )] public static Vector3 FlattenY( this Vector3 v ) => new Vector3( v.x, 0f, v.z );
-
- /// Sets Z to 0
- [MethodImpl( INLINE )] public static Vector3 FlattenZ( this Vector3 v ) => new Vector3( v.x, v.y, 0f );
-
- #endregion
-
- #region Vector directions & magnitudes
-
- /// Returns a vector with the same direction, but with the given magnitude.
- /// Equivalent to v.normalized*mag
- [MethodImpl( INLINE )] public static Vector2 WithMagnitude( this Vector2 v, float mag ) => v.normalized * mag;
-
- /// Returns a vector with the same direction, but with the given magnitude.
- /// Equivalent to v.normalized*mag
- [MethodImpl( INLINE )] public static Vector3 WithMagnitude( this Vector3 v, float mag ) => v.normalized * mag;
-
- /// Returns the vector going from one position to another, also known as the displacement.
- /// Equivalent to target-v
- [MethodImpl( INLINE )] public static Vector2 To( this Vector2 v, Vector2 target ) => target - v;
-
- /// Returns the vector going from one position to another, also known as the displacement.
- /// Equivalent to target-v
- [MethodImpl( INLINE )] public static Vector3 To( this Vector3 v, Vector3 target ) => target - v;
-
- /// Returns the normalized direction from this vector to the target.
- /// Equivalent to (target-v).normalized or v.To(target).normalized
- [MethodImpl( INLINE )] public static Vector2 DirTo( this Vector2 v, Vector2 target ) => ( target - v ).normalized;
-
- /// Returns the normalized direction from this vector to the target.
- /// Equivalent to (target-v).normalized or v.To(target).normalized
- [MethodImpl( INLINE )] public static Vector3 DirTo( this Vector3 v, Vector3 target ) => ( target - v ).normalized;
-
- #endregion
-
- #region Color manipulation
-
- /// Returns the same color, but with the specified alpha value
- /// The source color
- /// The new alpha value
- [MethodImpl( INLINE )] public static Color WithAlpha( this Color c, float a ) => new Color( c.r, c.g, c.b, a );
-
- /// Returns the same color and alpha, but with RGB multiplied by the given value
- /// The source color
- /// The multiplier for the RGB channels
- [MethodImpl( INLINE )] public static Color MultiplyRGB( this Color c, float m ) => new Color( c.r * m, c.g * m, c.b * m, c.a );
-
- /// Returns the same color and alpha, but with the RGB values multiplief by another color
- /// The source color
- /// The color to multiply RGB by
- [MethodImpl( INLINE )] public static Color MultiplyRGB( this Color c, Color m ) => new Color( c.r * m.r, c.g * m.g, c.b * m.b, c.a );
-
- /// Returns the same color, but with the alpha channel multiplied by the given value
- /// The source color
- /// The multiplier for the alpha
- [MethodImpl( INLINE )] public static Color MultiplyA( this Color c, float m ) => new Color( c.r, c.g, c.b, c.a * m );
-
- #endregion
-
- #region Rect
-
- /// Expands the rectangle to encapsulate the point p
- /// The rectangle to expand
- /// The point to encapsulate
- public static Rect Encapsulate( this Rect r, Vector2 p ) {
- r.xMax = Mathf.Max( r.xMax, p.x );
- r.xMin = Mathf.Min( r.xMin, p.x );
- r.yMax = Mathf.Max( r.yMax, p.y );
- r.yMin = Mathf.Min( r.yMin, p.y );
- return r;
- }
-
- #endregion
-
- #region Simple float and int operations
-
- /// Returns true if v is between or equal to min & max
- ///
- [MethodImpl( INLINE )] public static bool Within( this float v, float min, float max ) => v >= min && v <= max;
-
- /// Returns true if v is between or equal to min & max
- ///
- [MethodImpl( INLINE )] public static bool Within( this int v, int min, int max ) => v >= min && v <= max;
-
- /// Returns true if v is between, but not equal to, min & max
- ///
- [MethodImpl( INLINE )] public static bool Between( this float v, float min, float max ) => v > min && v < max;
-
- /// Returns true if v is between, but not equal to, min & max
- ///
- [MethodImpl( INLINE )] public static bool Between( this int v, int min, int max ) => v > min && v < max;
-
- /// Clamps the value to be at least min
- [MethodImpl( INLINE )] public static float AtLeast( this float v, float min ) => v < min ? min : v;
-
- /// Clamps the value to be at least min
- [MethodImpl( INLINE )] public static int AtLeast( this int v, int min ) => v < min ? min : v;
-
- /// Clamps the value to be at most max
- [MethodImpl( INLINE )] public static float AtMost( this float v, float max ) => v > max ? max : v;
-
- /// Clamps the value to be at most max
- [MethodImpl( INLINE )] public static int AtMost( this int v, int max ) => v > max ? max : v;
-
- /// Squares the value. Equivalent to v*v
- [MethodImpl( INLINE )] public static float Square( this float v ) => v * v;
-
- /// Cubes the value. Equivalent to v*v*v
- [MethodImpl( INLINE )] public static float Cube( this float v ) => v * v * v;
-
- /// Squares the value. Equivalent to v*v
- [MethodImpl( INLINE )] public static int Square( this int v ) => v * v;
-
- /// The next integer, modulo length. Behaves the way you want with negative values for stuff like array index access etc
- [MethodImpl( INLINE )] public static int NextMod( this int value, int length ) => ( value + 1 ).Mod( length );
-
- /// The previous integer, modulo length. Behaves the way you want with negative values for stuff like array index access etc
- [MethodImpl( INLINE )] public static int PrevMod( this int value, int length ) => ( value - 1 ).Mod( length );
-
- #endregion
-
- #region Extension method counterparts of the static Mathfs functions - lots of boilerplate in here
-
- #region Math operations
-
- ///
- [MethodImpl( INLINE )] public static float Sqrt( this float value ) => Mathfs.Sqrt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Sqrt( this Vector2 value ) => Mathfs.Sqrt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Sqrt( this Vector3 value ) => Mathfs.Sqrt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Sqrt( this Vector4 value ) => Mathfs.Sqrt( value );
-
- ///
- [MethodImpl( INLINE )] public static float Cbrt( this float value ) => Mathfs.Cbrt( value );
-
- ///
- [MethodImpl( INLINE )] public static float Pow( this float value, float exponent ) => Mathfs.Pow( value, exponent );
-
- #endregion
-
- #region Absolute Values
-
- ///
- [MethodImpl( INLINE )] public static float Abs( this float value ) => Mathfs.Abs( value );
-
- ///
- [MethodImpl( INLINE )] public static int Abs( this int value ) => Mathfs.Abs( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Abs( this Vector2 v ) => Mathfs.Abs( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Abs( this Vector3 v ) => Mathfs.Abs( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Abs( this Vector4 v ) => Mathfs.Abs( v );
-
- #endregion
-
- #region Clamping
-
- ///
- [MethodImpl( INLINE )] public static float Clamp( this float value, float min, float max ) => Mathfs.Clamp( value, min, max );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Clamp( this Vector2 v, Vector2 min, Vector2 max ) => Mathfs.Clamp( v, min, max );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Clamp( this Vector3 v, Vector3 min, Vector3 max ) => Mathfs.Clamp( v, min, max );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Clamp( this Vector4 v, Vector4 min, Vector4 max ) => Mathfs.Clamp( v, min, max );
-
- ///
- [MethodImpl( INLINE )] public static int Clamp( this int value, int min, int max ) => Mathfs.Clamp( value, min, max );
-
- ///
- [MethodImpl( INLINE )] public static float Clamp01( this float value ) => Mathfs.Clamp01( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Clamp01( this Vector2 v ) => Mathfs.Clamp01( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Clamp01( this Vector3 v ) => Mathfs.Clamp01( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Clamp01( this Vector4 v ) => Mathfs.Clamp01( v );
-
- ///
- [MethodImpl( INLINE )] public static float ClampNeg1to1( this float value ) => Mathfs.ClampNeg1to1( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 ClampNeg1to1( this Vector2 v ) => Mathfs.ClampNeg1to1( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 ClampNeg1to1( this Vector3 v ) => Mathfs.ClampNeg1to1( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 ClampNeg1to1( this Vector4 v ) => Mathfs.ClampNeg1to1( v );
-
- #endregion
-
- #region Min & Max
-
- ///
- [MethodImpl( INLINE )] public static float Min( this Vector2 v ) => Mathfs.Min( v );
-
- ///
- [MethodImpl( INLINE )] public static float Min( this Vector3 v ) => Mathfs.Min( v );
-
- ///
- [MethodImpl( INLINE )] public static float Min( this Vector4 v ) => Mathfs.Min( v );
-
- ///
- [MethodImpl( INLINE )] public static float Max( this Vector2 v ) => Mathfs.Max( v );
-
- ///
- [MethodImpl( INLINE )] public static float Max( this Vector3 v ) => Mathfs.Max( v );
-
- ///
- [MethodImpl( INLINE )] public static float Max( this Vector4 v ) => Mathfs.Max( v );
-
- #endregion
-
- #region Signs & Rounding
-
- ///
- [MethodImpl( INLINE )] public static float Sign( this float value ) => Mathfs.Sign( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Sign( this Vector2 value ) => Mathfs.Sign( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Sign( this Vector3 value ) => Mathfs.Sign( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Sign( this Vector4 value ) => Mathfs.Sign( value );
-
- ///
- [MethodImpl( INLINE )] public static int Sign( this int value ) => Mathfs.Sign( value );
-
- ///
- [MethodImpl( INLINE )] public static int SignAsInt( this float value ) => Mathfs.SignAsInt( value );
-
- ///
- [MethodImpl( INLINE )] public static float SignWithZero( this float value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 SignWithZero( this Vector2 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 SignWithZero( this Vector3 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 SignWithZero( this Vector4 value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZero( value, zeroThreshold );
-
- ///
- [MethodImpl( INLINE )] public static int SignWithZero( this int value ) => Mathfs.SignWithZero( value );
-
- ///
- [MethodImpl( INLINE )] public static int SignWithZeroAsInt( this float value, float zeroThreshold = 0.000001f ) => Mathfs.SignWithZeroAsInt( value, zeroThreshold );
-
- ///
- [MethodImpl( INLINE )] public static float Floor( this float value ) => Mathfs.Floor( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Floor( this Vector2 value ) => Mathfs.Floor( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Floor( this Vector3 value ) => Mathfs.Floor( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Floor( this Vector4 value ) => Mathfs.Floor( value );
-
- ///
- [MethodImpl( INLINE )] public static int FloorToInt( this float value ) => Mathfs.FloorToInt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2Int FloorToInt( this Vector2 value ) => Mathfs.FloorToInt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector3Int FloorToInt( this Vector3 value ) => Mathfs.FloorToInt( value );
-
- ///
- [MethodImpl( INLINE )] public static float Ceil( this float value ) => Mathfs.Ceil( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Ceil( this Vector2 value ) => Mathfs.Ceil( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Ceil( this Vector3 value ) => Mathfs.Ceil( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Ceil( this Vector4 value ) => Mathfs.Ceil( value );
-
- ///
- [MethodImpl( INLINE )] public static int CeilToInt( this float value ) => Mathfs.CeilToInt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2Int CeilToInt( this Vector2 value ) => Mathfs.CeilToInt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector3Int CeilToInt( this Vector3 value ) => Mathfs.CeilToInt( value );
-
- ///
- [MethodImpl( INLINE )] public static float Round( this float value ) => Mathfs.Round( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Round( this Vector2 value ) => Mathfs.Round( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Round( this Vector3 value ) => Mathfs.Round( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Round( this Vector4 value ) => Mathfs.Round( value );
-
- ///
- [MethodImpl( INLINE )] public static float Round( this float value, float snapInterval ) => Mathfs.Round( value, snapInterval );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Round( this Vector2 value, float snapInterval ) => Mathfs.Round( value, snapInterval );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Round( this Vector3 value, float snapInterval ) => Mathfs.Round( value, snapInterval );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Round( this Vector4 value, float snapInterval ) => Mathfs.Round( value, snapInterval );
-
- ///
- [MethodImpl( INLINE )] public static int RoundToInt( this float value ) => Mathfs.RoundToInt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector2Int RoundToInt( this Vector2 value ) => Mathfs.RoundToInt( value );
-
- ///
- [MethodImpl( INLINE )] public static Vector3Int RoundToInt( this Vector3 value ) => Mathfs.RoundToInt( value );
-
- #endregion
-
- #region Range Repeating
-
- ///
- [MethodImpl( INLINE )] public static float Frac( this float x ) => Mathfs.Frac( x );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Frac( this Vector2 v ) => Mathfs.Frac( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Frac( this Vector3 v ) => Mathfs.Frac( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Frac( this Vector4 v ) => Mathfs.Frac( v );
-
- ///
- [MethodImpl( INLINE )] public static float Repeat( this float value, float length ) => Mathfs.Repeat( value, length );
-
- ///
- [MethodImpl( INLINE )] public static int Mod( this int value, int length ) => Mathfs.Mod( value, length );
-
- #endregion
-
- #region Smoothing & Easing Curves
-
- ///
- [MethodImpl( INLINE )] public static float Smooth01( this float x ) => Mathfs.Smooth01( x );
-
- ///
- [MethodImpl( INLINE )] public static float Smoother01( this float x ) => Mathfs.Smoother01( x );
-
- ///
- [MethodImpl( INLINE )] public static float SmoothCos01( this float x ) => Mathfs.SmoothCos01( x );
-
- #endregion
-
- #region Value & Vector interpolation
-
- ///
- [MethodImpl( INLINE )] public static float Remap( this float value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, value );
-
- ///
- [MethodImpl( INLINE )] public static float Remap( this int value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.Lerp( oMin, oMax, Mathfs.InverseLerp( iMin, iMax, value ) );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Remap( this Vector2 v, Vector2 iMin, Vector2 iMax, Vector2 oMin, Vector2 oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, v );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Remap( this Vector3 v, Vector3 iMin, Vector3 iMax, Vector3 oMin, Vector3 oMax ) => Mathfs.Remap( iMin, iMax, oMin, oMax, v );
-
- ///
- [MethodImpl( INLINE )] public static Vector4 Remap( this Vector4 v, Vector4 iMin, Vector4 iMax, Vector4 oMin, Vector4 oMax ) => Mathfs.Lerp( oMin, oMax, Mathfs.InverseLerp( iMin, iMax, v ) );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 Remap( this Vector2 iPos, Rect iRect, Rect oRect ) => Mathfs.Remap( iRect.min, iRect.max, oRect.min, oRect.max, iPos );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 Remap( this Vector3 iPos, Bounds iBounds, Bounds oBounds ) => Mathfs.Remap( iBounds.min, iBounds.max, oBounds.min, oBounds.max, iPos );
-
- ///
- [MethodImpl( INLINE )] public static float RemapClamped( this float value, float iMin, float iMax, float oMin, float oMax ) => Mathfs.Lerp( oMin, oMax, Mathfs.InverseLerpClamped( iMin, iMax, value ) );
-
- #endregion
-
- #region Vector Math
-
- ///
- [MethodImpl( INLINE )] public static (Vector2 dir, float magnitude ) GetDirAndMagnitude( this Vector2 v ) => Mathfs.GetDirAndMagnitude( v );
-
- ///
- [MethodImpl( INLINE )] public static (Vector3 dir, float magnitude ) GetDirAndMagnitude( this Vector3 v ) => Mathfs.GetDirAndMagnitude( v );
-
- ///
- [MethodImpl( INLINE )] public static Vector2 ClampMagnitude( this Vector2 v, float min, float max ) => Mathfs.ClampMagnitude( v, min, max );
-
- ///
- [MethodImpl( INLINE )] public static Vector3 ClampMagnitude( this Vector3 v, float min, float max ) => Mathfs.ClampMagnitude( v, min, max );
-
- #endregion
-
- #endregion
-
-
- }
-
-}
\ No newline at end of file
diff --git a/LICENSE.txt.meta b/LICENSE.txt.meta
new file mode 100644
index 0000000..be2250f
--- /dev/null
+++ b/LICENSE.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: e2a7f2371f43d4faca3cdab60b7679a7
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/README.md b/README.md
index 7196817..f848811 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,26 @@
# Mathfs
-Expanded Math Functionality for Unity
+Freya's expanded math functionality for Unity!
+- This is primarily a way for me to share the math functionality I write and use in my own personal projects
+- I will recklessly edit and adapt things without too much thought into backwards compatibility
+- Minimum Unity version is currently 2021.2 due to using newer C# version features. It may be possible to auto-downgrade through your IDE if necessary
+- Commits with version tags should be relatively stable. Other commits may not be
+
+## Installation instructions
+
+There are several ways to install this library into your project:
+
+- **Plain install**
+ - Clone or [download](https://github.com/FreyaHolmer/Mathfs/archive/refs/heads/master.zip) this repository and put it somewhere in the Assets folder of your Unity project
+- **Unity Package Manager (UPM)**:
+ - Add either of the the following lines to *Packages/manifest.json*:
+ - `"com.acegikmo.mathfs": "https://github.com/FreyaHolmer/Mathfs.git#0.1.0",` if you want to target a specific version (recommended)
+ - `"com.acegikmo.mathfs": "https://github.com/FreyaHolmer/Mathfs.git",` if you want to pull the latest commit (potentially unstable)
+ - More information about UPM and git [here](https://docs.unity3d.com/Manual/upm-git.html)
+- **[OpenUPM](https://openupm.com)**
+ - After installing [openupm-cli](https://github.com/openupm/openupm-cli), run the following command:
+ - `openupm add com.acegikmo.mathfs`
+
+After installation you will be able to access the library in scripts by including the namespace `using Freya`
## Features
- 2D Intersection tests between all combinations of:
@@ -13,7 +34,7 @@ Expanded Math Functionality for Unity
- Catmull-Rom
- B-Spline (Uniform Cubic & Generalized Non-Uniform)
- NURBS (Non-Unifrom Rational B-Spline)
- - Trajectory (Cubic)
+ - Trajectory (Cubic & Generalized)
- Trajectory math
- GetDisplacement (point in trajectory), given gravity, angle, speed & time
- GetLaunchSpeed, given gravity, angle & lateral distance
diff --git a/README.md.meta b/README.md.meta
new file mode 100644
index 0000000..d4e58cb
--- /dev/null
+++ b/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c12d18034743d4bd7b7992ffada185ec
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime.meta b/Runtime.meta
new file mode 100644
index 0000000..31d92cb
--- /dev/null
+++ b/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 19167c22abdb4380b3b9de2d78ac7819
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves.meta b/Runtime/Curves.meta
new file mode 100644
index 0000000..4dbd77f
--- /dev/null
+++ b/Runtime/Curves.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 06a46a22856424f5184e3ca3d254c609
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/Arc2D.cs b/Runtime/Curves/Arc2D.cs
new file mode 100644
index 0000000..e188c73
--- /dev/null
+++ b/Runtime/Curves/Arc2D.cs
@@ -0,0 +1,94 @@
+using System;
+using UnityEngine;
+
+namespace Freya {
+
+ /// a 2D arc with support for straight lines
+ [Serializable]
+ public struct Arc2D {
+
+ /// The starting point of the arc
+ public Transform2D placement;
+ /// The signed curvature of the arc, equal to 1/radius (0 = straight line, 1 = turning left, -1 = turning right)
+ public float curvature;
+ /// The length of the arc
+ public float length;
+
+ /// The radius of the circle traced by the arc. Returns infinity if this segment is linear, ie: if curvature is 0
+ public float Radius => 1f / MathF.Abs( curvature );
+ /// The center of the circle traced by the arc. Returns infinity if this segment is linear, ie: if curvature is 0
+ public Vector2 CircleCenter => StartNormal / curvature;
+ /// The normal direction at the start of the arc
+ public Vector2 StartNormal => placement.AxisY;
+ /// The tangent direction at the start of the arc
+ public Vector2 StartTangent => placement.AxisX;
+ /// The normal direction at the end of the arc
+ public Vector2 EndNormal => GetNormal( length );
+ /// The end point of the arc
+ public Vector2 EndPoint => GetPosition( length );
+ /// The signed angular span covered across the arc. This returns 0 if this segment is linear, ie: if curvature is 0
+ public float AngularSpan => length * curvature; // s = ra ▶ s = a/k ▶ sk = a
+ /// Whether or not this is a straight line rather than an arc, ie: if curvature is 0
+ public bool IsStraight => Mathfs.Approximately( curvature, 0 );
+
+ /// Evaluates the position of this arc at the given arc length s
+ public Vector2 GetPosition( float s ) => Eval( s, nThDerivative: 0 );
+
+ /// Evaluates the tangent direction of this arc at the given arc length s
+ public Vector2 GetTangent( float s ) => s == 0 ? StartTangent : Eval( s, nThDerivative: 1 ); // no need to normalize, it's already arc-length parameterized
+
+ /// Evaluates the normal direction of this arc at the given arc length s
+ public Vector2 GetNormal( float s ) => Eval( s, nThDerivative: 1 ).Rotate90CCW(); // no need to normalize, it's already arc-length parameterized
+
+ /// Evaluates the given derivative of this arc, by arc length s
+ public Vector2 Eval( float s, int nThDerivative = 0 ) {
+ float ang = s * curvature;
+ float x, y;
+
+ switch( nThDerivative ) {
+ case 0:
+ x = s * Mathfs.Sinc( ang );
+ y = s * Mathfs.Cosinc( ang );
+ return placement.TransformPoint( x, y );
+ case 1:
+ x = MathF.Cos( ang );
+ y = MathF.Sin( ang );
+ break;
+ case 2:
+ x = -curvature * MathF.Sin( ang );
+ y = +curvature * MathF.Cos( ang );
+ break;
+ case 3:
+ float k2 = curvature * curvature;
+ x = -k2 * MathF.Cos( ang );
+ y = -k2 * MathF.Sin( ang );
+ break;
+ case 4:
+ float k3 = curvature * curvature * curvature;
+ x = +k3 * MathF.Sin( ang );
+ y = -k3 * MathF.Cos( ang );
+ break;
+ case 5:
+ float _k2 = curvature * curvature;
+ float k4 = _k2 * _k2;
+ x = k4 * MathF.Cos( ang );
+ y = k4 * MathF.Sin( ang );
+ break;
+ default:
+ // general form for n > 0
+ float scale = MathF.Pow( curvature, nThDerivative - 1 );
+ int xSgn = nThDerivative / 2 % 2 == 0 ? 1 : -1;
+ int ySgn = ( nThDerivative - 1 ) / 2 % 2 == 0 ? 1 : -1;
+ bool even = nThDerivative % 2 == 0;
+ x = xSgn * scale * ( even ? MathF.Sin( ang ) : MathF.Cos( ang ) );
+ y = ySgn * scale * ( even ? MathF.Cos( ang ) : MathF.Sin( ang ) );
+ break;
+ }
+
+ // space transformation
+ return placement.TransformVector( x, y );
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/Arc2D.cs.meta b/Runtime/Curves/Arc2D.cs.meta
new file mode 100644
index 0000000..29658a3
--- /dev/null
+++ b/Runtime/Curves/Arc2D.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 723d5270bbbe92b47b5152cbbc8928cb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/Catenary.cs b/Runtime/Curves/Catenary.cs
new file mode 100644
index 0000000..92a8136
--- /dev/null
+++ b/Runtime/Curves/Catenary.cs
@@ -0,0 +1,63 @@
+using System;
+using UnityEngine;
+
+namespace Freya {
+
+ /// Catenary math utility functions
+ public static class Catenary {
+
+ /// Returns the y coordinate of a catenary at the given x value
+ /// The x coordinate to evaluate at
+ /// The a-parameter of the catenary
+ public static float Eval( float x, float a ) => a * Mathfs.Cosh( x / a );
+
+ /// Evaluates the arc length from the apex of the catenary, to the given x coordinate.
+ /// Note that this is negative when x is less than 0
+ /// The x coordinate to get the length to
+ /// The a-parameter of the catenary
+ public static float EvalArcLen( float x, float a ) => a * Mathfs.Sinh( x / a );
+
+ /// Evaluates the x coordinate at the given arc length relative to the apex of the catenary.
+ /// Note that the input arc length can be negative, to get the negative x coordinates
+ /// The arc length to get the x coordinate of
+ /// The a-parameter of the catenary
+ public static float EvalXByArcLength( float s, float a ) => a * Mathfs.Asinh( s / a );
+
+ /// Evaluates the n:th 2D derivative at the given arc length relative to the apex of the catenary.
+ /// Note that the input arc length can be negative, to get the tangents on the negative x side
+ /// The arc length coordinate to get the tangent of
+ /// The a-parameter of the catenary
+ public static Vector2 EvalDerivByArcLength( float s, float a, int n = 1 ) {
+ if( n == 0 ) { // position
+ float x = EvalXByArcLength( s, a );
+ float y = Eval( x, a );
+ return new Vector2( x, y );
+ }
+ float xNum = default;
+ float yNum = default;
+ float aSq = a * a;
+ float sSq = s * s;
+
+ if( n == 1 ) { // velocity
+ xNum = a;
+ yNum = s;
+ } else if( n == 2 ) { // acceleration
+ xNum = -a * s;
+ yNum = aSq;
+ } else if( n == 3 ) { // jerk/jolt
+ xNum = a * ( -aSq + 2 * sSq );
+ yNum = 3 * aSq * s;
+ } else if( n == 4 ) { // 4th derivative
+ xNum = 3 * s * a * ( -3 * aSq + 2 * sSq );
+ yNum = 3 * aSq * ( -aSq + 4 * sSq );
+ } else {
+ throw new NotImplementedException( $"Derivative ({n}) of Catenaries are not implemented" );
+ }
+
+ float den = MathF.Pow( aSq + sSq, ( n * 2 - 1 ) / 2f );
+ return new Vector2( xNum / den, yNum / den );
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/Catenary.cs.meta b/Runtime/Curves/Catenary.cs.meta
new file mode 100644
index 0000000..053d6e7
--- /dev/null
+++ b/Runtime/Curves/Catenary.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b30c699ff7f0afe4aa0d122f4a3e313f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/Catenary2D.cs b/Runtime/Curves/Catenary2D.cs
new file mode 100644
index 0000000..a269e4b
--- /dev/null
+++ b/Runtime/Curves/Catenary2D.cs
@@ -0,0 +1,74 @@
+// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
+
+using UnityEngine;
+
+namespace Freya {
+
+ /// A catenary curve passing through two points with a given an arc length
+ public struct Catenary2D {
+
+ enum Evaluability {
+ NotReady,
+ Ready
+ }
+
+ // data
+ Vector2 p1;
+ CatenaryToPoint catenary; // stores arc length
+ Transform2D space; // stores p0 and slack direction
+ Evaluability evaluability;
+
+ public float Length {
+ get => catenary.Length;
+ set => catenary.Length = value; // does not change evaluability of this type, since space hasn't changed
+ }
+ public Vector2 P0 {
+ get => space.Origin;
+ set {
+ if( value != space.Origin )
+ ( space.Origin, evaluability ) = ( value, Evaluability.NotReady );
+ }
+ }
+ public Vector2 P1 {
+ get => p1;
+ set {
+ if( value != p1 )
+ ( p1, evaluability ) = ( value, Evaluability.NotReady );
+ }
+ }
+ public Vector2 SlackDirection {
+ get => -space.AxisY;
+ set {
+ if( value != SlackDirection )
+ ( space.AxisY, evaluability ) = ( -value, Evaluability.NotReady );
+ }
+ }
+
+ ///
+ public Catenary2D( Vector2 p0, Vector2 p1, float length, Vector2 slackDirection ) {
+ space = default;
+ catenary = new CatenaryToPoint( p1 - p0, length );
+ ( space.Origin, this.p1 ) = ( p0, p1 );
+ evaluability = Evaluability.NotReady;
+ }
+
+ ///
+ public Vector3 Eval( float sEval, int n = 1 ) {
+ ReadyForEvaluation();
+ return n switch {
+ 0 => space.TransformPoint( catenary.Eval( sEval, 0 ) ),
+ _ => space.TransformVector( catenary.Eval( sEval, n ) )
+ };
+ }
+
+ // ensures the space transformation is ready
+ void ReadyForEvaluation() {
+ if( evaluability == Evaluability.Ready )
+ return;
+ catenary.P = space.InverseTransformPoint( p1 );
+ evaluability = Evaluability.Ready;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/Catenary2D.cs.meta b/Runtime/Curves/Catenary2D.cs.meta
new file mode 100644
index 0000000..4e495e2
--- /dev/null
+++ b/Runtime/Curves/Catenary2D.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 04762b34aa4b834489c20d76358b2d7e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/Catenary3D.cs b/Runtime/Curves/Catenary3D.cs
new file mode 100644
index 0000000..9ea9d45
--- /dev/null
+++ b/Runtime/Curves/Catenary3D.cs
@@ -0,0 +1,80 @@
+// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
+
+using UnityEngine;
+
+namespace Freya {
+
+ /// A catenary curve passing through two points with a given an arc length
+ public struct Catenary3D {
+
+ enum Evaluability {
+ NotReady,
+ Ready
+ }
+
+ // data
+ Vector3 p1;
+ CatenaryToPoint cat2D; // also stores arc length
+ Plane2DIn3D space; // stores p0 and slack direction
+ Evaluability evaluability;
+
+ public float Length {
+ get => cat2D.Length;
+ set => cat2D.Length = value; // does not change evaluability of this type, since space hasn't changed
+ }
+ public Vector3 P0 {
+ get => space.origin;
+ set {
+ if( value != space.origin )
+ ( space.origin, evaluability ) = ( value, Evaluability.NotReady );
+ }
+ }
+ public Vector3 P1 {
+ get => p1;
+ set {
+ if( value != p1 )
+ ( p1, evaluability ) = ( value, Evaluability.NotReady );
+ }
+ }
+ public Vector3 SlackDirection {
+ get => -space.axisY;
+ set {
+ if( value != SlackDirection )
+ ( space.axisY, evaluability ) = ( -value, Evaluability.NotReady );
+ }
+ }
+
+ /// Creates a catenary curve between two points, given an arc length s and a slack direction
+ /// The start of the curve
+ /// The end of the curve
+ /// The length of the curve. note: has to be equal or longer than the distance between the points
+ /// The direction of "gravity" for the arc
+ public Catenary3D( Vector3 p0, Vector3 p1, float length, Vector3 slackDirection ) {
+ cat2D = new CatenaryToPoint( p1 - p0, length );
+ space.axisX = default; // set on first evaluation by RotateAroundYToInclude
+ ( space.origin, space.axisY, this.p1 ) = ( p0, -slackDirection, p1 );
+ evaluability = Evaluability.NotReady;
+ }
+
+ ///
+ public Vector3 Eval( float sEval, int n = 1 ) {
+ ReadyForEvaluation();
+ return n switch {
+ 0 => space.TransformPoint( cat2D.Eval( sEval, 0 ) ),
+ _ => space.TransformVector( cat2D.Eval( sEval, n ) )
+ };
+ }
+
+ // ensures the space transformation is ready
+ void ReadyForEvaluation() {
+ if( evaluability == Evaluability.Ready )
+ return;
+ // ready the embedded plane of the catenary and assign the 2D endpoint
+ space.RotateAroundYToInclude( P1, out Vector2 p1Local );
+ cat2D.P = p1Local;
+ evaluability = Evaluability.Ready;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/Catenary3D.cs.meta b/Runtime/Curves/Catenary3D.cs.meta
new file mode 100644
index 0000000..8fad120
--- /dev/null
+++ b/Runtime/Curves/Catenary3D.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1b20ae836093c6c4891a5bd00c155a6a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/CatenaryToPoint.cs b/Runtime/Curves/CatenaryToPoint.cs
new file mode 100644
index 0000000..212f897
--- /dev/null
+++ b/Runtime/Curves/CatenaryToPoint.cs
@@ -0,0 +1,215 @@
+using System;
+using UnityEngine;
+
+namespace Freya {
+
+ /// A catenary curve from the origin to a point P
+ public struct CatenaryToPoint {
+
+ enum Evaluability {
+ Unknown = 0,
+ Catenary,
+ LinearVertical,
+ LineSegment
+ }
+
+ const int INTERVAL_SEARCH_ITERATIONS = 12;
+ const int BISECT_REFINE_COUNT = 14;
+
+ // data
+ Vector2 p;
+ float s;
+
+ // cached state
+ Evaluability evaluability;
+ float a;
+ Vector2 delta;
+ float arcLenSampleOffset;
+
+ public CatenaryToPoint( Vector2 p, float s ) {
+ ( this.p, this.s ) = ( p, s );
+ a = default;
+ delta = default;
+ arcLenSampleOffset = default;
+ evaluability = Evaluability.Unknown;
+ }
+
+ public Vector2 P {
+ get => p;
+ set {
+ if( value != p )
+ ( p, evaluability ) = ( value, Evaluability.Unknown );
+ }
+ }
+
+ public float Length {
+ get => s;
+ set {
+ if( value != s )
+ ( s, evaluability ) = ( value, Evaluability.Unknown );
+ }
+ }
+
+ public bool IsVertical => MathF.Abs( p.x ) < 0.001f;
+ public bool IsStraightLine => s <= p.magnitude * 1.00005f;
+
+ /// Evaluates a position on this catenary curve at the given arc length of sEval
+ /// The arc length along the curve to sample, relative to the first point
+ /// The derivative to sample. 1 = first derivative, 2 = second derivative
+ public Vector2 Eval( float sEval, int nthDerivative = 0 ) {
+ ReadyForEvaluation();
+ return nthDerivative switch {
+ 0 => evaluability switch {
+ Evaluability.Catenary => EvalCatPosByArcLength( sEval ),
+ Evaluability.LineSegment => EvalStraightLineByArcLength( sEval ),
+ Evaluability.LinearVertical => EvalVerticalLinearApproxByArcLength( sEval ),
+ Evaluability.Unknown or _ => throw new Exception( "Failed to evaluate catenary, couldn't calculate evaluability" )
+ },
+ _ => evaluability switch {
+ Evaluability.Catenary => EvalCatDerivByArcLength( sEval, nthDerivative ),
+ Evaluability.LineSegment => nthDerivative == 1 ? p.normalized : Vector2.zero,
+ Evaluability.LinearVertical => new Vector2( 0, nthDerivative == 1 ? ( sEval < -( p.y - s ) / 2 ? -1 : 1 ) : 0 ),
+ Evaluability.Unknown or _ => throw new Exception( "Failed to evaluate catenary, couldn't calculate evaluability" )
+ }
+ };
+ }
+
+ // straight line from p0 to p1
+ Vector2 EvalStraightLineByArcLength( float sEval ) => p * ( sEval / s );
+
+ // almost completely vertical line when p0.x is approx. equal to p1.x
+ Vector2 EvalVerticalLinearApproxByArcLength( float sEval ) {
+ float x = Mathfs.Lerp( 0, p.x, sEval / s ); // just to make it not snap to x=0
+ float b = ( p.y - s ) / 2; // bottom
+ float seg0 = -b;
+ float y = ( sEval < seg0 ) ? -sEval : -2 * seg0 + sEval;
+ return new Vector2( x, y );
+ }
+
+ // evaluates the position of the catenary at the given arc length, relative to the first point
+ Vector2 EvalCatPosByArcLength( float sEval ) {
+ sEval *= p.x.Sign(); // since we go backwards when p0.x < p1.x
+ float x = Catenary.EvalXByArcLength( sEval + arcLenSampleOffset, a ) + delta.x;
+ float y = EvalPassingThrough0( x );
+ return new Vector2( x, y );
+ }
+
+ /// Evaluates the n-th derivative of the catenary at the given arc length
+ /// The arc length, relative to the first point
+ /// The derivative to evaluate
+ Vector2 EvalCatDerivByArcLength( float sEval, int n = 1 ) {
+ if( n == 0 )
+ return EvalCatPosByArcLength( sEval );
+ sEval *= p.x.Sign(); // since we go backwards when p0.x < p1.x
+ return Catenary.EvalDerivByArcLength( sEval + arcLenSampleOffset, a, n );
+ }
+
+ // Evaluate passing through the origin and p
+ float EvalPassingThrough0( float x ) => Catenary.Eval( x - delta.x, a ) + delta.y;
+
+ // calculates p, a, delta, arcLenSampleOffset, and which evaluation method to use
+ void ReadyForEvaluation() {
+ if( evaluability != Evaluability.Unknown )
+ return;
+
+ // CASE 1:
+ // first, test if it's a line segment
+ if( IsStraightLine ) {
+ evaluability = Evaluability.LineSegment;
+ return;
+ }
+
+ // CASE 2:
+ // check if it's basically a fully vertical hanging chain
+ if( IsVertical ) {
+ evaluability = Evaluability.LinearVertical;
+ return;
+ }
+
+ // CASE 3:
+ // Now we've got a catenary on our hands unless something explodes.
+ float c = MathF.Sqrt( s * s - p.y * p.y );
+ float pAbsX = p.x.Abs(); // solve only in x > 0
+
+ // find bounds of the root
+ float xRoot = ( p.x * p.x ) / ( 2 * s ); // intial guess based on freya's flawless heuristics
+ if( TryFindRootBounds( pAbsX, c, xRoot, out FloatRange xRange ) ) {
+ // refine range, if necessary (which is very likely)
+ if( Mathfs.Approximately( xRange.Length, 0 ) == false )
+ RootFindBisections( pAbsX, c, ref xRange, BISECT_REFINE_COUNT ); // Catenary seems valid, with roots inside, refine the range
+ a = xRange.Center; // set a to the middle of the latest range
+ delta = CalcCatenaryDelta( a, p ); // find delta to pass through both points
+ arcLenSampleOffset = CalcArcLenSampleOffset( delta.x, a );
+ evaluability = Evaluability.Catenary;
+ } else {
+ // CASE 4:
+ // something exploded, couldn't find a range, so let's use a straight line as a fallback
+ evaluability = Evaluability.LineSegment;
+ }
+ }
+
+ // root solve function
+ static float R( float a, float pAbsX, float c ) => 2 * a * Mathfs.Sinh( pAbsX / ( 2 * a ) ) - c;
+
+ // Calculates the arc length offset so that it's relative to the start of the chain when evaluating by arc length
+ static float CalcArcLenSampleOffset( float deltaX, float a ) => Catenary.EvalArcLen( -deltaX, a );
+
+ // Calculates the required offset to make a catenary pass through the origin and a point p
+ static Vector2 CalcCatenaryDelta( float a, Vector2 p ) {
+ Vector2 d;
+ d.x = p.x / 2 - a * Mathfs.Asinh( p.y / ( 2 * a * Mathfs.Sinh( p.x / ( 2 * a ) ) ) );
+ d.y = -Catenary.Eval( d.x, a ); // technically -d.x but because of symmetry d.x works too
+ return d;
+ }
+
+ // presumes a decreasing function with one root in x > 0
+ // g = initial guess
+ static bool TryFindRootBounds( float pAbsX, float c, float g, out FloatRange xRange ) {
+ float y = R( g, pAbsX, c );
+ xRange = new FloatRange( g, g );
+ if( Mathfs.Approximately( y, 0 ) ) // somehow landed *on* our root in our initial guess
+ return true;
+
+ bool findingUpper = y > 0;
+
+ for( int n = 1; n <= INTERVAL_SEARCH_ITERATIONS; n++ ) {
+ if( findingUpper ) {
+ // It's positive - we found our lower bound
+ // exponentially search for upper bound
+ xRange.a = xRange.b;
+ xRange.b = g * MathF.Pow( 2, n );
+ y = R( xRange.b, pAbsX, c );
+ if( y < 0 )
+ return true; // upper bound found!
+ } else {
+ // It's negative - we found our upper bound
+ // exponentially search for lower bound
+ xRange.b = xRange.a;
+ xRange.a = g * MathF.Pow( 2, -n );
+ y = R( xRange.a, pAbsX, c );
+ if( y > 0 )
+ return true; // lower bound found!
+ }
+ }
+
+ return false; // no root found
+ }
+
+ static void RootFindBisections( float pAbsX, float c, ref FloatRange xRange, int iterationCount ) {
+ for( int i = 0; i < iterationCount; i++ )
+ RootFindBisection( pAbsX, c, ref xRange );
+ }
+
+ static void RootFindBisection( float pAbsX, float c, ref FloatRange xRange ) {
+ float xInter = xRange.Center; // bisection
+ float yInter = R( xInter, pAbsX, c );
+ if( yInter > 0 )
+ xRange.a = xInter; // adjust left bound
+ else
+ xRange.b = xInter; // adjust right bound
+ }
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/CatenaryToPoint.cs.meta b/Runtime/Curves/CatenaryToPoint.cs.meta
new file mode 100644
index 0000000..b7d8d6d
--- /dev/null
+++ b/Runtime/Curves/CatenaryToPoint.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 725fcdb7e0f11c54eae09804333f8693
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/GenericTrajectory2D.cs b/Runtime/Curves/GenericTrajectory2D.cs
new file mode 100644
index 0000000..3a9e42f
--- /dev/null
+++ b/Runtime/Curves/GenericTrajectory2D.cs
@@ -0,0 +1,24 @@
+// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
+
+using System;
+using Freya;
+using UnityEngine;
+
+public class GenericTrajectory2D {
+
+ public Vector2[] derivatives;
+
+ public GenericTrajectory2D( params Vector2[] derivatives ) => this.derivatives = derivatives;
+
+ public Vector2 GetPosition( float time ) {
+ Vector2 pt = derivatives[0];
+ for( int i = 1; i < derivatives.Length; i++ ) {
+ float scale = MathF.Pow( time, i ) / Mathfs.Factorial( (uint)i );
+ pt += scale * derivatives[i];
+ }
+
+ return pt;
+ }
+
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/GenericTrajectory2D.cs.meta b/Runtime/Curves/GenericTrajectory2D.cs.meta
new file mode 100644
index 0000000..236cec4
--- /dev/null
+++ b/Runtime/Curves/GenericTrajectory2D.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 39f2a44aef2c7834da92d8e743f7d335
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/IParamCurve.cs b/Runtime/Curves/IParamCurve.cs
similarity index 61%
rename from Curves/IParamCurve.cs
rename to Runtime/Curves/IParamCurve.cs
index 1e077b9..320d5c7 100644
--- a/Curves/IParamCurve.cs
+++ b/Runtime/Curves/IParamCurve.cs
@@ -1,9 +1,21 @@
-using System.Runtime.CompilerServices;
+// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
+
+using System;
+using System.Runtime.CompilerServices;
using UnityEngine;
using static Freya.Mathfs;
namespace Freya {
+ public interface IParamSplineSegment {
+ /// The curve generated by the control points
+ P Curve { get; }
+
+ /// The matrix containing the control points of this spline segment
+ M PointMatrix { get; set; }
+
+ }
+
/// An interface representing a parametric curve
/// The vector type of the curve
public interface IParamCurve where V : struct {
@@ -11,18 +23,9 @@ public interface IParamCurve where V : struct {
/// Returns the degree of this curve. Quadratic = 2, Cubic = 3, etc
int Degree { get; }
- /// The number of control points in this curve
- int Count { get; }
-
/// Returns the point at the given t-value on the curve
/// The t-value along the curve to sample
- V GetPoint( float t );
-
- /// Returns the starting point of this curve, where t = 0
- V GetStartPoint();
-
- /// Returns the end point of this curve, where t = 1
- V GetEndPoint();
+ V Eval( float t );
}
@@ -31,7 +34,7 @@ public interface IParamCurve where V : struct {
public interface IParamCurve1Diff : IParamCurve where V : struct {
/// Returns the derivative at the given t-value on the curve. Loosely analogous to "velocity" of the point along the curve
/// The t-value along the curve to sample
- V GetDerivative( float t );
+ V EvalDerivative( float t );
}
/// An interface representing a parametric curve of degree 2 or higher
@@ -39,14 +42,14 @@ public interface IParamCurve1Diff : IParamCurve where V : struct {
public interface IParamCurve2Diff : IParamCurve1Diff where V : struct {
/// Returns the second derivative at the given t-value on the curve. Loosely analogous to "acceleration" of the point along the curve
/// The t-value along the curve to sample
- V GetSecondDerivative( float t );
+ V EvalSecondDerivative( float t );
}
/// An interface representing a parametric curve of degree 3 or higher
/// The vector type of the curve
public interface IParamCurve3Diff : IParamCurve2Diff where V : struct {
/// Returns the third derivative of the curve. Loosely analogous to "jerk/jolt" (rate of change of acceleration) of the point along the curve
- V GetThirdDerivative( float t );
+ V EvalThirdDerivative( float t );
}
/// Shared functionality for all 2D parametric curves
@@ -54,26 +57,29 @@ public static class IParamCurveExt2D {
const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;
- /// Returns the approximate length of the curve
+ /// Returns the approximate length of the curve in the 0 to 1 interval
/// The number of subdivisions to approximate the length with. Higher values are more accurate, but more expensive to calculate
- public static float GetLength( this T curve, int accuracy = 8 ) where T : IParamCurve {
- if( accuracy <= 2 )
- return ( curve.GetStartPoint() - curve.GetEndPoint() ).magnitude;
+ [MethodImpl( INLINE )] public static float GetArcLength( this T curve, int accuracy = 8 ) where T : IParamCurve => curve.GetArcLength( FloatRange.unit, accuracy );
+ /// Returns the approximate length of the curve in the given interval
+ /// The parameter interval of the curve to get the length of
+ /// The number of subdivisions to approximate the length with. Higher values are more accurate, but more expensive to calculate
+ public static float GetArcLength( this T curve, FloatRange interval, int accuracy = 8 ) where T : IParamCurve {
+ accuracy = accuracy.AtLeast( 2 );
+ bool unit = interval == FloatRange.unit;
float totalDist = 0;
- Vector2 prev = curve.GetStartPoint();
+ Vector2 prev = curve.Eval( interval.a );
for( int i = 1; i < accuracy; i++ ) {
float t = i / ( accuracy - 1f );
- Vector2 p = curve.GetPoint( t );
+ Vector2 p = curve.Eval( unit ? t : interval.Lerp( t ) );
float dx = p.x - prev.x;
float dy = p.y - prev.y;
- totalDist += Mathf.Sqrt( dx * dx + dy * dy );
+ totalDist += MathF.Sqrt( dx * dx + dy * dy );
prev = p;
}
return totalDist;
}
-
}
/// Shared functionality for all 3D parametric curves
@@ -81,26 +87,27 @@ public static class IParamCurveExt3D {
const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;
- /// Returns the approximate length of the curve
- /// The number of subdivisions to approximate the length with. Higher values are more accurate, but more expensive to calculate
- public static float GetLength( this T curve, int accuracy = 8 ) where T : IParamCurve {
- if( accuracy <= 2 )
- return ( curve.GetStartPoint() - curve.GetEndPoint() ).magnitude;
+ ///
+ [MethodImpl( INLINE )] public static float GetArcLength( this T curve, int accuracy = 8 ) where T : IParamCurve => curve.GetArcLength( FloatRange.unit, accuracy );
+ ///
+ public static float GetArcLength( this T curve, FloatRange interval, int accuracy = 8 ) where T : IParamCurve {
+ accuracy = accuracy.AtLeast( 2 );
+ bool unit = interval == FloatRange.unit;
float totalDist = 0;
- Vector3 prev = curve.GetStartPoint();
+ Vector3 prev = curve.Eval( interval.a );
for( int i = 1; i < accuracy; i++ ) {
float t = i / ( accuracy - 1f );
- Vector3 p = curve.GetPoint( t );
+ Vector3 p = curve.Eval( unit ? t : interval.Lerp( t ) );
float dx = p.x - prev.x;
float dy = p.y - prev.y;
- totalDist += Mathf.Sqrt( dx * dx + dy * dy );
+ float dz = p.z - prev.z;
+ totalDist += MathF.Sqrt( dx * dx + dy * dy + dz * dz );
prev = p;
}
return totalDist;
}
-
}
/// Shared functionality for 2D parametric curves of degree 1 or higher
@@ -110,28 +117,28 @@ public static class IParamCurve1DiffExt2D {
/// Returns the normalized tangent direction at the given t-value on the curve
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Vector2 GetTangent( this T curve, float t ) where T : IParamCurve1Diff => curve.GetDerivative( t ).normalized;
+ [MethodImpl( INLINE )] public static Vector2 EvalTangent( this T curve, float t ) where T : IParamCurve1Diff => curve.EvalDerivative( t ).normalized;
/// Returns the normal direction at the given t-value on the curve.
/// This normal will point to the inner arc of the current curvature
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Vector2 GetNormal( this T curve, float t ) where T : IParamCurve1Diff => curve.GetTangent( t ).Rotate90CCW();
+ [MethodImpl( INLINE )] public static Vector2 EvalNormal( this T curve, float t ) where T : IParamCurve1Diff => curve.EvalTangent( t ).Rotate90CCW();
/// Returns the 2D angle of the direction of the curve at the given point, in radians
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static float GetAngle( this T curve, float t ) where T : IParamCurve1Diff => DirToAng( curve.GetDerivative( t ) );
+ [MethodImpl( INLINE )] public static float EvalAngle( this T curve, float t ) where T : IParamCurve1Diff => DirToAng( curve.EvalDerivative( t ) );
/// Returns the orientation at the given point t, where the X axis is tangent to the curve
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Quaternion GetOrientation( this T curve, float t ) where T : IParamCurve1Diff => DirToOrientation( curve.GetDerivative( t ) );
+ [MethodImpl( INLINE )] public static Quaternion EvalOrientation( this T curve, float t ) where T : IParamCurve1Diff => DirToOrientation( curve.EvalDerivative( t ) );
/// Returns the position and orientation at the given t-value on the curve
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Pose GetPose( this T curve, float t ) where T : IParamCurve1Diff => PointDirToPose( curve.GetPoint( t ), curve.GetTangent( t ) );
+ [MethodImpl( INLINE )] public static Pose EvalPose( this T curve, float t ) where T : IParamCurve1Diff => PointDirToPose( curve.Eval( t ), curve.EvalTangent( t ) );
/// Returns the position and orientation at the given t-value on the curve, expressed as a matrix
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Matrix4x4 GetMatrix( this T curve, float t ) where T : IParamCurve1Diff => GetMatrixFrom2DPointDir( curve.GetPoint( t ), curve.GetTangent( t ) );
+ [MethodImpl( INLINE )] public static Matrix4x4 EvalMatrix( this T curve, float t ) where T : IParamCurve1Diff => GetMatrixFrom2DPointDir( curve.Eval( t ), curve.EvalTangent( t ) );
}
@@ -139,40 +146,40 @@ public static class IParamCurve1DiffExt2D {
public static class IParamCurve1DiffExt3D {
const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;
- ///
- [MethodImpl( INLINE )] public static Vector3 GetTangent( this T curve, float t ) where T : IParamCurve1Diff => curve.GetDerivative( t ).normalized;
+ ///
+ [MethodImpl( INLINE )] public static Vector3 EvalTangent( this T curve, float t ) where T : IParamCurve1Diff => curve.EvalDerivative( t ).normalized;
/// Returns a normal of the curve given a reference up vector and t-value on the curve.
/// The normal will be perpendicular to both the supplied up vector and the curve
/// The t-value along the curve to sample
/// The reference up vector. The normal will be perpendicular to both the supplied up vector and the curve
- [MethodImpl( INLINE )] public static Vector3 GetNormal( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => GetNormalFromLookTangent( curve.GetDerivative( t ), up );
+ [MethodImpl( INLINE )] public static Vector3 EvalNormal( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => GetNormalFromLookTangent( curve.EvalDerivative( t ), up );
/// Returns the binormal of the curve given a reference up vector and t-value on the curve.
/// The binormal will attempt to be as aligned with the reference vector as possible,
/// while still being perpendicular to the curve
/// The t-value along the curve to sample
/// The reference up vector. The binormal will attempt to be as aligned with the reference vector as possible, while still being perpendicular to the curve
- [MethodImpl( INLINE )] public static Vector3 GetBinormal( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => GetBinormalFromLookTangent( curve.GetDerivative( t ), up );
+ [MethodImpl( INLINE )] public static Vector3 EvalBinormal( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => GetBinormalFromLookTangent( curve.EvalDerivative( t ), up );
/// Returns the orientation at the given point t, where the Z direction is tangent to the curve.
/// The Y axis will attempt to align with the supplied up vector
/// The t-value along the curve to sample
/// The reference up vector. The Y axis will attempt to be as aligned with this vector as much as possible
- [MethodImpl( INLINE )] public static Quaternion GetOrientation( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => Quaternion.LookRotation( curve.GetDerivative( t ), up );
+ [MethodImpl( INLINE )] public static Quaternion EvalOrientation( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => Quaternion.LookRotation( curve.EvalDerivative( t ), up );
/// Returns the position and orientation of curve at the given point t, where the Z direction is tangent to the curve.
/// The Y axis will attempt to align with the supplied up vector
/// The t-value along the curve to sample
/// The reference up vector. The Y axis will attempt to be as aligned with this vector as much as possible
- [MethodImpl( INLINE )] public static Pose GetPose( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => new Pose( curve.GetPoint( t ), Quaternion.LookRotation( curve.GetDerivative( t ), up ) );
+ [MethodImpl( INLINE )] public static Pose EvalPose( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff => new Pose( curve.Eval( t ), Quaternion.LookRotation( curve.EvalDerivative( t ), up ) );
/// Returns the position and orientation of curve at the given point t, expressed as a matrix, where the Z direction is tangent to the curve.
/// The Y axis will attempt to align with the supplied up vector
/// The t-value along the curve to sample
/// The reference up vector. The Y axis will attempt to be as aligned with this vector as much as possible
- public static Matrix4x4 GetMatrix( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff {
- ( Vector3 Pt, Vector3 Tn ) = ( curve.GetPoint( t ), curve.GetTangent( t ) );
+ public static Matrix4x4 EvalMatrix( this T curve, float t, Vector3 up ) where T : IParamCurve1Diff {
+ ( Vector3 Pt, Vector3 Tn ) = ( curve.Eval( t ), curve.EvalTangent( t ) );
Vector3 Nm = Vector3.Cross( up, Tn ).normalized; // X axis
Vector3 Bn = Vector3.Cross( Tn, Nm ); // Y axis
return new Matrix4x4(
@@ -191,11 +198,11 @@ public static class IParamCurve2DiffExt2D {
/// Returns the signed curvature at the given t-value on the curve, in radians per distance unit (equivalent to the reciprocal radius of the osculating circle)
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static float GetCurvature( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetCurvature( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) );
+ [MethodImpl( INLINE )] public static float EvalCurvature( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetCurvature( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) );
/// Returns the osculating circle at the given t-value in the curve, if possible. Osculating circles are defined everywhere except on inflection points, where curvature is 0
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Circle2D GetOsculatingCircle( this T curve, float t ) where T : IParamCurve2Diff => Circle2D.GetOsculatingCircle( curve.GetPoint( t ), curve.GetDerivative( t ), curve.GetSecondDerivative( t ) );
+ [MethodImpl( INLINE )] public static Circle2D EvalOsculatingCircle( this T curve, float t ) where T : IParamCurve2Diff => Circle2D.GetOsculatingCircle( curve.Eval( t ), curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) );
}
@@ -205,29 +212,29 @@ public static class IParamCurve2DiffExt3D {
/// Returns a pseudovector at the given t-value on the curve, where the magnitude is the curvature in radians per distance unit, and the direction is the axis of curvature
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Vector3 GetCurvature( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetCurvature( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) );
+ [MethodImpl( INLINE )] public static Bivector3 EvalCurvature( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetCurvature( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) );
- ///
- [MethodImpl( INLINE )] public static Circle3D GetOsculatingCircle( this T curve, float t ) where T : IParamCurve2Diff => Circle3D.GetOsculatingCircle( curve.GetPoint( t ), curve.GetDerivative( t ), curve.GetSecondDerivative( t ) );
+ ///
+ [MethodImpl( INLINE )] public static Circle3D EvalOsculatingCircle( this T curve, float t ) where T : IParamCurve2Diff => Circle3D.GetOsculatingCircle( curve.Eval( t ), curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) );
/// Returns the frenet-serret (curvature-based) normal direction at the given t-value on the curve
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Vector3 GetArcNormal( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcNormal( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) );
+ [MethodImpl( INLINE )] public static Vector3 EvalArcNormal( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcNormal( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) );
/// Returns the frenet-serret (curvature-based) binormal direction at the given t-value on the curve
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Vector3 GetArcBinormal( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcBinormal( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) );
+ [MethodImpl( INLINE )] public static Vector3 EvalArcBinormal( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcBinormal( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) );
/// Returns the frenet-serret (curvature-based) orientation of curve at the given point t, where the Z direction is tangent to the curve.
/// The X axis will point to the inner arc of the current curvature
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Quaternion GetArcOrientation( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcOrientation( curve.GetDerivative( t ), curve.GetSecondDerivative( t ) );
+ [MethodImpl( INLINE )] public static Quaternion EvalArcOrientation( this T curve, float t ) where T : IParamCurve2Diff => Mathfs.GetArcOrientation( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) );
/// Returns the position and the frenet-serret (curvature-based) orientation of curve at the given point t, where the Z direction is tangent to the curve.
/// The X axis will point to the inner arc of the current curvature
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static Pose GetArcPose( this T curve, float t ) where T : IParamCurve2Diff {
- ( Vector3 pt, Vector3 vel, Vector3 acc ) = ( curve.GetPoint( t ), curve.GetDerivative( t ), curve.GetSecondDerivative( t ) );
+ [MethodImpl( INLINE )] public static Pose EvalArcPose( this T curve, float t ) where T : IParamCurve2Diff {
+ ( Vector3 pt, Vector3 vel, Vector3 acc ) = ( curve.Eval( t ), curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ) );
Vector3 binormal = Vector3.Cross( vel, acc );
return new Pose( pt, Quaternion.LookRotation( vel, binormal ) );
}
@@ -235,10 +242,10 @@ public static class IParamCurve2DiffExt3D {
/// Returns the position and the frenet-serret (curvature-based) orientation of curve at the given point t, expressed as a matrix, where the Z direction is tangent to the curve.
/// The X axis will point to the inner arc of the current curvature
/// The t-value along the curve to sample
- public static Matrix4x4 GetArcMatrix( this T curve, float t ) where T : IParamCurve2Diff {
- Vector3 P = curve.GetPoint( t );
- Vector3 vel = curve.GetDerivative( t );
- Vector3 acc = curve.GetSecondDerivative( t );
+ public static Matrix4x4 EvalArcMatrix( this T curve, float t ) where T : IParamCurve2Diff {
+ Vector3 P = curve.Eval( t );
+ Vector3 vel = curve.EvalDerivative( t );
+ Vector3 acc = curve.EvalSecondDerivative( t );
Vector3 Tn = vel.normalized;
Vector3 B = Vector3.Cross( vel, acc ).normalized;
Vector3 N = Vector3.Cross( B, Tn );
@@ -259,7 +266,7 @@ public static class IParamCurve3DiffExt3D {
/// Returns the torsion at the given t-value on the curve, in radians per distance unit
/// The t-value along the curve to sample
- [MethodImpl( INLINE )] public static float GetTorsion( this T curve, float t ) where T : IParamCurve3Diff => Mathfs.GetTorsion( curve.GetDerivative( t ), curve.GetSecondDerivative( t ), curve.GetThirdDerivative( t ) );
+ [MethodImpl( INLINE )] public static float EvalTorsion( this T curve, float t ) where T : IParamCurve3Diff => Mathfs.GetTorsion( curve.EvalDerivative( t ), curve.EvalSecondDerivative( t ), curve.EvalThirdDerivative( t ) );
}
diff --git a/Runtime/Curves/IParamCurve.cs.meta b/Runtime/Curves/IParamCurve.cs.meta
new file mode 100644
index 0000000..c3d9b5b
--- /dev/null
+++ b/Runtime/Curves/IParamCurve.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b0d319ba2d8a94359ad22777db36c523
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/IPolynomialCubic.cs b/Runtime/Curves/IPolynomialCubic.cs
new file mode 100644
index 0000000..5d8186e
--- /dev/null
+++ b/Runtime/Curves/IPolynomialCubic.cs
@@ -0,0 +1,54 @@
+namespace Freya {
+
+ public interface IPolynomialCubic {
+
+ /// The constant coefficient
+ public V C0 { get; set; }
+ /// The linear coefficient
+ public V C1 { get; set; }
+ /// The quadratic coefficient
+ public V C2 { get; set; }
+ /// The cubic coefficient
+ public V C3 { get; set; }
+
+ /// The degree of the polynomial
+ public int Degree { get; }
+
+ /// Returns the component polynomial of the given axis/dimension
+ /// Index of axis/dimension. 0 = x. 1 = y, etc
+ public Polynomial this[ int i ] { get; set; }
+
+ /// Gets the coefficient of the given degree
+ /// The degree of the coefficient you want to get. For example, 0 will return the constant coefficient, 3 will return the cubic coefficient
+ V GetCoefficient( int degree );
+
+ /// Sets the coefficient of the given degree
+ /// The degree of the coefficient you want to set. For example, 0 will return the constant coefficient, 3 will return the cubic coefficient
+ /// The value to set it to
+ public void SetCoefficient( int degree, V value );
+
+ /// Evaluates the polynomial at the given value t
+ /// The value to sample at
+ public V Eval( float t );
+
+ /// Evaluates the n:th derivative of the polynomial at the given value t
+ /// The value to sample at
+ /// The derivative to evaluate
+ public V Eval( float t, int n );
+
+ /// Differentiates this function, returning the n-th derivative of this polynomial
+ /// The number of times to differentiate this function. 0 returns the function itself, 1 returns the first derivative
+ public P Differentiate( int n = 1 );
+
+ /// Scales the parameter space by a factor. For example, the output of the polynomial in the input interval [0 to 1] will now be in the range [0 to factor]
+ /// The factor to scale the input parameters by
+ public P ScaleParameterSpace( float factor );
+
+ /// Given an inner function g(x), returns f(g(x))
+ /// The constant coefficient of the inner function g(x)
+ /// The linear coefficient of the inner function g(x)
+ public P Compose( float g0, float g1 );
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/IPolynomialCubic.cs.meta b/Runtime/Curves/IPolynomialCubic.cs.meta
new file mode 100644
index 0000000..6e2ecf4
--- /dev/null
+++ b/Runtime/Curves/IPolynomialCubic.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6d508c7c494c2a34f9d18768260e71e4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/IPolynomialMath.cs b/Runtime/Curves/IPolynomialMath.cs
new file mode 100644
index 0000000..20efc91
--- /dev/null
+++ b/Runtime/Curves/IPolynomialMath.cs
@@ -0,0 +1,39 @@
+using UnityEngine;
+
+namespace Freya {
+
+ public interface IPolynomialMath
{
+ public P NaN { get; }
+ public P FitCubicFrom0( float x1, float x2, float x3, V y0, V y1, V y2, V y3 );
+ }
+
+ public struct PolynomialMath1D : IPolynomialMath {
+ public Polynomial NaN => Polynomial.NaN;
+
+ ///
+ public Polynomial FitCubicFrom0( float x1, float x2, float x3, float y0, float y1, float y2, float y3 ) => Polynomial.FitCubicFrom0( x1, x2, x3, y0, y1, y2, y3 );
+ }
+
+ public struct PolynomialMath2D : IPolynomialMath {
+ public Polynomial2D NaN => Polynomial2D.NaN;
+
+ ///
+ public Polynomial2D FitCubicFrom0( float x1, float x2, float x3, Vector2 y0, Vector2 y1, Vector2 y2, Vector2 y3 ) => Polynomial2D.FitCubicFrom0( x1, x2, x3, y0, y1, y2, y3 );
+ }
+
+ public struct PolynomialMath3D : IPolynomialMath {
+ public Polynomial3D NaN => Polynomial3D.NaN;
+
+ ///
+ public Polynomial3D FitCubicFrom0( float x1, float x2, float x3, Vector3 y0, Vector3 y1, Vector3 y2, Vector3 y3 ) => Polynomial3D.FitCubicFrom0( x1, x2, x3, y0, y1, y2, y3 );
+ }
+
+ public struct PolynomialMath4D : IPolynomialMath {
+ public Polynomial4D NaN => Polynomial4D.NaN;
+
+ ///
+ public Polynomial4D FitCubicFrom0( float x1, float x2, float x3, Vector4 y0, Vector4 y1, Vector4 y2, Vector4 y3 ) => Polynomial4D.FitCubicFrom0( x1, x2, x3, y0, y1, y2, y3 );
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/IPolynomialMath.cs.meta b/Runtime/Curves/IPolynomialMath.cs.meta
new file mode 100644
index 0000000..42cd070
--- /dev/null
+++ b/Runtime/Curves/IPolynomialMath.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 47c9d879bc3df8e4abdda485b1cc9254
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/LagrangePolynomial2D.cs b/Runtime/Curves/LagrangePolynomial2D.cs
new file mode 100644
index 0000000..86abd3f
--- /dev/null
+++ b/Runtime/Curves/LagrangePolynomial2D.cs
@@ -0,0 +1,39 @@
+// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
+
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Freya {
+
+ public class LagrangePolynomial2D {
+
+ public List points = new List();
+ public List knots = null;
+ public bool Uniform => knots == null;
+ public FloatRange InternalKnotRange => Uniform ? ( 0, points.Count - 1 ) : ( knots[0], knots[^1] );
+
+ public Vector2 Eval( float u ) {
+ float l( int j ) {
+ float prod = 1;
+ for( int i = 0; i < points.Count; i++ ) {
+ if( i == j )
+ continue;
+ if( Uniform )
+ prod *= ( u - i ) / ( j - i );
+ else
+ prod *= Mathfs.InverseLerp( knots[i], knots[j], u );
+ }
+
+ return prod;
+ }
+
+ Vector2 sum = Vector2.zero;
+ for( int j = 0; j < points.Count; j++ )
+ sum += points[j] * l( j );
+
+ return sum;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/LagrangePolynomial2D.cs.meta b/Runtime/Curves/LagrangePolynomial2D.cs.meta
new file mode 100644
index 0000000..33f9f81
--- /dev/null
+++ b/Runtime/Curves/LagrangePolynomial2D.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: def04ed833bc2244996ce588be8fb734
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/MobiusTf.cs b/Runtime/Curves/MobiusTf.cs
new file mode 100644
index 0000000..7f7a263
--- /dev/null
+++ b/Runtime/Curves/MobiusTf.cs
@@ -0,0 +1,64 @@
+// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
+
+using System;
+using UnityEngine.Assertions;
+
+namespace Freya {
+
+ /// The Möbius Transformation as an equation of the form f(x) = (ax+b)/(cx+d)
+ [Serializable] public struct MobiusTf {
+
+ public float a, b, c, d;
+
+ public static readonly MobiusTf identity = new MobiusTf( 1, 1, 0, 0 );
+ public bool IsIdentity => b == 0 && c == 0 && a == d;
+
+ float Determinant => a * d - b * c;
+ public bool IsValid => Determinant != 0;
+ public bool IsAffine => c == 0;
+ public MobiusTf Inverse => new(d, -b, -c, a);
+
+ public MobiusTf( float a, float b, float c, float d ) {
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ this.d = d;
+ Assert.IsTrue( IsValid );
+ }
+
+ /// Evaluates the nth derivative of the mobius transform
+ /// The parameter value to evaluate at
+ /// The nth derivative. 0 = evaluates the function. 1 = evaluates the first derivative, etc.
+ public float eval( float x, int n = 0 ) {
+ float D = c * x + d;
+ switch( n ) {
+ case 0: return ( a * x + b ) / D;
+ case 1: return Determinant / ( D * D );
+ case 2: return -2 * Determinant / ( D * D * D );
+ default:
+ int scale = -Mathfs.Factorial( (uint)n ) * n.esign();
+ float num = Determinant * c.pow( n - 1 );
+ return scale * ( num / D.pow( n + 1 ) );
+ }
+ }
+
+ /// Evaluates the definite integral from x0 to x1
+ public float integrate( float x0, float x1 ) {
+ float r = ( x1 - x0 ) / ( c * x0 + d );
+ float rect = r * ( a * x0 + b );
+ float curv = r * r * Determinant * logrem( c * r );
+ return rect + curv;
+ }
+
+ /// A weird natural log remainder type thing (x-ln(x+1))/(x*x)
+ static float logrem( float x ) {
+ return x.abs() switch {
+ 0 => 0.5f, // singularity at 0
+ < 0.001f => 0.5f - x / 3 + ( x * x ) / 4, // approximation near 0
+ _ => ( x - MathF.Log( 1 + x ) ) / ( x * x )
+ };
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/MobiusTf.cs.meta b/Runtime/Curves/MobiusTf.cs.meta
new file mode 100644
index 0000000..fbd5737
--- /dev/null
+++ b/Runtime/Curves/MobiusTf.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f01d18fb05654c1eb34c0354a370a322
+timeCreated: 1784991861
\ No newline at end of file
diff --git a/Runtime/Curves/Polynomial.cs b/Runtime/Curves/Polynomial.cs
new file mode 100644
index 0000000..2f3b1bd
--- /dev/null
+++ b/Runtime/Curves/Polynomial.cs
@@ -0,0 +1,405 @@
+// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Text;
+using Unity.Mathematics;
+using UnityEngine.Serialization;
+
+namespace Freya {
+
+ /// A polynomial in the form ax³+bx²+cx+d, up to a cubic, with functions like derivatives, root finding, and more
+ [Serializable] public struct Polynomial : IPolynomialCubic {
+
+ const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;
+
+ /// A polynomial with all 0 coefficients. f(x) = 0
+ public static readonly Polynomial zero = new Polynomial( 0, 0, 0, 0 );
+
+ /// A polynomial with all NaN coefficients
+ public static readonly Polynomial NaN = new Polynomial( float.NaN, float.NaN, float.NaN, float.NaN );
+
+ /// The constant coefficient
+ [FormerlySerializedAs( "fConstant" )] public float c0;
+
+ /// The linear coefficient
+ [FormerlySerializedAs( "fLinear" )] public float c1;
+
+ /// The quadratic coefficient
+ [FormerlySerializedAs( "fQuadratic" )] public float c2;
+
+ /// The cubic coefficient
+ [FormerlySerializedAs( "fCubic" )] public float c3;
+
+ /// Creates a polynomial up to a cubic
+ /// The constant coefficient
+ /// The linear coefficient
+ /// The quadratic coefficient
+ /// The cubic coefficient
+ public Polynomial( float c0, float c1, float c2, float c3 ) => ( this.c0, this.c1, this.c2, this.c3 ) = ( c0, c1, c2, c3 );
+
+ /// Creates a polynomial up to a quadratic
+ /// The constant coefficient
+ /// The linear coefficient
+ /// The quadratic coefficient
+ public Polynomial( float c0, float c1, float c2 ) => ( this.c0, this.c1, this.c2, this.c3 ) = ( c0, c1, c2, 0 );
+
+ /// Creates a polynomial up to a linear
+ /// The constant coefficient
+ /// The linear coefficient
+ public Polynomial( float c0, float c1 ) => ( this.c0, this.c1, this.c2, this.c3 ) = ( c0, c1, 0, 0 );
+
+ /// Creates a polynomial
+ /// The coefficients to use
+ public Polynomial( float4 coefficients ) => ( c0, c1, c2, c3 ) = ( coefficients.x, coefficients.y, coefficients.z, coefficients.w );
+
+ ///
+ public Polynomial( Matrix4x1 coefficients ) => ( c0, c1, c2, c3 ) = ( coefficients.m0, coefficients.m1, coefficients.m2, coefficients.m3 );
+
+ ///
+ public Polynomial( Matrix3x1 coefficients ) => ( c0, c1, c2, c3 ) = ( coefficients.m0, coefficients.m1, coefficients.m2, 0 );
+
+ ///
+ public Polynomial( (float c0, float c1, float c2, float c3) coefficients ) => ( c0, c1, c2, c3 ) = coefficients;
+
+ ///
+ public Polynomial( (float c0, float c1, float c2) coefficients ) => ( c0, c1, c2, c3 ) = ( coefficients.c0, coefficients.c1, coefficients.c2, 0 );
+
+ #region IPolynomialCubic
+
+ public float C0 {
+ [MethodImpl( INLINE )] get => c0;
+ [MethodImpl( INLINE )] set => c0 = value;
+ }
+ public float C1 {
+ [MethodImpl( INLINE )] get => c1;
+ [MethodImpl( INLINE )] set => c1 = value;
+ }
+ public float C2 {
+ [MethodImpl( INLINE )] get => c2;
+ [MethodImpl( INLINE )] set => c2 = value;
+ }
+ public float C3 {
+ [MethodImpl( INLINE )] get => c3;
+ [MethodImpl( INLINE )] set => c3 = value;
+ }
+ public Polynomial this[ int i ] {
+ [MethodImpl( INLINE )] get => i == 0 ? this : throw new IndexOutOfRangeException( "float polynomials don't have vector components" );
+ [MethodImpl( INLINE )] set => this = value;
+ }
+
+ public int Degree => GetPolynomialDegree( c0, c1, c2, c3 );
+
+ [MethodImpl( INLINE )] public float GetCoefficient( int degree ) =>
+ degree switch {
+ 0 => c0,
+ 1 => c1,
+ 2 => c2,
+ 3 => c3,
+ _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" )
+ };
+
+ [MethodImpl( INLINE )] public void SetCoefficient( int degree, float value ) {
+ _ = degree switch {
+ 0 => c0 = value,
+ 1 => c1 = value,
+ 2 => c2 = value,
+ 3 => c3 = value,
+ _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" )
+ };
+ }
+
+ public float Eval( float t ) {
+ float t2 = t * t;
+ float t3 = t * t2;
+ return c3 * t3 + c2 * t2 + c1 * t + c0;
+ }
+
+ [MethodImpl( INLINE )] public float Eval( float t, int n ) => Differentiate( n ).Eval( t );
+
+ [MethodImpl( INLINE )] public readonly Polynomial Differentiate( int n = 1 ) {
+ return n switch {
+ 0 => this,
+ 1 => new Polynomial( c1, 2 * c2, 3 * c3, 0 ),
+ 2 => new Polynomial( 2 * c2, 6 * c3, 0, 0 ),
+ 3 => new Polynomial( 6 * c3, 0, 0, 0 ),
+ _ => n > 3 ? zero : throw new IndexOutOfRangeException( "Cannot differentiate a negative amount of times" )
+ };
+ }
+
+ public Polynomial ScaleParameterSpace( float factor ) {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if( factor == 1f )
+ return this;
+ float factor2 = factor * factor;
+ float factor3 = factor2 * factor;
+ return new Polynomial(
+ c0,
+ c1 / factor,
+ c2 / factor2,
+ c3 / factor3
+ );
+ }
+
+ /// Given an inner function g(x), returns f(g(x))
+ /// The constant coefficient of the inner function g(x)
+ /// The linear coefficient of the inner function g(x)
+ public Polynomial Compose( float g0, float g1 ) {
+ float g0_2 = g0 * g0;
+ float g0_3 = g0 * g0_2;
+ float g1_2 = g1 * g1;
+ float g1_3 = g1 * g1_2;
+ return new Polynomial(
+ c0 + c1 * g0 + c2 * g0_2 + c3 * g0_3,
+ c1 * g1 + c2 * 2 * g0 * g1 + c3 * 3 * g0_2 * g1,
+ c2 * g1_2 + c3 * 3 * g0 * g1_2,
+ c3 * g1_3
+ );
+ }
+
+ #endregion
+
+ /// Fits a cubic polynomial to pass through the given coordinates
+ public static Polynomial FitCubic( float x0, float x1, float x2, float x3, float y0, float y1, float y2, float y3 ) {
+ // precalcs
+ float i01 = x1 - x0;
+ float i02 = x2 - x0;
+ float i03 = x3 - x0;
+ float i12 = x2 - x1;
+ float i13 = x3 - x1;
+ float i23 = x3 - x2;
+ float x0x1 = x0 * x1;
+ float x0x2 = x0 * x2;
+ float x0x3 = x0 * x3;
+ float x1x2 = x1 * x2;
+ float x1x3 = x1 * x3;
+ float x2x3 = x2 * x3;
+ float x1x2x3 = x1 * x2x3;
+ float x0x2x3 = x0 * x2x3;
+ float x0x1x3 = x0 * x1x3;
+ float x0x1x2 = x0 * x1x2;
+ float x0plusx1 = x0 + x1;
+ float x0plusx1plusx2 = x0plusx1 + x2;
+ float x0plusx1plusx3 = x0plusx1 + x3;
+ float x2plusx3 = x2 + x3;
+ float x0plusx2plusx3 = x0 + x2plusx3;
+ float x1plusx2plusx3 = x1 + x2plusx3;
+ float x1x2plusx1x3plusx2x3 = ( x1x2 + x1x3 + x2x3 );
+ float x0x2plusx0x3plusx2x3 = ( x0x2 + x0x3 + x2x3 );
+ float x0x1plusx0x3plusx1x3 = ( x0x1 + x0x3 + x1x3 );
+ float x0x1plusx0x2plusx1x2 = ( x0x1 + x0x2 + x1x2 );
+
+ // scale factors
+ float scl0 = -( y0 / ( i01 * i02 * i03 ) );
+ float scl1 = +( y1 / ( i01 * i12 * i13 ) );
+ float scl2 = -( y2 / ( i02 * i12 * i23 ) );
+ float scl3 = +( y3 / ( i03 * i13 * i23 ) );
+
+ // polynomial form
+ float c0 = -( scl0 * x1x2x3 + scl1 * x0x2x3 + scl2 * x0x1x3 + scl3 * x0x1x2 );
+ float c1 = scl0 * x1x2plusx1x3plusx2x3 + scl1 * x0x2plusx0x3plusx2x3 + scl2 * x0x1plusx0x3plusx1x3 + scl3 * x0x1plusx0x2plusx1x2;
+ float c2 = -( scl0 * x1plusx2plusx3 + scl1 * x0plusx2plusx3 + scl2 * x0plusx1plusx3 + scl3 * x0plusx1plusx2 );
+ float c3 = scl0 + scl1 + scl2 + scl3;
+
+ return new Polynomial( (float)c0, (float)c1, (float)c2, (float)c3 );
+ }
+
+ /// Fits a cubic polynomial to pass through the given coordinates, assuming x0 = 0
+ public static Polynomial FitCubicFrom0( float x1, float x2, float x3, float y0, float y1, float y2, float y3 ) {
+ // precalcs
+ float i12 = x2 - x1;
+ float i13 = x3 - x1;
+ float i23 = x3 - x2;
+ float x1x2 = x1 * x2;
+ float x1x3 = x1 * x3;
+ float x2x3 = x2 * x3;
+ float x1x2x3 = x1 * x2x3;
+ float x2plusx3 = x2 + x3;
+
+ // scale factors
+ float scl0 = -( y0 / ( x1 * x2 * x3 ) );
+ float scl1 = +( y1 / ( x1 * i12 * i13 ) );
+ float scl2 = -( y2 / ( x2 * i12 * i23 ) );
+ float scl3 = +( y3 / ( x3 * i13 * i23 ) );
+
+ // polynomial form
+ float c0 = -( scl0 * x1x2x3 );
+ float c1 = scl0 * ( x1x2 + x1x3 + x2x3 ) + scl1 * x2x3 + scl2 * x1x3 + scl3 * x1x2;
+ float c2 = -( scl0 * ( x2plusx3 + x1 ) + scl1 * ( x2plusx3 ) + scl2 * ( x1 + x3 ) + scl3 * ( x1 + x2 ) );
+ float c3 = scl0 + scl1 + scl2 + scl3;
+
+ return new Polynomial( c0, c1, c2, c3 );
+ }
+
+
+ /// Splits the 0-1 range into two distinct polynomials at the given parameter value u, where both new curves cover the same total range with their individual 0-1 ranges
+ /// The parameter value to split at
+ public (Polynomial pre, Polynomial post) Split01( float u ) {
+ float d = 1f - u;
+ float dd = d * d;
+ float ddd = d * d * d;
+ float uu = u * u;
+ float uuu = u * u * u;
+
+ Polynomial pre = new Polynomial( c0, c1 * u, c2 * uu, c3 * uuu );
+ Polynomial post = new Polynomial(
+ Eval( u ),
+ d * Differentiate( 1 ).Eval( u ),
+ dd / 2 * Differentiate( 2 ).Eval( u ),
+ ddd / 6 * Differentiate( 3 ).Eval( u )
+ );
+ return ( pre, post );
+ }
+
+ /// Calculates the roots (values where this polynomial = 0)
+ public ResultsMax3 Roots => Solve.Polynomial( c0, c1, c2, c3 );
+
+ /// Calculates the local extrema of this polynomial
+ public ResultsMax2 LocalExtrema => (ResultsMax2)Differentiate().Roots;
+
+ /// Calculates the local extrema of this polynomial in the unit interval
+ public ResultsMax2 LocalExtrema01 {
+ get {
+ ResultsMax2 all = LocalExtrema;
+ ResultsMax2 valids = new ResultsMax2();
+ for( int i = 0; i < all.count; i++ ) {
+ float t = all[i];
+ if( t.Within( 0, 1 ) )
+ valids = valids.Add( all[i] );
+ }
+
+ return valids;
+ }
+ }
+
+ /// Returns the output value range within the unit interval
+ public FloatRange OutputRange01 {
+ get {
+ FloatRange range = ( Eval( 0 ), Eval( 1 ) );
+ foreach( float t in LocalExtrema01 )
+ range = range.Encapsulate( Eval( t ) );
+ return range;
+ }
+ }
+
+ #region Statics
+
+ /// Creates a constant polynomial
+ /// The constant coefficient
+ public static Polynomial Constant( float constant ) => new Polynomial( constant, 0, 0, 0 );
+
+ /// Creates a linear polynomial of the form ax+b
+ /// The constant coefficient b in ax+b
+ /// The linear coefficient a in ax+b
+ public static Polynomial Linear( float c0, float c1 ) => new Polynomial( c0, c1, 0, 0 );
+
+ /// Creates a linear polynomial of the form ax+b from two points a and b
+ /// The first point
+ /// The second point
+ public static Polynomial Linear( float2 a, float2 b ) => Linear( a.x, a.y, b.x, b.y );
+
+ /// Creates a linear polynomial of the form ax+b from two points
+ /// The coordinate of the first point
+ /// The value of the first point
+ /// The coordinate of the second point
+ /// The value of the second point
+ public static Polynomial Linear( float x0, float y0, float x1, float y1 ) {
+ float d = ( y1 - y0 ) / ( x1 - x0 );
+ return new Polynomial( y0 - d * x0, d, 0, 0 );
+ }
+
+ /// Creates a quadratic polynomial
+ /// The constant coefficient
+ /// The linear coefficient
+ /// The quadratic coefficient
+ public static Polynomial Quadratic( float c0, float c1, float c2 ) => new Polynomial( c0, c1, c2, 0 );
+
+ /// Creates a cubic polynomial
+ /// The constant coefficient
+ /// The linear coefficient
+ /// The quadratic coefficient
+ /// The cubic coefficient
+ public static Polynomial Cubic( float c0, float c1, float c2, float c3 ) => new Polynomial( c0, c1, c2, c3 );
+
+ /// Given the coefficients for a cubic polynomial, returns the net polynomial type/degree, accounting for values very close to 0
+ /// The constant coefficient
+ /// The linear coefficient
+ /// The quadratic coefficient
+ /// The cubic coefficient
+ /// The quartic coefficient
+ [MethodImpl( INLINE )] public static int GetPolynomialDegree( float c0, float c1 = 0, float c2 = 0, float c3 = 0, float c4 = 0 ) {
+ if( Mathfs.Approximately( c4, 0 ) == false ) return 4;
+ if( Mathfs.Approximately( c3, 0 ) == false ) return 3;
+ if( Mathfs.Approximately( c2, 0 ) == false ) return 2;
+ if( Mathfs.Approximately( c1, 0 ) == false ) return 1;
+ return 0;
+ }
+
+ /// Linearly interpolates between two polynomials
+ /// The first polynomial to blend from
+ /// The second polynomial to blend to
+ /// The blend value, typically from 0 to 1
+ public static Polynomial Lerp( Polynomial a, Polynomial b, float t ) =>
+ new(
+ t.Lerp( a.c0, b.c0 ),
+ t.Lerp( a.c1, b.c1 ),
+ t.Lerp( a.c2, b.c2 ),
+ t.Lerp( a.c3, b.c3 )
+ );
+
+ #endregion
+
+ #region Typecasting & Operators
+
+ public static Polynomial operator /( Polynomial p, float v ) => new(p.c0 / v, p.c1 / v, p.c2 / v, p.c3 / v);
+ public static Polynomial operator /( float v, Polynomial p ) => new(v / p.c0, v / p.c1, v / p.c2, v / p.c3);
+ public static Polynomial operator *( Polynomial p, float v ) => new(p.c0 * v, p.c1 * v, p.c2 * v, p.c3 * v);
+ public static Polynomial operator *( float v, Polynomial p ) => p * v;
+ public static Polynomial operator +( Polynomial a, Polynomial b ) => new(a.c0 + b.c0, a.c1 + b.c1, a.c2 + b.c2, a.c3 + b.c3);
+ public static Polynomial operator -( Polynomial a, Polynomial b ) => new(a.c0 - b.c0, a.c1 - b.c1, a.c2 - b.c2, a.c3 - b.c3);
+
+ public static explicit operator Matrix3x1( Polynomial poly ) => new(poly.c0, poly.c1, poly.c2);
+ public static explicit operator Matrix4x1( Polynomial poly ) => new(poly.c0, poly.c1, poly.c2, poly.c3);
+ public static explicit operator BezierQuad1D( Polynomial poly ) => poly.Degree < 3 ? new BezierQuad1D( CharMatrix.quadraticBezierInverse * (Matrix3x1)poly ) : throw new InvalidCastException( "Cannot cast a cubic polynomial to a quadratic curve" );
+ public static explicit operator BezierCubic1D( Polynomial poly ) => new(CharMatrix.cubicBezierInverse * (Matrix4x1)poly);
+ public static explicit operator CatRomCubic1D( Polynomial poly ) => new(CharMatrix.cubicCatmullRomInverse * (Matrix4x1)poly);
+ public static explicit operator HermiteCubic1D( Polynomial poly ) => new(CharMatrix.cubicHermiteInverse * (Matrix4x1)poly);
+ public static explicit operator UBSCubic1D( Polynomial poly ) => new(CharMatrix.cubicUniformBsplineInverse * (Matrix4x1)poly);
+
+ #endregion
+
+ static StringBuilder strBuilder = new StringBuilder( 64 );
+ static string[] tPowerSuffixStr = new[] { "", "x", "x²", "x³" };
+
+ public override string ToString() {
+ strBuilder.Clear();
+
+ bool hasAddedFirstTerm = false;
+ for( int c = 0; c < 4; c++ ) {
+ float value = GetCoefficient( c );
+ if( value != 0 ) {
+ if( hasAddedFirstTerm == false ) {
+ hasAddedFirstTerm = true;
+ strBuilder.Append( GetCoefficient( c ) );
+ } else {
+ if( value > 0 )
+ strBuilder.Append( "+" );
+ strBuilder.Append( GetCoefficient( c ) );
+ if( c > 0 )
+ strBuilder.Append( tPowerSuffixStr[c] );
+ }
+ }
+ }
+
+ if( hasAddedFirstTerm == false )
+ return "0"; // no terms. constant 0
+
+ return strBuilder.ToString();
+ }
+
+ public string ToStringCoefficients() => $"({c0},{c1},{c2},{c3})";
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/Runtime/Curves/Polynomial.cs.meta b/Runtime/Curves/Polynomial.cs.meta
new file mode 100644
index 0000000..3f7dd11
--- /dev/null
+++ b/Runtime/Curves/Polynomial.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d77cc56ff77094210bc83f74c421077c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Curves/Polynomial2D.cs b/Runtime/Curves/Polynomial2D.cs
new file mode 100644
index 0000000..59eddca
--- /dev/null
+++ b/Runtime/Curves/Polynomial2D.cs
@@ -0,0 +1,413 @@
+// by Freya Holmér (https://github.com/FreyaHolmer/Mathfs)
+
+using System;
+using System.Runtime.CompilerServices;
+using UnityEngine;
+
+namespace Freya {
+
+ [Serializable]
+ public struct Polynomial2D : IPolynomialCubic, IParamCurve3Diff {
+
+ const MethodImplOptions INLINE = MethodImplOptions.AggressiveInlining;
+
+ ///
+ public static readonly Polynomial2D NaN = new Polynomial2D { x = Polynomial.NaN, y = Polynomial.NaN };
+
+ public Polynomial x, y;
+
+ public Polynomial2D( Polynomial x, Polynomial y ) => ( this.x, this.y ) = ( x, y );
+
+ ///
+ public Polynomial2D( Vector2 c0, Vector2 c1, Vector2 c2, Vector2 c3 ) {
+ this.x = new Polynomial( c0.x, c1.x, c2.x, c3.x );
+ this.y = new Polynomial( c0.y, c1.y, c2.y, c3.y );
+ }
+
+ ///
+ public Polynomial2D( Vector2 c0, Vector2 c1, Vector2 c2 ) {
+ this.x = new Polynomial( c0.x, c1.x, c2.x );
+ this.y = new Polynomial( c0.y, c1.y, c2.y );
+ }
+
+ ///
+ public Polynomial2D( Vector2 c0, Vector2 c1 ) {
+ this.x = new Polynomial( c0.x, c1.x, 0, 0 );
+ this.y = new Polynomial( c0.y, c1.y, 0, 0 );
+ }
+
+ ///
+ public Polynomial2D( Vector2Matrix4x1 coefficients ) => ( x, y ) = ( new Polynomial( coefficients.X ), new Polynomial( coefficients.Y ) );
+
+ ///
+ public Polynomial2D( Vector2Matrix3x1 coefficients ) => ( x, y ) = ( new Polynomial( coefficients.X ), new Polynomial( coefficients.Y ) );
+
+ #region IPolynomialCubic
+
+ public Vector2 C0 {
+ [MethodImpl( INLINE )] get => new(x.c0, y.c0);
+ [MethodImpl( INLINE )] set => ( x.c0, y.c0 ) = ( value.x, value.y );
+ }
+ public Vector2 C1 {
+ [MethodImpl( INLINE )] get => new(x.c1, y.c1);
+ [MethodImpl( INLINE )] set => ( x.c1, y.c1 ) = ( value.x, value.y );
+ }
+ public Vector2 C2 {
+ [MethodImpl( INLINE )] get => new(x.c2, y.c2);
+ [MethodImpl( INLINE )] set => ( x.c2, y.c2 ) = ( value.x, value.y );
+ }
+ public Vector2 C3 {
+ [MethodImpl( INLINE )] get => new(x.c3, y.c3);
+ [MethodImpl( INLINE )] set => ( x.c3, y.c3 ) = ( value.x, value.y );
+ }
+
+ public Polynomial this[ int i ] {
+ get { return i switch { 0 => x, 1 => y, _ => throw new IndexOutOfRangeException( "Polynomial2D component index has to be either 0 or 1" ) }; }
+ set => _ = i switch { 0 => x = value, 1 => y = value, _ => throw new IndexOutOfRangeException() };
+ }
+
+ [MethodImpl( INLINE )] public Vector2 GetCoefficient( int degree ) =>
+ degree switch {
+ 0 => C0,
+ 1 => C1,
+ 2 => C2,
+ 3 => C3,
+ _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" )
+ };
+
+ [MethodImpl( INLINE )] public void SetCoefficient( int degree, Vector2 value ) {
+ _ = degree switch {
+ 0 => C0 = value,
+ 1 => C1 = value,
+ 2 => C2 = value,
+ 3 => C3 = value,
+ _ => throw new IndexOutOfRangeException( "Polynomial coefficient degree/index has to be between 0 and 3" )
+ };
+ }
+
+ public Vector2 Eval( float t ) {
+ float t2 = t * t;
+ float t3 = t2 * t;
+ return new Vector2(
+ x.c3 * t3 + x.c2 * t2 + x.c1 * t + x.c0,
+ y.c3 * t3 + y.c2 * t2 + y.c1 * t + y.c0
+ );
+ }
+
+ [MethodImpl( INLINE )] public Vector2 Eval( float t, int n ) => Differentiate( n ).Eval( t );
+
+ [MethodImpl( INLINE )] public Polynomial2D Differentiate( int n = 1 ) => new(x.Differentiate( n ), y.Differentiate( n ));
+
+ public Polynomial2D ScaleParameterSpace( float factor ) {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ if( factor == 1f )
+ return this;
+ float factor2 = factor * factor;
+ float factor3 = factor2 * factor;
+ return new Polynomial2D(
+ new Polynomial( x.c0, x.c1 / factor, x.c2 / factor2, x.c3 / factor3 ),
+ new Polynomial( y.c0, y.c1 / factor, y.c2 / factor2, y.c3 / factor3 )
+ );
+ }
+
+ public Polynomial2D Compose( float g0, float g1 ) => new(x.Compose( g0, g1 ), y.Compose( g0, g1 ));
+
+ #endregion
+
+ ///
+ public static Polynomial2D FitCubicFrom0( float x1, float x2, float x3, Vector2 y0, Vector2 y1, Vector2 y2, Vector2 y3 ) {
+ // precalcs
+ float i12 = x2 - x1;
+ float i13 = x3 - x1;
+ float i23 = x3 - x2;
+ float x1x2 = x1 * x2;
+ float x1x3 = x1 * x3;
+ float x2x3 = x2 * x3;
+ float x1x2x3 = x1 * x2x3;
+ float x0plusx1plusx2 = x1 + x2;
+ float x0plusx1plusx3 = x1 + x3;
+ float x2plusx3 = x2 + x3;
+ float x1plusx2plusx3 = x1 + x2plusx3;
+ float x1x2plusx1x3plusx2x3 = ( x1x2 + x1x3 + x2x3 );
+
+ // scale factors
+ Vector2 scl0 = y0 / -( x1 * x2 * x3 );
+ Vector2 scl1 = y1 / +( x1 * i12 * i13 );
+ Vector2 scl2 = y2 / -( x2 * i12 * i23 );
+ Vector2 scl3 = y3 / +( x3 * i13 * i23 );
+
+ // polynomial form
+ Vector2 c0 = new(
+ -( scl0.x * x1x2x3 ),
+ -( scl0.y * x1x2x3 )
+ );
+ Vector2 c1 = new(
+ scl0.x * x1x2plusx1x3plusx2x3 + scl1.x * x2x3 + scl2.x * x1x3 + scl3.x * x1x2,
+ scl0.y * x1x2plusx1x3plusx2x3 + scl1.y * x2x3 + scl2.y * x1x3 + scl3.y * x1x2
+ );
+ Vector2 c2 = new(
+ -( scl0.x * x1plusx2plusx3 + scl1.x * x2plusx3 + scl2.x * x0plusx1plusx3 + scl3.x * x0plusx1plusx2 ),
+ -( scl0.y * x1plusx2plusx3 + scl1.y * x2plusx3 + scl2.y * x0plusx1plusx3 + scl3.y * x0plusx1plusx2 )
+ );
+ Vector2 c3 = new(
+ scl0.x + scl1.x + scl2.x + scl3.x,
+ scl0.y + scl1.y + scl2.y + scl3.y
+ );
+
+ return new Polynomial2D( c0, c1, c2, c3 );
+ }
+
+
+ /// Returns the tight axis-aligned bounds of the curve in the unit interval
+ public Rect GetBounds01() => FloatRange.ToRect( x.OutputRange01, y.OutputRange01 );
+
+ ///
+ public (Polynomial2D pre, Polynomial2D post) Split01( float u ) {
+ ( Polynomial xPre, Polynomial xPost ) = x.Split01( u );
+ ( Polynomial yPre, Polynomial yPost ) = y.Split01( u );
+ return ( new Polynomial2D( xPre, yPre ), new Polynomial2D( xPost, yPost ) );
+ }
+
+ #region IParamCurve3Diff interface implementations
+
+ public int Degree => Mathfs.Max( x.Degree, y.Degree );
+ public Vector2 EvalDerivative( float t ) => Differentiate().Eval( t );
+ public Vector2 EvalSecondDerivative( float t ) => Differentiate( 2 ).Eval( t );
+ public Vector2 EvalThirdDerivative( float t = 0 ) => Differentiate( 3 ).Eval( 0 );
+
+ #endregion
+
+ #region Project Point
+
+ /// Returns the (approximate) point on the curve closest to the input point
+ /// The point to project against the curve
+ /// Recommended range: [8-32]. More subdivisions will be more accurate, but more expensive.
+ /// This is how many subdivisions to split the curve into, to find candidates for the closest point.
+ /// If your curves are complex, you might need to use around 16 subdivisions.
+ /// If they are usually very simple, then around 8 subdivisions is likely fine
+ /// Recommended range: [3-6]. More iterations will be more accurate, but more expensive.
+ /// This is how many times to refine the initial guesses, using Newton's method. This converges rapidly, so high numbers are generally not necessary
+ public Vector2 ProjectPoint( Vector2 point, int initialSubdivisions = 16, int refinementIterations = 4 ) => ProjectPoint( point, out _, initialSubdivisions, refinementIterations );
+
+ struct PointProjectSample {
+ public float t;
+ public float distDeltaSq;
+ public Vector2 f;
+ public Vector2 fp;
+ }
+
+ static PointProjectSample[] pointProjectGuesses = { default, default, default };
+
+ /// Returns the (approximate) point on the curve closest to the input point
+ /// The point to project against the curve
+ /// The t-value at the projected point on the curve
+ /// Recommended range: [8-32]. More subdivisions will be more accurate, but more expensive.
+ /// This is how many subdivisions to split the curve into, to find candidates for the closest point.
+ /// If your curves are complex, you might need to use around 16 subdivisions.
+ /// If they are usually very simple, then around 8 subdivisions is likely fine
+ /// Recommended range: [3-6]. More iterations will be more accurate, but more expensive.
+ /// This is how many times to refine the initial guesses, using Newton's method. This converges rapidly, so high numbers are generally not necessary
+ public Vector2 ProjectPoint( Vector2 point, out float t, int initialSubdivisions = 16, int refinementIterations = 4 ) {
+ // define a curve relative to the test point
+ Polynomial2D curve = this;
+ curve.x.c0 -= point.x; // constant coefficient defines the start position
+ curve.y.c0 -= point.y;
+ Polynomial2D vel = curve.Differentiate();
+ Polynomial2D acc = vel.Differentiate();
+ Vector2 curveStart = curve.Eval( 0 );
+ Vector2 curveEnd = curve.Eval( 1 );
+
+ PointProjectSample SampleDistSqDelta( float tSmp ) {
+ PointProjectSample s = new PointProjectSample {
+ t = tSmp,
+ f = curve.Eval( tSmp ),
+ fp = vel.Eval( tSmp )
+ };
+ s.distDeltaSq = Vector2.Dot( s.f, s.fp );
+ return s;
+ }
+
+ // find initial candidates
+ int candidatesFound = 0;
+ PointProjectSample prevSmp = SampleDistSqDelta( 0 );
+
+ for( int i = 1; i < initialSubdivisions; i++ ) {
+ float ti = i / ( initialSubdivisions - 1f );
+ PointProjectSample smp = SampleDistSqDelta( ti );
+ if( Mathfs.SignAsInt( smp.distDeltaSq ) != Mathfs.SignAsInt( prevSmp.distDeltaSq ) ) {
+ pointProjectGuesses[candidatesFound++] = SampleDistSqDelta( ( prevSmp.t + smp.t ) / 2 );
+ if( candidatesFound == 3 ) break; // no more than three possible candidates because of the polynomial degree
+ }
+
+ prevSmp = smp;
+ }
+
+ // refine each guess w. Newton-Raphson iterations
+ void Refine( ref PointProjectSample smp ) {
+ Vector2 fpp = acc.Eval( smp.t );
+ float tNew = smp.t - Vector2.Dot( smp.f, smp.fp ) / ( Vector2.Dot( smp.f, fpp ) + Vector2.Dot( smp.fp, smp.fp ) );
+ smp = SampleDistSqDelta( tNew );
+ }
+
+ for( int p = 0; p < candidatesFound; p++ )
+ for( int i = 0; i < refinementIterations; i++ )
+ Refine( ref pointProjectGuesses[p] );
+
+ // Now find closest. First include the endpoints
+ float sqDist0 = curveStart.sqrMagnitude; // include endpoints
+ float sqDist1 = curveEnd.sqrMagnitude;
+ bool firstClosest = sqDist0 < sqDist1;
+ float tClosest = firstClosest ? 0 : 1;
+ Vector2 ptClosest = ( firstClosest ? curveStart : curveEnd ) + point;
+ float distSqClosest = firstClosest ? sqDist0 : sqDist1;
+
+ // then check internal roots
+ for( int i = 0; i < candidatesFound; i++ ) {
+ float pSqmag = pointProjectGuesses[i].f.sqrMagnitude;
+ if( pSqmag < distSqClosest ) {
+ distSqClosest = pSqmag;
+ tClosest = pointProjectGuesses[i].t;
+ ptClosest = pointProjectGuesses[i].f + point;
+ }
+ }
+
+ t = tClosest;
+ return ptClosest;
+ }
+
+ #endregion
+
+ #region Intersection Tests
+
+ // Internal - used by all other intersections
+ private ResultsMax3 Intersect( Vector2 origin, Vector2 direction, bool rangeLimited = false, float minRayT = float.NaN, float maxRayT = float.NaN ) {
+ Polynomial2D rel = this;
+ rel.C0 -= origin;
+ float y0 = Mathfs.Determinant( rel.C0, direction ); // transform polynomial into the line space y components
+ float y1 = Mathfs.Determinant( rel.C1, direction );
+ float y2 = Mathfs.Determinant( rel.C2, direction );
+ float y3 = Mathfs.Determinant( rel.C3, direction );
+ Polynomial polynomY = new Polynomial( y0, y1, y2, y3 );
+ ResultsMax3 roots = polynomY.Roots; // t values of the function
+
+ Polynomial polynomX = default;
+ if( rangeLimited ) {
+ // if we're range limited, we need to verify position along the ray/line/lineSegment
+ // and if we do, we need to be able to go from t -> x coord
+ float x0 = Vector2.Dot( rel.C0, direction ); // transform into the line space x components
+ float x1 = Vector2.Dot( rel.C1, direction );
+ float x2 = Vector2.Dot( rel.C2, direction );
+ float x3 = Vector2.Dot( rel.C3, direction );
+ polynomX = new Polynomial( x0, x1, x2, x3 );
+ }
+
+ float CurveTtoRayT( float t ) => polynomX.Eval( t );
+
+ ResultsMax3 returnVals = default;
+
+ for( int i = 0; i < roots.count; i++ ) {
+ if( roots[i].Between( 0, 1 ) && ( rangeLimited == false || CurveTtoRayT( roots[i] ).Within( minRayT, maxRayT ) ) )
+ returnVals = returnVals.Add( roots[i] );
+ }
+
+ return returnVals;
+ }
+
+ // Internal - to unpack from curve t values to points
+ private ResultsMax3 TtoPoints( ResultsMax3 tVals ) {
+ ResultsMax3 pts = default;
+ for( int i = 0; i < tVals.count; i++ )
+ pts = pts.Add( Eval( tVals[i] ) );
+ return pts;
+ }
+
+ /// Returns the t-values at which the given line intersects with the curve
+ /// The line to test intersection against
+ public ResultsMax3 Intersect( Line2D line ) => Intersect( line.origin, line.dir );
+
+ /// Returns the t-values at which the given ray intersects with the curve
+ /// The ray to test intersection against
+ public ResultsMax3 Intersect( Ray2D ray ) => Intersect( ray.origin, ray.dir, rangeLimited: true, 0, float.MaxValue );
+
+ /// Returns the t-values at which the given line segment intersects with the curve
+ /// The line segment to test intersection against
+ public ResultsMax3 Intersect( LineSegment2D lineSegment ) => Intersect( lineSegment.start, lineSegment.end - lineSegment.start, rangeLimited: true, 0, lineSegment.LengthSquared );
+
+ /// Returns the points at which the given line intersects with the curve
+ /// The line to test intersection against
+ public ResultsMax3 IntersectionPoints( Line2D line ) => TtoPoints( Intersect( line.origin, line.dir ) );
+
+ ///