This commit is contained in:
2021-06-13 10:28:03 +02:00
parent eb70603c85
commit df2d24cbd3
7487 changed files with 943244 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
# Changelog
## [5.0.0] - 2021-03-17
### Changed
- Update version for release
## [5.0.0-pre.2] - 2021-01-16
### Changed
- Update license
### Fixed
- 1281920 Fixed case where vertices stay selected when switching between multiple SpriteShape objects while in Edit mode.
## [5.0.0-pre.1] - 2020-10-30
### Changed
- Version bump for Unity 2021.1
## [4.0.1] - 2020-07-17
### Changed
- Snapping Control Points and Tangents use Global Move Snap settings.
## [4.0.0] - 2020-05-11
### Changed
- Version bump for Unity 2020.2
## [3.0.1] - 2020-04-20
### Changed
- Added return for Path tool usage change.
## [3.0.0] - 2019-11-06
### Changed
- Update version number for Unity 2020.1
## [2.0.4] - 2019-10-27
### Fixed
- Added missing meta file
## [2.0.3] - 2019-10-02
### Fixed
- Removed resources
## [2.0.2] - 2019-07-24
### Added
- Add related test packages
## [2.0.1] - 2019-07-13
### Changed
- Mark package to support Unity 2019.3.0a10 onwards.
## [2.0.0] - 2019-06-19
### Changed.
- Stable Version.
## [1.0.0-preview.3] - 2019-06-14
### Fixed
- Fix undo on select point.
## [1.0.0-preview.1] - 2019-04-22
### Added
- Initial Version.

View File

@@ -0,0 +1,223 @@
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public static class BezierUtility
{
static Vector3[] s_TempPoints = new Vector3[3];
public static Vector3 BezierPoint(Vector3 startPosition, Vector3 startTangent, Vector3 endTangent, Vector3 endPosition, float t)
{
float s = 1.0f - t;
return startPosition * s * s * s + startTangent * s * s * t * 3.0f + endTangent * s * t * t * 3.0f + endPosition * t * t * t;
}
public static Vector3 ClosestPointOnCurve(Vector3 point, Vector3 startPosition, Vector3 endPosition, Vector3 startTangent, Vector3 endTangent, out float t)
{
Vector3 startToEnd = endPosition - startPosition;
Vector3 startToTangent = (startTangent - startPosition);
Vector3 endToTangent = (endTangent - endPosition);
float sqrError = 0.001f;
if (Colinear(startToTangent, startToEnd, sqrError) && Colinear(endToTangent, startToEnd, sqrError))
return ClosestPointToSegment(point, startPosition, endPosition, out t);
Vector3 leftStartPosition;
Vector3 leftEndPosition;
Vector3 leftStartTangent;
Vector3 leftEndTangent;
Vector3 rightStartPosition;
Vector3 rightEndPosition;
Vector3 rightStartTangent;
Vector3 rightEndTangent;
float leftStartT = 0f;
float leftEndT = 0.5f;
float rightStartT = 0.5f;
float rightEndT = 1f;
SplitBezier(0.5f, startPosition, endPosition, startTangent, endTangent,
out leftStartPosition, out leftEndPosition, out leftStartTangent, out leftEndTangent,
out rightStartPosition, out rightEndPosition, out rightStartTangent, out rightEndTangent);
Vector3 pointLeft = ClosestPointOnCurveIterative(point, leftStartPosition, leftEndPosition, leftStartTangent, leftEndTangent, sqrError, ref leftStartT, ref leftEndT);
Vector3 pointRight = ClosestPointOnCurveIterative(point, rightStartPosition, rightEndPosition, rightStartTangent, rightEndTangent, sqrError, ref rightStartT, ref rightEndT);
if ((point - pointLeft).sqrMagnitude < (point - pointRight).sqrMagnitude)
{
t = leftStartT;
return pointLeft;
}
t = rightStartT;
return pointRight;
}
public static Vector3 ClosestPointOnCurveFast(Vector3 point, Vector3 startPosition, Vector3 endPosition, Vector3 startTangent, Vector3 endTangent, out float t)
{
float sqrError = 0.001f;
float startT = 0f;
float endT = 1f;
Vector3 closestPoint = ClosestPointOnCurveIterative(point, startPosition, endPosition, startTangent, endTangent, sqrError, ref startT, ref endT);
t = startT;
return closestPoint;
}
private static Vector3 ClosestPointOnCurveIterative(Vector3 point, Vector3 startPosition, Vector3 endPosition, Vector3 startTangent, Vector3 endTangent, float sqrError, ref float startT, ref float endT)
{
while ((startPosition - endPosition).sqrMagnitude > sqrError)
{
Vector3 startToEnd = endPosition - startPosition;
Vector3 startToTangent = (startTangent - startPosition);
Vector3 endToTangent = (endTangent - endPosition);
if (Colinear(startToTangent, startToEnd, sqrError) && Colinear(endToTangent, startToEnd, sqrError))
{
float t;
Vector3 closestPoint = ClosestPointToSegment(point, startPosition, endPosition, out t);
t *= (endT - startT);
startT += t;
endT -= t;
return closestPoint;
}
Vector3 leftStartPosition;
Vector3 leftEndPosition;
Vector3 leftStartTangent;
Vector3 leftEndTangent;
Vector3 rightStartPosition;
Vector3 rightEndPosition;
Vector3 rightStartTangent;
Vector3 rightEndTangent;
SplitBezier(0.5f, startPosition, endPosition, startTangent, endTangent,
out leftStartPosition, out leftEndPosition, out leftStartTangent, out leftEndTangent,
out rightStartPosition, out rightEndPosition, out rightStartTangent, out rightEndTangent);
s_TempPoints[0] = leftStartPosition;
s_TempPoints[1] = leftStartTangent;
s_TempPoints[2] = leftEndTangent;
float sqrDistanceLeft = SqrDistanceToPolyLine(point, s_TempPoints);
s_TempPoints[0] = rightEndPosition;
s_TempPoints[1] = rightEndTangent;
s_TempPoints[2] = rightStartTangent;
float sqrDistanceRight = SqrDistanceToPolyLine(point, s_TempPoints);
if (sqrDistanceLeft < sqrDistanceRight)
{
startPosition = leftStartPosition;
endPosition = leftEndPosition;
startTangent = leftStartTangent;
endTangent = leftEndTangent;
endT -= (endT - startT) * 0.5f;
}
else
{
startPosition = rightStartPosition;
endPosition = rightEndPosition;
startTangent = rightStartTangent;
endTangent = rightEndTangent;
startT += (endT - startT) * 0.5f;
}
}
return endPosition;
}
public static void SplitBezier(float t, Vector3 startPosition, Vector3 endPosition, Vector3 startRightTangent, Vector3 endLeftTangent,
out Vector3 leftStartPosition, out Vector3 leftEndPosition, out Vector3 leftStartTangent, out Vector3 leftEndTangent,
out Vector3 rightStartPosition, out Vector3 rightEndPosition, out Vector3 rightStartTangent, out Vector3 rightEndTangent)
{
Vector3 tangent0 = (startRightTangent - startPosition);
Vector3 tangent1 = (endLeftTangent - endPosition);
Vector3 tangentEdge = (endLeftTangent - startRightTangent);
Vector3 tangentPoint0 = startPosition + tangent0 * t;
Vector3 tangentPoint1 = endPosition + tangent1 * (1f - t);
Vector3 tangentEdgePoint = startRightTangent + tangentEdge * t;
Vector3 newTangent0 = tangentPoint0 + (tangentEdgePoint - tangentPoint0) * t;
Vector3 newTangent1 = tangentPoint1 + (tangentEdgePoint - tangentPoint1) * (1f - t);
Vector3 newTangentEdge = newTangent1 - newTangent0;
Vector3 bezierPoint = newTangent0 + newTangentEdge * t;
leftStartPosition = startPosition;
leftEndPosition = bezierPoint;
leftStartTangent = tangentPoint0;
leftEndTangent = newTangent0;
rightStartPosition = bezierPoint;
rightEndPosition = endPosition;
rightStartTangent = newTangent1;
rightEndTangent = tangentPoint1;
}
private static Vector3 ClosestPointToSegment(Vector3 point, Vector3 segmentStart, Vector3 segmentEnd, out float t)
{
Vector3 relativePoint = point - segmentStart;
Vector3 segment = (segmentEnd - segmentStart);
Vector3 segmentDirection = segment.normalized;
float length = segment.magnitude;
float dot = Vector3.Dot(relativePoint, segmentDirection);
if (dot <= 0f)
dot = 0f;
else if (dot >= length)
dot = length;
t = dot / length;
return segmentStart + segment * t;
}
private static float SqrDistanceToPolyLine(Vector3 point, Vector3[] points)
{
float minDistance = float.MaxValue;
for (int i = 0; i < points.Length - 1; ++i)
{
float distance = SqrDistanceToSegment(point, points[i], points[i + 1]);
if (distance < minDistance)
minDistance = distance;
}
return minDistance;
}
private static float SqrDistanceToSegment(Vector3 point, Vector3 segmentStart, Vector3 segmentEnd)
{
Vector3 relativePoint = point - segmentStart;
Vector3 segment = (segmentEnd - segmentStart);
Vector3 segmentDirection = segment.normalized;
float length = segment.magnitude;
float dot = Vector3.Dot(relativePoint, segmentDirection);
if (dot <= 0f)
return (point - segmentStart).sqrMagnitude;
else if (dot >= length)
return (point - segmentEnd).sqrMagnitude;
return Vector3.Cross(relativePoint, segmentDirection).sqrMagnitude;
}
private static bool Colinear(Vector3 v1, Vector3 v2, float error = 0.0001f)
{
return Mathf.Abs(v1.x * v2.y - v1.y * v2.x + v1.x * v2.z - v1.z * v2.x + v1.y * v2.z - v1.z * v2.y) < error;
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public enum TangentMode
{
Linear = 0,
Continuous = 1,
Broken = 2
}
[Serializable]
public struct TangentCache
{
public Vector3 leftTangent;
public Vector3 rightTangent;
}
[Serializable]
public struct ControlPoint
{
public Vector3 position;
public Vector3 localLeftTangent;
public Vector3 localRightTangent;
public TangentMode tangentMode;
public TangentCache continuousCache;
public TangentCache brokenCache;
public bool mirrorLeft;
public Vector3 leftTangent
{
get { return localLeftTangent + position; }
set { localLeftTangent = value - position; }
}
public Vector3 rightTangent
{
get { return localRightTangent + position; }
set { localRightTangent = value - position; }
}
public void StoreTangents()
{
if (tangentMode == TangentMode.Continuous)
{
continuousCache.leftTangent = localLeftTangent;
continuousCache.rightTangent = localRightTangent;
}
else if (tangentMode == TangentMode.Broken)
{
brokenCache.leftTangent = localLeftTangent;
brokenCache.rightTangent = localRightTangent;
}
}
public void RestoreTangents()
{
if (tangentMode == TangentMode.Continuous)
{
localLeftTangent = continuousCache.leftTangent;
localRightTangent = continuousCache.rightTangent;
}
else if (tangentMode == TangentMode.Broken)
{
localLeftTangent = brokenCache.leftTangent;
localRightTangent = brokenCache.rightTangent;
}
}
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
[Serializable]
public class EditablePath : IEditablePath
{
[SerializeField]
private ShapeType m_ShapeType;
[SerializeField]
private IndexedSelection m_Selection = new IndexedSelection();
[SerializeField]
private List<ControlPoint> m_ControlPoints = new List<ControlPoint>();
[SerializeField]
private bool m_IsOpenEnded;
private Matrix4x4 m_LocalToWorldMatrix = Matrix4x4.identity;
private Matrix4x4 m_WorldToLocalMatrix = Matrix4x4.identity;
private Vector3 m_Forward = Vector3.forward;
private Vector3 m_Up = Vector3.up;
private Vector3 m_Right = Vector3.right;
public ShapeType shapeType
{
get { return m_ShapeType; }
set { m_ShapeType = value; }
}
public IUndoObject undoObject { get; set; }
public Matrix4x4 localToWorldMatrix
{
get { return m_LocalToWorldMatrix; }
set
{
m_LocalToWorldMatrix = value;
m_WorldToLocalMatrix = value.inverse;
}
}
public Vector3 forward
{
get { return m_Forward; }
set { m_Forward = value; }
}
public Vector3 up
{
get { return m_Up; }
set { m_Up = value; }
}
public Vector3 right
{
get { return m_Right; }
set { m_Right = value; }
}
public Matrix4x4 worldToLocalMatrix
{
get { return m_WorldToLocalMatrix; }
}
public bool isOpenEnded
{
get
{
if (pointCount < 3)
return true;
return m_IsOpenEnded;
}
set { m_IsOpenEnded = value; }
}
public ISelection<int> selection
{
get { return m_Selection; }
}
public int pointCount
{
get { return m_ControlPoints.Count; }
}
public ControlPoint GetPoint(int index)
{
return TransformPoint(localToWorldMatrix, m_ControlPoints[index]);
}
public void SetPoint(int index, ControlPoint controlPoint)
{
m_ControlPoints[index] = TransformPoint(worldToLocalMatrix, controlPoint);
}
public void AddPoint(ControlPoint controlPoint)
{
m_ControlPoints.Insert(pointCount, TransformPoint(worldToLocalMatrix, controlPoint));
}
public void InsertPoint(int index, ControlPoint controlPoint)
{
m_ControlPoints.Insert(index, TransformPoint(worldToLocalMatrix, controlPoint));
}
public void RemovePoint(int index)
{
m_ControlPoints.RemoveAt(index);
}
public void Clear()
{
m_ControlPoints.Clear();
}
private ControlPoint TransformPoint(Matrix4x4 transformMatrix, ControlPoint controlPoint)
{
if (transformMatrix == Matrix4x4.identity)
return controlPoint;
var newControlPoint = new ControlPoint()
{
position = transformMatrix.MultiplyPoint3x4(controlPoint.position),
tangentMode = controlPoint.tangentMode,
continuousCache = controlPoint.continuousCache,
brokenCache = controlPoint.brokenCache,
mirrorLeft = controlPoint.mirrorLeft
};
newControlPoint.rightTangent = transformMatrix.MultiplyPoint3x4(controlPoint.rightTangent);
newControlPoint.leftTangent = transformMatrix.MultiplyPoint3x4(controlPoint.leftTangent);
return newControlPoint;
}
public bool Select(ISelector<Vector3> selector)
{
var changed = false;
for (var i = 0; i < pointCount; ++i)
changed |= selection.Select(i, selector.Select(GetPoint(i).position));
return changed;
}
}
}

View File

@@ -0,0 +1,259 @@
using System;
using System.Linq;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public class EditablePathController : IEditablePathController
{
private ISnapping<Vector3> m_Snapping = new Snapping();
public IEditablePath editablePath { get; set; }
public IEditablePath closestEditablePath { get { return editablePath; } }
public ISnapping<Vector3> snapping
{
get { return m_Snapping; }
set { m_Snapping = value; }
}
public bool enableSnapping { get; set; }
public void RegisterUndo(string name)
{
if (editablePath.undoObject != null)
editablePath.undoObject.RegisterUndo(name);
}
public void ClearSelection()
{
editablePath.selection.Clear();
}
public void SelectPoint(int index, bool select)
{
editablePath.selection.Select(index, select);
}
public void CreatePoint(int index, Vector3 position)
{
ClearSelection();
if (editablePath.shapeType == ShapeType.Polygon)
{
editablePath.InsertPoint(index + 1, new ControlPoint() { position = position });
}
else if (editablePath.shapeType == ShapeType.Spline)
{
var nextIndex = NextIndex(index);
var currentPoint = editablePath.GetPoint(index);
var nextPoint = editablePath.GetPoint(nextIndex);
float t;
var closestPoint = BezierUtility.ClosestPointOnCurve(
position,
currentPoint.position,
nextPoint.position,
GetRightTangentPosition(index),
GetLeftTangentPosition(nextIndex),
out t);
Vector3 leftStartPosition;
Vector3 leftEndPosition;
Vector3 leftStartTangent;
Vector3 leftEndTangent;
Vector3 rightStartPosition;
Vector3 rightEndPosition;
Vector3 rightStartTangent;
Vector3 rightEndTangent;
BezierUtility.SplitBezier(t, currentPoint.position, nextPoint.position, GetRightTangentPosition(index), GetLeftTangentPosition(nextIndex),
out leftStartPosition, out leftEndPosition, out leftStartTangent, out leftEndTangent,
out rightStartPosition, out rightEndPosition, out rightStartTangent, out rightEndTangent);
var newPointIndex = index + 1;
var newPoint = new ControlPoint()
{
position = closestPoint,
leftTangent = leftEndTangent,
rightTangent = rightStartTangent,
tangentMode = TangentMode.Continuous
};
currentPoint.rightTangent = leftStartTangent;
nextPoint.leftTangent = rightEndTangent;
if (currentPoint.tangentMode == TangentMode.Linear && nextPoint.tangentMode == TangentMode.Linear)
{
newPoint.tangentMode = TangentMode.Linear;
newPoint.localLeftTangent = Vector3.zero;
newPoint.localRightTangent = Vector3.zero;
currentPoint.localRightTangent = Vector3.zero;
nextPoint.localLeftTangent = Vector3.zero;
}
else
{
if (currentPoint.tangentMode == TangentMode.Linear)
currentPoint.tangentMode = TangentMode.Broken;
if (nextPoint.tangentMode == TangentMode.Linear)
nextPoint.tangentMode = TangentMode.Broken;
}
editablePath.SetPoint(index, currentPoint);
editablePath.SetPoint(nextIndex, nextPoint);
editablePath.InsertPoint(newPointIndex, newPoint);
}
}
public void RemoveSelectedPoints()
{
var minPointCount = editablePath.isOpenEnded ? 2 : 3;
if (editablePath.pointCount > minPointCount)
{
var indices = editablePath.selection.elements.OrderByDescending( i => i);
foreach (var index in indices)
if (editablePath.pointCount > minPointCount)
editablePath.RemovePoint(index);
ClearSelection();
}
}
public void MoveSelectedPoints(Vector3 delta)
{
delta = Vector3.ProjectOnPlane(delta, editablePath.forward);
for (var i = 0; i < editablePath.pointCount; ++i)
{
if (editablePath.selection.Contains(i))
{
var controlPoint = editablePath.GetPoint(i);
controlPoint.position += delta;
editablePath.SetPoint(i, controlPoint);
}
}
}
public void MoveEdge(int index, Vector3 delta)
{
if (editablePath.isOpenEnded && index == editablePath.pointCount - 1)
return;
var controlPoint = editablePath.GetPoint(index);
controlPoint.position += delta;
editablePath.SetPoint(index, controlPoint);
controlPoint = NextControlPoint(index);
controlPoint.position += delta;
editablePath.SetPoint(NextIndex(index), controlPoint);
}
public void SetLeftTangent(int index, Vector3 position, bool setToLinear, bool mirror, Vector3 cachedRightTangent, TangentMode cachedTangentMode)
{
var controlPoint = editablePath.GetPoint(index);
controlPoint.tangentMode = cachedTangentMode;
controlPoint.leftTangent = position;
controlPoint.mirrorLeft = false;
if (setToLinear)
{
controlPoint.leftTangent = controlPoint.position;
controlPoint.rightTangent = cachedRightTangent;
}
else if (controlPoint.tangentMode == TangentMode.Continuous || mirror)
{
var magnitude = controlPoint.localRightTangent.magnitude;
if (mirror)
magnitude = controlPoint.localLeftTangent.magnitude;
controlPoint.localRightTangent = magnitude * -controlPoint.localLeftTangent.normalized;
}
editablePath.SetPoint(index, controlPoint);
editablePath.UpdateTangentMode(index);
}
public void SetRightTangent(int index, Vector3 position, bool setToLinear, bool mirror, Vector3 cachedLeftTangent, TangentMode cachedTangentMode)
{
var controlPoint = editablePath.GetPoint(index);
controlPoint.tangentMode = cachedTangentMode;
controlPoint.rightTangent = position;
controlPoint.mirrorLeft = true;
if (setToLinear)
{
controlPoint.rightTangent = controlPoint.position;
controlPoint.leftTangent = cachedLeftTangent;
}
else if (controlPoint.tangentMode == TangentMode.Continuous || mirror)
{
var magnitude = controlPoint.localLeftTangent.magnitude;
if (mirror)
magnitude = controlPoint.localRightTangent.magnitude;
controlPoint.localLeftTangent = magnitude * -controlPoint.localRightTangent.normalized;
}
editablePath.SetPoint(index, controlPoint);
editablePath.UpdateTangentMode(index);
}
public void ClearClosestPath() { }
public void AddClosestPath(float distance) { }
private Vector3 GetLeftTangentPosition(int index)
{
var isLinear = Mathf.Approximately(editablePath.GetPoint(index).localLeftTangent.sqrMagnitude, 0f);
if (isLinear)
{
var position = editablePath.GetPoint(index).position;
var prevPosition = PrevControlPoint(index).position;
return (1f / 3f) * (prevPosition - position) + position;
}
return editablePath.GetPoint(index).leftTangent;
}
private Vector3 GetRightTangentPosition(int index)
{
var isLinear = Mathf.Approximately(editablePath.GetPoint(index).localRightTangent.sqrMagnitude, 0f);
if (isLinear)
{
var position = editablePath.GetPoint(index).position;
var nextPosition = NextControlPoint(index).position;
return (1f / 3f) * (nextPosition - position) + position;
}
return editablePath.GetPoint(index).rightTangent;
}
private int NextIndex(int index)
{
return EditablePathUtility.Mod(index + 1, editablePath.pointCount);
}
private ControlPoint NextControlPoint(int index)
{
return editablePath.GetPoint(NextIndex(index));
}
private int PrevIndex(int index)
{
return EditablePathUtility.Mod(index - 1, editablePath.pointCount);
}
private ControlPoint PrevControlPoint(int index)
{
return editablePath.GetPoint(PrevIndex(index));
}
}
}

View File

@@ -0,0 +1,275 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public static class EditablePathExtensions
{
public static Polygon ToPolygon(this IEditablePath path)
{
var polygon = new Polygon()
{
isOpenEnded = path.isOpenEnded,
points = new Vector3[path.pointCount]
};
for (var i = 0; i < path.pointCount; ++i)
polygon.points[i] = path.GetPoint(i).position;
return polygon;
}
public static Spline ToSpline(this IEditablePath path)
{
var count = path.pointCount * 3;
if (path.isOpenEnded)
count -= 2;
var spline = new Spline()
{
isOpenEnded = path.isOpenEnded,
points = new Vector3[count]
};
for (var i = 0; i < path.pointCount; ++i)
{
var point = path.GetPoint(i);
spline.points[i*3] = point.position;
if (i * 3 + 1 < count)
{
var nextIndex = EditablePathUtility.Mod(i+1, path.pointCount);
spline.points[i*3 + 1] = path.CalculateRightTangent(i);
spline.points[i*3 + 2] = path.CalculateLeftTangent(nextIndex);
}
}
return spline;
}
public static Vector3 CalculateLocalLeftTangent(this IEditablePath path, int index)
{
return path.CalculateLeftTangent(index) - path.GetPoint(index).position;
}
public static Vector3 CalculateLeftTangent(this IEditablePath path, int index)
{
var point = path.GetPoint(index);
var isTangentLinear = point.localLeftTangent == Vector3.zero;
var isEndpoint = path.isOpenEnded && index == 0;
var tangent = point.leftTangent;
if (isEndpoint)
return point.position;
if (isTangentLinear)
{
var prevPoint = path.GetPrevPoint(index);
var v = prevPoint.position - point.position;
tangent = point.position + v.normalized * (v.magnitude / 3f);
}
return tangent;
}
public static Vector3 CalculateLocalRightTangent(this IEditablePath path, int index)
{
return path.CalculateRightTangent(index) - path.GetPoint(index).position;
}
public static Vector3 CalculateRightTangent(this IEditablePath path, int index)
{
var point = path.GetPoint(index);
var isTangentLinear = point.localRightTangent == Vector3.zero;
var isEndpoint = path.isOpenEnded && index == path.pointCount - 1;
var tangent = point.rightTangent;
if (isEndpoint)
return point.position;
if (isTangentLinear)
{
var nextPoint = path.GetNextPoint(index);
var v = nextPoint.position - point.position;
tangent = point.position + v.normalized * (v.magnitude / 3f);
}
return tangent;
}
public static ControlPoint GetPrevPoint(this IEditablePath path, int index)
{
return path.GetPoint(EditablePathUtility.Mod(index - 1, path.pointCount));
}
public static ControlPoint GetNextPoint(this IEditablePath path, int index)
{
return path.GetPoint(EditablePathUtility.Mod(index + 1, path.pointCount));
}
public static void UpdateTangentMode(this IEditablePath path, int index)
{
var localToWorldMatrix = path.localToWorldMatrix;
path.localToWorldMatrix = Matrix4x4.identity;
var controlPoint = path.GetPoint(index);
var isLeftTangentLinear = controlPoint.localLeftTangent == Vector3.zero;
var isRightTangentLinear = controlPoint.localRightTangent == Vector3.zero;
if (isLeftTangentLinear && isRightTangentLinear)
controlPoint.tangentMode = TangentMode.Linear;
else if (isLeftTangentLinear || isRightTangentLinear)
controlPoint.tangentMode = TangentMode.Broken;
else if (controlPoint.tangentMode != TangentMode.Continuous)
controlPoint.tangentMode = TangentMode.Broken;
controlPoint.StoreTangents();
path.SetPoint(index, controlPoint);
path.localToWorldMatrix = localToWorldMatrix;
}
public static void UpdateTangentsFromMode(this IEditablePath path)
{
const float kEpsilon = 0.001f;
var localToWorldMatrix = path.localToWorldMatrix;
path.localToWorldMatrix = Matrix4x4.identity;
for (var i = 0; i < path.pointCount; ++i)
{
var controlPoint = path.GetPoint(i);
if (controlPoint.tangentMode == TangentMode.Linear)
{
controlPoint.localLeftTangent = Vector3.zero;
controlPoint.localRightTangent = Vector3.zero;
}
else if (controlPoint.tangentMode == TangentMode.Broken)
{
var isLeftEndpoint = path.isOpenEnded && i == 0;
var prevPoint = path.GetPrevPoint(i);
var nextPoint = path.GetNextPoint(i);
var liniarLeftPosition = (prevPoint.position - controlPoint.position) / 3f;
var isLeftTangentLinear = isLeftEndpoint || (controlPoint.localLeftTangent - liniarLeftPosition).sqrMagnitude < kEpsilon;
if (isLeftTangentLinear)
controlPoint.localLeftTangent = Vector3.zero;
var isRightEndpoint = path.isOpenEnded && i == path.pointCount-1;
var liniarRightPosition = (nextPoint.position - controlPoint.position) / 3f;
var isRightTangentLinear = isRightEndpoint || (controlPoint.localRightTangent - liniarRightPosition).sqrMagnitude < kEpsilon;
if (isRightTangentLinear)
controlPoint.localRightTangent = Vector3.zero;
if (isLeftTangentLinear && isRightTangentLinear)
controlPoint.tangentMode = TangentMode.Linear;
}
else if (controlPoint.tangentMode == TangentMode.Continuous)
{
//TODO: ensure tangent continuity
}
controlPoint.StoreTangents();
path.SetPoint(i, controlPoint);
}
path.localToWorldMatrix = localToWorldMatrix;
}
public static void SetTangentMode(this IEditablePath path, int index, TangentMode tangentMode)
{
var localToWorldMatrix = path.localToWorldMatrix;
path.localToWorldMatrix = Matrix4x4.identity;
var controlPoint = path.GetPoint(index);
var isEndpoint = path.isOpenEnded && (index == 0 || index == path.pointCount - 1);
var oldTangentMode = controlPoint.tangentMode;
controlPoint.tangentMode = tangentMode;
controlPoint.RestoreTangents();
if (tangentMode == TangentMode.Linear)
{
controlPoint.localLeftTangent = Vector3.zero;
controlPoint.localRightTangent = Vector3.zero;
}
else if (tangentMode == TangentMode.Continuous && !isEndpoint)
{
var isLeftLinear = controlPoint.localLeftTangent == Vector3.zero;
var isRightLinear = controlPoint.localRightTangent == Vector3.zero;
var tangentDotProduct = Vector3.Dot(controlPoint.localLeftTangent.normalized, controlPoint.localRightTangent.normalized);
var isContinous = tangentDotProduct < 0f && (tangentDotProduct + 1) < 0.001f;
var isLinear = isLeftLinear && isRightLinear;
if ((isLinear || oldTangentMode == TangentMode.Broken) && !isContinous)
{
var prevPoint = path.GetPrevPoint(index);
var nextPoint = path.GetNextPoint(index);
var vLeft = prevPoint.position - controlPoint.position;
var vRight = nextPoint.position - controlPoint.position;
var rightDirection = Vector3.Cross(Vector3.Cross(vLeft, vRight), vLeft.normalized + vRight.normalized).normalized;
var scale = 1f / 3f;
if (isLeftLinear)
controlPoint.localLeftTangent = vLeft.magnitude * scale * -rightDirection;
else
controlPoint.localLeftTangent = controlPoint.localLeftTangent.magnitude * -rightDirection;
if (isRightLinear)
controlPoint.localRightTangent = vRight.magnitude * scale * rightDirection;
else
controlPoint.localRightTangent = controlPoint.localRightTangent.magnitude * rightDirection;
}
}
else
{
var isLeftLinear = controlPoint.localLeftTangent == Vector3.zero;
var isRightLinear = controlPoint.localRightTangent == Vector3.zero;
if (isLeftLinear || isRightLinear)
{
if (isLeftLinear)
controlPoint.localLeftTangent = path.CalculateLocalLeftTangent(index);
if (isRightLinear)
controlPoint.localRightTangent = path.CalculateLocalRightTangent(index);
}
}
controlPoint.StoreTangents();
path.SetPoint(index, controlPoint);
path.localToWorldMatrix = localToWorldMatrix;
}
public static void MirrorTangent(this IEditablePath path, int index)
{
var localToWorldMatrix = path.localToWorldMatrix;
path.localToWorldMatrix = Matrix4x4.identity;
var controlPoint = path.GetPoint(index);
if (controlPoint.tangentMode == TangentMode.Linear)
return;
if (!Mathf.Approximately((controlPoint.localLeftTangent + controlPoint.localRightTangent).sqrMagnitude, 0f))
{
if (controlPoint.mirrorLeft)
controlPoint.localLeftTangent = -controlPoint.localRightTangent;
else
controlPoint.localRightTangent = -controlPoint.localLeftTangent;
controlPoint.StoreTangents();
path.SetPoint(index, controlPoint);
}
path.localToWorldMatrix = localToWorldMatrix;
}
}
}

