Files
BABA_YAGA/Assets/D.A. Assets/Figma-Converter-for-Unity/Scripts/Runtime/Sprite/SpriteDownloader.cs
2026-05-13 23:02:02 +07:00

484 lines
17 KiB
C#

using DA_Assets.FCU.Model;
using DA_Assets.DAI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using DA_Assets.FCU.Extensions;
using UnityEngine;
using DA_Assets.Logging;
using DA_Assets.Extensions;
using System.Collections.Concurrent;
#if JSONNET_EXISTS
using Newtonsoft.Json;
#endif
namespace DA_Assets.FCU
{
[Serializable]
public class SpriteDownloader : MonoBehaviourLinkerRuntime<FigmaConverterUnity>
{
private int _maxConcurrentDownloads = 100;
private int _maxDownloadAttempts = 3;
private float _maxChunkSize = 24_000_000;
private int _errorLogSplitLimit = 50;
private int _logDelayMs = 1000;
private int _maxSpritesCount = 100;
private Dictionary<ImageFormatScaleKey, List<List<SpriteData>>> idFormatChunks;
public async Task CreateImageChunks(List<FObject> fobjects)
{
await Task.Run(() =>
{
List<FObject> needDownload = fobjects
.Where(x => x.Data.NeedDownload)
.GroupBy(x => x.Data.Hash)
.Select(g => g.First())
.ToList();
idFormatChunks = GetIdFormatChunks(needDownload);
});
}
public async Task DownloadSprites(List<FObject> fobjects)
{
DALogger.Log($"Download Sprites");
List<FObject> needDownload = fobjects
.Where(x => x.Data.NeedDownload)
.GroupBy(x => x.Data.Hash)
.Select(g => g.First())
.ToList();
if (needDownload.IsEmpty())
{
DALogger.Log($"DownloadSprites no need");
return;
}
int totalCount = needDownload.Count;
int downloadedCount = 0;
int lastLoggedCount = -1;
CancellationTokenSource downloadLogTokenSource = new CancellationTokenSource();
var missingSpriteLinks = await GetSpriteLinks(needDownload);
SemaphoreSlim semaphore = new SemaphoreSlim(_maxConcurrentDownloads);
List<Task> tasks = new List<Task>();
ConcurrentBag<FObject> failedObjects = new ConcurrentBag<FObject>();
_ = Task.Run(async () =>
{
while (!downloadLogTokenSource.Token.IsCancellationRequested)
{
if (lastLoggedCount != downloadedCount)
{
DALogger.Log(FcuLocKey.log_downloading_images.Localize(downloadedCount, totalCount));
lastLoggedCount = downloadedCount;
}
await Task.Delay(_logDelayMs, downloadLogTokenSource.Token);
}
}, downloadLogTokenSource.Token);
DALogger.Log(FcuLocKey.log_start_download_images.Localize());
foreach (var formatChunks in missingSpriteLinks)
{
foreach (var chunk in formatChunks.Value)
{
foreach (var idFormatLink in chunk)
{
await semaphore.WaitAsync();
Task task = Task.Run(async () =>
{
try
{
bool success = await DownloadSprite(idFormatLink, _maxDownloadAttempts);
if (!success)
{
failedObjects.Add(idFormatLink.FObject);
}
}
catch (Exception ex)
{
Debug.LogException(ex);
failedObjects.Add(idFormatLink.FObject);
}
finally
{
Interlocked.Increment(ref downloadedCount);
semaphore.Release();
}
});
tasks.Add(task);
}
}
}
await Task.WhenAll(tasks);
downloadLogTokenSource.Cancel();
DALogger.Log(FcuLocKey.log_downloading_images.Localize(downloadedCount, totalCount));
LogFailedDownloads(failedObjects);
}
public async Task<bool> DownloadSprite(SpriteData idFormatLink, int maxDownloadAttempts)
{
try
{
if (idFormatLink.Link.IsEmpty())
{
return false;
}
DARequest request = new DARequest
{
RequestType = RequestType.GetFile,
Query = idFormatLink.Link
};
DAResult<byte[]> result = default;
int attempts = 0;
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{
while (attempts < maxDownloadAttempts && result.Object == null)
{
attempts++;
result = await monoBeh.RequestSender.SendRequest<byte[]>(request);
}
}
switch (result.Error.Status)
{
case 909:
DALogger.LogError(FcuLocKey.log_ssl_error.Localize(result.Error.Message, result.Error.Status));
monoBeh.Events.OnImportFail?.Invoke(monoBeh);
monoBeh.AssetTools.StopImport(StopImportReason.Error);
break;
}
if (result.Object == null)
{
throw new NullReferenceException();
}
File.WriteAllBytes(idFormatLink.FObject.Data.SpritePath, result.Object);
return true;
}
catch (Exception)
{
return false;
}
}
public async Task<Dictionary<ImageFormatScaleKey, List<List<SpriteData>>>> GetSpriteLinks(List<FObject> fobjects)
{
var idFormatLinkChunks = new Dictionary<ImageFormatScaleKey, List<List<SpriteData>>>();
int totalLinks = fobjects.Count;
int obtainedLinks = 0;
int lastLoggedLinks = -1;
CancellationTokenSource linkLogTokenSource = new CancellationTokenSource();
_ = Task.Run(async () =>
{
while (!linkLogTokenSource.Token.IsCancellationRequested)
{
if (lastLoggedLinks != obtainedLinks)
{
DALogger.Log(FcuLocKey.log_getting_links.Localize(obtainedLinks, totalLinks));
lastLoggedLinks = obtainedLinks;
}
await Task.Delay(_logDelayMs, linkLogTokenSource.Token);
}
}, linkLogTokenSource.Token);
foreach (var idFormatChunk in idFormatChunks)
{
if (monoBeh.IsCancellationRequested(TokenType.Import))
break;
foreach (List<SpriteData> chunk in idFormatChunk.Value)
{
IEnumerable<string> ids = chunk.Select(x => x.FObject.Id);
DARequest request = RequestCreator.CreateImageLinksRequest(
monoBeh.Settings.MainSettings.ProjectUrl,
idFormatChunk.Key.ImageFormat.ToLower(),
idFormatChunk.Key.Scale,
ids,
monoBeh.RequestSender.GetRequestHeader(monoBeh.Authorizer.Token));
DAResult<FigmaImageRequest> result = await monoBeh.RequestSender.SendRequest<FigmaImageRequest>(request);
if (result.Success && result.Object.images.IsEmpty())
{
Debug.LogError("result.Success && result.Object.images.IsEmpty()");
}
else if (result.Success)
{
if (!idFormatLinkChunks.ContainsKey(idFormatChunk.Key))
{
idFormatLinkChunks[idFormatChunk.Key] = new List<List<SpriteData>>();
}
List<SpriteData> linkChunk = new List<SpriteData>();
foreach (var idFormat in chunk)
{
result.Object.images.TryGetValue(idFormat.FObject.Id, out string link);
if (monoBeh.Settings.MainSettings.Https == false)
{
link = link.Replace("https://", "http://");
}
linkChunk.Add(new SpriteData
{
FObject = idFormat.FObject,
Format = idFormat.Format,
Link = link ?? string.Empty
});
Interlocked.Increment(ref obtainedLinks);
}
idFormatLinkChunks[idFormatChunk.Key].Add(linkChunk);
}
else
{
Debug.LogError(result.Error.Message);
}
}
}
linkLogTokenSource.Cancel();
DALogger.Log(FcuLocKey.log_getting_links.Localize(obtainedLinks, totalLinks));
return idFormatLinkChunks;
}
public async Task SetScalesAndMaxSpriteSizes(List<FObject> fobjects)
{
await Task.Run(() =>
{
Parallel.ForEach(fobjects.Where(x => x.IsSprite()), fobject =>
{
fobject.Data.MaxSpriteSize = GetMaxSpriteSize(fobject);
fobject.Data.Scale = GetMaxAllowedScale(fobject.Data.MaxSpriteSize, monoBeh.Settings.ImageSpritesSettings.MaxSpriteSize, _maxChunkSize);
});
});
}
public Dictionary<ImageFormatScaleKey, List<List<SpriteData>>> GetIdFormatChunks(List<FObject> fobjects)
{
var formatChunks = new Dictionary<ImageFormatScaleKey, List<List<SpriteData>>>();
// Group FObjects by their ImageFormat and Scale
var fobjectsByFormatAndScale = new Dictionary<ImageFormatScaleKey, List<FObject>>();
foreach (FObject fobject in fobjects.Where(x => x.IsSprite()))
{
ImageFormatScaleKey key = new ImageFormatScaleKey
{
ImageFormat = fobject.Data.ImageFormat,
Scale = fobject.Data.Scale
};
if (!fobjectsByFormatAndScale.ContainsKey(key))
{
fobjectsByFormatAndScale[key] = new List<FObject>();
}
fobjectsByFormatAndScale[key].Add(fobject);
}
// For each group (ImageFormat + Scale) create chunks
foreach (var kvp in fobjectsByFormatAndScale)
{
ImageFormatScaleKey key = kvp.Key;
List<FObject> fobjectList = kvp.Value;
List<List<SpriteData>> chunks = new List<List<SpriteData>>();
List<SpriteData> currentChunk = new List<SpriteData>();
float currentChunkSize = 0;
foreach (FObject fobject in fobjectList)
{
float spriteSize = fobject.Data.MaxSpriteSize.x * fobject.Data.MaxSpriteSize.y * fobject.Data.Scale;
//Debug.LogError($"{spriteSize} | {fobject.Data.MaxSpriteSize.x * fobject.Data.Scale} | {fobject.Data.MaxSpriteSize.y * fobject.Data.Scale}");
// If adding this image exceeds the maximum chunk size, create a new chunk
if (currentChunkSize + spriteSize > _maxChunkSize || currentChunk.Count > _maxSpritesCount)
{
if (currentChunk.Count > 0)
{
chunks.Add(currentChunk);
}
currentChunk = new List<SpriteData>();
currentChunkSize = 0;
}
// Add the image to the current chunk
currentChunk.Add(new SpriteData
{
FObject = fobject,
Format = key.ImageFormat.ToString(),
Scale = key.Scale
});
currentChunkSize += spriteSize;
}
// Add the last chunk if it's not empty
if (currentChunk.Count > 0)
{
chunks.Add(currentChunk);
}
formatChunks[key] = chunks;
}
return formatChunks;
}
private void LogFailedDownloads(ConcurrentBag<FObject> failedObjects)
{
if (failedObjects.Count() > 0)
{
List<List<string>> comps = failedObjects.Select(x => x.Data.NameHierarchy).Split(_errorLogSplitLimit);
foreach (List<string> comp in comps)
{
string hierarchies = string.Join("\n", comp);
DALogger.LogError(
FcuLocKey.log_malformed_url.Localize(comp.Count, hierarchies));
}
}
}
private Vector2 GetMaxSpriteSize(FObject fobject)
{
float maxX;
float maxY;
bool hasBoundingSize = fobject.GetBoundingSize(out Vector2 bSize);
bool hasRenderSize = fobject.GetRenderSize(out Vector2 rSize);
if (hasBoundingSize && hasRenderSize)
{
maxX = Mathf.Max(bSize.x, rSize.x);
maxY = Mathf.Max(bSize.y, rSize.y);
}
else if (hasRenderSize)
{
maxX = rSize.x;
maxY = rSize.y;
}
else if (hasBoundingSize)
{
maxX = bSize.x;
maxY = bSize.y;
}
else
{
maxX = fobject.Size.x;
maxY = fobject.Size.y;
}
return new Vector2(maxX, maxY);
}
public float GetMaxAllowedScale(
Vector2 imageSize,
Vector2 maxSpriteSize,
float maxChunkSize,
float minScale = FcuConfig.IMAGE_SCALE_MIN,
float maxScale = FcuConfig.IMAGE_SCALE_MAX)
{
if (monoBeh.UsingSVG())
{
return 1f;
}
float effectiveMaxSpriteWidth = Mathf.Min(maxSpriteSize.x, monoBeh.Settings.ImageSpritesSettings.MaxSpriteSize.x);
float effectiveMaxSpriteHeight = Mathf.Min(maxSpriteSize.y, monoBeh.Settings.ImageSpritesSettings.MaxSpriteSize.y);
float scaleX = effectiveMaxSpriteWidth / imageSize.x;
float scaleY = effectiveMaxSpriteHeight / imageSize.y;
float maxScaleBySpriteSize = Mathf.Min(scaleX, scaleY);
maxScaleBySpriteSize = Mathf.Max(1f, maxScaleBySpriteSize);
float maxScaleByChunkSize = Mathf.Sqrt(maxChunkSize / (imageSize.x * imageSize.y));
float maxScaleAllowed = Mathf.Min(maxScaleBySpriteSize, maxScaleByChunkSize);
maxScaleAllowed = Mathf.Clamp(maxScaleAllowed, minScale, maxScale);
maxScaleAllowed = (float)Math.Round(maxScaleAllowed, 2);
//Debug.Log($"imageSize: {imageSize}, scaleX: {scaleX}, scaleY: {scaleY}, maxScaleBySpriteSize: {maxScaleBySpriteSize}, maxScaleAllowed: {maxScaleAllowed}");
return maxScaleAllowed;
}
public struct FigmaImageRequest
{
#if JSONNET_EXISTS
[JsonProperty("err")]
#endif
public string error;
#if JSONNET_EXISTS
[JsonProperty("images")]
#endif
// key = id, value = link
public Dictionary<string, string> images;
}
public struct ImageFormatScaleKey
{
public ImageFormat ImageFormat { get; set; }
public float Scale { get; set; }
public override bool Equals(object obj)
{
if (!(obj is ImageFormatScaleKey))
return false;
var other = (ImageFormatScaleKey)obj;
return ImageFormat == other.ImageFormat && Scale.Equals(other.Scale);
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + ImageFormat.GetHashCode();
hash = hash * 23 + Scale.GetHashCode();
return hash;
}
}
}
public struct SpriteData
{
public FObject FObject { get; set; }
public string Format { get; set; }
public string Link { get; set; }
public float Scale { get; set; }
}
}
}