A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections;
3using System.Linq;
4using System.Collections.Generic;
5using UnityEngine;
6using UnityEditorInternal;
7
8namespace UnityEditor.U2D.Sprites
9{
10 internal class SpriteRectModel : ScriptableObject, ISerializationCallbackReceiver
11 {
12 [Serializable]
13 struct StringGUID
14 {
15 [SerializeField]
16 string m_StringGUID;
17
18 public StringGUID(GUID guid)
19 {
20 m_StringGUID = guid.ToString();
21 }
22
23 public static implicit operator GUID(StringGUID d) => new GUID(d.m_StringGUID);
24 public static implicit operator StringGUID(GUID d) => new StringGUID(d);
25 }
26
27 [Serializable]
28 class StringGUIDList : IReadOnlyList<GUID>
29 {
30 [SerializeField]
31 List<StringGUID> m_List = new List<StringGUID>();
32
33 GUID IReadOnlyList<GUID>.this[int index]
34 {
35 get => m_List[index];
36 }
37
38 public StringGUID this[int index]
39 {
40 get => m_List[index];
41 set => m_List[index] = value;
42 }
43
44 IEnumerator<GUID> IEnumerable<GUID>.GetEnumerator()
45 {
46 // Not used for now
47 throw new NotImplementedException();
48 }
49
50 public int Count => m_List.Count;
51
52 public IEnumerator GetEnumerator()
53 {
54 return m_List.GetEnumerator();
55 }
56
57 public void Clear()
58 {
59 m_List.Clear();
60 }
61
62 public void RemoveAt(int i)
63 {
64 m_List.RemoveAt(i);
65 }
66
67 public void Add(StringGUID value)
68 {
69 m_List.Add(value);
70 }
71 }
72
73 /// <summary>
74 /// List of all SpriteRects
75 /// </summary>
76 [SerializeField] private List<SpriteRect> m_SpriteRects;
77 /// <summary>
78 /// List of all names in the Name-FileId Table
79 /// </summary>
80 [SerializeField] private List<string> m_SpriteNames;
81 /// <summary>
82 /// List of all FileIds in the Name-FileId Table
83 /// </summary>
84 [SerializeField] private StringGUIDList m_SpriteFileIds;
85 [SerializeField]
86 int m_Version = 0;
87 int m_CurrentVersion = 0;
88
89 /// <summary>
90 /// HashSet of all names currently in use by SpriteRects
91 /// </summary>
92 private HashSet<string> m_NamesInUse;
93 private HashSet<GUID> m_InternalIdsInUse;
94
95 public IReadOnlyList<SpriteRect> spriteRects => m_SpriteRects;
96 public IReadOnlyList<string> spriteNames => m_SpriteNames;
97 public IReadOnlyList<GUID> spriteFileIds => m_SpriteFileIds;
98
99 private SpriteRectModel()
100 {
101 m_SpriteNames = new List<string>();
102 m_SpriteFileIds = new StringGUIDList();
103 Clear();
104 }
105
106 public void RegisterUndo(IUndoSystem undoSystem, string undoMessage)
107 {
108 undoSystem.RegisterCompleteObjectUndo(this, undoMessage);
109 m_CurrentVersion++;
110 m_Version = m_CurrentVersion;
111 }
112
113 public bool VersionChanged(bool resetVersion)
114 {
115 var versionChanged = m_Version != m_CurrentVersion;
116 if (resetVersion)
117 m_CurrentVersion = m_Version;
118 return versionChanged;
119 }
120
121 public void SetSpriteRects(IList<SpriteRect> newSpriteRects)
122 {
123 m_SpriteRects.Clear();
124 m_SpriteRects.InsertRange(0, newSpriteRects);
125 m_NamesInUse = new HashSet<string>();
126 m_InternalIdsInUse = new HashSet<GUID>();
127 for (var i = 0; i < m_SpriteRects.Count; ++i)
128 {
129 m_NamesInUse.Add(m_SpriteRects[i].name);
130 m_InternalIdsInUse.Add(m_SpriteRects[i].spriteID);
131 }
132 }
133
134 public void SetNameFileIdPairs(IEnumerable<SpriteNameFileIdPair> pairs)
135 {
136 m_SpriteNames.Clear();
137 m_SpriteFileIds.Clear();
138
139 foreach (var pair in pairs)
140 AddNameFileIdPair(pair.name, pair.GetFileGUID());
141 }
142
143 public int FindIndex(Predicate<SpriteRect> match)
144 {
145 int i = 0;
146 foreach (var spriteRect in m_SpriteRects)
147 {
148 if (match.Invoke(spriteRect))
149 return i;
150 i++;
151 }
152 return -1;
153 }
154
155 public void Clear()
156 {
157 m_SpriteRects = new List<SpriteRect>();
158 m_NamesInUse = new HashSet<string>();
159 m_InternalIdsInUse = new HashSet<GUID>();
160 }
161
162 public bool Add(SpriteRect spriteRect, bool shouldReplaceInTable = false)
163 {
164 if (spriteRect.spriteID.Empty())
165 {
166 spriteRect.spriteID = GUID.Generate();
167 }
168 else
169 {
170 if (IsInternalIdInUsed(spriteRect.spriteID))
171 return false;
172 }
173
174 if (shouldReplaceInTable)
175 {
176 // replace id from sprite to file id table
177 if (!UpdateIdInNameIdPair(spriteRect.name, spriteRect.spriteID))
178 {
179 // add it into file id table if update wasn't successful i.e. it doesn't exist yet
180 AddNameFileIdPair(spriteRect.name, spriteRect.spriteID);
181 }
182 }
183 else
184 {
185 // Since we are not replacing the file id table,
186 // look for any existing id and set it to the SpriteRect
187 var index = m_SpriteNames.FindIndex(x => x == spriteRect.name);
188 if (index >= 0)
189 {
190 if (IsInternalIdInUsed(m_SpriteFileIds[index]))
191 return false;
192 spriteRect.spriteID = m_SpriteFileIds[index];
193 }
194 else
195 AddNameFileIdPair(spriteRect.name, spriteRect.spriteID);
196 }
197
198 m_SpriteRects.Add(spriteRect);
199 m_NamesInUse.Add(spriteRect.name);
200 m_InternalIdsInUse.Add(spriteRect.spriteID);
201 return true;
202 }
203
204 public void Remove(SpriteRect spriteRect)
205 {
206 m_SpriteRects.Remove(spriteRect);
207 m_NamesInUse.Remove(spriteRect.name);
208 m_InternalIdsInUse.Remove(spriteRect.spriteID);
209 }
210
211 /// <summary>
212 /// Checks whether or not the name is currently in use by any of the SpriteRects in the texture.
213 /// </summary>
214 /// <param name="rectName">The name to check for</param>
215 /// <returns>True if the name is currently in use</returns>
216 public bool IsNameUsed(string rectName)
217 {
218 return m_NamesInUse.Contains(rectName);
219 }
220
221 /// <summary>
222 /// Checks whether or not the id is currently in use by any of the SpriteRects in the texture.
223 /// </summary>
224 /// <param name="rectName">The id to check for</param>
225 /// <returns>True if the name is currently in use</returns>
226 public bool IsInternalIdInUsed(GUID internalId)
227 {
228 return m_InternalIdsInUse.Contains(internalId);
229 }
230
231 public List<SpriteRect> GetSpriteRects()
232 {
233 return m_SpriteRects;
234 }
235
236 public bool Rename(string oldName, string newName, GUID fileId)
237 {
238 if (!IsNameUsed(oldName))
239 return false;
240 if (IsNameUsed(newName))
241 return false;
242
243 var index = m_SpriteNames.FindIndex(x => x == oldName);
244 if (index >= 0)
245 {
246 m_SpriteNames.RemoveAt(index);
247 m_SpriteFileIds.RemoveAt(index);
248 }
249
250 index = m_SpriteNames.FindIndex(x => x == newName);
251 if (index >= 0)
252 m_SpriteFileIds[index] = fileId;
253 else
254 AddNameFileIdPair(newName, fileId);
255
256 m_NamesInUse.Remove(oldName);
257 m_NamesInUse.Add(newName);
258 return true;
259 }
260
261 void AddNameFileIdPair(string spriteName, GUID fileId)
262 {
263 m_SpriteNames.Add(spriteName);
264 m_SpriteFileIds.Add(fileId);
265 }
266
267 bool UpdateIdInNameIdPair(string spriteName, GUID newFileId)
268 {
269 var index = m_SpriteNames.FindIndex(x => x == spriteName);
270 if (index >= 0)
271 {
272 m_SpriteFileIds[index] = newFileId;
273 return true;
274 }
275
276 return false;
277 }
278
279 public void ClearUnusedFileID()
280 {
281 m_SpriteNames.Clear();
282 m_SpriteFileIds.Clear();
283 foreach (var sprite in m_SpriteRects)
284 {
285 m_SpriteNames.Add(sprite.name);
286 m_SpriteFileIds.Add(sprite.spriteID);
287 }
288 }
289
290 void ISerializationCallbackReceiver.OnBeforeSerialize()
291 {}
292
293 void ISerializationCallbackReceiver.OnAfterDeserialize()
294 {
295 SetSpriteRects(new List<SpriteRect>(m_SpriteRects));
296 }
297 }
298
299 internal class OutlineSpriteRect : SpriteRect
300 {
301 public List<Vector2[]> outlines;
302
303 public OutlineSpriteRect(SpriteRect rect)
304 {
305 this.name = rect.name;
306 this.originalName = rect.originalName;
307 this.pivot = rect.pivot;
308 this.alignment = rect.alignment;
309 this.border = rect.border;
310 this.rect = rect.rect;
311 this.spriteID = rect.spriteID;
312 outlines = new List<Vector2[]>();
313 }
314 }
315
316 internal abstract partial class SpriteFrameModuleBase : SpriteEditorModuleModeSupportBase
317 {
318 [Serializable]
319 internal class SpriteFrameModulePersistentState : ScriptableSingleton<SpriteFrameModulePersistentState>
320 {
321 public PivotUnitMode pivotUnitMode = PivotUnitMode.Normalized;
322 }
323
324 protected SpriteRectModel m_RectsCache;
325 protected ITextureDataProvider m_TextureDataProvider;
326 protected ISpriteEditorDataProvider m_SpriteDataProvider;
327 protected ISpriteNameFileIdDataProvider m_NameFileIdDataProvider;
328 string m_ModuleName;
329
330 internal enum PivotUnitMode
331 {
332 Normalized,
333 Pixels
334 }
335
336 static PivotUnitMode pivotUnitMode
337 {
338 get => SpriteFrameModulePersistentState.instance.pivotUnitMode;
339 set => SpriteFrameModulePersistentState.instance.pivotUnitMode = value;
340 }
341
342 protected SpriteFrameModuleBase(string name, ISpriteEditor sw, IEventSystem es, IUndoSystem us, IAssetDatabase ad)
343 {
344 spriteEditor = sw;
345 eventSystem = es;
346 undoSystem = us;
347 assetDatabase = ad;
348 m_ModuleName = name;
349 }
350
351 // implements ISpriteEditorModule
352
353 public override void OnModuleActivate()
354 {
355 m_SpriteDataProvider = GetDataProvider<ISpriteEditorDataProvider>();
356 spriteImportMode = SpriteFrameModule.GetSpriteImportMode(m_SpriteDataProvider);
357 m_TextureDataProvider = GetDataProvider<ITextureDataProvider>();
358 m_NameFileIdDataProvider = GetDataProvider<ISpriteNameFileIdDataProvider>();
359
360 m_TextureDataProvider.RegisterDataChangeCallback(OnTextureDataProviderChanged);
361 OnTextureDataProviderChanged(m_TextureDataProvider);
362 InitSpriteRectCache();
363
364 AddMainUI(spriteEditor.GetMainVisualContainer());
365 undoSystem.RegisterUndoCallback(UndoCallback);
366 foreach (var mode in modes)
367 {
368 mode.OnAddToModule(this);
369 }
370 }
371
372 void OnTextureDataProviderChanged(ITextureDataProvider obj)
373 {
374 int width, height;
375 m_TextureDataProvider.GetTextureActualWidthAndHeight(out width, out height);
376 textureActualWidth = width;
377 textureActualHeight = height;
378 }
379
380 void InitSpriteRectCache()
381 {
382 if(m_RectsCache != null)
383 ScriptableObject.DestroyImmediate(m_RectsCache);
384
385 var spriteList = m_SpriteDataProvider.GetSpriteRects().ToList();
386 if (m_NameFileIdDataProvider == null)
387 m_NameFileIdDataProvider = new DefaultSpriteNameFileIdDataProvider(spriteList);
388 var nameFileIdPairs = m_NameFileIdDataProvider.GetNameFileIdPairs();
389
390 m_RectsCache = ScriptableObject.CreateInstance<SpriteRectModel>();
391 m_RectsCache.hideFlags = HideFlags.HideAndDontSave;
392
393 m_RectsCache.SetSpriteRects(spriteList);
394 spriteEditor.spriteRects = spriteList;
395 m_RectsCache.SetNameFileIdPairs(nameFileIdPairs);
396
397 if (spriteEditor.selectedSpriteRect != null)
398 spriteEditor.selectedSpriteRect = m_RectsCache.spriteRects.FirstOrDefault(x => x.spriteID == spriteEditor.selectedSpriteRect.spriteID);
399 }
400
401 public override void OnModuleDeactivate()
402 {
403 foreach (var mode in modes)
404 {
405 mode.OnRemoveFromModule(this);
406 }
407 if (m_RectsCache != null)
408 {
409 undoSystem.ClearUndo(m_RectsCache);
410 ScriptableObject.DestroyImmediate(m_RectsCache);
411 spriteEditor.spriteRects = m_SpriteDataProvider.GetSpriteRects().ToList();
412 m_RectsCache = null;
413 }
414 m_TextureDataProvider.UnregisterDataChangeCallback(OnTextureDataProviderChanged);
415 undoSystem.UnregisterUndoCallback(UndoCallback);
416 RemoveMainUI(spriteEditor.GetMainVisualContainer());
417 }
418
419 public override bool ApplyRevert(bool apply)
420 {
421 if (apply)
422 {
423 var array = m_RectsCache != null ? m_RectsCache.spriteRects.ToArray() : null;
424 var spriteDataProvider = spriteEditor.GetDataProvider<ISpriteEditorDataProvider>();
425 var nameFileIdDataProvider = spriteEditor.GetDataProvider<ISpriteNameFileIdDataProvider>();
426 spriteDataProvider.SetSpriteRects(array);
427
428 var spriteNames = m_RectsCache?.spriteNames;
429 var spriteFileIds = m_RectsCache?.spriteFileIds;
430 if (spriteNames != null && spriteFileIds != null && nameFileIdDataProvider != null)
431 {
432 var pairList = new List<SpriteNameFileIdPair>(spriteNames.Count);
433 for (var i = 0; i < spriteNames.Count; ++i)
434 pairList.Add(new SpriteNameFileIdPair(spriteNames[i], spriteFileIds[i]));
435 nameFileIdDataProvider.SetNameFileIdPairs(pairList.ToArray());
436 }
437
438 var outlineDataProvider = spriteDataProvider.GetDataProvider<ISpriteOutlineDataProvider>();
439 var physicsDataProvider = spriteDataProvider.GetDataProvider<ISpritePhysicsOutlineDataProvider>();
440 foreach (var rect in array)
441 {
442 if (rect is OutlineSpriteRect outlineRect)
443 {
444 if (outlineRect.outlines.Count > 0)
445 {
446 outlineDataProvider.SetOutlines(outlineRect.spriteID, outlineRect.outlines);
447 physicsDataProvider.SetOutlines(outlineRect.spriteID, outlineRect.outlines);
448 }
449 }
450 }
451
452 if (m_RectsCache != null)
453 undoSystem.ClearUndo(m_RectsCache);
454 }
455 else
456 {
457 if (m_RectsCache != null)
458 {
459 undoSystem.ClearUndo(m_RectsCache);
460 InitSpriteRectCache();
461 }
462 }
463
464 return true;
465 }
466
467 public override string moduleName
468 {
469 get { return m_ModuleName; }
470 }
471
472 // injected interfaces
473 protected IEventSystem eventSystem
474 {
475 get;
476 private set;
477 }
478
479 protected IUndoSystem undoSystem
480 {
481 get;
482 private set;
483 }
484
485 protected IAssetDatabase assetDatabase
486 {
487 get;
488 private set;
489 }
490
491 protected SpriteRect selected
492 {
493 get { return spriteEditor.selectedSpriteRect; }
494 set { spriteEditor.selectedSpriteRect = value; }
495 }
496
497 protected SpriteImportMode spriteImportMode
498 {
499 get; private set;
500 }
501
502 protected string spriteAssetPath
503 {
504 get { return assetDatabase.GetAssetPath(m_SpriteDataProvider.targetObject); }
505 }
506
507 public bool hasSelected
508 {
509 get { return spriteEditor.selectedSpriteRect != null; }
510 }
511
512 public SpriteAlignment selectedSpriteAlignment
513 {
514 get { return selected.alignment; }
515 }
516
517 public Vector2 selectedSpritePivot
518 {
519 get { return selected.pivot; }
520 }
521
522 private Vector2 selectedSpritePivotInCurUnitMode
523 {
524 get
525 {
526 return pivotUnitMode == PivotUnitMode.Pixels
527 ? ConvertFromNormalizedToRectSpace(selectedSpritePivot, selectedSpriteRect_Rect)
528 : selectedSpritePivot;
529 }
530 }
531
532 public int CurrentSelectedSpriteIndex()
533 {
534 if (m_RectsCache != null && selected != null)
535 return m_RectsCache.FindIndex(x => x.spriteID == selected.spriteID);
536 return -1;
537 }
538
539 public Vector4 selectedSpriteBorder
540 {
541 get { return ClampSpriteBorderToRect(selected.border, selected.rect); }
542 set
543 {
544 m_RectsCache.RegisterUndo(undoSystem, "Change Sprite Border");
545 selected.border = ClampSpriteBorderToRect(value, selected.rect);
546 NotifyOnSpriteRectChanged();
547 spriteEditor.SetDataModified();
548 }
549 }
550
551 public Rect selectedSpriteRect_Rect
552 {
553 get { return selected.rect; }
554 set
555 {
556 m_RectsCache.RegisterUndo(undoSystem, "Change Sprite rect");
557 selected.rect = ClampSpriteRect(value, textureActualWidth, textureActualHeight);
558 NotifyOnSpriteRectChanged();
559 spriteEditor.SetDataModified();
560 }
561 }
562
563 public string selectedSpriteName
564 {
565 get { return selected.name; }
566 set
567 {
568 if (selected.name == value)
569 return;
570 if (m_RectsCache.IsNameUsed(value))
571 return;
572
573 string oldName = selected.name;
574 string newName = InternalEditorUtility.RemoveInvalidCharsFromFileName(value, true);
575
576 // These can only be changed in sprite multiple mode
577 if (string.IsNullOrEmpty(selected.originalName) && (newName != oldName))
578 selected.originalName = oldName;
579
580 // Is the name empty?
581 if (string.IsNullOrEmpty(newName))
582 newName = oldName;
583
584 // Did the rename succeed?
585 if (m_RectsCache.Rename(oldName, newName, selected.spriteID))
586 {
587 m_RectsCache.RegisterUndo(undoSystem, "Change Sprite Name");
588 selected.name = newName;
589 NotifyOnSpriteRectChanged();
590 spriteEditor.SetDataModified();
591 }
592 }
593 }
594
595 public int spriteCount
596 {
597 get { return m_RectsCache.spriteRects.Count; }
598 }
599
600 public Vector4 GetSpriteBorderAt(int i)
601 {
602 return m_RectsCache.spriteRects[i].border;
603 }
604
605 public Rect GetSpriteRectAt(int i)
606 {
607 return m_RectsCache.spriteRects[i].rect;
608 }
609
610 public int textureActualWidth { get; private set; }
611 public int textureActualHeight { get; private set; }
612
613 public void SetSpritePivotAndAlignment(Vector2 pivot, SpriteAlignment alignment)
614 {
615 m_RectsCache.RegisterUndo(undoSystem, "Change Sprite Pivot");
616 selected.alignment = alignment;
617 selected.pivot = SpriteEditorUtility.GetPivotValue(alignment, pivot);
618 NotifyOnSpriteRectChanged();
619 spriteEditor.SetDataModified();
620 }
621
622 public bool containsMultipleSprites
623 {
624 get { return spriteImportMode == SpriteImportMode.Multiple; }
625 }
626
627 protected void SnapPivotToSnapPoints(Vector2 pivot, out Vector2 outPivot, out SpriteAlignment outAlignment)
628 {
629 Rect rect = selectedSpriteRect_Rect;
630
631 // Convert from normalized space to texture space
632 Vector2 texturePos = new Vector2(rect.xMin + rect.width * pivot.x, rect.yMin + rect.height * pivot.y);
633
634 Vector2[] snapPoints = GetSnapPointsArray(rect);
635
636 // Snapping is now a firm action, it will always snap to one of the snapping points.
637 SpriteAlignment snappedAlignment = SpriteAlignment.Custom;
638 float nearestDistance = float.MaxValue;
639 for (int alignment = 0; alignment < snapPoints.Length; alignment++)
640 {
641 float distance = (texturePos - snapPoints[alignment]).magnitude * m_Zoom;
642 if (distance < nearestDistance)
643 {
644 snappedAlignment = (SpriteAlignment)alignment;
645 nearestDistance = distance;
646 }
647 }
648
649 outAlignment = snappedAlignment;
650 outPivot = ConvertFromTextureToNormalizedSpace(snapPoints[(int)snappedAlignment], rect);
651 }
652
653 protected void SnapPivotToPixels(Vector2 pivot, out Vector2 outPivot, out SpriteAlignment outAlignment)
654 {
655 outAlignment = SpriteAlignment.Custom;
656
657 Rect rect = selectedSpriteRect_Rect;
658 float unitsPerPixelX = 1.0f / rect.width;
659 float unitsPerPixelY = 1.0f / rect.height;
660 outPivot.x = Mathf.Round(pivot.x / unitsPerPixelX) * unitsPerPixelX;
661 outPivot.y = Mathf.Round(pivot.y / unitsPerPixelY) * unitsPerPixelY;
662 }
663
664 private void UndoCallback()
665 {
666 if(m_RectsCache.VersionChanged(true))
667 NotifyOnSpriteRectChanged();
668 UIUndoCallback();
669 }
670
671 protected static Rect ClampSpriteRect(Rect rect, float maxX, float maxY)
672 {
673 // Clamp rect to width height
674 rect = FlipNegativeRect(rect);
675 Rect newRect = new Rect();
676
677 newRect.xMin = Mathf.Clamp(rect.xMin, 0, maxX - 1);
678 newRect.yMin = Mathf.Clamp(rect.yMin, 0, maxY - 1);
679 newRect.xMax = Mathf.Clamp(rect.xMax, 1, maxX);
680 newRect.yMax = Mathf.Clamp(rect.yMax, 1, maxY);
681
682 // Prevent width and height to be 0 value after clamping.
683 if (Mathf.RoundToInt(newRect.width) == 0)
684 newRect.width = 1;
685 if (Mathf.RoundToInt(newRect.height) == 0)
686 newRect.height = 1;
687
688 return SpriteEditorUtility.RoundedRect(newRect);
689 }
690
691 protected static Rect FlipNegativeRect(Rect rect)
692 {
693 Rect newRect = new Rect();
694
695 newRect.xMin = Mathf.Min(rect.xMin, rect.xMax);
696 newRect.yMin = Mathf.Min(rect.yMin, rect.yMax);
697 newRect.xMax = Mathf.Max(rect.xMin, rect.xMax);
698 newRect.yMax = Mathf.Max(rect.yMin, rect.yMax);
699
700 return newRect;
701 }
702
703 protected static Vector4 ClampSpriteBorderToRect(Vector4 border, Rect rect)
704 {
705 Rect flipRect = FlipNegativeRect(rect);
706 float w = flipRect.width;
707 float h = flipRect.height;
708
709 Vector4 newBorder = new Vector4();
710
711 // Make sure borders are within the width/height and left < right and top < bottom
712 newBorder.x = Mathf.RoundToInt(Mathf.Clamp(border.x, 0, Mathf.Min(Mathf.Abs(w - border.z), w))); // Left
713 newBorder.z = Mathf.RoundToInt(Mathf.Clamp(border.z, 0, Mathf.Min(Mathf.Abs(w - newBorder.x), w))); // Right
714
715 newBorder.y = Mathf.RoundToInt(Mathf.Clamp(border.y, 0, Mathf.Min(Mathf.Abs(h - border.w), h))); // Bottom
716 newBorder.w = Mathf.RoundToInt(Mathf.Clamp(border.w, 0, Mathf.Min(Mathf.Abs(h - newBorder.y), h))); // Top
717
718 return newBorder;
719 }
720
721 protected virtual void NotifyOnSpriteRectChanged() { }
722 }
723}