View File

@@ -0,0 +1,14 @@
using UnityEngine;
using UnityEditor;
namespace UnityEditor.U2D.Path
{
public class EditablePathUtility
{
public static int Mod(int x, int m)
{
int r = x % m;
return r < 0 ? r + m : r;
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public interface IEditablePath : ISelectable<Vector3>
{
ShapeType shapeType { get; set; }
IUndoObject undoObject { get; set; }
ISelection<int> selection { get; }
Matrix4x4 localToWorldMatrix { get; set; }
Vector3 forward { get; set; }
Vector3 up { get; set; }
Vector3 right { get; set; }
bool isOpenEnded { get; set; }
int pointCount { get; }
ControlPoint GetPoint(int index);
void SetPoint(int index, ControlPoint controlPoint);
void AddPoint(ControlPoint controlPoint);
void InsertPoint(int index, ControlPoint controlPoint);
void RemovePoint(int index);
void Clear();
}
}

View File

@@ -0,0 +1,24 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public interface IEditablePathController
{
IEditablePath editablePath { get; set; }
IEditablePath closestEditablePath { get; }
ISnapping<Vector3> snapping { get; set; }
bool enableSnapping { get; set; }
void RegisterUndo(string name);
void ClearSelection();
void SelectPoint(int index, bool select);
void CreatePoint(int index, Vector3 position);
void RemoveSelectedPoints();
void MoveSelectedPoints(Vector3 delta);
void MoveEdge(int index, Vector3 delta);
void SetLeftTangent(int index, Vector3 position, bool setToLinear, bool mirror, Vector3 cachedRightTangent, TangentMode cachedTangentMode);
void SetRightTangent(int index, Vector3 position, bool setToLinear, bool mirror, Vector3 cachedLeftTangent, TangentMode cachedTangentMode);
void ClearClosestPath();
void AddClosestPath(float distance);
}
}

View File

@@ -0,0 +1,10 @@
using UnityEngine;
using UnityEditor;
namespace UnityEditor.U2D.Path
{
public interface ISnapping<T>
{
T Snap(T value);
}
}

View File

@@ -0,0 +1,9 @@
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public interface IUndoObject
{
void RegisterUndo(string name);
}
}

View File

@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public class MultipleEditablePathController : IEditablePathController
{
private IEditablePathController m_Controller = new EditablePathController();
private List<IEditablePath> m_Paths = new List<IEditablePath>();
private float m_ClosestDistance = float.MaxValue;
private IEditablePath m_ClosestPath;
public IEditablePath editablePath
{
get { return m_Controller.editablePath; }
set { m_Controller.editablePath = value; }
}
public IEditablePath closestEditablePath { get; private set; }
public ISnapping<Vector3> snapping
{
get { return m_Controller.snapping; }
set { m_Controller.snapping = value; }
}
public bool enableSnapping
{
get { return m_Controller.enableSnapping; }
set { m_Controller.enableSnapping = value; }
}
public void ClearPaths()
{
m_Paths.Clear();
}
public void AddPath(IEditablePath path)
{
if (!m_Paths.Contains(path))
m_Paths.Add(path);
}
public void RemovePath(IEditablePath path)
{
m_Paths.Remove(path);
}
public void RegisterUndo(string name)
{
var current = editablePath;
ForEach((s) =>
{
editablePath = s;
m_Controller.RegisterUndo(name);
});
editablePath = current;
}
public void ClearSelection()
{
var current = editablePath;
ForEach((s) =>
{
editablePath = s;
m_Controller.ClearSelection();
});
editablePath = current;
}
public void SelectPoint(int index, bool select)
{
m_Controller.SelectPoint(index, select);
}
public void CreatePoint(int index, Vector3 position)
{
m_Controller.CreatePoint(index, position);
}
public void RemoveSelectedPoints()
{
var current = editablePath;
ForEach((s) =>
{
editablePath = s;
m_Controller.RemoveSelectedPoints();
});
editablePath = current;
}
public void MoveSelectedPoints(Vector3 delta)
{
var current = editablePath;
ForEach((s) =>
{
editablePath = s;
m_Controller.MoveSelectedPoints(delta);
});
editablePath = current;
}
public void MoveEdge(int index, Vector3 delta)
{
m_Controller.MoveEdge(index, delta);
}
public void SetLeftTangent(int index, Vector3 position, bool setToLinear, bool mirror, Vector3 cachedRightTangent, TangentMode cachedTangentMode)
{
m_Controller.SetLeftTangent(index, position, setToLinear, mirror, cachedRightTangent, cachedTangentMode);
}
public void SetRightTangent(int index, Vector3 position, bool setToLinear, bool mirror, Vector3 cachedLeftTangent, TangentMode cachedTangentMode)
{
m_Controller.SetRightTangent(index, position, setToLinear, mirror, cachedLeftTangent, cachedTangentMode);
}
public void ClearClosestPath()
{
m_ClosestDistance = float.MaxValue;
closestEditablePath = null;
}
public void AddClosestPath(float distance)
{
if (distance <= m_ClosestDistance)
{
m_ClosestDistance = distance;
closestEditablePath = editablePath;
}
}
private void ForEach(Action<IEditablePath> action)
{
foreach(var path in m_Paths)
{
if (path == null)
continue;
action(path);
}
}
}
}

