-
-
Save koras/da1dbd98de058ed7bd0172592307fca3 to your computer and use it in GitHub Desktop.
SpriteSlicer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Linq; | |
| using Cysharp.Threading.Tasks; | |
| using Editor.CMSEditor; | |
| using Newtonsoft.Json; | |
| using ProjectX.CodeBase.Core.Tags; | |
| using ProjectX.CodeBase.Level.Characters; | |
| using ProjectX.CodeBase.SpriteSlicer.Data; | |
| using Runtime; | |
| using UnityEditor; | |
| using UnityEngine; | |
| namespace ProjectX.CodeBase.SpriteSlicer.Editor | |
| { | |
| public class SpriteSlicerLogic | |
| { | |
| private Sprite _sprite; | |
| private SpriteSliceData _sliceData; | |
| private SpritePiece _currentPiece; | |
| private int _gridRows = 2; | |
| private int _gridCols = 2; | |
| private List<Vector2> _horizontalLines = new List<Vector2>(); | |
| private List<Vector2> _verticalLines = new List<Vector2>(); | |
| private Rect _previewRect; | |
| public LevelCardView CardView; | |
| public Sprite Sprite => _sprite; | |
| public SpriteSliceData SliceData => _sliceData; | |
| public SpritePiece CurrentPiece => _currentPiece; | |
| public int GridRows => _gridRows; | |
| public int GridCols => _gridCols; | |
| public List<Vector2> HorizontalLines => _horizontalLines; | |
| public List<Vector2> VerticalLines => _verticalLines; | |
| private string _defaultName; | |
| private LevelCMSEntityPfb _entity; | |
| public Rect PreviewRect | |
| { | |
| get => _previewRect; | |
| set => _previewRect = value; | |
| } | |
| public void SetSprite(Sprite sprite) | |
| { | |
| _sprite = sprite; | |
| ValidateAndInitializeSprite(); | |
| } | |
| public void SetGridSize(int rows, int cols) | |
| { | |
| if (rows != _gridRows || cols != _gridCols) | |
| { | |
| _gridRows = Mathf.Max(1, rows); | |
| _gridCols = Mathf.Max(1, cols); | |
| GenerateGrid(); | |
| } | |
| } | |
| public void ValidateAndInitializeSprite() | |
| { | |
| if (_sprite == null) | |
| { | |
| _sliceData = null; | |
| _currentPiece = null; | |
| return; | |
| } | |
| InitializeSliceData(); | |
| } | |
| public void InitializeSliceData() | |
| { | |
| if (_sprite == null) return; | |
| string assetPath = AssetDatabase.GetAssetPath(_sprite); | |
| _sliceData = new SpriteSliceData(assetPath, new Vector2(_sprite.rect.width, _sprite.rect.height)); | |
| _currentPiece = null; | |
| GenerateGrid(); | |
| } | |
| public void RestoreRootGrid() | |
| { | |
| if (_sliceData != null && _sliceData.Pieces.Count > 0) | |
| { | |
| LoadGridFromPieces(_sliceData.Pieces, GetCurrentRect()); | |
| } | |
| else | |
| { | |
| GenerateGrid(); | |
| } | |
| } | |
| public void SetupSlicing(SpritePiece piece) | |
| { | |
| _currentPiece = piece; | |
| GenerateGrid(); | |
| } | |
| public void SelectPieceForEditing(SpritePiece piece) | |
| { | |
| _currentPiece = piece; | |
| if (piece.Children.Count > 0) | |
| { | |
| LoadGridFromPieces(piece.Children, piece.Rect); | |
| } | |
| else | |
| { | |
| _gridRows = 1; | |
| _gridCols = 1; | |
| _horizontalLines.Clear(); | |
| _verticalLines.Clear(); | |
| } | |
| } | |
| public void SelectRootPiece() | |
| { | |
| _currentPiece = null; | |
| RestoreRootGrid(); | |
| } | |
| public void GenerateGrid() | |
| { | |
| _horizontalLines.Clear(); | |
| _verticalLines.Clear(); | |
| for (int i = 1; i < _gridRows; i++) | |
| { | |
| _horizontalLines.Add(new Vector2((float)i / _gridRows, 0)); | |
| } | |
| for (int i = 1; i < _gridCols; i++) | |
| { | |
| _verticalLines.Add(new Vector2((float)i / _gridCols, 0)); | |
| } | |
| } | |
| public void ApplySlice() | |
| { | |
| if (_sprite == null) return; | |
| if (_currentPiece == null) | |
| { | |
| _sliceData.Pieces.Clear(); | |
| CreatePiecesFromGrid(_sliceData.Pieces, _sprite.name, GetCurrentRect(), 0); | |
| } | |
| else | |
| { | |
| _currentPiece.Children.Clear(); | |
| CreatePiecesFromGrid(_currentPiece.Children, _currentPiece.Name, _currentPiece.Rect, ++_currentPiece.DepthIndex); | |
| } | |
| } | |
| private void CreatePiecesFromGrid(List<SpritePiece> targetList, string baseName, Rect baseRect, byte depth) | |
| { | |
| List<float> hLines = new List<float> { 0f }; | |
| foreach (var line in _horizontalLines) | |
| hLines.Add(line.x); | |
| hLines.Add(1f); | |
| hLines.Sort(); | |
| List<float> vLines = new List<float> { 0f }; | |
| foreach (var line in _verticalLines) | |
| vLines.Add(line.x); | |
| vLines.Add(1f); | |
| vLines.Sort(); | |
| int pieceIndex = 1; | |
| int totalRows = hLines.Count - 1; | |
| int totalCols = vLines.Count - 1; | |
| for (int row = 0; row < totalRows; row++) | |
| { | |
| for (int col = 0; col < totalCols; col++) | |
| { | |
| float x = baseRect.x + vLines[col] * baseRect.width; | |
| float y = baseRect.y + hLines[row] * baseRect.height; | |
| int width = Mathf.RoundToInt((vLines[col + 1] - vLines[col]) * baseRect.width); | |
| int height = Mathf.RoundToInt((hLines[row + 1] - hLines[row]) * baseRect.height); | |
| Rect pieceRect = new Rect(x, y, width, height); | |
| string pieceName = $"{baseName}_{pieceIndex}"; | |
| bool isBorder = DetermineIfBorder(row, col, totalRows, totalCols, _currentPiece, baseRect); | |
| SpritePiece piece = new SpritePiece(pieceName, pieceRect, depth, isBorder); | |
| targetList.Add(piece); | |
| pieceIndex++; | |
| } | |
| } | |
| } | |
| private bool DetermineIfBorder(int row, int col, int totalRows, int totalCols, SpritePiece parentPiece, Rect currentRect) | |
| { | |
| if (parentPiece == null) | |
| { | |
| return row == 0 || row == totalRows - 1 || col == 0 || col == totalCols - 1; | |
| } | |
| if (!parentPiece.IsBorder) | |
| { | |
| return false; | |
| } | |
| var parentBorderSides = GetParentBorderSides(parentPiece.Rect); | |
| bool isBorder = false; | |
| if (parentBorderSides.isTopBorder && row == 0) | |
| isBorder = true; | |
| if (parentBorderSides.isBottomBorder && row == totalRows - 1) | |
| isBorder = true; | |
| if (parentBorderSides.isLeftBorder && col == 0) | |
| isBorder = true; | |
| if (parentBorderSides.isRightBorder && col == totalCols - 1) | |
| isBorder = true; | |
| return isBorder; | |
| } | |
| private (bool isTopBorder, bool isBottomBorder, bool isLeftBorder, bool isRightBorder) GetParentBorderSides(Rect parentRect) | |
| { | |
| if (_sliceData == null || _sliceData.Pieces.Count == 0) | |
| return (false, false, false, false); | |
| var allMainPieces = _sliceData.Pieces; | |
| if (allMainPieces.Count == 0) | |
| return (false, false, false, false); | |
| var minX = allMainPieces.Min(p => p.Rect.x); | |
| var minY = allMainPieces.Min(p => p.Rect.y); | |
| var maxX = allMainPieces.Max(p => p.Rect.x); | |
| var maxY = allMainPieces.Max(p => p.Rect.y); | |
| var tolerance = 1f; | |
| bool isTopBorder = Mathf.Abs(parentRect.y - minY) < tolerance; | |
| bool isBottomBorder = Mathf.Abs(parentRect.y - maxY) < tolerance; | |
| bool isLeftBorder = Mathf.Abs(parentRect.x - minX) < tolerance; | |
| bool isRightBorder = Mathf.Abs(parentRect.x - maxX) < tolerance; | |
| return (isTopBorder, isBottomBorder, isLeftBorder, isRightBorder); | |
| } | |
| private void LoadGridFromPieces(List<SpritePiece> pieces, Rect parentRect) | |
| { | |
| _horizontalLines.Clear(); | |
| _verticalLines.Clear(); | |
| HashSet<float> hLines = new HashSet<float>(); | |
| HashSet<float> vLines = new HashSet<float>(); | |
| foreach (var piece in pieces) | |
| { | |
| float topNorm = (piece.Rect.y - parentRect.y) / parentRect.height; | |
| float bottomNorm = (piece.Rect.y + piece.Rect.height - parentRect.y) / parentRect.height; | |
| float leftNorm = (piece.Rect.x - parentRect.x) / parentRect.width; | |
| float rightNorm = (piece.Rect.x + piece.Rect.width - parentRect.x) / parentRect.width; | |
| if (topNorm > 0.001f && topNorm < 0.999f) hLines.Add(topNorm); | |
| if (bottomNorm > 0.001f && bottomNorm < 0.999f) hLines.Add(bottomNorm); | |
| if (leftNorm > 0.001f && leftNorm < 0.999f) vLines.Add(leftNorm); | |
| if (rightNorm > 0.001f && rightNorm < 0.999f) vLines.Add(rightNorm); | |
| } | |
| var sortedHLines = hLines.OrderBy(x => x).ToList(); | |
| var sortedVLines = vLines.OrderBy(x => x).ToList(); | |
| foreach (float h in sortedHLines) | |
| { | |
| _horizontalLines.Add(new Vector2(h, 0)); | |
| } | |
| foreach (float v in sortedVLines) | |
| { | |
| _verticalLines.Add(new Vector2(v, 0)); | |
| } | |
| _gridRows = sortedHLines.Count + 1; | |
| _gridCols = sortedVLines.Count + 1; | |
| Debug.Log($"Loaded grid: {_gridRows}x{_gridCols} from {pieces.Count} pieces"); | |
| } | |
| public Rect GetCurrentRect() | |
| { | |
| if (_currentPiece != null) | |
| return _currentPiece.Rect; | |
| if (_sprite != null) | |
| return _sprite.rect; | |
| return new Rect(); | |
| } | |
| public Rect GetCurrentSpriteUV() | |
| { | |
| if (_sprite == null) | |
| return new Rect(); | |
| Texture2D texture = _sprite.texture; | |
| Rect spriteRect = GetCurrentRect(); | |
| return new Rect( | |
| spriteRect.x / texture.width, | |
| spriteRect.y / texture.height, | |
| spriteRect.width / texture.width, | |
| spriteRect.height / texture.height | |
| ); | |
| } | |
| public void HandleInput() | |
| { | |
| Event e = Event.current; | |
| if (e == null) return; | |
| if (e.type == EventType.MouseDown && e.button == 0) | |
| { | |
| if (_previewRect.Contains(e.mousePosition)) | |
| { | |
| Vector2 localPos = e.mousePosition - new Vector2(_previewRect.x, _previewRect.y); | |
| Vector2 normalizedPos = new Vector2(localPos.x / _previewRect.width, localPos.y / _previewRect.height); | |
| SpritePiece clickedPiece = GetPieceAtPosition(normalizedPos); | |
| if (clickedPiece != null) | |
| { | |
| SelectPieceForEditing(clickedPiece); | |
| } | |
| e.Use(); | |
| } | |
| } | |
| } | |
| private SpritePiece GetPieceAtPosition(Vector2 normalizedPos) | |
| { | |
| List<SpritePiece> piecesToCheck = null; | |
| Rect currentRect = GetCurrentRect(); | |
| if (_currentPiece == null) | |
| { | |
| piecesToCheck = _sliceData?.Pieces; | |
| } | |
| else | |
| { | |
| piecesToCheck = _currentPiece.Children; | |
| } | |
| if (piecesToCheck == null || piecesToCheck.Count == 0) return null; | |
| normalizedPos.y = 1f - normalizedPos.y; | |
| Vector2 worldPos = new Vector2( | |
| currentRect.x + normalizedPos.x * currentRect.width, | |
| currentRect.y + normalizedPos.y * currentRect.height | |
| ); | |
| foreach (var piece in piecesToCheck) | |
| { | |
| if (piece.Rect.Contains(worldPos)) | |
| { | |
| return piece; | |
| } | |
| } | |
| return null; | |
| } | |
| public void ImportFromEntity() | |
| { | |
| string path = EditorUtility.OpenFilePanel("Import Sprite Slice Data", "", "prefab"); | |
| if (!string.IsNullOrEmpty(path)) | |
| { | |
| var localPath = GetLocalPath(path); | |
| var level = AssetDatabase.LoadAssetAtPath<LevelCMSEntityPfb>(localPath); | |
| CardView = level.As<LevelCardTag>().CardView; | |
| string json = level.As<JsonTag>().Json.text; | |
| _sliceData = JsonConvert.DeserializeObject<SpriteSliceData>(json); | |
| if (!string.IsNullOrEmpty(_sliceData.OriginalSpritePath)) | |
| { | |
| _sprite = AssetDatabase.LoadAssetAtPath<Sprite>(_sliceData.OriginalSpritePath); | |
| } | |
| _currentPiece = null; | |
| if (_sliceData.Pieces.Count > 0) | |
| { | |
| LoadGridFromPieces(_sliceData.Pieces, GetCurrentRect()); | |
| } | |
| else | |
| { | |
| GenerateGrid(); | |
| } | |
| Debug.Log($"Imported sprite slice data from: {path}"); | |
| } | |
| } | |
| public void ResetToOriginal() | |
| { | |
| if (_sprite == null) return; | |
| InitializeSliceData(); | |
| SetupSlicing(null); | |
| } | |
| private string jsonPath = ""; | |
| public void ExportToJSON() | |
| { | |
| if (_sliceData == null || _sprite == null) return ; | |
| var settings = new JsonSerializerSettings | |
| { | |
| ReferenceLoopHandling = ReferenceLoopHandling.Ignore, | |
| Formatting = Formatting.Indented, | |
| NullValueHandling = NullValueHandling.Ignore | |
| }; | |
| string json = JsonConvert.SerializeObject(_sliceData, settings); | |
| string defaultName = _sprite.name + "_slices"; | |
| string path = EditorUtility.SaveFilePanel("Export Sprite Slice Data", "", defaultName, "json"); | |
| if (!string.IsNullOrEmpty(path)) | |
| { | |
| File.WriteAllText(path, json); | |
| jsonPath = GetLocalPath(path); | |
| Debug.Log($"Exported sprite slice data to: {path}"); | |
| AssetDatabase.Refresh(); | |
| } | |
| } | |
| public void ExportEntity() | |
| { | |
| string path = EditorUtility.SaveFilePanel("Export Level Entity", "", _defaultName, "prefab"); | |
| var localPath = GetLocalPath(path); | |
| if (!string.IsNullOrEmpty(localPath)) | |
| { | |
| if (AssetDatabase.CopyAsset("Assets/Resources/CMS/Prefabs/Level_Temaplate_Entity.prefab", localPath)) | |
| { | |
| var newLevel = AssetDatabase.LoadAssetAtPath<LevelCMSEntityPfb>(localPath); | |
| CMSEntityIdSetter.AutoFillCMSIds(); | |
| CMS.Unload(); | |
| CMS.Init(); | |
| newLevel.As<TagSprite>().sprite = _sprite; | |
| var jsonAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(jsonPath); | |
| newLevel.As<JsonTag>().Json = jsonAsset; | |
| newLevel.As<AnimationLocalUrl>().VideoClipUrl = SliceData.VideoUrl; | |
| var types = new List<LevelType>(); | |
| if(SliceData.IsMirrored) | |
| types.Add(LevelType.Mirrored); | |
| if(SliceData.IsRotatable) | |
| types.Add(LevelType.Rotatable); | |
| newLevel.As<LevelTypeTag>().Types = types.ToArray(); | |
| newLevel.As<LevelCardTag>().CardView = CardView; | |
| newLevel.As<DescribeTag>().DescriptionLoc = SliceData.DescriptionLoc; | |
| newLevel.Parse(); | |
| UnityEngine.Debug.Log(newLevel.As<AnimationLocalUrl>().VideoClipUrl + " " + SliceData.VideoUrl); | |
| Debug.Log($"Exported entity data to: {localPath}"); | |
| EditorUtility.SetDirty(newLevel); | |
| EditorApplication.ExecuteMenuItem("File/Save Project"); | |
| } | |
| } | |
| } | |
| private string GetLocalPath(string path) | |
| { | |
| return "Assets/" + path.Split("Assets/")[1]; | |
| } | |
| } | |
| } | |
| using System.Collections.Generic; | |
| using ProjectX.CodeBase.Level.Characters; | |
| using UnityEditor; | |
| using UnityEngine; | |
| namespace ProjectX.CodeBase.SpriteSlicer.Editor | |
| { | |
| public class SpriteSlicerDrawer | |
| { | |
| private readonly SpriteSlicerLogic _logic; | |
| private Vector2 _scrollPosition; | |
| public SpriteSlicerDrawer(SpriteSlicerLogic logic) | |
| { | |
| _logic = logic; | |
| } | |
| public void DrawGUI() | |
| { | |
| DrawToolbar(); | |
| if (_logic.Sprite == null) | |
| { | |
| DrawEmptyState(); | |
| return; | |
| } | |
| EditorGUILayout.BeginHorizontal(); | |
| DrawSpritePreview(); | |
| EditorGUILayout.BeginVertical(); | |
| DrawHierarchy(); | |
| DrawSpriteSettings(); | |
| EditorGUILayout.EndVertical(); | |
| DrawPieceSettings(); | |
| EditorGUILayout.EndHorizontal(); | |
| DrawControls(); | |
| } | |
| private void DrawToolbar() | |
| { | |
| EditorGUILayout.BeginHorizontal(EditorStyles.toolbar, GUILayout.Height(25)); | |
| GUILayout.Label("🎬", GUILayout.Width(20)); | |
| EditorGUI.BeginChangeCheck(); | |
| DrawSpriteSelection(); | |
| if (EditorGUI.EndChangeCheck()) | |
| { | |
| _logic.ValidateAndInitializeSprite(); | |
| } | |
| GUILayout.FlexibleSpace(); | |
| DrawToolbarButtons(); | |
| EditorGUILayout.EndHorizontal(); | |
| } | |
| private void DrawSpriteSelection() | |
| { | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.LabelField("Sprite:", GUILayout.Width(60)); | |
| EditorGUI.BeginChangeCheck(); | |
| Sprite selected = (Sprite)EditorGUILayout.ObjectField(_logic.Sprite, typeof(Sprite), false, GUILayout.Width(200)); | |
| if (EditorGUI.EndChangeCheck()) | |
| { | |
| _logic.SetSprite(selected); | |
| } | |
| EditorGUILayout.EndHorizontal(); | |
| } | |
| private void DrawToolbarButtons() | |
| { | |
| GUI.backgroundColor = new Color(0.7f, 1f, 0.7f); | |
| if (GUILayout.Button("📁 Import", EditorStyles.toolbarButton, GUILayout.Width(70))) | |
| { | |
| _logic.ImportFromEntity(); | |
| } | |
| GUI.backgroundColor = new Color(1f, 0.9f, 0.7f); | |
| if (GUILayout.Button("🔄 Reset", EditorStyles.toolbarButton, GUILayout.Width(60))) | |
| { | |
| _logic.ResetToOriginal(); | |
| } | |
| GUI.backgroundColor = new Color(0.7f, 0.9f, 1f); | |
| if (GUILayout.Button("💾 Export Json", EditorStyles.toolbarButton, GUILayout.Width(120))) | |
| { | |
| _logic.ExportToJSON(); | |
| } | |
| if (GUILayout.Button("💾 Export Entity", EditorStyles.toolbarButton, GUILayout.Width(120))) | |
| { | |
| _logic.ExportEntity(); | |
| } | |
| GUI.backgroundColor = Color.white; | |
| } | |
| private void DrawEmptyState() | |
| { | |
| EditorGUILayout.Space(20); | |
| EditorGUILayout.BeginVertical(GUI.skin.box); | |
| EditorGUILayout.Space(10); | |
| EditorGUILayout.LabelField("🎨 Sprite Slicer", EditorStyles.largeLabel); | |
| EditorGUILayout.Space(5); | |
| EditorGUILayout.LabelField("Select a sprite to slice!", EditorStyles.helpBox); | |
| EditorGUILayout.Space(10); | |
| EditorGUILayout.EndVertical(); | |
| } | |
| private void DrawSpritePreview() | |
| { | |
| EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(420)); | |
| EditorGUILayout.LabelField("🎯 Preview", EditorStyles.boldLabel); | |
| if (_logic.Sprite != null) | |
| { | |
| Sprite currentSprite = _logic.Sprite; | |
| Rect spriteArea = GUILayoutUtility.GetRect(800, 800); | |
| Rect previewRect = CalculatePreviewRect(currentSprite, spriteArea); | |
| _logic.PreviewRect = previewRect; | |
| DrawSpriteBackground(previewRect); | |
| DrawSpriteTexture(currentSprite, previewRect); | |
| DrawPreviewOverlays(previewRect); | |
| GUILayout.Space(10); | |
| DrawPreviewInfo(); | |
| } | |
| EditorGUILayout.EndVertical(); | |
| } | |
| private Rect CalculatePreviewRect(Sprite sprite, Rect area) | |
| { | |
| float spriteAspect = sprite.rect.width / sprite.rect.height; | |
| float areaAspect = area.width / area.height; | |
| Rect previewRect = new Rect(); | |
| if (spriteAspect > areaAspect) | |
| { | |
| previewRect.width = area.width * 0.95f; | |
| previewRect.height = (area.width * 0.95f) / spriteAspect; | |
| } | |
| else | |
| { | |
| previewRect.height = area.height * 0.95f; | |
| previewRect.width = (area.height * 0.95f) * spriteAspect; | |
| } | |
| previewRect.x = area.x + (area.width - previewRect.width) * 0.5f; | |
| previewRect.y = area.y + (area.height - previewRect.height) * 0.5f; | |
| return previewRect; | |
| } | |
| private void DrawSpriteBackground(Rect previewRect) | |
| { | |
| EditorGUI.DrawRect( | |
| new Rect(previewRect.x - 2, previewRect.y - 2, previewRect.width + 4, previewRect.height + 4), | |
| new Color(0.3f, 0.3f, 0.3f)); | |
| EditorGUI.DrawRect( | |
| new Rect(previewRect.x - 1, previewRect.y - 1, previewRect.width + 2, previewRect.height + 2), | |
| Color.white); | |
| } | |
| private void DrawSpriteTexture(Sprite sprite, Rect previewRect) | |
| { | |
| Rect uvRect = _logic.GetCurrentSpriteUV(); | |
| GUI.DrawTextureWithTexCoords(previewRect, sprite.texture, uvRect); | |
| } | |
| private void DrawPreviewOverlays(Rect previewRect) | |
| { | |
| DrawExistingPieces(previewRect); | |
| DrawGrid(previewRect); | |
| DrawSliceLines(previewRect); | |
| } | |
| private void DrawPreviewInfo() | |
| { | |
| string info = _logic.CurrentPiece != null ? $"Editing: {_logic.CurrentPiece.Name}" : "Editing: Original Sprite"; | |
| EditorGUILayout.LabelField(info, EditorStyles.centeredGreyMiniLabel); | |
| } | |
| private void DrawHierarchy() | |
| { | |
| EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(480)); | |
| DrawHierarchyHeader(); | |
| DrawCurrentPieceInfo(); | |
| _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition, GUILayout.Height(350)); | |
| if (_logic.SliceData != null && _logic.SliceData.Pieces.Count > 0) | |
| { | |
| DrawPieceHierarchy(_logic.SliceData.Pieces, 0); | |
| } | |
| else | |
| { | |
| EditorGUILayout.LabelField("No slices yet. Create some!", EditorStyles.centeredGreyMiniLabel); | |
| } | |
| EditorGUILayout.EndScrollView(); | |
| EditorGUILayout.EndVertical(); | |
| } | |
| private void DrawHierarchyHeader() | |
| { | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.LabelField("🌳 Sprite Hierarchy", EditorStyles.boldLabel); | |
| GUILayout.FlexibleSpace(); | |
| Color originalBg = GUI.backgroundColor; | |
| bool isRootSelected = _logic.CurrentPiece == null; | |
| if (isRootSelected) | |
| { | |
| GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f); | |
| } | |
| else | |
| { | |
| GUI.backgroundColor = new Color(0.8f, 1f, 0.8f); | |
| } | |
| if (GUILayout.Button("🏠 Root", GUILayout.Width(60), GUILayout.Height(20))) | |
| { | |
| _logic.SelectRootPiece(); | |
| } | |
| GUI.backgroundColor = originalBg; | |
| EditorGUILayout.EndHorizontal(); | |
| } | |
| private void DrawCurrentPieceInfo() | |
| { | |
| EditorGUILayout.Space(5); | |
| if (_logic.CurrentPiece != null) | |
| { | |
| EditorGUILayout.LabelField($"📍 Current: {TruncateText(_logic.CurrentPiece.Name, 50)}", EditorStyles.miniLabel); | |
| } | |
| else | |
| { | |
| EditorGUILayout.LabelField("📍 Current: Original Sprite", EditorStyles.miniLabel); | |
| } | |
| EditorGUILayout.Space(5); | |
| } | |
| private void DrawPieceHierarchy(List<SpritePiece> pieces, int indent) | |
| { | |
| foreach (var piece in pieces) | |
| { | |
| bool hasChildren = piece.Children.Count > 0; | |
| bool isSelected = _logic.CurrentPiece == piece; | |
| Color originalColor = GUI.backgroundColor; | |
| if (isSelected) | |
| { | |
| GUI.backgroundColor = new Color(0.3f, 0.6f, 1f, 0.6f); | |
| } | |
| EditorGUILayout.BeginHorizontal(GUI.skin.box); | |
| GUILayout.Space(indent * 15); | |
| if (hasChildren) | |
| { | |
| piece.IsExpanded = EditorGUILayout.Foldout(piece.IsExpanded, ""); | |
| if (GUILayout.Button($"📁 {piece.Name}", isSelected ? EditorStyles.whiteBoldLabel : EditorStyles.label, GUILayout.ExpandWidth(true))) | |
| { | |
| _logic.SelectPieceForEditing(piece); | |
| } | |
| } | |
| else | |
| { | |
| GUILayout.Space(12); | |
| if (GUILayout.Button($"📄 {piece.Name}", isSelected ? EditorStyles.whiteBoldLabel : EditorStyles.label, GUILayout.ExpandWidth(true))) | |
| { | |
| _logic.SelectPieceForEditing(piece); | |
| } | |
| } | |
| GUI.backgroundColor = new Color(0.9f, 0.9f, 1f, 0.8f); | |
| if (GUILayout.Button("✏️", GUILayout.Width(25), GUILayout.Height(18))) | |
| { | |
| _logic.SelectPieceForEditing(piece); | |
| } | |
| GUI.backgroundColor = originalColor; | |
| EditorGUILayout.EndHorizontal(); | |
| if (piece.IsExpanded && hasChildren) | |
| { | |
| DrawPieceHierarchy(piece.Children, indent + 1); | |
| } | |
| } | |
| } | |
| private void DrawControls() | |
| { | |
| EditorGUILayout.Space(10); | |
| if (_logic.Sprite == null) return; | |
| EditorGUILayout.BeginVertical(GUI.skin.box); | |
| EditorGUILayout.LabelField("⚙️ Slice Settings", EditorStyles.boldLabel); | |
| DrawGridControls(); | |
| DrawSliceInfo(); | |
| EditorGUILayout.EndVertical(); | |
| } | |
| private void DrawGridControls() | |
| { | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.LabelField("Grid Size:", GUILayout.Width(70)); | |
| GUI.backgroundColor = new Color(0.9f, 1f, 0.9f); | |
| EditorGUILayout.BeginVertical(GUILayout.Width(80)); | |
| EditorGUI.BeginChangeCheck(); | |
| int newRows = EditorGUILayout.IntField("Rows", _logic.GridRows); | |
| int newCols = EditorGUILayout.IntField("Cols", _logic.GridCols); | |
| if (EditorGUI.EndChangeCheck()) | |
| { | |
| _logic.SetGridSize(newRows, newCols); | |
| } | |
| EditorGUILayout.EndVertical(); | |
| GUI.backgroundColor = Color.white; | |
| GUILayout.FlexibleSpace(); | |
| GUI.backgroundColor = new Color(0.7f, 1f, 0.7f); | |
| if (GUILayout.Button("🔥 Apply Slice", GUILayout.Height(25), GUILayout.Width(100))) | |
| { | |
| _logic.ApplySlice(); | |
| } | |
| GUI.backgroundColor = Color.white; | |
| EditorGUILayout.EndHorizontal(); | |
| } | |
| private void DrawSliceInfo() | |
| { | |
| EditorGUILayout.Space(5); | |
| int totalPieces = _logic.GridRows * _logic.GridCols; | |
| EditorGUILayout.LabelField($"📊 Will create {totalPieces} pieces", EditorStyles.helpBox); | |
| EditorGUILayout.LabelField("💡 Tip: Drag red lines to adjust slice positions", EditorStyles.helpBox); | |
| } | |
| private void DrawGrid(Rect previewRect) | |
| { | |
| if (_logic.Sprite == null) return; | |
| Handles.BeginGUI(); | |
| Handles.color = new Color(1, 1, 1, 0.4f); | |
| foreach (var line in _logic.HorizontalLines) | |
| { | |
| float y = Mathf.Lerp(previewRect.y, previewRect.y + previewRect.height, line.x); | |
| Handles.DrawLine(new Vector3(previewRect.x, y), new Vector3(previewRect.x + previewRect.width, y)); | |
| } | |
| foreach (var line in _logic.VerticalLines) | |
| { | |
| float x = Mathf.Lerp(previewRect.x, previewRect.x + previewRect.width, line.x); | |
| Handles.DrawLine(new Vector3(x, previewRect.y), new Vector3(x, previewRect.y + previewRect.height)); | |
| } | |
| Handles.EndGUI(); | |
| } | |
| private void DrawSliceLines(Rect previewRect) | |
| { | |
| if (_logic.Sprite == null) return; | |
| Handles.BeginGUI(); | |
| Handles.color = new Color(1f, 0.3f, 0.3f, 0.9f); | |
| for (int i = 0; i < _logic.HorizontalLines.Count; i++) | |
| { | |
| float y = Mathf.Lerp(previewRect.y, previewRect.y + previewRect.height, _logic.HorizontalLines[i].x); | |
| Vector3 start = new Vector3(previewRect.x, y); | |
| Vector3 end = new Vector3(previewRect.x + previewRect.width, y); | |
| Handles.DrawAAPolyLine(3f, start, end); | |
| } | |
| for (int i = 0; i < _logic.VerticalLines.Count; i++) | |
| { | |
| float x = Mathf.Lerp(previewRect.x, previewRect.x + previewRect.width, _logic.VerticalLines[i].x); | |
| Vector3 start = new Vector3(x, previewRect.y); | |
| Vector3 end = new Vector3(x, previewRect.y + previewRect.height); | |
| Handles.DrawAAPolyLine(3f, start, end); | |
| } | |
| Handles.EndGUI(); | |
| } | |
| private void DrawExistingPieces(Rect previewRect) | |
| { | |
| if (_logic.CurrentPiece == null) return; | |
| List<SpritePiece> piecesToShow = _logic.CurrentPiece.Children; | |
| if (piecesToShow == null || piecesToShow.Count == 0) return; | |
| Handles.BeginGUI(); | |
| Rect currentRect = _logic.GetCurrentRect(); | |
| foreach (var piece in piecesToShow) | |
| { | |
| float normalizedX = (piece.Rect.x - currentRect.x) / currentRect.width; | |
| float normalizedY = (piece.Rect.y - currentRect.y) / currentRect.height; | |
| float normalizedWidth = piece.Rect.width / currentRect.width; | |
| float normalizedHeight = piece.Rect.height / currentRect.height; | |
| float x = previewRect.x + normalizedX * previewRect.width; | |
| float y = previewRect.y + normalizedY * previewRect.height; | |
| float width = normalizedWidth * previewRect.width; | |
| float height = normalizedHeight * previewRect.height; | |
| Rect pieceRect = new Rect(x, y, width, height); | |
| Handles.color = new Color(0, 1, 0, 0.3f); | |
| Handles.DrawSolidRectangleWithOutline(pieceRect, new Color(0, 1, 0, 0.1f), Color.green); | |
| GUI.Label(new Rect(x + 2, y + 2, width - 4, 16), TruncateText(piece.Name, 15), EditorStyles.miniLabel); | |
| } | |
| Handles.EndGUI(); | |
| } | |
| private string TruncateText(string text, int maxLength) | |
| { | |
| if (string.IsNullOrEmpty(text) || text.Length <= maxLength) | |
| return text; | |
| return text.Substring(0, maxLength - 3) + "..."; | |
| } | |
| private void DrawSpriteSettings() | |
| { | |
| if (_logic.SliceData == null) return; | |
| EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(480)); | |
| EditorGUILayout.LabelField("🖼 Sprite Settings", EditorStyles.boldLabel); | |
| EditorGUI.BeginChangeCheck(); | |
| _logic.SliceData.VideoUrl = EditorGUILayout.TextField("Video URL", _logic.SliceData.VideoUrl); | |
| _logic.SliceData.DescriptionLoc = EditorGUILayout.TextField("Describe loc key", _logic.SliceData.DescriptionLoc); | |
| _logic.SliceData.IsRotatable = EditorGUILayout.Toggle("Is Rotatable", _logic.SliceData.IsRotatable); | |
| _logic.SliceData.IsMirrored = EditorGUILayout.Toggle("Is Mirrored", _logic.SliceData.IsMirrored); | |
| _logic.CardView = (LevelCardView)EditorGUILayout.ObjectField("Card view",_logic.CardView, typeof(LevelCardView), false); | |
| EditorGUILayout.EndVertical(); | |
| } | |
| private void DrawPieceSettings() | |
| { | |
| if (_logic.CurrentPiece == null) return; | |
| EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.Width(480)); | |
| EditorGUILayout.LabelField("🔒 Piece Settings", EditorStyles.boldLabel); | |
| EditorGUI.BeginChangeCheck(); | |
| _logic.CurrentPiece.IsLock = EditorGUILayout.Toggle("Is Locked", _logic.CurrentPiece.IsLock); | |
| EditorGUILayout.EndVertical(); | |
| } | |
| } | |
| } | |
| using UnityEditor; | |
| namespace ProjectX.CodeBase.SpriteSlicer.Editor | |
| { | |
| public class SpriteSlicer : EditorWindow | |
| { | |
| private SpriteSlicerDrawer _drawer; | |
| private SpriteSlicerLogic _logic; | |
| [MenuItem("Tools/Sprite Slicer")] | |
| public static void ShowWindow() | |
| { | |
| GetWindow<SpriteSlicer>("Sprite Slicer"); | |
| } | |
| private void OnEnable() | |
| { | |
| _logic = new SpriteSlicerLogic(); | |
| _drawer = new SpriteSlicerDrawer(_logic); | |
| } | |
| private void OnGUI() | |
| { | |
| _drawer.DrawGUI(); | |
| _logic.HandleInput(); | |
| } | |
| } | |
| } | |
| using System; | |
| using DG.Tweening; | |
| using Sirenix.OdinInspector; | |
| using UnityEngine; | |
| namespace ProjectX.CodeBase.SpriteSlicer | |
| { | |
| public class SpritePieceComponent : MonoBehaviour | |
| { | |
| public AnimatedPieceMonoBeh AnimatedPieceMonoBeh; | |
| [SerializeField, ReadOnly] private SpritePiece _pieceData; | |
| private RenderTexture _videoTexture; | |
| private Material _material; | |
| private MaterialPropertyBlock _materialPropertyBlock; | |
| private Renderer _renderer; | |
| private Vector2 _uvMin; | |
| private Vector2 _uvMax; | |
| private float _rotationZ = 0f; | |
| public SpritePiece PieceData => _pieceData; | |
| [ShowInInspector] public float RotationZ | |
| { | |
| get => _rotationZ * Mathf.Rad2Deg; | |
| set | |
| { | |
| _rotationZ = Quaternion.Euler(0f, 0f, value).eulerAngles.z * Mathf.Deg2Rad; | |
| UpdateMaterialProperties(); | |
| } | |
| } | |
| private Vector3 _initScale = Vector2.one; | |
| private void Awake() | |
| { | |
| _renderer = GetComponent<Renderer>(); | |
| if (_renderer == null) | |
| { | |
| Debug.LogError($"No Renderer found on {gameObject.name}"); | |
| } | |
| _materialPropertyBlock = new MaterialPropertyBlock(); | |
| } | |
| public void Initialize(SpritePiece pieceData, RenderTexture videoTexture, Material material, | |
| Vector2 uvMin, Vector2 uvMax, Vector3 worldPosition, Vector3 worldScale) | |
| { | |
| _pieceData = pieceData; | |
| _videoTexture = videoTexture; | |
| _material = material; | |
| _uvMin = uvMin; | |
| _uvMax = uvMax; | |
| transform.localPosition = worldPosition; | |
| _initScale = new Vector3(CeilToDigits(worldScale.x, 2), CeilToDigits(worldScale.x, 2), worldPosition.z); | |
| transform.localScale = _initScale; | |
| if (_renderer != null) | |
| { | |
| _renderer.material = _material; | |
| } | |
| if(videoTexture == null || material == null) | |
| return; | |
| UpdateMaterialProperties(); | |
| } | |
| public void ResetScale() | |
| { | |
| transform.DOScale(_initScale, 0.2f); | |
| } | |
| public static float CeilToDigits(float value, int digits) | |
| { | |
| float scale = (float)Math.Pow(10, digits); | |
| return (float)Math.Ceiling(value * scale) / scale; | |
| } | |
| private void UpdateMaterialProperties() | |
| { | |
| if (_materialPropertyBlock == null || _renderer == null) | |
| return; | |
| _materialPropertyBlock.SetTexture("_MainTex", _videoTexture); | |
| _materialPropertyBlock.SetVector("_UVMin", _uvMin); | |
| _materialPropertyBlock.SetVector("_UVMax", _uvMax); | |
| _materialPropertyBlock.SetFloat("_RotationZ", _rotationZ); | |
| Vector2 uvCenter = new Vector2(0.5f, 0.5f); | |
| _materialPropertyBlock.SetVector("_UVCenter", uvCenter); | |
| _renderer.SetPropertyBlock(_materialPropertyBlock); | |
| } | |
| public void SetColor(Color color) | |
| { | |
| _materialPropertyBlock.SetColor("_Color", color); | |
| _renderer.SetPropertyBlock(_materialPropertyBlock); | |
| } | |
| public Color GetColor() | |
| { | |
| return _materialPropertyBlock.GetColor("_Color"); | |
| } | |
| [Button] | |
| public void SetRotation(float rotationZ) | |
| { | |
| RotationZ = rotationZ; | |
| } | |
| public void AddRotation(float deltaRotation) | |
| { | |
| RotationZ += deltaRotation; | |
| } | |
| } | |
| } | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Threading; | |
| using Cysharp.Threading.Tasks; | |
| using DG.Tweening; | |
| using ProjectX.CodeBase.Core; | |
| using ProjectX.CodeBase.Feel; | |
| using ProjectX.CodeBase.Level.InteractableObjects; | |
| using Sirenix.OdinInspector; | |
| using UnityEngine; | |
| namespace ProjectX.CodeBase.SpriteSlicer | |
| { | |
| [RequireComponent(typeof(Renderer), typeof(Collider))] | |
| public class AnimatedPieceMonoBeh : ManagedMonoBehaviour | |
| { | |
| [ReadOnly, ShowInInspector] public bool WasCompleted = false; | |
| [ReadOnly, ShowInInspector] public bool IsBordered => SpritePiece.PieceData.IsBorder; | |
| [HideInInspector] public List<InteractableImage> InteractableImages = new List<InteractableImage>(); | |
| public SpriteRenderer Outline; | |
| public SpriteRenderer Lock; | |
| public SpriteRenderer LockParent; | |
| public BoxCollider Collider; | |
| public SpritePieceComponent SpritePiece; | |
| public SpriteRenderer Hover; | |
| public bool LockHover = false; | |
| private CancellationTokenSource _cancellationTokenSource; | |
| private Color32 fullColor = new Color32(207, 104, 188, 255 / 2); | |
| private Color32 halfColor = new Color32(207, 104, 188, 255 / 4); | |
| private Color32 clear = new Color32(207, 104, 188, 0); | |
| private void Awake() | |
| { | |
| _cancellationTokenSource = new CancellationTokenSource(); | |
| } | |
| public void EnableLock() | |
| { | |
| LockParent.gameObject.SetActive(SpritePiece.PieceData.IsLock); | |
| } | |
| public void PlayLockAnim() | |
| { | |
| LockParent.transform.PunchScaleOnce(.5f,.2f); | |
| } | |
| public bool IsCompleted() | |
| { | |
| var isCompleted = InteractableImages.All(i => i.IsCompleted()); | |
| return isCompleted; | |
| } | |
| public int? GetCurrentRotationIndex() | |
| { | |
| var rotatable = InteractableImages | |
| .FirstOrDefault(i => i is RotatableImageView) as RotatableImageView; | |
| return rotatable?.CurrentRotationIndex; | |
| } | |
| public bool GetCurrentMirror() | |
| { | |
| return transform.localScale.x > 0; | |
| } | |
| public void SetStateInteractables(bool state) | |
| { | |
| foreach (var interactableImage in InteractableImages) | |
| { | |
| interactableImage.enabled = false; | |
| } | |
| } | |
| private void OnDestroy() | |
| { | |
| _cancellationTokenSource.Cancel(); | |
| } | |
| private void OnMouseEnter() | |
| { | |
| if(LockParent.gameObject.activeSelf) | |
| return; | |
| _cancellationTokenSource.Cancel(); | |
| _cancellationTokenSource = new CancellationTokenSource(); | |
| if(!LockHover) | |
| OnMouseEnterAsync(_cancellationTokenSource.Token).Forget(); | |
| } | |
| private void OnMouseExit() | |
| { | |
| if(LockParent.gameObject.activeSelf) | |
| return; | |
| _cancellationTokenSource.Cancel(); | |
| _cancellationTokenSource = new CancellationTokenSource(); | |
| if(!LockHover) | |
| OnMouseExitAsync(_cancellationTokenSource.Token).Forget(); | |
| } | |
| private async UniTask OnMouseEnterAsync(CancellationToken token) | |
| { | |
| await UniTask.WaitForSeconds(.2f, cancellationToken: token); | |
| await Hover.DOColor(fullColor, .5f).ToUniTask(TweenCancelBehaviour.Kill, token); | |
| while (!token.IsCancellationRequested) | |
| { | |
| await Hover.DOColor(halfColor, .5f).ToUniTask(TweenCancelBehaviour.Kill, token); | |
| await Hover.DOColor(fullColor, .5f).ToUniTask(TweenCancelBehaviour.Kill, token); | |
| } | |
| } | |
| private async UniTask OnMouseExitAsync(CancellationToken token) | |
| { | |
| await UniTask.WaitForSeconds(.2f, cancellationToken: token); | |
| await Hover.DOColor(clear, .5f).ToUniTask(TweenCancelBehaviour.Kill, token); | |
| } | |
| public void Resolve() | |
| { | |
| foreach (var interactableImage in InteractableImages) | |
| { | |
| interactableImage.Resolve(); | |
| } | |
| } | |
| public void SetRandom(bool canResolve = true) | |
| { | |
| foreach (var interactableImage in InteractableImages) | |
| { | |
| interactableImage.SetRandom(canResolve); | |
| } | |
| } | |
| public void ResetSize() | |
| { | |
| SpritePiece.ResetScale(); | |
| } | |
| } | |
| } | |
| using System; | |
| using System.Collections.Generic; | |
| using Cysharp.Threading.Tasks; | |
| using Newtonsoft.Json; | |
| using ProjectX.CodeBase.Core; | |
| using UnityEngine; | |
| using UnityEngine.Networking; | |
| using UnityEngine.Video; | |
| namespace ProjectX.CodeBase.SpriteSlicer | |
| { | |
| public class VideoSlicer : MonoBehaviour | |
| { | |
| public IReadOnlyList<SpritePieceComponent> Pieces => _pieces; | |
| [SerializeField] private VideoPlayer _videoPlayer; | |
| [SerializeField] private RenderTexture _renderTexture; | |
| [SerializeField] private Material _spriteMaterial; | |
| [SerializeField] private SpritePieceComponent _planePrefab; | |
| private readonly List<SpritePieceComponent> _pieces = new List<SpritePieceComponent>(); | |
| private string _jsonData; | |
| private SpriteSliceData _sliceData; | |
| private int _tryVideoLoading; | |
| private bool _handlersBound; | |
| private bool _prepared; | |
| private bool _error; | |
| public async UniTask Init(string jsonData, string clipName) | |
| { | |
| _jsonData = jsonData; | |
| _tryVideoLoading = 0; | |
| BindHandlersOnce(); | |
| var loaded = await TryLoadAndPlay(clipName); | |
| if (!loaded) | |
| { | |
| G.OpenMenu().Forget(); | |
| return; | |
| } | |
| ParseJsonAndCreatePieces(); | |
| } | |
| private void BindHandlersOnce() | |
| { | |
| if (_handlersBound) return; | |
| _videoPlayer.errorReceived += OnVideoError; | |
| _videoPlayer.prepareCompleted += OnVideoPrepared; | |
| _videoPlayer.playOnAwake = false; | |
| _videoPlayer.waitForFirstFrame = true; | |
| _handlersBound = true; | |
| } | |
| private void OnVideoPrepared(VideoPlayer source) | |
| { | |
| _prepared = true; | |
| } | |
| private void OnVideoError(VideoPlayer vp, string message) | |
| { | |
| Debug.LogError($"Video error: {message}"); | |
| _error = true; | |
| } | |
| private async UniTask<bool> TryLoadAndPlay(string clipName) | |
| { | |
| var url = System.IO.Path.Combine(Application.streamingAssetsPath, clipName); | |
| Debug.Log($"Video URL: {url}"); | |
| while (_tryVideoLoading < 3) | |
| { | |
| _tryVideoLoading++; | |
| _prepared = false; | |
| _error = false; | |
| #if UNITY_WEBGL && !UNITY_EDITOR | |
| var accessible = await CheckUrlAccessible(url); | |
| if (!accessible) | |
| { | |
| Debug.LogError($"Video not accessible at url: {url}"); | |
| continue; | |
| } | |
| #endif | |
| HardResetPlayer(); | |
| _videoPlayer.url = url; | |
| _videoPlayer.Prepare(); | |
| var timeoutSeconds = 20f; | |
| var start = Time.realtimeSinceStartup; | |
| while (!_prepared && !_error && Time.realtimeSinceStartup - start < timeoutSeconds) | |
| await UniTask.Yield(PlayerLoopTiming.Update); | |
| if (_error) | |
| { | |
| Debug.LogError($"Video prepare error on try #{_tryVideoLoading}"); | |
| continue; | |
| } | |
| if (!_prepared) | |
| { | |
| Debug.LogError($"Video prepare timeout on try #{_tryVideoLoading}"); | |
| continue; | |
| } | |
| Debug.Log($"Video prepared successfully on try #{_tryVideoLoading}"); | |
| _videoPlayer.Play(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| #if UNITY_WEBGL && !UNITY_EDITOR | |
| private async UniTask<bool> CheckUrlAccessible(string url) | |
| { | |
| using var req = UnityWebRequest.Get(url); | |
| req.downloadHandler = new DownloadHandlerBuffer(); | |
| await req.SendWebRequest(); | |
| if (req.result != UnityWebRequest.Result.Success) | |
| { | |
| Debug.LogError($"Video file not accessible: {req.error}"); | |
| return false; | |
| } | |
| return true; | |
| } | |
| #endif | |
| private void HardResetPlayer() | |
| { | |
| if (_videoPlayer.isPlaying) | |
| _videoPlayer.Stop(); | |
| _videoPlayer.url = null; | |
| _videoPlayer.targetTexture = _renderTexture; | |
| } | |
| private void ParseJsonAndCreatePieces() | |
| { | |
| if (string.IsNullOrEmpty(_jsonData)) | |
| { | |
| Debug.LogError("JSON data is empty"); | |
| return; | |
| } | |
| try | |
| { | |
| var settings = new JsonSerializerSettings | |
| { | |
| ReferenceLoopHandling = ReferenceLoopHandling.Ignore, | |
| NullValueHandling = NullValueHandling.Ignore, | |
| MissingMemberHandling = MissingMemberHandling.Ignore | |
| }; | |
| _sliceData = JsonConvert.DeserializeObject<SpriteSliceData>(_jsonData, settings); | |
| if (_sliceData == null || _sliceData.Pieces == null) | |
| { | |
| Debug.LogError("Failed to parse JSON or no pieces found"); | |
| return; | |
| } | |
| CreateRootPieces(); | |
| } | |
| catch (Exception e) | |
| { | |
| Debug.LogError($"Error parsing JSON: {e.Message}"); | |
| } | |
| } | |
| private void CreateRootPieces() | |
| { | |
| foreach (var piece in _sliceData.Pieces) | |
| CreatePieceComponent(piece, null); | |
| } | |
| private SpritePieceComponent CreatePieceComponent(SpritePiece pieceData, Transform parent) | |
| { | |
| if (_planePrefab == null) | |
| { | |
| Debug.LogError("Plane prefab is not assigned"); | |
| return null; | |
| } | |
| var pieceComponent = Instantiate(_planePrefab, parent != null ? parent : transform); | |
| pieceComponent.name = pieceData.Name; | |
| var hasChildren = pieceData.Children != null && pieceData.Children.Count > 0; | |
| if (!hasChildren) | |
| { | |
| Vector2 uvMin = new Vector2( | |
| pieceData.Rect.x / _sliceData.OriginalSpriteSize.x, | |
| pieceData.Rect.y / _sliceData.OriginalSpriteSize.y | |
| ); | |
| Vector2 uvMax = new Vector2( | |
| (pieceData.Rect.x + pieceData.Rect.width) / _sliceData.OriginalSpriteSize.x, | |
| (pieceData.Rect.y + pieceData.Rect.height) / _sliceData.OriginalSpriteSize.y | |
| ); | |
| float baseScale = 10f; | |
| Vector3 worldScale = new Vector3( | |
| (pieceData.Rect.width / _sliceData.OriginalSpriteSize.x) * baseScale, | |
| (pieceData.Rect.height / _sliceData.OriginalSpriteSize.y) * baseScale, | |
| 1f | |
| ); | |
| Vector3 worldPosition; | |
| if (parent == null) | |
| { | |
| worldPosition = new Vector3( | |
| ((pieceData.Rect.x + pieceData.Rect.width * 0.5f) / _sliceData.OriginalSpriteSize.x - 0.5f) * | |
| baseScale, | |
| ((pieceData.Rect.y + pieceData.Rect.height * 0.5f) / _sliceData.OriginalSpriteSize.y - 0.5f) * | |
| baseScale, | |
| 0f | |
| ); | |
| } | |
| else | |
| { | |
| Rect parentRect = GetParentRect(parent); | |
| float relativeX = | |
| (pieceData.Rect.x + pieceData.Rect.width * 0.5f - parentRect.x - parentRect.width * 0.5f) / | |
| _sliceData.OriginalSpriteSize.x * baseScale; | |
| float relativeY = | |
| (pieceData.Rect.y + pieceData.Rect.height * 0.5f - parentRect.y - parentRect.height * 0.5f) / | |
| _sliceData.OriginalSpriteSize.y * baseScale; | |
| worldPosition = new Vector3(relativeX, relativeY, 0f); | |
| } | |
| pieceComponent.Initialize(pieceData, _renderTexture, _spriteMaterial, uvMin, uvMax, worldPosition, | |
| worldScale); | |
| _pieces.Add(pieceComponent); | |
| } | |
| else | |
| { | |
| float baseScale = 10f; | |
| Vector3 worldPosition; | |
| if (parent == null) | |
| { | |
| worldPosition = new Vector3( | |
| ((pieceData.Rect.x + pieceData.Rect.width * 0.5f) / _sliceData.OriginalSpriteSize.x - 0.5f) * | |
| baseScale, | |
| ((pieceData.Rect.y + pieceData.Rect.height * 0.5f) / _sliceData.OriginalSpriteSize.y - 0.5f) * | |
| baseScale, | |
| 0f | |
| ); | |
| } | |
| else | |
| { | |
| Rect parentRect = GetParentRect(parent); | |
| float relativeX = | |
| (pieceData.Rect.x + pieceData.Rect.width * 0.5f - parentRect.x - parentRect.width * 0.5f) / | |
| _sliceData.OriginalSpriteSize.x * baseScale; | |
| float relativeY = | |
| (pieceData.Rect.y + pieceData.Rect.height * 0.5f - parentRect.y - parentRect.height * 0.5f) / | |
| _sliceData.OriginalSpriteSize.y * baseScale; | |
| worldPosition = new Vector3(relativeX, relativeY, 0f); | |
| } | |
| pieceComponent.Initialize(pieceData, null, null, Vector2.zero, Vector2.zero, worldPosition, | |
| Vector3.one); | |
| var renderer = pieceComponent.GetComponent<Renderer>(); | |
| if (renderer != null) renderer.enabled = false; | |
| foreach (var childPiece in pieceData.Children) | |
| CreatePieceComponent(childPiece, pieceComponent.transform); | |
| pieceComponent.AnimatedPieceMonoBeh.Outline.enabled = false; | |
| pieceComponent.AnimatedPieceMonoBeh.Collider.enabled = false; | |
| pieceComponent.AnimatedPieceMonoBeh.Lock.enabled = false; | |
| pieceComponent.AnimatedPieceMonoBeh.LockParent.enabled = false; | |
| } | |
| return pieceComponent; | |
| } | |
| private Rect GetParentRect(Transform parent) | |
| { | |
| var parentComponent = parent.GetComponent<SpritePieceComponent>(); | |
| if (parentComponent != null && parentComponent.PieceData != null) | |
| return parentComponent.PieceData.Rect; | |
| return new Rect(0, 0, _sliceData.OriginalSpriteSize.x, _sliceData.OriginalSpriteSize.y); | |
| } | |
| private void OnDestroy() | |
| { | |
| if (_handlersBound) | |
| { | |
| _videoPlayer.errorReceived -= OnVideoError; | |
| _videoPlayer.prepareCompleted -= OnVideoPrepared; | |
| } | |
| if (_renderTexture != null) | |
| _renderTexture.Release(); | |
| } | |
| } | |
| } | |
| using System; | |
| using System.Collections.Generic; | |
| using UnityEngine; | |
| using Newtonsoft.Json; | |
| [Serializable] | |
| public class SpritePiece | |
| { | |
| [JsonProperty("Name")] | |
| public string Name; | |
| [JsonProperty("Rect")] | |
| public Rect Rect; | |
| [JsonProperty("DepthIndex")] | |
| public byte DepthIndex; | |
| [JsonProperty("IsBorder")] | |
| public bool IsBorder; | |
| [JsonProperty("Children")] | |
| public List<SpritePiece> Children = new List<SpritePiece>(); | |
| [JsonProperty("IsExpanded")] | |
| public bool IsExpanded = true; | |
| [JsonProperty("IsLock")] | |
| public bool IsLock = false; | |
| public SpritePiece() { } | |
| public SpritePiece(string name, Rect rect, byte depthIndex = 0, bool isBorder = false) | |
| { | |
| this.Name = name; | |
| this.Rect = rect; | |
| this.DepthIndex = depthIndex; | |
| IsBorder = isBorder; | |
| } | |
| } | |
| [Serializable] | |
| public class SpriteSliceData | |
| { | |
| [JsonProperty("OriginalSpritePath")] | |
| public string OriginalSpritePath; | |
| [JsonProperty("OriginalSpriteSize")] | |
| public Vector2 OriginalSpriteSize; | |
| [JsonProperty("VideoUrl")] | |
| public string VideoUrl = ""; | |
| [JsonProperty("Description")] | |
| public string DescriptionLoc = ""; | |
| [JsonProperty("IsRotatable")] | |
| public bool IsRotatable = false; | |
| [JsonProperty("IsMirrored")] | |
| public bool IsMirrored = false; | |
| [JsonProperty("Pieces")] | |
| public List<SpritePiece> Pieces = new List<SpritePiece>(); | |
| public SpriteSliceData() { } | |
| public SpriteSliceData(string spritePath, Vector2 spriteSize) | |
| { | |
| OriginalSpritePath = spritePath; | |
| OriginalSpriteSize = spriteSize; | |
| } | |
| } | |
| Shader "Custom/SpriteClipShader" | |
| { | |
| Properties | |
| { | |
| _MainTex ("Video Texture", 2D) = "white" {} | |
| _UVMin ("UV Min", Vector) = (0, 0, 0, 0) | |
| _UVMax ("UV Max", Vector) = (1, 1, 0, 0) | |
| _UVCenter ("UV Center", Vector) = (0.5, 0.5, 0, 0) | |
| _RotationZ ("Rotation Z", Float) = 0 | |
| _Color ("Tint Color", Color) = (1, 1, 1, 1) | |
| _ColorBlendMode ("Color Blend Mode", Range(0, 2)) = 0 | |
| } | |
| SubShader | |
| { | |
| Tags | |
| { | |
| "RenderType"="Transparent" | |
| "Queue"="Transparent" | |
| } | |
| Blend SrcAlpha OneMinusSrcAlpha | |
| Cull Off | |
| ZWrite Off | |
| Pass | |
| { | |
| CGPROGRAM | |
| #pragma vertex vert | |
| #pragma fragment frag | |
| #include "UnityCG.cginc" | |
| struct appdata | |
| { | |
| float4 vertex : POSITION; | |
| float2 uv : TEXCOORD0; | |
| }; | |
| struct v2f | |
| { | |
| float2 uv : TEXCOORD0; | |
| float4 vertex : SV_POSITION; | |
| }; | |
| sampler2D _MainTex; | |
| float4 _MainTex_ST; | |
| float2 _UVMin; | |
| float2 _UVMax; | |
| float2 _UVCenter; | |
| float _RotationZ; | |
| fixed4 _Color; | |
| float _ColorBlendMode; | |
| float2 RotateUV(float2 uv, float2 center, float angle) | |
| { | |
| float2 dir = uv - center; | |
| float cosAngle = cos(angle); | |
| float sinAngle = sin(angle); | |
| float2 rotated; | |
| rotated.x = dir.x * cosAngle - dir.y * sinAngle; | |
| rotated.y = dir.x * sinAngle + dir.y * cosAngle; | |
| return rotated + center; | |
| } | |
| fixed4 BlendColor(fixed4 base, fixed4 blend, float mode) | |
| { | |
| if (mode < 0.5) | |
| { | |
| return base * blend; | |
| } | |
| else if (mode < 1.5) | |
| { | |
| return lerp(base, blend, blend.a); | |
| } | |
| else | |
| { | |
| return base + blend; | |
| } | |
| } | |
| v2f vert (appdata v) | |
| { | |
| v2f o; | |
| o.vertex = UnityObjectToClipPos(v.vertex); | |
| o.uv = TRANSFORM_TEX(v.uv, _MainTex); | |
| return o; | |
| } | |
| fixed4 frag (v2f i) : SV_Target | |
| { | |
| float2 localUV = i.uv; | |
| if (_RotationZ != 0) | |
| { | |
| localUV = RotateUV(localUV, _UVCenter, _RotationZ); | |
| } | |
| float2 clippedUV = lerp(_UVMin, _UVMax, localUV); | |
| if (clippedUV.x < 0 || clippedUV.x > 1 || clippedUV.y < 0 || clippedUV.y > 1) | |
| { | |
| return fixed4(0, 0, 0, 0); | |
| } | |
| fixed4 col = tex2D(_MainTex, clippedUV); | |
| col = BlendColor(col, _Color, _ColorBlendMode); | |
| return col; | |
| } | |
| ENDCG | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment