437 lines
20 KiB
C#
437 lines
20 KiB
C#
|
|
using System;
|
||
|
|
using UnityEditor;
|
||
|
|
using UnityEditor.EditorTools;
|
||
|
|
using UnityEditor.Splines;
|
||
|
|
using UnityEditor.UIElements;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.Splines;
|
||
|
|
using UnityEngine.UIElements;
|
||
|
|
|
||
|
|
namespace Unity.Cinemachine.Editor
|
||
|
|
{
|
||
|
|
[CustomEditor(typeof(CinemachineSplineRoll))]
|
||
|
|
class CinemachineSplineRollEditor : UnityEditor.Editor
|
||
|
|
{
|
||
|
|
public override VisualElement CreateInspectorGUI()
|
||
|
|
{
|
||
|
|
var ux = new VisualElement();
|
||
|
|
var splineData = target as CinemachineSplineRoll;
|
||
|
|
|
||
|
|
var invalidHelp = new HelpBox(
|
||
|
|
"This component should be associated with a non-empty spline",
|
||
|
|
HelpBoxMessageType.Warning);
|
||
|
|
ux.Add(invalidHelp);
|
||
|
|
|
||
|
|
var toolHelp = ux.AddChild(new HelpBox(
|
||
|
|
"Use the Scene View tool to adjust the roll data points", HelpBoxMessageType.Info));
|
||
|
|
toolHelp.OnInitialGeometry(() =>
|
||
|
|
{
|
||
|
|
var icon = toolHelp.Q(className: "unity-help-box__icon");
|
||
|
|
if (icon != null)
|
||
|
|
{
|
||
|
|
icon.style.backgroundImage = AssetDatabase.LoadAssetAtPath<Texture2D>(SplineRollTool.IconPath);
|
||
|
|
icon.style.marginRight = 12;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
ux.AddSpace();
|
||
|
|
|
||
|
|
ux.TrackAnyUserActivity(() =>
|
||
|
|
{
|
||
|
|
var haveSpline = splineData != null && splineData.SplineContainer != null;
|
||
|
|
invalidHelp.SetVisible(!haveSpline);
|
||
|
|
});
|
||
|
|
|
||
|
|
var rollProp = serializedObject.FindProperty(() => splineData.Roll);
|
||
|
|
ux.Add(SplineDataInspectorUtility.CreatePathUnitField(rollProp, () => splineData == null ? null : splineData.SplineContainer));
|
||
|
|
ux.Add(new PropertyField(serializedObject.FindProperty(() => splineData.Easing)));
|
||
|
|
|
||
|
|
ux.AddHeader("Data Points");
|
||
|
|
var list = ux.AddChild(SplineDataInspectorUtility.CreateDataListField(
|
||
|
|
splineData.Roll, rollProp, () => splineData == null ? null : splineData.SplineContainer));
|
||
|
|
|
||
|
|
var arrayProp = rollProp.FindPropertyRelative("m_DataPoints");
|
||
|
|
|
||
|
|
list.makeItem = () =>
|
||
|
|
{
|
||
|
|
var itemRootName = "ItemRoot";
|
||
|
|
var row = new BindableElement() { name = itemRootName, style = { flexDirection = FlexDirection.Row, marginRight = 4 }};
|
||
|
|
|
||
|
|
row.Add(new VisualElement { pickingMode = PickingMode.Ignore, style = { flexBasis = 12 }}); // pass-through for selecting row in list
|
||
|
|
var indexField = row.AddChild(InspectorUtility.CreateDraggableField(
|
||
|
|
typeof(float), "m_Index", SplineDataInspectorUtility.ItemIndexTooltip,
|
||
|
|
row.AddChild(new Label("Index")), out var dragger));
|
||
|
|
indexField.style.flexGrow = 1;
|
||
|
|
indexField.style.flexBasis = 50;
|
||
|
|
indexField.SafeSetIsDelayed();
|
||
|
|
dragger.OnStartDrag = (d) => list.selectedIndex = GetIndexInList(list, d.DragElement, itemRootName);
|
||
|
|
|
||
|
|
var def = new CinemachineSplineRoll.RollData();
|
||
|
|
var rollTooltip = SerializedPropertyHelper.PropertyTooltip(() => def.Value);
|
||
|
|
row.Add(new VisualElement { pickingMode = PickingMode.Ignore, style = { flexBasis = 12 }}); // pass-through for selecting row in list
|
||
|
|
var rollField = row.AddChild(InspectorUtility.CreateDraggableField(
|
||
|
|
typeof(float), "m_Value.Value", rollTooltip, row.AddChild(new Label("Roll")), out dragger));
|
||
|
|
rollField.style.flexGrow = 1;
|
||
|
|
rollField.style.flexBasis = 50;
|
||
|
|
dragger.OnStartDrag = (d) => list.selectedIndex = GetIndexInList(list, d.DragElement, itemRootName);
|
||
|
|
|
||
|
|
return row;
|
||
|
|
|
||
|
|
// Sneaky way to find out which list element we are
|
||
|
|
static int GetIndexInList(ListView list, VisualElement element, string itemRootName)
|
||
|
|
{
|
||
|
|
var container = list.Q("unity-content-container");
|
||
|
|
if (container != null)
|
||
|
|
{
|
||
|
|
while (element != null && element.name != itemRootName)
|
||
|
|
element = element.parent;
|
||
|
|
if (element != null)
|
||
|
|
return container.IndexOf(element);
|
||
|
|
}
|
||
|
|
return - 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
};
|
||
|
|
|
||
|
|
SplineRollTool.s_OnDataLookAtDragged += OnToolDragged;
|
||
|
|
SplineRollTool.s_OnDataIndexDragged += OnToolDragged;
|
||
|
|
void OnToolDragged(CinemachineSplineRoll data, int index)
|
||
|
|
{
|
||
|
|
EditorApplication.delayCall += () =>
|
||
|
|
{
|
||
|
|
// This is a hack to avoid spurious exceptions thrown by uitoolkit!
|
||
|
|
// GML TODO: Remove when they fix it
|
||
|
|
try
|
||
|
|
{
|
||
|
|
if (data == splineData)
|
||
|
|
list.selectedIndex = index;
|
||
|
|
}
|
||
|
|
catch {} // Ignore exceptions
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
ux.TrackPropertyValue(rollProp, (p) =>
|
||
|
|
{
|
||
|
|
// Invalidate the mesh cache when the property changes (SetDirty() not always called!)
|
||
|
|
SplineGizmoCache.Instance = null;
|
||
|
|
EditorApplication.delayCall += () => InspectorUtility.RepaintGameView();
|
||
|
|
});
|
||
|
|
|
||
|
|
return ux;
|
||
|
|
}
|
||
|
|
|
||
|
|
[DrawGizmo(GizmoType.Active | GizmoType.NotInSelectionHierarchy
|
||
|
|
| GizmoType.InSelectionHierarchy | GizmoType.Pickable, typeof(CinemachineSplineRoll))]
|
||
|
|
static void DrawGizmos(CinemachineSplineRoll splineRoll, GizmoType selectionType)
|
||
|
|
{
|
||
|
|
// For performance reasons, we only draw a gizmo for the current active game object
|
||
|
|
if (Selection.activeGameObject == splineRoll.gameObject)
|
||
|
|
{
|
||
|
|
DrawSplineGizmo(splineRoll,
|
||
|
|
splineRoll.enabled ? CinemachineSplineDollyPrefs.SplineRollColor.Value : Color.gray,
|
||
|
|
CinemachineSplineDollyPrefs.SplineWidth.Value, CinemachineSplineDollyPrefs.SplineResolution.Value,
|
||
|
|
CinemachineSplineDollyPrefs.ShowSplineNormals.Value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void DrawSplineGizmo(CinemachineSplineRoll splineRoll, Color pathColor, float width, int resolution, bool showNormals)
|
||
|
|
{
|
||
|
|
var splineContainer = splineRoll == null ? null : splineRoll.SplineContainer as SplineContainer;
|
||
|
|
if (!splineContainer.IsValid())
|
||
|
|
return;
|
||
|
|
|
||
|
|
var transform = splineContainer.transform;
|
||
|
|
var scale = transform.lossyScale;
|
||
|
|
width = width * 3 / (scale.x + scale.y + scale.z);
|
||
|
|
|
||
|
|
// Rebuild the cached mesh if necessary. This can be expensive!
|
||
|
|
if (SplineGizmoCache.Instance == null
|
||
|
|
|| SplineGizmoCache.Instance.Mesh == null
|
||
|
|
|| SplineGizmoCache.Instance.Spline != splineContainer.Spline
|
||
|
|
|| SplineGizmoCache.Instance.RollData != splineRoll.Roll
|
||
|
|
|| SplineGizmoCache.Instance.Width != width
|
||
|
|
|| SplineGizmoCache.Instance.Resolution != resolution
|
||
|
|
|| SplineGizmoCache.Instance.Enabled != splineRoll.enabled
|
||
|
|
|| SplineGizmoCache.Instance.ShowNormals != showNormals)
|
||
|
|
{
|
||
|
|
var numKnots = splineContainer.Spline.Count;
|
||
|
|
var numSteps = numKnots * resolution;
|
||
|
|
var stepSize = 1.0f / numSteps;
|
||
|
|
var halfWidth = width * 0.5f;
|
||
|
|
|
||
|
|
// For efficiency, we create a mesh with the track and draw it in one shot
|
||
|
|
var scaledSpline = new CachedScaledSpline(splineContainer.Spline, transform, Collections.Allocator.Temp);
|
||
|
|
scaledSpline.LocalEvaluateSplineWithRoll(0, splineRoll, out var p, out var q);
|
||
|
|
|
||
|
|
numSteps++; // ceil
|
||
|
|
var vertices = new Vector3[3 * numSteps];
|
||
|
|
var normals = new Vector3[vertices.Length];
|
||
|
|
var indices = new int[showNormals ? 2 * 2 * numSteps : 2 * 3 * numSteps];
|
||
|
|
int vIndex = 0;
|
||
|
|
|
||
|
|
if (showNormals)
|
||
|
|
{
|
||
|
|
// Draw line with normals
|
||
|
|
var w = q * Vector3.up * width;
|
||
|
|
|
||
|
|
vertices[vIndex] = p; normals[vIndex++] = Vector3.up;
|
||
|
|
vertices[vIndex] = p + w; normals[vIndex++] = Vector3.up;
|
||
|
|
|
||
|
|
int iIndex = 0;
|
||
|
|
for (int i = 1; i < numSteps; ++i)
|
||
|
|
{
|
||
|
|
var t = i * stepSize;
|
||
|
|
scaledSpline.LocalEvaluateSplineWithRoll(t, splineRoll, out p, out q);
|
||
|
|
w = q * Vector3.up * width;
|
||
|
|
indices[iIndex++] = vIndex - 2;
|
||
|
|
indices[iIndex++] = vIndex - 1;
|
||
|
|
|
||
|
|
vertices[vIndex] = p; normals[vIndex++] = Vector3.up;
|
||
|
|
vertices[vIndex] = p + w; normals[vIndex++] = Vector3.up;
|
||
|
|
|
||
|
|
indices[iIndex++] = vIndex - 4;
|
||
|
|
indices[iIndex++] = vIndex - 2;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Draw railroad track
|
||
|
|
var w = q * Vector3.right * halfWidth;
|
||
|
|
|
||
|
|
vertices[vIndex] = p - w; normals[vIndex++] = Vector3.up;
|
||
|
|
vertices[vIndex] = p + w; normals[vIndex++] = Vector3.up;
|
||
|
|
|
||
|
|
int iIndex = 0;
|
||
|
|
for (int i = 1; i < numSteps; ++i)
|
||
|
|
{
|
||
|
|
var t = i * stepSize;
|
||
|
|
scaledSpline.LocalEvaluateSplineWithRoll(t, splineRoll, out p, out q);
|
||
|
|
w = q * Vector3.right * halfWidth;
|
||
|
|
|
||
|
|
indices[iIndex++] = vIndex - 2;
|
||
|
|
indices[iIndex++] = vIndex - 1;
|
||
|
|
|
||
|
|
vertices[vIndex] = p - w; normals[vIndex++] = Vector3.up;
|
||
|
|
vertices[vIndex] = p + w; normals[vIndex++] = Vector3.up;
|
||
|
|
|
||
|
|
indices[iIndex++] = vIndex - 4;
|
||
|
|
indices[iIndex++] = vIndex - 2;
|
||
|
|
indices[iIndex++] = vIndex - 3;
|
||
|
|
indices[iIndex++] = vIndex - 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var mesh = new Mesh();
|
||
|
|
mesh.SetVertices(vertices);
|
||
|
|
mesh.SetNormals(normals);
|
||
|
|
mesh.SetIndices(indices, MeshTopology.Lines, 0);
|
||
|
|
|
||
|
|
SplineGizmoCache.Instance = new SplineGizmoCache
|
||
|
|
{
|
||
|
|
Mesh = mesh,
|
||
|
|
Spline = splineContainer.Spline,
|
||
|
|
RollData = splineRoll.Roll,
|
||
|
|
Width = width,
|
||
|
|
Resolution = resolution,
|
||
|
|
Enabled = splineRoll.enabled,
|
||
|
|
ShowNormals = showNormals
|
||
|
|
};
|
||
|
|
}
|
||
|
|
// Draw the path
|
||
|
|
var colorOld = Gizmos.color;
|
||
|
|
var matrixOld = Gizmos.matrix;
|
||
|
|
Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
|
||
|
|
Gizmos.color = pathColor;
|
||
|
|
Gizmos.DrawWireMesh(SplineGizmoCache.Instance.Mesh);
|
||
|
|
Gizmos.matrix = matrixOld;
|
||
|
|
Gizmos.color = colorOld;
|
||
|
|
}
|
||
|
|
|
||
|
|
[InitializeOnLoad]
|
||
|
|
class SplineGizmoCache
|
||
|
|
{
|
||
|
|
public Mesh Mesh;
|
||
|
|
public SplineData<CinemachineSplineRoll.RollData> RollData;
|
||
|
|
public Spline Spline;
|
||
|
|
public float Width;
|
||
|
|
public int Resolution;
|
||
|
|
public bool Enabled;
|
||
|
|
public bool ShowNormals;
|
||
|
|
|
||
|
|
public static SplineGizmoCache Instance;
|
||
|
|
|
||
|
|
// Invalidate the cache whenever the cached spline's data changes
|
||
|
|
static SplineGizmoCache()
|
||
|
|
{
|
||
|
|
Instance = null;
|
||
|
|
EditorSplineUtility.AfterSplineWasModified -= OnSplineChanged;
|
||
|
|
EditorSplineUtility.AfterSplineWasModified += OnSplineChanged;
|
||
|
|
EditorSplineUtility.UnregisterSplineDataChanged<CinemachineSplineRoll.RollData>(OnSplineDataChanged);
|
||
|
|
EditorSplineUtility.RegisterSplineDataChanged<CinemachineSplineRoll.RollData>(OnSplineDataChanged);
|
||
|
|
}
|
||
|
|
static void OnSplineChanged(Spline spline)
|
||
|
|
{
|
||
|
|
if (Instance != null && spline == Instance.Spline)
|
||
|
|
Instance = null;
|
||
|
|
}
|
||
|
|
static void OnSplineDataChanged(SplineData<CinemachineSplineRoll.RollData> data)
|
||
|
|
{
|
||
|
|
if (Instance != null && data == Instance.RollData)
|
||
|
|
Instance = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[EditorTool("Spline Roll Tool", typeof(CinemachineSplineRoll))]
|
||
|
|
sealed class SplineRollTool : EditorTool
|
||
|
|
{
|
||
|
|
GUIContent m_IconContent;
|
||
|
|
public override GUIContent toolbarIcon => m_IconContent;
|
||
|
|
|
||
|
|
public static Action<CinemachineSplineRoll, int> s_OnDataIndexDragged;
|
||
|
|
public static Action<CinemachineSplineRoll, int> s_OnDataLookAtDragged;
|
||
|
|
|
||
|
|
public static string IconPath => $"{CinemachineSceneToolHelpers.IconPath}/CmSplineRollTool@256.png";
|
||
|
|
|
||
|
|
void OnEnable()
|
||
|
|
{
|
||
|
|
m_IconContent = new GUIContent
|
||
|
|
{
|
||
|
|
image = AssetDatabase.LoadAssetAtPath<Texture2D>(IconPath),
|
||
|
|
tooltip = "Adjust the roll data points along the spline"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
bool GetTargets(out CinemachineSplineRoll splineData, out SplineContainer spline, out bool enabled)
|
||
|
|
{
|
||
|
|
splineData = target as CinemachineSplineRoll;
|
||
|
|
if (splineData != null)
|
||
|
|
{
|
||
|
|
enabled = splineData.enabled;
|
||
|
|
spline = splineData.SplineContainer as SplineContainer;
|
||
|
|
return spline != null && spline.Spline != null;
|
||
|
|
}
|
||
|
|
enabled = false;
|
||
|
|
spline = null;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public override void OnToolGUI(EditorWindow window)
|
||
|
|
{
|
||
|
|
if (!GetTargets(out var splineData, out var spline, out var enabled))
|
||
|
|
return;
|
||
|
|
|
||
|
|
Undo.RecordObject(splineData, "Modifying Roll RollData");
|
||
|
|
var color = enabled ? Handles.selectedColor : Color.gray;
|
||
|
|
using (new Handles.DrawingScope(color))
|
||
|
|
{
|
||
|
|
var nativeSpline = new NativeSpline(spline.Spline, spline.transform.localToWorldMatrix);
|
||
|
|
|
||
|
|
int changedIndex = -1;
|
||
|
|
if (enabled)
|
||
|
|
changedIndex = DrawIndexPointHandles(nativeSpline, splineData.Roll);
|
||
|
|
if (changedIndex >= 0)
|
||
|
|
s_OnDataIndexDragged?.Invoke(splineData, changedIndex);
|
||
|
|
changedIndex = DrawDataPointHandles(nativeSpline, splineData.Roll, enabled);
|
||
|
|
if (changedIndex >= 0)
|
||
|
|
s_OnDataLookAtDragged?.Invoke(splineData, changedIndex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int DrawIndexPointHandles(ISpline spline, SplineData<CinemachineSplineRoll.RollData> splineData)
|
||
|
|
{
|
||
|
|
int anchorId = GUIUtility.GetControlID(FocusType.Passive);
|
||
|
|
spline.DataPointHandles(splineData);
|
||
|
|
int nearestIndex = ControlIdToIndex(anchorId, HandleUtility.nearestControl, splineData.Count);
|
||
|
|
var hotIndex = ControlIdToIndex(anchorId, GUIUtility.hotControl, splineData.Count);
|
||
|
|
var tooltipIndex = hotIndex >= 0 ? hotIndex : nearestIndex;
|
||
|
|
if (tooltipIndex >= 0)
|
||
|
|
DrawTooltip(spline, splineData, tooltipIndex);
|
||
|
|
|
||
|
|
// Return the index that's being changed, or -1
|
||
|
|
return hotIndex;
|
||
|
|
|
||
|
|
// Local function
|
||
|
|
static int ControlIdToIndex(int anchorId, int controlId, int targetCount)
|
||
|
|
{
|
||
|
|
int index = controlId - anchorId - 2;
|
||
|
|
return index >= 0 && index < targetCount ? index : -1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// inverse pre-calculation optimization
|
||
|
|
readonly Quaternion m_DefaultHandleOrientation = Quaternion.Euler(270, 0, 0);
|
||
|
|
readonly Quaternion m_DefaultHandleOrientationInverse = Quaternion.Euler(90, 0, 0);
|
||
|
|
|
||
|
|
int DrawDataPointHandles(ISpline spline, SplineData<CinemachineSplineRoll.RollData> splineData, bool enabled)
|
||
|
|
{
|
||
|
|
int changed = -1;
|
||
|
|
int tooltipIndex = -1;
|
||
|
|
for (var i = 0; i < splineData.Count; ++i)
|
||
|
|
{
|
||
|
|
var dataPoint = splineData[i];
|
||
|
|
var t = SplineUtility.GetNormalizedInterpolation(spline, dataPoint.Index, splineData.PathIndexUnit);
|
||
|
|
spline.LocalEvaluateSplineWithRoll(t, null, out var position, out var rotation); // don't consider roll
|
||
|
|
|
||
|
|
var id = GUIUtility.GetControlID(FocusType.Passive);
|
||
|
|
if (DrawDataPoint(id, position, rotation, -dataPoint.Value, out var result) && enabled)
|
||
|
|
{
|
||
|
|
dataPoint.Value = -result;
|
||
|
|
splineData.SetDataPoint(i, dataPoint);
|
||
|
|
changed = i;
|
||
|
|
}
|
||
|
|
if (enabled && tooltipIndex < 0 && id == HandleUtility.nearestControl || id == GUIUtility.hotControl)
|
||
|
|
tooltipIndex = i;
|
||
|
|
}
|
||
|
|
if (tooltipIndex >= 0)
|
||
|
|
DrawTooltip(spline, splineData, tooltipIndex);
|
||
|
|
return changed;
|
||
|
|
|
||
|
|
// local function
|
||
|
|
bool DrawDataPoint(int controlID, Vector3 position, Quaternion rotation, float rollData, out float result)
|
||
|
|
{
|
||
|
|
result = 0;
|
||
|
|
var drawMatrix = Handles.matrix * Matrix4x4.TRS(position, rotation, Vector3.one);
|
||
|
|
using (new Handles.DrawingScope(drawMatrix)) // use draw matrix, so we work in local space
|
||
|
|
{
|
||
|
|
var localRot = Quaternion.Euler(0, rollData, 0);
|
||
|
|
var globalRot = m_DefaultHandleOrientation * localRot;
|
||
|
|
|
||
|
|
var handleSize = HandleUtility.GetHandleSize(Vector3.zero) / 2f;
|
||
|
|
if (Event.current.type == EventType.Repaint)
|
||
|
|
Handles.ArrowHandleCap(-1, Vector3.zero, globalRot, handleSize, EventType.Repaint);
|
||
|
|
|
||
|
|
var newGlobalRot = Handles.Disc(controlID, globalRot, Vector3.zero, Vector3.forward, handleSize, false, 0);
|
||
|
|
if (GUIUtility.hotControl == controlID)
|
||
|
|
{
|
||
|
|
// Handles.Disc returns roll values in the [0, 360] range. Therefore, it works only in fixed ranges
|
||
|
|
// For example, within any of these ..., [-720, -360], [-360, 0], [0, 360], [360, 720], ...
|
||
|
|
// But we want to be able to rotate through these ranges, and not get stuck. We can detect when to
|
||
|
|
// move between ranges: when the roll delta is big. e.g. 359 -> 1 (358), instead of 1 -> 2 (1)
|
||
|
|
var newLocalRot = m_DefaultHandleOrientationInverse * newGlobalRot;
|
||
|
|
var deltaRoll = newLocalRot.eulerAngles.y - localRot.eulerAngles.y;
|
||
|
|
if (deltaRoll > 180)
|
||
|
|
deltaRoll -= 360; // Roll down one range
|
||
|
|
else if (deltaRoll < -180)
|
||
|
|
deltaRoll += 360; // Roll up one range
|
||
|
|
|
||
|
|
rollData += deltaRoll;
|
||
|
|
result = rollData;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void DrawTooltip(ISpline spline, SplineData<CinemachineSplineRoll.RollData> splineData, int index)
|
||
|
|
{
|
||
|
|
var dataPoint = splineData[index];
|
||
|
|
var text = $"Index: {dataPoint.Index}\nRoll: {dataPoint.Value.Value}";
|
||
|
|
|
||
|
|
var t = SplineUtility.GetNormalizedInterpolation(spline, dataPoint.Index, splineData.PathIndexUnit);
|
||
|
|
var position = spline.EvaluatePosition(t);
|
||
|
|
CinemachineSceneToolHelpers.DrawLabel(position, text);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|