View File

@@ -0,0 +1,21 @@
using UnityEngine;
using UnityEditor;
namespace UnityEditor.U2D.Path
{
public class Snapping : ISnapping<Vector3>
{
public Vector3 Snap(Vector3 position)
{
return new Vector3(
Snap(position.x, EditorSnapSettings.move.x),
Snap(position.y, EditorSnapSettings.move.y),
position.z);
}
private float Snap(float value, float snap)
{
return Mathf.Round(value / snap) * snap;
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public class GenericScriptablePath<T> : ScriptablePath
{
[SerializeField]
private List<T> m_Data = new List<T>();
public T[] data
{
get { return m_Data.ToArray(); }
set
{
if (value.Length != pointCount)
throw new Exception("Custom data count does not match control point count");
m_Data.Clear();
m_Data.AddRange(value);
}
}
public override void Clear()
{
base.Clear();
m_Data.Clear();
}
public override void AddPoint(ControlPoint controlPoint)
{
base.AddPoint(controlPoint);
m_Data.Add(Create());
}
public override void InsertPoint(int index, ControlPoint controlPoint)
{
base.InsertPoint(index, controlPoint);
m_Data.Insert(index, Create());
}
public override void RemovePoint(int index)
{
base.RemovePoint(index);
Destroy(m_Data[index]);
m_Data.RemoveAt(index);
}
public T GetData(int index)
{
return m_Data[index];
}
public void SetData(int index, T data)
{
m_Data[index] = data;
}
protected virtual T Create()
{
return Activator.CreateInstance<T>();
}
protected virtual void Destroy(T data) { }
}
}

View File

@@ -0,0 +1,120 @@
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
namespace UnityEditor.U2D.Path
{
public class GenericScriptablePathInspector<U,T> : ScriptablePathInspector where U : ScriptableData<T>
{
private List<U> m_DataObjects = new List<U>();
private List<U> m_SelectedDataObjects = new List<U>();
private Editor m_CachedEditor = null;
private void OnEnable()
{
PrepareDataObjects();
}
private void OnDestroy()
{
DestroyDataObjects();
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
DoCustomDataInspector();
}
protected void DoCustomDataInspector()
{
PrepareDataObjects();
if (m_SelectedDataObjects.Count > 0)
{
CreateCachedEditor(m_SelectedDataObjects.ToArray(), null, ref m_CachedEditor);
EditorGUI.BeginChangeCheck();
m_CachedEditor.OnInspectorGUI();
if (EditorGUI.EndChangeCheck())
SetDataObjects();
}
}
private void PrepareDataObjects()
{
var elementCount = 0;
m_SelectedDataObjects.Clear();
foreach(var path in paths)
elementCount += path.pointCount;
while (m_DataObjects.Count < elementCount)
CreateDataObject();
var index = 0;
foreach(var path in paths)
{
var genericPath = path as GenericScriptablePath<T>;
var customDataArray = genericPath.data;
var length = customDataArray.Length;
for (var i = 0; i < length; ++i)
{
var dataObject = m_DataObjects[index + i];
dataObject.data = customDataArray[i];
if (path.selection.Contains(i))
{
dataObject.owner = path.owner;
dataObject.index = i;
m_SelectedDataObjects.Add(dataObject);
}
}
index += length;
}
}
private void SetDataObjects()
{
var index = 0;
foreach(var path in paths)
{
var genericPath = path as GenericScriptablePath<T>;
var customDataArray = genericPath.data;
var length = customDataArray.Length;
for (var i = 0; i < length; ++i)
customDataArray[i] = m_DataObjects[index + i].data;
genericPath.data = customDataArray;
index += length;
}
}
private U CreateDataObject()
{
var dataObject = ScriptableObject.CreateInstance<U>();
m_DataObjects.Add(dataObject);
return dataObject;
}
private void DestroyDataObjects()
{
foreach (var customDataObject in m_DataObjects)
DestroyImmediate(customDataObject);
m_DataObjects.Clear();
m_SelectedDataObjects.Clear();
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
namespace UnityEditor.U2D.Path
{
public abstract class PathComponentEditor<T> : Editor where T : ScriptablePath
{
private static class Contents
{
public static readonly GUIContent snappingLabel = new GUIContent("Snapping", "Snap points using the snap settings");
}
private Editor m_CachedEditor = null;
// Returns true on Changed.
protected bool DoEditButton<U>(GUIContent icon, string label) where U : PathEditorTool<T>
{
const float kButtonWidth = 33;
const float kButtonHeight = 23;
const float k_SpaceBetweenLabelAndButton = 5;
var buttonStyle = new GUIStyle("EditModeSingleButton");
var rect = EditorGUILayout.GetControlRect(true, kButtonHeight, buttonStyle);
var buttonRect = new Rect(rect.xMin + EditorGUIUtility.labelWidth, rect.yMin, kButtonWidth, kButtonHeight);
var labelContent = new GUIContent(label);
var labelSize = GUI.skin.label.CalcSize(labelContent);
var labelRect = new Rect(
buttonRect.xMax + k_SpaceBetweenLabelAndButton,
rect.yMin + (rect.height - labelSize.y) * .5f,
labelSize.x,
rect.height);
bool hasChanged = false;
using (new EditorGUI.DisabledGroupScope(!EditorToolManager.IsAvailable<U>()))
{
using (var check = new EditorGUI.ChangeCheckScope())
{
var isActive = GUI.Toggle(buttonRect, EditorToolManager.IsActiveTool<U>(), icon, buttonStyle);
GUI.Label(labelRect, label);
if (check.changed)
{
if (isActive)
ToolManager.SetActiveTool<U>();
else
ToolManager.RestorePreviousTool();
hasChanged = true;
}
}
}
return hasChanged;
}
protected void DoPathInspector<U>() where U : PathEditorTool<T>
{
if (EditorToolManager.IsActiveTool<U>() && EditorToolManager.IsAvailable<U>())
{
var paths = EditorToolManager.GetEditorTool<U>().paths;
CreateCachedEditor(paths, null, ref m_CachedEditor);
if (m_CachedEditor == null) //Needed to avoid a nullref on exiting playmode
return;
using (var check = new EditorGUI.ChangeCheckScope())
{
m_CachedEditor.OnInspectorGUI();
if (check.changed)
EditorToolManager.GetEditorTool<U>().SetShapes();
}
}
}
protected void DoSnappingInspector<U>() where U : PathEditorTool<T>
{
if (EditorToolManager.IsActiveTool<U>() && EditorToolManager.IsAvailable<U>())
{
var tool = EditorToolManager.GetEditorTool<U>();
tool.enableSnapping = EditorGUILayout.Toggle(Contents.snappingLabel, tool.enableSnapping);
}
}
protected void DoOpenEndedInspector<U>(SerializedProperty isOpenEndedProperty) where U : PathEditorTool<T>
{
serializedObject.Update();
using (var check = new EditorGUI.ChangeCheckScope())
{
EditorGUILayout.PropertyField(isOpenEndedProperty);
if (check.changed)
{
if (EditorToolManager.IsActiveTool<U>() && EditorToolManager.IsAvailable<U>())
{
var paths = EditorToolManager.GetEditorTool<U>().paths;
foreach (var path in paths)
{
path.undoObject.RegisterUndo("Set Open Ended");
path.isOpenEnded = isOpenEndedProperty.boolValue;
}
}
}
}
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@@ -0,0 +1,494 @@
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEditor.U2D.Path.GUIFramework;
using UnityObject = UnityEngine.Object;
namespace UnityEditor.U2D.Path
{
public static class PathEditorToolContents
{
internal static readonly GUIContent shapeToolIcon = IconContent("ShapeTool", "Start editing the Shape in the Scene View.");
internal static readonly GUIContent shapeToolPro = IconContent("ShapeToolPro", "Start editing the Shape in the Scene View.");
internal static GUIContent IconContent(string name, string tooltip = null)
{
return new GUIContent(AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/" + name + ".png"), tooltip);
}
public static GUIContent icon
{
get
{
if (EditorGUIUtility.isProSkin)
return shapeToolPro;
return shapeToolIcon;
}
}
}
internal interface IDuringSceneGuiTool
{
void DuringSceneGui(SceneView sceneView);
bool IsAvailable();
}
[InitializeOnLoad]
internal class EditorToolManager
{
private static List<IDuringSceneGuiTool> m_Tools = new List<IDuringSceneGuiTool>();
static EditorToolManager()
{
SceneView.duringSceneGui += DuringSceneGui;
}
internal static void Add(IDuringSceneGuiTool tool)
{
if (!m_Tools.Contains(tool) && tool is EditorTool)
m_Tools.Add(tool);
}
internal static void Remove(IDuringSceneGuiTool tool)
{
if (m_Tools.Contains(tool))
m_Tools.Remove(tool);
}
internal static bool IsActiveTool<T>() where T : EditorTool
{
return ToolManager.activeToolType.Equals(typeof(T));
}
internal static bool IsAvailable<T>() where T : EditorTool
{
var tool = GetEditorTool<T>();
if (tool != null)
return tool.IsAvailable();
return false;
}
internal static T GetEditorTool<T>() where T : EditorTool
{
foreach(var tool in m_Tools)
{
if (tool.GetType().Equals(typeof(T)))
return tool as T;
}
return null;
}
private static void DuringSceneGui(SceneView sceneView)
{
foreach (var tool in m_Tools)
{
if (tool.IsAvailable() && ToolManager.IsActiveTool(tool as EditorTool))
tool.DuringSceneGui(sceneView);
}
}
}
public abstract class PathEditorTool<T> : EditorTool, IDuringSceneGuiTool where T : ScriptablePath
{
private Dictionary<UnityObject, T> m_Paths = new Dictionary<UnityObject, T>();
private IGUIState m_GUIState = new GUIState();
private Dictionary<UnityObject, PathEditor> m_PathEditors = new Dictionary<UnityObject, PathEditor>();
private Dictionary<UnityObject, SerializedObject> m_SerializedObjects = new Dictionary<UnityObject, SerializedObject>();
private MultipleEditablePathController m_Controller = new MultipleEditablePathController();
private PointRectSelector m_RectSelector = new PointRectSelector();
private bool m_IsActive = false;
internal T[] paths
{
get { return m_Paths.Values.ToArray(); }
}
public bool enableSnapping
{
get { return m_Controller.enableSnapping; }
set { m_Controller.enableSnapping = value; }
}
public override GUIContent toolbarIcon
{
get { return PathEditorToolContents.icon; }
}
public override bool IsAvailable()
{
return targets.Count() > 0;
}
public T GetPath(UnityObject targetObject)
{
var path = default(T);
m_Paths.TryGetValue(targetObject, out path);
return path;
}
public void SetPath(UnityObject target)
{
var path = GetPath(target);
path.localToWorldMatrix = Matrix4x4.identity;
var undoName = Undo.GetCurrentGroupName();
var serializedObject = GetSerializedObject(target);
serializedObject.UpdateIfRequiredOrScript();
SetShape(path, serializedObject);
Undo.SetCurrentGroupName(undoName);
}
private void RepaintInspectors()
{
var editorWindows = Resources.FindObjectsOfTypeAll<EditorWindow>();
foreach (var editorWindow in editorWindows)
{
if (editorWindow.titleContent.text == "Inspector")
editorWindow.Repaint();
}
}
private void OnEnable()
{
m_IsActive = false;
EditorToolManager.Add(this);
SetupRectSelector();
HandleActivation();
ToolManager.activeToolChanged += HandleActivation;
}
private void OnDestroy()
{
EditorToolManager.Remove(this);
ToolManager.activeToolChanged -= HandleActivation;
UnregisterCallbacks();
}
private void HandleActivation()
{
if (m_IsActive == false && ToolManager.IsActiveTool(this))
Activate();
else if (m_IsActive)
Deactivate();
}
private void Activate()
{
m_IsActive = true;
RegisterCallbacks();
InitializeCache();
OnActivate();
}
private void Deactivate()
{
OnDeactivate();
DestroyCache();
UnregisterCallbacks();
m_IsActive = false;
}
private void RegisterCallbacks()
{
UnregisterCallbacks();
Selection.selectionChanged += SelectionChanged;
EditorApplication.playModeStateChanged += PlayModeStateChanged;
Undo.undoRedoPerformed += UndoRedoPerformed;
}
private void UnregisterCallbacks()
{
Selection.selectionChanged -= SelectionChanged;
EditorApplication.playModeStateChanged -= PlayModeStateChanged;
Undo.undoRedoPerformed -= UndoRedoPerformed;
}
private void DestroyCache()
{
foreach (var pair in m_Paths)
{
var path = pair.Value;
if (path != null)
{
Undo.ClearUndo(path);
UnityObject.DestroyImmediate(path);
}
}
m_Paths.Clear();
m_Controller.ClearPaths();
m_PathEditors.Clear();
m_SerializedObjects.Clear();
}
private void UndoRedoPerformed()
{
ForEachTarget((target) =>
{
var path = GetPath(target);
if (!path.modified)
InitializePath(target);
});
}
private void SelectionChanged()
{
InitializeCache();
}
private void PlayModeStateChanged(PlayModeStateChange stateChange)
{
if (stateChange == PlayModeStateChange.EnteredEditMode)
EditorApplication.delayCall += () => { InitializeCache(); }; //HACK: At this point target is null. Let's wait to next frame to refresh.
}
private void SetupRectSelector()
{
m_RectSelector.onSelectionBegin = BeginSelection;
m_RectSelector.onSelectionChanged = UpdateSelection;
m_RectSelector.onSelectionEnd = EndSelection;
}
private void ForEachTarget(Action<UnityObject> action)
{
foreach(var target in targets)
{
if (target == null)
continue;
action(target);
}
}
private void InitializeCache()
{
m_Controller.ClearPaths();
ForEachTarget((target) =>
{
var path = GetOrCreatePath(target);
var pointCount = path.pointCount;
InitializePath(target);
if (pointCount != path.pointCount)
path.selection.Clear();
CreatePathEditor(target);
m_Controller.AddPath(path);
});
}
private void InitializePath(UnityObject target)
{
IShape shape = null;
ControlPoint[] controlPoints = null;
try
{
shape = GetShape(target);
controlPoints = shape.ToControlPoints();
}
catch (Exception e)
{
Debug.LogError(e.Message);
}
var path = GetPath(target);
path.Clear();
if (shape != null && controlPoints != null)
{
path.localToWorldMatrix = Matrix4x4.identity;
path.shapeType = shape.type;
path.isOpenEnded = shape.isOpenEnded;
foreach (var controlPoint in controlPoints)
path.AddPoint(controlPoint);
}
Initialize(path, GetSerializedObject(target));
}
private T GetOrCreatePath(UnityObject targetObject)
{
var path = GetPath(targetObject);
if (path == null)
{
path = ScriptableObject.CreateInstance<T>();
path.owner = targetObject;
m_Paths[targetObject] = path;
}
return path;
}
private PathEditor GetPathEditor(UnityObject target)
{
PathEditor pathEditor;
m_PathEditors.TryGetValue(target, out pathEditor);
return pathEditor;
}
private void CreatePathEditor(UnityObject target)
{
var pathEditor = new PathEditor();
pathEditor.controller = m_Controller;
pathEditor.drawerOverride = GetCustomDrawer(target);
m_PathEditors[target] = pathEditor;
}
private SerializedObject GetSerializedObject(UnityObject target)
{
var serializedObject = default(SerializedObject);
if (!m_SerializedObjects.TryGetValue(target, out serializedObject))
{
serializedObject = new SerializedObject(target);
m_SerializedObjects[target] = serializedObject;
}
return serializedObject;
}
void IDuringSceneGuiTool.DuringSceneGui(SceneView sceneView)
{
if (m_GUIState.eventType == EventType.Layout)
m_Controller.ClearClosestPath();
m_RectSelector.OnGUI();
bool changed = false;
ForEachTarget((target) =>
{
var path = GetPath(target);
if (path != null)
{
path.localToWorldMatrix = GetLocalToWorldMatrix(target);
path.forward = GetForward(target);
path.up = GetUp(target);
path.right = GetRight(target);
m_Controller.editablePath = path;
using (var check = new EditorGUI.ChangeCheckScope())
{
var pathEditor = GetPathEditor(target);
pathEditor.linearTangentIsZero = GetLinearTangentIsZero(target);
pathEditor.OnGUI();
OnCustomGUI(path);
changed |= check.changed;
}
}
});
if (changed)
{
SetShapes();
RepaintInspectors();
}
}
private void BeginSelection(ISelector<Vector3> selector, bool isAdditive)
{
m_Controller.RegisterUndo("Selection");
if (isAdditive)
{
ForEachTarget((target) =>
{
var path = GetPath(target);
path.selection.BeginSelection();
});
}
else
{
UpdateSelection(selector);
}
}
private void UpdateSelection(ISelector<Vector3> selector)
{
var repaintInspectors = false;
ForEachTarget((target) =>
{
var path = GetPath(target);
repaintInspectors |= path.Select(selector);
});
if (repaintInspectors)
RepaintInspectors();
}
private void EndSelection(ISelector<Vector3> selector)
{
ForEachTarget((target) =>
{
var path = GetPath(target);
path.selection.EndSelection(true);
});
}
internal void SetShapes()
{
ForEachTarget((target) =>
{
SetPath(target);
});
}
private Transform GetTransform(UnityObject target)
{
return (target as Component).transform;
}
private Matrix4x4 GetLocalToWorldMatrix(UnityObject target)
{
return GetTransform(target).localToWorldMatrix;
}
private Vector3 GetForward(UnityObject target)
{
return GetTransform(target).forward;
}
private Vector3 GetUp(UnityObject target)
{
return GetTransform(target).up;
}
private Vector3 GetRight(UnityObject target)
{
return GetTransform(target).right;
}
protected abstract IShape GetShape(UnityObject target);
protected virtual void Initialize(T path, SerializedObject serializedObject) { }
protected abstract void SetShape(T path, SerializedObject serializedObject);
protected virtual void OnActivate() { }
protected virtual void OnDeactivate() { }
protected virtual void OnCustomGUI(T path) { }
protected virtual bool GetLinearTangentIsZero(UnityObject target) { return false; }
protected virtual IDrawer GetCustomDrawer(UnityObject target) { return null; }
}
}

View File

@@ -0,0 +1,108 @@
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEditor.U2D.Path.GUIFramework;
using UnityObject = UnityEngine.Object;
namespace UnityEditor.U2D.Path
{
public static class PathEditorToolExtensions
{
public static void CycleTangentMode<T>(this PathEditorTool<T> pathEditorTool) where T : ScriptablePath
{
var first = true;
var mixed = false;
var tangentMode = TangentMode.Linear;
var targets = pathEditorTool.targets;
foreach(var target in targets)
{
var path = pathEditorTool.GetPath(target);
if (path.selection.Count == 0)
continue;
for (var i = 0; i < path.pointCount; ++i)
{
if (!path.selection.Contains(i))
continue;
var point = path.GetPoint(i);
if (first)
{
first = false;
tangentMode = point.tangentMode;
}
else if (point.tangentMode != tangentMode)
{
mixed = true;
break;
}
}
if (mixed)
break;
}
if (mixed)
tangentMode = TangentMode.Linear;
else
tangentMode = GetNextTangentMode(tangentMode);
foreach(var target in targets)
{
var path = pathEditorTool.GetPath(target);
if (path.selection.Count == 0)
continue;
path.undoObject.RegisterUndo("Cycle Tangent Mode");
for (var i = 0; i < path.pointCount; ++i)
{
if (!path.selection.Contains(i))
continue;
path.SetTangentMode(i, tangentMode);
}
pathEditorTool.SetPath(target);
}
}
public static void MirrorTangent<T>(this PathEditorTool<T> pathEditorTool) where T : ScriptablePath
{
var targets = pathEditorTool.targets;
foreach(var target in targets)
{
var path = pathEditorTool.GetPath(target);
if (path.selection.Count == 0)
continue;
path.undoObject.RegisterUndo("Mirror Tangents");
for (var i = 0; i < path.pointCount; ++i)
{
if (!path.selection.Contains(i))
continue;
path.MirrorTangent(i);
}
pathEditorTool.SetPath(target);
}
}
private static TangentMode GetNextTangentMode(TangentMode tangentMode)
{
return (TangentMode)((((int)tangentMode) + 1) % Enum.GetValues(typeof(TangentMode)).Length);
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityObject = UnityEngine.Object;
namespace UnityEditor.U2D.Path
{
public class ScriptableData<T> : ScriptableObject
{
[SerializeField]
private T m_Data;
public UnityObject owner { get; set; }
public int index { get; set; }
public T data
{
get { return m_Data; }
set { m_Data = value; }
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityObject = UnityEngine.Object;
namespace UnityEditor.U2D.Path
{
public class ScriptablePath : ScriptableObject, IEditablePath, IUndoObject
{
[SerializeField]
private EditablePath m_EditablePath = new EditablePath();
[SerializeField]
private bool m_Modified = false;
internal bool modified
{
get { return m_Modified; }
}
internal UnityObject owner { get; set; }
public ShapeType shapeType
{
get { return m_EditablePath.shapeType; }
set { m_EditablePath.shapeType = value; }
}
public IUndoObject undoObject
{
get { return this; }
set { }
}
public ISelection<int> selection
{
get { return m_EditablePath.selection; }
}
public Matrix4x4 localToWorldMatrix
{
get { return m_EditablePath.localToWorldMatrix; }
set { m_EditablePath.localToWorldMatrix = value; }
}
public Vector3 forward
{
get { return m_EditablePath.forward; }
set { m_EditablePath.forward = value; }
}
public Vector3 up
{
get { return m_EditablePath.up; }
set { m_EditablePath.up = value; }
}
public Vector3 right
{
get { return m_EditablePath.right; }
set { m_EditablePath.right = value; }
}
public bool isOpenEnded
{
get { return m_EditablePath.isOpenEnded; }
set { m_EditablePath.isOpenEnded = value; }
}
public int pointCount
{
get { return m_EditablePath.pointCount; }
}
public bool Select(ISelector<Vector3> selector)
{
return m_EditablePath.Select(selector);
}
public virtual void Clear()
{
m_EditablePath.Clear();
}
public virtual ControlPoint GetPoint(int index)
{
return m_EditablePath.GetPoint(index);
}
public virtual void SetPoint(int index, ControlPoint controlPoint)
{
m_EditablePath.SetPoint(index, controlPoint);
}
public virtual void AddPoint(ControlPoint controlPoint)
{
m_EditablePath.AddPoint(controlPoint);
}
public virtual void InsertPoint(int index, ControlPoint controlPoint)
{
m_EditablePath.InsertPoint(index, controlPoint);
}
public virtual void RemovePoint(int index)
{
m_EditablePath.RemovePoint(index);
}
void IUndoObject.RegisterUndo(string name)
{
Undo.RegisterCompleteObjectUndo(this, name);
m_Modified = true;
}
}
}

View File

@@ -0,0 +1,241 @@
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
namespace UnityEditor.U2D.Path
{
[CanEditMultipleObjects]
[CustomEditor(typeof(ScriptablePath), true)]
public class ScriptablePathInspector : Editor
{
private static class Contents
{
public static readonly GUIContent linearIcon = IconContent("TangentLinear", "TangentLinearPro", "Linear");
public static readonly GUIContent continuousIcon = IconContent("TangentContinuous", "TangentContinuousPro", "Continuous");
public static readonly GUIContent brokenIcon = IconContent("TangentBroken", "TangentBrokenPro", "Broken");
public static readonly GUIContent positionLabel = new GUIContent("Position", "Position of the Control Point");
public static readonly GUIContent enableSnapLabel = new GUIContent("Snapping", "Snap points using the snap settings");
public static readonly GUIContent tangentModeLabel = new GUIContent("Tangent Mode");
public static readonly GUIContent pointLabel = new GUIContent("Point");
private static GUIContent IconContent(string name, string tooltip = null)
{
return new GUIContent(AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/" + name + ".png"), tooltip);
}
private static GUIContent IconContent(string personal, string pro, string tooltip)
{
if (EditorGUIUtility.isProSkin)
return IconContent(pro, tooltip);
return IconContent(personal, tooltip);
}
}
private List<ScriptablePath> m_Paths = null;
private bool m_Dragged = false;
protected List<ScriptablePath> paths
{
get
{
if (m_Paths == null)
m_Paths = targets.Select( t => t as ScriptablePath).ToList();
return m_Paths;
}
}
public override void OnInspectorGUI()
{
DoTangentModeInspector();
DoPositionInspector();
}
protected void DoTangentModeInspector()
{
if (!IsAnyShapeType(ShapeType.Spline))
return;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel(Contents.tangentModeLabel);
using (new EditorGUI.DisabledGroupScope(!IsAnyPointSelected()))
{
if (DoToggle(GetToggleStateFromTangentMode(TangentMode.Linear), Contents.linearIcon))
SetMixedTangentMode(TangentMode.Linear);
if (DoToggle(GetToggleStateFromTangentMode(TangentMode.Continuous), Contents.continuousIcon))
SetMixedTangentMode(TangentMode.Continuous);
if (DoToggle(GetToggleStateFromTangentMode(TangentMode.Broken), Contents.brokenIcon))
SetMixedTangentMode(TangentMode.Broken);
}
EditorGUILayout.EndHorizontal();
}
protected void DoPositionInspector()
{
var showMixedValue = EditorGUI.showMixedValue;
var wideMode = EditorGUIUtility.wideMode;
var position = Vector3.zero;
var isMixed = GetMixedPosition(out position);
EditorGUI.showMixedValue = isMixed;
EditorGUIUtility.wideMode = true;
using (new EditorGUI.DisabledGroupScope(!IsAnyPointSelected()))
{
if (GUIUtility.hotControl == 0)
m_Dragged = false;
EditorGUI.BeginChangeCheck();
var delta = EditorGUILayout.Vector2Field(Contents.positionLabel, position) - (Vector2)position;
if (EditorGUI.EndChangeCheck())
{
if (m_Dragged == false)
{
foreach(var path in paths)
path.undoObject.RegisterUndo("Point Position");
m_Dragged = true;
}
SetMixedDeltaPosition(delta);
}
}
EditorGUI.showMixedValue = showMixedValue;
EditorGUIUtility.wideMode = wideMode;
}
private bool DoToggle(bool value, GUIContent icon)
{
const float kButtonWidth = 33f;
const float kButtonHeight = 23f;
var buttonStyle = new GUIStyle("EditModeSingleButton");
var changed = false;
using (var check = new EditorGUI.ChangeCheckScope())
{
value = GUILayout.Toggle(value, icon, buttonStyle, GUILayout.Width(kButtonWidth), GUILayout.Height(kButtonHeight));
changed = check.changed;
}
return value && changed;
}
private bool GetToggleStateFromTangentMode(TangentMode mode)
{
foreach(var path in paths)
{
var selection = path.selection;
foreach (var index in selection.elements)
if (path.GetPoint(index).tangentMode != mode)
return false;
}
return true;
}
private void SetMixedTangentMode(TangentMode tangentMode)
{
foreach(var path in paths)
{
path.undoObject.RegisterUndo("Tangent Mode");
foreach (var index in path.selection.elements)
path.SetTangentMode(index, tangentMode);
}
SceneView.RepaintAll();
}
private bool GetMixedPosition(out Vector3 position)
{
var first = true;
position = Vector3.zero;
var activeObject = Selection.activeObject as GameObject;
if (Selection.count > 1 || !activeObject)
return true;
foreach(var path in paths)
{
MonoBehaviour behavior = path.owner as MonoBehaviour;
if (!behavior || activeObject != behavior.gameObject)
continue;
var selection = path.selection;
var matrix = path.localToWorldMatrix;
path.localToWorldMatrix = Matrix4x4.identity;
foreach (var index in selection.elements)
{
var controlPoint = path.GetPoint(index);
if (first)
{
position = controlPoint.position;
first = false;
}
else if (position != controlPoint.position)
{
return true;
}
}
path.localToWorldMatrix = matrix;
}
return false;
}
private void SetMixedDeltaPosition(Vector3 delta)
{
foreach(var path in paths)
{
var selection = path.selection;
var matrix = path.localToWorldMatrix;
path.localToWorldMatrix = Matrix4x4.identity;
foreach (var index in selection.elements)
{
var controlPoint = path.GetPoint(index);
controlPoint.position += delta;
path.SetPoint(index, controlPoint);
}
path.localToWorldMatrix = matrix;
}
}
private bool IsAnyShapeType(ShapeType shapeType)
{
foreach(var path in paths)
if (path.shapeType == shapeType)
return true;
return false;
}
protected bool IsAnyPointSelected()
{
foreach(var path in paths)
if (path.selection.Count > 0)
return true;
return false;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

View File

@@ -0,0 +1,48 @@
using System;
using UnityEngine;
using UnityEditor.U2D.Path.GUIFramework;
namespace UnityEditor.U2D.Path
{
public class CreatePointAction : ClickAction
{
private Control m_PointControl;
public Func<IGUIState, Vector2, Vector3> guiToWorld;
public Action<int, Vector3> onCreatePoint;
public CreatePointAction(Control pointControl, Control edgeControl) : base(edgeControl, 0, false)
{
m_PointControl = pointControl;
}
protected override void OnTrigger(IGUIState guiState)
{
base.OnTrigger(guiState);
var index = hoveredControl.layoutData.index;
var position = GetMousePositionWorld(guiState);
if (onCreatePoint != null)
onCreatePoint(index, position);
guiState.nearestControl = m_PointControl.ID;
var data = m_PointControl.layoutData;
data.index = index + 1;
data.position = position;
data.distance = 0f;
m_PointControl.layoutData = data;
guiState.changed = true;
}
private Vector3 GetMousePositionWorld(IGUIState guiState)
{
if (guiToWorld != null)
return guiToWorld(guiState, guiState.mousePosition);
return guiState.GUIToWorld(guiState.mousePosition, hoveredControl.layoutData.forward, hoveredControl.layoutData.position);
}
}
}

View File

@@ -0,0 +1,118 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.U2D.Path.GUIFramework;
namespace UnityEditor.U2D.Path
{
public class Drawer : IDrawer
{
internal class Styles
{
public readonly GUIStyle pointNormalStyle;
public readonly GUIStyle pointHoveredStyle;
public readonly GUIStyle pointSelectedStyle;
public readonly GUIStyle pointPreviewStyle;
public readonly GUIStyle tangentNormalStyle;
public readonly GUIStyle tangentHoveredStyle;
public Styles()
{
var pointNormal = AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointNormal.png");
var pointHovered = AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointHovered.png");
var pointSelected = AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointSelected.png");
var pointPreview = AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointPreview.png");
var tangentNormal = AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/tangentNormal.png");
pointNormalStyle = CreateStyle(AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointNormal.png"), Vector2.one * 12f);
pointHoveredStyle = CreateStyle(AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointHovered.png"), Vector2.one * 12f);
pointSelectedStyle = CreateStyle(AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointSelected.png"), Vector2.one * 12f);
pointPreviewStyle = CreateStyle(AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointPreview.png"), Vector2.one * 12f);
tangentNormalStyle = CreateStyle(AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/tangentNormal.png"), Vector2.one * 8f);
tangentHoveredStyle = CreateStyle(AssetDatabase.LoadAssetAtPath<Texture2D>("Packages/com.unity.2d.path/Editor/Handles/Path/pointHovered.png"), Vector2.one * 10f);
}
private GUIStyle CreateStyle(Texture2D texture, Vector2 size)
{
var guiStyle = new GUIStyle();
guiStyle.normal.background = texture;
guiStyle.fixedWidth = size.x;
guiStyle.fixedHeight = size.y;
return guiStyle;
}
}
private IGUIState m_GUIState = new GUIState();
private Styles m_Styles;
private Styles styles
{
get
{
if (m_Styles == null)
m_Styles = new Styles();
return m_Styles;
}
}
public void DrawCreatePointPreview(Vector3 position)
{
DrawGUIStyleCap(0, position, Quaternion.identity, m_GUIState.GetHandleSize(position), styles.pointPreviewStyle);
}
public void DrawPoint(Vector3 position)
{
DrawGUIStyleCap(0, position, Quaternion.identity, m_GUIState.GetHandleSize(position), styles.pointNormalStyle);
}
public void DrawPointHovered(Vector3 position)
{
DrawGUIStyleCap(0, position, Quaternion.identity, m_GUIState.GetHandleSize(position), styles.pointHoveredStyle);
}
public void DrawPointSelected(Vector3 position)
{
DrawGUIStyleCap(0, position, Quaternion.identity, m_GUIState.GetHandleSize(position), styles.pointSelectedStyle);
}
public void DrawLine(Vector3 p1, Vector3 p2, float width, Color color)
{
Handles.color = color;
Handles.DrawAAPolyLine(width, new Vector3[] { p1, p2 });
}
public void DrawBezier(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, float width, Color color)
{
Handles.color = color;
Handles.DrawBezier(p1, p4, p2, p3, color, null, width);
}
public void DrawTangent(Vector3 position, Vector3 tangent)
{
DrawLine(position, tangent, 3f, Color.yellow);
DrawGUIStyleCap(0, tangent, Quaternion.identity, m_GUIState.GetHandleSize(tangent), styles.tangentNormalStyle);
}
private void DrawGUIStyleCap(int controlID, Vector3 position, Quaternion rotation, float size, GUIStyle guiStyle)
{
if (Camera.current && Vector3.Dot(position - Camera.current.transform.position, Camera.current.transform.forward) < 0f)
return;
Handles.BeginGUI();
guiStyle.Draw(GetGUIStyleRect(guiStyle, position), GUIContent.none, controlID);
Handles.EndGUI();
}
private Rect GetGUIStyleRect(GUIStyle style, Vector3 position)
{
Vector2 vector = HandleUtility.WorldToGUIPoint(position);
float fixedWidth = style.fixedWidth;
float fixedHeight = style.fixedHeight;
return new Rect(vector.x - fixedWidth / 2f, vector.y - fixedHeight / 2f, fixedWidth, fixedHeight);
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents an Action to process when the user clicks a particular mouse button a certain number of times.
/// </summary>
public class ClickAction : HoveredControlAction
{
private int m_Button;
private bool m_UseEvent;
/// <summary>
/// The number of button clicks required to satisfy the trigger condition
/// </summary>
public int clickCount = 1;
/// <summary>
/// The Action to execute when the user satisfies the trigger condition.
/// </summary>
public Action<IGUIState, Control> onClick;
private int m_ClickCounter = 0;
/// <summary>
/// Initializes and returns an instance of ClickAction
/// </summary>
/// <param name="control">Current control</param>
/// <param name="button">The mouse button to check for.</param>
/// <param name="useEvent">Whether to Use the current event after the trigger condition has been met.</param>
public ClickAction(Control control, int button, bool useEvent = true) : base(control)
{
m_Button = button;
m_UseEvent = useEvent;
}
/// <summary>
/// Checks to see if the trigger condition has been met or not.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if the trigger condition has been met. Otherwise, returns false.</returns>
protected override bool GetTriggerContidtion(IGUIState guiState)
{
if (guiState.mouseButton == m_Button && guiState.eventType == EventType.MouseDown)
{
if (guiState.clickCount == 1)
m_ClickCounter = 0;
++m_ClickCounter;
if (m_ClickCounter == clickCount)
return true;
}
return false;
}
/// <summary>
/// Calls the methods in its invocation list when the trigger conditions are met.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected override void OnTrigger(IGUIState guiState)
{
base.OnTrigger(guiState);
if (onClick != null)
onClick(guiState, hoveredControl);
if (m_UseEvent)
guiState.UseEvent();
}
/// <summary>
/// Checks to see if the finish condition has been met or not. For a ClickAction, this is always `true`.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true`.</returns>
protected override bool GetFinishContidtion(IGUIState guiState)
{
return true;
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents an Action to process when the custom editor validates a command.
/// </summary>
public class CommandAction : GUIAction
{
private string m_CommandName;
/// <summary>
/// The Action to execute.
/// </summary>
public Action<IGUIState> onCommand;
/// <summary>
/// Initializes and returns an instance of CommandAction
/// </summary>
/// <param name="commandName">The name of the command. When the custom editor validates a command with this name, it triggers the action.</param>
public CommandAction(string commandName)
{
m_CommandName = commandName;
}
/// <summary>
/// Checks to see if the trigger condition has been met or not.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if the trigger condition has been met. Otherwise, returns `false`.</returns>
protected override bool GetTriggerContidtion(IGUIState guiState)
{
if (guiState.eventType == EventType.ValidateCommand && guiState.commandName == m_CommandName)
{
guiState.UseEvent();
return true;
}
return false;
}
/// <summary>
/// Checks to see if the finish condition has been met or not.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if the trigger condition is finished. Otherwise, returns `false`.</returns>
protected override bool GetFinishContidtion(IGUIState guiState)
{
if (guiState.eventType == EventType.ExecuteCommand && guiState.commandName == m_CommandName)
{
guiState.UseEvent();
return true;
}
return false;
}
/// <summary>
/// Calls the methods in its invocation list when the finish condition is met.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected override void OnFinish(IGUIState guiState)
{
if (onCommand != null)
onCommand(guiState);
}
}
}

View File

@@ -0,0 +1,250 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents a UI control in a custom editor.
/// </summary>
public abstract class Control
{
private string m_Name;
private int m_NameHashCode;
private int m_ID;
private LayoutData m_LayoutData;
private int m_ActionID = -1;
private LayoutData m_HotLayoutData;
/// <summary>
/// The name of the control.
/// </summary>
public string name
{
get { return m_Name; }
}
/// <summary>
/// The control ID. The GUI uses this to identify the control.
/// </summary>
public int ID
{
get { return m_ID; }
}
/// <summary>
/// The action ID.
/// </summary>
public int actionID
{
get { return m_ActionID; }
}
/// <summary>
/// The control's layout data. This contains information about the control's position and orientation.
/// </summary>
public LayoutData layoutData
{
get { return m_LayoutData; }
set { m_LayoutData = value; }
}
/// <summary>
/// The control's hot layout data
/// </summary>
public LayoutData hotLayoutData
{
get { return m_HotLayoutData; }
}
/// <summary>
/// Initializes and returns an instance of Control
/// </summary>
/// <param name="name">The name of the control</param>
public Control(string name)
{
m_Name = name;
m_NameHashCode = name.GetHashCode();
}
/// <summary>
/// Gets the control from the guiState.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
public void GetControl(IGUIState guiState)
{
m_ID = guiState.GetControlID(m_NameHashCode, FocusType.Passive);
}
internal void SetActionID(int actionID)
{
m_ActionID = actionID;
m_HotLayoutData = m_LayoutData;
}
/// <summary>
/// Begins the layout for this control. A call to EndLayout must always follow a call to this function.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
public void BeginLayout(IGUIState guiState)
{
Debug.Assert(guiState.eventType == EventType.Layout);
m_LayoutData = OnBeginLayout(LayoutData.zero, guiState);
}
/// <summary>
/// Gets the control's layout data from the guiState.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
public void Layout(IGUIState guiState)
{
Debug.Assert(guiState.eventType == EventType.Layout);
for (var i = 0; i < GetCount(); ++i)
{
if (guiState.hotControl == actionID && hotLayoutData.index == i)
continue;
var layoutData = new LayoutData()
{
index = i,
position = GetPosition(guiState, i),
distance = GetDistance(guiState, i),
forward = GetForward(guiState, i),
up = GetUp(guiState, i),
right = GetRight(guiState, i),
userData = GetUserData(guiState, i)
};
m_LayoutData = LayoutData.Nearest(m_LayoutData, layoutData);
}
}
/// <summary>
/// Ends the layout for this control. This function must always follow a call to BeginLayout().
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
public void EndLayout(IGUIState guiState)
{
Debug.Assert(guiState.eventType == EventType.Layout);
OnEndLayout(guiState);
}
/// <summary>
/// Repaints the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
public void Repaint(IGUIState guiState)
{
for (var i = 0; i < GetCount(); ++i)
OnRepaint(guiState, i);
}
/// <summary>
/// Called when the control begins its layout.
/// </summary>
/// <param name="data">The layout data.</param>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns the layout data to use.</returns>
protected virtual LayoutData OnBeginLayout(LayoutData data, IGUIState guiState)
{
return data;
}
/// <summary>
/// Called when the control ends its layout.
/// /// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected virtual void OnEndLayout(IGUIState guiState)
{
}
/// <summary>
/// Called when the control repaints its contents.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The index.</param>
protected virtual void OnRepaint(IGUIState guiState, int index)
{
}
/// <summary>
/// Gets the number of sub-controllers.
/// </summary>
/// <remarks>
/// By default, this is `1`. If you implement your own controller and want to use multiple sub-controllers within it, you can override this function to declare how to count the sub-controllers.
/// </remarks>
/// <returns>Returns the number of sub-controllers. If you do not override this function, this returns 1.</returns>
protected virtual int GetCount()
{
return 1;
}
/// <summary>
/// Gets the position of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The index.</param>
/// <returns>Returns Vector3.zero.</returns>
protected virtual Vector3 GetPosition(IGUIState guiState, int index)
{
return Vector3.zero;
}
/// <summary>
/// Gets the forward vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The index.</param>
/// <returns>Returns Vector3.forward.</returns>
protected virtual Vector3 GetForward(IGUIState guiState, int index)
{
return Vector3.forward;
}
/// <summary>
/// Gets the up vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The index.</param>
/// <returns>Returns Vector3.up,</returns>
protected virtual Vector3 GetUp(IGUIState guiState, int index)
{
return Vector3.up;
}
/// <summary>
/// Gets the right vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The index.</param>
/// <returns>Returns Vector3.right.</returns>
protected virtual Vector3 GetRight(IGUIState guiState, int index)
{
return Vector3.right;
}
/// <summary>
/// Gets the distance from the Scene view camera to the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The index.</param>
/// <returns>Returns layoutData.distance.</returns>
protected virtual float GetDistance(IGUIState guiState, int index)
{
return layoutData.distance;
}
/// <summary>
/// Gets the control's user data.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The index.</param>
/// <returns>Returns `null`.</returns>
protected virtual object GetUserData(IGUIState guiState, int index)
{
return null;
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents the default implementation of a control.
/// </summary>
public abstract class DefaultControl : Control
{
/// <summary>
/// Default kPickDistance == 5.0f
/// </summary>
public static readonly float kPickDistance = 5f;
/// <summary>
/// Initializes and returns an instance of DefaultControl
/// </summary>
/// <param name="name">The name of the default control.</param>
public DefaultControl(string name) : base(name)
{
}
/// <summary>
/// Overrides the Control.OnBeginLayout function.
/// </summary>
/// <remarks>
/// Sets the LayoutData.distance to DefaultControl.kPickDistance.
/// </remarks>
/// <param name="data">The layout data.</param>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns the modified layout data.</returns>
protected override LayoutData OnBeginLayout(LayoutData data, IGUIState guiState)
{
data.distance = kPickDistance;
return data;
}
}
}

View File

@@ -0,0 +1,184 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents an action that is tied to a GUI element.
/// </summary>
public abstract class GUIAction
{
private int m_ID = -1;
/// <summary>
/// Func for GetEnable
/// </summary>
public Func<IGUIState, GUIAction, bool> enable;
/// <summary>
/// Func for EnabledRepaint
/// </summary>
public Func<IGUIState, GUIAction, bool> enableRepaint;
/// <summary>
/// Func for repaintOnMouseMove
/// </summary>
public Func<IGUIState, GUIAction, bool> repaintOnMouseMove;
/// <summary>
/// Action for OnPreRepaint
/// </summary>
public Action<IGUIState, GUIAction> onPreRepaint;
/// <summary>
/// Func for OnRepaint
/// </summary>
public Action<IGUIState, GUIAction> onRepaint;
/// <summary>
/// The action ID.
/// </summary>
public int ID
{
get { return m_ID; }
}
/// <summary>
/// Calls the methods in its invocation list when Unity draws this GUIAction's GUI.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
public void OnGUI(IGUIState guiState)
{
m_ID = guiState.GetControlID(GetType().GetHashCode(), FocusType.Passive);
if (guiState.hotControl == 0 && IsEnabled(guiState) && CanTrigger(guiState) && GetTriggerContidtion(guiState))
{
guiState.hotControl = ID;
OnTrigger(guiState);
}
if (guiState.hotControl == ID)
{
if (GetFinishContidtion(guiState))
{
OnFinish(guiState);
guiState.hotControl = 0;
}
else
{
OnPerform(guiState);
}
}
if (guiState.eventType == EventType.Repaint && IsRepaintEnabled(guiState))
Repaint(guiState);
}
/// <summary>
/// Checks whether the GUIAction is enabled.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if the GUIAction is enabled in the custom editor. Otherwise, returns `false`.</returns>
public bool IsEnabled(IGUIState guiState)
{
if (enable != null)
return enable(guiState, this);
return true;
}
/// <summary>
/// Checks whether the GUIAction should repaint.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if the GUIAction should repaint. Otherwise, returns `false`.</returns>
public bool IsRepaintEnabled(IGUIState guiState)
{
if (!IsEnabled(guiState))
return false;
if (enableRepaint != null)
return enableRepaint(guiState, this);
return true;
}
/// <summary>
/// Preprocessing that occurs before the GUI repaints.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
public void PreRepaint(IGUIState guiState)
{
Debug.Assert(guiState.eventType == EventType.Repaint);
if (IsEnabled(guiState) && onPreRepaint != null)
onPreRepaint(guiState, this);
}
/// <summary>
/// Calls the methods in its invocation list when repainting the GUI.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
private void Repaint(IGUIState guiState)
{
Debug.Assert(guiState.eventType == EventType.Repaint);
if (onRepaint != null)
onRepaint(guiState, this);
}
/// <summary>
/// Checks whether the GUI should repaint if the mouse moves over it.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if the GUI should repaint if the moves moves over it. Otherwise, returns `false`.</returns>
internal bool IsRepaintOnMouseMoveEnabled(IGUIState guiState)
{
if (!IsEnabled(guiState) || !IsRepaintEnabled(guiState))
return false;
if (repaintOnMouseMove != null)
return repaintOnMouseMove(guiState, this);
return false;
}
/// <summary>
/// Determines whether the finish condition has been met.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if finish condition has been met. Otherwise, returns `false`.</returns>
protected abstract bool GetFinishContidtion(IGUIState guiState);
/// <summary>
/// Determines whether the trigger condition has been met.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if finish condition has been met. Otherwise, returns `false`.</returns>
protected abstract bool GetTriggerContidtion(IGUIState guiState);
/// <summary>
/// Determines whether the GUIAction can trigger.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Always returns `true`.</returns>
protected virtual bool CanTrigger(IGUIState guiState) { return true; }
/// <summary>
/// Calls the methods in its invocation list when triggered.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected virtual void OnTrigger(IGUIState guiState)
{
}
/// <summary>
/// Calls the methods in its invocation list when performed.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected virtual void OnPerform(IGUIState guiState)
{
}
/// <summary>
/// Calls the methods in its invocation list when finished.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected virtual void OnFinish(IGUIState guiState)
{
}
}
}

View File

@@ -0,0 +1,245 @@
using UnityEngine;
using UnityEditor;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// An implementation of an IGUIState that represents a generic GUI state.
/// </summary>
public class GUIState : IGUIState
{
private Handles.CapFunction nullCap = (int c, Vector3 p , Quaternion r, float s, EventType ev) => {};
/// <summary>
/// The current mouse position.
/// </summary>
public Vector2 mousePosition
{
get { return Event.current.mousePosition; }
}
/// <summary>
/// The currently pressed button.
/// </summary>
public int mouseButton
{
get { return Event.current.button; }
}
/// <summary>
/// The current number of mouse clicks.
/// </summary>
public int clickCount
{
get { return Event.current.clickCount; }
set { Event.current.clickCount = Mathf.Max(0, value); }
}
/// <summary>
/// Indicates whether the shift key is pressed.
/// </summary>
public bool isShiftDown
{
get { return Event.current.shift; }
}
/// <summary>
/// Indicates whether the alt key is pressed.
/// </summary>
public bool isAltDown
{
get { return Event.current.alt; }
}
/// <summary>
/// Indicates whether the action key is pressed.
/// </summary>
public bool isActionKeyDown
{
get { return EditorGUI.actionKey; }
}
/// <summary>
/// The KeyCode of the currently pressed key.
/// </summary>
public KeyCode keyCode
{
get { return Event.current.keyCode; }
}
/// <summary>
/// The type of the current event.
/// </summary>
public EventType eventType
{
get { return Event.current.type; }
}
/// <summary>
/// The name of the current event's command.
/// </summary>
public string commandName
{
get { return Event.current.commandName; }
}
/// <summary>
/// The closest control to the event.
/// </summary>
public int nearestControl
{
get { return HandleUtility.nearestControl; }
set { HandleUtility.nearestControl = value; }
}
/// <summary>
/// Hot Control
/// </summary>
public int hotControl
{
get { return GUIUtility.hotControl; }
set { GUIUtility.hotControl = value; }
}
/// <summary>
/// Indicates whether the GUI has changed.
/// </summary>
public bool changed
{
get { return GUI.changed; }
set { GUI.changed = value; }
}
/// <summary>
/// Gets the ID of a nested control by a hint and focus type.
/// </summary>
/// <param name="hint">The hint this function uses to identify the control ID.</param>
/// <param name="focusType">The focus Type</param>
/// <returns>Returns the ID of the control that matches the hint and focus type.</returns>
public int GetControlID(int hint, FocusType focusType)
{
return GUIUtility.GetControlID(hint, focusType);
}
/// <summary>
/// Adds a control to the GUIState.
/// </summary>
/// <param name="controlID">The ID of the control to add.</param>
/// <param name="distance">The distance from the camera to the control.</param>
public void AddControl(int controlID, float distance)
{
HandleUtility.AddControl(controlID, distance);
}
/// <summary>
/// Checks whether a slider value has changed.
/// </summary>
/// <param name="id">The ID of the slider to check.</param>
/// <param name="sliderData">The slider's data.</param>
/// <param name="newPosition">The new position of the slider.</param>
/// <returns>Returns `true` if the slider has changed. Otherwise, returns `false`.</returns>
public bool Slider(int id, SliderData sliderData, out Vector3 newPosition)
{
if (mouseButton == 0 && eventType == EventType.MouseDown)
{
hotControl = 0;
nearestControl = id;
}
EditorGUI.BeginChangeCheck();
newPosition = Handles.Slider2D(id, sliderData.position, sliderData.forward, sliderData.right, sliderData.up, 1f, nullCap, Vector2.zero);
return EditorGUI.EndChangeCheck();
}
/// <summary>
/// Uses the current event.
/// </summary>
public void UseEvent()
{
Event.current.Use();
}
/// <summary>
/// Repaints the GUI.
/// </summary>
public void Repaint()
{
HandleUtility.Repaint();
}
/// <summary>
/// Checks if the current camera is valid.
/// </summary>
/// <returns>Returns `true` if the current camera is not null. Otherwise, returns `false`.</returns>
public bool HasCurrentCamera()
{
return Camera.current != null;
}
/// <summary>
/// Gets the size of the handle.
/// </summary>
/// <param name="position">The position of the handle.</param>
/// <returns>Returns the size of the handle.</returns>
public float GetHandleSize(Vector3 position)
{
var scale = HasCurrentCamera() ? 0.01f : 0.05f;
return HandleUtility.GetHandleSize(position) * scale;
}
/// <summary>
/// Measures the GUI-space distance between two points of a segment.
/// </summary>
/// <param name="p1">The first point.</param>
/// <param name="p2">The seconde point.</param>
/// <returns>Returns the GUI-space distance between p1 and p2.</returns>
public float DistanceToSegment(Vector3 p1, Vector3 p2)
{
p1 = HandleUtility.WorldToGUIPoint(p1);
p2 = HandleUtility.WorldToGUIPoint(p2);
return HandleUtility.DistancePointToLineSegment(Event.current.mousePosition, p1, p2);
}
/// <summary>
/// Measures the distance to a circle.
/// </summary>
/// <param name="center">The center of the circle.</param>
/// <param name="radius">The radius of the circle.</param>
/// <returns>Returns the distance to a circle with the specified center and radius.</returns>
public float DistanceToCircle(Vector3 center, float radius)
{
return HandleUtility.DistanceToCircle(center, radius);
}
/// <summary>
/// Transforms a GUI-space position into world space.
/// </summary>
/// <param name="guiPosition">The GUI position</param>
/// <param name="planeNormal">The plane normal.</param>
/// <param name="planePos">The plane position.</param>
/// <returns>Returns the world-space position of `guiPosition`.</returns>
public Vector3 GUIToWorld(Vector2 guiPosition, Vector3 planeNormal, Vector3 planePos)
{
Vector3 worldPos = Handles.inverseMatrix.MultiplyPoint(guiPosition);
if (Camera.current)
{
Ray ray = HandleUtility.GUIPointToWorldRay(guiPosition);
planeNormal = Handles.matrix.MultiplyVector(planeNormal);
planePos = Handles.matrix.MultiplyPoint(planePos);
Plane plane = new Plane(planeNormal, planePos);
float distance = 0f;
if (plane.Raycast(ray, out distance))
{
worldPos = Handles.inverseMatrix.MultiplyPoint(ray.GetPoint(distance));
}
}
return worldPos;
}
}
}

View File

@@ -0,0 +1,154 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents a system of GUI elements and controls.
/// </summary>
public class GUISystem
{
private readonly int kControlIDCheckHashCode = "ControlIDCheckHashCode".GetHashCode();
private List<Control> m_Controls = new List<Control>();
private List<GUIAction> m_Actions = new List<GUIAction>();
private IGUIState m_GUIState;
private int m_PrevNearestControl = -1;
private LayoutData m_PrevNearestLayoutData = LayoutData.zero;
private int m_ControlIDCheck = -1;
/// <summary>
/// Initializes and returns an instance of GUISystem
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
public GUISystem(IGUIState guiState)
{
m_GUIState = guiState;
}
/// <summary>
/// Adds a control to the internal list of controls.
/// </summary>
/// <param name="control">The control to add.</param>
public void AddControl(Control control)
{
if (control == null)
throw new NullReferenceException("Control is null");
m_Controls.Add(control);
}
/// <summary>
/// Removes a control from the internal list of controls.
/// </summary>
/// <param name="control">The control to remove.</param>
public void RemoveControl(Control control)
{
m_Controls.Remove(control);
}
/// <summary>
/// Adds an action to the internal list of actions.
/// </summary>
/// <param name="action">The action to add.</param>
public void AddAction(GUIAction action)
{
if (action == null)
throw new NullReferenceException("Action is null");
m_Actions.Add(action);
}
/// <summary>
/// Removes an action from the internal list of actions.
/// </summary>
/// <param name="action">The action to remove.</param>
public void RemoveAction(GUIAction action)
{
m_Actions.Remove(action);
}
/// <summary>
/// Calls the methods in its invocation list when Unity draws this GUISystems's GUI.
/// </summary>
public void OnGUI()
{
var controlIDCheck = m_GUIState.GetControlID(kControlIDCheckHashCode, FocusType.Passive);
if (m_GUIState.eventType == EventType.Layout)
m_ControlIDCheck = controlIDCheck;
else if (m_GUIState.eventType != EventType.Used && m_ControlIDCheck != controlIDCheck)
Debug.LogWarning("GetControlID at event " + m_GUIState.eventType + " returns a controlID different from the one in Layout event");
var nearestLayoutData = LayoutData.zero;
foreach (var control in m_Controls)
control.GetControl(m_GUIState);
if (m_GUIState.eventType == EventType.Layout)
{
foreach (var control in m_Controls)
control.BeginLayout(m_GUIState);
foreach (var control in m_Controls)
{
control.Layout(m_GUIState);
nearestLayoutData = LayoutData.Nearest(nearestLayoutData, control.layoutData);
}
foreach (var control in m_Controls)
m_GUIState.AddControl(control.ID, control.layoutData.distance);
foreach (var control in m_Controls)
control.EndLayout(m_GUIState);
if (m_PrevNearestControl == m_GUIState.nearestControl)
{
if (nearestLayoutData.index != m_PrevNearestLayoutData.index)
m_GUIState.Repaint();
}
else
{
m_PrevNearestControl = m_GUIState.nearestControl;
m_GUIState.Repaint();
}
m_PrevNearestLayoutData = nearestLayoutData;
}
if (m_GUIState.eventType == EventType.Repaint)
{
foreach (var action in m_Actions)
if (action.IsRepaintEnabled(m_GUIState))
action.PreRepaint(m_GUIState);
foreach (var control in m_Controls)
control.Repaint(m_GUIState);
}
var repaintOnMouseMove = false;
foreach (var action in m_Actions)
{
if (IsMouseMoveEvent())
repaintOnMouseMove |= action.IsRepaintOnMouseMoveEnabled(m_GUIState);
action.OnGUI(m_GUIState);
}
if (repaintOnMouseMove)
m_GUIState.Repaint();
}
/// <summary>
/// Calls the methods in its invocation list when the mouse moves.
/// </summary>
/// <returns>Returns `true` if the mouse moved. Otherwise, returns `false`.</returns>
private bool IsMouseMoveEvent()
{
return m_GUIState.eventType == EventType.MouseMove || m_GUIState.eventType == EventType.MouseDrag;
}
}
}

View File

@@ -0,0 +1,195 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents a generic UI control.
/// </summary>
public class GenericControl : Control
{
/// <summary>
/// Func for OnBeginLayout
/// </summary>
public Func<IGUIState, LayoutData> onBeginLayout;
/// <summary>
/// Action for OnEndLayout
/// </summary>
public Action<IGUIState> onEndLayout;
/// <summary>
/// Action for OnRepaint
/// </summary>
public Action<IGUIState, Control, int> onRepaint;
/// <summary>
/// Func for GetCount
/// </summary>
public Func<int> count;
/// <summary>
/// Func for GetPosition
/// </summary>
public Func<int, Vector3> position;
/// <summary>
/// Func for GetDistance
/// </summary>
public Func<IGUIState, int, float> distance;
/// <summary>
/// Func for GetForward
/// </summary>
public Func<int, Vector3> forward;
/// <summary>
/// Func for GetUp
/// </summary>
public Func<int, Vector3> up;
/// <summary>
/// Func for GetRight
/// </summary>
public Func<int, Vector3> right;
/// <summary>
/// Func for GetUserData
/// </summary>
public Func<int, object> userData;
/// <summary>
/// Initializes and returns an instance of GenericControl
/// </summary>
/// <param name="name">The name of the generic control.</param>
public GenericControl(string name) : base(name)
{
}
/// <summary>
/// Gets the number of sub-controllers.
/// </summary>
/// <remarks>
/// By default, this is `1`. If you implement your own controller and want to use multiple sub-controllers within it, you can assign getCount to a function that returns the number of sub-controllers.
/// </remarks>
/// <returns>Returns the number of sub-controllers. If you do not assign getCount, this returns 1.</returns>
protected override int GetCount()
{
if (count != null)
return count();
return base.GetCount();
}
/// <summary>
/// Called when the control ends its layout.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected override void OnEndLayout(IGUIState guiState)
{
if (onEndLayout != null)
onEndLayout(guiState);
}
/// <summary>
/// Called when the control repaints its contents.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">Current Index</param>
protected override void OnRepaint(IGUIState guiState, int index)
{
if (onRepaint != null)
onRepaint(guiState, this, index);
}
/// <summary>
/// Called when the control begins its layout.
/// </summary>
/// <param name="data">The layout data.</param>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>The LayoutData</returns>
protected override LayoutData OnBeginLayout(LayoutData data, IGUIState guiState)
{
if (onBeginLayout != null)
return onBeginLayout(guiState);
return data;
}
/// <summary>
/// Gets the position of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>The position</returns>
protected override Vector3 GetPosition(IGUIState guiState, int index)
{
if (position != null)
return position(index);
return base.GetPosition(guiState,index);
}
/// <summary>
/// Gets the distance from the Scene view camera to the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Returns the distance from the Scene view camera to the control.</returns>
protected override float GetDistance(IGUIState guiState, int index)
{
if (distance != null)
return distance(guiState, index);
return base.GetDistance(guiState, index);
}
/// <summary>
/// Gets the forward vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Returns the generic control's forward vector.</returns>
protected override Vector3 GetForward(IGUIState guiState, int index)
{
if (forward != null)
return forward(index);
return base.GetForward(guiState,index);
}
/// <summary>
/// Gets the up vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Returns the generic control's up vector.</returns>
protected override Vector3 GetUp(IGUIState guiState, int index)
{
if (up != null)
return up(index);
return base.GetUp(guiState,index);
}
/// <summary>
/// Gets the right vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Returns the generic control's right vector.</returns>
protected override Vector3 GetRight(IGUIState guiState, int index)
{
if (right != null)
return right(index);
return base.GetRight(guiState,index);
}
/// <summary>
/// Override for GetUserData
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Return the user data</returns>
protected override object GetUserData(IGUIState guiState, int index)
{
if (userData != null)
return userData(index);
return base.GetUserData(guiState,index);
}
}
}

View File

@@ -0,0 +1,110 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents a default generic UI control.
/// </summary>
public class GenericDefaultControl : DefaultControl
{
/// <summary>
/// Func for GetPosition
/// </summary>
public Func<IGUIState, Vector3> position;
/// <summary>
/// Func for GetForward
/// </summary>
public Func<IGUIState, Vector3> forward;
/// <summary>
/// Func for GetUp
/// </summary>
public Func<IGUIState, Vector3> up;
/// <summary>
/// Func for GetRight
/// </summary>
public Func<IGUIState, Vector3> right;
/// <summary>
/// Func for GetUserData
/// </summary>
public Func<IGUIState, object> userData;
/// <summary>
/// Initializes and returns an instance of GenericDefaultControl
/// </summary>
/// <param name="name">The name of the generic default control.</param>
public GenericDefaultControl(string name) : base(name)
{
}
/// <summary>
/// Gets the distance from the Scene view camera to the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>The distance from the Scene view camera to the control.</returns>
protected override Vector3 GetPosition(IGUIState guiState, int index)
{
if (position != null)
return position(guiState);
return base.GetPosition(guiState, index);
}
/// <summary>
/// Gets the forward vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Returns the generic control's forward vector.</returns>
protected override Vector3 GetForward(IGUIState guiState, int index)
{
if (forward != null)
return forward(guiState);
return base.GetForward(guiState, index);
}
/// <summary>
/// Gets the up vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Returns the generic control's up vector.</returns>
protected override Vector3 GetUp(IGUIState guiState, int index)
{
if (up != null)
return up(guiState);
return base.GetUp(guiState, index);
}
/// <summary>
/// Gets the right vector of the control.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Returns the generic control's right vector.</returns>
protected override Vector3 GetRight(IGUIState guiState, int index)
{
if (right != null)
return right(guiState);
return base.GetRight(guiState, index);
}
/// <summary>
/// Gets the control's user data.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <param name="index">The Index</param>
/// <returns>Returns the user data</returns>
protected override object GetUserData(IGUIState guiState, int index)
{
if (userData != null)
return userData(guiState);
return base.GetUserData(guiState, index);
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents an action that is tied to a Control.
/// </summary>
public abstract class HoveredControlAction : GUIAction
{
private Control m_HoveredControl;
/// <summary>
/// The hovered control.
/// </summary>
public Control hoveredControl
{
get { return m_HoveredControl; }
}
/// <summary>
/// Initializes and returns an instance of HoverControlAction.
/// </summary>
/// <param name="control">The control to execcute an action for on hover.</param>
public HoveredControlAction(Control control)
{
m_HoveredControl = control;
}
/// <summary>
/// Determines whether the HoveredControlAction can trigger.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if the HoveredControlAction can trigger. Otherwise, returns `false`.</returns>
protected override bool CanTrigger(IGUIState guiState)
{
return guiState.nearestControl == hoveredControl.ID;
}
/// <summary>
/// Calls the methods in its invocation list when triggered.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected override void OnTrigger(IGUIState guiState)
{
m_HoveredControl.SetActionID(ID);
}
}
}

View File

@@ -0,0 +1,155 @@
using UnityEngine;
using UnityEditor;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents transform data for a slider.
/// </summary>
/// <remarks>
/// Unity uses this data to position and orient the slider in the custom editor.
/// </remarks>
public struct SliderData
{
/// <summary>
/// The slider's position.
/// </summary>
public Vector3 position;
/// <summary>
/// The slider's forward vector.
/// </summary>
public Vector3 forward;
/// <summary>
/// The slider's up vector.
/// </summary>
public Vector3 up;
/// <summary>
/// The slider's right vector.
/// </summary>
public Vector3 right;
/// <summary>
/// zero definition for SliderData
/// </summary>
public static readonly SliderData zero = new SliderData() { position = Vector3.zero, forward = Vector3.forward, up = Vector3.up, right = Vector3.right };
}
/// <summary>
/// Interface for GUIStates
/// </summary>
public interface IGUIState
{
/// <summary>
/// The mouse position.
/// </summary>
Vector2 mousePosition { get; }
/// <summary>
/// The mouse button pressed.
/// </summary>
int mouseButton { get; }
/// <summary>
/// The number of mouse clicks.
/// </summary>
int clickCount { get; set; }
/// <summary>
/// Indicates whether the shift key is pressed.
/// </summary>
bool isShiftDown { get; }
/// <summary>
/// Indicates whether the alt key is pressed.
/// </summary>
bool isAltDown { get; }
/// <summary>
/// Indicates whether the action key is pressed.
/// </summary>
bool isActionKeyDown { get; }
/// <summary>
/// The KeyCode of the currently pressed key.
/// </summary>
KeyCode keyCode { get; }
/// <summary>
/// The type of the event.
/// </summary>
EventType eventType { get; }
/// <summary>
/// The name of the event's command.
/// </summary>
string commandName { get; }
/// <summary>
/// The closest control to the event.
/// </summary>
int nearestControl { get; set; }
/// <summary>
/// Hot Control
/// </summary>
int hotControl { get; set; }
/// <summary>
/// Indicates whether the GUI has changed.
/// </summary>
bool changed { get; set; }
/// <summary>
/// <summary>
/// Gets the ID of a nested control by a hint and focus type.
/// </summary>
/// <param name="hint">The hint this function uses to identify the control ID.</param>
/// <param name="focusType">The focus Type</param>
/// <returns>Returns the ID of the control that matches the hint and focus type.</returns>
int GetControlID(int hint, FocusType focusType);
/// <summary>
/// Adds a control to the GUIState.
/// </summary>
/// <param name="controlID">The ID of the control to add.</param>
/// <param name="distance">The distance from the camera to the control.</param>
void AddControl(int controlID, float distance);
/// <summary>
/// Checks whether a slider value has changed.
/// </summary>
/// <param name="id">The ID of the slider to check.</param>
/// <param name="sliderData">The slider's data.</param>
/// <param name="newPosition">The new position of the slider.</param>
/// <returns>Returns `true` if the slider has changed. Otherwise, returns `false`.</returns>
bool Slider(int id, SliderData sliderData, out Vector3 newPosition);
/// <summary>
/// Uses the event.
/// </summary>
void UseEvent();
/// <summary>
/// Repaints the GUI.
/// </summary>
void Repaint();
/// <summary>
/// Checks if the current camera is valid.
/// </summary>
/// <returns>Returns `true` if the current camera is not null. Otherwise, returns `false`.</returns>
bool HasCurrentCamera();
/// <summary>
/// Gets the size of the handle.
/// </summary>
/// <param name="position">The position of the handle.</param>
/// <returns>Returns the size of the handle.</returns>
float GetHandleSize(Vector3 position);
/// <summary>
/// Measures the GUI-space distance between two points of a segment.
/// </summary>
/// <param name="p1">The first point.</param>
/// <param name="p2">The second point.</param>
/// <returns>Returns the GUI-space distance between p1 and p2.</returns>
float DistanceToSegment(Vector3 p1, Vector3 p2);
/// <summary>
/// Measures the distance to a circle.
/// </summary>
/// <param name="center">The center of the circle</param>
/// <param name="radius">The radius of the circle</param>
/// <returns>Returns the distance to a circle with the specified center and radius.</returns>
float DistanceToCircle(Vector3 center, float radius);
/// <summary>
/// Transforms a GUI-space position into world space.
/// </summary>
/// <param name="guiPosition">The GUI Position.</param>
/// <param name="planeNormal">The plane normal.</param>
/// <param name="planePos">The plane position.</param>
/// <returns>Returns the world-space position of `guiPosition`.</returns>
Vector3 GUIToWorld(Vector2 guiPosition, Vector3 planeNormal, Vector3 planePos);
}
}

View File

@@ -0,0 +1,58 @@
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// Represents the layout of a GUI element in a custom editor.
/// </summary>
public struct LayoutData
{
/// <summary>
/// The layout's index.
/// </summary>
public int index;
/// <summary>
/// The distance from the layout to the camera.
/// </summary>
public float distance;
/// <summary>
/// The layout's world-space position.
/// </summary>
public Vector3 position;
/// <summary>
/// The layout's world-space forward vector.
/// </summary>
public Vector3 forward;
/// <summary>
/// The layout's world-space up vector.
/// </summary>
public Vector3 up;
/// <summary>
/// The layout's world-space right vector.
/// </summary>
public Vector3 right;
/// <summary>
/// The layout's user data.
/// </summary>
public object userData;
/// <summary>
/// Zero definition of LayoutData.
/// </summary>
public static readonly LayoutData zero = new LayoutData() { index = 0, distance = float.MaxValue, position = Vector3.zero, forward = Vector3.forward, up = Vector3.up, right = Vector3.right };
/// <summary>
/// Gets the layout that is closest to the camera,
/// </summary>
/// <param name="currentData">The current layout.</param>
/// <param name="newData">The new layout to compare with.</param>
/// <returns>Returns the closest layout to the camera. If `currentData` is closest to the camera, returns `currentData`. Otherwise, if `newData` is closest to the camera, returns `newData`.</returns>
public static LayoutData Nearest(LayoutData currentData, LayoutData newData)
{
if (newData.distance <= currentData.distance)
return newData;
return currentData;
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using UnityEngine;
namespace UnityEditor.U2D.Path.GUIFramework
{
/// <summary>
/// SliderAction implementation of a ClickAction
/// </summary>
public class SliderAction : ClickAction
{
private SliderData m_SliderData;
/// <summary>
/// Action for OnSliderBegin
/// </summary>
public Action<IGUIState, Control, Vector3> onSliderBegin;
/// <summary>
/// Action for OnSliderChanged
/// </summary>
public Action<IGUIState, Control, Vector3> onSliderChanged;
/// <summary>
/// Action for OnSliderEnd
/// </summary>
public Action<IGUIState, Control, Vector3> onSliderEnd;
/// <summary>
/// Initializes and returns an instance of SliderAction
/// </summary>
/// <param name="control">The control to execcute an action for on slide.</param>
public SliderAction(Control control) : base(control, 0, false)
{
}
/// <summary>
/// Checks to see if the finish condition has been met or not.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
/// <returns>Returns `true` if the finish condition has been met. Otherwise, returns `false`.</returns>
protected override bool GetFinishContidtion(IGUIState guiState)
{
return guiState.eventType == EventType.MouseUp && guiState.mouseButton == 0;
}
/// <summary>
/// Called when there is interaction with the slider. It updates the stored slider data with data post-interaction.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected override void OnTrigger(IGUIState guiState)
{
base.OnTrigger(guiState);
m_SliderData.position = hoveredControl.hotLayoutData.position;
m_SliderData.forward = hoveredControl.hotLayoutData.forward;
m_SliderData.right = hoveredControl.hotLayoutData.right;
m_SliderData.up = hoveredControl.hotLayoutData.up;
if (onSliderBegin != null)
onSliderBegin(guiState, hoveredControl, m_SliderData.position);
}
/// <summary>
/// Post-processing for when the slider interaction finishes.
/// </summary>
/// <param name="guiState">The current state of the custom editor.</param>
protected override void OnFinish(IGUIState guiState)
{
if (onSliderEnd != null)
onSliderEnd(guiState, hoveredControl, m_SliderData.position);
guiState.UseEvent();
guiState.Repaint();
}
/// <summary>
/// Moves the slider to the new permission and executes `onSliderChanged` using the new position.
/// </summary>
/// <param name="guiState">The current state of the custom editor</param>
protected override void OnPerform(IGUIState guiState)
{
Vector3 newPosition;
var changed = guiState.Slider(ID, m_SliderData, out newPosition);
if (changed)
{
m_SliderData.position = newPosition;
if (onSliderChanged != null)
onSliderChanged(guiState, hoveredControl, newPosition);
}
}
}
}

View File

@@ -0,0 +1,15 @@
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public interface IDrawer
{
void DrawCreatePointPreview(Vector3 position);
void DrawPoint(Vector3 position);
void DrawPointHovered(Vector3 position);
void DrawPointSelected(Vector3 position);
void DrawLine(Vector3 p1, Vector3 p2, float width, Color color);
void DrawBezier(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, float width, Color color);
void DrawTangent(Vector3 position, Vector3 tangent);
}
}

View File

@@ -0,0 +1,537 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.U2D.Path.GUIFramework;
namespace UnityEditor.U2D.Path
{
public class PathEditor
{
const float kSnappingDistance = 15f;
const string kDeleteCommandName = "Delete";
const string kSoftDeleteCommandName = "SoftDelete";
public IEditablePathController controller { get; set; }
public bool linearTangentIsZero { get; set; }
private IDrawer m_Drawer = new Drawer();
private IDrawer m_DrawerOverride;
private GUISystem m_GUISystem;
public IDrawer drawerOverride { get; set; }
private IDrawer drawer
{
get
{
if (drawerOverride != null)
return drawerOverride;
return m_Drawer;
}
}
public PathEditor() : this(new GUISystem(new GUIState())) { }
public PathEditor(GUISystem guiSystem)
{
m_GUISystem = guiSystem;
var m_PointControl = new GenericControl("Point")
{
count = GetPointCount,
distance = (guiState, i) =>
{
var position = GetPoint(i).position;
return guiState.DistanceToCircle(position, guiState.GetHandleSize(position) * 10f);
},
position = (i) => { return GetPoint(i).position; },
forward = (i) => { return GetForward(); },
up = (i) => { return GetUp(); },
right = (i) => { return GetRight(); },
onRepaint = DrawPoint
};
var m_EdgeControl = new GenericControl("Edge")
{
count = GetEdgeCount,
distance = DistanceToEdge,
position = (i) => { return GetPoint(i).position; },
forward = (i) => { return GetForward(); },
up = (i) => { return GetUp(); },
right = (i) => { return GetRight(); },
onRepaint = DrawEdge
};
m_EdgeControl.onEndLayout = (guiState) => { controller.AddClosestPath(m_EdgeControl.layoutData.distance); };
var m_LeftTangentControl = new GenericControl("LeftTangent")
{
count = () =>
{
if (GetShapeType() != ShapeType.Spline)
return 0;
return GetPointCount();
},
distance = (guiState, i) =>
{
if (linearTangentIsZero && GetPoint(i).tangentMode == TangentMode.Linear)
return float.MaxValue;
if (!IsSelected(i) || IsOpenEnded() && i == 0)
return float.MaxValue;
var position = GetLeftTangent(i);
return guiState.DistanceToCircle(position, guiState.GetHandleSize(position) * 10f);
},
position = (i) => { return GetLeftTangent(i); },
forward = (i) => { return GetForward(); },
up = (i) => { return GetUp(); },
right = (i) => { return GetRight(); },
onRepaint = (guiState, control, i) =>
{
if (!IsSelected(i) || IsOpenEnded() && i == 0)
return;
var point = GetPoint(i);
if (linearTangentIsZero && point.tangentMode == TangentMode.Linear)
return;
var position = point.position;
var leftTangent = GetLeftTangent(i);
drawer.DrawTangent(position, leftTangent);
}
};
var m_RightTangentControl = new GenericControl("RightTangent")
{
count = () =>
{
if (GetShapeType() != ShapeType.Spline)
return 0;
return GetPointCount();
},
distance = (guiState, i) =>
{
if (linearTangentIsZero && GetPoint(i).tangentMode == TangentMode.Linear)
return float.MaxValue;
if (!IsSelected(i) || IsOpenEnded() && i == GetPointCount()-1)
return float.MaxValue;
var position = GetRightTangent(i);
return guiState.DistanceToCircle(position, guiState.GetHandleSize(position) * 10f);
},
position = (i) => { return GetRightTangent(i); },
forward = (i) => { return GetForward(); },
up = (i) => { return GetUp(); },
right = (i) => { return GetRight(); },
onRepaint = (guiState, control, i) =>
{
if (!IsSelected(i) || IsOpenEnded() && i == GetPointCount()-1)
return;
var point = GetPoint(i);
if (linearTangentIsZero && point.tangentMode == TangentMode.Linear)
return;
var position = point.position;
var rightTangent = GetRightTangent(i);
drawer.DrawTangent(position, rightTangent);
}
};
var m_CreatePointAction = new CreatePointAction(m_PointControl, m_EdgeControl)
{
enable = (guiState, action) => !IsAltDown(guiState) && !guiState.isActionKeyDown && controller.closestEditablePath == controller.editablePath,
enableRepaint = (guiState, action) => EnableCreatePointRepaint(guiState, m_PointControl, m_LeftTangentControl, m_RightTangentControl),
repaintOnMouseMove = (guiState, action) => true,
guiToWorld = GUIToWorld,
onCreatePoint = (index, position) =>
{
controller.RegisterUndo("Create Point");
controller.CreatePoint(index, position);
},
onPreRepaint = (guiState, action) =>
{
if (GetPointCount() > 0)
{
var position = ClosestPointInEdge(guiState, guiState.mousePosition, m_EdgeControl.layoutData.index);
drawer.DrawCreatePointPreview(position);
}
}
};
Action<IGUIState> removePoints = (guiState) =>
{
controller.RegisterUndo("Remove Point");
controller.RemoveSelectedPoints();
guiState.changed = true;
};
var m_RemovePointAction1 = new CommandAction(kDeleteCommandName)
{
enable = (guiState, action) => { return GetSelectedPointCount() > 0; },
onCommand = removePoints
};
var m_RemovePointAction2 = new CommandAction(kSoftDeleteCommandName)
{
enable = (guiState, action) => { return GetSelectedPointCount() > 0; },
onCommand = removePoints
};
var dragged = false;
var m_MovePointAction = new SliderAction(m_PointControl)
{
enable = (guiState, action) => !IsAltDown(guiState),
onClick = (guiState, control) =>
{
dragged = false;
var index = control.layoutData.index;
if (!IsSelected(index))
{
controller.RegisterUndo("Selection");
if (!guiState.isActionKeyDown)
controller.ClearSelection();
controller.SelectPoint(index, true);
guiState.changed = true;
}
},
onSliderChanged = (guiState, control, position) =>
{
var index = control.hotLayoutData.index;
var delta = SnapIfNeeded(position) - GetPoint(index).position;
if (!dragged)
{
controller.RegisterUndo("Move Point");
dragged = true;
}
controller.MoveSelectedPoints(delta);
}
};
var m_MoveEdgeAction = new SliderAction(m_EdgeControl)
{
enable = (guiState, action) => !IsAltDown(guiState) && guiState.isActionKeyDown,
onSliderBegin = (guiState, control, position) =>
{
dragged = false;
},
onSliderChanged = (guiState, control, position) =>
{
var index = control.hotLayoutData.index;
var delta = position - GetPoint(index).position;
if (!dragged)
{
controller.RegisterUndo("Move Edge");
dragged = true;
}
controller.MoveEdge(index, delta);
}
};
var cachedRightTangent = Vector3.zero;
var cachedLeftTangent = Vector3.zero;
var cachedTangentMode = TangentMode.Linear;
var m_MoveLeftTangentAction = new SliderAction(m_LeftTangentControl)
{
enable = (guiState, action) => !IsAltDown(guiState),
onSliderBegin = (guiState, control, position) =>
{
dragged = false;
var point = GetPoint(control.hotLayoutData.index);
cachedRightTangent = point.rightTangent;
cachedTangentMode = point.tangentMode;
},
onSliderChanged = (guiState, control, position) =>
{
var index = control.hotLayoutData.index;
var setToLinear = m_PointControl.distance(guiState, index) <= DefaultControl.kPickDistance;
if (!dragged)
{
controller.RegisterUndo("Move Tangent");
dragged = true;
}
position = SnapIfNeeded(position);
controller.SetLeftTangent(index, position, setToLinear, guiState.isShiftDown, cachedRightTangent, cachedTangentMode);
}
};
var m_MoveRightTangentAction = new SliderAction(m_RightTangentControl)
{
enable = (guiState, action) => !IsAltDown(guiState),
onSliderBegin = (guiState, control, position) =>
{
dragged = false;
var point = GetPoint(control.hotLayoutData.index);
cachedLeftTangent = point.leftTangent;
cachedTangentMode = point.tangentMode;
},
onSliderChanged = (guiState, control, position) =>
{
var index = control.hotLayoutData.index;
var setToLinear = m_PointControl.distance(guiState, index) <= DefaultControl.kPickDistance;
if (!dragged)
{
controller.RegisterUndo("Move Tangent");
dragged = true;
}
position = SnapIfNeeded(position);
controller.SetRightTangent(index, position, setToLinear, guiState.isShiftDown, cachedLeftTangent, cachedTangentMode);
}
};
m_GUISystem.AddControl(m_EdgeControl);
m_GUISystem.AddControl(m_PointControl);
m_GUISystem.AddControl(m_LeftTangentControl);
m_GUISystem.AddControl(m_RightTangentControl);
m_GUISystem.AddAction(m_CreatePointAction);
m_GUISystem.AddAction(m_RemovePointAction1);
m_GUISystem.AddAction(m_RemovePointAction2);
m_GUISystem.AddAction(m_MovePointAction);
m_GUISystem.AddAction(m_MoveEdgeAction);
m_GUISystem.AddAction(m_MoveLeftTangentAction);
m_GUISystem.AddAction(m_MoveRightTangentAction);
}
public void OnGUI()
{
m_GUISystem.OnGUI();
}
private bool IsAltDown(IGUIState guiState)
{
return guiState.hotControl == 0 && guiState.isAltDown;
}
private ControlPoint GetPoint(int index)
{
return controller.editablePath.GetPoint(index);
}
private int GetPointCount()
{
return controller.editablePath.pointCount;
}
private int GetEdgeCount()
{
if (controller.editablePath.isOpenEnded)
return controller.editablePath.pointCount - 1;
return controller.editablePath.pointCount;
}
private int GetSelectedPointCount()
{
return controller.editablePath.selection.Count;
}
private bool IsSelected(int index)
{
return controller.editablePath.selection.Contains(index);
}
private Vector3 GetForward()
{
return controller.editablePath.forward;
}
private Vector3 GetUp()
{
return controller.editablePath.up;
}
private Vector3 GetRight()
{
return controller.editablePath.right;
}
private Matrix4x4 GetLocalToWorldMatrix()
{
return controller.editablePath.localToWorldMatrix;
}
private ShapeType GetShapeType()
{
return controller.editablePath.shapeType;
}
private bool IsOpenEnded()
{
return controller.editablePath.isOpenEnded;
}
private Vector3 GetLeftTangent(int index)
{
if (linearTangentIsZero)
return GetPoint(index).leftTangent;
return controller.editablePath.CalculateLeftTangent(index);
}
private Vector3 GetRightTangent(int index)
{
if (linearTangentIsZero)
return GetPoint(index).rightTangent;
return controller.editablePath.CalculateRightTangent(index);
}
private int NextIndex(int index)
{
return EditablePathUtility.Mod(index + 1, GetPointCount());
}
private ControlPoint NextControlPoint(int index)
{
return GetPoint(NextIndex(index));
}
private int PrevIndex(int index)
{
return EditablePathUtility.Mod(index - 1, GetPointCount());
}
private ControlPoint PrevControlPoint(int index)
{
return GetPoint(PrevIndex(index));
}
private Vector3 ClosestPointInEdge(IGUIState guiState, Vector2 mousePosition, int index)
{
if (GetShapeType() == ShapeType.Polygon)
{
var p0 = GetPoint(index).position;
var p1 = NextControlPoint(index).position;
var mouseWorldPosition = GUIToWorld(guiState, mousePosition);
var dir1 = (mouseWorldPosition - p0);
var dir2 = (p1 - p0);
return Mathf.Clamp01(Vector3.Dot(dir1, dir2.normalized) / dir2.magnitude) * dir2 + p0;
}
else if (GetShapeType() == ShapeType.Spline)
{
var nextIndex = NextIndex(index);
float t;
return BezierUtility.ClosestPointOnCurve(
GUIToWorld(guiState, mousePosition),
GetPoint(index).position,
GetPoint(nextIndex).position,
GetRightTangent(index),
GetLeftTangent(nextIndex),
out t);
}
return Vector3.zero;
}
private float DistanceToEdge(IGUIState guiState, int index)
{
if (GetShapeType() == ShapeType.Polygon)
{
return guiState.DistanceToSegment(GetPoint(index).position, NextControlPoint(index).position);
}
else if (GetShapeType() == ShapeType.Spline)
{
var closestPoint = ClosestPointInEdge(guiState, guiState.mousePosition, index);
var closestPoint2 = HandleUtility.WorldToGUIPoint(closestPoint);
return (closestPoint2 - guiState.mousePosition).magnitude;
}
return float.MaxValue;
}
private Vector3 GUIToWorld(IGUIState guiState, Vector2 position)
{
return guiState.GUIToWorld(position, GetForward(), GetLocalToWorldMatrix().MultiplyPoint3x4(Vector3.zero));
}
private void DrawPoint(IGUIState guiState, Control control, int index)
{
var position = GetPoint(index).position;
if (guiState.hotControl == control.actionID && control.hotLayoutData.index == index || IsSelected(index))
drawer.DrawPointSelected(position);
else if (guiState.hotControl == 0 && guiState.nearestControl == control.ID && !IsAltDown(guiState) && control.layoutData.index == index)
drawer.DrawPointHovered(position);
else
drawer.DrawPoint(position);
}
private void DrawEdge(IGUIState guiState, Control control, int index)
{
if (GetShapeType() == ShapeType.Polygon)
{
var nextIndex = NextIndex(index);
var color = Color.white;
if(guiState.nearestControl == control.ID && control.layoutData.index == index && guiState.hotControl == 0 && !IsAltDown(guiState))
color = Color.yellow;
drawer.DrawLine(GetPoint(index).position, GetPoint(nextIndex).position, 5f, color);
}
else if (GetShapeType() == ShapeType.Spline)
{
var nextIndex = NextIndex(index);
var color = Color.white;
if(guiState.nearestControl == control.ID && control.layoutData.index == index && guiState.hotControl == 0 && !IsAltDown(guiState))
color = Color.yellow;
drawer.DrawBezier(
GetPoint(index).position,
GetRightTangent(index),
GetLeftTangent(nextIndex),
GetPoint(nextIndex).position,
5f,
color);
}
}
private bool EnableCreatePointRepaint(IGUIState guiState, Control pointControl, Control leftTangentControl, Control rightTangentControl)
{
return guiState.nearestControl != pointControl.ID &&
guiState.hotControl == 0 &&
(guiState.nearestControl != leftTangentControl.ID) &&
(guiState.nearestControl != rightTangentControl.ID);
}
private Vector3 SnapIfNeeded(Vector3 position)
{
if (!controller.enableSnapping || controller.snapping == null)
return position;
var guiPosition = HandleUtility.WorldToGUIPoint(position);
var snappedGuiPosition = HandleUtility.WorldToGUIPoint(controller.snapping.Snap(position));
var sqrDistance = (guiPosition - snappedGuiPosition).sqrMagnitude;
if (sqrDistance < kSnappingDistance * kSnappingDistance)
position = controller.snapping.Snap(position);
return position;
}
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.U2D.Path.GUIFramework;
namespace UnityEditor.U2D.Path
{
public abstract class RectSelector<T> : ISelector<T>
{
public class Styles
{
public readonly GUIStyle selectionRectStyle;
public Styles()
{
selectionRectStyle = GUI.skin.FindStyle("selectionRect");
}
}
public Action<ISelector<T>, bool> onSelectionBegin;
public Action<ISelector<T>> onSelectionChanged;
public Action<ISelector<T>> onSelectionEnd;
private GUISystem m_GUISystem;
private Control m_RectSelectorControl;
private GUIAction m_RectSelectAction;
private Rect m_GUIRect;
private Styles m_Styles;
public Rect guiRect
{
get { return m_GUIRect; }
}
private Styles styles
{
get
{
if (m_Styles == null)
m_Styles = new Styles();
return m_Styles;
}
}
public RectSelector() : this(new GUISystem(new GUIState())) { }
public RectSelector(GUISystem guiSystem)
{
m_GUISystem = guiSystem;
m_RectSelectorControl = new GenericDefaultControl("RectSelector");
var start = Vector2.zero;
var rectEnd = Vector2.zero;
m_RectSelectAction = new SliderAction(m_RectSelectorControl)
{
enable = (guiState, action) => !IsAltDown(guiState),
enableRepaint = (guiState, action) =>
{
var size = start - rectEnd;
return size != Vector2.zero && guiState.hotControl == action.ID;
},
onSliderBegin = (guiState, control, position) =>
{
start = guiState.mousePosition;
rectEnd = guiState.mousePosition;
m_GUIRect = FromToRect(start, rectEnd);
if (onSelectionBegin != null)
onSelectionBegin(this, guiState.isShiftDown);
},
onSliderChanged = (guiState, control, position) =>
{
rectEnd = guiState.mousePosition;
m_GUIRect = FromToRect(start, rectEnd);
if (onSelectionChanged != null)
onSelectionChanged(this);
},
onSliderEnd = (guiState, control, position) =>
{
if (onSelectionEnd != null)
onSelectionEnd(this);
},
onRepaint = (guiState, action) =>
{
Handles.BeginGUI();
styles.selectionRectStyle.Draw(m_GUIRect, GUIContent.none, false, false, false, false);
Handles.EndGUI();
}
};
m_GUISystem.AddControl(m_RectSelectorControl);
m_GUISystem.AddAction(m_RectSelectAction);
}
private bool IsAltDown(IGUIState guiState)
{
return guiState.hotControl == 0 && guiState.isAltDown;
}
private Rect FromToRect(Vector2 start, Vector2 end)
{
Rect r = new Rect(start.x, start.y, end.x - start.x, end.y - start.y);
if (r.width < 0)
{
r.x += r.width;
r.width = -r.width;
}
if (r.height < 0)
{
r.y += r.height;
r.height = -r.height;
}
return r;
}
public void OnGUI()
{
m_GUISystem.OnGUI();
}
bool ISelector<T>.Select(T element)
{
return Select(element);
}
protected abstract bool Select(T element);
}
}

View File

@@ -0,0 +1,7 @@
namespace UnityEditor.U2D.Path
{
public interface ISelectable<T>
{
bool Select(ISelector<T> selector);
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace UnityEditor.U2D.Path
{
public interface ISelection<T>
{
int Count { get; }
T activeElement { get; set; }
T[] elements { get; set; }
void Clear();
void BeginSelection();
void EndSelection(bool select);
bool Select(T element, bool select);
bool Contains(T element);
}
}

View File

@@ -0,0 +1,7 @@
namespace UnityEditor.U2D.Path
{
public interface ISelector<T>
{
bool Select(T element);
}
}

View File

@@ -0,0 +1,13 @@
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
namespace UnityEditor.U2D.Path
{
[Serializable]
internal class IndexedSelection : SerializableSelection<int>
{
protected override int GetInvalidElement() { return -1; }
}
}

View File

@@ -0,0 +1,13 @@
using UnityEngine;
using UnityEditor;
namespace UnityEditor.U2D.Path
{
public class PointRectSelector : RectSelector<Vector3>
{
protected override bool Select(Vector3 element)
{
return guiRect.Contains(HandleUtility.WorldToGUIPoint(element), true);
}
}
}

View File

@@ -0,0 +1,143 @@
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
namespace UnityEditor.U2D.Path
{
[Serializable]
internal abstract class SerializableSelection<T> : ISelection<T>, ISerializationCallbackReceiver
{
internal readonly static int kInvalidID = -1;
[SerializeField]
private T[] m_Keys = new T[0];
private HashSet<T> m_Selection = new HashSet<T>();
private HashSet<T> m_TemporalSelection = new HashSet<T>();
private bool m_SelectionInProgress = false;
public int Count
{
get { return m_Selection.Count + m_TemporalSelection.Count; }
}
public T activeElement
{
get { return First(); }
set
{
Clear();
Select(value, true);
}
}
public T[] elements
{
get
{
var set = m_Selection;
if (m_SelectionInProgress)
{
var union = new HashSet<T>(m_Selection);
union.UnionWith(m_TemporalSelection);
set = union;
}
return new List<T>(set).ToArray();
}
set
{
Clear();
foreach(var element in value)
Select(element, true);
}
}
protected abstract T GetInvalidElement();
public void Clear()
{
GetSelection().Clear();
}
public void BeginSelection()
{
m_SelectionInProgress = true;
Clear();
}
public void EndSelection(bool select)
{
m_SelectionInProgress = false;
if (select)
m_Selection.UnionWith(m_TemporalSelection);
else
m_Selection.ExceptWith(m_TemporalSelection);
m_TemporalSelection.Clear();
}
public bool Select(T element, bool select)
{
var changed = false;
if(EqualityComparer<T>.Default.Equals(element, GetInvalidElement()))
return changed;
if (select)
changed = GetSelection().Add(element);
else if (Contains(element))
changed = GetSelection().Remove(element);
return changed;
}
public bool Contains(T element)
{
return m_Selection.Contains(element) || m_TemporalSelection.Contains(element);
}
private HashSet<T> GetSelection()
{
if (m_SelectionInProgress)
return m_TemporalSelection;
return m_Selection;
}
private T First()
{
T element = First(m_Selection);
if(EqualityComparer<T>.Default.Equals(element, GetInvalidElement()))
element = First(m_TemporalSelection);
return element;
}
private T First(HashSet<T> set)
{
if(set.Count == 0)
return GetInvalidElement();
using (var enumerator = set.GetEnumerator())
{
Debug.Assert(enumerator.MoveNext());
return enumerator.Current;
}
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
m_Keys = new List<T>(m_Selection).ToArray();
}
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
elements = m_Keys;
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public enum ShapeType
{
Polygon,
Spline
}
public interface IShape
{
ShapeType type { get; }
bool isOpenEnded { get; }
ControlPoint[] ToControlPoints();
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public struct Polygon : IShape
{
public bool isOpenEnded;
public Vector3[] points;
ShapeType IShape.type => ShapeType.Polygon;
bool IShape.isOpenEnded => isOpenEnded;
ControlPoint[] IShape.ToControlPoints()
{
if (points == null)
throw new NullReferenceException("Points array is null");
var controlPoints = new List<ControlPoint>();
foreach (var point in points)
{
controlPoints.Add(new ControlPoint() { position = point });
}
return controlPoints.ToArray();
}
public static Polygon empty = new Polygon() { isOpenEnded = true, points = new Vector3[0] };
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public static class ShapeExtensions
{
public static Polygon ToPolygon(this Vector3[] points, bool isOpenEnded)
{
return new Polygon()
{
isOpenEnded = isOpenEnded,
points = points
};
}
public static Spline ToSpline(this Vector3[] points, bool isOpenEnded)
{
if (!points.IsSpline(isOpenEnded) && points.IsSpline(!isOpenEnded))
{
var pointList = new List<Vector3>(points);
if (isOpenEnded)
{
while (pointList.Count % 3 != 1)
pointList.RemoveAt(pointList.Count-1);
points = pointList.ToArray();
}
else
{
var last = pointList[pointList.Count-1];
var first = pointList[0];
var v = first - last;
pointList.Add(last + v.normalized * (v.magnitude / 3f));
pointList.Add(first - v.normalized * (v.magnitude / 3f));
points = pointList.ToArray();
}
}
if (!points.IsSpline(isOpenEnded))
throw new Exception("The provided control point array can't conform a Spline.");
return new Spline()
{
isOpenEnded = isOpenEnded,
points = points
};
}
public static bool IsSpline(this Vector3[] points, bool isOpenEnded)
{
if (points.Length < 4)
return false;
if (isOpenEnded && points.Length % 3 != 1)
return false;
if (!isOpenEnded && points.Length % 3 != 0)
return false;
return true;
}
public static Spline ToSpline(this Polygon polygon)
{
var newPointCount = polygon.points.Length * 3;
if (polygon.isOpenEnded)
newPointCount = (polygon.points.Length - 1) * 3 + 1;
var newPoints = new Vector3[newPointCount];
var controlPoints = polygon.points;
var pointCount = controlPoints.Length;
for (var i = 0; i < pointCount; ++i)
{
var nextIndex = (i + 1) % pointCount;
var point = controlPoints[i];
var v = controlPoints[nextIndex] - point;
newPoints[i * 3] = point;
if (i * 3 + 2 < newPointCount)
{
newPoints[i * 3 + 1] = point + v / 3f;
newPoints[i * 3 + 2] = point + v * 2f / 3f;
}
}
return new Spline()
{
isOpenEnded = polygon.isOpenEnded,
points = newPoints
};
}
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.U2D.Path
{
public struct Spline : IShape
{
public bool isOpenEnded;
public Vector3[] points;
ShapeType IShape.type => ShapeType.Spline;
bool IShape.isOpenEnded => isOpenEnded;
ControlPoint[] IShape.ToControlPoints()
{
if (points == null)
throw new NullReferenceException("Points array is null");
if (!points.IsSpline(isOpenEnded))
throw new Exception("The provided control point array can't conform a Spline.");
var controlPoints = new List<ControlPoint>();
var leftTangent = Vector3.zero;
var rightTangent = Vector3.zero;
var pointCount = points.Length;
for (var i = 0; i < pointCount; i += 3)
{
if (i == 0)
{
if (isOpenEnded)
leftTangent = points[0];
else
leftTangent = points[EditablePathUtility.Mod(-1, pointCount)];
}
if (i == pointCount - 1 && isOpenEnded)
rightTangent = points[i];
else
rightTangent = points[i+1];
controlPoints.Add(
new ControlPoint()
{
position = points[i],
leftTangent = leftTangent,
rightTangent = rightTangent,
tangentMode = TangentMode.Broken
});
if (i == pointCount - 1 && isOpenEnded)
leftTangent = Vector3.zero;
else
leftTangent = points[i+2];
}
pointCount = controlPoints.Count;
for (var i = 0; i < pointCount; ++i)
{
var prevIndex = EditablePathUtility.Mod(i-1, pointCount);
var nextIndex = EditablePathUtility.Mod(i+1, pointCount);
var controlPoint = controlPoints[i];
var prevControlPoint = controlPoints[prevIndex];
var nextControlPoint = controlPoints[nextIndex];
var liniarLeftPosition = (prevControlPoint.position - controlPoint.position) / 3f;
var isLeftTangentLinear = (controlPoint.localLeftTangent - liniarLeftPosition).sqrMagnitude < 0.001f;
if (isLeftTangentLinear)
controlPoint.localLeftTangent = Vector3.zero;
var liniarRightPosition = (nextControlPoint.position - controlPoint.position) / 3f;
var isRightTangentLinear = (controlPoint.localRightTangent - liniarRightPosition).sqrMagnitude < 0.001f;
if (isRightTangentLinear)
controlPoint.localRightTangent = Vector3.zero;
var tangentDotProduct = Vector3.Dot(controlPoint.localLeftTangent.normalized, controlPoint.localRightTangent.normalized);
var isContinous = tangentDotProduct < 0f && (tangentDotProduct + 1) * (tangentDotProduct + 1) < 0.001f;
if (isLeftTangentLinear && isRightTangentLinear)
controlPoint.tangentMode = TangentMode.Linear;
else if (isLeftTangentLinear || isRightTangentLinear)
controlPoint.tangentMode = TangentMode.Broken;
else if (isContinous)
controlPoint.tangentMode = TangentMode.Continuous;
controlPoints[i] = controlPoint;
}
return controlPoints.ToArray();
}
public static Spline empty = new Spline() { isOpenEnded = true, points = new Vector3[0] };
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "Unity.2D.Path.Editor",
"references": [],
"optionalUnityReferences": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": []
}

View File

@@ -0,0 +1,5 @@
com.unity.2d.path copyright © 2020 Unity Technologies ApS
Licensed under the Unity Companion License for Unity-dependent projects (see https://unity3d.com/legal/licenses/unity_companion_license).
Unless expressly provided otherwise, the Software under this license is made available strictly on an “AS IS” BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the license for details on these and other terms and conditions.

View File

@@ -0,0 +1,22 @@
# QA Report
Use this file to outline the test strategy for this package.
## QA Owner: [*Add Name*]
## Test strategy
*Use this section to describe how this feature was tested.*
* A link to the Test Plan (Test Rails, other)
* Results from the package's editor and runtime test suite.
* Link to automated test results (if any)
* Manual test Results, [here's an example](https://docs.google.com/spreadsheets/d/12A76U5Gf969w10KL4Ik0wC1oFIBDUoRrqIvQgD18TFo/edit#gid=0)
* Scenario test week outcome
* etc.
## Package Status
Use this section to describe:
* package stability
* known bugs, issues
* performance metrics,
* etc
In other words, a general feeling on the health of this package.

View File

@@ -0,0 +1 @@
Shape Editor

View File

@@ -0,0 +1,10 @@
using NUnit.Framework;
internal class PathPlaceholder
{
[Test]
public void PlaceHolderTest()
{
Assert.Pass("Path tests are in a separate package.");
}
}

View File

@@ -0,0 +1,20 @@
{
"name": "Unity.2D.Path.Tests",
"references": [
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,23 @@
{
"name": "com.unity.2d.path",
"version": "5.0.0",
"unity": "2021.1",
"displayName": "2D Path",
"description": "2D Path provides tooling to edit shapes (polygons and Bézier splines) in EditorWindows and the SceneView.",
"category": "2D",
"keywords": [
"2d",
"path"
],
"relatedPackages": {
"com.unity.2d.path.tests": "5.0.0"
},
"upmCi": {
"footprint": "d1ccb04dbe5b9cc41426f021c1e6d896c1b657cf"
},
"repository": {
"url": "https://github.cds.internal.unity3d.com/unity/2d.git",
"type": "git",
"revision": "82ee1598c2f1270bc1081c1f908364e528335bd5"
}
}