Finalize the cursed work

This commit is contained in:
Michal Pikulski
2025-11-27 00:58:19 +01:00
parent 5bab6d9596
commit 8bc4a88958
11 changed files with 381 additions and 530 deletions

View File

@@ -34,7 +34,7 @@ namespace Levels
/// </summary>
[SerializeField] private bool startUnlocked = false;
private SpriteRenderer iconRenderer;
[SerializeField] private SpriteRenderer iconRenderer;
private IInteractionSettings interactionSettings;
private bool isUnlocked;

View File

@@ -315,6 +315,7 @@ namespace Minigames.StatueDressup.Controllers
{
if (decoration == null || decoration.Data == null) continue;
RectTransform rectTransform = decoration.GetComponent<RectTransform>();
SpriteRenderer spriteRenderer = decoration.GetComponent<SpriteRenderer>();
DecorationPlacement placement = new DecorationPlacement
@@ -322,6 +323,7 @@ namespace Minigames.StatueDressup.Controllers
decorationId = decoration.Data.DecorationId,
localPosition = decoration.transform.localPosition,
localScale = decoration.transform.localScale,
sizeDelta = rectTransform != null ? rectTransform.sizeDelta : Vector2.zero,
rotation = decoration.transform.eulerAngles.z,
sortingOrder = spriteRenderer != null ? spriteRenderer.sortingOrder : 0
};
@@ -430,6 +432,16 @@ namespace Minigames.StatueDressup.Controllers
instance.transform.localScale = placement.localScale;
instance.transform.localEulerAngles = new Vector3(0, 0, placement.rotation);
// Apply saved sizeDelta if available (overrides AuthoredSize from InitializeAsPlaced)
if (placement.sizeDelta != Vector2.zero)
{
RectTransform rectTransform = instance.GetComponent<RectTransform>();
if (rectTransform != null)
{
rectTransform.sizeDelta = placement.sizeDelta;
}
}
// Set sorting order if has SpriteRenderer
SpriteRenderer spriteRenderer = instance.GetComponent<SpriteRenderer>();
if (spriteRenderer != null)

View File

@@ -13,6 +13,7 @@ namespace Minigames.StatueDressup.Data
public string decorationId; // Unique ID to load decoration
public Vector2 localPosition; // Position relative to statue
public Vector2 localScale; // Scale relative to statue
public Vector2 sizeDelta; // UI RectTransform size (for UI decorations)
public float rotation; // Z rotation in degrees
public int sortingOrder; // Sprite sorting order
}

View File

