first commit

This commit is contained in:
lethanhsonvsp
2025-11-17 15:16:36 +07:00
commit a40d0921eb
17012 changed files with 2652386 additions and 0 deletions

View File

@@ -0,0 +1,999 @@
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;
}
/// <summary>
/// Editor utility functions for working with <see cref="Spline"/> and <see cref="SplineData{T}"/>.
/// </summary>
public static class EditorSplineUtility
{
/// <summary>
/// Invoked once per-frame if a spline property has been modified.
/// </summary>
[Obsolete("Use AfterSplineWasModified instead.", false)]
public static event Action<Spline> afterSplineWasModified;
/// <summary>
/// Invoked once per-frame if a spline property has been modified.
/// </summary>
public static event Action<Spline> AfterSplineWasModified;
static readonly List<SplineInfo> s_SplinePtrBuffer = new List<SplineInfo>(16);
internal static event Action<SelectableKnot> knotInserted;
internal static event Action<SelectableKnot> knotRemoved;
static readonly List<ISelectableElement> s_ElementBuffer = new List<ISelectableElement>();
[UserSetting]
internal static Pref<TangentMode> s_DefaultTangentMode = new("Splines.DefaultTangentMode", TangentMode.AutoSmooth);
/// <summary>
/// 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.
/// </summary>
public static TangentMode DefaultTangentMode => s_DefaultTangentMode;
static EditorSplineUtility()
{
Spline.afterSplineWasModified += (spline) =>
{
afterSplineWasModified?.Invoke(spline);
AfterSplineWasModified?.Invoke(spline);
};
}
/// <summary>
/// Use this function to register a callback that gets invoked
/// once per-frame if any <see cref="SplineData{T}"/> changes occur.
/// </summary>
/// <param name="action">The callback to register.</param>
/// <typeparam name="T">
/// The type parameter of <see cref="SplineData{T}"/>.
/// </typeparam>
public static void RegisterSplineDataChanged<T>(Action<SplineData<T>> action)
{
SplineData<T>.afterSplineDataWasModified += action;
}
/// <summary>
/// Use this function to unregister <see cref="SplineData{T}"/> change callback.
/// </summary>
/// <param name="action">The callback to unregister.</param>
/// <typeparam name="T">
/// The type parameter of <see cref="SplineData{T}"/>.
/// </typeparam>
public static void UnregisterSplineDataChanged<T>(Action<SplineData<T>> action)
{
SplineData<T>.afterSplineDataWasModified -= action;
}
internal static IReadOnlyList<SplineInfo> GetSplinesFromTargetsInternal(IEnumerable<Object> targets)
{
GetSplinesFromTargets(targets, s_SplinePtrBuffer);
return s_SplinePtrBuffer;
}
/// <summary>
/// Get a <see cref="SplineInfo"/> representation of the splines in a list of targets.
/// </summary>
/// <param name="targets">A list of Objects inheriting from <see cref="ISplineContainer"/>.</param>
/// <returns>An array to store the <see cref="SplineInfo"/> of splines found in the targets.</returns>
internal static SplineInfo[] GetSplinesFromTargets(IEnumerable<Object> targets)
{
return GetSplinesFromTargetsInternal(targets).ToArray();
}
/// <summary>
/// Get a <see cref="SplineInfo"/> representation of the splines in a list of targets.
/// </summary>
/// <param name="targets">A list of Objects inheriting from <see cref="ISplineContainer"/>.</param>
/// <param name="results">A list to store the <see cref="SplineInfo"/> of splines found in the targets.</param>
internal static void GetSplinesFromTargets(IEnumerable<Object> targets, List<SplineInfo> results)
{
results.Clear();
foreach (var target in targets)
GetSplineInfosFromContainer(target, results);
}
/// <summary>
/// Get a <see cref="SplineInfo"/> representation of the splines in a target.
/// </summary>
/// <param name="target">An Object inheriting from <see cref="ISplineContainer"/>.</param>
/// <param name="results">A list to store the <see cref="SplineInfo"/> of splines found in the targets.</param>
internal static void GetSplinesFromTarget(Object target, List<SplineInfo> results)
{
results.Clear();
GetSplineInfosFromContainer(target, results);
}
/// <summary>
/// Get a <see cref="SplineInfo"/> representation of the splines in a target.
/// </summary>
/// <param name="target">An Object inheriting from <see cref="ISplineContainer"/>.</param>
/// <param name="results">A list to store the <see cref="SplineInfo"/> of splines found in the targets.</param>
/// <returns>True if a at least a spline was found in the target.</returns>
internal static bool TryGetSplinesFromTarget(Object target, List<SplineInfo> results)
{
results.Clear();
GetSplineInfosFromContainer(target, results);
return results.Count > 0;
}
/// <summary>
/// Get a <see cref="SplineInfo"/> representation of the splines in a target.
/// </summary>
/// <param name="target">An Object inheriting from <see cref="ISplineContainer"/>.</param>
/// <returns>An array to store the <see cref="SplineInfo"/> of splines found in the target.</returns>
internal static SplineInfo[] GetSplinesFromTarget(Object target)
{
GetSplinesFromTarget(target, s_SplinePtrBuffer);
return s_SplinePtrBuffer.ToArray();
}
/// <summary>
/// Get a <see cref="SplineInfo"/> representation of the first spline found in the target.
/// </summary>
/// <param name="target">An Object inheriting from <see cref="ISplineContainer"/>.</param>
/// <param name="splineInfo">The <see cref="SplineInfo"/> of the first spline found in the target.</param>
/// <returns>True if a spline was found in the target.</returns>
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;
}
/// <summary>
/// Sets the current active context to the <see cref="SplineToolContext"/> and the current active tool to the
/// Draw Splines Tool (<see cref="KnotPlacementTool"/>)
/// </summary>
public static void SetKnotPlacementTool()
{
if(ToolManager.activeContextType != typeof(SplineToolContext))
{
ToolManager.SetActiveContext<SplineToolContext>();
ToolManager.SetActiveTool<KnotPlacementTool>();
}
else if(ToolManager.activeToolType != typeof(KnotPlacementTool))
{
ToolManager.SetActiveTool<KnotPlacementTool>();
}
}
static void GetSplineInfosFromContainer(Object target, List<SplineInfo> 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<SplineInfo> 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<T>(IReadOnlyList<T> 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>(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<T>(IReadOnlyList<T> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="curve">The curve to evaluate.</param>
/// <param name="spline">The ISpline that curve belongs to. Not used if curve is not part of any spline.</param>
/// <param name="curveIndex">The index of the curve if it's part of the spine.</param>
/// <typeparam name="T">A type implementing ISpline.</typeparam>
internal static float GetCurveMiddleInterpolation<T>(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<Spline, Dictionary<int, List<BezierKnot>>> affectedCurves)
internal static void GetAffectedCurves(SplineCurveHit hit, List<(Spline s, int index, List<BezierKnot> 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<BezierKnot>();
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<BezierKnot>() { 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<BezierKnot>() { 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<BezierKnot> 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<BezierKnot>() { autoSmoothKnot, previousKnot }));
else
affectedCurves.Add((spline, previousKnotIndex, new List<BezierKnot>() { 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<SelectableKnot> 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<SelectableKnot> 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<SelectableKnot> 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<SplineInfo> 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<SplineInfo> 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<SplineInfo> selectedSplines)
{
List<ISelectableElement> selectedElements = new List<ISelectableElement>();
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<SplineInfo> GetSplines<T>(IReadOnlyList<T> elements)
where T : ISelectableElement
{
HashSet<SplineInfo> splines = new HashSet<SplineInfo>();
for (int i = 0; i < elements.Count; ++i)
splines.Add(elements[i].SplineInfo);
return splines;
}
internal static void GetAdjacentTangents<T>(
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>(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;
}
/// <summary>
/// Sets the position of a tangent. This could actually result in the knot being rotated depending on the tangent mode
/// </summary>
/// <param name="tangent">The tangent to place</param>
/// <param name="position">The position that should be used to place the tangent</param>
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);
}
/// <summary>
/// Copy an embedded <see cref="SplineData{T}"/> collection to a new <see cref="Spline"/> if the destination
/// does not already contain an entry matching the <paramref name="type"/> and <paramref name="key"/>.
/// </summary>
/// <param name="type">The <see cref="EmbeddedSplineDataType"/>.</param>
/// <param name="key">A string value used to identify and access a <see cref="SplineData{T}"/>.</param>
/// <param name="container">The <see cref="ISplineContainer"/> that contains the spline.</param>
/// <param name="source">The index of the <see cref="Spline"/> in the <paramref name="container"/> to copy data from.</param>
/// <param name="destination">The index of the <see cref="Spline"/> in the <paramref name="container"/> to copy data to.</param>
/// <returns>True if data was copied, otherwise false.</returns>
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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e7363f1a86c8d4620b2e20ff9a768e6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
using UnityEngine;
namespace UnityEditor.Splines
{
static class PathIcons
{
public static GUIContent splineMoveTool = EditorGUIUtility.TrIconContent("MoveTool", "Spline Move Tool");
public static GUIContent splineRotateTool = EditorGUIUtility.TrIconContent("RotateTool", "Spline Rotate Tool");
public static GUIContent splineScaleTool = EditorGUIUtility.TrIconContent("ScaleTool", "Spline Scale Tool");
public static Texture2D GetIcon(string name)
{
bool is2x = EditorGUIUtility.pixelsPerPoint > 1;
bool darkSkin = EditorGUIUtility.isProSkin;
string path = string.Format($"Icons/{(darkSkin ? "d_" : "")}{name}{(is2x ? "@2x" : "")}");
Texture2D texture = Resources.Load<Texture2D>(path);
if (texture != null)
return texture;
path = string.Format($"Icons/{(darkSkin ? "d_" : "")}{name}");
texture = Resources.Load<Texture2D>(path);
if (texture != null)
return texture;
path = string.Format($"Icons/{name}");
return Resources.Load<Texture2D>(path);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7fd3d3d53b474642bd4aeaf58564b69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,255 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// Contains specialized utility functions for creating SerializedObject and SerializedProperty objects from
/// <see cref="SplineContainer"/>, <see cref="Spline"/>, and <see cref="SplineData{T}"/>.
/// </summary>
public static class SerializedPropertyUtility
{
const string k_ArrayIndicator = ".Array.data[";
static Dictionary<int, SerializedObject> s_SerializedObjectCache = new();
static Dictionary<int, SerializedProperty> s_SerializedPropertyCache = new();
static readonly Regex k_ExtractArrayPath = new Regex("(?!\\[)[0-9]+(?=\\])", RegexOptions.RightToLeft | RegexOptions.Compiled);
static SerializedPropertyUtility()
{
Selection.selectionChanged += ClearCaches;
Undo.undoRedoPerformed += ClearPropertyCache;
}
static void ClearCaches()
{
s_SerializedObjectCache.Clear();
s_SerializedPropertyCache.Clear();
}
/// <summary>
/// Clear cached SerializedProperty objects. This is automatically called on every selection change. Use this
/// function if you need to insert or remove properties that may have been cached earlier in the frame.
/// </summary>
public static void ClearPropertyCache()
{
s_SerializedPropertyCache.Clear();
}
internal static object GetSerializedPropertyObject(SerializedProperty property)
{
var mbObject = property.serializedObject.targetObject;
// Array fields in SerializedProperty paths have this slightly complicated pathing in the format of
// arrayFieldName.Array.data[i] which we can simplify to just arrayFieldName[i] for easier parsing.
var path = property.propertyPath.Replace(k_ArrayIndicator, "[");
var splitPath = path.Split('.');
var parentObject = (object)mbObject;
var pathPartObject = default(object);
for (int i = 0; i < splitPath.Length; ++i)
{
var propertyPathPart = splitPath[i];
var isArray = IsPropertyIndexer(propertyPathPart, out var fieldName, out var arrayIndex);
var fieldInfo = parentObject.GetType().GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
pathPartObject = fieldInfo.GetValue(parentObject);
if (isArray)
{
if (pathPartObject is IList list)
pathPartObject = list[arrayIndex];
}
parentObject = pathPartObject;
}
return pathPartObject;
}
static bool IsPropertyIndexer(string propertyPart, out string fieldName, out int index)
{
var regex = new Regex(@"(.+)\[(\d+)\]");
var match = regex.Match(propertyPart);
if (match.Success) // Property refers to an array or list
{
fieldName = match.Groups[1].Value;
index = int.Parse(match.Groups[2].Value);
return true;
}
else
{
fieldName = propertyPart;
index = -1;
return false;
}
}
/// <summary>
/// Create a SerializedObject for a <see cref="SplineContainer"/>. This value is cached.
/// </summary>
/// <param name="container">The <see cref="SplineContainer"/> to create a SerializedObject for.</param>
/// <returns>A SerializedObject for the requested <see cref="SplineContainer"/>, or null if container is null.
/// </returns>
public static SerializedObject GetSerializedObject(SplineContainer container)
{
var hash = container.GetInstanceID();
if (!s_SerializedObjectCache.TryGetValue(hash, out var so))
s_SerializedObjectCache.Add(hash, so = new SerializedObject(container));
return so;
}
/// <summary>
/// Create a SerializedProperty for a <see cref="Spline"/> at the requested index in the
/// <see cref="SplineContainer.Splines"/>.
/// </summary>
/// <param name="splineContainer">The <see cref="SplineContainer"/> to reference.</param>
/// <param name="splineIndex">The index of the Spline in the <see cref="SplineContainer.Splines"/> array.</param>
/// <returns>A SerializedProperty for the requested <see cref="Spline"/>, or null if not found.</returns>
public static SerializedProperty GetSplineSerializedProperty(SerializedObject splineContainer, int splineIndex)
{
if (splineContainer == null || splineContainer.targetObject == null)
return null;
var hash = HashCode.Combine(splineContainer.targetObject.GetInstanceID(), splineIndex);
if (!s_SerializedPropertyCache.TryGetValue(hash, out var splineProperty))
{
var splines = splineContainer?.FindProperty("m_Splines");
splineProperty = splines == null || splineIndex < 0 || splineIndex >= splines.arraySize
? null
: splines.GetArrayElementAtIndex(splineIndex);
s_SerializedPropertyCache.Add(hash, splineProperty);
}
return splineProperty;
}
static string GetEmbeddedSplineDataPropertyName(EmbeddedSplineDataType type)
{
return type switch
{
EmbeddedSplineDataType.Int => "m_IntData",
EmbeddedSplineDataType.Float => "m_FloatData",
EmbeddedSplineDataType.Float4 => "m_Float4Data",
EmbeddedSplineDataType.Object => "m_ObjectData",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
/// <summary>
/// Create a SerializedProperty for a <see cref="SplineData{T}"/> value embedded in a <see cref="Spline"/>
/// class. These are keyed collections of <see cref="SplineData{T}"/> that are managed by the <see cref="Spline"/>
/// instance. See <see cref="Spline.GetOrCreateIntData"/>, <see cref="Spline.GetOrCreateFloatData"/>, etc.
/// </summary>
/// <param name="container">The <see cref="SplineContainer"/> that contains the target <see cref="Spline"/>.</param>
/// <param name="index">The index of the Spline in the <see cref="SplineContainer.Splines"/> array.</param>
/// <param name="type">The <see cref="EmbeddedSplineDataType"/>.</param>
/// <param name="key">A string value used to identify and access a <see cref="SplineData{T}"/>.</param>
/// <returns>A SerializedProperty for the requested <see cref="SplineData{T}"/>, or null if not found.</returns>
public static SerializedProperty GetEmbeddedSplineDataProperty(
SplineContainer container,
int index,
EmbeddedSplineDataType type,
string key)
{
var splineProperty = GetSplineSerializedProperty(GetSerializedObject(container), index);
if (splineProperty == null)
return null;
return GetEmbeddedSplineDataProperty(splineProperty, type, key);
}
/// <summary>
/// Create a SerializedProperty for a <see cref="SplineData{T}"/> value embedded in a <see cref="Spline"/>
/// class. These are keyed collections of <see cref="SplineData{T}"/> that are managed by the <see cref="Spline"/>
/// instance. See <see cref="Spline.GetOrCreateIntData"/>, <see cref="Spline.GetOrCreateFloatData"/>, etc.
/// </summary>
/// <param name="splineProperty">The SerializedProperty for the target <see cref="Spline"/>.</param>
/// <param name="type">The <see cref="EmbeddedSplineDataType"/>.</param>
/// <param name="key">A string value used to identify and access a <see cref="SplineData{T}"/>.</param>
/// <returns>A SerializedProperty for the requested <see cref="SplineData{T}"/>, or null if not found.</returns>
public static SerializedProperty GetEmbeddedSplineDataProperty(SerializedProperty splineProperty,
EmbeddedSplineDataType type,
string key)
{
var hash = HashCode.Combine(splineProperty.serializedObject.targetObject.GetInstanceID(),
splineProperty.propertyPath.GetHashCode(),
type.GetHashCode(),
key.GetHashCode());
if (s_SerializedPropertyCache.TryGetValue(hash, out var splineDataProperty) && splineDataProperty?.serializedObject != null)
return splineDataProperty;
var dict = splineProperty.FindPropertyRelative(GetEmbeddedSplineDataPropertyName(type));
var data = dict?.FindPropertyRelative("m_Data");
for (int i = 0; i < data?.arraySize; ++i)
{
var kvp = data.GetArrayElementAtIndex(i);
var k = kvp.FindPropertyRelative("Key");
if (k.stringValue == key)
{
s_SerializedPropertyCache[hash] = splineDataProperty = kvp.FindPropertyRelative("Value");
return splineDataProperty;
}
}
s_SerializedPropertyCache[hash] = null;
return null;
}
internal static SerializedProperty GetEmbeddedSplineDataProperty(SerializedProperty embeddedSplineDataProperty)
{
var container = embeddedSplineDataProperty.FindPropertyRelative("m_Container");
var index = embeddedSplineDataProperty.FindPropertyRelative("m_SplineIndex");
var type = embeddedSplineDataProperty.FindPropertyRelative("m_Type");
var key = embeddedSplineDataProperty.FindPropertyRelative("m_Key");
if (container == null || !(container.objectReferenceValue is SplineContainer containerBehaviour))
return null;
var containerSerializedObject = GetSerializedObject(containerBehaviour);
var spline = GetSplineSerializedProperty(containerSerializedObject, index.intValue);
if (spline == null)
return null;
return GetEmbeddedSplineDataProperty(spline, (EmbeddedSplineDataType)type.enumValueIndex, key.stringValue);
}
internal static bool GetContainerAndIndex(SerializedProperty spline, out ISplineContainer container, out int index)
{
container = spline?.serializedObject?.targetObject as ISplineContainer;
index = 0;
if (container == null)
return false;
if(TryGetSplineIndex(spline, out index))
return true;
return container.Splines.Count == 1;
}
// Extracts the index of a Spline in the ISplineContainer array, or 0 if not part of an array.
internal static bool TryGetSplineIndex(SerializedProperty splineProperty, out int index)
{
index = 0;
var match = k_ExtractArrayPath.Match(splineProperty.propertyPath);
return match.Success && int.TryParse(match.Value, out index);
}
internal static bool TryGetSpline(SerializedProperty splineProperty, out Spline spline)
{
if (GetContainerAndIndex(splineProperty, out var container, out var index))
{
spline = container.Splines[index];
return true;
}
spline = null;
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b7b7ed25b78f49208462984ca88f556
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
static class SplineCacheUtility
{
const int k_SegmentsCount = 32;
static Dictionary<Spline, Vector3[]> s_SplineCacheTable = new Dictionary<Spline, Vector3[]>();
[InitializeOnLoadMethod]
static void Initialize()
{
Spline.Changed += ClearCache;
Undo.undoRedoPerformed += ClearAllCache;
PrefabStage.prefabStageClosing += _ => ClearAllCache();
#if UNITY_2022_3_OR_NEWER
PrefabUtility.prefabInstanceReverting += _ => ClearAllCache();
#else
// PrefabUtility.prefabInstanceReverting is unfortunately not backported yet.
ObjectChangeEvents.changesPublished += (ref ObjectChangeEventStream stream) =>
{
bool cacheCleared = false;
for (int i = 0; i < stream.length; ++i)
{
if (cacheCleared)
break;
int instanceID = 0;
switch (stream.GetEventType(i))
{
// Called after prefab instance override Revert All.
case ObjectChangeKind.ChangeGameObjectStructureHierarchy:
stream.GetChangeGameObjectStructureHierarchyEvent(i, out var changeGameObjectStructureHierarchyEvt);
instanceID = changeGameObjectStructureHierarchyEvt.instanceId;
break;
// Called after prefab instance override Revert on field or component.
case ObjectChangeKind.ChangeGameObjectOrComponentProperties:
stream.GetChangeGameObjectOrComponentPropertiesEvent(i, out var changeGameObjectOrComponentPropertiesEvt);
instanceID = changeGameObjectOrComponentPropertiesEvt.instanceId;
break;
}
if (instanceID != 0)
{
var obj = EditorUtility.InstanceIDToObject(instanceID);
if(obj == null)
continue;
if (PrefabUtility.GetPrefabInstanceStatus(obj) == PrefabInstanceStatus.Connected)
{
SplineContainer splineContainer = null;
GameObject splineGO = obj as GameObject;
if (splineGO != null)
splineContainer = splineGO.GetComponent<SplineContainer>();
else
splineContainer = obj as SplineContainer;
if (splineContainer != null)
{
foreach (var spline in splineContainer.Splines)
{
if (s_SplineCacheTable.ContainsKey(spline))
{
ClearAllCache();
cacheCleared = true;
break;
}
}
}
}
}
}
};
#endif
}
internal static void ClearAllCache()
{
s_SplineCacheTable.Clear();
s_CurvesBuffers?.Clear();
s_TangentsCache?.Clear();
}
public static void GetCachedPositions(Spline spline, out Vector3[] positions)
{
if(!s_SplineCacheTable.ContainsKey(spline))
s_SplineCacheTable.Add(spline, null);
int count = spline.Closed ? spline.Count : spline.Count - 1;
if(s_SplineCacheTable[spline] == null)
{
s_SplineCacheTable[spline] = new Vector3[count * k_SegmentsCount];
CacheCurvePositionsTable(spline, count);
}
positions = s_SplineCacheTable[spline];
}
static void CacheCurvePositionsTable(Spline spline, int curveCount)
{
float inv = 1f / (k_SegmentsCount - 1);
for(int i = 0; i < curveCount; ++i)
{
var curve = spline.GetCurve(i);
var startIndex = i * k_SegmentsCount;
for(int n = 0; n < k_SegmentsCount; n++)
(s_SplineCacheTable[spline])[startIndex + n] = CurveUtility.EvaluatePosition(curve, n * inv);
}
}
internal struct CurveBufferData
{
internal Vector3[] positions;
internal (Vector3[] positions, Matrix4x4 trs) flowArrow;
}
//internal for tests
internal static Dictionary<BezierCurve, CurveBufferData> s_CurvesBuffers = null;
//internal for tests
internal static Dictionary<SelectableTangent, (float3 position, quaternion rotation)> s_TangentsCache = null;
const int k_CurveDrawResolution = 64;
internal static int CurveDrawResolution => k_CurveDrawResolution;
static void ClearCache(Spline spline, int knotIndex, SplineModification modificationType)
{
if (knotIndex == -1 || modificationType != SplineModification.KnotModified)
{
s_CurvesBuffers?.Clear();
s_SplineCacheTable.Remove(spline);
}
else // If Knot modified by the tools
{
// If knots are in auto mode they can influence up to the 4 curves around them
var knotIndexBefore = SplineUtility.PreviousIndex(knotIndex, spline.Count, spline.Closed);
var knotIndexBeforeBefore = SplineUtility.PreviousIndex(knotIndexBefore, spline.Count, spline.Closed);
var knotIndexAfter = SplineUtility.NextIndex(knotIndex, spline.Count, spline.Closed);
UpdateCachePosition(spline, knotIndexBeforeBefore);
UpdateCachePosition(spline, knotIndexBefore);
UpdateCachePosition(spline, knotIndex);
UpdateCachePosition(spline, knotIndexAfter);
}
//We don't have access to the spline container here so we cannot detect which specific SelectableTangents are impacted
s_TangentsCache?.Clear();
}
static void UpdateCachePosition(Spline spline, int curveIndex)
{
if (curveIndex > spline.Count)
return;
var curve = spline.GetCurve(curveIndex);
if (s_CurvesBuffers != null)
{
// Update curve cache
if (s_CurvesBuffers.ContainsKey(curve))
s_CurvesBuffers.Remove(curve);
}
if (s_SplineCacheTable == null || !s_SplineCacheTable.ContainsKey(spline) || s_SplineCacheTable[spline] == null)
return;
// Update position cache
float inv = 1f / (k_SegmentsCount - 1);
var startIndex = curveIndex * k_SegmentsCount;
for (int n = 0; n < k_SegmentsCount; n++)
{
if(startIndex + n < (s_SplineCacheTable[spline]).Length)
(s_SplineCacheTable[spline])[startIndex + n] = CurveUtility.EvaluatePosition(curve, n * inv);
}
}
internal static void InitializeCache()
{
s_CurvesBuffers = new Dictionary<BezierCurve, CurveBufferData>();
s_TangentsCache = new Dictionary<SelectableTangent, (float3, quaternion)>();
}
internal static void ClearCache()
{
s_TangentsCache?.Clear();
s_CurvesBuffers?.Clear();
s_TangentsCache = null;
s_CurvesBuffers = null;
}
static (float3, quaternion) InitTangentEntry(SelectableTangent tangent)
{
var pos = tangent.Position;
var rot = EditorSplineUtility.GetElementRotation(math.length(tangent.LocalPosition) > 0 ? (ISelectableElement)tangent : tangent.Owner);
s_TangentsCache.Add(tangent, (pos, rot));
return (pos, rot);
}
internal static (float3 position, quaternion rotation) GetTangentPositionAndRotation(SelectableTangent tangent)
{
if (s_TangentsCache == null)
return (tangent.Position, EditorSplineUtility.GetElementRotation(math.length(tangent.LocalPosition) > 0 ? (ISelectableElement)tangent : tangent.Owner));
if (!s_TangentsCache.TryGetValue(tangent, out var tuple))
tuple = InitTangentEntry(tangent);
return tuple;
}
internal static quaternion GetTangentRotation(SelectableTangent tangent)
{
if (s_TangentsCache == null)
return EditorSplineUtility.GetElementRotation(math.length(tangent.LocalPosition) > 0 ? (ISelectableElement)tangent : tangent.Owner);
if (!s_TangentsCache.TryGetValue(tangent, out var tuple))
tuple = InitTangentEntry(tangent);
return tuple.rotation;
}
internal static float3 GetTangentPosition(SelectableTangent tangent)
{
if (s_TangentsCache == null)
return tangent.Position;
if (!s_TangentsCache.TryGetValue(tangent, out var tuple))
tuple = InitTangentEntry(tangent);
return tuple.position;
}
internal static void GetCurvePositions(BezierCurve curve, Vector3[] buffer)
{
if (s_CurvesBuffers == null)
{
ComputeCurveBuffer(curve, buffer);
return;
}
if (!s_CurvesBuffers.TryGetValue(curve, out var curveBufferData))
{
curveBufferData = new CurveBufferData() { positions = null, flowArrow = new(null, new Matrix4x4()) };
s_CurvesBuffers.Add(curve, curveBufferData);
}
if(curveBufferData.positions == null)
{
curveBufferData.positions = new Vector3[buffer.Length];
ComputeCurveBuffer(curve, curveBufferData.positions);
s_CurvesBuffers[curve] = curveBufferData;
}
Array.Copy(curveBufferData.positions, buffer, curveBufferData.positions.Length);
}
internal static void ComputeCurveBuffer(BezierCurve curve, Vector3[] buffer)
{
const float segmentPercentage = 1f / k_CurveDrawResolution;
for (int i = 0; i <= k_CurveDrawResolution; ++i)
{
buffer[i] = CurveUtility.EvaluatePosition(curve, i * segmentPercentage);
}
}
internal static (Vector3[] positions, Matrix4x4 trs) GetCurveArrow(ISpline spline, int curveIndex, BezierCurve curve)
{
if (s_CurvesBuffers == null)
return ComputeArrowPositions(spline, curveIndex, curve);
if (!s_CurvesBuffers.TryGetValue(curve, out var curveBufferData))
{
curveBufferData = new CurveBufferData() { positions = null, flowArrow = new(null, new Matrix4x4()) };
s_CurvesBuffers.Add(curve, curveBufferData);
}
if (curveBufferData.flowArrow.positions == null)
{
var arrow = ComputeArrowPositions(spline, curveIndex, curve);
curveBufferData.flowArrow.positions = arrow.positions;
curveBufferData.flowArrow.trs = arrow.trs;
s_CurvesBuffers[curve] = curveBufferData;
}
return curveBufferData.flowArrow;
}
internal static (Vector3[] positions, Matrix4x4 trs) ComputeArrowPositions(ISpline spline, int curveIndex, BezierCurve curve)
{
var t = EditorSplineUtility.GetCurveMiddleInterpolation(curve, spline, curveIndex);
var position = CurveUtility.EvaluatePosition(curve, t);
var tangent = math.normalizesafe(CurveUtility.EvaluateTangent(curve, t));
var up = spline.GetCurveUpVector(curveIndex, t);
var rotation = quaternion.LookRotationSafe(tangent, up);
var arrowMaxSpline = .05f * CurveUtility.ApproximateLength(curve);
var size = HandleUtility.GetHandleSize(position) * .5f;
tangent = new float3(0, 0, .1f) * size;
var right = new float3(0.075f, 0, 0) * size;
var magnitude = math.length(tangent);
if(magnitude > arrowMaxSpline)
{
var ratio = arrowMaxSpline / magnitude;
tangent *= ratio;
right *= ratio;
}
var a = tangent;
var b = -tangent + right;
var c = -tangent - right;
var positions = new Vector3[] { a, b, c};
return (positions, Matrix4x4.TRS(position, rotation, Vector3.one));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d545f10e9c00154e8e57c13456d355f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[Obsolete("Use SplineDataHandles.DataPointHandles instead and EditorTools to interact with SplineData.", false)]
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CustomSplineDataHandle : Attribute
{
internal Type m_Type;
public CustomSplineDataHandle(Type type)
{
m_Type = type;
}
}
#pragma warning disable 618
interface ISplineDataHandle
{
public SplineDataHandleAttribute attribute
{ get; }
void SetAttribute(SplineDataHandleAttribute attribute);
}
#pragma warning restore 618
/// <summary>
/// SplineDataHandle is a base class to override in order to enable custom handles for spline data.
/// The Drawer needs to inherit from this class and override the method corresponding to the correct splineData type.
/// Either one of the method or both can be overriden regarding the user needs.
/// </summary>
/// <typeparam name="T">
/// The type parameter of the <see cref="SplineData{T}"/> that this drawer targets.
/// </typeparam>
[Obsolete("Use SplineDataHandles.DataPointHandles instead and EditorTools to interact with SplineData.", false)]
public abstract class SplineDataHandle<T> : ISplineDataHandle
{
internal int[] m_ControlIDs;
SplineDataHandleAttribute m_Attribute;
public SplineDataHandleAttribute attribute => m_Attribute;
/// <summary>
/// Array of reserved control IDs used for <see cref="SplineData{T}"/> handles.
/// </summary>
public int[] controlIDs => m_ControlIDs;
void ISplineDataHandle.SetAttribute(SplineDataHandleAttribute attribute)
{
m_Attribute = attribute;
}
/// <summary>
/// Override this method to create custom handles for <see cref="SplineData{T}"/>,
/// this method is called before DrawKeyframe in the render loop.
/// </summary>
/// <param name="splineData">The <see cref="SplineData{T}"/> for which the method is drawing handles.</param>
/// <param name="spline">The target Spline associated to the SplineData for the drawing.</param>
/// <param name="localToWorld">The spline localToWorld Matrix.</param>
/// <param name="color">The color defined in the SplineData scene interface.</param>
public virtual void DrawSplineData(SplineData<T> splineData, Spline spline, Matrix4x4 localToWorld, Color color)
{}
/// <summary>
/// Override this method to create custom handles for a <see cref="DataPoint{T}"/>in <see cref="SplineData{T}"/>,
/// 'position' and 'direction' are given in the Spline-space basis.
/// This method is called after DrawSplineData in the render loop.
/// </summary>
/// <param name="controlID">A control ID from <see cref="controlIDs"/> that represents this handle.</param>
/// <param name="position">The position of the keyframe data in spline space.</param>
/// <param name="direction">The direction of the spline at the current keyframe.</param>
/// <param name="upDirection">The up vector orthogonal to the spline direction at the current keyframe regarding knot rotation.</param>
/// <param name="splineData">The <see cref="SplineData{T}"/> for which the method is drawing handles.</param>
/// <param name="dataPointIndex">The index of the current keyframe to handle.</param>
public virtual void DrawDataPoint(
int controlID,
Vector3 position,
Vector3 direction,
Vector3 upDirection,
SplineData<T> splineData,
int dataPointIndex)
{}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2495f958516e4867aa9cbcbcf916836c
timeCreated: 1625521293

View File

@@ -0,0 +1,100 @@
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// A utility class providing methods to handle and manage spline extrusion operations.
/// Initializes event listeners on load to monitor and respond to object changes, duplication, and paste operations specific to splines.
/// </summary>
[InitializeOnLoad]
public static class SplineExtrudeUtility
{
static SplineExtrudeUtility()
{
#if UNITY_2022_2_OR_NEWER
ClipboardUtility.duplicatedGameObjects += OnPasteOrDuplicated;
ClipboardUtility.pastedGameObjects += OnPasteOrDuplicated;
ObjectChangeEvents.changesPublished += ObjectEventChangesPublished;
#else
ObjectChangeEvents.changesPublished += ObjectEventChangesPublished;
#endif
}
#if UNITY_2022_2_OR_NEWER
static void OnPasteOrDuplicated(GameObject[] duplicates)
{
foreach (var duplicate in duplicates)
CheckForExtrudeMeshCreatedOrModified(duplicate);
}
static void ObjectEventChangesPublished(ref ObjectChangeEventStream stream)
{
for (int i = 0; i < stream.length; ++i)
{
var type = stream.GetEventType(i);
if (type == ObjectChangeKind.ChangeGameObjectStructure)
{
stream.GetChangeGameObjectStructureEvent(i, out var changeGameObjectStructure);
if (EditorUtility.InstanceIDToObject(changeGameObjectStructure.instanceId) is GameObject go)
CheckForSplineExtrudeAdded(go);
}
}
}
#else
static void ObjectEventChangesPublished(ref ObjectChangeEventStream stream)
{
for (int i = 0, c = stream.length; i < c; ++i)
{
// SplineExtrude was created via duplicate, copy paste
var type = stream.GetEventType(i);
if (type == ObjectChangeKind.CreateGameObjectHierarchy)
{
stream.GetCreateGameObjectHierarchyEvent(i, out CreateGameObjectHierarchyEventArgs data);
GameObjectCreatedOrStructureModified(data.instanceId);
}
else if (type == ObjectChangeKind.ChangeGameObjectStructure)
{
stream.GetChangeGameObjectStructureEvent(i, out var changeGameObjectStructure);
if (EditorUtility.InstanceIDToObject(changeGameObjectStructure.instanceId) is GameObject go)
CheckForSplineExtrudeAdded(go);
}
}
}
static void GameObjectCreatedOrStructureModified(int instanceId)
{
if (EditorUtility.InstanceIDToObject(instanceId) is GameObject go)
CheckForExtrudeMeshCreatedOrModified(go);
}
#endif
static void CheckForSplineExtrudeAdded(GameObject go)
{
if (go.TryGetComponent<SplineExtrude>(out var splineExtrude))
splineExtrude.SetSplineContainerOnGO();
var childCount = go.transform.childCount;
if (childCount > 0)
{
for (int childIndex = 0; childIndex < childCount; ++childIndex)
CheckForSplineExtrudeAdded(go.transform.GetChild(childIndex).gameObject);
}
}
static void CheckForExtrudeMeshCreatedOrModified(GameObject go)
{
//Check if the current GameObject has a SplineExtrude component
if(go.TryGetComponent<SplineExtrude>(out var extrudeComponent))
extrudeComponent.Reset();
var childCount = go.transform.childCount;
if (childCount > 0)
{
//Check through the children
for(int childIndex = 0; childIndex < childCount; ++childIndex)
CheckForExtrudeMeshCreatedOrModified(go.transform.GetChild(childIndex).gameObject);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f254493af340bfd48a3311e87b482712

View File

@@ -0,0 +1,83 @@
using System;
using UnityEditor.SettingsManagement;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// SplineGizmoUtility provides methods for drawing in-scene representations of Splines.
/// </summary>
public static class SplineGizmoUtility
{
[UserSetting]
internal static UserSetting<Color> s_GizmosLineColor = new UserSetting<Color>(PathSettings.instance, "Gizmos.SplineColor", Color.blue, SettingsScope.User);
[UserSettingBlock("Gizmos")]
static void GizmosColorPreferences(string searchContext)
{
s_GizmosLineColor.value = SettingsGUILayout.SettingsColorField("Splines Color", s_GizmosLineColor, searchContext);
}
static readonly Color s_OutlineColor = new Color(0f, 0f, 0f, .5f);
/// <summary>
/// Draw a line gizmo for a <see cref="ISplineContainer"/>.
/// </summary>
/// <param name="container">An object implementing the ISplineContainer interface. Usually this will be a MonoBehaviour.</param>
public static void DrawGizmos(ISplineContainer container)
{
var splines = container.Splines;
if (splines == null)
return;
Gizmos.matrix = ((MonoBehaviour)container).transform.localToWorldMatrix;
foreach (var spline in splines)
{
if(spline == null || spline.Count < 2)
continue;
Vector3[] positions;
SplineCacheUtility.GetCachedPositions(spline, out positions);
#if UNITY_2023_1_OR_NEWER
Gizmos.DrawLineStrip(positions, false);
#else
for (int i = 1; i < positions.Length; ++i)
Gizmos.DrawLine(positions[i-1], positions[i]);
#endif
}
Gizmos.matrix = Matrix4x4.identity;
}
/// <summary>
/// Draw a line gizmo for a <see cref="ISplineProvider"/>.
/// </summary>
/// <param name="provider">An object implementing the ISplineProvider interface. Usually this will be a MonoBehaviour.</param>
[Obsolete("Use the overload that uses " + nameof(ISplineContainer))]
public static void DrawGizmos(ISplineProvider provider)
{
var splines = provider.Splines;
if (splines == null)
return;
Gizmos.matrix = ((MonoBehaviour)provider).transform.localToWorldMatrix;
foreach (var spline in splines)
{
if (spline == null || spline.Count < 2)
continue;
Vector3[] positions;
SplineCacheUtility.GetCachedPositions(spline, out positions);
#if UNITY_2023_1_OR_NEWER
Gizmos.DrawLineStrip(positions, false);
#else
for (int i = 1; i < positions.Length; ++i)
Gizmos.DrawLine(positions[i-1], positions[i]);
#endif
}
Gizmos.matrix = Matrix4x4.identity;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 004bce94384eadb4ebd753c96f4ecc5f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,372 @@
using System;
using System.Collections.Generic;
using Unity.Collections;
using UnityEditor.SettingsManagement;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Splines;
using Unity.Mathematics;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
struct ColorScope : IDisposable
{
readonly Color m_PrevColor;
public ColorScope(Color color)
{
m_PrevColor = Handles.color;
Handles.color = color;
}
public void Dispose()
{
Handles.color = m_PrevColor;
}
}
struct ZTestScope : IDisposable
{
readonly CompareFunction m_Original;
public ZTestScope(CompareFunction function)
{
m_Original = Handles.zTest;
Handles.zTest = function;
}
public void Dispose()
{
Handles.zTest = m_Original;
}
}
static class SplineHandleUtility
{
[UserSetting]
static UserSetting<Color> s_LineNormalFrontColor = new UserSetting<Color>(PathSettings.instance, "Handles.CurveNormalInFrontColor", new Color(0f, 0f, 0f, 1.0f), SettingsScope.User);
[UserSetting]
static UserSetting<Color> s_LineNormalBehindColor = new UserSetting<Color>(PathSettings.instance, "Handles.CurveNormalBehindColor", new Color(0f, 0f, 0f, 0.4f), SettingsScope.User);
#if !UNITY_2022_2_OR_NEWER
[UserSetting]
static UserSetting<Color> s_KnotColor = new UserSetting<Color>(PathSettings.instance, "Handles.KnotDefaultColor", new Color(0f, 224f / 255f, 1f, 1f), SettingsScope.User);
#endif
[UserSetting]
static UserSetting<Color> s_TangentColor = new UserSetting<Color>(PathSettings.instance, "Handles.TangentDefaultColor", Color.black, SettingsScope.User);
[UserSettingBlock("Handles")]
static void HandleColorPreferences(string searchContext)
{
s_LineNormalFrontColor.value = SettingsGUILayout.SettingsColorField("Curve Color", s_LineNormalFrontColor, searchContext);
s_LineNormalBehindColor.value = SettingsGUILayout.SettingsColorField("Curve Color Behind Surface", s_LineNormalBehindColor, searchContext);
#if !UNITY_2022_2_OR_NEWER
s_KnotColor.value = SettingsGUILayout.SettingsColorField("Knot Color", s_KnotColor, searchContext);
#endif
s_TangentColor.value = SettingsGUILayout.SettingsColorField("Tangent Color", s_TangentColor, searchContext);
}
internal static Color lineBehindColor => s_LineNormalBehindColor;
internal static Color lineColor => s_LineNormalFrontColor;
#if !UNITY_2022_2_OR_NEWER
internal static Color knotColor => s_KnotColor;
#endif
internal static Color tangentColor => s_TangentColor;
#if UNITY_2022_2_OR_NEWER
static Color s_DefaultElementColor = Handles.elementColor;
static Color s_DefaultElementPreselectionColor = Handles.elementPreselectionColor;
static Color s_DefaultElementSelectionColor = Handles.elementSelectionColor;
#else
static Color s_DefaultElementColor = SplineHandleUtility.knotColor;
static Color s_DefaultElementPreselectionColor = Handles.preselectionColor;
static Color s_DefaultElementSelectionColor = Handles.selectedColor;
#endif
internal static Color elementColor = s_DefaultElementColor;
internal static Color elementPreselectionColor = s_DefaultElementPreselectionColor;
internal static Color elementSelectionColor = s_DefaultElementSelectionColor;
internal const float pickingDistance = 8f;
internal const float handleWidth = 4f;
internal const float aliasedLineSizeMultiplier = 0.5f;
internal const float sizeFactor = 0.15f;
internal const float knotDiscRadiusFactorDefault = 0.06f;
internal const float knotDiscRadiusFactorHover = 0.07f;
internal const float knotDiscRadiusFactorSelected = 0.085f;
internal static readonly Texture2D denseLineAATex = Resources.Load<Texture2D>(k_TangentLineAATexPath);
const string k_TangentLineAATexPath = "Textures/TangentLineAATex";
const int k_MaxDecimals = 15;
const int k_SegmentsPointCount = 30;
static readonly Vector3[] s_ClosestPointArray = new Vector3[k_SegmentsPointCount];
static readonly Vector3[] s_AAWireDiscBuffer = new Vector3[18];
const float k_KnotPickingDistance = 18f;
static readonly Vector3[] s_LineBuffer = new Vector3[2];
internal static bool canDrawOnCurves = false;
internal static ISelectableElement lastHoveredElement { get; private set; }
internal static int lastHoveredElementId { get; private set; }
//Settings min and max ids used by handles when drawing curves/knots/tangents
//This helps to determine if nearest control is a spline element or a built-in tool
static Vector2Int s_ElementIdRange = Vector2Int.zero;
internal static int minElementId
{
get => s_ElementIdRange.x;
set => s_ElementIdRange.x = value;
}
internal static int maxElementId
{
get => s_ElementIdRange.y;
set => s_ElementIdRange.y = value;
}
internal static void UpdateElementColors()
{
#if UNITY_2022_2_OR_NEWER
elementColor = Handles.elementColor;
elementPreselectionColor = Handles.elementPreselectionColor;
elementSelectionColor = Handles.elementSelectionColor;
#else
elementColor = SplineHandleUtility.knotColor;
elementPreselectionColor = Handles.preselectionColor;
elementSelectionColor = Handles.selectedColor;
#endif
}
internal static bool ShouldShowTangent(SelectableTangent tangent)
{
if (!SplineSelectionUtility.IsSelectable(tangent) || Mathf.Approximately(math.length(tangent.LocalDirection), 0f))
return false;
if (SplineHandleSettings.ShowAllTangents)
return true;
return SplineSelection.IsSelectedOrAdjacentToSelected(tangent);
}
internal static void ResetLastHoveredElement()
{
lastHoveredElementId = -1;
lastHoveredElement = null;
}
internal static bool IsLastHoveredElement<T>(T element)
{
return element.Equals(lastHoveredElement);
}
internal static void SetLastHoveredElement<T>(T element, int controlId) where T : ISelectableElement
{
lastHoveredElementId = controlId;
lastHoveredElement = element;
}
internal static bool IsElementHovered(int controlId)
{
//Hovering the element itself
var isElementHovered = (GUIUtility.hotControl == 0 && HandleUtility.nearestControl == controlId);
// starting (Mouse down) or performing direct manip on that element
var isDirectManipElement = GUIUtility.hotControl == controlId;
return isElementHovered || isDirectManipElement;
}
//Check if the nearest control is one belonging to the spline elements
internal static bool IsHoverAvailableForSplineElement()
{
return GUIUtility.hotControl == 0 && (!canDrawOnCurves || (HandleUtility.nearestControl > minElementId && HandleUtility.nearestControl < maxElementId))
|| GUIUtility.hotControl > minElementId && GUIUtility.hotControl < maxElementId;
}
internal static Ray TransformRay(Ray ray, Matrix4x4 matrix)
{
return new Ray(matrix.MultiplyPoint3x4(ray.origin), matrix.MultiplyVector(ray.direction));
}
internal static Vector3 DoIncrementSnap(Vector3 position, Vector3 previousPosition)
{
var delta = position - previousPosition;
var right = Tools.handleRotation * Vector3.right;
var up = Tools.handleRotation * Vector3.up;
var forward = Tools.handleRotation * Vector3.forward;
var snappedDelta =
Snapping.Snap(Vector3.Dot(delta, right), EditorSnapSettings.move[0]) * right +
Snapping.Snap(Vector3.Dot(delta, up), EditorSnapSettings.move[1]) * up +
Snapping.Snap(Vector3.Dot(delta, forward), EditorSnapSettings.move[2]) * forward;
return previousPosition + snappedDelta;
}
static Vector3 SnapToGrid(Vector3 position)
{
#if UNITY_2022_2_OR_NEWER
return EditorSnapSettings.gridSnapActive ?
Snapping.Snap(position, EditorSnapSettings.gridSize) :
position;
#else
GameObject tmp = new GameObject();
tmp.hideFlags = HideFlags.HideAndDontSave;
var trs = tmp.transform;
trs.position = position;
Handles.SnapToGrid(new[] { trs });
var snapped = trs.position;
Object.DestroyImmediate(tmp);
return snapped;
#endif
}
internal static bool GetPointOnSurfaces(Vector2 mousePosition, out Vector3 point, out Vector3 normal)
{
#if UNITY_2020_1_OR_NEWER
if (HandleUtility.PlaceObject(mousePosition, out point, out normal))
{
if (EditorSnapSettings.gridSnapEnabled)
point = SnapToGrid(point);
return true;
}
#endif
var ray = HandleUtility.GUIPointToWorldRay(mousePosition);
#if !UNITY_2020_1_OR_NEWER
if (Physics.Raycast(ray, out RaycastHit hit))
{
point = hit.point;
normal = hit.normal;
return true;
}
#endif
//Backup if couldn't find a surface
var constraint = new Plane(SceneView.lastActiveSceneView.in2DMode ? Vector3.back : Vector3.up, Vector3.zero); //This should be in the direction of the current grid
if (constraint.Raycast(ray, out float distance))
{
normal = constraint.normal;
point = ray.origin + ray.direction * distance;
if (EditorSnapSettings.gridSnapEnabled)
point = SnapToGrid(point);
return true;
}
point = normal = Vector3.zero;
return false;
}
internal static void DrawLineWithWidth(Vector3 a, Vector3 b, float width, Texture2D lineAATex = null)
{
s_LineBuffer[0] = a;
s_LineBuffer[1] = b;
Handles.DrawAAPolyLine(lineAATex, width, s_LineBuffer);
}
public static float DistanceToCircle(Vector3 point, float radius)
{
Vector3 screenPos = HandleUtility.WorldToGUIPointWithDepth(point);
if (screenPos.z < 0)
return float.MaxValue;
return Mathf.Max(0, Vector2.Distance(screenPos, Event.current.mousePosition) - radius);
}
internal static Vector3 GetMinDifference(Vector3 position)
{
return Vector3.one * (HandleUtility.GetHandleSize(position) / 80f);
}
internal static float RoundBasedOnMinimumDifference(float valueToRound, float minDifference)
{
var numberOfDecimals = Mathf.Clamp(-Mathf.FloorToInt(Mathf.Log10(Mathf.Abs(minDifference))), 0, k_MaxDecimals);
return (float)Math.Round(valueToRound, numberOfDecimals, MidpointRounding.AwayFromZero);
}
internal static void GetNearestPointOnCurve(BezierCurve curve, out Vector3 position, out float t)
{
GetNearestPointOnCurve(curve, out position, out t, out _);
}
internal static void GetNearestPointOnCurve(BezierCurve curve, out Vector3 position, out float t, out float distance)
{
Vector3 closestA = Vector3.zero;
Vector3 closestB = Vector3.zero;
float closestDist = float.MaxValue;
int closestSegmentFirstPoint = -1;
GetCurveSegments(curve, s_ClosestPointArray);
for (int j = 0; j < s_ClosestPointArray.Length - 1; ++j)
{
Vector3 a = s_ClosestPointArray[j];
Vector3 b = s_ClosestPointArray[j + 1];
float dist = HandleUtility.DistanceToLine(a, b);
if (dist < closestDist)
{
closestA = a;
closestB = b;
closestDist = dist;
closestSegmentFirstPoint = j;
}
}
//Calculate position
Vector2 screenPosA = HandleUtility.WorldToGUIPoint(closestA);
Vector2 screenPosB = HandleUtility.WorldToGUIPoint(closestB);
Vector2 relativePoint = Event.current.mousePosition - screenPosA;
Vector2 lineDirection = screenPosB - screenPosA;
float length = lineDirection.magnitude;
float dot = Vector3.Dot(lineDirection, relativePoint);
if (length > .000001f)
dot /= length * length;
dot = Mathf.Clamp01(dot);
position = Vector3.Lerp(closestA, closestB, dot);
//Calculate percent on curve's segment
float percentPerSegment = 1.0f / (k_SegmentsPointCount - 1);
float percentA = closestSegmentFirstPoint * percentPerSegment;
float lengthAB = (closestB - closestA).magnitude;
float lengthAToClosest = (position - closestA).magnitude;
t = percentA + percentPerSegment * (lengthAToClosest / lengthAB);
distance = closestDist;
}
static void GetCurveSegments(BezierCurve curve, Vector3[] results)
{
float segmentPercentage = 1f / (results.Length - 1);
for (int i = 0; i < k_SegmentsPointCount; ++i)
{
results[i] = CurveUtility.EvaluatePosition(curve, i * segmentPercentage);
}
}
internal static void DrawAAWireDisc(Vector3 position, Vector3 normal, float radius, float thickness)
{
// Right vector calculation here is identical to Handles.DrawWireDisc
Vector3 right = Vector3.Cross(normal, Vector3.up);
if ((double)right.sqrMagnitude < 1.0 / 1000.0)
right = Vector3.Cross(normal, Vector3.right);
var angleStep = 360f / (s_AAWireDiscBuffer.Length - 1);
for (int i = 0; i < s_AAWireDiscBuffer.Length - 1; i++)
{
s_AAWireDiscBuffer[i] = position + right * radius;
right = Quaternion.AngleAxis(angleStep, normal) * right;
}
s_AAWireDiscBuffer[s_AAWireDiscBuffer.Length - 1] = s_AAWireDiscBuffer[0];
var tex = thickness > 2f ? denseLineAATex : null;
Handles.DrawAAPolyLine(tex, thickness, s_AAWireDiscBuffer);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2da6d0ba602194831a1a64e5bc72f6ad
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
static class SplineSelectionUtility
{
static readonly List<SelectableKnot> s_KnotBuffer = new List<SelectableKnot>();
internal static void ValidateTangentSelection(SelectableKnot knot)
{
if (!SplineUtility.AreTangentsModifiable(knot.Mode))
{
SplineSelection.Remove(knot.TangentIn);
SplineSelection.Remove(knot.TangentOut);
}
}
internal static void HandleSelection<T>(T element, bool addLinkedKnots = true)
where T : struct, ISelectableElement
{
HandleSelection(element, EditorGUI.actionKey || Event.current.modifiers == EventModifiers.Shift,
Event.current.modifiers == EventModifiers.Shift, addLinkedKnots);
}
internal static void HandleSelection<T>(T element, bool appendElement, bool setActive, bool addLinkedKnots = true)
where T : struct, ISelectableElement
{
if (appendElement)
{
if(element is SelectableKnot knot)
{
if(addLinkedKnots)
{
EditorSplineUtility.GetKnotLinks(knot, s_KnotBuffer);
var allContained = true;
var hasActive = false;
foreach (var k in s_KnotBuffer)
{
allContained &= SplineSelection.Contains(k);
hasActive |= SplineSelection.IsActive(k);
}
if (allContained && (!setActive || hasActive))
SplineSelection.RemoveRange(s_KnotBuffer);
else
SplineSelection.AddRange(s_KnotBuffer);
}
else
{
var activeKnot = GetSelectedKnot(knot);
if (SplineSelection.Contains(activeKnot) && (!setActive || SplineSelection.IsActive(activeKnot)))
SplineSelection.Remove(activeKnot);
else
SplineSelection.Add(activeKnot);
}
}
else
{
if(SplineSelection.Contains(element) && (!setActive || SplineSelection.IsActive(element)))
SplineSelection.Remove(element);
else
SplineSelection.Add(element);
}
if (setActive && SplineSelection.Contains(element))
SplineSelection.SetActive(element);
}
else
{
List<ISelectableElement> newSelection = new List<ISelectableElement>();
if(element is SelectableKnot knot)
{
if(addLinkedKnots)
{
EditorSplineUtility.GetKnotLinks(knot, s_KnotBuffer);
foreach(var k in s_KnotBuffer)
newSelection.Add(k);
}
else
newSelection.Add(GetSelectedKnot(knot));
}
else
newSelection.Add(element);
SplineSelection.Clear();
SplineSelection.AddRange(newSelection);
}
}
static SelectableKnot GetSelectedKnot(SelectableKnot knot)
{
EditorSplineUtility.GetKnotLinks(knot, s_KnotBuffer);
var activeKnot = new SelectableKnot();
float minDist = Single.PositiveInfinity, dist;
foreach(var k in s_KnotBuffer)
{
var spline = k.SplineInfo.Spline;
var localToWorld = k.SplineInfo.LocalToWorld;
if(k.KnotIndex > 0)
{
var curve = spline.GetCurve(k.KnotIndex - 1).Transform(localToWorld);
dist = CurveHandles.DistanceToCurve(curve);
if(dist < minDist)
{
minDist = dist;
activeKnot = k;
}
}
if(k.KnotIndex < spline.Count - 1)
{
var curve = spline.GetCurve(k.KnotIndex).Transform(localToWorld);
dist = CurveHandles.DistanceToCurve(curve);
if(dist < minDist)
{
minDist = dist;
activeKnot = k;
}
}
}
return activeKnot;
}
internal static bool IsSelectable(SelectableTangent tangent)
{
// Tangents should not be selectable if not modifiable
if(!SplineUtility.AreTangentsModifiable(tangent.Owner.Mode))
return false;
// For open splines, tangentIn of first knot and tangentOut of last knot should not be selectable
switch (tangent.TangentIndex)
{
case (int)BezierTangent.In:
return tangent.KnotIndex != 0 || tangent.SplineInfo.Spline.Closed;
case (int)BezierTangent.Out:
return tangent.KnotIndex != tangent.SplineInfo.Spline.Count - 1 || tangent.SplineInfo.Spline.Closed;
}
return true;
}
internal static bool IsSelectable(ISelectableElement element)
{
if (element is SelectableTangent tangent)
return IsSelectable(tangent);
return true;
}
internal static bool CanLinkKnots(List<SelectableKnot> knots)
{
if (knots.Count == 0)
return false;
var knotCounts = new Dictionary<ISplineContainer, List<SelectableKnot>>();
foreach (var knot in knots)
{
var container = knot.SplineInfo.Container;
if (!knotCounts.ContainsKey(container))
{
var knotList = new List<SelectableKnot> {knot};
knotCounts.Add(container, knotList);
}
else
knotCounts[container].Add(knot);
EditorSplineUtility.GetKnotLinks(knot, s_KnotBuffer);
if (s_KnotBuffer.Count > 1)
return true;
var otherSelectedKnots = knotCounts[container];
if (otherSelectedKnots.Count > 1)
{
for (int i = 0; i < otherSelectedKnots.Count - 1; ++i)
{
var otherSelectedKnot = otherSelectedKnots[i];
if (!s_KnotBuffer.Contains(otherSelectedKnot))
return true;
}
}
}
return false;
}
internal static bool CanUnlinkKnots(List<SelectableKnot> knots)
{
if (knots.Count == 0)
return false;
foreach (var knot in knots)
{
EditorSplineUtility.GetKnotLinks(knot, s_KnotBuffer);
if (s_KnotBuffer.Count > 1)
return true;
}
return false;
}
internal static bool CanSplitSelection(List<SelectableKnot> knots)
{
if(knots.Count != 1)
return false;
var knot = knots[0];
bool endKnot = knot.KnotIndex == 0 || knot.KnotIndex == knot.SplineInfo.Spline.Count - 1;
return !endKnot || knot.SplineInfo.Spline.Closed;
}
internal static bool CanJoinSelection(List<SelectableKnot> knots)
{
if(knots.Count != 2)
return false;
var isActiveKnotInSelection = SplineSelection.IsActive(knots[0]) || SplineSelection.IsActive(knots[1]);
var areInSameContainer = knots[0].SplineInfo.Container == knots[1].SplineInfo.Container;
var areOnDifferentSplines = knots[0].SplineInfo.Index != knots[1].SplineInfo.Index;
var areKnotsOnExtremities =
(knots[0].KnotIndex == 0 || knots[0].KnotIndex == knots[0].SplineInfo.Spline.Count - 1)
&& (knots[1].KnotIndex == 0 || knots[1].KnotIndex == knots[1].SplineInfo.Spline.Count - 1);
return isActiveKnotInSelection && areInSameContainer && areOnDifferentSplines && areKnotsOnExtremities;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9480991a8f9cd457891d0c23ec511b49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: