com.wallstop-studios.unity-helpers 2.0.0-rc76 → 2.0.0-rc76.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -415,16 +415,8 @@
415
415
 
416
416
  private void CreateNewScriptableSpriteAtlas()
417
417
  {
418
- if (!Directory.Exists(NewAtlasConfigDirectory))
419
- {
420
- Directory.CreateDirectory(NewAtlasConfigDirectory);
421
- AssetDatabase.Refresh();
422
- }
423
-
418
+ DirectoryHelper.EnsureDirectoryExists(NewAtlasConfigDirectory);
424
419
  ScriptableSpriteAtlas newAtlasConfig = CreateInstance<ScriptableSpriteAtlas>();
425
- newAtlasConfig.outputSpriteAtlasDirectory = "Assets/GeneratedSpriteAtlases";
426
- newAtlasConfig.outputSpriteAtlasName = "NewlyCreatedAtlas";
427
-
428
420
  string path = AssetDatabase.GenerateUniqueAssetPath(
429
421
  Path.Combine(NewAtlasConfigDirectory, "NewScriptableSpriteAtlas.asset")
430
422
  );
@@ -180,6 +180,7 @@
180
180
  return;
181
181
  }
182
182
 
183
+ HashSet<string> processedFiles = new(StringComparer.OrdinalIgnoreCase);
183
184
  List<string> needReprocessing = new();
184
185
  string lastProcessed = null;
185
186
  try
@@ -214,6 +215,10 @@
214
215
  for (int i = 0; i < _filesToProcess.Count; ++i)
