first commit

This commit is contained in:
lethanhsonvsp
2025-11-17 15:36:52 +07:00
commit 6f2eafa33c
14093 changed files with 1253472 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
static class CopyPaste
{
// JSONUtility needs a root object to serialize
[Serializable]
class CopyPasteBuffer
{
public SerializedSpline[] Splines;
public SerializedLink[] Links;
}
[Serializable]
struct SerializedKnot
{
public BezierKnot Knot;
public TangentMode Mode;
public float Tension;
public SerializedKnot(SelectableKnot knot)
{
Knot = knot.GetBezierKnot(false);
Mode = knot.Mode;
Tension = knot.Tension;
}
}
[Serializable]
class SerializedSpline
{
public float4x4 Transform;
public bool Closed;
public SerializedKnot[] Knots;
}
[Serializable]
class SerializedLink
{
public SplineKnotIndex[] Indices;
}
public static bool IsSplineCopyBuffer(string contents)
{
if (string.IsNullOrEmpty(contents))
return false;
var buffer = new CopyPasteBuffer();
try
{
EditorJsonUtility.FromJsonOverwrite(contents, buffer);
}
catch (ArgumentException)
{
return false;
}
return buffer.Splines?.Length > 0;
}
static int CompareKnot(SelectableKnot a, SelectableKnot b)
{
var compareTarget = (int)math.sign(a.SplineInfo.Object.GetInstanceID() - b.SplineInfo.Object.GetInstanceID());
if (compareTarget != 0)
return compareTarget;
var compareSpline = (int)math.sign(a.SplineInfo.Index - b.SplineInfo.Index);
if (compareSpline != 0)
return compareSpline;
return (int)math.sign(a.KnotIndex - b.KnotIndex);
}
public static string Copy(IEnumerable<SelectableKnot> selection)
{
SerializedKnot[] ToArray(SelectableKnot[] original)
{
var result = new SerializedKnot[original.Length];
for (int i = 0; i < result.Length; ++i)
result[i] = new SerializedKnot(original[i]);
return result;
}
void Flatten(List<SelectableKnot[]> arrays, List<SelectableKnot> results)
{
results.Clear();
foreach (var knotArray in arrays)
results.AddRange(knotArray);
}
Dictionary<SelectableKnot, SplineKnotIndex> knotToSerializedIndex = new Dictionary<SelectableKnot, SplineKnotIndex>();
List<SerializedSpline> splines = new List<SerializedSpline>();
List<SelectableKnot> originalKnots = new List<SelectableKnot>(selection);
var connectedKnots = GetConnectedKnots(originalKnots);
foreach (var connectedKnotArray in connectedKnots)
{
// Skip Orphan Knots
if (connectedKnotArray.Length < 2)
continue;
var splineInfo = connectedKnotArray[0].SplineInfo;
splines.Add(new SerializedSpline
{
Closed = splineInfo.Spline.Closed && connectedKnotArray.Length == splineInfo.Spline.Count,
Knots = ToArray(connectedKnotArray),
Transform = splineInfo.LocalToWorld
});
for (int i = 0; i < connectedKnotArray.Length; ++i)
knotToSerializedIndex.Add(connectedKnotArray[i], new SplineKnotIndex(splines.Count - 1, i));
}
// Add the links
List<SplineKnotIndex> indices = new List<SplineKnotIndex>();
List<SerializedLink> links = new List<SerializedLink>();
List<SelectableKnot> knots = new List<SelectableKnot>();
// Update the original knots array with the removal of orphan knots
Flatten(connectedKnots, originalKnots);
foreach (var originalKnot in originalKnots)
{
EditorSplineUtility.GetKnotLinks(originalKnot, knots);
indices.Clear();
foreach (var knot in knots)
{
if (knotToSerializedIndex.TryGetValue(knot, out var index))
{
indices.Add(index);
//Remove the pair to ensure we don't get duplicates for every knot in the same link
knotToSerializedIndex.Remove(knot);
}
}
// Only serialized the link if at least 2 copied knots were linked together
if (indices.Count >= 2)
links.Add(new SerializedLink {Indices = indices.ToArray()});
}
if (splines.Count == 0)
return string.Empty;
CopyPasteBuffer buffer = new CopyPasteBuffer
{
Splines = splines.ToArray(),
Links = links.ToArray(),
};
return EditorJsonUtility.ToJson(buffer);
}
static List<SelectableKnot[]> GetConnectedKnots(List<SelectableKnot> knots)
{
if (knots.Count == 0)
return new List<SelectableKnot[]>();
knots.Sort(CompareKnot);
List<SelectableKnot[]> results = new List<SelectableKnot[]>();
List<SelectableKnot> connected = new List<SelectableKnot> { knots[0] };
for (int i = 1; i < knots.Count; ++i)
{
var previous = connected[^1];
var current = knots[i];
// Check if adjacent and on the same spline as previous
if (!previous.SplineInfo.Equals(current.SplineInfo)
|| previous.KnotIndex + 1 != current.KnotIndex)
{
results.Add(connected.ToArray());
connected.Clear();
}
connected.Add(current);
}
results.Add(connected.ToArray());
// Merge ends if the spline is closed and first and last knots are connected
for (int i = 0; i < results.Count; ++i)
{
var firstKnot = results[i][0];
if (firstKnot.KnotIndex == 0 && firstKnot.SplineInfo.Spline.Closed)
{
// Look for the last knot on the same spline
for (int j = i + 1; j < results.Count; ++j)
{
var lastKnot = results[j][^1];
// Early exit if not on the same spline
if (!lastKnot.SplineInfo.Equals(firstKnot.SplineInfo))
break;
if (lastKnot.KnotIndex == lastKnot.SplineInfo.Spline.Count - 1)
{
// combine both arrays
var a = results[j];
var b = results[i];
var newArray = new SelectableKnot[a.Length + b.Length];
Array.Copy(a, newArray, a.Length);
Array.Copy(b, 0, newArray, a.Length, b.Length);
results[i] = newArray;
results.RemoveAt(j);
break;
}
}
}
}
return results;
}
// Paste will create all new splines in the first active ISplineContainer in the selection.
// Duplicate will try to create new splines in the same container that the knots were copied from.
public static void Paste(string copyPasteBuffer)
{
ISplineContainer target = Selection.GetFiltered<ISplineContainer>(SelectionMode.TopLevel).FirstOrDefault() ??
ObjectFactory.CreateGameObject("New Spline", typeof(SplineContainer)).GetComponent<SplineContainer>();
Paste(copyPasteBuffer, target);
}
public static void Paste(string copyPasteBuffer, ISplineContainer target)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (string.IsNullOrEmpty(copyPasteBuffer))
return;
var buffer = new CopyPasteBuffer();
try
{
EditorJsonUtility.FromJsonOverwrite(copyPasteBuffer, buffer);
}
catch (ArgumentException)
{
//If the copy buffer wasn't for a spline copy buffer, we just don't do anything
return;
}
var selection = new List<SelectableKnot>();
var inverse = (target is Component component)
? component.transform.localToWorldMatrix.inverse
: Matrix4x4.identity;
var branches = new List<Spline>(target.Splines);
int splineIndexOffset = branches.Count;
foreach (var serialized in buffer.Splines)
{
var knots = serialized.Knots;
var spline = new Spline(knots.Length);
spline.Closed = serialized.Closed;
var trs = serialized.Transform;
var index = branches.Count;
var info = new SplineInfo(target, index);
branches.Add(spline);
for (int i = 0, c = knots.Length; i < c; ++i)
{
spline.Add(knots[i].Knot.Transform(math.mul(inverse, trs)), knots[i].Mode, knots[i].Tension);
selection.Add(new SelectableKnot(info, i));
}
}
if (target is Object obj)
Undo.RecordObject(obj, "Paste Knots");
target.Splines = branches;
foreach (var link in buffer.Links)
{
var firstIndex = link.Indices[0];
firstIndex.Spline += splineIndexOffset;
for (int i = 1; i < link.Indices.Length; ++i)
{
var indexPair = link.Indices[i];
indexPair.Spline += splineIndexOffset;
target.KnotLinkCollection.Link(firstIndex, indexPair);
}
}
SplineSelection.Clear();
SplineSelection.AddRange(selection);
SceneView.RepaintAll();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 641a050c859644e7804f826b7d4d8c0b
timeCreated: 1656006689

View File

@@ -0,0 +1,41 @@
using UnityEditor.SettingsManagement;
using UnityEngine;
namespace UnityEditor.Splines
{
sealed class PathSettings
{
static Settings s_SettingsInstance;
public static Settings instance
{
get
{
if (s_SettingsInstance == null)
s_SettingsInstance = new Settings(new [] { new UserSettingsRepository() });
return s_SettingsInstance;
}
}
// Register a new SettingsProvider that will scrape the owning assembly for [UserSetting] marked fields.
[SettingsProvider]
static SettingsProvider CreateSettingsProvider()
{
var provider = new UserSettingsProvider("Preferences/Splines",
instance,
new[] { typeof(PathSettings).Assembly });
return provider;
}
}
/// <summary>
/// The wrapper to define user preferences through Settings Manager.
/// </summary>
/// <typeparam name="T"></typeparam>
class Pref<T> : UserSetting<T>
{
public Pref(string key, T value)
: base(PathSettings.instance, key, value, SettingsScope.User) { }
}
}

View File

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

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
[Serializable]
struct SelectableSplineElement : IEquatable<SelectableSplineElement>
{
public Object target;
public int targetIndex;
public int knotIndex;
public int tangentIndex;
public SelectableSplineElement(ISelectableElement element)
{
target = element.SplineInfo.Object;
targetIndex = element.SplineInfo.Index;
knotIndex = element.KnotIndex;
tangentIndex = element is SelectableTangent tangent ? tangent.TangentIndex : -1;
}
public bool Equals(SelectableSplineElement other)
{
return target == other.target && targetIndex == other.targetIndex && knotIndex == other.knotIndex && tangentIndex == other.tangentIndex;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is SelectableSplineElement other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(target, targetIndex, knotIndex, tangentIndex);
}
}
sealed class SelectionContext : ScriptableSingleton<SelectionContext>
{
public List<SelectableSplineElement> selection = new List<SelectableSplineElement>();
public int version;
}
}

View File

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

View File

@@ -0,0 +1,385 @@
using System;
using Unity.Mathematics;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// An interface that represents a selectable spline element. A selectable spline element can be a knot or a tangent.
/// `ISelectableElement` is used by the selection to get information about the spline, the knot, and the positions of the spline elements.
/// </summary>
public interface ISelectableElement : IEquatable<ISelectableElement>
{
/// <summary>
/// The <see cref="SplineInfo"/> that describes the spline.
/// </summary>
SplineInfo SplineInfo { get; }
/// <summary>
/// The index of the knot in the spline. If the spline element is a tangent, this is the index of the knot
/// that the tangent is attached to.
/// </summary>
int KnotIndex { get; }
/// <summary>
/// The position of the spline element in local space.
/// </summary>
float3 LocalPosition { get; set; }
/// <summary>
/// The position of the spline element in world space.
/// </summary>
float3 Position { get; set; }
/// <summary>
/// Checks if the element is valid. For example, checks if the spline is not null and the index is valid.
/// </summary>
/// <returns>Returns true if all fields from the element have valid values.</returns>
bool IsValid();
}
/// <summary>
/// Implements the <see cref="ISelectableElement"/> interface. SelectableKnot is used by the
/// spline selection and handles to use tools and handles to manipulate spline elements.
/// </summary>
public struct SelectableKnot : ISelectableElement, IEquatable<SelectableKnot>
{
/// <inheritdoc />
public SplineInfo SplineInfo { get; }
/// <inheritdoc />
public int KnotIndex { get; }
/// <summary>
/// Transforms a knot from local space to world space (Read Only).
/// </summary>
internal float4x4 LocalToWorld => SplineInfo.LocalToWorld;
/// <inheritdoc />
public float3 Position
{
get => math.transform(LocalToWorld, LocalPosition);
set => LocalPosition = math.transform(math.inverse(LocalToWorld), value);
}
/// <inheritdoc />
public float3 LocalPosition
{
get => SplineInfo.Spline[KnotIndex].Position;
set
{
var knot = SplineInfo.Spline[KnotIndex];
knot.Position = value;
SplineInfo.Spline[KnotIndex] = knot;
}
}
/// <inheritdoc />
public bool IsValid()
{
return SplineInfo.Spline != null && KnotIndex >= 0 && KnotIndex < SplineInfo.Spline.Count;
}
/// <summary>
/// The rotation of the spline element in world space.
/// </summary>
public quaternion Rotation
{
get => math.mul(new quaternion(LocalToWorld), LocalRotation);
set => LocalRotation = math.mul(math.inverse(new quaternion(LocalToWorld)), value);
}
/// <summary>
/// The rotation of the spline element in local space.
/// </summary>
public quaternion LocalRotation
{
get => SplineInfo.Spline[KnotIndex].Rotation;
set
{
var knot = SplineInfo.Spline[KnotIndex];
knot.Rotation = math.normalize(value);
SplineInfo.Spline[KnotIndex] = knot;
}
}
/// <summary>
/// The <see cref="TangentMode"/> associated with a knot.
/// </summary>
public TangentMode Mode
{
get => SplineInfo.Spline.GetTangentMode(KnotIndex);
set
{
SplineInfo.Spline.SetTangentMode(KnotIndex, value);
SplineSelectionUtility.ValidateTangentSelection(this);
}
}
/// <summary>
/// The tension associated with a knot. `Tension` is only used if the tangent mode is Auto Smooth.
/// </summary>
public float Tension
{
get => SplineInfo.Spline.GetAutoSmoothTension(KnotIndex);
set => SplineInfo.Spline.SetAutoSmoothTension(KnotIndex, value);
}
/// <summary>
/// Sets the tangent mode of the knot.
/// </summary>
/// <param name="mode">The <see cref="TangentMode"/> to apply to the knot.</param>
/// <param name="main">The tangent to use as the main tangent when the tangent is set to the Mirrored or Continuous tangent mode.
/// The main tangent is not modified, but the other tangent attached to that knot is modified to adopt the new tangent mode.</param>
public void SetTangentMode(TangentMode mode, BezierTangent main)
{
var spline = SplineInfo.Spline;
spline.SetTangentMode(KnotIndex, mode, main);
SplineSelectionUtility.ValidateTangentSelection(this);
}
/// <summary>
/// The In tangent associated with the knot. The In tangent defines the curvature of the segment that enters the knot.
/// </summary>
public SelectableTangent TangentIn => new SelectableTangent(SplineInfo, KnotIndex, BezierTangent.In);
/// <summary>
/// The Out tangent associated with the knot. The Out tangent defines the curvature of the segment that exits the knot.
/// </summary>
public SelectableTangent TangentOut => new SelectableTangent(SplineInfo, KnotIndex, BezierTangent.Out);
/// <summary>
/// Creates a <see cref="SelectableKnot"/> from a SplineInfo and a knot index.
/// </summary>
/// <param name="info">The <see cref="SplineInfo"/> associated with the tangent.</param>
/// <param name="index">The index of the knot.</param>
public SelectableKnot(SplineInfo info, int index)
{
this.SplineInfo = info;
this.KnotIndex = index;
}
/// <summary>
/// Creates the BezierKnot representation associated with a SelectableKnot.
/// </summary>
/// <param name="worldSpace">Set to true for the BezierKnot to be in world space, or set to false for the Bezierknot to be in local space.</param>
/// <returns>The <see cref="BezierKnot"/> associated with the knot.</returns>
public BezierKnot GetBezierKnot(bool worldSpace)
{
return worldSpace ? SplineInfo.Spline[KnotIndex].Transform(LocalToWorld) : SplineInfo.Spline[KnotIndex];
}
/// <summary>
/// Checks if two instances of `SplineElement` are equal.
/// </summary>
/// <param name="other">The <see cref="ISelectableElement"/> to compare against.</param>
/// <returns>
/// Returns true when <paramref name="other"/> is a <see cref="SelectableKnot"/> and the values of each instance are identical.
/// </returns>
public bool Equals(ISelectableElement other)
{
if (other is SelectableKnot knot)
return Equals(knot);
return false;
}
/// <summary>
/// Checks if two instances of SelectableKnot are equal.
/// </summary>
/// <param name="other">The <see cref="SelectableKnot"/> to compare against.</param>
/// <returns>
/// Returns true if the values of each instance of `SelectableKnot` are identical.
/// </returns>
public bool Equals(SelectableKnot other)
{
return Equals(SplineInfo.Spline, other.SplineInfo.Spline) && KnotIndex == other.KnotIndex;
}
/// <summary>
/// Checks if two instances of an object are equal.
/// </summary>
/// <param name="obj">The object to compare against.</param>
/// <returns>
/// Returns true if <paramref name="obj"/> is a <see cref="SelectableKnot"/> and its values are identical to the original instance.
/// </returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is SelectableKnot other && Equals(other);
}
/// <summary>
/// Gets a hash code for this knot.
/// </summary>
/// <returns>
/// A hash code for the <see cref="SelectableKnot"/>.
/// </returns>
public override int GetHashCode()
{
return HashCode.Combine(SplineInfo.Spline, KnotIndex);
}
}
/// <summary>
/// Represents a struct that implements the <see cref="ISelectableElement"/> interface. Spline selection uses
/// `SelectableTangent` and handles to easily manipulate spline elements with tools and handles.
/// </summary>
public struct SelectableTangent : ISelectableElement, IEquatable<SelectableTangent>
{
/// <inheritdoc />
public SplineInfo SplineInfo { get; }
/// <inheritdoc />
public int KnotIndex { get; }
/// <summary>
/// The index of the tangent. A value of 0 represents an In tangent. A value of 1 represents an Out tangent.
/// </summary>
public int TangentIndex { get; }
/// <summary>
/// The knot associated with this tangent.
/// </summary>
public SelectableKnot Owner => new SelectableKnot(SplineInfo, KnotIndex);
/// <summary>
/// The opposite tangent on the knot. If this tangent is the In tangent, then the opposite tangent is the Out tangent. If this tangent is the Out tangent, then the opposite tangent is the In tangent.
/// </summary>
public SelectableTangent OppositeTangent => new SelectableTangent(SplineInfo, KnotIndex, 1 - TangentIndex);
/// <inheritdoc />
public bool IsValid()
{
return SplineInfo.Spline != null
&& KnotIndex >= 0
&& KnotIndex < SplineInfo.Spline.Count
&& TangentIndex >= 0
&& TangentIndex < 2
&& Owner.Mode != TangentMode.Linear
&& Owner.Mode != TangentMode.AutoSmooth;
}
/// <summary>
/// The direction of the tangent in world space.
/// </summary>
public float3 Direction
{
get => MathUtility.MultiplyVector(LocalToWorld, LocalDirection);
set => LocalDirection = MathUtility.MultiplyVector(math.inverse(LocalToWorld), value);
}
/// <summary>
/// The direction of the tangent in local space.
/// </summary>
public float3 LocalDirection
{
get => TangentIndex == (int)BezierTangent.In ? SplineInfo.Spline[KnotIndex].TangentIn : SplineInfo.Spline[KnotIndex].TangentOut;
set
{
var spline = SplineInfo.Spline;
var knot = spline[KnotIndex];
switch (TangentIndex)
{
case (int)BezierTangent.In:
knot.TangentIn = value;
break;
case (int)BezierTangent.Out:
knot.TangentOut = value;
break;
}
spline.SetKnot(KnotIndex, knot, (BezierTangent)TangentIndex);
}
}
/// <summary>
/// Matrix that transforms a tangent point from local space into world space using its associated knot (Read Only).
/// </summary>
internal float4x4 LocalToWorld => math.mul(SplineInfo.LocalToWorld, new float4x4(Owner.LocalRotation, Owner.LocalPosition));
/// <inheritdoc />
public float3 Position
{
get => math.transform(LocalToWorld, LocalPosition);
set => LocalPosition = math.transform(math.inverse(LocalToWorld), value);
}
/// <inheritdoc />
public float3 LocalPosition
{
get => LocalDirection;
set => LocalDirection = value;
}
/// <summary>
/// Creates a new <see cref="SelectableTangent"/> object.
/// </summary>
/// <param name="info">The <see cref="SplineInfo"/> associated with the tangent.</param>
/// <param name="knotIndex">The index of the knot that the tangent is attached to.</param>
/// <param name="tangent">The <see cref="BezierTangent"/> that represents this tangent.</param>
public SelectableTangent(SplineInfo info, int knotIndex, BezierTangent tangent)
: this(info, knotIndex, (int)tangent) { }
/// <summary>
/// Creates a new <see cref="SelectableTangent"/> object.
/// </summary>
/// <param name="info">The <see cref="SplineInfo"/> associated with the tangent.</param>
/// <param name="knotIndex">The index of the knot that the tangent is attached to.</param>
/// <param name="tangentIndex">The index of the tangent. A value of 0 represents an In tangent. A value of 1 represents an Out tangent.</param>
public SelectableTangent(SplineInfo info, int knotIndex, int tangentIndex)
{
SplineInfo = info;
KnotIndex = knotIndex;
TangentIndex = tangentIndex;
}
/// <summary>
/// Checks if two instances of a `SplineElement` are equal.
/// </summary>
/// <param name="other">The <see cref="ISelectableElement"/> to compare against.</param>
/// <returns>
/// Returns true when <paramref name="other"/> is a <see cref="SelectableTangent"/> and the values of each instance are identical.
/// </returns>
public bool Equals(ISelectableElement other)
{
if (other is SelectableTangent tangent)
return Equals(tangent);
return false;
}
/// <summary>
/// Checks if two instances of `SelectableTangent` are equal.
/// </summary>
/// <param name="other">The <see cref="SelectableTangent"/> to compare against.</param>
/// <returns>
/// Returns true if the values of each instance are identical.
/// </returns>
public bool Equals(SelectableTangent other)
{
return Equals(SplineInfo.Spline, other.SplineInfo.Spline) && KnotIndex == other.KnotIndex && TangentIndex == other.TangentIndex;
}
/// <summary>
/// Checks if two objects are equal.
/// </summary>
/// <param name="obj">The object to compare against.</param>
/// <returns>
/// Returns true when <paramref name="obj"/> is a <see cref="SelectableTangent"/> and the values of each instance are identical.
/// </returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is SelectableTangent other && Equals(other);
}
/// <summary>
/// Gets a hash code for this tangent.
/// </summary>
/// <returns>
/// A hash code for the <see cref="SelectableTangent"/>.
/// </returns>
public override int GetHashCode()
{
return HashCode.Combine(SplineInfo.Spline, KnotIndex, TangentIndex);
}
}
}