@@ -13,45 +13,39 @@ namespace Minigames.StatueDressup.Display
/// Place this component on a GameObject with a SpriteRenderer showing the statue.
/// On Start, loads all DecorationData via Addressables label, then spawns decorations from metadata.
/// </summary>
[RequireComponent(typeof(SpriteRenderer))]
public class StatueDecorationLoader : ManagedBehaviour
{
[Header("Settings")]
[Tooltip("Root GameObject for spawning decorations (clears only this, not statue children)")]
[SerializeField] private Transform decorationRoot;
[SerializeField] private SpriteRenderer statueSpriteRenderer;
[Tooltip("Load specific photo ID, or leave empty to load latest")]
[SerializeField] private string specificPhotoId = "";
[Tooltip("Apply pivot offset to position decorations relative to sprite's visual center instead of pivot point")]
[SerializeField] private bool applyPivotOffset = true;
[Header("Debug")]
[SerializeField] private bool showDebugInfo = true;
private SpriteRenderer _statueSpriteRenderer;
private Dictionary<string, DecorationData> _decorationDataDict;
private AsyncOperationHandle<IList<DecorationData>> _decorationDataHandle;
private AppleHills.Core.Settings.IStatueDressupSettings _settings;
private Dictionary<string, DecorationData> decorationDataDict;
private AsyncOperationHandle<IList<DecorationData>> decorationDataHandle;
private AppleHills.Core.Settings.IStatueDressupSettings settings;
internal override void OnManagedStart()
{
base.OnManagedStart();
_statueSpriteRenderer = GetComponent<SpriteRenderer>();
if (statueSpriteRenderer == null)
statueSpriteRenderer = GetComponent<SpriteRenderer>();
if (statueSpriteRenderer == null)
{
Logging.Error("[StatueDecorationLoader] No SpriteRenderer found! Please assign statueSpriteRenderer.");
return;
}
// Get settings
_settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IStatueDressupSettings>();
// Ensure decoration root exists
if (decorationRoot == null)
{
GameObject rootObj = new GameObject("DecorationRoot");
rootObj.transform.SetParent(transform, false);
decorationRoot = rootObj.transform;
if (showDebugInfo)
{
Logging.Debug("[StatueDecorationLoader] Created decoration root automatically");
}
}
settings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IStatueDressupSettings>();
// Start async loading via coroutine wrapper
StartCoroutine(LoadAndDisplayDecorationsCoroutine());
@@ -87,7 +81,7 @@ namespace Minigames.StatueDressup.Display
/// </summary>
private async System.Threading.Tasks.Task LoadDecorationDataAsync()
{
string label = _settings?.DecorationDataLabel;
string label = settings?.DecorationDataLabel;
if (string.IsNullOrEmpty(label))
{
@@ -107,12 +101,12 @@ namespace Minigames.StatueDressup.Display
progress => { /* Optional: could show loading bar */ }
);
_decorationDataDict = result.dictionary;
_decorationDataHandle = result.handle;
decorationDataDict = result.dictionary;
decorationDataHandle = result.handle;
if (showDebugInfo)
{
Logging.Debug($"[StatueDecorationLoader] Loaded {_decorationDataDict.Count} DecorationData assets");
Logging.Debug($"[StatueDecorationLoader] Loaded {decorationDataDict.Count} DecorationData assets");
}
}
@@ -122,7 +116,7 @@ namespace Minigames.StatueDressup.Display
public void LoadAndDisplayDecorations()
{
// Check if DecorationData is loaded
if (_decorationDataDict == null || _decorationDataDict.Count == 0)
if (decorationDataDict == null || decorationDataDict.Count == 0)
{
Logging.Warning("[StatueDecorationLoader] DecorationData not loaded yet. Cannot display decorations.");
return;
@@ -149,13 +143,13 @@ namespace Minigames.StatueDressup.Display
ClearDecorations();
// Calculate coordinate conversion factor if needed
float conversionFactor = CalculateCoordinateConversion(data);
float conversionFactor = CalculateCoordinateConversion(data, out Vector2 targetStatueWorldSize);
// Spawn each decoration synchronously (data already loaded)
int successCount = 0;
foreach (var placement in data.placements)
{
if (SpawnDecoration(placement, conversionFactor))
if (SpawnDecoration(placement, conversionFactor, data.sourceStatueSize, targetStatueWorldSize))
{
successCount++;
}
@@ -170,7 +164,7 @@ namespace Minigames.StatueDressup.Display
/// <summary>
/// Calculate coordinate conversion factor between source and target coordinate systems
/// </summary>
private float CalculateCoordinateConversion(StatueDecorationData data)
private float CalculateCoordinateConversion(StatueDecorationData data, out Vector2 targetStatueWorldSize)
{
// If source was world space and we're also world space, no conversion needed
if (data.coordinateSystem == CoordinateSystemType.WorldSpace)
@@ -179,29 +173,32 @@ namespace Minigames.StatueDressup.Display
{
Logging.Debug("[StatueDecorationLoader] No coordinate conversion needed (WorldSpace → WorldSpace)");
}
targetStatueWorldSize = Vector2.one;
return 1f;
}
// Source was UI RectTransform (pixels), target is WorldSpace (units)
// Need to convert from source statue pixel size to target statue world size
// Need to convert from source statue pixel size to target statue VISUAL world size
// Get target statue size (world units)
Vector2 targetStatueSize = Vector2.one;
if (_statueSpriteRenderer != null && _statueSpriteRenderer.sprite != null)
{
targetStatueSize = _statueSpriteRenderer.sprite.bounds.size;
}
// Get target statue VISUAL size (including transform scale)
Vector2 spriteNativeSize = statueSpriteRenderer.sprite.bounds.size;
Vector3 spriteScale = statueSpriteRenderer.transform.localScale;
targetStatueWorldSize = new Vector2(
spriteNativeSize.x * spriteScale.x,
spriteNativeSize.y * spriteScale.y
);
// Calculate conversion factor (target size / source size)
float conversionX = targetStatueSize.x / data.sourceStatueSize.x;
float conversionY = targetStatueSize.y / data.sourceStatueSize.y;
float conversionX = targetStatueWorldSize.x / data.sourceStatueSize.x;
float conversionY = targetStatueWorldSize.y / data.sourceStatueSize.y;
// Use average of X and Y for uniform scaling (or could use separate X/Y)
float conversionFactor = (conversionX + conversionY) / 2f;
if (showDebugInfo)
{
Logging.Debug($"[StatueDecorationLoader] Coordinate conversion: UI({data.sourceStatueSize}) → World({targetStatueSize}) = factor {conversionFactor:F3}");
Logging.Debug($"[StatueDecorationLoader] Coordinate conversion: UI({data.sourceStatueSize}px) → World({targetStatueWorldSize}units)");
Logging.Debug($"[StatueDecorationLoader] Sprite native: {spriteNativeSize}, scale: {spriteScale}, conversion factor: {conversionFactor:F3}");
}
return conversionFactor;
@@ -211,10 +208,10 @@ namespace Minigames.StatueDressup.Display
/// Spawn a single decoration from placement data
/// Looks up DecorationData from pre-loaded dictionary and applies coordinate conversion
/// </summary>
private bool SpawnDecoration(DecorationPlacement placement, float conversionFactor)
private bool SpawnDecoration(DecorationPlacement placement, float conversionFactor, Vector2 sourceStatueSize, Vector2 targetStatueWorldSize)
{
// Look up DecorationData from dictionary
if (!_decorationDataDict.TryGetValue(placement.decorationId, out DecorationData decorationData))
if (!decorationDataDict.TryGetValue(placement.decorationId, out DecorationData decorationData))
{
Logging.Warning($"[StatueDecorationLoader] DecorationData not found for ID: {placement.decorationId}");
return false;
@@ -229,47 +226,151 @@ namespace Minigames.StatueDressup.Display
return false;
}
// Create GameObject for decoration
// Create GameObject for decoration as child of statue sprite renderer
GameObject decorationObj = new GameObject($"Decoration_{placement.decorationId}");
decorationObj.transform.SetParent(decorationRoot, false); // false = keep local position
decorationObj.transform.SetParent(statueSpriteRenderer.transform, false);
// Add SpriteRenderer
SpriteRenderer spriteRenderer = decorationObj.AddComponent<SpriteRenderer>();
spriteRenderer.sprite = decorationSprite;
spriteRenderer.sortingLayerName = "Foreground";
spriteRenderer.sortingOrder = _statueSpriteRenderer.sortingOrder + placement.sortingOrder;
spriteRenderer.sortingOrder = statueSpriteRenderer.sortingOrder + placement.sortingOrder;
// Apply transform with coordinate conversion
Vector3 convertedPosition = placement.localPosition * conversionFactor;
decorationObj.transform.localPosition = convertedPosition;
decorationObj.transform.localScale = placement.localScale;
// ===== POSITION CALCULATION =====
// Calculate pivot offset - decorations should be positioned relative to sprite's visual center, not pivot
Sprite statueSprite = statueSpriteRenderer.sprite;
Bounds spriteBounds = statueSprite.bounds;
// Sprite.bounds.center gives us the offset from pivot to visual center in local space
Vector2 pivotToCenterOffset = spriteBounds.center;
// Convert UI pixel position to world space position
Vector3 worldPosition = placement.localPosition * conversionFactor;
// Convert world position to local position (accounting for parent scale)
Vector3 parentScale = statueSpriteRenderer.transform.localScale;
Vector3 localPosition = new Vector3(
worldPosition.x / parentScale.x,
worldPosition.y / parentScale.y,
worldPosition.z
);
// Apply pivot offset IN LOCAL SPACE (after conversion from world to local)
// This ensures both values are in the same coordinate system
if (applyPivotOffset)
{
localPosition += new Vector3(pivotToCenterOffset.x, pivotToCenterOffset.y, 0f);
if (showDebugInfo)
{
Logging.Debug($"[StatueDecorationLoader] Pivot offset APPLIED: {pivotToCenterOffset} (bounds center: {spriteBounds.center}, pivot normalized: {statueSprite.pivot / statueSprite.rect.size})");
}
}
else if (showDebugInfo)
{
Logging.Debug($"[StatueDecorationLoader] Pivot offset SKIPPED (applyPivotOffset = false)");
}
// ===== SCALE CALCULATION =====
Vector3 localScale = placement.localScale;
if (placement.sizeDelta != Vector2.zero)
{
// Calculate relative size in UI (decoration size / statue size)
Vector2 relativeSizeUI = new Vector2(
placement.sizeDelta.x / sourceStatueSize.x,
placement.sizeDelta.y / sourceStatueSize.y
);
// Calculate target world size for decoration (relative size × statue world size)
Vector2 targetDecorationWorldSize = new Vector2(
relativeSizeUI.x * targetStatueWorldSize.x,
relativeSizeUI.y * targetStatueWorldSize.y
);
// Get decoration sprite's native world size
Vector2 decorationNativeWorldSize = decorationSprite.bounds.size;
// Calculate world scale needed to achieve target size
Vector2 worldScaleNeeded = new Vector2(
targetDecorationWorldSize.x / decorationNativeWorldSize.x,
targetDecorationWorldSize.y / decorationNativeWorldSize.y
);
// Apply saved scale multiplier
worldScaleNeeded = new Vector2(
worldScaleNeeded.x * placement.localScale.x,
worldScaleNeeded.y * placement.localScale.y
);
// Convert world scale to local scale (accounting for parent scale)
localScale = new Vector3(
worldScaleNeeded.x / parentScale.x,
worldScaleNeeded.y / parentScale.y,
1f
);
if (showDebugInfo)
{
Logging.Debug($"[StatueDecorationLoader] Size calc: UI sizeDelta={placement.sizeDelta}, relativeUI={relativeSizeUI}");
Logging.Debug($"[StatueDecorationLoader] Target world size={targetDecorationWorldSize}, native={decorationNativeWorldSize}, worldScale={worldScaleNeeded}, localScale={localScale}");
}
}
else
{
// No sizeDelta saved, just apply saved scale divided by parent scale
localScale = new Vector3(
placement.localScale.x / parentScale.x,
placement.localScale.y / parentScale.y,
1f
);
if (showDebugInfo)
{
Logging.Debug($"[StatueDecorationLoader] No sizeDelta saved, using scale directly (compensated): {localScale}");
}
}
// Apply transform
decorationObj.transform.localPosition = localPosition;
decorationObj.transform.localScale = localScale;
decorationObj.transform.localEulerAngles = new Vector3(0, 0, placement.rotation);
if (showDebugInfo)
{
Logging.Debug($"[StatueDecorationLoader] Spawned: {placement.decorationId} at {convertedPosition} (original: {placement.localPosition}, factor: {conversionFactor:F3})");
Logging.Debug($"[StatueDecorationLoader] Spawned: {placement.decorationId}");
Logging.Debug($"[StatueDecorationLoader] Position: UI={placement.localPosition} → world={worldPosition} → local={localPosition}");
Logging.Debug($"[StatueDecorationLoader] Parent scale: {parentScale}");
}
return true;
}
/// <summary>
/// Clear all existing decorations from decorationRoot
/// Clear all existing decorations (children of statue sprite renderer that are decorations)
/// </summary>
public void ClearDecorations()
{
if (decorationRoot == null) return;
if (statueSpriteRenderer == null) return;
// Remove all children from decoration root only
for (int i = decorationRoot.childCount - 1; i >= 0; i--)
Transform parent = statueSpriteRenderer.transform;
// Remove all children that are decorations (identified by name pattern)
for (int i = parent.childCount - 1; i >= 0; i--)
{
if (Application.isPlaying)
GameObject child = parent.GetChild(i).gameObject;
// Only destroy objects that look like decorations (by name pattern)
if (child.name.StartsWith("Decoration_"))
{
Destroy(decorationRoot.GetChild(i).gameObject);
}
else
{
DestroyImmediate(decorationRoot.GetChild(i).gameObject);
if (Application.isPlaying)
{
Destroy(child);
}
else
{
DestroyImmediate(child);
}
}
}
}
@@ -280,7 +381,7 @@ namespace Minigames.StatueDressup.Display
private void OnDestroy()
{
// Release DecorationData handle
AddressablesUtility.ReleaseHandle(_decorationDataHandle);
AddressablesUtility.ReleaseHandle(decorationDataHandle);
}
/// <summary>

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: fe03648f638e4872abafaf49234a3f55
timeCreated: 1763745490

View File

@@ -1,393 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using Core;
using UnityEngine;
using SDev;
using ScreenUtils = Utils.ScreenSpaceUtility;
namespace Minigames.StatueDressup.Utils
{
/// <summary>
/// Manages statue photo capture, storage, and retrieval.
/// Supports area-limited screenshots, persistent file storage (mobile + PC),
/// and optimized gallery loading with pagination.
/// </summary>
public static class StatuePhotoManager
{
private const string PhotoFolder = "StatuePhotos";
private const string PhotoPrefix = "MrCementStatue_";
private const string MetadataKeyPrefix = "StatuePhoto_Meta_";
private const string PhotoIndexKey = "StatuePhoto_Index";
/// <summary>
/// Photo metadata stored in PlayerPrefs
/// </summary>
[Serializable]
public class PhotoMetadata
{
public string photoId;
public string timestamp;
public int decorationCount;
public long fileSizeBytes;
}
#region Capture
/// <summary>
/// Capture a specific area of the screen using Screenshot Helper
/// </summary>
/// <param name="captureArea">RectTransform defining the capture region</param>
/// <param name="onComplete">Callback with captured Texture2D</param>
/// <param name="mainCamera">Camera used for coordinate conversion (null = Camera.main)</param>
/// <param name="clampToScreenBounds">If true, clamps capture area to visible screen bounds</param>
public static void CaptureAreaPhoto(
RectTransform captureArea,
Action<Texture2D> onComplete,
Camera mainCamera = null,
bool clampToScreenBounds = true)
{
if (captureArea == null)
{
Logging.Error("[StatuePhotoManager] CaptureArea RectTransform is null!");
onComplete?.Invoke(null);
return;
}
if (mainCamera == null) mainCamera = Camera.main;
// Use ScreenSpaceUtility to convert RectTransform to screen rect
// returnCenterPosition = true because ScreenshotHelper expects center position
Rect screenRect = ScreenUtils.RectTransformToScreenRect(
captureArea,
mainCamera,
clampToScreenBounds,
returnCenterPosition: true
);
Logging.Debug($"[StatuePhotoManager] Capturing area: pos={screenRect.position}, size={screenRect.size}");
// Use Screenshot Helper's Capture method
ScreenshotHelper.Instance.Capture(
screenRect.position,
screenRect.size,
(texture) =>
{
if (texture != null)
{
Logging.Debug($"[StatuePhotoManager] Photo captured: {texture.width}x{texture.height}");
onComplete?.Invoke(texture);
}
else
{
Logging.Error("[StatuePhotoManager] Screenshot Helper returned null texture!");
onComplete?.Invoke(null);
}
}
);
}
#endregion
#region Save/Load
/// <summary>
/// Save photo to persistent storage with metadata
/// </summary>
/// <param name="photo">Texture2D to save</param>
/// <param name="decorationCount">Number of decorations placed (for metadata)</param>
/// <returns>Photo ID if successful, null if failed</returns>
public static string SavePhoto(Texture2D photo, int decorationCount = 0)
{
if (photo == null)
{
Logging.Error("[StatuePhotoManager] Cannot save null photo");
return null;
}
try
{
// Generate unique photo ID
string photoId = $"{PhotoPrefix}{DateTime.Now.Ticks}";
// Save texture using FileSaveUtil
string savedPath = FileSaveUtil.Instance.SaveTextureAsPNG(
photo,
FileSaveUtil.AppPath.PersistentDataPath,
PhotoFolder,
photoId
);
// Calculate file size
FileInfo fileInfo = new FileInfo(savedPath);
long fileSize = fileInfo.Exists ? fileInfo.Length : 0;
// Save metadata
PhotoMetadata metadata = new PhotoMetadata
{
photoId = photoId,
timestamp = DateTime.Now.ToString("o"),
decorationCount = decorationCount,
fileSizeBytes = fileSize
};
SaveMetadata(metadata);
AddToPhotoIndex(photoId);
Logging.Debug($"[StatuePhotoManager] Photo saved: {savedPath} ({fileSize} bytes)");
return photoId;
}
catch (Exception e)
{
Logging.Error($"[StatuePhotoManager] Failed to save photo: {e.Message}");
return null;
}
}
/// <summary>
/// Load photo texture from storage
/// </summary>
public static Texture2D LoadPhoto(string photoId)
{
if (string.IsNullOrEmpty(photoId))
{
Logging.Warning("[StatuePhotoManager] PhotoId is null or empty");
return null;
}
try
{
string filePath = GetPhotoFilePath(photoId);
if (!File.Exists(filePath))
{
Logging.Warning($"[StatuePhotoManager] Photo not found: {filePath}");
return null;
}
byte[] fileData = File.ReadAllBytes(filePath);
Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
if (texture.LoadImage(fileData))
{
Logging.Debug($"[StatuePhotoManager] Photo loaded: {photoId} ({texture.width}x{texture.height})");
return texture;
}
else
{
Logging.Error($"[StatuePhotoManager] Failed to decode image: {photoId}");
UnityEngine.Object.Destroy(texture);
return null;
}
}
catch (Exception e)
{
Logging.Error($"[StatuePhotoManager] Failed to load photo {photoId}: {e.Message}");
return null;
}
}
/// <summary>
/// Delete photo and its metadata
/// </summary>
public static bool DeletePhoto(string photoId)
{
if (string.IsNullOrEmpty(photoId)) return false;
try
{
string filePath = GetPhotoFilePath(photoId);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
DeleteMetadata(photoId);
RemoveFromPhotoIndex(photoId);
Logging.Debug($"[StatuePhotoManager] Photo deleted: {photoId}");
return true;
}
catch (Exception e)
{
Logging.Error($"[StatuePhotoManager] Failed to delete photo {photoId}: {e.Message}");
return false;
}
}
#endregion
#region Gallery Support
/// <summary>
/// Get all photo IDs sorted by timestamp (newest first)
/// </summary>
public static List<string> GetAllPhotoIds()
{
string indexJson = PlayerPrefs.GetString(PhotoIndexKey, "[]");
List<string> photoIds = JsonUtility.FromJson<PhotoIdList>(WrapJsonArray(indexJson))?.ids ?? new List<string>();
// Sort by timestamp descending (newest first)
photoIds.Sort((a, b) =>
{
PhotoMetadata metaA = LoadMetadata(a);
PhotoMetadata metaB = LoadMetadata(b);
DateTime dateA = DateTime.Parse(metaA?.timestamp ?? DateTime.MinValue.ToString("o"));
DateTime dateB = DateTime.Parse(metaB?.timestamp ?? DateTime.MinValue.ToString("o"));
return dateB.CompareTo(dateA);
});
return photoIds;
}
/// <summary>
/// Get paginated photo IDs for optimized gallery loading
/// </summary>
/// <param name="page">Page number (0-indexed)</param>
/// <param name="pageSize">Number of items per page</param>
public static List<string> GetPhotoIdsPage(int page, int pageSize)
{
List<string> allIds = GetAllPhotoIds();
int startIndex = page * pageSize;
if (startIndex >= allIds.Count) return new List<string>();
int count = Mathf.Min(pageSize, allIds.Count - startIndex);
return allIds.GetRange(startIndex, count);
}
/// <summary>
/// Get total number of saved photos
/// </summary>
public static int GetPhotoCount()
{
return GetAllPhotoIds().Count;
}
/// <summary>
/// Get latest photo ID (most recent)
/// </summary>
public static string GetLatestPhotoId()
{
List<string> allIds = GetAllPhotoIds();
return allIds.Count > 0 ? allIds[0] : null;
}
/// <summary>
/// Load photo metadata
/// </summary>
public static PhotoMetadata GetPhotoMetadata(string photoId)
{
return LoadMetadata(photoId);
}
/// <summary>
/// Create thumbnail from full-size photo (for gallery preview)
/// </summary>
public static Texture2D CreateThumbnail(Texture2D fullSizePhoto, int maxSize = 256)
{
if (fullSizePhoto == null) return null;
int width = fullSizePhoto.width;
int height = fullSizePhoto.height;
// Calculate thumbnail size maintaining aspect ratio
float scale = Mathf.Min((float)maxSize / width, (float)maxSize / height);
int thumbWidth = Mathf.RoundToInt(width * scale);
int thumbHeight = Mathf.RoundToInt(height * scale);
// Create thumbnail using bilinear filtering
RenderTexture rt = RenderTexture.GetTemporary(thumbWidth, thumbHeight, 0, RenderTextureFormat.ARGB32);
RenderTexture.active = rt;
Graphics.Blit(fullSizePhoto, rt);
Texture2D thumbnail = new Texture2D(thumbWidth, thumbHeight, TextureFormat.RGB24, false);
thumbnail.ReadPixels(new Rect(0, 0, thumbWidth, thumbHeight), 0, 0);
thumbnail.Apply();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
return thumbnail;
}
#endregion
#region Internal Helpers
public static string GetPhotoDirectory()
{
return Path.Combine(Application.persistentDataPath, PhotoFolder);
}
private static string GetPhotoFilePath(string photoId)
{
return Path.Combine(GetPhotoDirectory(), $"{photoId}.png");
}
private static void SaveMetadata(PhotoMetadata metadata)
{
string json = JsonUtility.ToJson(metadata);
PlayerPrefs.SetString(MetadataKeyPrefix + metadata.photoId, json);
PlayerPrefs.Save();
}
private static PhotoMetadata LoadMetadata(string photoId)
{
string json = PlayerPrefs.GetString(MetadataKeyPrefix + photoId, null);
return string.IsNullOrEmpty(json) ? null : JsonUtility.FromJson<PhotoMetadata>(json);
}
private static void DeleteMetadata(string photoId)
{
PlayerPrefs.DeleteKey(MetadataKeyPrefix + photoId);
PlayerPrefs.Save();
}
private static void AddToPhotoIndex(string photoId)
{
List<string> photoIds = GetAllPhotoIds();
if (!photoIds.Contains(photoId))
{
photoIds.Add(photoId);
SavePhotoIndex(photoIds);
}
}
private static void RemoveFromPhotoIndex(string photoId)
{
List<string> photoIds = GetAllPhotoIds();
if (photoIds.Remove(photoId))
{
SavePhotoIndex(photoIds);
}
}
private static void SavePhotoIndex(List<string> photoIds)
{
string json = JsonUtility.ToJson(new PhotoIdList { ids = photoIds });
PlayerPrefs.SetString(PhotoIndexKey, json);
PlayerPrefs.Save();
}
private static string WrapJsonArray(string json)
{
if (json.StartsWith("[")) return "{\"ids\":" + json + "}";
return json;
}
[Serializable]
private class PhotoIdList
{
public List<string> ids = new List<string>();
}
#endregion
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 7f3e9a2b4c5d6e7f8a9b0c1d2e3f4a5b