using System; using Unity.Mathematics; using UnityEngine; namespace UnityEditor.U2D.Animation { internal class SpriteMeshController { const float k_SnapDistance = 10f; struct EdgeIntersectionResult { public int startVertexIndex; public int endVertexIndex; public int intersectEdgeIndex; public Vector2 endPosition; } SpriteMeshDataController m_SpriteMeshDataController = new(); EdgeIntersectionResult m_EdgeIntersectionResult; public ISpriteMeshView spriteMeshView { get; set; } public BaseSpriteMeshData spriteMeshData { get; set; } public ISelection selection { get; set; } public ICacheUndo cacheUndo { get; set; } public ITriangulator triangulator { get; set; } public bool disable { get; set; } public Rect frame { get; set; } public void OnGUI() { m_SpriteMeshDataController.spriteMeshData = spriteMeshData; Debug.Assert(spriteMeshView != null); Debug.Assert(spriteMeshData != null); Debug.Assert(selection != null); Debug.Assert(cacheUndo != null); ValidateSelectionValues(); spriteMeshView.selection = selection; spriteMeshView.frame = frame; EditorGUI.BeginDisabledGroup(disable); spriteMeshView.BeginLayout(); if (spriteMeshView.CanLayout()) { LayoutVertices(); LayoutEdges(); } spriteMeshView.EndLayout(); if (spriteMeshView.CanRepaint()) { DrawEdges(); if (GUI.enabled) { PreviewCreateVertex(); PreviewCreateEdge(); PreviewSplitEdge(); } DrawVertices(); } HandleSplitEdge(); HandleCreateEdge(); HandleCreateVertex(); EditorGUI.EndDisabledGroup(); HandleSelectVertex(); HandleSelectEdge(); EditorGUI.BeginDisabledGroup(disable); HandleMoveVertexAndEdge(); EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(disable); HandleRemoveVertices(); spriteMeshView.DoRepaint(); EditorGUI.EndDisabledGroup(); } void ValidateSelectionValues() { foreach (var index in selection.elements) { if (index >= spriteMeshData.vertexCount) { selection.Clear(); break; } } } void LayoutVertices() { for (var i = 0; i < spriteMeshData.vertexCount; i++) { spriteMeshView.LayoutVertex(spriteMeshData.vertices[i], i); } } void LayoutEdges() { for (var i = 0; i < spriteMeshData.edges.Length; i++) { var edge = spriteMeshData.edges[i]; var startPosition = spriteMeshData.vertices[edge.x]; var endPosition = spriteMeshData.vertices[edge.y]; spriteMeshView.LayoutEdge(startPosition, endPosition, i); } } void DrawEdges() { UpdateEdgeIntersection(); spriteMeshView.BeginDrawEdges(); for (var i = 0; i < spriteMeshData.edges.Length; ++i) { if (SkipDrawEdge(i)) continue; var edge = spriteMeshData.edges[i]; var startPosition = spriteMeshData.vertices[edge.x]; var endPosition = spriteMeshData.vertices[edge.y]; if (selection.Contains(edge.x) && selection.Contains(edge.y)) spriteMeshView.DrawEdgeSelected(startPosition, endPosition); else spriteMeshView.DrawEdge(startPosition, endPosition); } if (spriteMeshView.IsActionActive(MeshEditorAction.SelectEdge)) { var hoveredEdge = spriteMeshData.edges[spriteMeshView.hoveredEdge]; var startPosition = spriteMeshData.vertices[hoveredEdge.x]; var endPosition = spriteMeshData.vertices[hoveredEdge.y]; spriteMeshView.DrawEdgeHovered(startPosition, endPosition); } spriteMeshView.EndDrawEdges(); } bool SkipDrawEdge(int edgeIndex) { if (GUI.enabled == false) return false; return edgeIndex == -1 || spriteMeshView.hoveredEdge == edgeIndex && spriteMeshView.IsActionActive(MeshEditorAction.SelectEdge) || spriteMeshView.hoveredEdge == edgeIndex && spriteMeshView.IsActionActive(MeshEditorAction.CreateVertex) || spriteMeshView.closestEdge == edgeIndex && spriteMeshView.IsActionActive(MeshEditorAction.SplitEdge) || edgeIndex == m_EdgeIntersectionResult.intersectEdgeIndex && spriteMeshView.IsActionActive(MeshEditorAction.CreateEdge); } void PreviewCreateVertex() { if (spriteMeshView.mode == SpriteMeshViewMode.CreateVertex && spriteMeshView.IsActionActive(MeshEditorAction.CreateVertex)) { var clampedMousePos = ClampToFrame(spriteMeshView.mouseWorldPosition); if (spriteMeshView.hoveredEdge != -1) { var edge = spriteMeshData.edges[spriteMeshView.hoveredEdge]; spriteMeshView.BeginDrawEdges(); spriteMeshView.DrawEdge(spriteMeshData.vertices[edge.x], clampedMousePos); spriteMeshView.DrawEdge(spriteMeshData.vertices[edge.y], clampedMousePos); spriteMeshView.EndDrawEdges(); } spriteMeshView.DrawVertex(clampedMousePos); } } void PreviewCreateEdge() { if (!spriteMeshView.IsActionActive(MeshEditorAction.CreateEdge)) return; spriteMeshView.BeginDrawEdges(); spriteMeshView.DrawEdge(spriteMeshData.vertices[m_EdgeIntersectionResult.startVertexIndex], m_EdgeIntersectionResult.endPosition); if (m_EdgeIntersectionResult.intersectEdgeIndex != -1) { var intersectingEdge = spriteMeshData.edges[m_EdgeIntersectionResult.intersectEdgeIndex]; spriteMeshView.DrawEdge(spriteMeshData.vertices[intersectingEdge.x], m_EdgeIntersectionResult.endPosition); spriteMeshView.DrawEdge(spriteMeshData.vertices[intersectingEdge.y], m_EdgeIntersectionResult.endPosition); } spriteMeshView.EndDrawEdges(); if (m_EdgeIntersectionResult.endVertexIndex == -1) spriteMeshView.DrawVertex(m_EdgeIntersectionResult.endPosition); } void PreviewSplitEdge() { if (!spriteMeshView.IsActionActive(MeshEditorAction.SplitEdge)) return; var clampedMousePos = ClampToFrame(spriteMeshView.mouseWorldPosition); var closestEdge = spriteMeshData.edges[spriteMeshView.closestEdge]; spriteMeshView.BeginDrawEdges(); spriteMeshView.DrawEdge(spriteMeshData.vertices[closestEdge.x], clampedMousePos); spriteMeshView.DrawEdge(spriteMeshData.vertices[closestEdge.y], clampedMousePos); spriteMeshView.EndDrawEdges(); spriteMeshView.DrawVertex(clampedMousePos); } void DrawVertices() { for (var i = 0; i < spriteMeshData.vertexCount; i++) { var position = spriteMeshData.vertices[i]; if (selection.Contains(i)) spriteMeshView.DrawVertexSelected(position); else if (i == spriteMeshView.hoveredVertex && spriteMeshView.IsActionHot(MeshEditorAction.None)) spriteMeshView.DrawVertexHovered(position); else spriteMeshView.DrawVertex(position); } } void HandleSelectVertex() { if (spriteMeshView.DoSelectVertex(out var additive)) SelectVertex(spriteMeshView.hoveredVertex, additive); } void HandleSelectEdge() { if (spriteMeshView.DoSelectEdge(out var additive)) SelectEdge(spriteMeshView.hoveredEdge, additive); } void HandleMoveVertexAndEdge() { if (selection.Count == 0) return; if (spriteMeshView.DoMoveVertex(out var finalDeltaPos) || spriteMeshView.DoMoveEdge(out finalDeltaPos)) { var selectionArray = selection.elements; finalDeltaPos = MathUtility.MoveRectInsideFrame(CalculateRectFromSelection(), frame, finalDeltaPos); var movedVertexSelection = GetMovedVertexSelection(in selectionArray, spriteMeshData.vertices, finalDeltaPos); if (IsMovedEdgeIntersectingWithOtherEdge(in selectionArray, in movedVertexSelection, spriteMeshData.edges, spriteMeshData.vertices)) return; if (IsMovedVertexIntersectingWithOutline(in selectionArray, in movedVertexSelection, spriteMeshData.outlineEdges, spriteMeshData.vertices)) return; cacheUndo.BeginUndoOperation(TextContent.moveVertices); MoveSelectedVertices(in movedVertexSelection); } } void HandleCreateVertex() { if (spriteMeshView.DoCreateVertex()) { var position = ClampToFrame(spriteMeshView.mouseWorldPosition); var edgeIndex = spriteMeshView.hoveredEdge; if (spriteMeshView.hoveredEdge != -1) CreateVertex(position, edgeIndex); else if (m_SpriteMeshDataController.FindTriangle(position, out var indices, out var barycentricCoords)) CreateVertex(position, indices, barycentricCoords); } } void HandleSplitEdge() { if (spriteMeshView.DoSplitEdge()) SplitEdge(ClampToFrame(spriteMeshView.mouseWorldPosition), spriteMeshView.closestEdge); } void HandleCreateEdge() { if (spriteMeshView.DoCreateEdge()) { var clampedMousePosition = ClampToFrame(spriteMeshView.mouseWorldPosition); var edgeIntersectionResult = CalculateEdgeIntersection(selection.activeElement, spriteMeshView.hoveredVertex, spriteMeshView.hoveredEdge, clampedMousePosition); if (edgeIntersectionResult.endVertexIndex != -1) { CreateEdge(selection.activeElement, edgeIntersectionResult.endVertexIndex); } else { if (edgeIntersectionResult.intersectEdgeIndex != -1) { CreateVertex(edgeIntersectionResult.endPosition, edgeIntersectionResult.intersectEdgeIndex); CreateEdge(selection.activeElement, spriteMeshData.vertexCount - 1); } else if (m_SpriteMeshDataController.FindTriangle(edgeIntersectionResult.endPosition, out var indices, out var barycentricCoords)) { CreateVertex(edgeIntersectionResult.endPosition, indices, barycentricCoords); CreateEdge(selection.activeElement, spriteMeshData.vertexCount - 1); } } } } void HandleRemoveVertices() { if (spriteMeshView.DoRemove()) RemoveSelectedVertices(); } void CreateVertex(Vector2 position, Vector3Int indices, Vector3 barycentricCoords) { var bw1 = spriteMeshData.vertexWeights[indices.x]; var bw2 = spriteMeshData.vertexWeights[indices.y]; var bw3 = spriteMeshData.vertexWeights[indices.z]; var result = new EditableBoneWeight(); foreach (var channel in bw1) { if (!channel.enabled) continue; var weight = channel.weight * barycentricCoords.x; if (weight > 0f) result.AddChannel(channel.boneIndex, weight, true); } foreach (var channel in bw2) { if (!channel.enabled) continue; var weight = channel.weight * barycentricCoords.y; if (weight > 0f) result.AddChannel(channel.boneIndex, weight, true); } foreach (var channel in bw3) { if (!channel.enabled) continue; var weight = channel.weight * barycentricCoords.z; if (weight > 0f) result.AddChannel(channel.boneIndex, weight, true); } result.UnifyChannelsWithSameBoneIndex(); result.FilterChannels(0f); result.Clamp(4, true); var boneWeight = result.ToBoneWeight(true); cacheUndo.BeginUndoOperation(TextContent.createVertex); m_SpriteMeshDataController.CreateVertex(position, -1); spriteMeshData.vertexWeights[spriteMeshData.vertexCount - 1].SetFromBoneWeight(boneWeight); Triangulate(); } void CreateVertex(Vector2 position, int edgeIndex) { var edge = spriteMeshData.edges[edgeIndex]; var pos1 = spriteMeshData.vertices[edge.x]; var pos2 = spriteMeshData.vertices[edge.y]; var dir1 = (position - pos1); var dir2 = (pos2 - pos1); var t = Vector2.Dot(dir1, dir2.normalized) / dir2.magnitude; t = Mathf.Clamp01(t); var bw1 = spriteMeshData.vertexWeights[edge.x].ToBoneWeight(true); var bw2 = spriteMeshData.vertexWeights[edge.y].ToBoneWeight(true); var boneWeight = EditableBoneWeightUtility.Lerp(bw1, bw2, t); cacheUndo.BeginUndoOperation(TextContent.createVertex); m_SpriteMeshDataController.CreateVertex(position, edgeIndex); spriteMeshData.vertexWeights[spriteMeshData.vertexCount - 1].SetFromBoneWeight(boneWeight); Triangulate(); } void SelectVertex(int index, bool additiveToggle) { if (index < 0) throw new ArgumentException("Index out of range"); var selected = selection.Contains(index); if (selected) { if (additiveToggle) { cacheUndo.BeginUndoOperation(TextContent.selection); selection.Select(index, false); } } else { cacheUndo.BeginUndoOperation(TextContent.selection); if (!additiveToggle) ClearSelection(); selection.Select(index, true); } cacheUndo.IncrementCurrentGroup(); } void SelectEdge(int index, bool additiveToggle) { Debug.Assert(index >= 0); var edge = spriteMeshData.edges[index]; cacheUndo.BeginUndoOperation(TextContent.selection); var selected = selection.Contains(edge.x) && selection.Contains(edge.y); if (selected) { if (additiveToggle) { selection.Select(edge.x, false); selection.Select(edge.y, false); } } else { if (!additiveToggle) ClearSelection(); selection.Select(edge.x, true); selection.Select(edge.y, true); } cacheUndo.IncrementCurrentGroup(); } void ClearSelection() { cacheUndo.BeginUndoOperation(TextContent.selection); selection.Clear(); } void MoveSelectedVertices(in Vector2[] movedVertices) { for (var i = 0; i < selection.Count; ++i) { var index = selection.elements[i]; spriteMeshData.vertices[index] = movedVertices[i]; } Triangulate(); } void CreateEdge(int fromVertexIndex, int toVertexIndex) { cacheUndo.BeginUndoOperation(TextContent.createEdge); m_SpriteMeshDataController.CreateEdge(fromVertexIndex, toVertexIndex); Triangulate(); ClearSelection(); selection.Select(toVertexIndex, true); cacheUndo.IncrementCurrentGroup(); } void SplitEdge(Vector2 position, int edgeIndex) { cacheUndo.BeginUndoOperation(TextContent.splitEdge); CreateVertex(position, edgeIndex); cacheUndo.IncrementCurrentGroup(); } bool IsEdgeSelected() { if (selection.Count != 2) return false; var indices = selection.elements; var index1 = indices[0]; var index2 = indices[1]; var edge = new int2(index1, index2); return spriteMeshData.edges.ContainsAny(edge); } void RemoveSelectedVertices() { cacheUndo.BeginUndoOperation(IsEdgeSelected() ? TextContent.removeEdge : TextContent.removeVertices); var verticesToRemove = selection.elements; var noOfVertsToDelete = verticesToRemove.Length; var noOfVertsInMesh = m_SpriteMeshDataController.spriteMeshData.vertexCount; var shouldClearMesh = (noOfVertsInMesh - noOfVertsToDelete) < 3; if (shouldClearMesh) { m_SpriteMeshDataController.spriteMeshData.Clear(); m_SpriteMeshDataController.CreateQuad(); } else m_SpriteMeshDataController.RemoveVertex(verticesToRemove); Triangulate(); selection.Clear(); } void Triangulate() { m_SpriteMeshDataController.Triangulate(triangulator); m_SpriteMeshDataController.SortTrianglesByDepth(); } Vector2 ClampToFrame(Vector2 position) { return MathUtility.ClampPositionToRect(position, frame); } Rect CalculateRectFromSelection() { var rect = new Rect(); var min = new Vector2(float.MaxValue, float.MaxValue); var max = new Vector2(float.MinValue, float.MinValue); var indices = selection.elements; foreach (var index in indices) { var v = spriteMeshData.vertices[index]; min.x = Mathf.Min(min.x, v.x); min.y = Mathf.Min(min.y, v.y); max.x = Mathf.Max(max.x, v.x); max.y = Mathf.Max(max.y, v.y); } rect.min = min; rect.max = max; return rect; } void UpdateEdgeIntersection() { if (selection.Count == 1) m_EdgeIntersectionResult = CalculateEdgeIntersection(selection.activeElement, spriteMeshView.hoveredVertex, spriteMeshView.hoveredEdge, ClampToFrame(spriteMeshView.mouseWorldPosition)); } EdgeIntersectionResult CalculateEdgeIntersection(int vertexIndex, int hoveredVertexIndex, int hoveredEdgeIndex, Vector2 targetPosition) { Debug.Assert(vertexIndex >= 0); var edgeIntersection = new EdgeIntersectionResult { startVertexIndex = vertexIndex, endVertexIndex = hoveredVertexIndex, endPosition = targetPosition, intersectEdgeIndex = -1 }; var startPoint = spriteMeshData.vertices[edgeIntersection.startVertexIndex]; var intersectsEdge = false; var lastIntersectingEdgeIndex = -1; do { lastIntersectingEdgeIndex = edgeIntersection.intersectEdgeIndex; if (intersectsEdge) { var dir = edgeIntersection.endPosition - startPoint; edgeIntersection.endPosition += dir.normalized * 10f; } intersectsEdge = SegmentIntersectsEdge(startPoint, edgeIntersection.endPosition, vertexIndex, ref edgeIntersection.endPosition, out edgeIntersection.intersectEdgeIndex); //if we are hovering a vertex and intersect an edge indexing it we forget about the intersection var edges = spriteMeshData.edges; var edge = intersectsEdge ? edges[edgeIntersection.intersectEdgeIndex] : default; if (intersectsEdge && (edge.x == edgeIntersection.endVertexIndex || edge.y == edgeIntersection.endVertexIndex)) { edgeIntersection.intersectEdgeIndex = -1; intersectsEdge = false; edgeIntersection.endPosition = spriteMeshData.vertices[edgeIntersection.endVertexIndex]; } if (intersectsEdge) { edgeIntersection.endVertexIndex = -1; var intersectingEdge = spriteMeshData.edges[edgeIntersection.intersectEdgeIndex]; var newPointScreen = spriteMeshView.WorldToScreen(edgeIntersection.endPosition); var edgeV1 = spriteMeshView.WorldToScreen(spriteMeshData.vertices[intersectingEdge.x]); var edgeV2 = spriteMeshView.WorldToScreen(spriteMeshData.vertices[intersectingEdge.y]); if ((newPointScreen - edgeV1).magnitude <= k_SnapDistance) edgeIntersection.endVertexIndex = intersectingEdge.x; else if ((newPointScreen - edgeV2).magnitude <= k_SnapDistance) edgeIntersection.endVertexIndex = intersectingEdge.y; if (edgeIntersection.endVertexIndex != -1) { edgeIntersection.intersectEdgeIndex = -1; intersectsEdge = false; edgeIntersection.endPosition = spriteMeshData.vertices[edgeIntersection.endVertexIndex]; } } } while (intersectsEdge && lastIntersectingEdgeIndex != edgeIntersection.intersectEdgeIndex); edgeIntersection.intersectEdgeIndex = intersectsEdge ? edgeIntersection.intersectEdgeIndex : hoveredEdgeIndex; if (edgeIntersection.endVertexIndex != -1 && !intersectsEdge) edgeIntersection.endPosition = spriteMeshData.vertices[edgeIntersection.endVertexIndex]; return edgeIntersection; } bool SegmentIntersectsEdge(Vector2 p1, Vector2 p2, int ignoreIndex, ref Vector2 point, out int intersectingEdgeIndex) { intersectingEdgeIndex = -1; var sqrDistance = float.MaxValue; for (var i = 0; i < spriteMeshData.edges.Length; i++) { var edge = spriteMeshData.edges[i]; var v1 = spriteMeshData.vertices[edge.x]; var v2 = spriteMeshData.vertices[edge.y]; var pointTmp = Vector2.zero; if (edge.x != ignoreIndex && edge.y != ignoreIndex && MathUtility.SegmentIntersection(p1, p2, v1, v2, ref pointTmp)) { var sqrMagnitude = (pointTmp - p1).sqrMagnitude; if (sqrMagnitude < sqrDistance) { sqrDistance = sqrMagnitude; intersectingEdgeIndex = i; point = pointTmp; } } } return intersectingEdgeIndex != -1; } static Vector2[] GetMovedVertexSelection(in int[] selection, in Vector2[] vertices, Vector2 deltaPosition) { var movedVertices = new Vector2[selection.Length]; for (var i = 0; i < selection.Length; i++) { var index = selection[i]; movedVertices[i] = vertices[index] + deltaPosition; } return movedVertices; } static bool IsMovedEdgeIntersectingWithOtherEdge(in int[] selection, in Vector2[] movedVertices, in int2[] meshEdges, in Vector2[] meshVertices) { var edgeCount = meshEdges.Length; var edgeIntersectionPoint = Vector2.zero; for (var i = 0; i < edgeCount; i++) { var selectionIndex = FindSelectionIndexFromEdge(selection, meshEdges[i]); if (selectionIndex.x == -1 && selectionIndex.y == -1) continue; var edgeStart = selectionIndex.x != -1 ? movedVertices[selectionIndex.x] : meshVertices[meshEdges[i].x]; var edgeEnd = selectionIndex.y != -1 ? movedVertices[selectionIndex.y] : meshVertices[meshEdges[i].y]; for (var o = 0; o < edgeCount; o++) { if (o == i) continue; if (meshEdges[i].x == meshEdges[o].x || meshEdges[i].y == meshEdges[o].x || meshEdges[i].x == meshEdges[o].y || meshEdges[i].y == meshEdges[o].y) continue; var otherSelectionIndex = FindSelectionIndexFromEdge(in selection, meshEdges[o]); var otherEdgeStart = otherSelectionIndex.x != -1 ? movedVertices[otherSelectionIndex.x] : meshVertices[meshEdges[o].x]; var otherEdgeEnd = otherSelectionIndex.y != -1 ? movedVertices[otherSelectionIndex.y] : meshVertices[meshEdges[o].y]; if (MathUtility.SegmentIntersection(edgeStart, edgeEnd, otherEdgeStart, otherEdgeEnd, ref edgeIntersectionPoint)) return true; } } return false; } static int2 FindSelectionIndexFromEdge(in int[] selection, int2 edge) { var selectionIndex = new int2(-1, -1); for (var m = 0; m < selection.Length; ++m) { if (selection[m] == edge.x) { selectionIndex.x = m; break; } if (selection[m] == edge.y) { selectionIndex.y = m; break; } } return selectionIndex; } static bool IsMovedVertexIntersectingWithOutline(in int[] selection, in Vector2[] movedVertices, in int2[] outlineEdges, in Vector2[] meshVertices) { var edgeIntersectionPoint = Vector2.zero; for (var i = 0; i < selection.Length; ++i) { var edgeStart = meshVertices[selection[i]]; var edgeEnd = movedVertices[i]; for (var m = 0; m < outlineEdges.Length; ++m) { if (selection[i] == outlineEdges[m].x || selection[i] == outlineEdges[m].y) continue; var otherSelectionIndex = FindSelectionIndexFromEdge(in selection, outlineEdges[m]); var otherEdgeStart = otherSelectionIndex.x != -1 ? movedVertices[otherSelectionIndex.x] : meshVertices[outlineEdges[m].x]; var otherEdgeEnd = otherSelectionIndex.y != -1 ? movedVertices[otherSelectionIndex.y] : meshVertices[outlineEdges[m].y]; if (MathUtility.SegmentIntersection(edgeStart, edgeEnd, otherEdgeStart, otherEdgeEnd, ref edgeIntersectionPoint)) return true; } } return false; } } }