215
216
  {
216
217
  string file = _filesToProcess[i];
218
+ if (!processedFiles.Add(file))
219
+ {
220
+ continue;
221
+ }
217
222
  lastProcessed = file;
218
223
  EditorUtility.DisplayProgressBar(
219
224
  Name,
@@ -292,8 +297,10 @@
292
297
  return;
293
298
  }
294
299
 
295
- TextureImporter importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
296
- if (importer is not { textureType: TextureImporterType.Sprite })
300
+ if (
301
+ AssetImporter.GetAtPath(assetPath)
302
+ is not TextureImporter { textureType: TextureImporterType.Sprite } importer
303
+ )
297
304
  {
298
305
  return;
299
306
  }
@@ -0,0 +1,259 @@
1
+ namespace WallstopStudios.UnityHelpers.Editor.Sprites
2
+ {
3
+ #if UNITY_EDITOR
4
+ using System;
5
+ using System.Collections.Generic;
6
+ using System.IO;
7
+ using System.Linq;
8
+ using System.Text.RegularExpressions;
9
+ using System.Threading;
10
+ using System.Threading.Tasks;
11
+ using Core.Extension;
12
+ using CustomEditors;
13
+ using UnityEditor;
14
+ using UnityEngine;
15
+ using Object = UnityEngine.Object;
16
+
17
+ public class SpritePivotAdjuster : EditorWindow
18
+ {
19
+ private const float AlphaCutoff = 0.01f;
20
+
21
+ private static readonly string[] ImageFileExtensions =
22
+ {
23
+ ".png",
24
+ ".jpg",
25
+ ".jpeg",
26
+ ".bmp",
27
+ ".tga",
28
+ ".psd",
29
+ ".gif",
30
+ };
31
+
32
+ [SerializeField]
33
+ private List<Object> _directoryPaths = new();
34
+
35
+ [SerializeField]
36
+ private string _spriteNameRegex = ".*";
37
+
38
+ private SerializedObject _serializedObject;
39
+ private SerializedProperty _directoryPathsProperty;
40
+ private SerializedProperty _spriteNameRegexProperty;
41
+
42
+ private List<string> _filesToProcess;
43
+ private Regex _regex;
44
+
45
+ [MenuItem("Tools/Wallstop Studios/Unity Helpers/Sprite Pivot Adjuster")]
46
+ public static void ShowWindow()
47
+ {
48
+ GetWindow<SpritePivotAdjuster>("Sprite Pivot Adjuster");
49
+ }
50
+
51
+ private void OnEnable()
52
+ {
53
+ _serializedObject = new SerializedObject(this);
54
+ _directoryPathsProperty = _serializedObject.FindProperty(nameof(_directoryPaths));
55
+ _spriteNameRegexProperty = _serializedObject.FindProperty(nameof(_spriteNameRegex));
56
+ }
57
+
58
+ private void OnGUI()
59
+ {
60
+ EditorGUILayout.LabelField("Input directories", EditorStyles.boldLabel);
61
+ _serializedObject.Update();
62
+ PersistentDirectoryGUI.PathSelectorObjectArray(
63
+ _directoryPathsProperty,
64
+ nameof(SpriteCropper)
65
+ );
66
+
67
+ _serializedObject.ApplyModifiedProperties();
68
+ if (GUILayout.Button("Find Sprites To Process"))
69
+ {
70
+ _regex = !string.IsNullOrWhiteSpace(_spriteNameRegex)
71
+ ? new Regex(_spriteNameRegex)
72
+ : null;
73
+ FindFilesToProcess();
74
+ }
75
+
76
+ if (_filesToProcess is { Count: > 0 })
77
+ {
78
+ GUILayout.Label(
79
+ $"Found {_filesToProcess.Count} sprites to process.",
80
+ EditorStyles.boldLabel
81
+ );
82
+ if (GUILayout.Button("Adjust Pivots in Directory"))
83
+ {
84
+ AdjustPivotsInDirectory();
85
+ _filesToProcess = null;
86
+ }
87
+ }
88
+ else if (_filesToProcess != null)
89
+ {
90
+ GUILayout.Label(
91
+ "No sprites found to process in the selected directories.",
92
+ EditorStyles.label
93
+ );
94
+ }
95
+ }
96
+
97
+ private void FindFilesToProcess()
98
+ {
99
+ _filesToProcess = new List<string>();
100
+ if (_directoryPaths is not { Count: > 0 })
101
+ {
102
+ this.LogWarn($"No input directories selected.");
103
+ return;
104
+ }
105
+
106
+ foreach (Object maybeDirectory in _directoryPaths.Where(d => d != null))
107
+ {
108
+ string assetPath = AssetDatabase.GetAssetPath(maybeDirectory);
109
+ if (!AssetDatabase.IsValidFolder(assetPath))
110
+ {
111
+ this.LogWarn($"Skipping invalid path: {assetPath}");
112
+ continue;
113
+ }
114
+
115
+ IEnumerable<string> files = Directory
116
+ .GetFiles(assetPath, "*.*", SearchOption.AllDirectories)
117
+ .Where(file =>
118
+ Array.Exists(
119
+ ImageFileExtensions,
120
+ extension =>
121
+ file.EndsWith(extension, StringComparison.OrdinalIgnoreCase)
122
+ )
123
+ );
124
+
125
+ foreach (string file in files)
126
+ {
127
+ string fileName = Path.GetFileNameWithoutExtension(file);
128
+ if (_regex != null && !_regex.IsMatch(fileName))
129
+ {
130
+ continue;
131
+ }
132
+
133
+ _filesToProcess.Add(file);
134
+ }
135
+ }
136
+ Repaint();
137
+ }
138
+
139
+ private void AdjustPivotsInDirectory()
140
+ {
141
+ HashSet<string> processedFiles = new(StringComparer.OrdinalIgnoreCase);
142
+ List<TextureImporter> importers = new();
143
+ AssetDatabase.StartAssetEditing();
144
+ try
145
+ {
146
+ for (int i = 0; i < _filesToProcess.Count; i++)
147
+ {
148
+ string assetPath = _filesToProcess[i];
149
+ if (!processedFiles.Add(assetPath))
150
+ {
151
+ continue;
152
+ }
153
+ if (
154
+ AssetImporter.GetAtPath(assetPath)
155
+ is not TextureImporter { textureType: TextureImporterType.Sprite } importer
156
+ )
157
+ {
158
+ return;
159
+ }
160
+
161
+ Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
162
+
163
+ if (sprite == null)
164
+ {
165
+ continue;
166
+ }
167
+
168
+ EditorUtility.DisplayProgressBar(
169
+ "Processing sprites",
170
+ $"Processing {sprite.name}",
171
+ (float)i / _filesToProcess.Count
172
+ );
173
+
174
+ if (importer.spriteImportMode == SpriteImportMode.Single)
175
+ {
176
+ TextureImporterSettings settings = new();
177
+ importer.ReadTextureSettings(settings);
178
+
179
+ Vector2 newPivot = CalculateCenterOfMassPivot(sprite);
180
+ settings.spritePivot = newPivot;
181
+ settings.spriteAlignment = (int)SpriteAlignment.Custom;
182
+ importer.SetTextureSettings(settings);
183
+
184
+ importer.spritePivot = newPivot;
185
+ importer.SaveAndReimport();
186
+ importers.Add(importer);
187
+ }
188
+ }
189
+ }
190
+ finally
191
+ {
192
+ EditorUtility.ClearProgressBar();
193
+ AssetDatabase.StopAssetEditing();
194
+ foreach (TextureImporter importer in importers)
195
+ {
196
+ importer.SaveAndReimport();
197
+ }
198
+ AssetDatabase.SaveAssets();
199
+ AssetDatabase.Refresh();
200
+ }
201
+ }
202
+
203
+ private static Vector2 CalculateCenterOfMassPivot(Sprite sprite)
204
+ {
205
+ Texture2D texture = sprite.texture;
206
+ Rect spriteRect = sprite.rect;
207
+ int startX = Mathf.FloorToInt(spriteRect.x);
208
+ int startY = Mathf.FloorToInt(spriteRect.y);
209
+ int width = Mathf.FloorToInt(spriteRect.width);
210
+ int height = Mathf.FloorToInt(spriteRect.height);
211
+
212
+ long totalX = 0;
213
+ long totalY = 0;
214
+ long pixelCount = 0;
215
+
216
+ Color[] pixels = texture.GetPixels(startX, startY, width, height);
217
+
218
+ Parallel.For(
219
+ 0,
220
+ height,
221
+ () => (sumX: 0L, sumY: 0L, count: 0L),
222
+ (y, _, local) =>
223
+ {
224
+ int rowOffset = y * width;
225
+ for (int x = 0; x < width; ++x)
226
+ {
227
+ if (pixels[rowOffset + x].a > AlphaCutoff)
228
+ {
229
+ local.sumX += x;
230
+ local.sumY += y;
231
+ local.count++;
232
+ }
233
+ }
234
+ return local;
235
+ },
236
+ local =>
237
+ {
238
+ Interlocked.Add(ref totalX, local.sumX);
239
+ Interlocked.Add(ref totalY, local.sumY);
240
+ Interlocked.Add(ref pixelCount, local.count);
241
+ }
242
+ );
243
+
244
+ if (pixelCount == 0L)
245
+ {
246
+ return new Vector2(0.5f, 0.5f);
247
+ }
248
+
249
+ double averageX = (double)totalX / pixelCount;
250
+ double averageY = (double)totalY / pixelCount;
251
+
252
+ double pivotX = averageX / width;
253
+ double pivotY = averageY / height;
254
+
255
+ return new Vector2((float)pivotX, (float)pivotY);
256
+ }
257
+ }
258
+ #endif
259
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: eee3124006b84c8d9f0a22b5057c1bb7
3
+ timeCreated: 1748553682
@@ -34,7 +34,7 @@
34
34
  float yN = Math.Max(center.y, rectangle.y);
35
35
  float dX = xN - center.x;
36
36
  float dY = yN - center.y;
37
- return dX * dX + dY * dY <= _radiusSquared;
37
+ return (dX * dX + dY * dY) <= _radiusSquared;
38
38
  }
39
39
 
40
40
  public bool Overlaps(Bounds bounds)
@@ -53,6 +53,7 @@
53
53
  public int Count { get; private set; }
54
54
 
55
55
  private readonly List<T> _buffer;
56
+ private readonly List<T> _cache;
56
57
  private int _position;
57
58
 
58
59
  public T this[int index]
@@ -80,9 +81,13 @@
80
81
  _position = 0;
81
82
  Count = 0;
82
83
  _buffer = new List<T>();
83
- foreach (T item in initialContents ?? Enumerable.Empty<T>())
84
+ _cache = new List<T>();
85
+ if (initialContents != null)
84
86
  {
85
- Add(item);
87
+ foreach (T item in initialContents)
88
+ {
89
+ Add(item);
90
+ }
86
91
  }
87
92
  }