View File

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

View File

@@ -0,0 +1,610 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Splines;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
/// <summary>
/// Provides methods to track the selection of spline elements, knots, and tangents.
/// `SplineTools` and `SplineHandles` use `SplineSelection` to manage these elements.
/// </summary>
public static class SplineSelection
{
/// <summary>
/// Action that is called when the element selection changes.
/// </summary>
public static event Action changed;
static readonly HashSet<Object> s_ObjectSet = new HashSet<Object>();
static readonly HashSet<SplineInfo> s_SelectedSplineInfo = new HashSet<SplineInfo>();
static Object[] s_SelectedTargetsBuffer = new Object[0];
static SelectionContext context => SelectionContext.instance;
internal static List<SelectableSplineElement> selection => context.selection;
// Tracks selected splines in the SplineReorderableList
static List<SplineInfo> s_SelectedSplines = new ();
/// <summary>
/// The number of elements in the current selection.
/// </summary>
public static int Count => selection.Count;
static HashSet<SelectableTangent> s_AdjacentTangentCache = new HashSet<SelectableTangent>();
static int s_SelectionVersion;
static SplineSelection()
{
context.version = 0;
Undo.undoRedoPerformed += OnUndoRedoPerformed;
EditorSplineUtility.knotInserted += OnKnotInserted;
EditorSplineUtility.knotRemoved += OnKnotRemoved;
Selection.selectionChanged += OnSelectionChanged;
}
static void OnSelectionChanged()
{
ClearInspectorSelectedSplines();
}
static void OnUndoRedoPerformed()
{
if (context.version != s_SelectionVersion)
{
s_SelectionVersion = context.version;
ClearInspectorSelectedSplines();
NotifySelectionChanged();
}
}
/// <summary>
/// Clears the current selection.
/// </summary>
public static void Clear()
{
if (selection.Count == 0)
return;
IncrementVersion();
ClearNoUndo(true);
}
internal static void ClearNoUndo(bool notify)
{
selection.Clear();
if (notify)
NotifySelectionChanged();
}
/// <summary>
/// Checks if the current selection contains at least one element from the given targeted splines.
/// </summary>
/// <param name="targets">The splines to consider when looking at selected elements.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the current selection contains at least an element of the desired type.</returns>
public static bool HasAny<T>(IReadOnlyList<SplineInfo> targets)
where T : struct, ISelectableElement
{
for (int i = 0; i < Count; ++i)
for (int j = 0; j < targets.Count; ++j)
if (TryGetElement(selection[i], targets[j], out T _))
return true;
return false;
}
/// <summary>
/// Gets the active element of the selection. The active element is generally the last one added to this selection.
/// </summary>
/// <param name="targets">The splines to consider when getting the active element.</param>
/// <returns>The <see cref="ISelectableElement"/> that represents the active knot or tangent. Returns null if no active element is found.</returns>
public static ISelectableElement GetActiveElement(IReadOnlyList<SplineInfo> targets)
{
for (int i = 0; i < Count; ++i)
for (int j = 0; j < targets.Count; ++j)
if (TryGetElement(selection[i], targets[j], out ISelectableElement result))
return result;
return null;
}
/// <summary>
/// Gets all the elements of the current selection, filtered by target splines. Elements are added to the given collection.
/// </summary>
/// <param name="targets">The splines to consider when looking at selected elements.</param>
/// <param name="results">The collection to fill with spline elements from the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void GetElements<T>(IReadOnlyList<SplineInfo> targets, ICollection<T> results)
where T : ISelectableElement
{
results.Clear();
for (int i = 0; i < Count; ++i)
for (int j = 0; j < targets.Count; ++j)
if (TryGetElement(selection[i], targets[j], out T result))
results.Add(result);
}
/// <summary>
/// Gets all the elements of the current selection, from a single spline target. Elements are added to the given collection.
/// </summary>
/// <param name="target">The spline to consider when looking at selected elements.</param>
/// <param name="results">The collection to fill with spline elements from the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void GetElements<T>(SplineInfo target, ICollection<T> results)
where T : ISelectableElement
{
results.Clear();
for (int i = 0; i < Count; ++i)
if (TryGetElement(selection[i], target, out T result))
results.Add(result);
}
static bool TryGetElement<T>(SelectableSplineElement element, SplineInfo splineInfo, out T value)
where T : ISelectableElement
{
if (element.target == splineInfo.Container as Object)
{
if (element.targetIndex == splineInfo.Index)
{
if (element.tangentIndex >= 0)
{
var tangent = new SelectableTangent(splineInfo, element.knotIndex, element.tangentIndex);
if (tangent.IsValid() && tangent is T t)
{
value = t;
return true;
}
value = default;
return false;
}
var knot = new SelectableKnot(splineInfo, element.knotIndex);
if (knot.IsValid() && knot is T k)
{
value = k;
return true;
}
}
}
value = default;
return false;
}
internal static Object[] GetAllSelectedTargets()
{
s_ObjectSet.Clear();
foreach (var element in selection)
{
s_ObjectSet.Add(element.target);
}
Array.Resize(ref s_SelectedTargetsBuffer, s_ObjectSet.Count);
s_ObjectSet.CopyTo(s_SelectedTargetsBuffer);
return s_SelectedTargetsBuffer;
}
/// <summary>
/// Used for selecting splines from the inspector, only internal from now.
/// </summary>
internal static IEnumerable<SplineInfo> SelectedSplines => s_SelectedSplines;
/// <summary>
/// Checks if an element is currently the active one in the selection.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to test.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the element is the active element, false if it is not.</returns>
public static bool IsActive<T>(T element)
where T : ISelectableElement
{
if (selection.Count == 0)
return false;
return IsEqual(element, selection[0]);
}
static bool IsEqual<T>(T element, SelectableSplineElement selectionData)
where T : ISelectableElement
{
int tangentIndex = element is SelectableTangent tangent ? tangent.TangentIndex : -1;
return element.SplineInfo.Object == selectionData.target
&& element.SplineInfo.Index == selectionData.targetIndex
&& element.KnotIndex == selectionData.knotIndex
&& tangentIndex == selectionData.tangentIndex;
}
/// <summary>
/// Sets the active element of the selection.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to set as the active element.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void SetActive<T>(T element)
where T : ISelectableElement
{
var index = IndexOf(element);
if (index == 0)
return;
IncrementVersion();
if (index > 0)
selection.RemoveAt(index);
var e = new SelectableSplineElement(element);
selection.Insert(0, e);
if(e.target is Component component)
{
//Set the active unity object so the spline is the first target
Object[] unitySelection = Selection.objects;
var target = component.gameObject;
index = Array.IndexOf(unitySelection, target);
if(index > 0)
{
Object prevObj = unitySelection[0];
unitySelection[0] = unitySelection[index];
unitySelection[index] = prevObj;
Selection.objects = unitySelection;
}
}
NotifySelectionChanged();
}
/// <summary>
/// Sets the selection to the element.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to set as the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void Set<T>(T element)
where T : ISelectableElement
{
IncrementVersion();
ClearNoUndo(false);
selection.Insert(0, new SelectableSplineElement(element));
NotifySelectionChanged();
}
internal static void Set(IEnumerable<SelectableSplineElement> selection)
{
IncrementVersion();
context.selection.Clear();
context.selection.AddRange(selection);
NotifySelectionChanged();
}
/// <summary>
/// Adds an element to the current selection.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to add to the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the element was added to the selection, and false if the element is already in the selection.</returns>
public static bool Add<T>(T element)
where T : ISelectableElement
{
if (Contains(element))
return false;
IncrementVersion();
selection.Insert(0, new SelectableSplineElement(element));
NotifySelectionChanged();
return true;
}
/// <summary>
/// Add a set of elements to the current selection.
/// </summary>
/// <param name="elements">The set of <see cref="ISelectableElement"/> to add to the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void AddRange<T>(IEnumerable<T> elements)
where T : ISelectableElement
{
bool changed = false;
foreach (var element in elements)
{
if (!Contains(element))
{
if (!changed)
{
changed = true;
IncrementVersion();
}
selection.Insert(0, new SelectableSplineElement(element));
}
}
if (changed)
NotifySelectionChanged();
}
/// <summary>
/// Remove an element from the current selection.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to remove from the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the element has been removed from the selection, false otherwise.</returns>
public static bool Remove<T>(T element)
where T : ISelectableElement
{
var index = IndexOf(element);
if (index >= 0)
{
IncrementVersion();
selection.RemoveAt(index);
NotifySelectionChanged();
return true;
}
return false;
}
/// <summary>
/// Remove a set of elements from the current selection.
/// </summary>
/// <param name="elements">The set of <see cref="ISelectableElement"/> to remove from the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if at least an element has been removed from the selection, false otherwise.</returns>
public static bool RemoveRange<T>(IReadOnlyList<T> elements)
where T : ISelectableElement
{
bool changed = false;
for (int i = 0; i < elements.Count; ++i)
{
var index = IndexOf(elements[i]);
if (index >= 0)
{
if (!changed)
{
IncrementVersion();
changed = true;
}
selection.RemoveAt(index);
}
}
if (changed)
NotifySelectionChanged();
return changed;
}
static int IndexOf<T>(T element)
where T : ISelectableElement
{
for (int i = 0; i < selection.Count; ++i)
if (IsEqual(element, selection[i]))
return i;
return -1;
}
/// <summary>
/// Checks if the selection contains a knot or a tangent.c'est
/// </summary>
/// <param name="element">The element to verify.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the element is contained in the current selection, false otherwise.</returns>
public static bool Contains<T>(T element)
where T : ISelectableElement
{
return IndexOf(element) >= 0;
}
// Used when the selection is changed in the tools.
internal static void UpdateObjectSelection(IEnumerable<Object> targets)
{
s_ObjectSet.Clear();
foreach (var target in targets)
if (target != null)
s_ObjectSet.Add(target);
bool changed = false;
for (int i = Count - 1; i >= 0; --i)
{
bool removeElement = false;
if (!EditorSplineUtility.Exists(selection[i].target as ISplineContainer, selection[i].targetIndex))
{
removeElement = true;
}
else if (!s_ObjectSet.Contains(selection[i].target))
{
ClearInspectorSelectedSplines();
removeElement = true;
}
else if(selection[i].tangentIndex > 0)
{
// In the case of a tangent, also check that the tangent is still valid if the spline type
// or tangent mode has been updated
var spline = SplineToolContext.GetSpline(selection[i].target, selection[i].targetIndex);
removeElement = !SplineUtility.AreTangentsModifiable(spline.GetTangentMode(selection[i].knotIndex));
}
if (removeElement)
{
if (!changed)
{
changed = true;
IncrementVersion();
}
if (i < selection.Count)
selection.RemoveAt(i);
}
}
if (changed)
{
RebuildAdjacentCache();
NotifySelectionChanged();
}
}
//Used when inserting new elements in spline
static void OnKnotInserted(SelectableKnot inserted)
{
for (var i = 0; i < selection.Count; ++i)
{
var knot = selection[i];
if (knot.target == inserted.SplineInfo.Object
&& knot.targetIndex == inserted.SplineInfo.Index
&& knot.knotIndex >= inserted.KnotIndex)
{
++knot.knotIndex;
selection[i] = knot;
}
}
RebuildAdjacentCache();
}
//Used when deleting an element in spline
static void OnKnotRemoved(SelectableKnot removed)
{
bool changed = false;
for (var i = selection.Count - 1; i >= 0; --i)
{
var knot = selection[i];
if (knot.target == removed.SplineInfo.Object && knot.targetIndex == removed.SplineInfo.Index)
{
if (knot.knotIndex == removed.KnotIndex)
{
if (!changed)
{
changed = true;
IncrementVersion();
}
selection.RemoveAt(i);
}
else if (knot.knotIndex >= removed.KnotIndex)
{
--knot.knotIndex;
selection[i] = knot;
}
}
}
RebuildAdjacentCache();
if (changed)
NotifySelectionChanged();
}
static void IncrementVersion()
{
Undo.RecordObject(context, "Spline Selection Changed");
++s_SelectionVersion;
++context.version;
}
static void NotifySelectionChanged()
{
RebuildAdjacentCache();
changed?.Invoke();
}
static bool TryGetSplineInfo(SelectableSplineElement element, out SplineInfo splineInfo)
{
//Checking null in case the object was destroyed
if (element.target != null && element.target is ISplineContainer container)
{
splineInfo = new SplineInfo(container, element.targetIndex);
return true;
}
splineInfo = default;
return false;
}
static bool TryCast(SelectableSplineElement element, out SelectableTangent result)
{
if (TryGetSplineInfo(element, out var splineInfo) && element.tangentIndex >= 0)
{
result = new SelectableTangent(splineInfo, element.knotIndex, element.tangentIndex);
return true;
}
result = default;
return false;
}
static bool TryCast(SelectableSplineElement element, out SelectableKnot result)
{
if (TryGetSplineInfo(element, out var splineInfo) && element.tangentIndex < 0)
{
result = new SelectableKnot(splineInfo, element.knotIndex);
return true;
}
result = default;
return false;
}
internal static bool IsSelectedOrAdjacentToSelected(SelectableTangent tangent)
{
return s_AdjacentTangentCache.Contains(tangent);
}
static void RebuildAdjacentCache()
{
s_AdjacentTangentCache.Clear();
foreach(var element in selection)
{
SelectableTangent previousOut, currentIn, currentOut, nextIn;
if(TryCast(element, out SelectableKnot knot))
EditorSplineUtility.GetAdjacentTangents(knot, out previousOut, out currentIn, out currentOut, out nextIn);
else if(TryCast(element, out SelectableTangent tangent))
EditorSplineUtility.GetAdjacentTangents(tangent, out previousOut, out currentIn, out currentOut, out nextIn);
else
continue;
s_AdjacentTangentCache.Add(previousOut);
s_AdjacentTangentCache.Add(currentIn);
s_AdjacentTangentCache.Add(currentOut);
s_AdjacentTangentCache.Add(nextIn);
}
}
internal static void ClearInspectorSelectedSplines()
{
s_SelectedSplines.Clear();
}
internal static bool HasActiveSplineSelection()
{
return s_SelectedSplines.Count > 0;
}
// Inspector spline selection is a one-way operation. It can only be set by the SplineReorderableList. Changes
// to selected splines in the Scene or Hierarchy will only clear the selected inspector splines.
internal static void SetInspectorSelectedSplines(SplineContainer container, IEnumerable<int> selected)
{
s_SelectedSplines.Clear();
foreach (var index in selected)
s_SelectedSplines.Add(new SplineInfo(container, index));
IncrementVersion();
context.selection = selection.Where(x => x.target == container &&
(selected.Contains(x.targetIndex) || container.KnotLinkCollection.TryGetKnotLinks(new SplineKnotIndex(x.targetIndex, x.knotIndex), out _))).ToList();
NotifySelectionChanged();
}
internal static bool Contains(SplineInfo info)
{
return s_SelectedSplines.Contains(info);
}
internal static bool Remove(SplineInfo info)
{
return s_SelectedSplines.Remove(info);
}
}
}

View File

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