using System; using System.Collections; using System.Linq; using System.Collections.Generic; using UnityEngine; using UnityEditorInternal; namespace UnityEditor.U2D.Sprites { internal class SpriteRectModel : ScriptableObject, ISerializationCallbackReceiver { [Serializable] struct StringGUID { [SerializeField] string m_StringGUID; public StringGUID(GUID guid) { m_StringGUID = guid.ToString(); } public static implicit operator GUID(StringGUID d) => new GUID(d.m_StringGUID); public static implicit operator StringGUID(GUID d) => new StringGUID(d); } [Serializable] class StringGUIDList : IReadOnlyList { [SerializeField] List m_List = new List(); GUID IReadOnlyList.this[int index] { get => m_List[index]; } public StringGUID this[int index] { get => m_List[index]; set => m_List[index] = value; } IEnumerator IEnumerable.GetEnumerator() { // Not used for now throw new NotImplementedException(); } public int Count => m_List.Count; public IEnumerator GetEnumerator() { return m_List.GetEnumerator(); } public void Clear() { m_List.Clear(); } public void RemoveAt(int i) { m_List.RemoveAt(i); } public void Add(StringGUID value) { m_List.Add(value); } } /// /// List of all SpriteRects /// [SerializeField] private List m_SpriteRects; /// /// List of all names in the Name-FileId Table /// [SerializeField] private List m_SpriteNames; /// /// List of all FileIds in the Name-FileId Table /// [SerializeField] private StringGUIDList m_SpriteFileIds; /// /// HashSet of all names currently in use by SpriteRects /// private HashSet m_NamesInUse; private HashSet m_InternalIdsInUse; public IReadOnlyList spriteRects => m_SpriteRects; public IReadOnlyList spriteNames => m_SpriteNames; public IReadOnlyList spriteFileIds => m_SpriteFileIds; private SpriteRectModel() { m_SpriteNames = new List(); m_SpriteFileIds = new StringGUIDList(); Clear(); } public void SetSpriteRects(List newSpriteRects) { m_SpriteRects = newSpriteRects; m_NamesInUse = new HashSet(); m_InternalIdsInUse = new HashSet(); for (var i = 0; i < m_SpriteRects.Count; ++i) { m_NamesInUse.Add(m_SpriteRects[i].name); m_InternalIdsInUse.Add(m_SpriteRects[i].spriteID); } } public void SetNameFileIdPairs(IEnumerable pairs) { m_SpriteNames.Clear(); m_SpriteFileIds.Clear(); foreach (var pair in pairs) AddNameFileIdPair(pair.name, pair.GetFileGUID()); } public int FindIndex(Predicate match) { int i = 0; foreach (var spriteRect in m_SpriteRects) { if (match.Invoke(spriteRect)) return i; i++; } return -1; } public void Clear() { m_SpriteRects = new List(); m_NamesInUse = new HashSet(); m_InternalIdsInUse = new HashSet(); } public bool Add(SpriteRect spriteRect, bool shouldReplaceInTable = false) { if (spriteRect.spriteID.Empty()) { spriteRect.spriteID = GUID.Generate(); } else { if (IsInternalIdInUsed(spriteRect.spriteID)) return false; } if (shouldReplaceInTable) { // replace id from sprite to file id table if (!UpdateIdInNameIdPair(spriteRect.name, spriteRect.spriteID)) { // add it into file id table if update wasn't successful i.e. it doesn't exist yet AddNameFileIdPair(spriteRect.name, spriteRect.spriteID); } } else { // Since we are not replacing the file id table, // look for any existing id and set it to the SpriteRect var index = m_SpriteNames.FindIndex(x => x == spriteRect.name); if (index >= 0) { if (IsInternalIdInUsed(m_SpriteFileIds[index])) return false; spriteRect.spriteID = m_SpriteFileIds[index]; } else AddNameFileIdPair(spriteRect.name, spriteRect.spriteID); } m_SpriteRects.Add(spriteRect); m_NamesInUse.Add(spriteRect.name); m_InternalIdsInUse.Add(spriteRect.spriteID); return true; } public void Remove(SpriteRect spriteRect) { m_SpriteRects.Remove(spriteRect); m_NamesInUse.Remove(spriteRect.name); m_InternalIdsInUse.Remove(spriteRect.spriteID); } /// /// Checks whether or not the name is currently in use by any of the SpriteRects in the texture. /// /// The name to check for /// True if the name is currently in use public bool IsNameUsed(string rectName) { return m_NamesInUse.Contains(rectName); } /// /// Checks whether or not the id is currently in use by any of the SpriteRects in the texture. /// /// The id to check for /// True if the name is currently in use public bool IsInternalIdInUsed(GUID internalId) { return m_InternalIdsInUse.Contains(internalId); } public List GetSpriteRects() { return m_SpriteRects; } public bool Rename(string oldName, string newName, GUID fileId) { if (!IsNameUsed(oldName)) return false; if (IsNameUsed(newName)) return false; var index = m_SpriteNames.FindIndex(x => x == oldName); if (index >= 0) { m_SpriteNames.RemoveAt(index); m_SpriteFileIds.RemoveAt(index); } index = m_SpriteNames.FindIndex(x => x == newName); if (index >= 0) m_SpriteFileIds[index] = fileId; else AddNameFileIdPair(newName, fileId); m_NamesInUse.Remove(oldName); m_NamesInUse.Add(newName); return true; } void AddNameFileIdPair(string spriteName, GUID fileId) { m_SpriteNames.Add(spriteName); m_SpriteFileIds.Add(fileId); } bool UpdateIdInNameIdPair(string spriteName, GUID newFileId) { var index = m_SpriteNames.FindIndex(x => x == spriteName); if (index >= 0) { m_SpriteFileIds[index] = newFileId; return true; } return false; } public void ClearUnusedFileID() { m_SpriteNames.Clear(); m_SpriteFileIds.Clear(); foreach (var sprite in m_SpriteRects) { m_SpriteNames.Add(sprite.name); m_SpriteFileIds.Add(sprite.spriteID); } } void ISerializationCallbackReceiver.OnBeforeSerialize() {} void ISerializationCallbackReceiver.OnAfterDeserialize() { SetSpriteRects(m_SpriteRects); } } internal class OutlineSpriteRect : SpriteRect { public List outlines; public OutlineSpriteRect(SpriteRect rect) { this.name = rect.name; this.originalName = rect.originalName; this.pivot = rect.pivot; this.alignment = rect.alignment; this.border = rect.border; this.rect = rect.rect; this.spriteID = rect.spriteID; outlines = new List(); } } internal abstract partial class SpriteFrameModuleBase : SpriteEditorModuleBase { [Serializable] internal class SpriteFrameModulePersistentState : ScriptableSingleton { public PivotUnitMode pivotUnitMode = PivotUnitMode.Normalized; } protected SpriteRectModel m_RectsCache; protected ITextureDataProvider m_TextureDataProvider; protected ISpriteEditorDataProvider m_SpriteDataProvider; protected ISpriteNameFileIdDataProvider m_NameFileIdDataProvider; string m_ModuleName; internal enum PivotUnitMode { Normalized, Pixels } static PivotUnitMode pivotUnitMode { get => SpriteFrameModulePersistentState.instance.pivotUnitMode; set => SpriteFrameModulePersistentState.instance.pivotUnitMode = value; } protected SpriteFrameModuleBase(string name, ISpriteEditor sw, IEventSystem es, IUndoSystem us, IAssetDatabase ad) { spriteEditor = sw; eventSystem = es; undoSystem = us; assetDatabase = ad; m_ModuleName = name; } // implements ISpriteEditorModule public override void OnModuleActivate() { spriteImportMode = SpriteFrameModule.GetSpriteImportMode(spriteEditor.GetDataProvider()); m_TextureDataProvider = spriteEditor.GetDataProvider(); m_NameFileIdDataProvider = spriteEditor.GetDataProvider(); m_SpriteDataProvider = spriteEditor.GetDataProvider(); int width, height; m_TextureDataProvider.GetTextureActualWidthAndHeight(out width, out height); textureActualWidth = width; textureActualHeight = height; m_RectsCache = ScriptableObject.CreateInstance(); m_RectsCache.hideFlags = HideFlags.HideAndDontSave; var spriteList = m_SpriteDataProvider.GetSpriteRects().ToList(); m_RectsCache.SetSpriteRects(spriteList); spriteEditor.spriteRects = spriteList; if (m_NameFileIdDataProvider == null) m_NameFileIdDataProvider = new DefaultSpriteNameFileIdDataProvider(spriteList); var nameFileIdPairs = m_NameFileIdDataProvider.GetNameFileIdPairs(); m_RectsCache.SetNameFileIdPairs(nameFileIdPairs); if (spriteEditor.selectedSpriteRect != null) spriteEditor.selectedSpriteRect = m_RectsCache.spriteRects.FirstOrDefault(x => x.spriteID == spriteEditor.selectedSpriteRect.spriteID); AddMainUI(spriteEditor.GetMainVisualContainer()); undoSystem.RegisterUndoCallback(UndoCallback); } public override void OnModuleDeactivate() { if (m_RectsCache != null) { undoSystem.ClearUndo(m_RectsCache); ScriptableObject.DestroyImmediate(m_RectsCache); m_RectsCache = null; } undoSystem.UnregisterUndoCallback(UndoCallback); RemoveMainUI(spriteEditor.GetMainVisualContainer()); } public override bool ApplyRevert(bool apply) { if (apply) { var array = m_RectsCache != null ? m_RectsCache.spriteRects.ToArray() : null; m_SpriteDataProvider.SetSpriteRects(array); var spriteNames = m_RectsCache?.spriteNames; var spriteFileIds = m_RectsCache?.spriteFileIds; if (spriteNames != null && spriteFileIds != null) { var pairList = new List(spriteNames.Count); for (var i = 0; i < spriteNames.Count; ++i) pairList.Add(new SpriteNameFileIdPair(spriteNames[i], spriteFileIds[i])); m_NameFileIdDataProvider.SetNameFileIdPairs(pairList.ToArray()); } var outlineDataProvider = m_SpriteDataProvider.GetDataProvider(); var physicsDataProvider = m_SpriteDataProvider.GetDataProvider(); foreach (var rect in array) { if (rect is OutlineSpriteRect outlineRect) { if (outlineRect.outlines.Count > 0) { outlineDataProvider.SetOutlines(outlineRect.spriteID, outlineRect.outlines); physicsDataProvider.SetOutlines(outlineRect.spriteID, outlineRect.outlines); } } } if (m_RectsCache != null) undoSystem.ClearUndo(m_RectsCache); } else { if (m_RectsCache != null) { undoSystem.ClearUndo(m_RectsCache); var spriteList = m_SpriteDataProvider.GetSpriteRects().ToList(); m_RectsCache.SetSpriteRects(spriteList); var nameFileIdPairs = m_NameFileIdDataProvider.GetNameFileIdPairs(); m_RectsCache.SetNameFileIdPairs(nameFileIdPairs); spriteEditor.spriteRects = spriteList; if (spriteEditor.selectedSpriteRect != null) spriteEditor.selectedSpriteRect = m_RectsCache.spriteRects.FirstOrDefault(x => x.spriteID == spriteEditor.selectedSpriteRect.spriteID); } } return true; } public override string moduleName { get { return m_ModuleName; } } // injected interfaces protected IEventSystem eventSystem { get; private set; } protected IUndoSystem undoSystem { get; private set; } protected IAssetDatabase assetDatabase { get; private set; } protected SpriteRect selected { get { return spriteEditor.selectedSpriteRect; } set { spriteEditor.selectedSpriteRect = value; } } protected SpriteImportMode spriteImportMode { get; private set; } protected string spriteAssetPath { get { return assetDatabase.GetAssetPath(m_TextureDataProvider.texture); } } public bool hasSelected { get { return spriteEditor.selectedSpriteRect != null; } } public SpriteAlignment selectedSpriteAlignment { get { return selected.alignment; } } public Vector2 selectedSpritePivot { get { return selected.pivot; } } private Vector2 selectedSpritePivotInCurUnitMode { get { return pivotUnitMode == PivotUnitMode.Pixels ? ConvertFromNormalizedToRectSpace(selectedSpritePivot, selectedSpriteRect) : selectedSpritePivot; } } public int CurrentSelectedSpriteIndex() { if (m_RectsCache != null && selected != null) return m_RectsCache.FindIndex(x => x.spriteID == selected.spriteID); return -1; } public Vector4 selectedSpriteBorder { get { return ClampSpriteBorderToRect(selected.border, selected.rect); } set { undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Change Sprite Border"); spriteEditor.SetDataModified(); selected.border = ClampSpriteBorderToRect(value, selected.rect); } } public Rect selectedSpriteRect { get { return selected.rect; } set { undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Change Sprite rect"); spriteEditor.SetDataModified(); selected.rect = ClampSpriteRect(value, textureActualWidth, textureActualHeight); } } public string selectedSpriteName { get { return selected.name; } set { if (selected.name == value) return; if (m_RectsCache.IsNameUsed(value)) return; undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Change Sprite Name"); spriteEditor.SetDataModified(); string oldName = selected.name; string newName = InternalEditorUtility.RemoveInvalidCharsFromFileName(value, true); // These can only be changed in sprite multiple mode if (string.IsNullOrEmpty(selected.originalName) && (newName != oldName)) selected.originalName = oldName; // Is the name empty? if (string.IsNullOrEmpty(newName)) newName = oldName; // Did the rename succeed? if (m_RectsCache.Rename(oldName, newName, selected.spriteID)) selected.name = newName; } } public int spriteCount { get { return m_RectsCache.spriteRects.Count; } } public Vector4 GetSpriteBorderAt(int i) { return m_RectsCache.spriteRects[i].border; } public Rect GetSpriteRectAt(int i) { return m_RectsCache.spriteRects[i].rect; } public int textureActualWidth { get; private set; } public int textureActualHeight { get; private set; } public void SetSpritePivotAndAlignment(Vector2 pivot, SpriteAlignment alignment) { undoSystem.RegisterCompleteObjectUndo(m_RectsCache, "Change Sprite Pivot"); spriteEditor.SetDataModified(); selected.alignment = alignment; selected.pivot = SpriteEditorUtility.GetPivotValue(alignment, pivot); } public bool containsMultipleSprites { get { return spriteImportMode == SpriteImportMode.Multiple; } } protected void SnapPivotToSnapPoints(Vector2 pivot, out Vector2 outPivot, out SpriteAlignment outAlignment) { Rect rect = selectedSpriteRect; // Convert from normalized space to texture space Vector2 texturePos = new Vector2(rect.xMin + rect.width * pivot.x, rect.yMin + rect.height * pivot.y); Vector2[] snapPoints = GetSnapPointsArray(rect); // Snapping is now a firm action, it will always snap to one of the snapping points. SpriteAlignment snappedAlignment = SpriteAlignment.Custom; float nearestDistance = float.MaxValue; for (int alignment = 0; alignment < snapPoints.Length; alignment++) { float distance = (texturePos - snapPoints[alignment]).magnitude * m_Zoom; if (distance < nearestDistance) { snappedAlignment = (SpriteAlignment)alignment; nearestDistance = distance; } } outAlignment = snappedAlignment; outPivot = ConvertFromTextureToNormalizedSpace(snapPoints[(int)snappedAlignment], rect); } protected void SnapPivotToPixels(Vector2 pivot, out Vector2 outPivot, out SpriteAlignment outAlignment) { outAlignment = SpriteAlignment.Custom; Rect rect = selectedSpriteRect; float unitsPerPixelX = 1.0f / rect.width; float unitsPerPixelY = 1.0f / rect.height; outPivot.x = Mathf.Round(pivot.x / unitsPerPixelX) * unitsPerPixelX; outPivot.y = Mathf.Round(pivot.y / unitsPerPixelY) * unitsPerPixelY; } private void UndoCallback() { UIUndoCallback(); } protected static Rect ClampSpriteRect(Rect rect, float maxX, float maxY) { // Clamp rect to width height rect = FlipNegativeRect(rect); Rect newRect = new Rect(); newRect.xMin = Mathf.Clamp(rect.xMin, 0, maxX - 1); newRect.yMin = Mathf.Clamp(rect.yMin, 0, maxY - 1); newRect.xMax = Mathf.Clamp(rect.xMax, 1, maxX); newRect.yMax = Mathf.Clamp(rect.yMax, 1, maxY); // Prevent width and height to be 0 value after clamping. if (Mathf.RoundToInt(newRect.width) == 0) newRect.width = 1; if (Mathf.RoundToInt(newRect.height) == 0) newRect.height = 1; return SpriteEditorUtility.RoundedRect(newRect); } protected static Rect FlipNegativeRect(Rect rect) { Rect newRect = new Rect(); newRect.xMin = Mathf.Min(rect.xMin, rect.xMax); newRect.yMin = Mathf.Min(rect.yMin, rect.yMax); newRect.xMax = Mathf.Max(rect.xMin, rect.xMax); newRect.yMax = Mathf.Max(rect.yMin, rect.yMax); return newRect; } protected static Vector4 ClampSpriteBorderToRect(Vector4 border, Rect rect) { Rect flipRect = FlipNegativeRect(rect); float w = flipRect.width; float h = flipRect.height; Vector4 newBorder = new Vector4(); // Make sure borders are within the width/height and left < right and top < bottom newBorder.x = Mathf.RoundToInt(Mathf.Clamp(border.x, 0, Mathf.Min(Mathf.Abs(w - border.z), w))); // Left newBorder.z = Mathf.RoundToInt(Mathf.Clamp(border.z, 0, Mathf.Min(Mathf.Abs(w - newBorder.x), w))); // Right newBorder.y = Mathf.RoundToInt(Mathf.Clamp(border.y, 0, Mathf.Min(Mathf.Abs(h - border.w), h))); // Bottom newBorder.w = Mathf.RoundToInt(Mathf.Clamp(border.w, 0, Mathf.Min(Mathf.Abs(h - newBorder.y), h))); // Top return newBorder; } } }