88
93
 
@@ -124,6 +129,64 @@
124
129
  }
125
130
  }
126
131
 
132
+ public bool Remove(T element, IEqualityComparer<T> comparer = null)
133
+ {
134
+ bool removed = false;
135
+ _cache.Clear();
136
+ comparer ??= EqualityComparer<T>.Default;
137
+ foreach (T item in this)
138
+ {
139
+ if (!removed && comparer.Equals(item, element))
140
+ {
141
+ removed = true;
142
+ continue;
143
+ }
144
+ _cache.Add(item);
145
+ }
146
+
147
+ if (!removed)
148
+ {
149
+ return false;
150
+ }
151
+
152
+ Clear();
153
+ foreach (T item in _cache)
154
+ {
155
+ Add(item);
156
+ }
157
+
158
+ return true;
159
+ }
160
+
161
+ public int RemoveAll(Func<T, bool> predicate)
162
+ {
163
+ int removedCount = 0;
164
+ foreach (T item in this)
165
+ {
166
+ if (predicate(item))
167
+ {
168
+ removedCount++;
169
+ }
170
+ else
171
+ {
172
+ _cache.Add(item);
173
+ }
174
+ }
175
+
176
+ if (removedCount == 0)
177
+ {
178
+ return 0;
179
+ }
180
+
181
+ Clear();
182
+ foreach (T item in _cache)
183
+ {
184
+ Add(item);
185
+ }
186
+
187
+ return removedCount;
188
+ }
189
+
127
190
  public void Clear()
