A game about forced loneliness, made by TACStudios
1using System;
2using System.IO;
3using UnityEngine;
4using UnityEditorInternal;
5using System.Collections.Generic;
6using System.Text;
7using UnityTexture2D = UnityEngine.Texture2D;
8using UnityEditor.ShortcutManagement;
9using Object = UnityEngine.Object;
10
11namespace UnityEditor.U2D.Sprites
12{
13 [RequireSpriteDataProvider(typeof(ITextureDataProvider))]
14 internal partial class SpriteFrameModule : SpriteFrameModuleBase
15 {
16 public enum AutoSlicingMethod
17 {
18 DeleteAll = 0,
19 Smart = 1,
20 Safe = 2
21 }
22
23 private bool[] m_AlphaPixelCache;
24 SpriteFrameModuleContext m_SpriteFrameModuleContext;
25 public event Action onModuleDeactivated = () => { };
26 SpriteEditorModeBase m_CurrentMode = null;
27
28 private const float kOverlapTolerance = 0.00001f;
29 private StringBuilder m_SpriteNameStringBuilder;
30
31 private List<Rect> m_PotentialRects;
32 Texture2D m_TextureToSlice;
33 public List<Rect> potentialRects
34 {
35 set => m_PotentialRects = value;
36 }
37
38 public SpriteFrameModule(ISpriteEditor sw, IEventSystem es, IUndoSystem us, IAssetDatabase ad) :
39 base("Sprite Editor", sw, es, us, ad)
40 {}
41
42 public override void SetModuleModes(IEnumerable<Type> modes)
43 {
44 base.SetModuleModes(modes);
45 foreach (var mode in this.modes)
46 {
47 mode.RegisterOnModeRequestActivate(OnModuleExtensionActivate);
48 }
49 }
50
51 void OnModuleExtensionActivate(SpriteEditorModeBase activatingMode)
52 {
53 m_CurrentMode?.DeactivateMode();
54 m_CurrentMode = activatingMode;
55 var activated = m_CurrentMode?.ActivateMode();
56 // Mode did not activate
57 if(activated.HasValue && !activated.Value)
58 {
59 m_CurrentMode = null;
60 }
61
62 bool modeNull = m_CurrentMode == null;
63 if (modeNull)
64 {
65 spriteEditor.spriteRects = m_RectsCache.GetSpriteRects();
66 }
67 EnableInspector(modeNull);
68 }
69
70 public override bool ApplyRevert(bool apply)
71 {
72 var returnValue = base.ApplyRevert(apply);
73 var dataProviderApplied = new HashSet<Type>();
74 dataProviderApplied.Add(typeof(ISpriteEditorDataProvider));
75 foreach(var mode in modes)
76 {
77 returnValue |= mode.ApplyModeData(apply, dataProviderApplied);
78 }
79 if(apply)
80 m_SourceOverrideCallback?.Invoke(spriteAssetPath);
81 return returnValue;
82 }
83
84 class SpriteFrameModuleContext : IShortcutContext
85 {
86 SpriteFrameModule m_SpriteFrameModule;
87
88 public SpriteFrameModuleContext(SpriteFrameModule spriteFrame)
89 {
90 m_SpriteFrameModule = spriteFrame;
91 }
92
93 public bool active
94 {
95 get { return true; }
96 }
97 public SpriteFrameModule spriteFrameModule
98 {
99 get { return m_SpriteFrameModule; }
100 }
101 }
102
103 [FormerlyPrefKeyAs("Sprite Editor/Trim", "#t")]
104 [Shortcut("Sprite Editor/Trim", typeof(SpriteFrameModuleContext), KeyCode.T, ShortcutModifiers.Shift)]
105 static void ShortcutTrim(ShortcutArguments args)
106 {
107 if (!string.IsNullOrEmpty(GUI.GetNameOfFocusedControl()))
108 return;
109 var spriteFrameContext = (SpriteFrameModuleContext)args.context;
110 spriteFrameContext.spriteFrameModule.TrimAlpha();
111 spriteFrameContext.spriteFrameModule.spriteEditor.RequestRepaint();
112 }
113
114 public override void OnModuleActivate()
115 {
116 base.OnModuleActivate();
117 spriteEditor.enableMouseMoveEvent = true;
118 m_SpriteFrameModuleContext = new SpriteFrameModuleContext(this);
119 ShortcutIntegration.instance.contextManager.RegisterToolContext(m_SpriteFrameModuleContext);
120 m_SpriteNameStringBuilder = new StringBuilder(GetSpriteNamePrefix() + "_");
121 m_PotentialRects = null;
122 RegisterDataChangeCallback(OnTextureDataProviderDataChanged);
123 SignalModuleActivate();
124 }
125
126 public override void OnModuleDeactivate()
127 {
128 base.OnModuleDeactivate();
129
130 ShortcutIntegration.instance.contextManager.DeregisterToolContext(m_SpriteFrameModuleContext);
131 m_PotentialRects = null;
132 m_AlphaPixelCache = null;
133 m_CurrentMode?.DeactivateMode();
134 m_CurrentMode = null;
135 UnregisterDataChangeCallback(OnTextureDataProviderDataChanged);
136 CleanUpDataDataProviderOverride();
137 foreach (var mode in modes)
138 {
139 mode.UnregisterOnModeRequestActivate(OnModuleExtensionActivate);
140 }
141 if(m_TextureToSlice != null)
142 Object.DestroyImmediate(m_TextureToSlice);
143 modes.Clear();
144 onModuleDeactivated();
145 }
146
147 void OnTextureDataProviderDataChanged(ISpriteEditorDataProvider obj)
148 {
149 if(m_TextureToSlice != null)
150 Object.DestroyImmediate(m_TextureToSlice);
151 m_TextureToSlice = null;
152 }
153
154 public static SpriteImportMode GetSpriteImportMode(ISpriteEditorDataProvider dataProvider)
155 {
156 return dataProvider == null ? SpriteImportMode.None : dataProvider.spriteImportMode;
157 }
158
159 public override bool CanBeActivated()
160 {
161 return GetSpriteImportMode(spriteEditor.GetDataProvider<ISpriteEditorDataProvider>()) != SpriteImportMode.Polygon;
162 }
163
164 private string GenerateSpriteNameWithIndex(int startIndex)
165 {
166 int originalLength = m_SpriteNameStringBuilder.Length;
167 m_SpriteNameStringBuilder.Append(startIndex);
168 var name = m_SpriteNameStringBuilder.ToString();
169 m_SpriteNameStringBuilder.Length = originalLength;
170 return name;
171 }
172
173 private int AddSprite(Rect frame, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod, int originalCount, ref int nameIndex)
174 {
175 int outSprite = -1;
176 switch (slicingMethod)
177 {
178 case AutoSlicingMethod.DeleteAll:
179 {
180 while (outSprite == -1)
181 {
182 outSprite = AddSprite(frame, alignment, pivot, GenerateSpriteNameWithIndex(nameIndex++), Vector4.zero);
183 }
184 }
185 break;
186 case AutoSlicingMethod.Smart:
187 {
188 outSprite = GetExistingOverlappingSprite(frame, originalCount, true);
189 if (outSprite != -1)
190 {
191 var existingRect = m_RectsCache.spriteRects[outSprite];
192 existingRect.rect = frame;
193 existingRect.alignment = (SpriteAlignment)alignment;
194 existingRect.pivot = pivot;
195 }
196 else
197 {
198 while (outSprite == -1)
199 {
200 outSprite = AddSprite(frame, alignment, pivot, GenerateSpriteNameWithIndex(nameIndex++), Vector4.zero);
201 }
202 }
203 }
204 break;
205 case AutoSlicingMethod.Safe:
206 {
207 outSprite = GetExistingOverlappingSprite(frame, originalCount);
208 while (outSprite == -1)
209 {
210 outSprite = AddSprite(frame, alignment, pivot, GenerateSpriteNameWithIndex(nameIndex++), Vector4.zero);
211 }
212 }
213 break;
214 }
215 return outSprite;
216 }
217
218 private int GetExistingOverlappingSprite(Rect rect, int originalCount, bool bestFit = false)
219 {
220 var count = Math.Min(originalCount, m_RectsCache.spriteRects.Count);
221 int bestRect = -1;
222 float rectArea = rect.width * rect.height;
223 if (rectArea < kOverlapTolerance)
224 return bestRect;
225
226 float bestRatio = float.MaxValue;
227 float bestArea = float.MaxValue;
228 for (int i = 0; i < count; i++)
229 {
230 Rect existingRect = m_RectsCache.spriteRects[i].rect;
231 if (existingRect.Overlaps(rect))
232 {
233 if (bestFit)
234 {
235 float dx = Math.Min(rect.xMax, existingRect.xMax) - Math.Max(rect.xMin, existingRect.xMin);
236 float dy = Math.Min(rect.yMax, existingRect.yMax) - Math.Max(rect.yMin, existingRect.yMin);
237 float overlapArea = dx * dy;
238 float overlapRatio = Math.Abs((overlapArea / rectArea) - 1.0f);
239 float existingArea = existingRect.width * existingRect.height;
240 if (overlapRatio < bestRatio || (overlapRatio < kOverlapTolerance && existingArea < bestArea))
241 {
242 bestRatio = overlapRatio;
243 if (overlapRatio < kOverlapTolerance)
244 bestArea = existingArea;
245 bestRect = i;
246 }
247 }
248 else
249 {
250 bestRect = i;
251 break;
252 }
253 }
254 }
255 return bestRect;
256 }
257
258 private bool PixelHasAlpha(int x, int y, UnityTexture2D texture)
259 {
260 if (m_AlphaPixelCache == null)
261 {
262 m_AlphaPixelCache = new bool[texture.width * texture.height];
263 Color32[] pixels = texture.GetPixels32();
264
265 for (int i = 0; i < pixels.Length; i++)
266 m_AlphaPixelCache[i] = pixels[i].a != 0;
267 }
268 int index = y * (int)texture.width + x;
269 return m_AlphaPixelCache[index];
270 }
271
272 private int AddSprite(Rect rect, int alignment, Vector2 pivot, string name, Vector4 border)
273 {
274 if (m_RectsCache.IsNameUsed(name))
275 return -1;
276
277 SpriteRect spriteRect = new SpriteRect();
278 spriteRect.rect = rect;
279 spriteRect.alignment = (SpriteAlignment)alignment;
280 spriteRect.pivot = pivot;
281 spriteRect.name = name;
282 spriteRect.originalName = spriteRect.name;
283 spriteRect.border = border;
284
285 spriteRect.spriteID = GUID.Generate();
286
287 if (!m_RectsCache.Add(spriteRect))
288 return -1;
289 spriteEditor.SetDataModified();
290
291 return m_RectsCache.spriteRects.Count - 1;
292 }
293
294 private string GetSpriteNamePrefix()
295 {
296 return Path.GetFileNameWithoutExtension(spriteAssetPath);
297 }
298
299 public void DoAutomaticSlicing(int minimumSpriteSize, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod)
300 {
301 m_RectsCache.RegisterUndo(undoSystem, "Automatic Slicing");
302
303 if (slicingMethod == AutoSlicingMethod.DeleteAll)
304 m_RectsCache.Clear();
305
306 var textureToUse = GetTextureToSlice();
307 List<Rect> frames = new List<Rect>(InternalSpriteUtility.GenerateAutomaticSpriteRectangles((UnityTexture2D)textureToUse, minimumSpriteSize, 0));
308 if (frames.Count == 0)
309 frames.Add(new Rect(0, 0, textureToUse.width, textureToUse.height));
310
311 int index = 0;
312 int originalCount = m_RectsCache.spriteRects.Count;
313
314 foreach (Rect frame in frames)
315 AddSprite(frame, alignment, pivot, slicingMethod, originalCount, ref index);
316
317 if (slicingMethod == AutoSlicingMethod.DeleteAll)
318 m_RectsCache.ClearUnusedFileID();
319 selected = null;
320 NotifyOnSpriteRectChanged();
321 spriteEditor.SetDataModified();
322 Repaint();
323 }
324
325 UnityTexture2D GetTextureToSlice()
326 {
327 int width, height;
328 GetTextureActualWidthAndHeight(out width, out height);
329 var readableTexture = GetReadableTexture2D();
330 if (readableTexture == null || (readableTexture.width == width && readableTexture.height == height))
331 return readableTexture;
332
333 if (m_TextureToSlice == null)
334 {
335 // we want to slice based on the original texture slice. Upscale the imported texture
336 m_TextureToSlice = UnityEditor.SpriteUtility.CreateTemporaryDuplicate(readableTexture, width, height);
337 m_TextureToSlice.hideFlags = HideFlags.HideAndDontSave;
338 }
339
340 return m_TextureToSlice;
341 }
342
343 public IEnumerable<Rect> GetGridRects(Vector2 size, Vector2 offset, Vector2 padding, bool keepEmptyRects)
344 {
345 var textureToUse = GetTextureToSlice();
346 return InternalSpriteUtility.GenerateGridSpriteRectangles((UnityTexture2D)textureToUse, offset, size, padding, keepEmptyRects);
347 }
348
349 public void DoGridSlicing(Vector2 size, Vector2 offset, Vector2 padding, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod, bool keepEmptyRects = false)
350 {
351 var frames = GetGridRects(size, offset, padding, keepEmptyRects);
352
353 m_RectsCache.RegisterUndo(undoSystem, "Grid Slicing");
354 if (slicingMethod == AutoSlicingMethod.DeleteAll)
355 m_RectsCache.Clear();
356
357 int index = 0;
358 int originalCount = m_RectsCache.spriteRects.Count;
359 foreach (Rect frame in frames)
360 AddSprite(frame, alignment, pivot, slicingMethod, originalCount, ref index);
361
362 if (slicingMethod == AutoSlicingMethod.DeleteAll)
363 m_RectsCache.ClearUnusedFileID();
364 selected = null;
365 NotifyOnSpriteRectChanged();
366 spriteEditor.SetDataModified();
367 Repaint();
368 }
369
370 public IEnumerable<Rect> GetIsometricRects(Vector2 size, Vector2 offset, bool isAlternate, bool keepEmptyRects)
371 {
372 var textureToUse = GetTextureToSlice();
373 var gradient = (size.x / 2) / (size.y / 2);
374 bool isAlt = isAlternate;
375 float x = offset.x;
376 if (isAlt)
377 x += size.x / 2;
378 float y = textureToUse.height - offset.y;
379 while (y - size.y >= 0)
380 {
381 while (x + size.x <= textureToUse.width)
382 {
383 var rect = new Rect(x, y - size.y, size.x, size.y);
384 if (!keepEmptyRects)
385 {
386 int sx = (int)rect.x;
387 int sy = (int)rect.y;
388 int width = (int)size.x;
389 int odd = ((int)size.y) % 2;
390 int topY = ((int)size.y / 2) - 1;
391 int bottomY = topY + odd;
392 int totalPixels = 0;
393 int alphaPixels = 0;
394 {
395 for (int ry = 0; ry <= topY; ry++)
396 {
397 var pixelOffset = Mathf.CeilToInt(gradient * ry);
398 for (int rx = pixelOffset; rx < width - pixelOffset; ++rx)
399 {
400 if (PixelHasAlpha(sx + rx, sy + topY - ry, textureToUse))
401 alphaPixels++;
402 if (PixelHasAlpha(sx + rx, sy + bottomY + ry, textureToUse))
403 alphaPixels++;
404 totalPixels += 2;
405 }
406 }
407 }
408 if (odd > 0)
409 {
410 int ry = topY + 1;
411 for (int rx = 0; rx < size.x; ++rx)
412 {
413 if (PixelHasAlpha(sx + rx, sy + ry, textureToUse))
414 alphaPixels++;
415 totalPixels++;
416 }
417 }
418 if (totalPixels > 0 && ((float)alphaPixels) / totalPixels > 0.01f)
419 yield return rect;
420 }
421 else
422 yield return rect;
423 x += size.x;
424 }
425 isAlt = !isAlt;
426 x = offset.x;
427 if (isAlt)
428 x += size.x / 2;
429 y -= size.y / 2;
430 }
431 }
432
433 public void DoIsometricGridSlicing(Vector2 size, Vector2 offset, int alignment, Vector2 pivot, AutoSlicingMethod slicingMethod, bool keepEmptyRects = false, bool isAlternate = false)
434 {
435 var frames = GetIsometricRects(size, offset, isAlternate, keepEmptyRects);
436
437 List<Vector2[]> outlines = new List<Vector2[]>(4);
438 outlines.Add(new[] { new Vector2(0.0f, -size.y / 2)
439 , new Vector2(size.x / 2, 0.0f)
440 , new Vector2(0.0f, size.y / 2)
441 , new Vector2(-size.x / 2, 0.0f)});
442
443 m_RectsCache.RegisterUndo(undoSystem, "Isometric Grid Slicing");
444 if (slicingMethod == AutoSlicingMethod.DeleteAll)
445 m_RectsCache.Clear();
446
447 int index = 0;
448 var spriteRects = m_RectsCache.GetSpriteRects();
449 int originalCount = spriteRects.Count;
450 foreach (var frame in frames)
451 {
452 var spriteIndex = AddSprite(frame, alignment, pivot, slicingMethod, originalCount, ref index);
453 var outlineRect = new OutlineSpriteRect(spriteRects[spriteIndex]);
454 outlineRect.outlines = outlines;
455 spriteRects[spriteIndex] = outlineRect;
456 }
457 if (slicingMethod == AutoSlicingMethod.DeleteAll)
458 m_RectsCache.ClearUnusedFileID();
459 selected = null;
460 NotifyOnSpriteRectChanged();
461 spriteEditor.SetDataModified();
462 Repaint();
463 }
464
465 public void ScaleSpriteRect(Rect r)
466 {
467 if (selected != null)
468 {
469 m_RectsCache.RegisterUndo(undoSystem, "Scale sprite");
470 selected.rect = ClampSpriteRect(r, textureActualWidth, textureActualHeight);
471 selected.border = ClampSpriteBorderToRect(selected.border, selected.rect);
472 NotifyOnSpriteRectChanged();
473 spriteEditor.SetDataModified();
474 }
475 }
476
477 public void TrimAlpha()
478 {
479 var texture = GetTextureToSlice();
480 if (texture == null)
481 return;
482
483 Rect rect = selected.rect;
484
485 int xMin = (int)rect.xMax;
486 int xMax = (int)rect.xMin;
487 int yMin = (int)rect.yMax;
488 int yMax = (int)rect.yMin;
489
490 for (int y = (int)rect.yMin; y < (int)rect.yMax; y++)
491 {
492 for (int x = (int)rect.xMin; x < (int)rect.xMax; x++)
493 {
494 if (PixelHasAlpha(x, y, texture))
495 {
496 xMin = Mathf.Min(xMin, x);
497 xMax = Mathf.Max(xMax, x);
498 yMin = Mathf.Min(yMin, y);
499 yMax = Mathf.Max(yMax, y);
500 }
501 }
502 }
503 // Case 582309: Return an empty rectangle if no pixel has an alpha
504 if (xMin > xMax || yMin > yMax)
505 rect = new Rect(0, 0, 0, 0);
506 else
507 rect = new Rect(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1);
508
509 if (rect.width <= 0 && rect.height <= 0)
510 {
511 m_RectsCache.Remove(selected);
512 spriteEditor.SetDataModified();
513 selected = null;
514 }
515 else
516 {
517 rect = ClampSpriteRect(rect, texture.width, texture.height);
518 if (selected.rect != rect)
519 spriteEditor.SetDataModified();
520
521 selected.rect = rect;
522 PopulateSpriteFrameInspectorField();
523 }
524 }
525
526 public void DuplicateSprite()
527 {
528 if (selected != null)
529 {
530 m_RectsCache.RegisterUndo(undoSystem, "Duplicate sprite");
531 var index = 0;
532 var createdIndex = -1;
533 while (createdIndex == -1)
534 {
535 createdIndex = AddSprite(selected.rect, (int)selected.alignment, selected.pivot, GenerateSpriteNameWithIndex(index++), selected.border);
536 }
537 selected = m_RectsCache.spriteRects[createdIndex];
538 NotifyOnSpriteRectChanged();
539 }
540 }
541
542 public void CreateSprite(Rect rect)
543 {
544 rect = ClampSpriteRect(rect, textureActualWidth, textureActualHeight);
545 m_RectsCache.RegisterUndo(undoSystem, "Create sprite");
546 var index = 0;
547 var createdIndex = -1;
548 while (createdIndex == -1)
549 {
550 createdIndex = AddSprite(rect, 0, Vector2.zero, GenerateSpriteNameWithIndex(index++), Vector4.zero);
551 }
552 selected = m_RectsCache.spriteRects[createdIndex];
553 NotifyOnSpriteRectChanged();
554 }
555
556 public void DeleteSprite()
557 {
558 if (selected != null)
559 {
560 m_RectsCache.RegisterUndo(undoSystem, "Delete sprite");
561 m_RectsCache.Remove(selected);
562 selected = null;
563 NotifyOnSpriteRectChanged();
564 spriteEditor.SetDataModified();
565 }
566 }
567
568 public bool IsOnlyUsingDefaultNamedSpriteRects()
569 {
570 var onlyDefaultNames = true;
571 var names = m_RectsCache.spriteNames;
572 var defaultName = m_SpriteNameStringBuilder.ToString();
573
574 foreach (var name in names)
575 {
576 if (!name.Contains(defaultName))
577 {
578 onlyDefaultNames = false;
579 break;
580 }
581 }
582
583 return onlyDefaultNames;
584 }
585 }
586}