using System; using System.Collections.Generic; using System.Linq; using UnityEditor.U2D.Common; using UnityEditor.U2D.Layout; using UnityEngine; using UnityEngine.U2D; namespace UnityEditor.U2D.Animation { internal interface ICopyToolStringStore { string stringStore { get; set; } } internal class SystemCopyBufferStringStore : ICopyToolStringStore { public string stringStore { get => EditorGUIUtility.systemCopyBuffer; set => EditorGUIUtility.systemCopyBuffer = value; } } internal class BoneStorage { public BoneCache[] bones; public Dictionary boneMapping; public BoneStorage(BoneCache[] bones, Dictionary boneMapping = null) { this.bones = bones; this.boneMapping = boneMapping ?? new Dictionary(); } } internal class CopyTool : MeshToolWrapper { ICopyToolStringStore m_CopyToolStringStore; CopyToolView m_CopyToolView; bool m_HasValidCopyData = false; int m_LastCopyDataHash; public float pixelsPerUnit { private get; set; } public bool hasValidCopiedData { get { int hashCode = m_CopyToolStringStore.stringStore.GetHashCode(); if (hashCode != m_LastCopyDataHash) { m_HasValidCopyData = IsValidCopyData(m_CopyToolStringStore.stringStore); m_LastCopyDataHash = hashCode; } return m_HasValidCopyData; } } public ICopyToolStringStore copyToolStringStore { set => m_CopyToolStringStore = value; } internal override void OnCreate() { m_CopyToolView = new CopyToolView(); m_CopyToolView.onPasteActivated += OnPasteActivated; m_CopyToolStringStore = new SystemCopyBufferStringStore(); disableMeshEditor = true; } public override void Initialize(LayoutOverlay layout) { m_CopyToolView.Initialize(layout); } protected override void OnActivate() { base.OnActivate(); m_CopyToolView.Show(skinningCache.bonesReadOnly); } protected override void OnDeactivate() { base.OnDeactivate(); m_CopyToolView.Hide(); } void CopyMeshFromSpriteCache(SpriteCache sprite, SkinningCopySpriteData skinningSpriteData) { if (meshTool == null) return; meshTool.SetupSprite(sprite); skinningSpriteData.vertices = meshTool.mesh.vertices; skinningSpriteData.vertexWeights = meshTool.mesh.vertexWeights; skinningSpriteData.indices = meshTool.mesh.indices; skinningSpriteData.edges = meshTool.mesh.edges; skinningSpriteData.boneWeightGuids = new List(meshTool.mesh.bones.Length); skinningSpriteData.boneWeightNames = new List(meshTool.mesh.bones.Length); foreach (BoneCache bone in meshTool.mesh.bones) { skinningSpriteData.boneWeightGuids.Add(bone.guid); skinningSpriteData.boneWeightNames.Add(bone.name); } } public void OnCopyActivated() { SkinningCopyData skinningCopyData = null; SpriteCache selectedSprite = skinningCache.selectedSprite; if (selectedSprite == null) { SpriteCache[] sprites = skinningCache.GetSprites(); if (!skinningCache.character || sprites.Length > 1) skinningCopyData = CopyAll(); else if (sprites.Length == 1) skinningCopyData = CopySingle(sprites[0]); } else { skinningCopyData = CopySingle(selectedSprite); } if (skinningCopyData != null) m_CopyToolStringStore.stringStore = SkinningCopyUtility.SerializeSkinningCopyDataToString(skinningCopyData); skinningCache.events.copy.Invoke(); } SkinningCopyData CopyAll() { SkinningCopyData skinningCopyData = new SkinningCopyData { pixelsPerUnit = pixelsPerUnit, isCharacterData = skinningCache.hasCharacter, characterBones = skinningCache.hasCharacter ? skinningCache.character.skeleton.bones.ToSpriteBone(Matrix4x4.identity) : null }; SpriteCache[] sprites = skinningCache.GetSprites(); foreach (SpriteCache sprite in sprites) skinningCopyData.copyData.Add(GetDataForSprite(sprite)); if (meshTool != null) meshTool.SetupSprite(null); return skinningCopyData; } SkinningCopyData CopySingle(SpriteCache sprite) { SkinningCopyData skinningCopyData = new SkinningCopyData { pixelsPerUnit = pixelsPerUnit, isCharacterData = false, characterBones = Array.Empty() }; skinningCopyData.copyData.Add(GetDataForSprite(sprite)); return skinningCopyData; } SkinningCopySpriteData GetDataForSprite(SpriteCache sprite) { SkinningCopySpriteData skinningSpriteData = new SkinningCopySpriteData(); skinningSpriteData.spriteName = sprite.name; CopyMeshFromSpriteCache(sprite, skinningSpriteData); // Bones List rootBones = new List(); BoneCache[] boneCache = Array.Empty(); if (skinningCache.hasCharacter) { CharacterPartCache characterPart = skinningCache.GetCharacterPart(sprite); if (characterPart != null && characterPart.bones != null) { boneCache = characterPart.bones; BoneCache[] bones = characterPart.bones.FindRoots(); foreach (BoneCache bone in bones) rootBones.Add(bone); } } else { SkeletonCache skeleton = skinningCache.GetEffectiveSkeleton(sprite); if (skeleton != null && skeleton.boneCount > 0) { boneCache = skeleton.bones; BoneCache[] bones = boneCache.FindRoots(); foreach (BoneCache bone in bones) rootBones.Add(bone); } } if (rootBones.Count > 0) { skinningSpriteData.spriteBones = new List(); foreach (BoneCache rootBone in rootBones) { int rootBoneIndex = skinningSpriteData.spriteBones.Count; GetSpriteBoneDataRecursively(skinningSpriteData.spriteBones, rootBone, new List(boneCache)); if (skinningCache.hasCharacter) { // Offset the bones based on the currently selected Sprite in Character mode CharacterPartCache characterPart = sprite.GetCharacterPart(); if (characterPart != null) { Vector3 offset = characterPart.position; SpriteBoneCopyData rootSpriteBone = skinningSpriteData.spriteBones[rootBoneIndex]; rootSpriteBone.spriteBone.position -= offset; skinningSpriteData.spriteBones[rootBoneIndex] = rootSpriteBone; } } } } return skinningSpriteData; } static void GetSpriteBoneDataRecursively(IList bones, BoneCache rootBone, List boneCache) { AppendSpriteBoneDataRecursively(bones, rootBone, -1, boneCache); } static void AppendSpriteBoneDataRecursively(IList bones, BoneCache currentBone, int parentIndex, List boneCache) { int currentParentIndex = bones.Count; SpriteBoneCopyData boneCopyData = new SpriteBoneCopyData() { spriteBone = new SpriteBone() { name = currentBone.name, guid = currentBone.guid, color = currentBone.bindPoseColor, parentId = parentIndex }, order = boneCache.FindIndex(x => x == currentBone) }; if (boneCopyData.order < 0) { boneCopyData.order = boneCache.Count; boneCache.Add(currentBone); } if (parentIndex == -1 && currentBone.parentBone != null) { boneCopyData.spriteBone.position = currentBone.position; boneCopyData.spriteBone.rotation = currentBone.rotation; } else { boneCopyData.spriteBone.position = currentBone.localPosition; boneCopyData.spriteBone.rotation = currentBone.localRotation; } boneCopyData.spriteBone.position = new Vector3(boneCopyData.spriteBone.position.x, boneCopyData.spriteBone.position.y, currentBone.depth); boneCopyData.spriteBone.length = currentBone.localLength; bones.Add(boneCopyData); foreach (TransformCache child in currentBone) { BoneCache childBone = child as BoneCache; if (childBone != null) AppendSpriteBoneDataRecursively(bones, childBone, currentParentIndex, boneCache); } } public void OnPasteActivated(bool shouldPasteBones, bool shouldPasteMesh, bool shouldFlipX, bool shouldFlipY) { string copyBuffer = m_CopyToolStringStore.stringStore; if (!IsValidCopyData(copyBuffer)) return; SkinningCopyData skinningCopyData = SkinningCopyUtility.DeserializeStringToSkinningCopyData(copyBuffer); if (skinningCopyData == null || skinningCopyData.copyData.Count == 0) return; bool doesCopyContainMultipleSprites = skinningCopyData.copyData.Count > 1; SpriteCache[] sprites = skinningCache.GetSprites(); if (doesCopyContainMultipleSprites && skinningCopyData.copyData.Count != sprites.Length && shouldPasteMesh) { Debug.Log(string.Format(TextContent.copyIncorrectNumberOfSprites, skinningCopyData.copyData.Count, sprites.Length)); return; } SpriteCache selectedSprite = skinningCache.selectedSprite; using (skinningCache.UndoScope(TextContent.pasteData)) { float scale = skinningCopyData.pixelsPerUnit > 0f ? pixelsPerUnit / skinningCopyData.pixelsPerUnit : 1f; HashSet pastedBonesToSelect = new HashSet(); BoneCache[] characterBones = Array.Empty(); bool replaceCharacterSkeleton = shouldPasteBones && skinningCache.hasCharacter && skinningCopyData.isCharacterData; if (replaceCharacterSkeleton) { SpriteBone[] spriteBones = skinningCopyData.characterBones; characterBones = PasteBonesInCharacter(skinningCache, spriteBones, shouldFlipX, shouldFlipY, scale); foreach (BoneCache newBone in characterBones) pastedBonesToSelect.Add(newBone); } List pastedToSprites = new List(); foreach (SkinningCopySpriteData copySpriteData in skinningCopyData.copyData) { SpriteCache sprite = null; if (selectedSprite != null && !doesCopyContainMultipleSprites) sprite = selectedSprite; if (sprite == null && !string.IsNullOrEmpty(copySpriteData.spriteName)) sprite = FindSpriteWithName(sprites.Except(pastedToSprites).ToList(), copySpriteData.spriteName) ?? FindSpriteWithName(sprites, copySpriteData.spriteName); if (sprite == null) continue; pastedToSprites.Add(sprite); Dictionary boneMapping = new Dictionary(); if (shouldPasteBones && !replaceCharacterSkeleton) { SpriteBone[] bonesToPaste = GetBonesInCorrectOrder(copySpriteData.spriteBones); BoneStorage boneStorage = PasteBonesInSprite(skinningCache, sprite, bonesToPaste, characterBones, shouldFlipX, shouldFlipY, scale); if (boneStorage != null) { boneMapping = boneStorage.boneMapping; if (skinningCache.hasCharacter || sprite == selectedSprite) { foreach (BoneCache newBone in boneStorage.bones) pastedBonesToSelect.Add(newBone); } } } if (shouldPasteMesh) PasteMeshInSprite(meshTool, sprite, copySpriteData, shouldFlipX, shouldFlipY, scale, boneMapping); } bool refreshSelection = skinningCache.hasCharacter || skinningCache.selectedSprite != null; if (refreshSelection) { BoneCache[] newBoneSelection = new BoneCache[pastedBonesToSelect.Count]; pastedBonesToSelect.CopyTo(newBoneSelection); meshTool.SetupSprite(selectedSprite); // This is to refresh the selected Sprite in meshTool. skinningCache.skeletonSelection.elements = newBoneSelection; skinningCache.events.boneSelectionChanged.Invoke(); } } skinningCache.events.paste.Invoke(shouldPasteBones, shouldPasteMesh, shouldFlipX, shouldFlipY); } static bool IsValidCopyData(string copyBuffer) { return SkinningCopyUtility.CanDeserializeStringToSkinningCopyData(copyBuffer); } static Vector3 GetFlippedBonePosition(BoneCache bone, Vector2 startPosition, Rect spriteRect, bool flipX, bool flipY) { Vector3 position = startPosition; if (flipX) position.x += spriteRect.width - bone.position.x; else position.x += bone.position.x; if (flipY) position.y += spriteRect.height - bone.position.y; else position.y += bone.position.y; position.z = bone.position.z; return position; } static Quaternion GetFlippedBoneRotation(BoneCache bone, bool flipX, bool flipY) { Vector3 euler = bone.rotation.eulerAngles; if (flipX) { if (euler.z <= 180) euler.z = 180 - euler.z; else euler.z = 540 - euler.z; } if (flipY) euler.z = 360 - euler.z; return Quaternion.Euler(euler); } static void SetBonePositionAndRotation(BoneCache[] boneCache, TransformCache bone, Vector3[] position, Quaternion[] rotation) { int index = Array.FindIndex(boneCache, x => x == bone); if (index >= 0) { bone.position = position[index]; bone.rotation = rotation[index]; } foreach (TransformCache child in bone.children) { SetBonePositionAndRotation(boneCache, child, position, rotation); } } static BoneCache[] PasteBonesInCharacter(SkinningCache skinningCache, SpriteBone[] spriteBones, bool shouldFlipX, bool shouldFlipY, float scale) { if (!skinningCache.hasCharacter) return null; BoneCache[] boneCache = skinningCache.CreateBoneCacheFromSpriteBones(spriteBones, scale); if (shouldFlipX || shouldFlipY) { Rect characterRect = new Rect(Vector2.zero, skinningCache.character.dimension); Vector3[] newPositions = new Vector3[boneCache.Length]; Quaternion[] newRotations = new Quaternion[boneCache.Length]; for (int i = 0; i < boneCache.Length; ++i) { newPositions[i] = GetFlippedBonePosition(boneCache[i], Vector2.zero, characterRect, shouldFlipX, shouldFlipY); newRotations[i] = GetFlippedBoneRotation(boneCache[i], shouldFlipX, shouldFlipY); } for (int i = 0; i < boneCache.Length; ++i) { boneCache[i].position = newPositions[i]; boneCache[i].rotation = newRotations[i]; } } SkeletonCache skeleton = skinningCache.character.skeleton; skeleton.SetBones(boneCache); skinningCache.events.skeletonTopologyChanged.Invoke(skeleton); return boneCache; } static BoneStorage PasteBonesInSprite(SkinningCache skinningCache, SpriteCache sprite, SpriteBone[] newBones, BoneCache[] characterBones, bool shouldFlipX, bool shouldFlipY, float scale) { if (sprite == null || skinningCache.mode == SkinningMode.SpriteSheet && skinningCache.hasCharacter) return null; Rect spriteRect = sprite.textureRect; SkeletonCache skeleton = skinningCache.GetEffectiveSkeleton(sprite); BoneCache[] newBonesCache = skinningCache.CreateBoneCacheFromSpriteBones(newBones, scale); if (newBonesCache.Length == 0) return null; Vector2 rectPosition; if (skinningCache.mode == SkinningMode.Character) { CharacterPartCache characterPart = sprite.GetCharacterPart(); if (characterPart == null) return null; rectPosition = characterPart.position; } else rectPosition = spriteRect.position; Vector3[] newPositions = new Vector3[newBonesCache.Length]; Quaternion[] newRotations = new Quaternion[newBonesCache.Length]; for (int i = 0; i < newBonesCache.Length; ++i) { newPositions[i] = GetFlippedBonePosition(newBonesCache[i], rectPosition, spriteRect, shouldFlipX, shouldFlipY); newRotations[i] = GetFlippedBoneRotation(newBonesCache[i], shouldFlipX, shouldFlipY); } foreach (BoneCache bone in newBonesCache) { if (bone.parent == null) { SetBonePositionAndRotation(newBonesCache, bone, newPositions, newRotations); if (skinningCache.mode == SkinningMode.Character) bone.SetParent(skeleton); } } Dictionary boneNameMapping = new Dictionary(); if (skinningCache.mode == SkinningMode.SpriteSheet) { skeleton.SetBones(newBonesCache); skeleton.SetDefaultPose(); } else { boneNameMapping = AddBonesToSkeletonWithUniqueNames(characterBones, newBonesCache, skeleton); skeleton.SetDefaultPose(); } skinningCache.events.skeletonTopologyChanged.Invoke(skeleton); return new BoneStorage(newBonesCache, boneNameMapping); } static Dictionary AddBonesToSkeletonWithUniqueNames(IList characterBones, IList newBones, SkeletonCache skeleton) { Dictionary nameMapping = new Dictionary(); HashSet existingBoneGuids = new HashSet(); HashSet existingBoneNames = new HashSet(skeleton.boneCount); for (int i = 0; i < characterBones.Count; i++) { if (!string.IsNullOrEmpty(characterBones[i].guid)) existingBoneGuids.Add(characterBones[i].guid); } for (int i = 0; i < skeleton.boneCount; i++) existingBoneNames.Add(skeleton.bones[i].name); foreach (BoneCache newBone in newBones) { string guid = newBone.guid; if (string.IsNullOrEmpty(guid) || existingBoneGuids.Contains(guid)) continue; string boneName = newBone.name; if (existingBoneNames.Contains(boneName)) newBone.name = SkeletonController.AutoNameBoneCopy(boneName, skeleton.bones); existingBoneGuids.Add(newBone.guid); existingBoneNames.Add(newBone.name); nameMapping[boneName] = newBone.name; skeleton.AddBone(newBone); } return nameMapping; } static void PasteMeshInSprite(MeshTool meshTool, SpriteCache sprite, SkinningCopySpriteData copySpriteData, bool shouldFlipX, bool shouldFlipY, float scale, Dictionary boneMapping) { if (meshTool == null || sprite == null) return; Vector2[] vertices = copySpriteData.vertices ?? Array.Empty(); EditableBoneWeight[] vertexWeights = copySpriteData.vertexWeights ?? Array.Empty(); meshTool.SetupSprite(sprite); meshTool.mesh.SetVertices(vertices, vertexWeights); if (!Mathf.Approximately(scale, 1f) || shouldFlipX || shouldFlipY) { Rect spriteRect = sprite.textureRect; for (int i = 0; i < meshTool.mesh.vertexCount; ++i) { Vector2 position = meshTool.mesh.vertices[i]; if (!Mathf.Approximately(scale, 1f)) position *= scale; if (shouldFlipX) position.x = spriteRect.width - meshTool.mesh.vertices[i].x; if (shouldFlipY) position.y = spriteRect.height - meshTool.mesh.vertices[i].y; meshTool.mesh.vertices[i] = position; } } meshTool.mesh.SetIndices(copySpriteData.indices); meshTool.mesh.SetEdges(copySpriteData.edges); SkinningCache skinningCache = meshTool.skinningCache; SkeletonCache skeleton = skinningCache.GetEffectiveSkeleton(sprite); bool hasGuids = copySpriteData.boneWeightGuids.Count > 0; for (int i = 0; i < copySpriteData.boneWeightGuids.Count; i++) { if (string.IsNullOrEmpty(copySpriteData.boneWeightGuids[i])) { hasGuids = false; break; } } BoneCache[] skeletonBones = skeleton.bones; BoneCache[] influenceBones = hasGuids ? GetBonesFromGuids(copySpriteData, skeletonBones, boneMapping) : GetBonesFromNames(copySpriteData, skeletonBones, boneMapping); // Update associated bones for mesh meshTool.mesh.SetCompatibleBoneSet(influenceBones); meshTool.mesh.bones = influenceBones; // Fixes weights for bones that do not exist // Update associated bones for character if (skinningCache.hasCharacter) { CharacterPartCache characterPart = sprite.GetCharacterPart(); if (characterPart != null) { characterPart.bones = influenceBones; skinningCache.events.characterPartChanged.Invoke(characterPart); } } meshTool.UpdateMesh(); } static BoneCache[] GetBonesFromGuids(SkinningCopySpriteData copySpriteData, IList skeletonBones, Dictionary boneMapping) { List spriteBones = new List(); for (int i = 0; i < copySpriteData.boneWeightGuids.Count; i++) { BoneCache bone = FindBoneWithGuid(skeletonBones, copySpriteData.boneWeightGuids[i]); if (bone == null) continue; if (boneMapping != null && boneMapping.ContainsKey(bone.name)) { bone = FindBoneWithName(skeletonBones, boneMapping[bone.name]); if (bone == null) continue; } spriteBones.Add(bone); } return spriteBones.ToArray(); } static BoneCache[] GetBonesFromNames(SkinningCopySpriteData copySpriteData, IList skeletonBones, Dictionary boneMapping) { List spriteBones = new List(); for (int i = 0; i < copySpriteData.boneWeightNames.Count; ++i) { string boneName = copySpriteData.boneWeightNames[i]; if (boneMapping != null && boneMapping.ContainsKey(boneName)) boneName = boneMapping[boneName]; BoneCache bone = FindBoneWithName(skeletonBones, boneName); if (bone == null) continue; spriteBones.Add(bone); } return spriteBones.ToArray(); } static SpriteBone[] GetBonesInCorrectOrder(IList spriteBones) { SpriteBone[] orderedBones = new SpriteBone[spriteBones.Count]; for (int i = 0; i < spriteBones.Count; ++i) { int order = spriteBones[i].order; if (order >= 0) { orderedBones[order] = spriteBones[i].spriteBone; int parentId = orderedBones[order].parentId; if (parentId >= 0) orderedBones[order].parentId = spriteBones[parentId].order; } else { orderedBones[i] = spriteBones[i].spriteBone; } } return orderedBones; } static SpriteCache FindSpriteWithName(IList sprites, string spriteName) { for (int i = 0; i < sprites.Count; i++) { SpriteCache sprite = sprites[i]; if (sprite.name == spriteName) return sprite; } return null; } static BoneCache FindBoneWithName(IList bones, string boneName) { for (int i = 0; i < bones.Count; i++) { BoneCache bone = bones[i]; if (bone.name == boneName) return bone; } return null; } static BoneCache FindBoneWithGuid(IList bones, string guid) { for (int i = 0; i < bones.Count; i++) { BoneCache bone = bones[i]; if (bone.guid == guid) return bone; } return null; } } internal class CopyToolView { PastePanel m_PastePanel; public event Action onPasteActivated = (bone, mesh, flipX, flipY) => { }; public void Show(bool readonlyBone) { m_PastePanel.SetHiddenFromLayout(false); m_PastePanel.BonePasteEnable(!readonlyBone); } public void Hide() { m_PastePanel.SetHiddenFromLayout(true); } public void Initialize(LayoutOverlay layoutOverlay) { m_PastePanel = PastePanel.GenerateFromUXML(); BindElements(); layoutOverlay.rightOverlay.Add(m_PastePanel); m_PastePanel.SetHiddenFromLayout(true); } void BindElements() { m_PastePanel.onPasteActivated += OnPasteActivated; } void OnPasteActivated(bool bone, bool mesh, bool flipX, bool flipY) { onPasteActivated(bone, mesh, flipX, flipY); } } }