128
191
  {
129
192
  Count = 0;
@@ -1,59 +1,20 @@
1
1
  namespace WallstopStudios.UnityHelpers.Core.DataStructure
2
2
  {
3
- using System;
4
3
  using System.Collections.Generic;
5
- using System.Linq;
6
4
  using UnityEngine;
7
5
 
8
6
  public interface ISpatialTree<T>
9
7
  {
10
8
  Bounds Boundary { get; }
11
- Func<T, Vector2> ElementTransformer { get; }
12
9
 
13
- IEnumerable<T> GetElementsInRange(Vector2 position, float range, float minimumRange = 0f)
14
- {
15
- Circle area = new(position, range);
16
- Func<T, Vector2> elementTransformer = ElementTransformer;
17
- if (0 < minimumRange)
18
- {
19
- Circle minimumArea = new(position, minimumRange);
20
- return GetElementsInBounds(
21
- new Bounds(
22
- new Vector3(position.x, position.y, 0f),
23
- new Vector3(range * 2f, range * 2f, 1f)
24
- )
25
- )
26
- .Where(element =>
27
- {
28
- Vector2 elementPosition = elementTransformer(element);
29
- if (!area.Contains(elementPosition))
30
- {
31
- return false;
32
- }
10
+ List<T> GetElementsInRange(
11
+ Vector2 position,
12
+ float range,
13
+ List<T> elementsInRange,
14
+ float minimumRange = 0f
15
+ );
33
16
 
34
- return !minimumArea.Contains(elementPosition);
35
- });
36
- }
37
-
38
- return GetElementsInBounds(
39
- new Bounds(
40
- new Vector3(position.x, position.y, 0f),
41
- new Vector3(range * 2f, range * 2f, 1f)
42
- )
43
- )
44
- .Where(element =>
45
- {
46
- Vector2 elementPosition = elementTransformer(element);
47
- if (!area.Contains(elementPosition))
48
- {
49
- return false;
50
- }
51
-
52
- return true;
53
- });
54
- }
55
-
56
- IEnumerable<T> GetElementsInBounds(Bounds bounds);
17
+ List<T> GetElementsInBounds(Bounds bounds, List<T> elementsInBounds);
57
18
 
58
19
  void GetApproximateNearestNeighbors(Vector2 position, int count, List<T> nearestNeighbors);
59
20
  }