diff --git a/Curves.meta b/Curves.meta
new file mode 100644
index 0000000..854c08a
--- /dev/null
+++ b/Curves.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 563bd956a3113b74294192fccab9b301
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Licenses.meta b/Curves/Licenses.meta
new file mode 100644
index 0000000..5129c10
--- /dev/null
+++ b/Curves/Licenses.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 40527a6e86e0a9347a098edd331a55ab
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Licenses/Siccity_license.txt b/Curves/Licenses/Siccity_license.txt
new file mode 100644
index 0000000..b8e14fa
--- /dev/null
+++ b/Curves/Licenses/Siccity_license.txt
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2017 Thor Brigsted
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Curves/Licenses/Siccity_license.txt.meta b/Curves/Licenses/Siccity_license.txt.meta
new file mode 100644
index 0000000..0b52a78
--- /dev/null
+++ b/Curves/Licenses/Siccity_license.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 5df5e3a5055b0ad40b9d8b67b8fff331
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Licenses/license.txt b/Curves/Licenses/license.txt
new file mode 100644
index 0000000..195a8a2
--- /dev/null
+++ b/Curves/Licenses/license.txt
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2017 Jeff Campbell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Curves/Licenses/license.txt.meta b/Curves/Licenses/license.txt.meta
new file mode 100644
index 0000000..0adefd7
--- /dev/null
+++ b/Curves/Licenses/license.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 27cdff8b8528ce64c841361d384e1ff0
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts.meta b/Curves/Scripts.meta
new file mode 100644
index 0000000..40f7b02
--- /dev/null
+++ b/Curves/Scripts.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 66a2f4def4d45434d94abfb0bc1c25ce
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/AssemblyInfo.cs b/Curves/Scripts/AssemblyInfo.cs
new file mode 100644
index 0000000..78828a5
--- /dev/null
+++ b/Curves/Scripts/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("JCMG.Curves.Editor")]
diff --git a/Curves/Scripts/AssemblyInfo.cs.meta b/Curves/Scripts/AssemblyInfo.cs.meta
new file mode 100644
index 0000000..5c69f8c
--- /dev/null
+++ b/Curves/Scripts/AssemblyInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 87fe00a2aca8eb349840a39d564c827a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Components.meta b/Curves/Scripts/Components.meta
new file mode 100644
index 0000000..4e6dfb0
--- /dev/null
+++ b/Curves/Scripts/Components.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7d1b72941a5ad714db71f38f47d8225e
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Components/Bezier3DSpline.cs b/Curves/Scripts/Components/Bezier3DSpline.cs
new file mode 100644
index 0000000..e54767a
--- /dev/null
+++ b/Curves/Scripts/Components/Bezier3DSpline.cs
@@ -0,0 +1,454 @@
+using System;
+using System.Linq;
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// A Bezier 3D spline whose positions and rotations are transformed by a 's ;
+ ///
+ [AddComponentMenu("JCMG/Curves/Bezier3DSpline")]
+ [ExecuteInEditMode]
+ public sealed class Bezier3DSpline : MonoBehaviour,
+ IBezier3DSplineData
+ {
+ #region Properties
+
+ ///
+ /// Returns true if the spline is a closed loop, otherwise false.
+ ///
+ public bool IsClosed
+ {
+ get { return _splineData.IsClosed; }
+ }
+
+ ///
+ /// Returns the density of the curve caches. This determines the number of interpolation steps calculated
+ /// per curve.
+ ///
+ public int InterpolationStepsPerCurve
+ {
+ get { return _splineData.InterpolationStepsPerCurve; }
+ }
+
+ ///
+ /// Returns the number of curves in the spline.
+ ///
+ public int CurveCount
+ {
+ get { return _splineData.CurveCount; }
+ }
+
+ ///
+ /// Returns the number of s in the spline.
+ ///
+ public int KnotCount
+ {
+ get { return _splineData.KnotCount; }
+ }
+
+ ///
+ /// Returns the total length of the spline based on the length of all curves.
+ ///
+ public float TotalLength
+ {
+ get { return _splineData.TotalLength; }
+ }
+
+ ///
+ /// Returns the internal of this scene-based spline.
+ ///
+ internal Bezier3DSplineData SplineData
+ {
+ get { return _splineData; }
+ }
+
+ #endregion
+
+ #region Fields
+
+ [HideInInspector]
+ [SerializeField]
+ private Bezier3DSplineData _splineData;
+
+ #endregion
+
+ #region Unity
+
+ private void Awake()
+ {
+ if (_splineData == null)
+ {
+ _splineData = ScriptableObject.CreateInstance();
+ }
+ }
+
+ private void Reset()
+ {
+ _splineData = ScriptableObject.CreateInstance();
+ }
+
+ #if UNITY_EDITOR
+
+ private void OnDrawGizmos()
+ {
+ if (UnityEditor.Selection.objects.Contains(this))
+ {
+ return;
+ }
+
+ SceneGUITools.DrawCurveLinesGizmos(this, transform);
+ }
+
+ #endif
+
+ #endregion
+
+ #region Settings
+
+ ///
+ /// Recache all individual curves with new interpolation step count.
+ ///
+ /// Number of steps per curve to cache position and rotation.
+ public void SetStepsPerCurve(int stepCount)
+ {
+ _splineData.SetStepsPerCurve(stepCount);
+ }
+
+ ///
+ /// Setting spline to closed will generate an extra curve, connecting end point to start point.
+ ///
+ public void SetClosed(bool isClosed)
+ {
+ _splineData.SetClosed(isClosed);
+ }
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// Returns a normalized value [0-1] based on the passed compared
+ /// to the of the spline.
+ ///
+ ///
+ ///
+ public float GetNormalizedValueForSplineDistance(float splineDistance)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Returns a normalized value [0-1] based on the passed compared
+ /// to the of the that the distance falls along
+ /// the spline.
+ ///
+ ///
+ ///
+ public float GetCurveDistanceForSplineDistance(float splineDistance)
+ {
+ return _splineData.GetCurveDistanceForSplineDistance(splineDistance);
+ }
+
+ ///
+ /// Returns the length of the spline leading up to and ending on the at
+ /// position in the collection.
+ ///
+ ///
+ ///
+ public float GetSplineDistanceForKnotIndex(int index)
+ {
+ return _splineData.GetSplineDistanceForKnotIndex(index);
+ }
+
+ ///
+ /// Returns the length of the spline leading up to and ending at the end of the at
+ /// position in the collection.
+ ///
+ ///
+ ///
+ public float GetSplineDistanceForCurveIndex(int index)
+ {
+ return _splineData.GetSplineDistanceForCurveIndex(index);
+ }
+
+ public float GetSplineDistanceForNormalizedValue(float value)
+ {
+ return _splineData.GetSplineDistanceForNormalizedValue(value);
+ }
+
+ #endregion
+
+ #region Actions
+
+ ///
+ /// Flip the spline direction.
+ ///
+ public void Flip()
+ {
+ _splineData.Flip();
+ }
+
+ #endregion
+
+ #region Curve
+
+ ///
+ /// Get at position in the collection.
+ ///
+ public Bezier3DCurve GetCurve(int index)
+ {
+ return _splineData.GetCurve(index);
+ }
+
+ ///
+ /// Returns the where falls upon it along the spline;
+ /// and are initialized to the position in the collection
+ /// and the normalized value [0-1] of time through the curve.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Bezier3DCurve GetCurveIndexTime(float splineDist, out int index, out float curveTime)
+ {
+ return _splineData.GetCurveIndexTime(splineDist, out index, out curveTime);
+ }
+
+ ///
+ /// Get the curve indices in direct contact with the at position
+ /// in the collection.
+ ///
+ public void GetCurveIndicesForKnot(int knotIndex, out int preCurveIndex, out int postCurveIndex)
+ {
+ _splineData.GetCurveIndicesForKnot(knotIndex, out preCurveIndex, out postCurveIndex);
+ }
+
+ #endregion
+
+ #region Rotation
+
+ ///
+ /// Returns rotation along spline at set distance along the .
+ ///
+ public Quaternion GetRotation(float splineDistance)
+ {
+ return _splineData.GetRotation(splineDistance, transform);
+ }
+
+ ///
+ /// Returns rotation along spline at set distance along the . Uses approximation.
+ ///
+ public Quaternion GetRotationFast(float splineDistance)
+ {
+ return _splineData.GetRotationFast(splineDistance, transform);
+ }
+
+ ///
+ /// Returns a rotation along the spline where is a normalized value between [0-1] of
+ /// its .
+ ///
+ ///
+ ///
+ public Quaternion GetNormalizedRotation(float value)
+ {
+ var splineDistance = _splineData.GetSplineDistanceForNormalizedValue(value);
+ return GetRotation(splineDistance);
+ }
+
+ ///
+ /// Returns rotation along spline at set distance along the in local coordinates.
+ /// Uses approximation.
+ ///
+ internal Quaternion GetRotationLocal(float splineDistance)
+ {
+ return _splineData.GetRotationLocal(splineDistance);
+ }
+
+ ///
+ /// Returns rotation along spline at set distance along the in local coordinates.
+ /// Uses approximation.
+ ///
+ internal Quaternion GetRotationLocalFast(float splineDistance)
+ {
+ return _splineData.GetRotationLocalFast(splineDistance);
+ }
+
+ #endregion
+
+ #region Position
+
+ ///
+ /// Returns position along spline at set distance along the .
+ ///
+ public Vector3 GetPosition(float splineDistance)
+ {
+ return _splineData.GetPosition(splineDistance, transform);
+ }
+
+ ///
+ /// Returns a position along the spline where is a normalized value between [0-1] of
+ /// its .
+ ///
+ ///
+ ///
+ public Vector3 GetNormalizedPosition(float value)
+ {
+ var splineDistance = GetSplineDistanceForNormalizedValue(value);
+ return GetPosition(splineDistance);
+ }
+
+ ///
+ /// Returns position along spline at set distance along the .
+ ///
+ internal Vector3 GetPointLocal(float splineDistance)
+ {
+ return _splineData.GetPositionLocal(splineDistance);
+ }
+
+ #endregion
+
+ #region Direction
+
+ ///
+ /// Returns up vector at set distance along the .
+ ///
+ public Vector3 GetUp(float splineDistance)
+ {
+ return _splineData.GetUp(splineDistance, transform);
+ }
+
+ ///
+ /// Returns up vector at set distance along the in local coordinates.
+ ///
+ internal Vector3 GetUpLocal(float splineDistance)
+ {
+ return _splineData.GetUpLocal(splineDistance);
+ }
+
+ ///
+ /// Returns left vector at set distance along the .
+ ///
+ public Vector3 GetLeft(float splineDistance)
+ {
+ return _splineData.GetLeft(splineDistance, transform);
+ }
+
+ ///
+ /// Returns left vector at set distance along the in local coordinates.
+ ///
+ internal Vector3 GetLeftLocal(float splineDistance)
+ {
+ return _splineData.GetLeftLocal(splineDistance);
+ }
+
+ ///
+ /// Returns right vector at set distance along the .
+ ///
+ public Vector3 GetRight(float splineDistance)
+ {
+ return _splineData.GetRight(splineDistance, transform);
+ }
+
+ ///
+ /// Returns right vector at set distance along the in local coordinates.
+ ///
+ internal Vector3 GetRightLocal(float splineDistance)
+ {
+ return _splineData.GetRightLocal(splineDistance);
+ }
+
+ ///
+ /// Returns forward vector at set distance along the .
+ ///
+ public Vector3 GetForward(float splineDistance)
+ {
+ return _splineData.GetForward(splineDistance, transform);
+ }
+
+ ///
+ /// Returns forward vector at set distance along the . Uses approximation.
+ ///
+ public Vector3 GetForwardFast(float splineDistance)
+ {
+ return _splineData.GetForwardFast(splineDistance, transform);
+ }
+
+ ///
+ /// Returns forward vector at set distance along the in local coordinates.
+ ///
+ internal Vector3 GetForwardLocal(float splineDistance)
+ {
+ return _splineData.GetForwardLocal(splineDistance);
+ }
+
+ ///
+ /// Returns forward vector at set distance along the in local coordinates. Uses
+ /// approximation.
+ ///
+ internal Vector3 GetForwardLocalFast(float splineDistance)
+ {
+ return _splineData.GetForwardLocalFast(splineDistance);
+ }
+
+ #endregion
+
+ #region Knot
+
+ ///
+ /// Adds a new to the end of the spline.
+ ///
+ ///
+ public void AddKnot(Knot knot)
+ {
+ _splineData.AddKnot(knot);
+ }
+
+ ///
+ /// Returns info in local coordinates at the position in the collection.
+ ///
+ public Knot GetKnot(int index)
+ {
+ return _splineData.GetKnot(index);
+ }
+
+ ///
+ /// Inserts a new at the position in the collection.
+ ///
+ ///
+ ///
+ public void InsertKnot(int index, Knot knot)
+ {
+ _splineData.InsertKnot(index, knot);
+ }
+
+ ///
+ /// Removes the at the position in the collection.
+ ///
+ ///
+ public void RemoveKnot(int index)
+ {
+ _splineData.RemoveKnot(index);
+ }
+
+ ///
+ /// Set info in local coordinates at the
+ /// position in the collection.
+ ///
+ public void SetKnot(int index, Knot knot)
+ {
+ _splineData.SetKnot(index, knot);
+ }
+
+ ///
+ /// Get the knot indices in direct contact with knot. If a knot is not found before and/or after, that index
+ /// will be initialized to -1.
+ ///
+ public void GetKnotIndicesForKnot(int knotIndex, out int preKnotIndex, out int postKnotIndex)
+ {
+ _splineData.GetKnotIndicesForKnot(knotIndex, out preKnotIndex, out postKnotIndex);
+ }
+
+ #endregion
+ }
+}
diff --git a/Curves/Scripts/Components/Bezier3DSpline.cs.meta b/Curves/Scripts/Components/Bezier3DSpline.cs.meta
new file mode 100644
index 0000000..56baf12
--- /dev/null
+++ b/Curves/Scripts/Components/Bezier3DSpline.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3f0e6588f74691844b25110a973044fa
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: c5c32b6c450791943b0b231a833c00b6, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Components/SplineWalker.cs b/Curves/Scripts/Components/SplineWalker.cs
new file mode 100644
index 0000000..3ab8c25
--- /dev/null
+++ b/Curves/Scripts/Components/SplineWalker.cs
@@ -0,0 +1,195 @@
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// Moves a transform along a spline at either a constant speed or over a fixed time duration.
+ ///
+ [AddComponentMenu("JCMG/Curves/SplineWalker")]
+ public sealed class SplineWalker : MonoBehaviour
+ {
+ private enum MoveType
+ {
+ UseConstantSpeed,
+ UseFixedDuration
+ }
+
+ private enum LoopType
+ {
+ Clamp,
+ Loop,
+ PingPong
+ }
+
+ #pragma warning disable 0649
+
+ [Header("Scene References")]
+ [SerializeField]
+ private Bezier3DSpline _spline;
+
+ [Space]
+ [Header("Movement Settings")]
+ [SerializeField]
+ private LoopType _loopType;
+
+ [SerializeField]
+ private MoveType _moveType;
+
+ [SerializeField]
+ private float _startingSplineDistance;
+
+ [Space]
+ [Header("Constant Speed")]
+
+ [SerializeField]
+ private float _speed = 1;
+
+ [Space]
+ [Header("Over Fixed Duration")]
+ [SerializeField]
+ private float _duration;
+
+ [Space]
+ [Header("Debugging")]
+ [SerializeField]
+ private float _currentTime;
+
+ [SerializeField]
+ private float _currentSplineDistance;
+
+ #pragma warning restore 0649
+
+ private bool _isMovingForward;
+ private Vector3 _currentPosition;
+ private Quaternion _currentRotation;
+
+ private void Start()
+ {
+ _isMovingForward = true;
+
+ if (_spline == null)
+ {
+ Debug.LogError("Please assign a spline to this SplineWalker.", this);
+ enabled = false;
+ }
+ else
+ {
+ _startingSplineDistance = Mathf.Clamp(_startingSplineDistance, 0, _spline.TotalLength);
+ _currentSplineDistance = _startingSplineDistance;
+ _currentTime = _currentSplineDistance / _spline.TotalLength;
+ _currentPosition = _spline.GetPosition(_currentSplineDistance);
+ _currentRotation = _spline.GetRotation(_currentSplineDistance);
+
+ transform.SetPositionAndRotation(_currentPosition, _currentRotation);
+ }
+ }
+
+ private void Update()
+ {
+ SetTargetPositionAndRotation();
+
+ var lerpPosition = Vector3.Lerp(transform.position, _currentPosition, Time.deltaTime * 25f);
+ var lerpRotation = Quaternion.Lerp(transform.rotation, _currentRotation, Time.deltaTime * 25f);
+
+ transform.SetPositionAndRotation(lerpPosition, lerpRotation);
+ }
+
+ private void SetTargetPositionAndRotation()
+ {
+ if (_moveType == MoveType.UseConstantSpeed)
+ {
+ var length = _spline.TotalLength;
+
+ switch (_loopType)
+ {
+ case LoopType.Clamp:
+ _currentSplineDistance += Time.unscaledDeltaTime * _speed;
+ _currentSplineDistance = Mathf.Clamp(_currentSplineDistance, 0, length);
+ break;
+ case LoopType.Loop:
+ _currentSplineDistance += Time.unscaledDeltaTime * _speed;
+ _currentSplineDistance = Mathf.Repeat(_currentSplineDistance, length);
+ break;
+ case LoopType.PingPong:
+ if (_isMovingForward)
+ {
+ _currentSplineDistance += Time.unscaledDeltaTime * _speed;
+ }
+ else if (!_isMovingForward)
+ {
+ _currentSplineDistance -= Time.unscaledDeltaTime * _speed;
+ }
+
+ _currentSplineDistance = Mathf.Clamp(_currentSplineDistance, 0, length);
+ if (_currentSplineDistance <= 0 && !_isMovingForward ||
+ _currentSplineDistance >= length && _isMovingForward)
+ {
+ _isMovingForward = !_isMovingForward;
+ }
+
+ break;
+ }
+
+ _currentPosition = _spline.GetPosition(_currentSplineDistance);
+ _currentRotation = _spline.GetRotation(_currentSplineDistance);
+ }
+ else if(_moveType == MoveType.UseFixedDuration)
+ {
+ switch (_loopType)
+ {
+ case LoopType.Clamp:
+ _currentTime += Time.unscaledDeltaTime;
+ _currentTime = Mathf.Clamp(_currentTime, 0, _duration);
+ break;
+ case LoopType.Loop:
+ _currentTime += Time.unscaledDeltaTime;
+ _currentTime = Mathf.Repeat(_currentTime, _duration);
+ break;
+ case LoopType.PingPong:
+ if (_isMovingForward)
+ {
+ _currentTime += Time.unscaledDeltaTime;
+ }
+ else if (!_isMovingForward)
+ {
+ _currentTime -= Time.unscaledDeltaTime;
+ }
+
+ _currentTime = Mathf.Clamp(_currentTime, 0, _duration);
+ if (_currentTime <= 0 && !_isMovingForward ||
+ _currentTime >= _duration && _isMovingForward)
+ {
+ _isMovingForward = !_isMovingForward;
+ }
+
+ break;
+ }
+
+ var progress = _currentTime / _duration;
+
+ _currentPosition = _spline.GetNormalizedPosition(progress);
+ _currentRotation = _spline.GetNormalizedRotation(progress);
+ }
+ }
+
+ #if UNITY_EDITOR
+
+ private void OnValidate()
+ {
+ if (_spline == null)
+ {
+ return;
+ }
+
+ _startingSplineDistance = Mathf.Clamp(_startingSplineDistance, 0, _spline.TotalLength);
+ _currentSplineDistance = _startingSplineDistance;
+ _currentTime = _currentSplineDistance / _spline.TotalLength;
+ _currentPosition = _spline.GetPosition(_currentSplineDistance);
+ _currentRotation = _spline.GetRotation(_currentSplineDistance);
+
+ transform.SetPositionAndRotation(_currentPosition, _currentRotation);
+ }
+
+ #endif
+ }
+}
diff --git a/Curves/Scripts/Components/SplineWalker.cs.meta b/Curves/Scripts/Components/SplineWalker.cs.meta
new file mode 100644
index 0000000..5be9506
--- /dev/null
+++ b/Curves/Scripts/Components/SplineWalker.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e990ee1fd69e3a040a8ec8a4d9349c6b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: d2f3876f83c47304fb039eb650289db8, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Core.meta b/Curves/Scripts/Core.meta
new file mode 100644
index 0000000..bab95fc
--- /dev/null
+++ b/Curves/Scripts/Core.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9575181e155d52b4aac13d016cdbeb70
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Core/Bezier3DCurve.cs b/Curves/Scripts/Core/Bezier3DCurve.cs
new file mode 100644
index 0000000..6dcc1c8
--- /dev/null
+++ b/Curves/Scripts/Core/Bezier3DCurve.cs
@@ -0,0 +1,307 @@
+using System;
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// Immutable Bezier curve between two points.
+ ///
+ [System.Serializable]
+ public class Bezier3DCurve
+ {
+ ///
+ /// Start point.
+ ///
+ public Vector3 StartPoint
+ {
+ get { return _startPoint; }
+ }
+
+ ///
+ /// First handle. Local to start point.
+ ///
+ public Vector3 FirstHandle
+ {
+ get { return _firstHandle; }
+ }
+
+ ///
+ /// Second handle. Local to end point.
+ ///
+ public Vector3 SecondHandle
+ {
+ get { return _secondHandle; }
+ }
+
+ ///
+ /// End point
+ ///
+ public Vector3 EndPoint
+ {
+ get { return _endPoint; }
+ }
+
+ ///
+ /// Total length of the curve
+ /// .
+ public float Length
+ {
+ get { return _length; }
+ }
+
+ ///
+ /// True if the curve is defined as a straight line.
+ ///
+ public bool IsLinear
+ {
+ get { return _isLinear; }
+ }
+
+ public AnimationCurve DistanceCache
+ {
+ get { return _distanceCache; }
+ }
+
+ [SerializeField]
+ private Vector3 _startPoint;
+
+ [SerializeField]
+ private Vector3 _firstHandle;
+
+ [SerializeField]
+ private Vector3 _startHandleWorldPosition;
+
+ [SerializeField]
+ private Vector3 _endHandleWorldPosition;
+
+ [SerializeField]
+ private Vector3 _secondHandle;
+
+ [SerializeField]
+ private AnimationCurve _distanceCache;
+
+ [SerializeField]
+ private Vector3 _endPoint;
+
+ [SerializeField]
+ private bool _isLinear;
+
+ [SerializeField]
+ private float _length;
+
+ [SerializeField]
+ private Vector3AnimationCurve _tangentCache;
+
+ /// Constructor
+ /// Start point
+ /// First handle. Local to start point
+ /// Second handle. Local to end point
+ /// End point
+ public Bezier3DCurve(Vector3 startPoint, Vector3 firstHandle, Vector3 secondHandle, Vector3 endPoint, int steps)
+ {
+ _startPoint = startPoint;
+ _firstHandle = firstHandle;
+ _secondHandle = secondHandle;
+ _endPoint = endPoint;
+ _startHandleWorldPosition = startPoint + firstHandle;
+ _endHandleWorldPosition = endPoint + secondHandle;
+ _isLinear = Math.Abs(firstHandle.sqrMagnitude) < 0.00001f &&
+ Math.Abs(secondHandle.sqrMagnitude) < 0.00001f;
+
+ _distanceCache = GetDistanceCache(
+ startPoint,
+ startPoint + firstHandle,
+ secondHandle + endPoint,
+ endPoint,
+ steps);
+
+ _tangentCache = GetTangentCache(
+ startPoint,
+ startPoint + firstHandle,
+ secondHandle + endPoint,
+ endPoint,
+ steps);
+
+ _length = _distanceCache.keys[_distanceCache.keys.Length - 1].time;
+ }
+
+ #region Public methods
+
+ public Vector3 GetPoint(float t)
+ {
+ return GetPoint(
+ _startPoint,
+ _startHandleWorldPosition,
+ _endHandleWorldPosition,
+ _endPoint,
+ t);
+ }
+
+ public void GetPoint(float t, out Vector3 point)
+ {
+ GetPoint(
+ ref _startPoint,
+ ref _startHandleWorldPosition,
+ ref _endHandleWorldPosition,
+ ref _endPoint,
+ t,
+ out point);
+ }
+
+ public void GetForward(float t, out Vector3 forward)
+ {
+ GetForward(
+ ref _startPoint,
+ ref _startHandleWorldPosition,
+ ref _endHandleWorldPosition,
+ ref _endPoint,
+ t,
+ out forward);
+ }
+
+
+ public Vector3 GetForward(float t)
+ {
+ return GetForward(
+ _startPoint,
+ _startHandleWorldPosition,
+ _endHandleWorldPosition,
+ _endPoint,
+ t);
+ }
+
+ public Vector3 GetForwardFast(float t)
+ {
+ return _tangentCache.Evaluate(t);
+ }
+
+ public float ConvertDistanceToTime(float distance)
+ {
+ return _distanceCache.Evaluate(distance);
+ }
+
+ #endregion
+
+ #region Private methods
+
+ private static Vector3AnimationCurve GetTangentCache(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, int steps)
+ {
+ var curve = new Vector3AnimationCurve(); //time = distance, value = time
+ var delta = 1f / steps;
+ for (var i = 0; i < steps + 1; i++)
+ {
+ curve.AddKey(
+ delta * i,
+ GetForward(
+ p0,
+ p1,
+ p2,
+ p3,
+ delta * i)
+ .normalized);
+ }
+
+ return curve;
+ }
+
+ private static AnimationCurve GetDistanceCache(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, int steps)
+ {
+ var curve = new AnimationCurve(); //time = distance, value = time
+ var prevPos = Vector3.zero;
+ var totalLength = 0f;
+ for (var i = 0; i <= steps; i++)
+ {
+ //Normalize i
+ var t = (float)i / (float)steps;
+
+ //Get position from t
+ var newPos = GetPoint(
+ p0,
+ p1,
+ p2,
+ p3,
+ t);
+
+ //First step
+ if (i == 0)
+ {
+ //Add point at (0,0)
+ prevPos = GetPoint(
+ p0,
+ p1,
+ p2,
+ p3,
+ 0);
+ curve.AddKey(0, 0);
+ }
+ //Per step
+ else
+ {
+ //Get distance from previous point
+ var segmentLength = Vector3.Distance(prevPos, newPos);
+
+ //Accumulate total distance traveled
+ totalLength += segmentLength;
+
+ //Save current position for next iteration
+ prevPos = newPos;
+
+ //Cache data
+ curve.AddKey(totalLength, t);
+ }
+ }
+
+ return curve;
+ }
+
+ public static Vector3 GetPoint(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float t)
+ {
+ t = Mathf.Clamp01(t);
+ var oneMinusT = 1f - t;
+ return
+ oneMinusT * oneMinusT * oneMinusT * a +
+ 3f * oneMinusT * oneMinusT * t * b +
+ 3f * oneMinusT * t * t * c +
+ t * t * t * d;
+ }
+
+ private static Vector3 GetForward(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float t)
+ {
+ //Also known as first derivative
+ t = Mathf.Clamp01(t);
+ var oneMinusT = 1f - t;
+ return
+ 3f * oneMinusT * oneMinusT * (b - a) +
+ 6f * oneMinusT * t * (c - b) +
+ 3f * t * t * (d - c);
+ }
+
+ private static void GetForward(ref Vector3 a, ref Vector3 b, ref Vector3 c, ref Vector3 d, float t, out Vector3 result)
+ {
+ //Also known as first derivative
+ var oneMinusT = 1f - t;
+ var baScale = 3f * oneMinusT * oneMinusT;
+ var cbScale = 6f * oneMinusT * t;
+ var dcScale = 3f * t * t;
+
+ result.x = baScale * (b.x - a.x) + cbScale * (c.x - b.x) + dcScale * (d.x - c.x);
+ result.y = baScale * (b.y - a.y) + cbScale * (c.y - b.y) + dcScale * (d.y - c.y);
+ result.z = baScale * (b.z - a.z) + cbScale * (c.z - b.z) + dcScale * (d.z - c.z);
+ }
+
+ private static void GetPoint(ref Vector3 a, ref Vector3 b, ref Vector3 c, ref Vector3 d, float t, out Vector3 result)
+ {
+ var oneMinusT = 1f - t;
+ var aScale = oneMinusT * oneMinusT * oneMinusT;
+ var bScale = 3f * oneMinusT * oneMinusT * t;
+ var cScale = 3f * oneMinusT * t * t;
+ var dScale = t * t * t;
+
+ result.x = aScale * a.x + bScale * b.x + cScale * c.x + dScale * d.x;
+ result.y = aScale * a.y + bScale * b.y + cScale * c.y + dScale * d.y;
+ result.z = aScale * a.z + bScale * b.z + cScale * c.z + dScale * d.z;
+ }
+
+ #endregion
+ }
+}
diff --git a/Curves/Scripts/Core/Bezier3DCurve.cs.meta b/Curves/Scripts/Core/Bezier3DCurve.cs.meta
new file mode 100644
index 0000000..c42037d
--- /dev/null
+++ b/Curves/Scripts/Core/Bezier3DCurve.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 74e4d65f14457b345a17854149e24d40
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Core/ConstantAnimationCurve.cs b/Curves/Scripts/Core/ConstantAnimationCurve.cs
new file mode 100644
index 0000000..aca2bfc
--- /dev/null
+++ b/Curves/Scripts/Core/ConstantAnimationCurve.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// Similar to AnimationCurve, except all values are constant. No smoothing applied between keys.
+ ///
+ [System.Serializable]
+ public class ConstantAnimationCurve
+ {
+ ///
+ /// The number of keys in the curve.
+ ///
+ public int Length
+ {
+ get { return _time.Count; }
+ }
+
+ [SerializeField]
+ List _time;
+
+ [SerializeField]
+ List _value;
+
+ public ConstantAnimationCurve()
+ {
+ _value = new List();
+ _time = new List();
+ }
+
+ ///
+ /// Returns the float value at in the curve.
+ ///
+ ///
+ ///
+ public float Evaluate(float time)
+ {
+ if (Length == 0)
+ {
+ return 0;
+ }
+
+ var returnValue = GetKeyValue(0);
+ for (var i = 0; i < _time.Count; i++)
+ {
+ if (_time[i] <= time)
+ {
+ returnValue = _value[i];
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return returnValue;
+ }
+
+ ///
+ /// Adds float at on the curve.
+ ///
+ ///
+ ///
+ public void AddKey(float time, float value)
+ {
+ for (var i = 0; i < _time.Count; i++)
+ {
+ if (_time[i] > time)
+ {
+ _time.Insert(i, time);
+ _value.Insert(i, value);
+ return;
+ }
+ else if (_time[i] == time)
+ {
+ _time[i] = time;
+ _value[i] = value;
+ return;
+ }
+ }
+
+ _time.Add(time);
+ _value.Add(value);
+ }
+
+ ///
+ /// Gets the last value of the curve.
+ ///
+ public float EvaluateEnd()
+ {
+ return _value[_value.Count - 1];
+ }
+
+ ///
+ /// Returns the time value at the position in the curve.
+ ///
+ ///
+ ///
+ public float GetKeyTime(int keyIndex)
+ {
+ return _time[keyIndex];
+ }
+
+ ///
+ /// Returns the float value at the position in the curve.
+ ///
+ ///
+ ///
+ public float GetKeyValue(int keyIndex)
+ {
+ return _value[keyIndex];
+ }
+ }
+}
diff --git a/Curves/Scripts/Core/ConstantAnimationCurve.cs.meta b/Curves/Scripts/Core/ConstantAnimationCurve.cs.meta
new file mode 100644
index 0000000..b5799a2
--- /dev/null
+++ b/Curves/Scripts/Core/ConstantAnimationCurve.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 68c5f9e36dc199e4f9c5285c05805235
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Core/ExtendedAnimationCurves.cs b/Curves/Scripts/Core/ExtendedAnimationCurves.cs
new file mode 100644
index 0000000..d08e10e
--- /dev/null
+++ b/Curves/Scripts/Core/ExtendedAnimationCurves.cs
@@ -0,0 +1,39 @@
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// Class Extensions
+ ///
+ public static class ExtendedAnimationCurves
+ {
+ public static void Serialize(this AnimationCurve anim, out float[] times, out float[] values)
+ {
+ times = new float[anim.length];
+ values = new float[anim.length];
+ for (var i = 0; i < anim.length; i++)
+ {
+ times[i] = anim.keys[i].time;
+ values[i] = anim.keys[i].value;
+ }
+ }
+
+ public static AnimationCurve Deserialize(float[] times, float[] values)
+ {
+ var anim = new AnimationCurve();
+ if (times.Length != values.Length)
+ {
+ Debug.LogWarning("Input data lengths do not match");
+ }
+ else
+ {
+ for (var i = 0; i < times.Length; i++)
+ {
+ anim.AddKey(new Keyframe(times[i], values[i]));
+ }
+ }
+
+ return anim;
+ }
+ }
+}
diff --git a/Curves/Scripts/Core/ExtendedAnimationCurves.cs.meta b/Curves/Scripts/Core/ExtendedAnimationCurves.cs.meta
new file mode 100644
index 0000000..ce9d80b
--- /dev/null
+++ b/Curves/Scripts/Core/ExtendedAnimationCurves.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e28b7486b5d05454a94c012c6496ffb5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Core/Knot.cs b/Curves/Scripts/Core/Knot.cs
new file mode 100644
index 0000000..19f08bd
--- /dev/null
+++ b/Curves/Scripts/Core/Knot.cs
@@ -0,0 +1,66 @@
+using System;
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// A user-configurable control point that can be altered to update a curve.
+ ///
+ public struct Knot
+ {
+ ///
+ /// Returns true if a custom rotation has been specified, otherwise false.
+ ///
+ public bool IsUsingRotation => rotation != null;
+
+ ///
+ /// Returns true if a handles are auto-adjusted, otherwise false.
+ ///
+ public bool IsUsingAutoHandles => Math.Abs(auto) > .00001f;
+
+ ///
+ /// Position of the knot local to spline.
+ ///
+ public Vector3 position;
+
+ ///
+ /// Left handle position local to knot position.
+ ///
+ public Vector3 handleIn;
+
+ ///
+ /// Right handle position local to knot position.
+ ///
+ public Vector3 handleOut;
+
+ ///
+ /// Any value above 0 will result in an automatically configured knot (ignoring handle inputs).
+ ///
+ public float auto;
+
+ ///
+ /// The rotation to influence the any point along the curve before or after this knot.
+ ///
+ public Quaternion? rotation;
+
+ /// Constructor
+ /// Position of the knot local to spline
+ /// Left handle position local to knot position
+ /// Right handle position local to knot position
+ /// Any value above 0 will result in an automatically configured knot (ignoring handle inputs)
+ /// The rotation to influence the any point along the curve before or after this knot
+ public Knot(
+ Vector3 position,
+ Vector3 handleIn,
+ Vector3 handleOut,
+ float automatic = 0f,
+ Quaternion? rotation = null)
+ {
+ this.position = position;
+ this.handleIn = handleIn;
+ this.handleOut = handleOut;
+ auto = automatic;
+ this.rotation = rotation;
+ }
+ }
+}
diff --git a/Curves/Scripts/Core/Knot.cs.meta b/Curves/Scripts/Core/Knot.cs.meta
new file mode 100644
index 0000000..3043c78
--- /dev/null
+++ b/Curves/Scripts/Core/Knot.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 84a74be362038eb448d2ae528e35f20e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Core/NullableQuaternion.cs b/Curves/Scripts/Core/NullableQuaternion.cs
new file mode 100644
index 0000000..39fd0f8
--- /dev/null
+++ b/Curves/Scripts/Core/NullableQuaternion.cs
@@ -0,0 +1,57 @@
+using System;
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// A serializable version of a nullable-Quaternion.
+ ///
+ [Serializable]
+ public struct NullableQuaternion
+ {
+ ///
+ /// Returns the value.
+ ///
+ public Quaternion Value
+ {
+ get { return rotation; }
+ }
+
+ ///
+ /// Returns the value if present, otherwise null.
+ ///
+ public Quaternion? NullableValue
+ {
+ get { return hasValue ? (Quaternion?)rotation : null; }
+ }
+
+ ///
+ /// Returns true if a value is present, otherwise false.
+ ///
+ public bool HasValue
+ {
+ get { return hasValue; }
+ }
+
+ [SerializeField]
+ private Quaternion rotation;
+
+ [SerializeField]
+ private bool hasValue;
+
+ public NullableQuaternion(Quaternion? rot)
+ {
+ rotation = rot.HasValue ? rot.Value : Quaternion.identity;
+ hasValue = rot.HasValue;
+ }
+
+ ///
+ /// User-defined conversion from nullable type to NullableQuaternion
+ ///
+ ///
+ public static implicit operator NullableQuaternion(Quaternion? r)
+ {
+ return new NullableQuaternion(r);
+ }
+ }
+}
diff --git a/Curves/Scripts/Core/NullableQuaternion.cs.meta b/Curves/Scripts/Core/NullableQuaternion.cs.meta
new file mode 100644
index 0000000..310bde2
--- /dev/null
+++ b/Curves/Scripts/Core/NullableQuaternion.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 630cdd6c9d8b7fe4c833d5140f65e681
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Core/QuaternionAnimationCurve.cs b/Curves/Scripts/Core/QuaternionAnimationCurve.cs
new file mode 100644
index 0000000..c5abdd1
--- /dev/null
+++ b/Curves/Scripts/Core/QuaternionAnimationCurve.cs
@@ -0,0 +1,132 @@
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// Animation curve which stores quaternions, and can evaluate smoothed values in between keyframes.
+ ///
+ [System.Serializable]
+ public class QuaternionAnimationCurve
+ {
+ [System.Serializable]
+ public class Serializable
+ {
+ public float[] xT;
+ public float[] xV;
+ public float[] yT;
+ public float[] yV;
+ public float[] zT;
+ public float[] zV;
+ public float[] wT;
+ public float[] wV;
+
+ public Serializable(QuaternionAnimationCurve curve)
+ {
+ curve.xQ.Serialize(out xT, out xV);
+ curve.yQ.Serialize(out yT, out yV);
+ curve.zQ.Serialize(out zT, out zV);
+ curve.wQ.Serialize(out wT, out wV);
+ }
+ }
+
+ ///
+ /// The number of keys in the curve.
+ ///
+ public int Length
+ {
+ get { return xQ.length; }
+ }
+
+ [SerializeField]
+ private AnimationCurve xQ;
+
+ [SerializeField]
+ private AnimationCurve yQ;
+
+ [SerializeField]
+ private AnimationCurve zQ;
+
+ [SerializeField]
+ private AnimationCurve wQ;
+
+ public QuaternionAnimationCurve()
+ {
+ wQ = new AnimationCurve();
+ zQ = new AnimationCurve();
+ yQ = new AnimationCurve();
+ xQ = new AnimationCurve();
+ }
+
+ public QuaternionAnimationCurve(Serializable serialized)
+ {
+ wQ = new AnimationCurve();
+ zQ = new AnimationCurve();
+ yQ = new AnimationCurve();
+ xQ = new AnimationCurve();
+
+ xQ = ExtendedAnimationCurves.Deserialize(serialized.xT, serialized.xV);
+ yQ = ExtendedAnimationCurves.Deserialize(serialized.yT, serialized.yV);
+ zQ = ExtendedAnimationCurves.Deserialize(serialized.zT, serialized.zV);
+ wQ = ExtendedAnimationCurves.Deserialize(serialized.wT, serialized.wV);
+ }
+
+ ///
+ /// Returns the at in the curve.
+ ///
+ ///
+ ///
+ public Quaternion Evaluate(float time)
+ {
+ return new Quaternion(
+ xQ.Evaluate(time),
+ yQ.Evaluate(time),
+ zQ.Evaluate(time),
+ wQ.Evaluate(time));
+ }
+
+ ///
+ /// Adds at on the curve.
+ ///
+ ///
+ ///
+ public void AddKey(float time, Quaternion value)
+ {
+ xQ.AddKey(time, value.x);
+ yQ.AddKey(time, value.y);
+ zQ.AddKey(time, value.z);
+ wQ.AddKey(time, value.w);
+ }
+
+ ///
+ /// Gets the rotation of the last key.
+ ///
+ public Quaternion EvaluateEnd()
+ {
+ return GetKeyValue(xQ.length - 1);
+ }
+
+ ///
+ /// Returns the time value at the position in the curve.
+ ///
+ ///
+ ///
+ public float GetKeyTime(int keyIndex)
+ {
+ return wQ.keys[keyIndex].time;
+ }
+
+ ///
+ /// Returns the value at the position in the curve.
+ ///
+ ///
+ ///
+ public Quaternion GetKeyValue(int keyIndex)
+ {
+ return new Quaternion(
+ xQ.keys[keyIndex].value,
+ yQ.keys[keyIndex].value,
+ zQ.keys[keyIndex].value,
+ wQ.keys[keyIndex].value);
+ }
+ }
+}
diff --git a/Curves/Scripts/Core/QuaternionAnimationCurve.cs.meta b/Curves/Scripts/Core/QuaternionAnimationCurve.cs.meta
new file mode 100644
index 0000000..f906b46
--- /dev/null
+++ b/Curves/Scripts/Core/QuaternionAnimationCurve.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fe2316d4eb4973c4d8ad11fd7e59465c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Core/Vector3AnimationCurve.cs b/Curves/Scripts/Core/Vector3AnimationCurve.cs
new file mode 100644
index 0000000..27e6b58
--- /dev/null
+++ b/Curves/Scripts/Core/Vector3AnimationCurve.cs
@@ -0,0 +1,114 @@
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// Animation curve which stores , and can evaluate smoothed values in between keyframes.
+ ///
+ [System.Serializable]
+ public class Vector3AnimationCurve
+ {
+ [System.Serializable]
+ public class Serializable
+ {
+ public float[] xT;
+ public float[] xV;
+ public float[] yT;
+ public float[] yV;
+ public float[] zT;
+ public float[] zV;
+
+ public Serializable(Vector3AnimationCurve curve)
+ {
+ curve.xV.Serialize(out xT, out xV);
+ curve.yV.Serialize(out yT, out yV);
+ curve.zV.Serialize(out zT, out zV);
+ }
+ }
+
+ ///
+ /// The number of keys in the curve.
+ ///
+ public int Length
+ {
+ get { return xV.length; }
+ }
+
+ [SerializeField]
+ private AnimationCurve xV;
+
+ [SerializeField]
+ private AnimationCurve yV;
+
+ [SerializeField]
+ private AnimationCurve zV;
+
+ public Vector3AnimationCurve()
+ {
+ xV = new AnimationCurve();
+ yV = new AnimationCurve();
+ zV = new AnimationCurve();
+ }
+
+ public Vector3AnimationCurve(Serializable serialized)
+ {
+ xV = new AnimationCurve();
+ yV = new AnimationCurve();
+ zV = new AnimationCurve();
+
+ xV = ExtendedAnimationCurves.Deserialize(serialized.xT, serialized.xV);
+ yV = ExtendedAnimationCurves.Deserialize(serialized.yT, serialized.yV);
+ zV = ExtendedAnimationCurves.Deserialize(serialized.zT, serialized.zV);
+ }
+
+ ///
+ /// Returns the at in the curve.
+ ///
+ ///
+ ///
+ public Vector3 Evaluate(float time)
+ {
+ return new Vector3(xV.Evaluate(time), yV.Evaluate(time), zV.Evaluate(time));
+ }
+
+ ///
+ /// Adds at on the curve.
+ ///
+ ///
+ ///
+ public void AddKey(float time, Vector3 value)
+ {
+ xV.AddKey(time, value.x);
+ yV.AddKey(time, value.y);
+ zV.AddKey(time, value.z);
+ }
+
+ ///
+ /// Gets the of the last key.
+ ///
+ public Vector3 EvaluateEnd()
+ {
+ return GetKeyValue(xV.length - 1);
+ }
+
+ ///
+ /// Returns the time value at the position in the curve.
+ ///
+ ///
+ ///
+ public float GetKeyTime(int keyIndex)
+ {
+ return xV.keys[keyIndex].time;
+ }
+
+ ///
+ /// Returns the value at the position in the curve.
+ ///
+ ///
+ ///
+ public Vector3 GetKeyValue(int keyIndex)
+ {
+ return new Vector3(xV.keys[keyIndex].value, yV.keys[keyIndex].value, zV.keys[keyIndex].value);
+ }
+ }
+}
diff --git a/Curves/Scripts/Core/Vector3AnimationCurve.cs.meta b/Curves/Scripts/Core/Vector3AnimationCurve.cs.meta
new file mode 100644
index 0000000..6b434cd
--- /dev/null
+++ b/Curves/Scripts/Core/Vector3AnimationCurve.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c519766a13b361841aac0125bb4a7420
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor.meta b/Curves/Scripts/Editor.meta
new file mode 100644
index 0000000..b6ddf3b
--- /dev/null
+++ b/Curves/Scripts/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: de73badbd2f40df47b9eddaae710f038
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/CurveEditorState.cs b/Curves/Scripts/Editor/CurveEditorState.cs
new file mode 100644
index 0000000..ee604d3
--- /dev/null
+++ b/Curves/Scripts/Editor/CurveEditorState.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using UnityEditor;
+
+namespace JCMG.Curves.Editor
+{
+ ///
+ /// Shared state for modifying curves in the Unity Editor.
+ ///
+ internal static class CurveEditorState
+ {
+ public static bool HasKnotSelected => SelectedKnotIndex != -1;
+
+ public static bool HasSingleKnotSelected => SelectedKnots.Count == 1;
+
+ public static bool HasMultipleKnotsSelected => SelectedKnots.Count > 1;
+
+ public static int SelectedKnotIndex { get; set; }
+
+ public static List SelectedKnots { get; set; }
+
+ static CurveEditorState()
+ {
+ SelectedKnots = new List();
+ }
+
+ public static bool ValidateSelectedKnotIsValid(IReadOnly3DSplineData splineData)
+ {
+ return SelectedKnotIndex > splineData.CurveCount;
+ }
+
+ public static void ClearKnotSelection()
+ {
+ SelectKnot(-1, false);
+ }
+
+ public static void SelectKnot(int i, bool add)
+ {
+ SelectedKnotIndex = i;
+ if (i == -1)
+ {
+ SelectedKnots.Clear();
+ Tools.hidden = false;
+ }
+ else
+ {
+ Tools.hidden = true;
+ if (add)
+ {
+ if (SelectedKnots.Contains(i))
+ {
+ SelectedKnots.Remove(i);
+ if (SelectedKnots.Count == 0)
+ {
+ SelectedKnotIndex = -1;
+ Tools.hidden = false;
+ }
+ else
+ {
+ SelectedKnotIndex = SelectedKnots[SelectedKnots.Count - 1];
+ }
+ }
+ else
+ {
+ SelectedKnots.Add(i);
+
+ SelectedKnotIndex = i;
+ }
+ }
+ else
+ {
+ SelectedKnots.Clear();
+ SelectedKnots.Add(i);
+
+ SelectedKnotIndex = i;
+ }
+ }
+ }
+
+ public static void Reset()
+ {
+ ClearKnotSelection();
+
+ SelectedKnots.Clear();
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/CurveEditorState.cs.meta b/Curves/Scripts/Editor/CurveEditorState.cs.meta
new file mode 100644
index 0000000..9a7553d
--- /dev/null
+++ b/Curves/Scripts/Editor/CurveEditorState.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c51d16d3870c27c46b9f0a84fd34a681
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/CurveEditorStyles.cs b/Curves/Scripts/Editor/CurveEditorStyles.cs
new file mode 100644
index 0000000..85b08c0
--- /dev/null
+++ b/Curves/Scripts/Editor/CurveEditorStyles.cs
@@ -0,0 +1,59 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ ///
+ /// GUI constants and styles for the Unity Editor.
+ ///
+ internal static class CurveEditorStyles
+ {
+ public static GUIStyle HeaderStyle
+ {
+ get
+ {
+ if(_headerStyle == null)
+ {
+ _headerStyle = new GUIStyle(EditorStyles.boldLabel);
+ _headerStyle.padding.right += 4;
+ _headerStyle.normal.textColor = TextColor;
+ _headerStyle.fontSize += 1;
+ }
+
+ return _headerStyle;
+ }
+ }
+
+ public static GUIStyle LabelStyle
+ {
+ get
+ {
+ if(_labelStyle == null)
+ {
+ _labelStyle = new GUIStyle(EditorStyles.label);
+ _labelStyle.padding.right += 4;
+ _labelStyle.normal.textColor = TextColor;
+ }
+
+ return _labelStyle;
+ }
+ }
+
+ public static Color TextColor
+ {
+ get
+ {
+ if (_textColor == null)
+ {
+ _textColor = new Color(0.7f, 0.7f, 0.7f);
+ }
+
+ return _textColor.Value;
+ }
+ }
+
+ private static GUIStyle _headerStyle;
+ private static GUIStyle _labelStyle;
+ private static Color? _textColor;
+ }
+}
diff --git a/Curves/Scripts/Editor/CurveEditorStyles.cs.meta b/Curves/Scripts/Editor/CurveEditorStyles.cs.meta
new file mode 100644
index 0000000..1be97a7
--- /dev/null
+++ b/Curves/Scripts/Editor/CurveEditorStyles.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 19af2718e33854d43bda9ffa58e6ecb8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/CurvePreferences.cs b/Curves/Scripts/Editor/CurvePreferences.cs
new file mode 100644
index 0000000..c38f155
--- /dev/null
+++ b/Curves/Scripts/Editor/CurvePreferences.cs
@@ -0,0 +1,152 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ ///
+ /// An editor class for managing project and user preferences for the Curves library.
+ ///
+ public static class CurvePreferences
+ {
+ ///
+ /// Returns true if debug features should be enabled, otherwise false.
+ ///
+ public static bool IsDebugEnabled
+ {
+ get { return GetBoolPref(ENABLE_DEBUG_PREF, ENABLE_DEBUG_DEFAULT); }
+ set { EditorPrefs.SetBool(ENABLE_DEBUG_PREF, value); }
+ }
+
+ ///
+ /// Returns true if rotation visualization info should be enabled, otherwise false.
+ ///
+ public static bool ShouldVisualizeRotation
+ {
+ get { return GetBoolPref(SHOW_ROTATION_PREF, SHOW_ROTATION_DEFAULT); }
+ set { EditorPrefs.SetBool(SHOW_ROTATION_PREF, value); }
+ }
+
+ public static bool ShouldMirrorHandleMovement
+ {
+ get { return GetBoolPref(MIRROR_HANDLE_MOVEMENT_PREF, MIRROR_HANDLE_MOVEMENT_DEFAULT); }
+ set { EditorPrefs.SetBool(MIRROR_HANDLE_MOVEMENT_PREF, value); }
+ }
+
+ // UI
+ private const string PREFERENCES_TITLE_PATH = "Preferences/JCMG Curves";
+ private const string USER_PREFERENCES_HEADER = "User Preferences";
+
+ private static readonly GUILayoutOption MAX_WIDTH;
+
+ // Searchable Fields
+ private static readonly string[] KEYWORDS =
+ {
+ "Curve",
+ "Curves"
+ };
+
+ // User Editor Preferences
+ private const string SHOW_ROTATION_PREF = "JCMG.Curves.ShowRotationVisualization";
+ private const string ENABLE_DEBUG_PREF = "JCMG.Curves.EnableDebug";
+ private const string MIRROR_HANDLE_MOVEMENT_PREF = "JCMG.Curves.MirrorHandleMovement";
+
+ private const bool SHOW_ROTATION_DEFAULT = true;
+ private const bool ENABLE_DEBUG_DEFAULT = true;
+ private const bool MIRROR_HANDLE_MOVEMENT_DEFAULT = true;
+
+ static CurvePreferences()
+ {
+ MAX_WIDTH = GUILayout.MaxWidth(175f);
+ }
+
+ [SettingsProvider]
+ private static SettingsProvider CreatePersonalPreferenceSettingsProvider()
+ {
+ return new SettingsProvider(PREFERENCES_TITLE_PATH, SettingsScope.User)
+ {
+ guiHandler = DrawPersonalPrefsGUI, keywords = KEYWORDS
+ };
+ }
+
+ private static void DrawAllGUI()
+ {
+ DrawPersonalPrefsGUI();
+ }
+
+ private static void DrawPersonalPrefsGUI(string value = "")
+ {
+ EditorGUILayout.LabelField(USER_PREFERENCES_HEADER, EditorStyles.boldLabel);
+
+ // Enable Orientation Visualization
+ EditorGUILayout.HelpBox(
+ "This will enable visualization of a point's rotation along the curve, " +
+ "with lines drawn to show its local forward, up, and right vectors.",
+ MessageType.Info);
+
+ GUI.changed = false;
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ EditorGUILayout.LabelField("Should Visualize Rotation", MAX_WIDTH);
+ var drawEventPref = EditorGUILayout.Toggle(ShouldVisualizeRotation);
+ if (GUI.changed)
+ {
+ ShouldVisualizeRotation = drawEventPref;
+ SceneView.RepaintAll();
+ }
+ }
+
+ // Enable Debugging
+ EditorGUILayout.Space();
+ EditorGUILayout.HelpBox(
+ "This will enable debug features for troubleshooting purposes",
+ MessageType.Info);
+
+ GUI.changed = false;
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ EditorGUILayout.LabelField("Enable Debug", MAX_WIDTH);
+ var enableDebugPref = EditorGUILayout.Toggle( IsDebugEnabled, MAX_WIDTH);
+ if (GUI.changed)
+ {
+ IsDebugEnabled = enableDebugPref;
+ SceneView.RepaintAll();
+ }
+ }
+
+ // Enable Mirror Handle Movement
+ EditorGUILayout.Space();
+ EditorGUILayout.HelpBox(
+ "When enabled, moving a handle will cause the other handle to copy its " +
+ "movement in the opposite direction.",
+ MessageType.Info);
+
+ GUI.changed = false;
+ using (new EditorGUILayout.HorizontalScope())
+ {
+ EditorGUILayout.LabelField("Mirror Handle Movement", MAX_WIDTH);
+ var mirrorHandleMovementPref = EditorGUILayout.Toggle(ShouldMirrorHandleMovement, MAX_WIDTH);
+ if (GUI.changed)
+ {
+ ShouldMirrorHandleMovement = mirrorHandleMovementPref;
+ SceneView.RepaintAll();
+ }
+ }
+ }
+
+ ///
+ /// Returns the current bool preference; if none exists, the default is set and returned.
+ ///
+ ///
+ ///
+ ///
+ private static bool GetBoolPref(string key, bool defaultValue)
+ {
+ if (!EditorPrefs.HasKey(key))
+ {
+ EditorPrefs.SetBool(key, defaultValue);
+ }
+
+ return EditorPrefs.GetBool(key);
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/CurvePreferences.cs.meta b/Curves/Scripts/Editor/CurvePreferences.cs.meta
new file mode 100644
index 0000000..f1d1f0b
--- /dev/null
+++ b/Curves/Scripts/Editor/CurvePreferences.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ba2dd14dd659cda4abf7ad439803e71f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Icons.meta b/Curves/Scripts/Editor/Icons.meta
new file mode 100644
index 0000000..47a4ca2
--- /dev/null
+++ b/Curves/Scripts/Editor/Icons.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 85d9a51db23905f47a1a37776f3128b9
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Icons/Bezier3DCurveDataIcon.png b/Curves/Scripts/Editor/Icons/Bezier3DCurveDataIcon.png
new file mode 100644
index 0000000..3a6ddc6
Binary files /dev/null and b/Curves/Scripts/Editor/Icons/Bezier3DCurveDataIcon.png differ
diff --git a/Curves/Scripts/Editor/Icons/Bezier3DCurveDataIcon.png.meta b/Curves/Scripts/Editor/Icons/Bezier3DCurveDataIcon.png.meta
new file mode 100644
index 0000000..1f183ae
--- /dev/null
+++ b/Curves/Scripts/Editor/Icons/Bezier3DCurveDataIcon.png.meta
@@ -0,0 +1,115 @@
+fileFormatVersion: 2
+guid: c5c32b6c450791943b0b231a833c00b6
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 10
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 0
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: -1
+ aniso: 1
+ mipBias: -100
+ wrapU: 1
+ wrapV: 1
+ wrapW: -1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 2
+ textureShape: 1
+ singleChannelComponent: 0
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ platformSettings:
+ - serializedVersion: 3
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spritePackingTag:
+ pSDRemoveMatte: 0
+ pSDShowRemoveMatteOption: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Icons/IconTemplate.psd b/Curves/Scripts/Editor/Icons/IconTemplate.psd
new file mode 100644
index 0000000..50d12c8
Binary files /dev/null and b/Curves/Scripts/Editor/Icons/IconTemplate.psd differ
diff --git a/Curves/Scripts/Editor/Icons/IconTemplate.psd.meta b/Curves/Scripts/Editor/Icons/IconTemplate.psd.meta
new file mode 100644
index 0000000..a683ff5
--- /dev/null
+++ b/Curves/Scripts/Editor/Icons/IconTemplate.psd.meta
@@ -0,0 +1,91 @@
+fileFormatVersion: 2
+guid: 9d16932e19cb4564b83e61862132c615
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 10
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: -1
+ aniso: -1
+ mipBias: -100
+ wrapU: -1
+ wrapV: -1
+ wrapW: -1
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ platformSettings:
+ - serializedVersion: 3
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spritePackingTag:
+ pSDRemoveMatte: 0
+ pSDShowRemoveMatteOption: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Icons/SplineWalkerIcon.png b/Curves/Scripts/Editor/Icons/SplineWalkerIcon.png
new file mode 100644
index 0000000..fd98f08
Binary files /dev/null and b/Curves/Scripts/Editor/Icons/SplineWalkerIcon.png differ
diff --git a/Curves/Scripts/Editor/Icons/SplineWalkerIcon.png.meta b/Curves/Scripts/Editor/Icons/SplineWalkerIcon.png.meta
new file mode 100644
index 0000000..6cd39eb
--- /dev/null
+++ b/Curves/Scripts/Editor/Icons/SplineWalkerIcon.png.meta
@@ -0,0 +1,115 @@
+fileFormatVersion: 2
+guid: d2f3876f83c47304fb039eb650289db8
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 10
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 0
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: -1
+ aniso: 1
+ mipBias: -100
+ wrapU: 1
+ wrapV: 1
+ wrapW: -1
+ nPOTScale: 0
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 1
+ spriteTessellationDetail: -1
+ textureType: 2
+ textureShape: 1
+ singleChannelComponent: 0
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ platformSettings:
+ - serializedVersion: 3
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: Android
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spritePackingTag:
+ pSDRemoveMatte: 0
+ pSDShowRemoveMatteOption: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Inspectors.meta b/Curves/Scripts/Editor/Inspectors.meta
new file mode 100644
index 0000000..62fc550
--- /dev/null
+++ b/Curves/Scripts/Editor/Inspectors.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e66086bcdd585e846bdceaa37590e9d6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Inspectors/Bezier3DSplineDataInspector.cs b/Curves/Scripts/Editor/Inspectors/Bezier3DSplineDataInspector.cs
new file mode 100644
index 0000000..d5941bc
--- /dev/null
+++ b/Curves/Scripts/Editor/Inspectors/Bezier3DSplineDataInspector.cs
@@ -0,0 +1,407 @@
+using System;
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ [CustomEditor(typeof(Bezier3DSplineData))]
+ internal sealed class Bezier3DSplineDataInspector : UnityEditor.Editor
+ {
+ ///
+ /// Get or sets whether or not this inspector should be drawing the scene GUI. Default is true.
+ ///
+ public bool ShouldDrawSceneGUI
+ {
+ get
+ {
+ return _shouldDrawSceneGUI;
+ }
+ set
+ {
+ if (_shouldDrawSceneGUI && !value)
+ {
+ SceneView.duringSceneGui -= OnSceneGUI;
+ }
+ else if (!_shouldDrawSceneGUI && value)
+ {
+ SceneView.duringSceneGui += OnSceneGUI;
+ }
+
+ _shouldDrawSceneGUI = value;
+ }
+ }
+
+ #pragma warning disable 0649
+ public static event Action SplineUpdated;
+ #pragma warning restore 0649
+
+ private bool _shouldDrawSceneGUI;
+ private Bezier3DSplineData _spline;
+ private static Bezier3DSplineData _copyAndPasteSplineData;
+
+ internal void Awake()
+ {
+ ShouldDrawSceneGUI = true;
+ }
+
+ private void OnDestroy()
+ {
+ ShouldDrawSceneGUI = false;
+ }
+
+ internal void OnEnable()
+ {
+ CurveEditorState.Reset();
+
+ _spline = target as Bezier3DSplineData;
+ }
+
+ internal void OnDisable()
+ {
+ ShouldDrawSceneGUI = false;
+
+ Tools.hidden = false;
+ CurveEditorState.ClearKnotSelection();
+ Repaint();
+ }
+
+ private void OnSceneGUI(SceneView sceneView)
+ {
+ if (ShouldDrawSceneGUI)
+ {
+ OnSceneGUI();
+ }
+ }
+
+ internal void OnSceneGUI()
+ {
+ HotkeyTools.CheckGeneralHotkeys(_spline, SplineUpdated);
+
+ SceneGUITools.DrawCurveLinesHandles(_spline);
+ SceneGUITools.DrawSceneScreenUI();
+
+ ValidateSelected();
+ SceneGUITools.DrawUnselectedKnots(_spline, this);
+
+ if (CurvePreferences.ShouldVisualizeRotation)
+ {
+ SceneGUITools.DrawCurveOrientations(_spline);
+ }
+
+ if (CurveEditorState.HasKnotSelected)
+ {
+ if (CurveEditorState.HasSingleKnotSelected)
+ {
+ SceneGUITools.DrawSelectedSplitters(_spline, SplineUpdated);
+ SceneGUITools.DrawSelectedKnot(_spline, SplineUpdated, this);
+
+ // Hotkeys
+ HotkeyTools.CheckSelectedKnotHotkeys(_spline, SplineUpdated);
+ }
+ else
+ {
+ SceneGUITools.DrawMultiSelect(_spline, this);
+ }
+ }
+ }
+
+ public override void OnInspectorGUI()
+ {
+ ValidateSelected();
+
+ // Spline Properties
+ EditorGUI.indentLevel = 0;
+ GUILayout.BeginVertical(GUI.skin.box);
+ EditorGUILayout.LabelField("Spline Settings", EditorStyles.boldLabel);
+ EditorGUILayout.Space(5);
+ DrawInterpolationSteps();
+ DrawClosedToggle();
+
+ // Spline Actions
+ GUILayout.BeginVertical(GUI.skin.box);
+ EditorGUILayout.LabelField("Actions", EditorStyles.boldLabel);
+ EditorGUILayout.Space(5);
+ DrawSplineFlipAction();
+ DrawCopySplineDataToClipboard();
+ DrawPasteSplineDataToClipboard();
+ DrawResetSplineData();
+ GUILayout.EndVertical();
+
+ GUILayout.EndVertical();
+
+ // Selected Point Properties
+ EditorGUILayout.Space();
+ if (CurveEditorState.HasKnotSelected)
+ {
+ // Header information for selected knot.
+ GUILayout.BeginVertical(GUI.skin.box);
+ EditorGUILayout.LabelField(
+ $"Selected Knot (index = {CurveEditorState.SelectedKnotIndex})",
+ EditorStyles.boldLabel);
+ EditorGUILayout.Space(2);
+
+ var knot = _spline.GetKnot(CurveEditorState.SelectedKnotIndex);
+
+ // Draw Position
+ DrawKnotPosition(knot);
+ EditorGUILayout.Space(2);
+
+ // Draw Orientation
+ DrawKnotOrientationToggle(knot);
+ DrawKnotOrientationValue(knot);
+
+ EditorGUILayout.Space(2);
+
+ // Draw Auto-Handle
+ DrawKnotAutoHandleToggle(knot);
+ if (knot.IsUsingAutoHandles)
+ {
+ DrawKnotAutoHandleValue(knot);
+ }
+ else
+ {
+ DrawKnotHandleValues(knot);
+ }
+
+ GUILayout.EndVertical();
+ }
+ }
+
+ private void ValidateSelected()
+ {
+ if (CurveEditorState.ValidateSelectedKnotIsValid(_spline))
+ {
+ CurveEditorState.ClearKnotSelection();
+
+ Repaint();
+ }
+ }
+
+ private void DrawInterpolationSteps()
+ {
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ var steps = _spline.InterpolationStepsPerCurve;
+ steps = EditorGUILayout.DelayedIntField("Interpolation Steps Per Curve", steps);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Set interpolation steps per curve");
+
+ _spline.SetStepsPerCurve(steps);
+ SplineUpdated?.Invoke(_spline);
+ }
+ }
+ }
+
+ private void DrawClosedToggle()
+ {
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ var closed = _spline.IsClosed;
+ closed = EditorGUILayout.Toggle(
+ new GUIContent("Closed", "Generate an extra curve, connecting the final point to the first point."),
+ closed);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Set closed");
+
+ _spline.SetClosed(closed);
+ SplineUpdated?.Invoke(_spline);
+ SceneView.RepaintAll();
+ }
+ }
+ }
+
+ private void DrawSplineFlipAction()
+ {
+ if (GUILayout.Button(new GUIContent("Flip", "Flip spline direction.")))
+ {
+ Undo.RecordObject(_spline, "Flip spline");
+
+ _spline.Flip();
+ SplineUpdated?.Invoke(_spline);
+ SceneView.RepaintAll();
+ }
+ }
+
+ private void DrawCopySplineDataToClipboard()
+ {
+ if (GUILayout.Button(new GUIContent("Copy To Clipboard", "Copies this spline to the inspector")))
+ {
+ _copyAndPasteSplineData = _spline;
+ }
+ }
+
+ private void DrawPasteSplineDataToClipboard()
+ {
+ using (new EditorGUI.DisabledGroupScope(_copyAndPasteSplineData == null))
+ {
+ if (GUILayout.Button(new GUIContent("Paste From Clipboard", "Copies this spline to the inspector")))
+ {
+ Undo.RecordObject(_spline, "Paste spline");
+
+ EditorUtility.CopySerialized(_copyAndPasteSplineData, _spline);
+ }
+ }
+ }
+
+ private void DrawResetSplineData()
+ {
+ if (GUILayout.Button(new GUIContent("Reset", "Resets the spline back to its starting values")))
+ {
+ Undo.RecordObject(_spline, "Reset spline");
+
+ _spline.Reset();
+ }
+ }
+
+ private void DrawKnotPosition(Knot knot)
+ {
+ // Draw Position
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ knot.position = EditorGUILayout.Vector3Field("Position", knot.position);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Edit Bezier Point");
+
+ _spline.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+
+ SplineUpdated?.Invoke(_spline);
+ SceneView.RepaintAll();
+ }
+ }
+ }
+
+ private void DrawKnotOrientationToggle(Knot knot)
+ {
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.PrefixLabel(new GUIContent("Uses Orientation Anchor"));
+ var isUsingOrientation = GUILayout.Toggle(knot.IsUsingRotation, string.Empty);
+ EditorGUILayout.EndHorizontal();
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Toggle Bezier Orientation Anchor");
+
+ knot.rotation = !knot.IsUsingRotation ? (Quaternion?)Quaternion.identity : null;
+ _spline.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+
+ SplineUpdated?.Invoke(_spline);
+ SceneView.RepaintAll();
+ }
+ }
+ }
+
+ private void DrawKnotOrientationValue(Knot knot)
+ {
+ if (knot.IsUsingRotation)
+ {
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ var orientationEuler = knot.rotation.Value.eulerAngles;
+ orientationEuler = EditorGUILayout.Vector3Field("Orientation", orientationEuler);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Modify Knot Rotaton");
+
+ knot.rotation = Quaternion.Euler(orientationEuler);
+ _spline.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+
+ SceneView.RepaintAll();
+ }
+ }
+ }
+ }
+
+ private void DrawKnotAutoHandleToggle(Knot knot)
+ {
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ EditorGUILayout.BeginHorizontal();
+ EditorGUILayout.PrefixLabel(new GUIContent("Uses Auto Handles"));
+ var isUsingAutoHandles = GUILayout.Toggle(knot.IsUsingAutoHandles, string.Empty);
+ EditorGUILayout.EndHorizontal();
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Toggle Bezier Auto Handles");
+
+ knot.auto = isUsingAutoHandles ? 0.33f : 0f;
+ _spline.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+
+ SplineUpdated?.Invoke(_spline);
+ SceneView.RepaintAll();
+ }
+ }
+ }
+
+ private void DrawKnotAutoHandleValue(Knot knot)
+ {
+ // Auto-Handles Distance
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ knot.auto = EditorGUILayout.FloatField("Distance", knot.auto);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Edit Bezier Point");
+
+ _spline.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+
+ SplineUpdated?.Invoke(_spline);
+ SceneView.RepaintAll();
+ }
+ }
+ }
+
+ private void DrawKnotHandleValues(Knot knot)
+ {
+ // In-Handle
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ knot.handleIn = EditorGUILayout.Vector3Field("Handle in", knot.handleIn);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Edit Bezier Handle");
+
+ if (CurvePreferences.ShouldMirrorHandleMovement)
+ {
+ knot.handleOut = -knot.handleIn;
+ }
+ _spline.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+
+ SplineUpdated?.Invoke(_spline);
+ SceneView.RepaintAll();
+ }
+ }
+
+ // Out-Handle
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ knot.handleOut = EditorGUILayout.Vector3Field("Handle out", knot.handleOut);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(_spline, "Edit Bezier Handle");
+
+ if (CurvePreferences.ShouldMirrorHandleMovement)
+ {
+ knot.handleIn = -knot.handleOut;
+ }
+ _spline.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+
+ SplineUpdated?.Invoke(_spline);
+ SceneView.RepaintAll();
+ }
+ }
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/Inspectors/Bezier3DSplineDataInspector.cs.meta b/Curves/Scripts/Editor/Inspectors/Bezier3DSplineDataInspector.cs.meta
new file mode 100644
index 0000000..6142490
--- /dev/null
+++ b/Curves/Scripts/Editor/Inspectors/Bezier3DSplineDataInspector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ae5a321944a38fc4d8fc9f3397a0ab14
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Inspectors/Bezier3DSplineInspector.cs b/Curves/Scripts/Editor/Inspectors/Bezier3DSplineInspector.cs
new file mode 100644
index 0000000..91f6ce0
--- /dev/null
+++ b/Curves/Scripts/Editor/Inspectors/Bezier3DSplineInspector.cs
@@ -0,0 +1,83 @@
+using System;
+using UnityEditor;
+
+namespace JCMG.Curves.Editor
+{
+ [CustomEditor(typeof(Bezier3DSpline))]
+ internal sealed class Bezier3DSplineInspector : UnityEditor.Editor
+ {
+ #pragma warning disable 0649
+ public static event Action SplineUpdated;
+ #pragma warning restore 0649
+
+ private Bezier3DSpline _spline;
+ private Bezier3DSplineDataInspector _splineDataEditor;
+
+ private void OnEnable()
+ {
+ CurveEditorState.Reset();
+
+ _spline = (Bezier3DSpline)target;
+
+ _splineDataEditor = (Bezier3DSplineDataInspector)CreateEditor(_spline.SplineData, typeof(Bezier3DSplineDataInspector));
+ _splineDataEditor.ShouldDrawSceneGUI = false;
+ }
+
+ private void OnDisable()
+ {
+ _splineDataEditor.OnDisable();
+
+ DestroyImmediate(_splineDataEditor);
+ }
+
+ public override void OnInspectorGUI()
+ {
+ _spline = (Bezier3DSpline)target;
+
+ _splineDataEditor.OnInspectorGUI();
+ }
+
+ private void OnSceneGUI()
+ {
+ HotkeyTools.CheckGeneralHotkeys(_spline, SplineUpdated);
+
+ SceneGUITools.DrawCurveLinesHandles(_spline, _spline.transform);
+ SceneGUITools.DrawSceneScreenUI();
+
+ ValidateSelected();
+
+ SceneGUITools.DrawUnselectedKnots(_spline, this, _spline.transform);
+
+ if (CurvePreferences.ShouldVisualizeRotation)
+ {
+ SceneGUITools.DrawCurveOrientations(_spline);
+ }
+
+ if (CurveEditorState.HasKnotSelected)
+ {
+ if (CurveEditorState.HasSingleKnotSelected)
+ {
+ SceneGUITools.DrawSelectedSplitters(_spline.SplineData, SplineUpdated, _spline.transform);
+ SceneGUITools.DrawSelectedKnot(_spline.SplineData, SplineUpdated, this, _spline.transform);
+
+ // Hotkeys
+ HotkeyTools.CheckSelectedKnotHotkeys(_spline, SplineUpdated);
+ }
+ else
+ {
+ SceneGUITools.DrawMultiSelect(_spline, this, _spline.transform);
+ }
+ }
+ }
+
+ private void ValidateSelected()
+ {
+ if (CurveEditorState.ValidateSelectedKnotIsValid(_spline))
+ {
+ CurveEditorState.ClearKnotSelection();
+
+ Repaint();
+ }
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/Inspectors/Bezier3DSplineInspector.cs.meta b/Curves/Scripts/Editor/Inspectors/Bezier3DSplineInspector.cs.meta
new file mode 100644
index 0000000..bd91385
--- /dev/null
+++ b/Curves/Scripts/Editor/Inspectors/Bezier3DSplineInspector.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f6a91a7f55c3e474c94806960b2d7a6c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/JCMG.Curves.Editor.asmdef b/Curves/Scripts/Editor/JCMG.Curves.Editor.asmdef
new file mode 100644
index 0000000..ae17516
--- /dev/null
+++ b/Curves/Scripts/Editor/JCMG.Curves.Editor.asmdef
@@ -0,0 +1,17 @@
+{
+ "name": "JCMG.Curves.Editor",
+ "references": [
+ "GUID:466f8dfa7a6bcc34f8b53de55b1bbd94"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Curves/Scripts/Editor/JCMG.Curves.Editor.asmdef.meta b/Curves/Scripts/Editor/JCMG.Curves.Editor.asmdef.meta
new file mode 100644
index 0000000..17f6620
--- /dev/null
+++ b/Curves/Scripts/Editor/JCMG.Curves.Editor.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 269e8f16c3ed16f4bb6f3a3fcdcfd27a
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/MenuItems.cs b/Curves/Scripts/Editor/MenuItems.cs
new file mode 100644
index 0000000..c57eaa7
--- /dev/null
+++ b/Curves/Scripts/Editor/MenuItems.cs
@@ -0,0 +1,24 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ ///
+ /// Menu items for the curves library.
+ ///
+ internal static class MenuItems
+ {
+ [MenuItem("GameObject/JCMG/Curves/Bezier3DSpline", false, 10)]
+ internal static void CreateBezierSpline()
+ {
+ var obj = new GameObject("Bezier3DSpline").AddComponent();
+
+ Selection.objects = new Object[]
+ {
+ obj.gameObject
+ };
+
+ EditorGUIUtility.PingObject(obj.gameObject);
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/MenuItems.cs.meta b/Curves/Scripts/Editor/MenuItems.cs.meta
new file mode 100644
index 0000000..5b5e2fa
--- /dev/null
+++ b/Curves/Scripts/Editor/MenuItems.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0257aac608e7e8a418a91b92dc089f91
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/ObjectPreviews.meta b/Curves/Scripts/Editor/ObjectPreviews.meta
new file mode 100644
index 0000000..30d5a00
--- /dev/null
+++ b/Curves/Scripts/Editor/ObjectPreviews.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c8c9fe547a1676240bacdcc8ef2c8af1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/ObjectPreviews/Base3DSplineDataPreview.cs b/Curves/Scripts/Editor/ObjectPreviews/Base3DSplineDataPreview.cs
new file mode 100644
index 0000000..e9f10a3
--- /dev/null
+++ b/Curves/Scripts/Editor/ObjectPreviews/Base3DSplineDataPreview.cs
@@ -0,0 +1,27 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ internal abstract class Base3DSplineDataPreview : ObjectPreview
+ {
+ public sealed override GUIContent GetPreviewTitle()
+ {
+ return new GUIContent("Properties");
+ }
+
+ public sealed override bool HasPreviewGUI()
+ {
+ return true;
+ }
+
+ protected void DrawProperty(ref Rect labelRect, ref Rect valueRect, string label, string value)
+ {
+ EditorGUI.LabelField(labelRect, label, CurveEditorStyles.LabelStyle);
+ EditorGUI.LabelField(valueRect, value);
+
+ labelRect.y += EditorGUIUtility.singleLineHeight;
+ valueRect.y += EditorGUIUtility.singleLineHeight;
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/ObjectPreviews/Base3DSplineDataPreview.cs.meta b/Curves/Scripts/Editor/ObjectPreviews/Base3DSplineDataPreview.cs.meta
new file mode 100644
index 0000000..9586dd2
--- /dev/null
+++ b/Curves/Scripts/Editor/ObjectPreviews/Base3DSplineDataPreview.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bf6cc90050e110f4b80cd668aa242fee
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplineDataPreview.cs b/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplineDataPreview.cs
new file mode 100644
index 0000000..eab118d
--- /dev/null
+++ b/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplineDataPreview.cs
@@ -0,0 +1,50 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ [CustomPreview(typeof(Bezier3DSplineData))]
+ internal sealed class Bezier3DSplineDataPreview : Base3DSplineDataPreview
+ {
+ public override void OnPreviewGUI(Rect rect, GUIStyle background)
+ {
+ if (Event.current.type != EventType.Repaint)
+ {
+ return;
+ }
+
+ var spline = (Bezier3DSplineData)target;
+
+ var rectOffset = new RectOffset(
+ -5,
+ -5,
+ -5,
+ -5);
+ rect = rectOffset.Add(rect);
+
+ var position1 = rect;
+ position1.width = 110f;
+
+ var position2 = rect;
+ position2.xMin += 110f;
+ position2.width = 110f;
+
+ EditorGUI.LabelField(position1, "Property", CurveEditorStyles.HeaderStyle);
+ EditorGUI.LabelField(position2, "Value", CurveEditorStyles.HeaderStyle);
+
+ position1.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
+ position2.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
+
+ DrawProperty(
+ ref position1,
+ ref position2,
+ "Point Count",
+ spline.KnotCount.ToString());
+ DrawProperty(
+ ref position1,
+ ref position2,
+ "Total Length",
+ spline.TotalLength.ToString("F"));
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplineDataPreview.cs.meta b/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplineDataPreview.cs.meta
new file mode 100644
index 0000000..c61445c
--- /dev/null
+++ b/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplineDataPreview.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1dc0e165b70675e40a6a6809273a51af
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplinePreview.cs b/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplinePreview.cs
new file mode 100644
index 0000000..8b98f0c
--- /dev/null
+++ b/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplinePreview.cs
@@ -0,0 +1,51 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ [CustomPreview(typeof(Bezier3DSpline))]
+ internal sealed class Bezier3DSplinePreview : Base3DSplineDataPreview
+ {
+ public override void OnPreviewGUI(Rect rect, GUIStyle background)
+ {
+ if (Event.current.type != EventType.Repaint)
+ {
+ return;
+ }
+
+ var spline = (Bezier3DSpline)target;
+
+ var rectOffset = new RectOffset(
+ -5,
+ -5,
+ -5,
+ -5);
+ rect = rectOffset.Add(rect);
+
+ var position1 = rect;
+ position1.width = 110f;
+
+ var position2 = rect;
+ position2.xMin += 110f;
+ position2.width = 110f;
+
+ EditorGUI.LabelField(position1, "Property", CurveEditorStyles.HeaderStyle);
+ EditorGUI.LabelField(position2, "Value", CurveEditorStyles.HeaderStyle);
+
+ position1.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
+ position2.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
+
+ DrawProperty(
+ ref position1,
+ ref position2,
+ "Point Count",
+ spline.KnotCount.ToString());
+
+ DrawProperty(
+ ref position1,
+ ref position2,
+ "Total Length",
+ spline.TotalLength.ToString("F"));
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplinePreview.cs.meta b/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplinePreview.cs.meta
new file mode 100644
index 0000000..4ddd860
--- /dev/null
+++ b/Curves/Scripts/Editor/ObjectPreviews/Bezier3DSplinePreview.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 0c865634102966c46ac55ec636acbc9c
+timeCreated: 1503151146
+licenseType: Free
+MonoImporter:
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/SceneGUIConstants.cs b/Curves/Scripts/Editor/SceneGUIConstants.cs
new file mode 100644
index 0000000..b9e4d1b
--- /dev/null
+++ b/Curves/Scripts/Editor/SceneGUIConstants.cs
@@ -0,0 +1,18 @@
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ internal static class SceneGUIConstants
+ {
+ // TODO some of these seem like they could be preferences.
+ public static float HandleSize { get; }
+
+ public static Vector2 GUIOffset { get; }
+
+ static SceneGUIConstants()
+ {
+ HandleSize = 0.1f;
+ GUIOffset = new Vector2(10, 10);
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/SceneGUIConstants.cs.meta b/Curves/Scripts/Editor/SceneGUIConstants.cs.meta
new file mode 100644
index 0000000..d3a7952
--- /dev/null
+++ b/Curves/Scripts/Editor/SceneGUIConstants.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a844dcaf435956a46a661512edbf0360
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Tools.meta b/Curves/Scripts/Editor/Tools.meta
new file mode 100644
index 0000000..6727f01
--- /dev/null
+++ b/Curves/Scripts/Editor/Tools.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e52e66c8227afd341bddd96f63525910
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Tools/HotkeyTools.cs b/Curves/Scripts/Editor/Tools/HotkeyTools.cs
new file mode 100644
index 0000000..2fde0fa
--- /dev/null
+++ b/Curves/Scripts/Editor/Tools/HotkeyTools.cs
@@ -0,0 +1,85 @@
+using System;
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ internal static class HotkeyTools
+ {
+ private const string UNDO_REDO_PERFORMED = "UndoRedoPerformed";
+
+ public static void CheckGeneralHotkeys(
+ IBezier3DSplineData splineData,
+ Action onUpdateSpline)
+ {
+ var evt = Event.current;
+ switch (evt.type)
+ {
+ // Undo Last Command
+ case EventType.ValidateCommand:
+ if (evt.commandName == UNDO_REDO_PERFORMED)
+ {
+ onUpdateSpline?.Invoke(splineData);
+ }
+ break;
+
+ // Flip Spline
+ case EventType.KeyDown:
+ if (evt.keyCode == KeyCode.I)
+ {
+ if ((evt.modifiers & (EventModifiers.Control | EventModifiers.Command)) != 0)
+ {
+ splineData.Flip();
+ }
+ }
+ break;
+ }
+ }
+
+ public static void CheckSelectedKnotHotkeys(
+ IBezier3DSplineData splineData,
+ Action onUpdateSpline)
+ {
+ var evt = Event.current;
+ switch (evt.type)
+ {
+ case EventType.KeyDown:
+ // Delete Selected Knot
+ if (evt.keyCode == KeyCode.Delete)
+ {
+ if (splineData.KnotCount > 2)
+ {
+ Undo.RecordObject((UnityEngine.Object)splineData, "Remove Bezier Point");
+ splineData.RemoveKnot(CurveEditorState.SelectedKnotIndex);
+
+ CurveEditorState.ClearKnotSelection();
+
+ onUpdateSpline?.Invoke(splineData);
+ }
+
+ evt.Use();
+ }
+
+ // Focus Selected Knot
+ if (evt.keyCode == KeyCode.F)
+ {
+ var dist = splineData.GetSplineDistanceForKnotIndex(CurveEditorState.SelectedKnotIndex);
+ var pos = splineData.GetPosition(dist);
+
+ SceneView.lastActiveSceneView.Frame(new Bounds(pos, Vector3.one * 5f), false);
+
+ evt.Use();
+ }
+
+ // Clear Knot Selection
+ if (evt.keyCode == KeyCode.Escape)
+ {
+ CurveEditorState.ClearKnotSelection();
+ evt.Use();
+ }
+
+ break;
+ }
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/Tools/HotkeyTools.cs.meta b/Curves/Scripts/Editor/Tools/HotkeyTools.cs.meta
new file mode 100644
index 0000000..9a3946c
--- /dev/null
+++ b/Curves/Scripts/Editor/Tools/HotkeyTools.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 02623d97de819cf4b924a82e32b13f7b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Editor/Tools/SceneGUITools.cs b/Curves/Scripts/Editor/Tools/SceneGUITools.cs
new file mode 100644
index 0000000..710fb08
--- /dev/null
+++ b/Curves/Scripts/Editor/Tools/SceneGUITools.cs
@@ -0,0 +1,483 @@
+using System;
+using UnityEditor;
+using UnityEngine;
+
+namespace JCMG.Curves.Editor
+{
+ ///
+ /// Helper methods for drawing in the scene
+ ///
+ internal static class SceneGUITools
+ {
+ public static void DrawCurveLinesHandles(IBezier3DSplineData splineData, Transform transform = null)
+ {
+ Handles.color = Color.yellow;
+
+ //Loop through each curve in spline
+ var segments = splineData.InterpolationStepsPerCurve;
+ var spacing = 1f / segments;
+ for (var i = 0; i < splineData.CurveCount; i++)
+ {
+ var curve = splineData.GetCurve(i);
+
+ //Get curve in world space
+ Vector3 a, b, c, d;
+
+ if (transform != null)
+ {
+ a = transform.TransformPoint(curve.StartPoint);
+ b = transform.TransformPoint(curve.FirstHandle + curve.StartPoint);
+ c = transform.TransformPoint(curve.SecondHandle + curve.EndPoint);
+ d = transform.TransformPoint(curve.EndPoint);
+ }
+ else
+ {
+ a = curve.StartPoint;
+ b = curve.FirstHandle + curve.StartPoint;
+ c = curve.SecondHandle + curve.EndPoint;
+ d = curve.EndPoint;
+ }
+
+ var prev = Bezier3DCurve.GetPoint(
+ a,
+ b,
+ c,
+ d,
+ 0f);
+
+ for (var k = 0; k <= segments; k++)
+ {
+ var cur = Bezier3DCurve.GetPoint(
+ a,
+ b,
+ c,
+ d,
+ k * spacing);
+ Handles.DrawLine(prev, cur);
+ prev = cur;
+ }
+ }
+ }
+
+ public static void DrawCurveOrientations(IBezier3DSplineData splineData)
+ {
+ for (var dist = 0f; dist < splineData.TotalLength; dist += 1)
+ {
+ var point = splineData.GetPosition(dist);
+
+ // Draw Up Vector
+ var up = splineData.GetUp(dist);
+ Handles.color = Handles.yAxisColor;
+ Handles.DrawLine(point, point + up);
+
+ // Draw Forward Vector
+ var forward = splineData.GetForward(dist);
+ Handles.color = Handles.zAxisColor;
+ Handles.DrawLine(point, point + forward);
+
+ // Draw Right Vector
+ var right = splineData.GetRight(dist);
+ Handles.color = Handles.xAxisColor;
+ Handles.DrawLine(point, point + right);
+ }
+ }
+
+ public static void DrawSelectedKnot(
+ Bezier3DSplineData splineData,
+ Action onUpdateSpline,
+ UnityEditor.Editor editorWindow,
+ Transform transform = null)
+ {
+ var knot = splineData.GetKnot(CurveEditorState.SelectedKnotIndex);
+ Handles.color = Color.green;
+
+ var knotWorldPos = transform == null
+ ? knot.position
+ : transform.TransformPoint(knot.position);
+
+ if (knot.rotation.HasValue)
+ {
+ Handles.color = Handles.yAxisColor;
+ var rot = knot.rotation.Value;
+ Handles.ArrowHandleCap(
+ 0,
+ knotWorldPos,
+ rot * Quaternion.AngleAxis(90, Vector3.left),
+ 0.15f,
+ EventType.Repaint);
+ }
+
+ if (Tools.current == Tool.Move)
+ {
+ //Position handle
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ knotWorldPos = Handles.PositionHandle(knotWorldPos, Tools.handleRotation);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(splineData, "Edit Bezier Point");
+ knot.position = transform == null ? knotWorldPos : transform.InverseTransformPoint(knotWorldPos);
+ splineData.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+ onUpdateSpline?.Invoke(splineData);
+ editorWindow.Repaint();
+ }
+ }
+
+ Handles.color = Color.white;
+
+ //In Handle
+ if (knot.handleIn != Vector3.zero)
+ {
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ var inHandleWorldPos = transform == null
+ ? knot.position + knot.handleIn
+ : transform.TransformPoint(knot.position + knot.handleIn);
+
+ inHandleWorldPos = Handles.PositionHandle(inHandleWorldPos, Tools.handleRotation);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(splineData, "Edit Bezier Handle");
+ knot.handleIn = transform == null
+ ? inHandleWorldPos - knot.position
+ : transform.InverseTransformPoint(inHandleWorldPos) - knot.position;
+ knot.auto = 0;
+ if (CurvePreferences.ShouldMirrorHandleMovement)
+ {
+ knot.handleOut = -knot.handleIn;
+ }
+
+ splineData.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+ onUpdateSpline?.Invoke(splineData);
+ editorWindow.Repaint();
+ }
+
+ Handles.DrawLine(knotWorldPos, inHandleWorldPos);
+ }
+ }
+
+ //outHandle
+ if (knot.handleOut != Vector3.zero)
+ {
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ var outHandleWorldPos = transform == null
+ ? knot.position + knot.handleOut
+ : transform.TransformPoint(knot.position + knot.handleOut);
+
+ outHandleWorldPos = Handles.PositionHandle(outHandleWorldPos, Tools.handleRotation);
+
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(splineData, "Edit Bezier Handle");
+ knot.handleOut = transform == null
+ ? outHandleWorldPos - knot.position
+ : transform.InverseTransformPoint(outHandleWorldPos) - knot.position;
+ knot.auto = 0;
+ if (CurvePreferences.ShouldMirrorHandleMovement)
+ {
+ knot.handleIn = -knot.handleOut;
+ }
+
+ splineData.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+ onUpdateSpline?.Invoke(splineData);
+ editorWindow.Repaint();
+ }
+
+ Handles.DrawLine(knotWorldPos, outHandleWorldPos);
+ }
+ }
+
+ }
+ else if (Tools.current == Tool.Rotate)
+ {
+ //Rotation handle
+ using (var changeCheck = new EditorGUI.ChangeCheckScope())
+ {
+ var rot = (knot.rotation.HasValue ? knot.rotation.Value : Quaternion.identity).normalized;
+ rot = Handles.RotationHandle(rot, knotWorldPos);
+ if (changeCheck.changed)
+ {
+ Undo.RecordObject(splineData, "Edit Bezier Point");
+ knot.rotation = rot;
+ splineData.SetKnot(CurveEditorState.SelectedKnotIndex, knot);
+ onUpdateSpline?.Invoke(splineData);
+ editorWindow.Repaint();
+ }
+ }
+ }
+ }
+
+ public static void DrawMultiSelect(
+ IBezier3DSplineData splineData,
+ UnityEditor.Editor editorWindow,
+ Transform transform = null)
+ {
+ Handles.color = Color.blue;
+ for (var i = 0; i < CurveEditorState.SelectedKnots.Count; i++)
+ {
+ if (Handles.Button(
+ transform == null
+ ? splineData.GetKnot(CurveEditorState.SelectedKnots[i]).position
+ : transform.TransformPoint(splineData.GetKnot(CurveEditorState.SelectedKnots[i]).position),
+ Camera.current.transform.rotation,
+ SceneGUIConstants.HandleSize,
+ SceneGUIConstants.HandleSize,
+ Handles.CircleHandleCap))
+ {
+ CurveEditorState.SelectKnot(CurveEditorState.SelectedKnots[i], true);
+ editorWindow.Repaint();
+ }
+ }
+
+ var handlePos = Vector3.zero;
+ if (Tools.pivotMode == PivotMode.Center)
+ {
+ for (var i = 0; i < CurveEditorState.SelectedKnots.Count; i++)
+ {
+ handlePos += splineData.GetKnot(CurveEditorState.SelectedKnots[i]).position;
+ }
+
+ handlePos /= CurveEditorState.SelectedKnots.Count;
+ }
+ else
+ {
+ handlePos = splineData.GetKnot(CurveEditorState.SelectedKnotIndex).position;
+ }
+
+ if (transform != null)
+ {
+ handlePos = transform.TransformPoint(handlePos);
+ }
+
+ Handles.PositionHandle(handlePos, Tools.handleRotation);
+ }
+
+ public static void DrawUnselectedKnots(
+ IBezier3DSplineData splineData,
+ UnityEditor.Editor editorWindow,
+ Transform transform = null)
+ {
+ for (var i = 0; i < splineData.KnotCount; i++)
+ {
+ if (CurveEditorState.SelectedKnots.Contains(i))
+ {
+ continue;
+ }
+
+ var knot = splineData.GetKnot(i);
+ var knotWorldPos = transform == null
+ ? knot.position
+ : transform.TransformPoint(knot.position);
+
+ if (knot.rotation.HasValue)
+ {
+ Handles.color = Handles.yAxisColor;
+ var rot = knot.rotation.Value;
+ Handles.ArrowHandleCap(
+ 0,
+ knotWorldPos,
+ rot * Quaternion.AngleAxis(90, Vector3.left),
+ 0.15f,
+ EventType.Repaint);
+ }
+
+ Handles.color = Color.white;
+ if (Handles.Button(
+ knotWorldPos,
+ Camera.current.transform.rotation,
+ HandleUtility.GetHandleSize(knotWorldPos) * SceneGUIConstants.HandleSize,
+ HandleUtility.GetHandleSize(knotWorldPos) * SceneGUIConstants.HandleSize,
+ Handles.CircleHandleCap))
+ {
+ CurveEditorState.SelectKnot(i, Event.current.control);
+ editorWindow.Repaint();
+ }
+ }
+ }
+
+ public static void DrawSelectedSplitters(
+ Bezier3DSplineData splineData,
+ Action onUpdateSpline,
+ Transform transform = null)
+ {
+ Handles.color = Color.white;
+
+ //Start add
+ if (!splineData.IsClosed && CurveEditorState.SelectedKnotIndex == 0)
+ {
+ var curve = splineData.GetCurve(0);
+ var a = transform == null
+ ? curve.StartPoint
+ : transform.TransformPoint(curve.StartPoint);
+ var b = transform == null
+ ? curve.FirstHandle.normalized * 2f
+ : transform.TransformDirection(curve.FirstHandle).normalized * 2f;
+
+ var handleScale = HandleUtility.GetHandleSize(a);
+ b *= handleScale;
+ Handles.DrawDottedLine(a, a - b, 3f);
+ if (Handles.Button(
+ a - b,
+ Camera.current.transform.rotation,
+ handleScale * SceneGUIConstants.HandleSize * 0.4f,
+ handleScale * SceneGUIConstants.HandleSize * 0.4f,
+ Handles.DotHandleCap))
+ {
+ Undo.RecordObject(splineData, "Add Bezier Point");
+ var knot = splineData.GetKnot(CurveEditorState.SelectedKnotIndex);
+ splineData.InsertKnot(
+ 0,
+ new Knot(
+ curve.StartPoint - curve.FirstHandle.normalized * handleScale * 2,
+ Vector3.zero,
+ curve.FirstHandle.normalized * 0.5f,
+ knot.auto,
+ knot.rotation));
+ onUpdateSpline?.Invoke(splineData);
+ }
+ }
+
+ //End add
+ if (!splineData.IsClosed && CurveEditorState.SelectedKnotIndex == splineData.CurveCount)
+ {
+ var curve = splineData.GetCurve(splineData.CurveCount - 1);
+ var c = transform == null
+ ? curve.SecondHandle.normalized * 2f
+ : transform.TransformDirection(curve.SecondHandle).normalized * 2f;
+ var d = transform == null
+ ? curve.EndPoint
+ : transform.TransformPoint(curve.EndPoint);
+ var handleScale = HandleUtility.GetHandleSize(d);
+ c *= handleScale;
+ Handles.DrawDottedLine(d, d - c, 3f);
+
+ if (Handles.Button(
+ d - c,
+ Camera.current.transform.rotation,
+ handleScale * SceneGUIConstants.HandleSize * 0.4f,
+ handleScale * SceneGUIConstants.HandleSize * 0.4f,
+ Handles.DotHandleCap))
+ {
+ Undo.RecordObject(splineData, "Add Bezier Point");
+
+ var knot = splineData.GetKnot(CurveEditorState.SelectedKnotIndex);
+ splineData.AddKnot(
+ new Knot(
+ curve.EndPoint - curve.SecondHandle.normalized * handleScale * 2,
+ curve.SecondHandle.normalized * 0.5f,
+ Vector3.zero,
+ knot.auto,
+ knot.rotation));
+
+ CurveEditorState.SelectKnot(splineData.CurveCount, false);
+
+ onUpdateSpline?.Invoke(splineData);
+ }
+ }
+
+ // Prev split
+ if (splineData.IsClosed || CurveEditorState.SelectedKnotIndex != 0)
+ {
+ var curve = splineData.GetCurve(CurveEditorState.SelectedKnotIndex == 0 ? splineData.CurveCount - 1 : CurveEditorState.SelectedKnotIndex - 1);
+ var centerLocal = curve.GetPoint(curve.ConvertDistanceToTime(curve.Length * 0.5f));
+ var center = transform == null ? centerLocal : transform.TransformPoint(centerLocal);
+
+ var a = curve.StartPoint + curve.FirstHandle;
+ var b = curve.SecondHandle + curve.EndPoint;
+ var ab = (b - a) * 0.3f;
+ var handleScale = HandleUtility.GetHandleSize(center);
+
+ if (Handles.Button(
+ center,
+ Camera.current.transform.rotation,
+ handleScale * SceneGUIConstants.HandleSize * 0.4f,
+ handleScale * SceneGUIConstants.HandleSize * 0.4f,
+ Handles.DotHandleCap))
+ {
+ Undo.RecordObject(splineData, "Add Bezier Point");
+ var knot = splineData.GetKnot(CurveEditorState.SelectedKnotIndex);
+ splineData.InsertKnot(
+ CurveEditorState.SelectedKnotIndex == 0 ? splineData.CurveCount : CurveEditorState.SelectedKnotIndex,
+ new Knot(
+ centerLocal,
+ -ab,
+ ab,
+ knot.auto,
+ knot.rotation));
+
+ if (CurveEditorState.SelectedKnotIndex == 0)
+ {
+ CurveEditorState.SelectKnot(splineData.CurveCount - 1, false);
+ }
+
+ onUpdateSpline?.Invoke(splineData);
+ }
+ }
+
+ // Next split
+ if (CurveEditorState.SelectedKnotIndex != splineData.CurveCount)
+ {
+ var curve = splineData.GetCurve(CurveEditorState.SelectedKnotIndex);
+ var centerLocal = curve.GetPoint(curve.ConvertDistanceToTime(curve.Length * 0.5f));
+ var center = transform == null ? centerLocal : transform.TransformPoint(centerLocal);
+
+ var a = curve.StartPoint + curve.FirstHandle;
+ var b = curve.SecondHandle + curve.EndPoint;
+ var ab = (b - a) * 0.3f;
+ var handleScale = HandleUtility.GetHandleSize(center);
+ if (Handles.Button(
+ center,
+ Camera.current.transform.rotation,
+ handleScale * SceneGUIConstants.HandleSize * 0.4f,
+ handleScale * SceneGUIConstants.HandleSize * 0.4f,
+ Handles.DotHandleCap))
+ {
+ Undo.RecordObject(splineData, "Add Bezier Point");
+ splineData.InsertKnot(CurveEditorState.SelectedKnotIndex + 1, new Knot(centerLocal, -ab, ab));
+ CurveEditorState.SelectKnot(CurveEditorState.SelectedKnotIndex + 1, false);
+ onUpdateSpline?.Invoke(splineData);
+ }
+ }
+ }
+
+ public static void DrawSceneScreenUI()
+ {
+ Handles.BeginGUI();
+ var defaultColor = GUI.contentColor;
+ var guiLayoutOptions = new GUILayoutOption[]
+ {
+ GUILayout.MaxWidth(50f),
+ GUILayout.MinWidth(50f),
+ GUILayout.MinHeight(50f),
+ GUILayout.MaxHeight(50f)
+ };
+ using (new GUILayout.AreaScope(new Rect(SceneGUIConstants.GUIOffset, new Vector2(125f, 50f))))
+ {
+ using (new GUILayout.HorizontalScope())
+ {
+ GUI.contentColor = CurvePreferences.ShouldMirrorHandleMovement ? Color.green : Color.red;
+ if (GUILayout.Button(new GUIContent(
+ (Texture2D)EditorGUIUtility.Load("EchoFilter Icon"),
+ "Should opposite handles mirror edited handles?"),
+ guiLayoutOptions))
+ {
+ CurvePreferences.ShouldMirrorHandleMovement = !CurvePreferences.ShouldMirrorHandleMovement;
+ }
+
+ GUI.contentColor = CurvePreferences.ShouldVisualizeRotation ? Color.white : Color.red;
+ if (GUILayout.Button(new GUIContent(
+ (Texture2D)EditorGUIUtility.Load("Transform Icon"),
+ "Should visualize rotation along spline?"),
+ guiLayoutOptions))
+ {
+ CurvePreferences.ShouldVisualizeRotation = !CurvePreferences.ShouldVisualizeRotation;
+ }
+ }
+ }
+ Handles.EndGUI();
+ }
+ }
+}
diff --git a/Curves/Scripts/Editor/Tools/SceneGUITools.cs.meta b/Curves/Scripts/Editor/Tools/SceneGUITools.cs.meta
new file mode 100644
index 0000000..86a6340
--- /dev/null
+++ b/Curves/Scripts/Editor/Tools/SceneGUITools.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: feda812df5330644da804507970c2f25
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Interfaces.meta b/Curves/Scripts/Interfaces.meta
new file mode 100644
index 0000000..3c9fda6
--- /dev/null
+++ b/Curves/Scripts/Interfaces.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d10a7d8436e25bb4ea68f3d1c1f126a8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Interfaces/IBezier3DSplineData.cs b/Curves/Scripts/Interfaces/IBezier3DSplineData.cs
new file mode 100644
index 0000000..c29c01f
--- /dev/null
+++ b/Curves/Scripts/Interfaces/IBezier3DSplineData.cs
@@ -0,0 +1,26 @@
+
+namespace JCMG.Curves
+{
+ ///
+ /// A Bezier 3D spline.
+ ///
+ public interface IBezier3DSplineData : IReadOnly3DSplineData
+ {
+ // Settings
+ void SetStepsPerCurve(int stepCount);
+ void SetClosed(bool isClosed);
+
+ // Actions
+ void Flip();
+
+ // Curve
+ Bezier3DCurve GetCurve(int index);
+ Bezier3DCurve GetCurveIndexTime(float splineDist, out int index, out float curveTime);
+ void GetCurveIndicesForKnot(int knotIndex, out int preCurveIndex, out int postCurveIndex);
+
+ // Knot
+ void InsertKnot(int index, Knot knot);
+ void RemoveKnot(int index);
+ void SetKnot(int index, Knot knot);
+ }
+}
diff --git a/Curves/Scripts/Interfaces/IBezier3DSplineData.cs.meta b/Curves/Scripts/Interfaces/IBezier3DSplineData.cs.meta
new file mode 100644
index 0000000..706713d
--- /dev/null
+++ b/Curves/Scripts/Interfaces/IBezier3DSplineData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3d0c5d716c4067742b47ce276192a2da
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Interfaces/IReadOnly3DSplineData.cs b/Curves/Scripts/Interfaces/IReadOnly3DSplineData.cs
new file mode 100644
index 0000000..dccc7af
--- /dev/null
+++ b/Curves/Scripts/Interfaces/IReadOnly3DSplineData.cs
@@ -0,0 +1,45 @@
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// A read-only 3D-spline
+ ///
+ public interface IReadOnly3DSplineData
+ {
+ // Properties
+ bool IsClosed { get; }
+ int InterpolationStepsPerCurve { get; }
+ int CurveCount { get; }
+ int KnotCount { get; }
+ float TotalLength { get; }
+
+ // Helper
+ float GetNormalizedValueForSplineDistance(float splineDistance);
+ float GetCurveDistanceForSplineDistance(float splineDistance);
+ float GetSplineDistanceForKnotIndex(int index);
+ float GetSplineDistanceForCurveIndex(int index);
+ float GetSplineDistanceForNormalizedValue(float value);
+
+ // Orientation
+ Quaternion GetRotation(float splineDistance);
+ Quaternion GetRotationFast(float splineDistance);
+ Quaternion GetNormalizedRotation(float value);
+
+ // Position
+ Vector3 GetPosition(float splineDistance);
+ Vector3 GetNormalizedPosition(float value);
+
+ // Direction
+ Vector3 GetUp(float splineDistance);
+ Vector3 GetLeft(float splineDistance);
+ Vector3 GetRight(float splineDistance);
+ Vector3 GetForward(float splineDistance);
+ Vector3 GetForwardFast(float splineDistance);
+
+ // Knot
+ void AddKnot(Knot knot);
+ Knot GetKnot(int index);
+ void GetKnotIndicesForKnot(int knotIndex, out int preKnotIndex, out int postKnotIndex);
+ }
+}
diff --git a/Curves/Scripts/Interfaces/IReadOnly3DSplineData.cs.meta b/Curves/Scripts/Interfaces/IReadOnly3DSplineData.cs.meta
new file mode 100644
index 0000000..6d0c277
--- /dev/null
+++ b/Curves/Scripts/Interfaces/IReadOnly3DSplineData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 016cc8fc557b4e64ab4b5c2b12c5de3d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/JCMG.Curves.asmdef b/Curves/Scripts/JCMG.Curves.asmdef
new file mode 100644
index 0000000..3c4dcbb
--- /dev/null
+++ b/Curves/Scripts/JCMG.Curves.asmdef
@@ -0,0 +1,3 @@
+{
+ "name": "JCMG.Curves"
+}
diff --git a/Curves/Scripts/JCMG.Curves.asmdef.meta b/Curves/Scripts/JCMG.Curves.asmdef.meta
new file mode 100644
index 0000000..2df4aa7
--- /dev/null
+++ b/Curves/Scripts/JCMG.Curves.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 466f8dfa7a6bcc34f8b53de55b1bbd94
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/ScriptableObjects.meta b/Curves/Scripts/ScriptableObjects.meta
new file mode 100644
index 0000000..aab7d96
--- /dev/null
+++ b/Curves/Scripts/ScriptableObjects.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ceeaeda0ce5214242bb22e80a259bc2a
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/ScriptableObjects/Bezier3DSplineData.cs b/Curves/Scripts/ScriptableObjects/Bezier3DSplineData.cs
new file mode 100644
index 0000000..76858eb
--- /dev/null
+++ b/Curves/Scripts/ScriptableObjects/Bezier3DSplineData.cs
@@ -0,0 +1,1285 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// A Bezier 3D spline whose positions and rotations are set in World Space.
+ ///
+ [CreateAssetMenu(fileName = "Bezier3DSplineData", menuName = "JCMG/Curves/Bezier3DSplineData")]
+ public sealed class Bezier3DSplineData : ScriptableObject,
+ IBezier3DSplineData
+ {
+ #region Properties
+
+ ///
+ /// Returns true if the spline is a closed loop, otherwise false.
+ ///
+ public bool IsClosed
+ {
+ get { return _isClosed; }
+ }
+
+ ///
+ /// Returns the density of the curve caches. This determines the number of interpolation steps calculated
+ /// per curve.
+ ///
+ public int InterpolationStepsPerCurve
+ {
+ get { return _interpolationStepsPerCurve; }
+ }
+
+ ///
+ /// Returns the number of curves in the spline.
+ ///
+ public int CurveCount
+ {
+ get { return _curves.Length; }
+ }
+
+ ///
+ /// Returns the number of s in the spline.
+ ///
+ public int KnotCount
+ {
+ get { return _curves.Length + (IsClosed ? 0 : 1); }
+ }
+
+ ///
+ /// Returns the total length of the spline based on the length of all curves.
+ ///
+ public float TotalLength
+ {
+ get { return _totalLength; }
+ }
+
+ #endregion
+
+ #region Fields
+
+ #pragma warning disable 0649
+
+ [SerializeField]
+ private bool _isClosed;
+
+ [Min(10)]
+ [SerializeField]
+ private int _interpolationStepsPerCurve = 60;
+
+ [SerializeField]
+ private float _totalLength;
+
+ ///
+ /// Automatic knots don't have handles. Instead they have a percentage and adjust their handles accordingly. A
+ /// percentage of 0 indicates that this is not automatic
+ ///
+ [SerializeField]
+ private List _autoKnotsCache;
+
+ ///
+ /// Curves of the spline
+ ///
+ [SerializeField]
+ private Bezier3DCurve[] _curves;
+
+ ///
+ /// The cache of rotations for each knot.
+ ///
+ [SerializeField]
+ private List _knotRotations;
+
+ [SerializeField]
+ private Vector3[] tangentCache;
+
+ #pragma warning restore 0649
+
+ #endregion
+
+ #region Unity
+
+ private void OnEnable()
+ {
+ Init();
+ }
+
+ #endregion
+
+ #region Settings
+
+ ///
+ /// Recache all individual curves with new interpolation step count.
+ ///
+ /// Number of steps per curve to cache position and rotation.
+ public void SetStepsPerCurve(int stepCount)
+ {
+ _interpolationStepsPerCurve = stepCount;
+ for (var i = 0; i < CurveCount; i++)
+ {
+ _curves[i] = new Bezier3DCurve(
+ _curves[i].StartPoint,
+ _curves[i].FirstHandle,
+ _curves[i].SecondHandle,
+ _curves[i].EndPoint,
+ _interpolationStepsPerCurve);
+ }
+
+ _totalLength = GetTotalLength();
+ }
+
+ ///
+ /// Setting spline to closed will generate an extra curve, connecting end point to start point.
+ ///
+ public void SetClosed(bool isClosed)
+ {
+ if (isClosed != _isClosed)
+ {
+ _isClosed = isClosed;
+ if (isClosed)
+ {
+ var curveList = new List(_curves);
+ curveList.Add(
+ new Bezier3DCurve(
+ _curves[CurveCount - 1].EndPoint,
+ -_curves[CurveCount - 1].SecondHandle,
+ -_curves[0].FirstHandle,
+ _curves[0].StartPoint,
+ InterpolationStepsPerCurve));
+ _curves = curveList.ToArray();
+ }
+ else
+ {
+ var curveList = new List(_curves);
+ curveList.RemoveAt(CurveCount - 1);
+ _curves = curveList.ToArray();
+ }
+
+ RecalculateCurve();
+
+ _totalLength = GetTotalLength();
+ }
+ }
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// Returns a normalized value [0-1] based on the passed compared
+ /// to the of the spline.
+ ///
+ ///
+ ///
+ public float GetNormalizedValueForSplineDistance(float splineDistance)
+ {
+ return Mathf.Clamp01(splineDistance / TotalLength);
+ }
+
+ ///
+ /// Returns a normalized value [0-1] based on the passed compared
+ /// to the of the that the distance falls along
+ /// the spline.
+ ///
+ ///
+ ///
+ public float GetCurveDistanceForSplineDistance(float splineDistance)
+ {
+ var time = 0f;
+ var curveDistance = splineDistance;
+ for (var i = 0; i < CurveCount; i++)
+ {
+ if (_curves[i].Length < curveDistance)
+ {
+ curveDistance -= _curves[i].Length;
+ time += 1f / CurveCount;
+ }
+ else
+ {
+ time += _curves[i].ConvertDistanceToTime(curveDistance) / CurveCount;
+ return time;
+ }
+ }
+
+ return 1f;
+ }
+
+ ///
+ /// Returns the length of the spline leading up to and ending on the at
+ /// position in the collection.
+ ///
+ ///
+ ///
+ public float GetSplineDistanceForKnotIndex(int index)
+ {
+ float length;
+
+ GetCurveIndicesForKnot(index, out var preCurveIndex, out var postCurveIndex);
+ if (preCurveIndex == -1)
+ {
+ length = 0f;
+ }
+ else if (postCurveIndex == -1)
+ {
+ length = TotalLength;
+ }
+ else
+ {
+ return GetSplineDistanceForCurveIndex(preCurveIndex);
+ }
+
+ return length;
+ }
+
+ ///
+ /// Returns the length of the spline leading up to and ending at the end of the at
+ /// position in the collection.
+ ///
+ ///
+ ///
+ public float GetSplineDistanceForCurveIndex(int index)
+ {
+ var length = 0f;
+ for (var i = 0; i <= index; i++)
+ {
+ length += _curves[i].Length;
+ }
+
+ return length;
+ }
+
+ ///
+ /// Returns a set distance along the spline based along a normalized between [0-1].
+ ///
+ ///
+ ///
+ public float GetSplineDistanceForNormalizedValue(float value)
+ {
+ return Mathf.Clamp01(value) * TotalLength;
+ }
+
+ ///
+ /// Resets the spline back to its starting values.
+ ///
+ public void Reset()
+ {
+ Init(true);
+ }
+
+ ///
+ /// Initializes the starting values for the spline if not already set or if is set to
+ /// true.
+ ///
+ ///
+ private void Init(bool force = false)
+ {
+ if (_autoKnotsCache == null || force)
+ {
+ _autoKnotsCache = new List()
+ {
+ 0, 0
+ };
+ }
+
+ if (_curves == null || force)
+ {
+ _curves = new[]
+ {
+ new Bezier3DCurve(
+ new Vector3(-2, 0, 0),
+ new Vector3(0, 0, 2),
+ new Vector3(0, 0, -2),
+ new Vector3(2, 0, 0),
+ _interpolationStepsPerCurve)
+ };
+ }
+
+ if (_knotRotations == null || force)
+ {
+ _knotRotations = new List()
+ {
+ new NullableQuaternion(null), new NullableQuaternion(null)
+ };
+ }
+
+ if (force)
+ {
+ _interpolationStepsPerCurve = 60;
+ _isClosed = false;
+ }
+
+ RecalculateCurve();
+ }
+
+ ///
+ /// Recalculates the entire curve. Should only be used when changes to the curve would fundamentally change the
+ /// shape.
+ ///
+ private void RecalculateCurve()
+ {
+ for (var i = 0; i < KnotCount; i++)
+ {
+ var knot = GetKnot(i);
+
+ SetKnot(i, knot);
+ }
+
+ SetStepsPerCurve(InterpolationStepsPerCurve);
+ }
+
+ #endregion
+
+ #region Actions
+
+ ///
+ /// Flip the spline direction.
+ ///
+ public void Flip()
+ {
+ var curves = new Bezier3DCurve[CurveCount];
+ for (var i = 0; i < CurveCount; i++)
+ {
+ curves[CurveCount - 1 - i] = new Bezier3DCurve(
+ _curves[i].EndPoint,
+ _curves[i].SecondHandle,
+ _curves[i].FirstHandle,
+ _curves[i].StartPoint,
+ InterpolationStepsPerCurve);
+ }
+
+ _curves = curves;
+ _autoKnotsCache.Reverse();
+ _knotRotations.Reverse();
+ }
+
+ #endregion
+
+ #region Curve
+
+ ///
+ /// Get at position in the collection.
+ ///
+ public Bezier3DCurve GetCurve(int index)
+ {
+ if (index >= CurveCount || index < 0)
+ {
+ throw new IndexOutOfRangeException($"Curve index [{index}] out of range");
+ }
+
+ return _curves[index];
+ }
+
+ ///
+ /// Returns the where falls upon it along the spline;
+ /// and are initialized to the position in the collection
+ /// and the normalized value [0-1] of time through the curve.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Bezier3DCurve GetCurveIndexTime(float splineDist, out int index, out float curveTime)
+ {
+ Bezier3DCurve result;
+ for (var i = 0; i < CurveCount; i++)
+ {
+ result = _curves[i];
+ if (result.Length < splineDist)
+ {
+ splineDist -= result.Length;
+ }
+ else
+ {
+ index = i;
+ curveTime = result.ConvertDistanceToTime(splineDist);
+ return result;
+ }
+ }
+
+ index = CurveCount - 1;
+ result = _curves[index];
+ curveTime = 1f;
+ return result;
+ }
+
+ ///
+ /// Get the curve indices in direct contact with the at position
+ /// in the collection.
+ ///
+ public void GetCurveIndicesForKnot(int knotIndex, out int preCurveIndex, out int postCurveIndex)
+ {
+ //Get the curve index in direct contact with, before the knot
+ preCurveIndex = -1;
+ if (knotIndex != 0)
+ {
+ preCurveIndex = knotIndex - 1;
+ }
+ else if (IsClosed)
+ {
+ preCurveIndex = CurveCount - 1;
+ }
+
+ //Get the curve index in direct contact with, after the knot
+ postCurveIndex = -1;
+ if (knotIndex != CurveCount)
+ {
+ postCurveIndex = knotIndex;
+ }
+ else if (IsClosed)
+ {
+ postCurveIndex = 0;
+ }
+ }
+
+ #endregion
+
+ #region Rotation
+
+ ///
+ /// Returns rotation along spline at set distance along the .
+ ///
+ public Quaternion GetRotation(float splineDistance)
+ {
+ var forward = GetForward(splineDistance);
+ var up = GetUp(splineDistance, forward);
+ if (Math.Abs(forward.sqrMagnitude) > 0.00001f)
+ {
+ return Quaternion.LookRotation(forward, up);
+ }
+ else
+ {
+ return Quaternion.identity;
+ }
+ }
+
+ ///
+ /// Returns rotation along spline at set distance along the in the local
+ /// coordinate space of the passed .
+ ///
+ internal Quaternion GetRotation(float splineDistance, Transform transform)
+ {
+ var forward = GetForward(splineDistance);
+ var up = GetUp(splineDistance, forward, transform);
+ if (Math.Abs(forward.sqrMagnitude) > 0.00001f)
+ {
+ return Quaternion.LookRotation(forward, up);
+ }
+ else
+ {
+ return Quaternion.identity;
+ }
+ }
+
+ ///
+ /// Returns rotation along spline at set distance along the . Uses approximation.
+ ///
+ public Quaternion GetRotationFast(float splineDistance)
+ {
+ var forward = GetForwardFast(splineDistance);
+ var up = GetUp(splineDistance, forward);
+ if (Math.Abs(forward.sqrMagnitude) > 0.00001f)
+ {
+ return Quaternion.LookRotation(forward, up);
+ }
+ else
+ {
+ return Quaternion.identity;
+ }
+ }
+
+ ///
+ /// Returns rotation along spline at set distance along the in the local
+ /// coordinate space of the passed . Uses approximation.
+ ///
+ internal Quaternion GetRotationFast(float splineDistance, Transform transform)
+ {
+ var forward = GetForwardFast(splineDistance);
+ var up = GetUp(splineDistance, forward, transform);
+ if (Math.Abs(forward.sqrMagnitude) > 0.00001f)
+ {
+ return Quaternion.LookRotation(forward, up);
+ }
+ else
+ {
+ return Quaternion.identity;
+ }
+ }
+
+ ///
+ /// Returns rotation along spline at set distance along the in local coordinates.
+ /// Uses approximation.
+ ///
+ internal Quaternion GetRotationLocal(float splineDistance)
+ {
+ var forward = GetForwardLocal(splineDistance);
+ var up = GetUp(splineDistance, forward);
+ if (Math.Abs(forward.sqrMagnitude) > 0.00001f)
+ {
+ return Quaternion.LookRotation(forward, up);
+ }
+ else
+ {
+ return Quaternion.identity;
+ }
+ }
+
+ ///
+ /// Returns rotation along spline at set distance along the in local coordinates.
+ /// Uses approximation.
+ ///
+ internal Quaternion GetRotationLocalFast(float splineDistance)
+ {
+ var forward = GetForwardLocalFast(splineDistance);
+ var up = GetUp(splineDistance, forward);
+ if (Math.Abs(forward.sqrMagnitude) > 0.00001f)
+ {
+ return Quaternion.LookRotation(forward, up);
+ }
+ else
+ {
+ return Quaternion.identity;
+ }
+ }
+
+ ///
+ /// Returns a rotation along the spline where is a normalized value between [0-1] of
+ /// its .
+ ///
+ ///
+ ///
+ public Quaternion GetNormalizedRotation(float value)
+ {
+ var normalizedValue = Mathf.Clamp01(value);
+ var splineDistance = TotalLength * normalizedValue;
+
+ return GetRotation(splineDistance);
+ }
+
+ #endregion
+
+ #region Position
+
+ ///
+ /// Returns position along spline at set distance along the .
+ ///
+ public Vector3 GetPosition(float splineDistance)
+ {
+ return GetPositionLocal(splineDistance);
+ }
+
+ ///
+ /// Returns position along spline at set distance along the where the point is
+ /// transformed by the .
+ ///
+ internal Vector3 GetPosition(float splineDistance, Transform transform)
+ {
+ return transform.TransformPoint(GetPositionLocal(splineDistance));
+ }
+
+ ///
+ /// Returns position along spline at set distance along the .
+ ///
+ internal Vector3 GetPositionLocal(float splineDistance)
+ {
+ var curve = GetCurveDistance(splineDistance, out var curveDistance);
+ return curve.GetPoint(curve.ConvertDistanceToTime(curveDistance));
+ }
+
+ ///
+ /// Returns position along the spline where is a normalized value between [0-1] of
+ /// its .
+ ///
+ ///
+ ///
+ public Vector3 GetNormalizedPosition(float value)
+ {
+ var normalizedValue = Mathf.Clamp01(value);
+ var splineDistance = TotalLength * normalizedValue;
+
+ return GetPosition(splineDistance);
+ }
+
+ #endregion
+
+ #region Direction
+
+ ///
+ /// Returns up vector at set distance along the .
+ ///
+ public Vector3 GetUp(float splineDistance)
+ {
+ return GetUpLocal(splineDistance);
+ }
+
+ ///
+ /// Returns up vector at set distance along the where direction is transformed
+ /// based on the passed .
+ ///
+ internal Vector3 GetUp(float splineDistance, Transform transform)
+ {
+ return GetUp(splineDistance, GetForward(splineDistance, transform), transform);
+ }
+
+ ///
+ /// Returns up vector at set distance along the in local coordinates.
+ ///
+ internal Vector3 GetUpLocal(float splineDistance)
+ {
+ return GetUp(splineDistance, GetForward(splineDistance));
+ }
+
+ private Vector3 GetUp(float splineDistance, Vector3 tangent, Transform transform = null)
+ {
+ var t = GetCurveDistanceForSplineDistance(splineDistance);
+ t *= CurveCount;
+
+ var rotA = Quaternion.identity;
+ var rotB = Quaternion.identity;
+ var tA = 0;
+ var tB = 0;
+
+ // Find earlier rotations
+ var foundRotation = false;
+ var startIndex = Mathf.Min((int)t, CurveCount);
+ for (var i = startIndex; i >= 0; i--)
+ {
+ i = (int)Mathf.Repeat(i, KnotCount);
+
+ if (_knotRotations[i].HasValue)
+ {
+ rotA = _knotRotations[i].Value;
+ rotB = _knotRotations[i].Value;
+ tA = i;
+ tB = i;
+ foundRotation = true;
+ break;
+ }
+ }
+
+ // If we don't find any earlier rotations and the curve is closed, start over from the end to our original
+ // starting point.
+ if (!foundRotation && IsClosed)
+ {
+ for (var i = CurveCount - 1; i > startIndex; i--)
+ {
+ i = (int)Mathf.Repeat(i, KnotCount);
+
+ if (_knotRotations[i].HasValue)
+ {
+ rotA = _knotRotations[i].Value;
+ rotB = _knotRotations[i].Value;
+ tA = i;
+ tB = i;
+ break;
+ }
+ }
+ }
+
+ // Find later rotations
+ foundRotation = false;
+ var endIndex = Mathf.Max((int)t + 1, 0);
+ for (var i = endIndex; i < _knotRotations.Count; i++)
+ {
+ if (_knotRotations[i].HasValue)
+ {
+ rotB = _knotRotations[i].Value;
+ tB = i;
+ foundRotation = true;
+ break;
+ }
+ }
+
+ // If we don't find any later rotations and the curve is closed, start over from the beginning to our
+ // original starting point.
+ if (!foundRotation && IsClosed)
+ {
+ var upperLimit = Mathf.Min(_knotRotations.Count, endIndex);
+ for (var i = 0; i < upperLimit; i++)
+ {
+ if (_knotRotations[i].HasValue)
+ {
+ rotB = _knotRotations[i].Value;
+ tB = i;
+ break;
+ }
+ }
+ }
+
+ // If we end up finding we need to lerp between the end and beginning rotations, set the end index to the
+ // length of the knot/curve count
+ if (tA > tB)
+ {
+ tB = tA + 1;
+ }
+
+ t = Mathf.InverseLerp(tA, tB, t);
+ var rot = Quaternion.Lerp(rotA, rotB, t);
+
+ if (transform != null)
+ {
+ rot = transform.rotation * rot;
+ }
+
+ return Vector3.ProjectOnPlane(rot * Vector3.up, tangent).normalized;
+ }
+
+ ///
+ /// Returns left vector at set distance along the .
+ ///
+ public Vector3 GetLeft(float splineDistance)
+ {
+ return Vector3.Cross(GetForward(splineDistance), GetUp(splineDistance));
+ }
+
+ ///
+ /// Returns left vector at set distance along the where direction is transformed
+ /// based on the passed .
+ ///
+ internal Vector3 GetLeft(float splineDistance, Transform transform)
+ {
+ return Vector3.Cross(GetForward(splineDistance, transform), GetUp(splineDistance, transform));
+ }
+
+ ///
+ /// Returns left vector at set distance along the in local coordinates.
+ ///
+ internal Vector3 GetLeftLocal(float splineDistance)
+ {
+ return Vector3.Cross(GetForwardLocal(splineDistance), GetUpLocal(splineDistance));
+ }
+
+ ///
+ /// Returns right vector at set distance along the .
+ ///
+ public Vector3 GetRight(float splineDistance)
+ {
+ return -Vector3.Cross(GetForward(splineDistance), GetUp(splineDistance));
+ }
+
+ ///
+ /// Returns right vector at set distance along the where direction is transformed
+ /// based on the passed .
+ ///
+ internal Vector3 GetRight(float splineDistance, Transform transform)
+ {
+ return -GetLeft(splineDistance, transform);
+ }
+
+ ///
+ /// Returns right vector at set distance along the in local coordinates.
+ ///
+ internal Vector3 GetRightLocal(float splineDistance)
+ {
+ return -GetLeftLocal(splineDistance);
+ }
+
+ ///
+ /// Returns forward vector at set distance along the .
+ ///
+ public Vector3 GetForward(float splineDistance)
+ {
+ return GetForwardLocal(splineDistance);
+ }
+
+ ///
+ /// Returns forward vector at set distance along the where the direction is
+ /// transformed based on the passed .
+ ///
+ internal Vector3 GetForward(float splineDistance, Transform transform)
+ {
+ return transform.TransformDirection(GetForwardLocal(splineDistance));
+ }
+
+ ///
+ /// Returns forward vector at set distance along the . Uses approximation.
+ ///
+ public Vector3 GetForwardFast(float splineDistance)
+ {
+ return GetForwardLocalFast(splineDistance);
+ }
+
+ ///
+ /// Returns forward vector at set distance along the where the direction is
+ /// transformed based on the passed . Uses approximation.
+ ///
+ internal Vector3 GetForwardFast(float splineDistance, Transform transform)
+ {
+ return transform.TransformDirection(GetForwardLocal(splineDistance));
+ }
+
+ ///
+ /// Returns forward vector at set distance along the in local coordinates.
+ ///
+ internal Vector3 GetForwardLocal(float splineDistance)
+ {
+ var curve = GetCurveDistance(splineDistance, out var curveDistance);
+ return curve.GetForward(curve.ConvertDistanceToTime(curveDistance)).normalized;
+ }
+
+ ///
+ /// Returns forward vector at set distance along the in local coordinates. Uses
+ /// approximation.
+ ///
+ internal Vector3 GetForwardLocalFast(float splineDistance)
+ {
+ var curve = GetCurveDistance(splineDistance, out var curveDistance);
+ return curve.GetForwardFast(curve.ConvertDistanceToTime(curveDistance)).normalized;
+ }
+
+ #endregion
+
+ #region Knot
+
+ ///
+ /// Adds a new to the end of the spline.
+ ///
+ ///
+ public void AddKnot(Knot knot)
+ {
+ var curve = new Bezier3DCurve(
+ _curves[CurveCount - 1].EndPoint,
+ -_curves[CurveCount - 1].SecondHandle,
+ knot.handleIn,
+ knot.position,
+ InterpolationStepsPerCurve);
+
+ var curveList = new List(_curves);
+ curveList.Add(curve);
+ _curves = curveList.ToArray();
+
+ _autoKnotsCache.Add(knot.auto);
+ _knotRotations.Add(knot.rotation);
+
+ SetKnot(KnotCount - 1, knot);
+ }
+
+ ///
+ /// Returns info in local coordinates at the position in the collection.
+ ///
+ public Knot GetKnot(int index)
+ {
+ if (index == 0)
+ {
+ if (IsClosed)
+ {
+ return new Knot(
+ _curves[0].StartPoint,
+ _curves[CurveCount - 1].SecondHandle,
+ _curves[0].FirstHandle,
+ _autoKnotsCache[index],
+ _knotRotations[index].NullableValue);
+ }
+ else
+ {
+ return new Knot(
+ _curves[0].StartPoint,
+ Vector3.zero,
+ _curves[0].FirstHandle,
+ _autoKnotsCache[index],
+ _knotRotations[index].NullableValue);
+ }
+ }
+ else if (index == CurveCount)
+ {
+ return new Knot(
+ _curves[index - 1].EndPoint,
+ _curves[index - 1].SecondHandle,
+ Vector3.zero,
+ _autoKnotsCache[index],
+ _knotRotations[index].NullableValue);
+ }
+ else
+ {
+ return new Knot(
+ _curves[index].StartPoint,
+ _curves[index - 1].SecondHandle,
+ _curves[index].FirstHandle,
+ _autoKnotsCache[index],
+ _knotRotations[index].NullableValue);
+ }
+ }
+
+ ///
+ /// Inserts a new at the position in the collection.
+ ///
+ ///
+ ///
+ public void InsertKnot(int index, Knot knot)
+ {
+ Bezier3DCurve curve;
+ if (index == 0)
+ {
+ curve = new Bezier3DCurve(
+ knot.position,
+ knot.handleOut,
+ -_curves[0].FirstHandle,
+ _curves[0].StartPoint,
+ InterpolationStepsPerCurve);
+ }
+ else if (index == CurveCount)
+ {
+ curve = GetCurve(index - 1);
+ }
+ else
+ {
+ curve = GetCurve(index);
+ }
+
+ var curveList = new List(_curves);
+ curveList.Insert(index, curve);
+ _curves = curveList.ToArray();
+
+ _autoKnotsCache.Insert(index, knot.auto);
+ _knotRotations.Insert(index, knot.rotation);
+
+ SetKnot(index, knot);
+ }
+
+ ///
+ /// Removes the at the position in the collection.
+ ///
+ ///
+ public void RemoveKnot(int index)
+ {
+ if (index == 0)
+ {
+ var knot = GetKnot(1);
+
+ var curveList = new List(_curves);
+ curveList.RemoveAt(0);
+ _curves = curveList.ToArray();
+
+ _autoKnotsCache.RemoveAt(0);
+ _knotRotations.RemoveAt(0);
+
+ SetKnot(0, knot);
+ }
+ else if (index == CurveCount)
+ {
+ var curveList = new List(_curves);
+ curveList.RemoveAt(index - 1);
+ _curves = curveList.ToArray();
+
+ _autoKnotsCache.RemoveAt(index);
+ _knotRotations.RemoveAt(index);
+
+ if (Math.Abs(_autoKnotsCache[KnotCount - 1]) > 0.00001f)
+ {
+ SetKnot(KnotCount - 1, GetKnot(KnotCount - 1));
+ }
+ }
+ else
+ {
+ int preCurveIndex, postCurveIndex;
+ GetCurveIndicesForKnot(index, out preCurveIndex, out postCurveIndex);
+
+ var curve = new Bezier3DCurve(
+ _curves[preCurveIndex].StartPoint,
+ _curves[preCurveIndex].FirstHandle,
+ _curves[postCurveIndex].SecondHandle,
+ _curves[postCurveIndex].EndPoint,
+ InterpolationStepsPerCurve);
+
+ _curves[preCurveIndex] = curve;
+
+ var curveList = new List(_curves);
+ curveList.RemoveAt(postCurveIndex);
+ _curves = curveList.ToArray();
+
+ _autoKnotsCache.RemoveAt(index);
+ _knotRotations.RemoveAt(index);
+
+ int preKnotIndex, postKnotIndex;
+ GetKnotIndicesForKnot(index, out preKnotIndex, out postKnotIndex);
+
+ SetKnot(preKnotIndex, GetKnot(preKnotIndex));
+ }
+ }
+
+ ///
+ /// Set info in local coordinates at the
+ /// position in the collection.
+ ///
+ public void SetKnot(int index, Knot knot)
+ {
+ //If knot is set to auto, adjust handles accordingly
+ _knotRotations[index] = knot.rotation;
+ _autoKnotsCache[index] = knot.auto;
+ if (knot.IsUsingAutoHandles)
+ {
+ PositionAutoHandles(index, ref knot);
+ }
+
+ //Automate knots around this knot
+ int preKnotIndex, postKnotIndex;
+ GetKnotIndicesForKnot(index, out preKnotIndex, out postKnotIndex);
+
+ var preKnot = new Knot();
+ if (preKnotIndex != -1)
+ {
+ preKnot = GetKnot(preKnotIndex);
+ if (preKnot.IsUsingAutoHandles)
+ {
+ int preKnotPreCurveIndex, preKnotPostCurveIndex;
+ GetCurveIndicesForKnot(preKnotIndex, out preKnotPreCurveIndex, out preKnotPostCurveIndex);
+ if (preKnotPreCurveIndex != -1)
+ {
+ PositionAutoHandles(
+ preKnotIndex,
+ ref preKnot,
+ _curves[preKnotPreCurveIndex].StartPoint,
+ knot.position);
+ _curves[preKnotPreCurveIndex] = new Bezier3DCurve(
+ _curves[preKnotPreCurveIndex].StartPoint,
+ _curves[preKnotPreCurveIndex].FirstHandle,
+ preKnot.handleIn,
+ preKnot.position,
+ InterpolationStepsPerCurve);
+ }
+ else
+ {
+ PositionAutoHandles(
+ preKnotIndex,
+ ref preKnot,
+ Vector3.zero,
+ knot.position);
+ }
+ }
+ }
+
+ var postKnot = new Knot();
+ if (postKnotIndex != -1)
+ {
+ postKnot = GetKnot(postKnotIndex);
+ if (postKnot.IsUsingAutoHandles)
+ {
+ int postKnotPreCurveIndex, postKnotPostCurveIndex;
+ GetCurveIndicesForKnot(postKnotIndex, out postKnotPreCurveIndex, out postKnotPostCurveIndex);
+ if (postKnotPostCurveIndex != -1)
+ {
+ PositionAutoHandles(
+ postKnotIndex,
+ ref postKnot,
+ knot.position,
+ _curves[postKnotPostCurveIndex].EndPoint);
+ _curves[postKnotPostCurveIndex] = new Bezier3DCurve(
+ postKnot.position,
+ postKnot.handleOut,
+ _curves[postKnotPostCurveIndex].SecondHandle,
+ _curves[postKnotPostCurveIndex].EndPoint,
+ InterpolationStepsPerCurve);
+ }
+ else
+ {
+ PositionAutoHandles(
+ postKnotIndex,
+ ref postKnot,
+ knot.position,
+ Vector3.zero);
+ }
+ }
+ }
+
+ //Get the curve indices in direct contact with knot
+ int preCurveIndex, postCurveIndex;
+ GetCurveIndicesForKnot(index, out preCurveIndex, out postCurveIndex);
+
+ //Adjust curves in direct contact with the knot
+ if (preCurveIndex != -1)
+ {
+ _curves[preCurveIndex] = new Bezier3DCurve(
+ preKnot.position,
+ preKnot.handleOut,
+ knot.handleIn,
+ knot.position,
+ InterpolationStepsPerCurve);
+ }
+
+ if (postCurveIndex != -1)
+ {
+ _curves[postCurveIndex] = new Bezier3DCurve(
+ knot.position,
+ knot.handleOut,
+ postKnot.handleIn,
+ postKnot.position,
+ InterpolationStepsPerCurve);
+ }
+
+ _totalLength = GetTotalLength();
+ }
+
+ ///
+ /// Get the knot indices in direct contact with knot. If a knot is not found before and/or after, that index
+ /// will be initialized to -1.
+ ///
+ public void GetKnotIndicesForKnot(int knotIndex, out int preKnotIndex, out int postKnotIndex)
+ {
+ //Get the curve index in direct contact with, before the knot
+ preKnotIndex = -1;
+ if (knotIndex != 0)
+ {
+ preKnotIndex = knotIndex - 1;
+ }
+ else if (IsClosed)
+ {
+ preKnotIndex = KnotCount - 1;
+ }
+
+ //Get the curve index in direct contact with, after the knot
+ postKnotIndex = -1;
+ if (knotIndex != KnotCount - 1)
+ {
+ postKnotIndex = knotIndex + 1;
+ }
+ else if (IsClosed)
+ {
+ postKnotIndex = 0;
+ }
+ }
+
+ #endregion
+
+ #region Private
+
+ ///
+ /// Returns the appropriate curve based on the passed spline distance where that distance falls on that curve;
+ /// will be initialized to a clamped distance along the returned curve.
+ ///
+ ///
+ ///
+ ///
+ private Bezier3DCurve GetCurveDistance(float splineDist, out float curveDist)
+ {
+ for (var i = 0; i < CurveCount; i++)
+ {
+ if (_curves[i].Length < splineDist)
+ {
+ splineDist -= _curves[i].Length;
+ }
+ else
+ {
+ curveDist = splineDist;
+ return _curves[i];
+ }
+ }
+
+ curveDist = _curves[CurveCount - 1].Length;
+ return _curves[CurveCount - 1];
+ }
+
+ ///
+ /// Position handles automatically based on start and end point positions of the curve.
+ ///
+ private void PositionAutoHandles(int index, ref Knot knot)
+ {
+ // Terminology: Points are referred to as A B and C
+ // A = prev point, B = current point, C = next point
+ Vector3 prevPos;
+ if (index != 0)
+ {
+ prevPos = _curves[index - 1].StartPoint;
+ }
+ else if (IsClosed)
+ {
+ prevPos = _curves[CurveCount - 1].StartPoint;
+ }
+ else
+ {
+ prevPos = Vector3.zero;
+ }
+
+ Vector3 nextPos;
+ if (index != KnotCount - 1)
+ {
+ nextPos = _curves[index].EndPoint;
+ }
+ else if (IsClosed)
+ {
+ nextPos = _curves[0].StartPoint;
+ }
+ else
+ {
+ nextPos = Vector3.zero;
+ }
+
+ PositionAutoHandles(
+ index,
+ ref knot,
+ prevPos,
+ nextPos);
+ }
+
+ ///
+ /// Position handles automatically based on start and end point positions of the curve.
+ ///
+ private void PositionAutoHandles(int index, ref Knot knot, Vector3 prevPos, Vector3 nextPos)
+ {
+ // Terminology: Points are referred to as A B and C
+ // A = prev point, B = current point, C = next point
+ var amount = knot.auto;
+
+ // Calculate directional vectors
+ var AB = knot.position - prevPos;
+ var CB = knot.position - nextPos;
+
+ // Calculate the across vector
+ var AB_CB = (CB.normalized - AB.normalized).normalized;
+
+ if (!IsClosed)
+ {
+ if (index == 0)
+ {
+ knot.handleOut = CB * -amount;
+ }
+ else if (index == CurveCount)
+ {
+ knot.handleIn = AB * -amount;
+ }
+ else
+ {
+ knot.handleOut = -AB_CB * CB.magnitude * amount;
+ knot.handleIn = AB_CB * AB.magnitude * amount;
+ }
+ }
+ else
+ {
+ if (KnotCount == 2)
+ {
+ var left = new Vector3(AB.z, 0, -AB.x) * amount;
+ if (index == 0)
+ {
+ knot.handleIn = left;
+ knot.handleOut = -left;
+ }
+
+ if (index == 1)
+ {
+ knot.handleIn = left;
+ knot.handleOut = -left;
+ }
+ }
+ else
+ {
+ knot.handleIn = AB_CB * AB.magnitude * amount;
+ knot.handleOut = -AB_CB * CB.magnitude * amount;
+ }
+ }
+ }
+
+ ///
+ /// Calculates the total length of the spline based on the aggregated length of the curves.
+ ///
+ ///
+ private float GetTotalLength()
+ {
+ var length = 0f;
+ for (var i = 0; i < CurveCount; i++)
+ {
+ length += _curves[i].Length;
+ }
+
+ return length;
+ }
+
+ #endregion
+ }
+}
diff --git a/Curves/Scripts/ScriptableObjects/Bezier3DSplineData.cs.meta b/Curves/Scripts/ScriptableObjects/Bezier3DSplineData.cs.meta
new file mode 100644
index 0000000..97a7ff2
--- /dev/null
+++ b/Curves/Scripts/ScriptableObjects/Bezier3DSplineData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d1ae1d2d83c13f8419ed3ac9e3124a9e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: c5c32b6c450791943b0b231a833c00b6, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Tools.meta b/Curves/Scripts/Tools.meta
new file mode 100644
index 0000000..d8715b2
--- /dev/null
+++ b/Curves/Scripts/Tools.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4af500f8c0ce5f24e9044eed552122db
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Curves/Scripts/Tools/SceneGUITools.cs b/Curves/Scripts/Tools/SceneGUITools.cs
new file mode 100644
index 0000000..f12afe0
--- /dev/null
+++ b/Curves/Scripts/Tools/SceneGUITools.cs
@@ -0,0 +1,60 @@
+using UnityEngine;
+
+namespace JCMG.Curves
+{
+ ///
+ /// Helper methods for drawing in the scene
+ ///
+ internal static class SceneGUITools
+ {
+ public static void DrawCurveLinesGizmos(IBezier3DSplineData splineData, Transform transform = null)
+ {
+ Gizmos.color = Color.white;
+
+ //Loop through each curve in spline
+ var segments = splineData.InterpolationStepsPerCurve;
+ var spacing = 1f / segments;
+ for (var i = 0; i < splineData.CurveCount; i++)
+ {
+ var curve = splineData.GetCurve(i);
+
+ //Get curve in world space
+ Vector3 a, b, c, d;
+
+ if (transform != null)
+ {
+ a = transform.TransformPoint(curve.StartPoint);
+ b = transform.TransformPoint(curve.FirstHandle + curve.StartPoint);
+ c = transform.TransformPoint(curve.SecondHandle + curve.EndPoint);
+ d = transform.TransformPoint(curve.EndPoint);
+ }
+ else
+ {
+ a = curve.StartPoint;
+ b = curve.FirstHandle + curve.StartPoint;
+ c = curve.SecondHandle + curve.EndPoint;
+ d = curve.EndPoint;
+ }
+
+ var prev = Bezier3DCurve.GetPoint(
+ a,
+ b,
+ c,
+ d,
+ 0f);
+
+ for (var k = 0; k <= segments; k++)
+ {
+ var cur = Bezier3DCurve.GetPoint(
+ a,
+ b,
+ c,
+ d,
+ k * spacing);
+ Gizmos.DrawLine(prev, cur);
+ prev = cur;
+ }
+ }
+ }
+ }
+}
diff --git a/Curves/Scripts/Tools/SceneGUITools.cs.meta b/Curves/Scripts/Tools/SceneGUITools.cs.meta
new file mode 100644
index 0000000..4999f24
--- /dev/null
+++ b/Curves/Scripts/Tools/SceneGUITools.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 677a367687bfeda49801913ec18d9363
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ed172fe
--- /dev/null
+++ b/package.json
@@ -0,0 +1 @@
+{"name":"com.jeffcampbellmakesgames.curves","displayName":"JCMG Curves","version":"1.0.0","unity":"2019.3","description":"JCMG Curves is a 3D Bezier curve library focused on ease of use in both its API and Unity Editor integration.","keywords":["Curve","Curves"],"category":""}
\ No newline at end of file
diff --git a/package.json.meta b/package.json.meta
new file mode 100644
index 0000000..f71a48c
--- /dev/null
+++ b/package.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 15dd5b8a156150e47953a6e3f4adbec5
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant: