using System;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
using UnityEditor.EditorTools;
using UnityEditor.SettingsManagement;
using UnityEngine.Splines;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
struct SplineCurveHit
{
public float T;
public float3 Normal;
public float3 Position;
public SelectableKnot PreviousKnot;
public SelectableKnot NextKnot;
}
///
/// Editor utility functions for working with and .
///
public static class EditorSplineUtility
{
///
/// Invoked once per-frame if a spline property has been modified.
///
[Obsolete("Use AfterSplineWasModified instead.", false)]
public static event Action afterSplineWasModified;
///
/// Invoked once per-frame if a spline property has been modified.
///
public static event Action AfterSplineWasModified;
static readonly List s_SplinePtrBuffer = new List(16);
internal static event Action knotInserted;
internal static event Action knotRemoved;
static readonly List s_ElementBuffer = new List();
[UserSetting]
internal static Pref s_DefaultTangentMode = new("Splines.DefaultTangentMode", TangentMode.AutoSmooth);
///
/// Represents the default TangentMode used to place or insert knots. If the user does not define tangent
/// handles, then the tangent takes the default TangentMode.
///
public static TangentMode DefaultTangentMode => s_DefaultTangentMode;
static EditorSplineUtility()
{
Spline.afterSplineWasModified += (spline) =>
{
afterSplineWasModified?.Invoke(spline);
AfterSplineWasModified?.Invoke(spline);
};
}
///
/// Use this function to register a callback that gets invoked
/// once per-frame if any changes occur.
///
/// The callback to register.
///
/// The type parameter of .
///
public static void RegisterSplineDataChanged(Action> action)
{
SplineData.afterSplineDataWasModified += action;
}
///
/// Use this function to unregister change callback.
///
/// The callback to unregister.
///
/// The type parameter of .
///
public static void UnregisterSplineDataChanged(Action> action)
{
SplineData.afterSplineDataWasModified -= action;
}
internal static IReadOnlyList GetSplinesFromTargetsInternal(IEnumerable targets)
{
GetSplinesFromTargets(targets, s_SplinePtrBuffer);
return s_SplinePtrBuffer;
}
///
/// Get a representation of the splines in a list of targets.
///
/// A list of Objects inheriting from .
/// An array to store the of splines found in the targets.
internal static SplineInfo[] GetSplinesFromTargets(IEnumerable targets)
{
return GetSplinesFromTargetsInternal(targets).ToArray();
}
///
/// Get a representation of the splines in a list of targets.
///
/// A list of Objects inheriting from .
/// A list to store the of splines found in the targets.
internal static void GetSplinesFromTargets(IEnumerable targets, List results)
{
results.Clear();
foreach (var target in targets)
GetSplineInfosFromContainer(target, results);
}
///
/// Get a representation of the splines in a target.
///
/// An Object inheriting from .
/// A list to store the of splines found in the targets.
internal static void GetSplinesFromTarget(Object target, List results)
{
results.Clear();
GetSplineInfosFromContainer(target, results);
}
///
/// Get a representation of the splines in a target.
///
/// An Object inheriting from .
/// A list to store the of splines found in the targets.
/// True if a at least a spline was found in the target.
internal static bool TryGetSplinesFromTarget(Object target, List results)
{
results.Clear();
GetSplineInfosFromContainer(target, results);
return results.Count > 0;
}
///
/// Get a representation of the splines in a target.
///
/// An Object inheriting from .
/// An array to store the of splines found in the target.
internal static SplineInfo[] GetSplinesFromTarget(Object target)
{
GetSplinesFromTarget(target, s_SplinePtrBuffer);
return s_SplinePtrBuffer.ToArray();
}
///
/// Get a representation of the first spline found in the target.
///
/// An Object inheriting from .
/// The of the first spline found in the target.
/// True if a spline was found in the target.
internal static bool TryGetSplineFromTarget(Object target, out SplineInfo splineInfo)
{
GetSplinesFromTarget(target, s_SplinePtrBuffer);
if (s_SplinePtrBuffer.Count > 0)
{
splineInfo = s_SplinePtrBuffer[0];
return true;
}
splineInfo = default;
return false;
}
///
/// Sets the current active context to the and the current active tool to the
/// Draw Splines Tool ( )
///
public static void SetKnotPlacementTool()
{
if(ToolManager.activeContextType != typeof(SplineToolContext))
{
ToolManager.SetActiveContext();
ToolManager.SetActiveTool();
}
else if(ToolManager.activeToolType != typeof(KnotPlacementTool))
{
ToolManager.SetActiveTool();
}
}
static void GetSplineInfosFromContainer(Object target, List results)
{
if (target != null && target is ISplineContainer container)
{
var splines = container.Splines;
for (int i = 0; i < splines.Count; ++i)
results.Add(new SplineInfo(container, i));
}
}
internal static Bounds GetBounds(IReadOnlyList splines)
{
Bounds bounds = default;
bool initialized = false;
for (int i = 0; i < splines.Count; ++i)
{
var spline = splines[i].Spline;
if (spline.Count > 0 && !initialized)
bounds = spline.GetBounds(splines[i].LocalToWorld);
else
bounds.Encapsulate(spline.GetBounds(splines[i].LocalToWorld));
}
return bounds;
}
internal static Bounds GetElementBounds(IReadOnlyList elements, bool useKnotPositionForTangents)
where T : ISelectableElement
{
if (elements == null)
throw new ArgumentNullException(nameof(elements));
if (elements.Count == 0)
return new Bounds(Vector3.positiveInfinity, Vector3.zero);
var element = elements[0];
var position = (useKnotPositionForTangents && element is SelectableTangent tangent)
? tangent.Owner.Position
: element.Position;
Bounds bounds = new Bounds(position, Vector3.zero);
for (int i = 1; i < elements.Count; ++i)
{
element = elements[i];
if (useKnotPositionForTangents && element is SelectableTangent t)
bounds.Encapsulate(t.Owner.Position);
else
bounds.Encapsulate(element.Position);
}
return bounds;
}
internal static SelectableKnot GetKnot(T element)
where T : ISelectableElement
{
return new SelectableKnot(element.SplineInfo, element.KnotIndex);
}
internal static void RecordSelection(string name)
{
Undo.RecordObjects(SplineSelection.GetAllSelectedTargets(), name);
}
internal static void RecordObjects(IReadOnlyList elements, string name)
where T : ISelectableElement
{
foreach (var spline in GetSplines(elements))
RecordObject(spline, name);
}
internal static void RecordObject(SplineInfo splineInfo, string name)
{
if (splineInfo.Container is Object target && target != null)
Undo.RecordObject(target, name);
}
internal static SplineInfo CreateSpline(ISplineContainer container)
{
container.AddSpline();
return new SplineInfo(container, container.Splines.Count - 1);
}
internal static SelectableKnot CreateSpline(SelectableKnot from, float3 tangentOut)
{
var splineInfo = CreateSpline(from.SplineInfo.Container);
var knot = AddKnotToTheEnd(splineInfo, from.Position, math.mul(from.Rotation, math.up()), tangentOut, false);
LinkKnots(knot, from);
return knot;
}
internal static TangentMode GetModeFromPlacementTangent(float3 tangent)
{
return math.lengthsq(tangent) < float.Epsilon ? DefaultTangentMode : TangentMode.Mirrored;
}
static SelectableKnot AddKnotInternal(SplineInfo splineInfo, float3 worldPosition, float3 normal, float3 tangentOut, int index, int previousIndex, bool updateSelection)
{
var spline = splineInfo.Spline;
if (spline.Closed && spline.Count >= 2)
Debug.LogWarning("Cannot add a point to the extremity of a closed spline.");
var localToWorld = splineInfo.LocalToWorld;
var mode = GetModeFromPlacementTangent(tangentOut);
var localPosition = math.transform(math.inverse(splineInfo.LocalToWorld), worldPosition);
quaternion localRotation;
BezierKnot newKnot;
// If we're in AutoSmooth mode
if (!SplineUtility.AreTangentsModifiable(mode))
newKnot = SplineUtility.GetAutoSmoothKnot(localPosition, previousIndex != -1 ? spline[previousIndex].Position : localPosition, localPosition, normal);
else
{
localRotation = math.mul(math.inverse(math.quaternion(localToWorld)), quaternion.LookRotationSafe(tangentOut, normal));
var tangentMagnitude = math.length(tangentOut);
// Tangents are always assumed to be +/- forward when TangentMode is not Broken.
var localTangentIn = new float3(0f, 0f, -tangentMagnitude);
var localTangentOut = new float3(0f, 0f, tangentMagnitude);
newKnot = new BezierKnot(localPosition, localTangentIn, localTangentOut, localRotation);
}
spline.Insert(index, newKnot, mode);
// When appending a knot, update the previous knot with an average rotation accounting for the new point.
// This is assuming that if the previous knot is Continuous the rotation was explicitly set, and thus will
// not update the rotation.
if (spline.Count > 1 && !SplineUtility.AreTangentsModifiable(spline.GetTangentMode(previousIndex)))
{
// calculate rotation from the average direction from points p0 -> p1 -> p2
BezierKnot current = spline[previousIndex];
BezierKnot previous = spline.Previous(previousIndex);
BezierKnot next = spline.Next(previousIndex);
current.Rotation = CalculateKnotRotation(previous.Position, current.Position, next.Position, normal);
spline[previousIndex] = current;
}
// If the element is part of a prefab, the changes have to be recorded AFTER being done on the prefab instance
// otherwise they would not be saved in the scene.
PrefabUtility.RecordPrefabInstancePropertyModifications(splineInfo.Object);
var knot = new SelectableKnot(splineInfo, index);
if(updateSelection)
SplineSelection.Set(knot);
return knot;
}
internal static SelectableKnot AddKnotToTheEnd(SplineInfo splineInfo, float3 worldPosition, float3 normal, float3 tangentOut, bool updateSelection = true)
{
return AddKnotInternal(splineInfo, worldPosition, normal, tangentOut, splineInfo.Spline.Count, splineInfo.Spline.Count - 1, updateSelection);
}
internal static SelectableKnot AddKnotToTheStart(SplineInfo splineInfo, float3 worldPosition, float3 normal, float3 tangentIn, bool updateSelection = true)
{
return AddKnotInternal(splineInfo, worldPosition, normal, -tangentIn, 0, 1, updateSelection);
}
internal static void RemoveKnot(SelectableKnot knot)
{
knot.SplineInfo.Spline.RemoveAt(knot.KnotIndex);
//Force to record changes if part of a prefab instance
PrefabUtility.RecordPrefabInstancePropertyModifications(knot.SplineInfo.Object);
knotRemoved?.Invoke(knot);
}
internal static bool ShouldRemoveSpline(SplineInfo splineInfo)
{
// Spline is Empty
if (splineInfo.Spline.Count == 0)
return true;
// Spline has one knot that is linked to another. This makes it "hidden" to the user because it's on top of another knot without having a curve associated with it.
if (splineInfo.Spline.Count == 1 && splineInfo.Container.KnotLinkCollection.TryGetKnotLinks(new SplineKnotIndex(splineInfo.Index, 0), out _))
return true;
return false;
}
// Zero out the given tangent and switch knot's tangent mode to Continous if it's Mirrored.
internal static void ClearTangent(SelectableTangent tangent)
{
var spline = tangent.SplineInfo.Spline;
var knotIndex = tangent.Owner.KnotIndex;
var selectableKnot = tangent.Owner;
var bezierKnot = spline[knotIndex];
if (selectableKnot.Mode == TangentMode.Mirrored)
selectableKnot.Mode = TangentMode.Continuous;
switch (tangent.TangentIndex)
{
case (int)BezierTangent.In:
bezierKnot.TangentIn = float3.zero;
break;
case (int)BezierTangent.Out:
bezierKnot.TangentOut = float3.zero;
break;
}
spline[knotIndex] = bezierKnot;
}
///
/// Returns the interpolation value that corresponds to the middle (distance wise) of the curve.
/// If spline and curveIndex are provided, the function leverages the spline's LUTs, otherwise the LUT is built on the fly.
///
/// The curve to evaluate.
/// The ISpline that curve belongs to. Not used if curve is not part of any spline.
/// The index of the curve if it's part of the spine.
/// A type implementing ISpline.
internal static float GetCurveMiddleInterpolation(BezierCurve curve, T spline, int curveIndex) where T: ISpline
{
var curveMidT = 0f;
if (curveIndex >= 0)
curveMidT = spline.GetCurveInterpolation(curveIndex, spline.GetCurveLength(curveIndex) * 0.5f);
else
curveMidT = CurveUtility.GetDistanceToInterpolation(curve, CurveUtility.ApproximateLength(curve) * 0.5f);
return curveMidT;
}
static BezierCurve GetPreviewCurveInternal(SplineInfo info, int from, float3 fromWorldTangent, float3 toWorldPoint, float3 toWorldTangent, TangentMode toMode, int previousIndex)
{
var spline = info.Spline;
var trs = info.Transform.localToWorldMatrix;
var aMode = spline.GetTangentMode(from);
var bMode = toMode;
var p0 = math.transform(trs, spline[from].Position);
var p1 = math.transform(trs, spline[from].Position + math.mul(spline[from].Rotation, fromWorldTangent));
var p3 = toWorldPoint;
var p2 = p3 - toWorldTangent;
if (!SplineUtility.AreTangentsModifiable(aMode))
p1 = aMode == TangentMode.Linear ? p0 : p0 + SplineUtility.GetAutoSmoothTangent(math.transform(trs, spline[previousIndex].Position), p0, p3, SplineUtility.CatmullRomTension);
if (!SplineUtility.AreTangentsModifiable(bMode))
p2 = bMode == TangentMode.Linear ? p3 : p3 + SplineUtility.GetAutoSmoothTangent(p3, p3, p0, SplineUtility.CatmullRomTension);
return new BezierCurve(p0, p1, p2, p3);
}
// Calculate the curve control points in world space given a new end knot.
internal static BezierCurve GetPreviewCurveFromEnd(SplineInfo info, int from, float3 toWorldPoint, float3 toWorldTangent, TangentMode toMode)
{
var tangentOut = info.Spline[from].TangentOut;
if(info.Spline.Closed && (from == 0 || AreKnotLinked( new SelectableKnot(info, from), new SelectableKnot(info, 0))))
{
var fromKnot = info.Spline[from];
tangentOut = -fromKnot.TangentIn;
}
return GetPreviewCurveInternal(info, from, tangentOut, toWorldPoint, toWorldTangent, toMode, info.Spline.PreviousIndex(from));
}
// Calculate the curve control points in world space given a new start knot.
internal static BezierCurve GetPreviewCurveFromStart(SplineInfo info, int from, float3 toWorldPoint, float3 toWorldTangent, TangentMode toMode)
{
var tangentIn = info.Spline[from].TangentIn;
if(info.Spline.Closed && (from == info.Spline.Count - 1 || AreKnotLinked( new SelectableKnot(info, from), new SelectableKnot(info, info.Spline.Count - 1))))
{
var fromKnot = info.Spline[from];
tangentIn = -fromKnot.TangentOut;
}
return GetPreviewCurveInternal(info, from, tangentIn, toWorldPoint, toWorldTangent, toMode, info.Spline.NextIndex(from));
}
internal static quaternion CalculateKnotRotation(float3 previous, float3 position, float3 next, float3 normal)
{
float3 tangent = new float3(0f, 0f, 1f);
bool hasPrevious = math.distancesq(position, previous) > float.Epsilon;
bool hasNext = math.distancesq(position, next) > float.Epsilon;
if (hasPrevious && hasNext)
tangent = ((position - previous) + (next - position)) * 5f;
else if (hasPrevious)
tangent = position - previous;
else if (hasNext)
tangent = next - position;
return SplineUtility.GetKnotRotation(tangent, normal);
}
//Get the affected curves when trying to add a knot on an existing segment
//internal static void GetAffectedCurves(SplineCurveHit hit, Dictionary>> affectedCurves)
internal static void GetAffectedCurves(SplineCurveHit hit, List<(Spline s, int index, List knots)> affectedCurves)
{
var spline = hit.PreviousKnot.SplineInfo.Spline;
var curveIndex = hit.PreviousKnot.KnotIndex;
var hitLocalPosition = hit.PreviousKnot.SplineInfo.Transform.InverseTransformPoint(hit.Position);
var previewKnots = new List();
var sKnot = hit.PreviousKnot;
var bKnot = new BezierKnot(sKnot.LocalPosition, sKnot.TangentIn.LocalPosition, sKnot.TangentOut.LocalPosition, sKnot.LocalRotation);
var insertedKnot = GetInsertedKnotPreview(hit.PreviousKnot.SplineInfo, hit.NextKnot.KnotIndex, hit.T, out var leftTangent, out var rightTangent);
if(spline.GetTangentMode(sKnot.KnotIndex) == TangentMode.AutoSmooth)
{
var previousKnot = spline.Previous(sKnot.KnotIndex);
var previousKnotIndex = spline.PreviousIndex(sKnot.KnotIndex);
bKnot = SplineUtility.GetAutoSmoothKnot(sKnot.LocalPosition, previousKnot.Position, hitLocalPosition);
affectedCurves.Add((spline, previousKnotIndex, new List() { previousKnot, bKnot }));
}
else
bKnot.TangentOut = math.mul(math.inverse(sKnot.LocalRotation), leftTangent);
previewKnots.Add(bKnot);
previewKnots.Add(insertedKnot);
var affectedCurveIndex = affectedCurves.FindIndex(x => x.s == spline && x.index == curveIndex);
if(affectedCurveIndex >= 0)
affectedCurves.RemoveAt(affectedCurveIndex);
affectedCurves.Add((spline, curveIndex, previewKnots));
sKnot = hit.NextKnot;
bKnot = new BezierKnot(sKnot.LocalPosition, sKnot.TangentIn.LocalPosition, sKnot.TangentOut.LocalPosition, sKnot.LocalRotation);
if(spline.GetTangentMode(sKnot.KnotIndex) == TangentMode.AutoSmooth)
{
var nextKnot = spline.Next(sKnot.KnotIndex);
bKnot = SplineUtility.GetAutoSmoothKnot(sKnot.LocalPosition, hitLocalPosition, nextKnot.Position);
affectedCurves.Add((spline, sKnot.KnotIndex, new List() { bKnot, nextKnot }));
}
else
bKnot.TangentIn = math.mul(math.inverse(sKnot.LocalRotation), rightTangent);
previewKnots.Add(bKnot);
}
internal static void GetAffectedCurves(SplineInfo splineInfo, Vector3 knotPosition, bool addingToStart, SelectableKnot lastKnot, int previousKnotIndex, List<(Spline s, int index, List knots)> affectedCurves)
{
var spline = splineInfo.Spline;
if (spline != null)
{
var lastKnotIndex = lastKnot.KnotIndex;
var affectedCurveIndex = affectedCurves.FindIndex(x => x.s == spline && x.index == (addingToStart ? lastKnotIndex : previousKnotIndex));
var lastTangentMode = spline.GetTangentMode(lastKnotIndex);
if(lastTangentMode == TangentMode.AutoSmooth)
{
var previousKnot = spline[previousKnotIndex];
var autoSmoothKnot = addingToStart ?
SplineUtility.GetAutoSmoothKnot(lastKnot.LocalPosition, knotPosition, previousKnot.Position) :
SplineUtility.GetAutoSmoothKnot(lastKnot.LocalPosition, previousKnot.Position, knotPosition);
if (affectedCurveIndex < 0)
{
if (addingToStart)
affectedCurves.Insert(0, (spline, lastKnot.KnotIndex, new List() { autoSmoothKnot, previousKnot }));
else
affectedCurves.Add((spline, previousKnotIndex, new List() { previousKnot, autoSmoothKnot }));
}
else
{
//The segment as already some changes due to some modifications on the previous knots in the previews
//So we only want to adapt the last knot in that case
var knots = affectedCurves[affectedCurveIndex].knots;
knots[addingToStart ? 0 : 1] = autoSmoothKnot;
}
}
}
}
internal static BezierKnot GetInsertedKnotPreview(SplineInfo splineInfo, int index, float t, out Vector3 leftOutTangent, out Vector3 rightInTangent)
{
var spline = splineInfo.Spline;
var previousIndex = SplineUtility.PreviousIndex(index, spline.Count, spline.Closed);
var previous = spline[previousIndex];
var curveToSplit = new BezierCurve(previous, spline[index]);
CurveUtility.Split(curveToSplit, t, out var leftCurve, out var rightCurve);
var nextIndex = SplineUtility.NextIndex(index, spline.Count, spline.Closed);
var next = spline[nextIndex];
var up = CurveUtility.EvaluateUpVector(curveToSplit, t, math.rotate(previous.Rotation, math.up()), math.rotate(next.Rotation, math.up()));
var rotation = quaternion.LookRotationSafe(math.normalizesafe(rightCurve.Tangent0), up);
var inverseRotation = math.inverse(rotation);
leftOutTangent = leftCurve.Tangent0;
rightInTangent = rightCurve.Tangent1;
return new BezierKnot(leftCurve.P3, math.mul(inverseRotation, leftCurve.Tangent1), math.mul(inverseRotation, rightCurve.Tangent0), rotation);
}
internal static SelectableKnot InsertKnot(SplineInfo splineInfo, int index, float t)
{
var spline = splineInfo.Spline;
if (spline == null)
return default;
spline.InsertOnCurve(index, t);
var knot = new SelectableKnot(splineInfo, index);
knotInserted?.Invoke(knot);
return knot;
}
internal static SplineKnotIndex GetIndex(SelectableKnot knot)
{
return new SplineKnotIndex(knot.SplineInfo.Index, knot.KnotIndex);
}
internal static void GetKnotLinks(SelectableKnot knot, List knots)
{
var container = knot.SplineInfo.Container;
if (container == null)
return;
knots.Clear();
if (container.KnotLinkCollection == null)
{
knots.Add(knot);
return;
}
var linkedKnots = container.KnotLinkCollection.GetKnotLinks(new SplineKnotIndex(knot.SplineInfo.Index, knot.KnotIndex));
foreach (var index in linkedKnots)
knots.Add(new SelectableKnot(new SplineInfo(container, index.Spline), index.Knot));
}
internal static void LinkKnots(IReadOnlyList knots)
{
for (int i = 0; i < knots.Count; ++i)
{
var knot = knots[i];
var container = knot.SplineInfo.Container;
var spline = knot.SplineInfo.Spline;
var splineKnotIndex = new SplineKnotIndex() { Spline = knot.SplineInfo.Index, Knot = knot.KnotIndex };
for (int j = i + 1; j < knots.Count; ++j)
{
var otherKnot = knots[j];
// Do not link knots from different containers
if (otherKnot.SplineInfo.Container != container)
continue;
var otherSplineInfo = otherKnot.SplineInfo;
// Do not link same knots
if (otherSplineInfo.Spline == spline && otherKnot.KnotIndex == knot.KnotIndex)
continue;
var otherSplineKnotIndex = new SplineKnotIndex() { Spline = otherKnot.SplineInfo.Index, Knot = otherKnot.KnotIndex };
RecordObject(knot.SplineInfo, "Link Knots");
container.KnotLinkCollection.Link(splineKnotIndex, otherSplineKnotIndex);
container.SetLinkedKnotPosition(splineKnotIndex);
}
}
//Force to record changes if part of a prefab instance
if(knots.Count > 0 && knots[0].IsValid())
PrefabUtility.RecordPrefabInstancePropertyModifications(knots[0].SplineInfo.Object);
}
internal static void UnlinkKnots(IReadOnlyList knots)
{
foreach (var knot in knots)
{
var container = knot.SplineInfo.Container;
var splineKnotIndex = new SplineKnotIndex() { Spline = knot.SplineInfo.Index, Knot = knot.KnotIndex };
RecordObject(knot.SplineInfo, "Unlink Knots");
container.KnotLinkCollection.Unlink(splineKnotIndex);
}
//Force to record changes if part of a prefab instance
if(knots.Count > 0 && knots[0].IsValid())
PrefabUtility.RecordPrefabInstancePropertyModifications(knots[0].SplineInfo.Object);
}
internal static void LinkKnots(SelectableKnot a, SelectableKnot b)
{
var containerA = a.SplineInfo.Container;
var containerB = b.SplineInfo.Container;
if (containerA != containerB)
return;
containerA.KnotLinkCollection.Link(GetIndex(a), GetIndex(b));
//Force to record changes if part of a prefab instance
if(a.IsValid())
PrefabUtility.RecordPrefabInstancePropertyModifications(a.SplineInfo.Object);
}
internal static bool AreKnotLinked(SelectableKnot a, SelectableKnot b)
{
var containerA = a.SplineInfo.Container;
var containerB = b.SplineInfo.Container;
if (containerA != containerB)
return false;
return containerA.AreKnotLinked(
new SplineKnotIndex(a.SplineInfo.Index, a.KnotIndex),
new SplineKnotIndex(b.SplineInfo.Index, b.KnotIndex));
}
internal static bool TryGetNearestKnot(IReadOnlyList splines, out SelectableKnot knot, float maxDistance = SplineHandleUtility.pickingDistance)
{
float nearestDist = float.MaxValue;
SelectableKnot nearest = knot = default;
for (int i = 0; i < splines.Count; ++i)
{
var spline = splines[i].Spline;
var localToWorld = splines[i].LocalToWorld;
for (int j = 0; j < spline.Count; ++j)
{
var dist = SplineHandleUtility.DistanceToCircle(spline[j].Transform(localToWorld).Position, SplineHandleUtility.pickingDistance);
if (dist <= nearestDist)
{
nearestDist = dist;
nearest = new SelectableKnot(splines[i], j);
}
}
}
if (nearestDist > maxDistance)
return false;
knot = nearest;
return true;
}
internal static bool TryGetNearestPositionOnCurve(IReadOnlyList splines, out SplineCurveHit hit, float maxDistance = SplineHandleUtility.pickingDistance)
{
SplineCurveHit nearestHit = hit = default;
BezierCurve nearestCurve = default;
float nearestDist = float.MaxValue;
for (int i = 0; i < splines.Count; ++i)
{
var spline = splines[i].Spline;
var localToWorld = splines[i].LocalToWorld;
for (int j = 0; j < spline.GetCurveCount(); ++j)
{
var curve = spline.GetCurve(j).Transform(localToWorld);
SplineHandleUtility.GetNearestPointOnCurve(curve, out Vector3 position, out float t, out float dist);
if (dist < nearestDist && t > 0f && t < 1f)
{
nearestCurve = curve;
nearestDist = dist;
nearestHit = new SplineCurveHit
{
Position = position,
T = t,
PreviousKnot = new SelectableKnot(splines[i], j),
NextKnot = new SelectableKnot(splines[i], spline.NextIndex(j))
};
}
}
}
if (nearestDist > maxDistance)
return false;
var up = CurveUtility.EvaluateUpVector(nearestCurve, nearestHit.T, math.rotate(nearestHit.PreviousKnot.Rotation, math.up()), math.rotate(nearestHit.NextKnot.Rotation, math.up()));
nearestHit.Normal = up;
hit = nearestHit;
return true;
}
internal static bool IsEndKnot(SelectableKnot knot)
{
return knot.IsValid() && knot.KnotIndex == knot.SplineInfo.Spline.Count - 1;
}
internal static SelectableKnot SplitKnot(SelectableKnot knot)
{
if (!knot.IsValid())
throw new ArgumentException("Knot is invalid", nameof(knot));
var newKnot = knot.SplineInfo.Container.SplitSplineOnKnot(new SplineKnotIndex(knot.SplineInfo.Index, knot.KnotIndex));
if (newKnot.IsValid())
{
PrefabUtility.RecordPrefabInstancePropertyModifications(knot.SplineInfo.Object);
return new SelectableKnot(new SplineInfo(knot.SplineInfo.Container, newKnot.Spline), newKnot.Knot);
}
return new SelectableKnot();
}
internal static SelectableKnot JoinKnots(SelectableKnot knotA, SelectableKnot knotB)
{
if (!knotA.IsValid())
throw new ArgumentException("Knot is invalid", nameof(knotA));
if (!knotB.IsValid())
throw new ArgumentException("Knot is invalid", nameof(knotB));
//Check knots properties
var isKnotAActive = !SplineSelection.IsActive(knotB);
var knotIndexA = new SplineKnotIndex(knotA.SplineInfo.Index, knotA.KnotIndex);
var knotIndexB = new SplineKnotIndex(knotB.SplineInfo.Index, knotB.KnotIndex);
var activeKnot = isKnotAActive ? knotIndexA : knotIndexB;
var otherKnot = isKnotAActive ? knotIndexB : knotIndexA;
var res = knotA.SplineInfo.Container.JoinSplinesOnKnots(activeKnot, otherKnot);
//Force to record changes if part of a prefab instance
var activeSplineInfo = isKnotAActive ? knotA.SplineInfo : knotB.SplineInfo;
PrefabUtility.RecordPrefabInstancePropertyModifications(activeSplineInfo.Object);
return new SelectableKnot(new SplineInfo(knotA.SplineInfo.Container, res.Spline), res.Knot);
}
internal static void ReverseSplinesFlow(IReadOnlyList selectedSplines)
{
List selectedElements = new List();
var formerActiveElement = SplineSelection.GetActiveElement(selectedSplines);
SplineSelection.GetElements(selectedSplines, selectedElements);
var splines = GetSplines(selectedElements);
foreach (var splineInfo in splines)
SplineUtility.ReverseFlow(splineInfo);
int newActiveElementIndex = -1;
for (int i = 0; i < selectedElements.Count; ++i)
{
var element = selectedElements[i];
if (element.Equals(formerActiveElement))
newActiveElementIndex = i;
if (element is SelectableKnot knot)
selectedElements[i] = new SelectableKnot(knot.SplineInfo, knot.SplineInfo.Spline.Count - knot.KnotIndex - 1);
else if (element is SelectableTangent tangent)
selectedElements[i] = new SelectableTangent(tangent.SplineInfo, tangent.SplineInfo.Spline.Count - tangent.KnotIndex - 1, (tangent.TangentIndex + 1) % 2);
}
SplineSelection.Clear();
SplineSelection.AddRange(selectedElements);
if(newActiveElementIndex >= 0)
SplineSelection.SetActive(selectedElements[newActiveElementIndex]);
else
SplineSelection.SetActive(selectedElements[^1]);
}
internal static HashSet GetSplines(IReadOnlyList elements)
where T : ISelectableElement
{
HashSet splines = new HashSet();
for (int i = 0; i < elements.Count; ++i)
splines.Add(elements[i].SplineInfo);
return splines;
}
internal static void GetAdjacentTangents(
T element,
out SelectableTangent previousOut,
out SelectableTangent currentIn,
out SelectableTangent currentOut,
out SelectableTangent nextIn)
where T : ISelectableElement
{
var knot = GetKnot(element);
var spline = knot.SplineInfo.Spline;
bool isFirstKnot = knot.KnotIndex == 0 && !spline.Closed;
bool isLastKnot = knot.KnotIndex == spline.Count - 1 && !spline.Closed;
previousOut = isFirstKnot ? default : new SelectableTangent(knot.SplineInfo, spline.PreviousIndex(knot.KnotIndex), BezierTangent.Out);
currentIn = isFirstKnot ? default : knot.TangentIn;
currentOut = isLastKnot ? default : knot.TangentOut;
nextIn = isLastKnot ? default : new SelectableTangent(knot.SplineInfo, spline.NextIndex(knot.KnotIndex), BezierTangent.In);
}
internal static quaternion GetElementRotation(T element)
where T : ISelectableElement
{
if (element is SelectableTangent editableTangent)
{
float3 forward;
var knotUp = math.rotate(editableTangent.Owner.Rotation, math.up());
if (math.length(editableTangent.Direction) > 0)
forward = math.normalize(editableTangent.Direction);
else // Treat zero length tangent same way as when it's parallel to knot's up vector
forward = knotUp;
float3 right;
var dotForwardKnotUp = math.dot(forward, knotUp);
if (Mathf.Approximately(math.abs(dotForwardKnotUp), 1f))
right = math.rotate(editableTangent.Owner.Rotation, math.right()) * math.sign(dotForwardKnotUp);
else
right = math.cross(forward, knotUp);
return quaternion.LookRotationSafe(forward, math.cross(right, forward));
}
if (element is SelectableKnot editableKnot)
return editableKnot.Rotation;
return quaternion.identity;
}
///
/// Sets the position of a tangent. This could actually result in the knot being rotated depending on the tangent mode
///
/// The tangent to place
/// The position that should be used to place the tangent
internal static void ApplyPositionToTangent(SelectableTangent tangent, float3 position)
{
var knot = tangent.Owner;
switch (knot.Mode)
{
case TangentMode.Broken:
tangent.Position = position;
break;
case TangentMode.Continuous:
case TangentMode.Mirrored:
var deltas = TransformOperation.CalculateMirroredTangentTranslationDeltas(tangent, position);
knot.Rotation = math.mul(deltas.knotRotationDelta, knot.Rotation);
tangent.LocalDirection += math.normalize(tangent.LocalDirection) * deltas.tangentLocalMagnitudeDelta;
break;
}
}
internal static bool Exists(ISplineContainer container, int index)
{
if (container == null)
return false;
return index < container.Splines.Count;
}
internal static bool Exists(SplineInfo spline)
{
return Exists(spline.Container, spline.Index);
}
///
/// Copy an embedded collection to a new if the destination
/// does not already contain an entry matching the and .
///
/// The .
/// A string value used to identify and access a .
/// The that contains the spline.
/// The index of the in the to copy data from.
/// The index of the in the to copy data to.
/// True if data was copied, otherwise false.
public static bool CopySplineDataIfEmpty(ISplineContainer container, int source, int destination, EmbeddedSplineDataType type, string key)
{
if (container == null)
return false;
var splines = container.Splines;
if (source < 0 || source >= splines.Count || destination < 0 || destination >= splines.Count)
return false;
var src = splines[source];
var dst = splines[destination];
// copy SplineData if the target spline is empty
switch (type)
{
case EmbeddedSplineDataType.Int:
if((dst.TryGetIntData(key, out var existingIntData) && existingIntData.Count > 0)
|| !src.TryGetIntData(key, out var srcIntData))
return false;
dst.SetIntData(key, srcIntData);
return true;
case EmbeddedSplineDataType.Float:
if((dst.TryGetFloatData(key, out var existingFloatData) && existingFloatData.Count > 0)
|| !src.TryGetFloatData(key, out var srcFloatData))
return false;
dst.SetFloatData(key, srcFloatData);
return true;
case EmbeddedSplineDataType.Float4:
if((dst.TryGetFloat4Data(key, out var existingFloat4Data) && existingFloat4Data.Count > 0)
|| !src.TryGetFloat4Data(key, out var srcFloat4Data))
return false;
dst.SetFloat4Data(key, srcFloat4Data);
return true;
case EmbeddedSplineDataType.Object:
if((dst.TryGetObjectData(key, out var existingObjectData) && existingObjectData.Count > 0)
|| !src.TryGetObjectData(key, out var srcObjectData))
return false;
dst.SetObjectData(key, srcObjectData);
return true;
}
return false;
}
}
}