You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2030 lines
76 KiB
2030 lines
76 KiB
2 years ago
|
/******************************************************************************
|
||
|
* Spine Runtimes Software License v2.5
|
||
|
*
|
||
|
* Copyright (c) 2013-2016, Esoteric Software
|
||
|
* All rights reserved.
|
||
|
*
|
||
|
* You are granted a perpetual, non-exclusive, non-sublicensable, and
|
||
|
* non-transferable license to use, install, execute, and perform the Spine
|
||
|
* Runtimes software and derivative works solely for personal or internal
|
||
|
* use. Without the written permission of Esoteric Software (see Section 2 of
|
||
|
* the Spine Software License Agreement), you may not (a) modify, translate,
|
||
|
* adapt, or develop new applications using the Spine Runtimes or otherwise
|
||
|
* create derivative works or improvements of the Spine Runtimes or (b) remove,
|
||
|
* delete, alter, or obscure any trademarks or any copyright, trademark, patent,
|
||
|
* or other intellectual property or proprietary rights notices on or in the
|
||
|
* Software, including any copy thereof. Redistributions in binary or source
|
||
|
* form must include this license and terms.
|
||
|
*
|
||
|
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
|
||
|
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||
|
* EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
|
||
|
* USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||
|
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||
|
*****************************************************************************/
|
||
|
|
||
|
#pragma warning disable 0219
|
||
|
|
||
|
// Original contribution by: Mitch Thompson
|
||
|
|
||
|
#define SPINE_SKELETONANIMATOR
|
||
|
using UnityEngine;
|
||
|
using UnityEditor;
|
||
|
using System.Collections.Generic;
|
||
|
using System.IO;
|
||
|
using System.Text;
|
||
|
using System.Linq;
|
||
|
using System.Reflection;
|
||
|
using Spine;
|
||
|
|
||
|
namespace Spine.Unity.Editor {
|
||
|
using EventType = UnityEngine.EventType;
|
||
|
|
||
|
// Analysis disable once ConvertToStaticType
|
||
|
[InitializeOnLoad]
|
||
|
public class SpineEditorUtilities : AssetPostprocessor {
|
||
|
|
||
|
public static class Icons {
|
||
|
public static Texture2D skeleton;
|
||
|
public static Texture2D nullBone;
|
||
|
public static Texture2D bone;
|
||
|
public static Texture2D poseBones;
|
||
|
public static Texture2D boneNib;
|
||
|
public static Texture2D slot;
|
||
|
public static Texture2D slotRoot;
|
||
|
public static Texture2D skinPlaceholder;
|
||
|
public static Texture2D image;
|
||
|
public static Texture2D genericAttachment;
|
||
|
public static Texture2D boundingBox;
|
||
|
public static Texture2D point;
|
||
|
public static Texture2D mesh;
|
||
|
public static Texture2D weights;
|
||
|
public static Texture2D path;
|
||
|
public static Texture2D clipping;
|
||
|
public static Texture2D skin;
|
||
|
public static Texture2D skinsRoot;
|
||
|
public static Texture2D animation;
|
||
|
public static Texture2D animationRoot;
|
||
|
public static Texture2D spine;
|
||
|
public static Texture2D userEvent;
|
||
|
public static Texture2D constraintNib;
|
||
|
public static Texture2D constraintRoot;
|
||
|
public static Texture2D constraintTransform;
|
||
|
public static Texture2D constraintPath;
|
||
|
public static Texture2D constraintIK;
|
||
|
public static Texture2D warning;
|
||
|
public static Texture2D skeletonUtility;
|
||
|
public static Texture2D hingeChain;
|
||
|
public static Texture2D subMeshRenderer;
|
||
|
public static Texture2D skeletonDataAssetIcon;
|
||
|
public static Texture2D info;
|
||
|
public static Texture2D unity;
|
||
|
// public static Texture2D controllerIcon;
|
||
|
|
||
|
static Texture2D LoadIcon (string filename) {
|
||
|
return (Texture2D)AssetDatabase.LoadMainAssetAtPath(SpineEditorUtilities.editorGUIPath + "/" + filename);
|
||
|
}
|
||
|
|
||
|
public static void Initialize () {
|
||
|
skeleton = LoadIcon("icon-skeleton.png");
|
||
|
nullBone = LoadIcon("icon-null.png");
|
||
|
bone = LoadIcon("icon-bone.png");
|
||
|
poseBones = LoadIcon("icon-poseBones.png");
|
||
|
boneNib = LoadIcon("icon-boneNib.png");
|
||
|
slot = LoadIcon("icon-slot.png");
|
||
|
slotRoot = LoadIcon("icon-slotRoot.png");
|
||
|
skinPlaceholder = LoadIcon("icon-skinPlaceholder.png");
|
||
|
|
||
|
genericAttachment = LoadIcon("icon-attachment.png");
|
||
|
image = LoadIcon("icon-image.png");
|
||
|
boundingBox = LoadIcon("icon-boundingBox.png");
|
||
|
point = LoadIcon("icon-point.png");
|
||
|
mesh = LoadIcon("icon-mesh.png");
|
||
|
weights = LoadIcon("icon-weights.png");
|
||
|
path = LoadIcon("icon-path.png");
|
||
|
clipping = LoadIcon("icon-clipping.png");
|
||
|
|
||
|
skin = LoadIcon("icon-skin.png");
|
||
|
skinsRoot = LoadIcon("icon-skinsRoot.png");
|
||
|
animation = LoadIcon("icon-animation.png");
|
||
|
animationRoot = LoadIcon("icon-animationRoot.png");
|
||
|
spine = LoadIcon("icon-spine.png");
|
||
|
userEvent = LoadIcon("icon-event.png");
|
||
|
constraintNib = LoadIcon("icon-constraintNib.png");
|
||
|
|
||
|
constraintRoot = LoadIcon("icon-constraints.png");
|
||
|
constraintTransform = LoadIcon("icon-constraintTransform.png");
|
||
|
constraintPath = LoadIcon("icon-constraintPath.png");
|
||
|
constraintIK = LoadIcon("icon-constraintIK.png");
|
||
|
|
||
|
warning = LoadIcon("icon-warning.png");
|
||
|
skeletonUtility = LoadIcon("icon-skeletonUtility.png");
|
||
|
hingeChain = LoadIcon("icon-hingeChain.png");
|
||
|
subMeshRenderer = LoadIcon("icon-subMeshRenderer.png");
|
||
|
|
||
|
skeletonDataAssetIcon = LoadIcon("SkeletonDataAsset Icon.png");
|
||
|
|
||
|
info = EditorGUIUtility.FindTexture("console.infoicon.sml");
|
||
|
unity = EditorGUIUtility.FindTexture("SceneAsset Icon");
|
||
|
// controllerIcon = EditorGUIUtility.FindTexture("AnimatorController Icon");
|
||
|
}
|
||
|
|
||
|
public static Texture2D GetAttachmentIcon (Attachment attachment) {
|
||
|
// Analysis disable once CanBeReplacedWithTryCastAndCheckForNull
|
||
|
if (attachment is RegionAttachment)
|
||
|
return Icons.image;
|
||
|
else if (attachment is MeshAttachment)
|
||
|
return ((MeshAttachment)attachment).IsWeighted() ? Icons.weights : Icons.mesh;
|
||
|
else if (attachment is BoundingBoxAttachment)
|
||
|
return Icons.boundingBox;
|
||
|
else if (attachment is PointAttachment)
|
||
|
return Icons.point;
|
||
|
else if (attachment is PathAttachment)
|
||
|
return Icons.path;
|
||
|
else if (attachment is ClippingAttachment)
|
||
|
return Icons.clipping;
|
||
|
else
|
||
|
return Icons.warning;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static string editorPath = "";
|
||
|
public static string editorGUIPath = "";
|
||
|
public static bool initialized;
|
||
|
|
||
|
/// HACK: This list keeps the asset reference temporarily during importing.
|
||
|
///
|
||
|
/// In cases of very large projects/sufficient RAM pressure, when AssetDatabase.SaveAssets is called,
|
||
|
/// Unity can mistakenly unload assets whose references are only on the stack.
|
||
|
/// This leads to MissingReferenceException and other errors.
|
||
|
static readonly List<ScriptableObject> protectFromStackGarbageCollection = new List<ScriptableObject>();
|
||
|
static HashSet<string> assetsImportedInWrongState = new HashSet<string>();
|
||
|
|
||
|
#if SPINE_TK2D
|
||
|
const float DEFAULT_DEFAULT_SCALE = 1f;
|
||
|
#else
|
||
|
const float DEFAULT_DEFAULT_SCALE = 0.01f;
|
||
|
#endif
|
||
|
const string DEFAULT_SCALE_KEY = "SPINE_DEFAULT_SCALE";
|
||
|
public static float defaultScale = DEFAULT_DEFAULT_SCALE;
|
||
|
|
||
|
const float DEFAULT_DEFAULT_MIX = 0.2f;
|
||
|
const string DEFAULT_MIX_KEY = "SPINE_DEFAULT_MIX";
|
||
|
public static float defaultMix = DEFAULT_DEFAULT_MIX;
|
||
|
|
||
|
const string DEFAULT_DEFAULT_SHADER = "Spine/Skeleton";
|
||
|
const string DEFAULT_SHADER_KEY = "SPINE_DEFAULT_SHADER";
|
||
|
public static string defaultShader = DEFAULT_DEFAULT_SHADER;
|
||
|
|
||
|
const float DEFAULT_DEFAULT_ZSPACING = 0f;
|
||
|
const string DEFAULT_ZSPACING_KEY = "SPINE_DEFAULT_ZSPACING";
|
||
|
public static float defaultZSpacing = DEFAULT_DEFAULT_ZSPACING;
|
||
|
|
||
|
const bool DEFAULT_SHOW_HIERARCHY_ICONS = true;
|
||
|
const string SHOW_HIERARCHY_ICONS_KEY = "SPINE_SHOW_HIERARCHY_ICONS";
|
||
|
public static bool showHierarchyIcons = DEFAULT_SHOW_HIERARCHY_ICONS;
|
||
|
|
||
|
const bool DEFAULT_SET_TEXTUREIMPORTER_SETTINGS = true;
|
||
|
const string SET_TEXTUREIMPORTER_SETTINGS_KEY = "SPINE_SET_TEXTUREIMPORTER_SETTINGS";
|
||
|
public static bool setTextureImporterSettings = DEFAULT_SET_TEXTUREIMPORTER_SETTINGS;
|
||
|
|
||
|
internal const float DEFAULT_MIPMAPBIAS = -0.5f;
|
||
|
|
||
|
public const float DEFAULT_SCENE_ICONS_SCALE = 1f;
|
||
|
public const string SCENE_ICONS_SCALE_KEY = "SPINE_SCENE_ICONS_SCALE";
|
||
|
|
||
|
#region Initialization
|
||
|
static SpineEditorUtilities () {
|
||
|
Initialize();
|
||
|
}
|
||
|
|
||
|
static void LoadPreferences () {
|
||
|
defaultMix = EditorPrefs.GetFloat(DEFAULT_MIX_KEY, DEFAULT_DEFAULT_MIX);
|
||
|
defaultScale = EditorPrefs.GetFloat(DEFAULT_SCALE_KEY, DEFAULT_DEFAULT_SCALE);
|
||
|
defaultZSpacing = EditorPrefs.GetFloat(DEFAULT_ZSPACING_KEY, DEFAULT_DEFAULT_ZSPACING);
|
||
|
defaultShader = EditorPrefs.GetString(DEFAULT_SHADER_KEY, DEFAULT_DEFAULT_SHADER);
|
||
|
showHierarchyIcons = EditorPrefs.GetBool(SHOW_HIERARCHY_ICONS_KEY, DEFAULT_SHOW_HIERARCHY_ICONS);
|
||
|
setTextureImporterSettings = EditorPrefs.GetBool(SET_TEXTUREIMPORTER_SETTINGS_KEY, DEFAULT_SET_TEXTUREIMPORTER_SETTINGS);
|
||
|
SpineHandles.handleScale = EditorPrefs.GetFloat(SCENE_ICONS_SCALE_KEY, DEFAULT_SCENE_ICONS_SCALE);
|
||
|
preferencesLoaded = true;
|
||
|
}
|
||
|
|
||
|
static void Initialize () {
|
||
|
LoadPreferences();
|
||
|
|
||
|
DirectoryInfo rootDir = new DirectoryInfo(Application.dataPath);
|
||
|
FileInfo[] files = rootDir.GetFiles("SpineEditorUtilities.cs", SearchOption.AllDirectories);
|
||
|
editorPath = Path.GetDirectoryName(files[0].FullName.Replace("\\", "/").Replace(Application.dataPath, "Assets"));
|
||
|
editorGUIPath = editorPath + "/GUI";
|
||
|
|
||
|
Icons.Initialize();
|
||
|
|
||
|
// Drag and Drop
|
||
|
SceneView.onSceneGUIDelegate -= SceneViewDragAndDrop;
|
||
|
SceneView.onSceneGUIDelegate += SceneViewDragAndDrop;
|
||
|
EditorApplication.hierarchyWindowItemOnGUI -= SpineEditorHierarchyHandler.HierarchyDragAndDrop;
|
||
|
EditorApplication.hierarchyWindowItemOnGUI += SpineEditorHierarchyHandler.HierarchyDragAndDrop;
|
||
|
|
||
|
// Hierarchy Icons
|
||
|
#if UNITY_2017_2_OR_NEWER
|
||
|
EditorApplication.playModeStateChanged -= SpineEditorHierarchyHandler.HierarchyIconsOnPlaymodeStateChanged;
|
||
|
EditorApplication.playModeStateChanged += SpineEditorHierarchyHandler.HierarchyIconsOnPlaymodeStateChanged;
|
||
|
SpineEditorHierarchyHandler.HierarchyIconsOnPlaymodeStateChanged(PlayModeStateChange.EnteredEditMode);
|
||
|
#else
|
||
|
EditorApplication.playmodeStateChanged -= SpineEditorHierarchyHandler.HierarchyIconsOnPlaymodeStateChanged;
|
||
|
EditorApplication.playmodeStateChanged += SpineEditorHierarchyHandler.HierarchyIconsOnPlaymodeStateChanged;
|
||
|
SpineEditorHierarchyHandler.HierarchyIconsOnPlaymodeStateChanged();
|
||
|
#endif
|
||
|
|
||
|
initialized = true;
|
||
|
}
|
||
|
|
||
|
public static void ConfirmInitialization () {
|
||
|
if (!initialized || Icons.skeleton == null)
|
||
|
Initialize();
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Spine Preferences and Defaults
|
||
|
static bool preferencesLoaded = false;
|
||
|
|
||
|
[PreferenceItem("Spine")]
|
||
|
static void PreferencesGUI () {
|
||
|
if (!preferencesLoaded)
|
||
|
LoadPreferences();
|
||
|
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
showHierarchyIcons = EditorGUILayout.Toggle(new GUIContent("Show Hierarchy Icons", "Show relevant icons on GameObjects with Spine Components on them. Disable this if you have large, complex scenes."), showHierarchyIcons);
|
||
|
if (EditorGUI.EndChangeCheck()) {
|
||
|
EditorPrefs.SetBool(SHOW_HIERARCHY_ICONS_KEY, showHierarchyIcons);
|
||
|
#if UNITY_2017_2_OR_NEWER
|
||
|
SpineEditorHierarchyHandler.HierarchyIconsOnPlaymodeStateChanged(PlayModeStateChange.EnteredEditMode);
|
||
|
#else
|
||
|
SpineEditorHierarchyHandler.HierarchyIconsOnPlaymodeStateChanged();
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
EditorGUILayout.Separator();
|
||
|
|
||
|
EditorGUILayout.LabelField("Auto-Import Settings", EditorStyles.boldLabel);
|
||
|
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
defaultMix = EditorGUILayout.FloatField("Default Mix", defaultMix);
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
EditorPrefs.SetFloat(DEFAULT_MIX_KEY, defaultMix);
|
||
|
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
defaultScale = EditorGUILayout.FloatField("Default SkeletonData Scale", defaultScale);
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
EditorPrefs.SetFloat(DEFAULT_SCALE_KEY, defaultScale);
|
||
|
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
var shader = (EditorGUILayout.ObjectField("Default Shader", Shader.Find(defaultShader), typeof(Shader), false) as Shader);
|
||
|
defaultShader = shader != null ? shader.name : DEFAULT_DEFAULT_SHADER;
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
EditorPrefs.SetString(DEFAULT_SHADER_KEY, defaultShader);
|
||
|
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
setTextureImporterSettings = EditorGUILayout.Toggle(new GUIContent("Apply Atlas Texture Settings", "Apply the recommended settings for Texture Importers."), setTextureImporterSettings);
|
||
|
if (EditorGUI.EndChangeCheck()) {
|
||
|
EditorPrefs.SetBool(SET_TEXTUREIMPORTER_SETTINGS_KEY, setTextureImporterSettings);
|
||
|
}
|
||
|
|
||
|
EditorGUILayout.Space();
|
||
|
|
||
|
EditorGUILayout.LabelField("Editor Instantiation", EditorStyles.boldLabel);
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
defaultZSpacing = EditorGUILayout.Slider("Default Slot Z-Spacing", defaultZSpacing, -0.1f, 0f);
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
EditorPrefs.SetFloat(DEFAULT_ZSPACING_KEY, defaultZSpacing);
|
||
|
|
||
|
|
||
|
EditorGUILayout.Space();
|
||
|
EditorGUILayout.LabelField("Handles and Gizmos", EditorStyles.boldLabel);
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
SpineHandles.handleScale = EditorGUILayout.Slider("Editor Bone Scale", SpineHandles.handleScale, 0.01f, 2f);
|
||
|
SpineHandles.handleScale = Mathf.Max(0.01f, SpineHandles.handleScale);
|
||
|
if (EditorGUI.EndChangeCheck()) {
|
||
|
EditorPrefs.SetFloat(SCENE_ICONS_SCALE_KEY, SpineHandles.handleScale);
|
||
|
SceneView.RepaintAll();
|
||
|
}
|
||
|
|
||
|
|
||
|
GUILayout.Space(20);
|
||
|
EditorGUILayout.LabelField("3rd Party Settings", EditorStyles.boldLabel);
|
||
|
using (new GUILayout.HorizontalScope()) {
|
||
|
EditorGUILayout.PrefixLabel("Define TK2D");
|
||
|
if (GUILayout.Button("Enable", GUILayout.Width(64)))
|
||
|
SpineTK2DEditorUtility.EnableTK2D();
|
||
|
if (GUILayout.Button("Disable", GUILayout.Width(64)))
|
||
|
SpineTK2DEditorUtility.DisableTK2D();
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Drag and Drop Instantiation
|
||
|
public delegate Component InstantiateDelegate (SkeletonDataAsset skeletonDataAsset);
|
||
|
|
||
|
public struct SpawnMenuData {
|
||
|
public Vector3 spawnPoint;
|
||
|
public SkeletonDataAsset skeletonDataAsset;
|
||
|
public InstantiateDelegate instantiateDelegate;
|
||
|
public bool isUI;
|
||
|
}
|
||
|
|
||
|
public class SkeletonComponentSpawnType {
|
||
|
public string menuLabel;
|
||
|
public InstantiateDelegate instantiateDelegate;
|
||
|
public bool isUI;
|
||
|
}
|
||
|
|
||
|
internal static readonly List<SkeletonComponentSpawnType> additionalSpawnTypes = new List<SkeletonComponentSpawnType>();
|
||
|
|
||
|
static void SceneViewDragAndDrop (SceneView sceneview) {
|
||
|
var current = UnityEngine.Event.current;
|
||
|
var references = DragAndDrop.objectReferences;
|
||
|
if (current.type == EventType.Layout) return;
|
||
|
|
||
|
// Allow drag and drop of one SkeletonDataAsset.
|
||
|
if (references.Length == 1) {
|
||
|
var skeletonDataAsset = references[0] as SkeletonDataAsset;
|
||
|
if (skeletonDataAsset != null) {
|
||
|
var mousePos = current.mousePosition;
|
||
|
|
||
|
bool invalidSkeletonData = skeletonDataAsset.GetSkeletonData(true) == null;
|
||
|
if (invalidSkeletonData) {
|
||
|
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
|
||
|
Handles.BeginGUI();
|
||
|
GUI.Label(new Rect(mousePos + new Vector2(20f, 20f), new Vector2(400f, 40f)), new GUIContent(string.Format("{0} is invalid.\nCannot create new Spine GameObject.", skeletonDataAsset.name), SpineEditorUtilities.Icons.warning));
|
||
|
Handles.EndGUI();
|
||
|
return;
|
||
|
} else {
|
||
|
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||
|
Handles.BeginGUI();
|
||
|
GUI.Label(new Rect(mousePos + new Vector2(20f, 20f), new Vector2(400f, 20f)), new GUIContent(string.Format("Create Spine GameObject ({0})", skeletonDataAsset.skeletonJSON.name), SpineEditorUtilities.Icons.skeletonDataAssetIcon));
|
||
|
Handles.EndGUI();
|
||
|
|
||
|
if (current.type == EventType.DragPerform) {
|
||
|
RectTransform rectTransform = (Selection.activeGameObject == null) ? null : Selection.activeGameObject.GetComponent<RectTransform>();
|
||
|
Plane plane = (rectTransform == null) ? new Plane(Vector3.back, Vector3.zero) : new Plane(-rectTransform.forward, rectTransform.position);
|
||
|
Vector3 spawnPoint = MousePointToWorldPoint2D(mousePos, sceneview.camera, plane);
|
||
|
ShowInstantiateContextMenu(skeletonDataAsset, spawnPoint);
|
||
|
DragAndDrop.AcceptDrag();
|
||
|
current.Use();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void ShowInstantiateContextMenu (SkeletonDataAsset skeletonDataAsset, Vector3 spawnPoint) {
|
||
|
var menu = new GenericMenu();
|
||
|
|
||
|
// SkeletonAnimation
|
||
|
menu.AddItem(new GUIContent("SkeletonAnimation"), false, HandleSkeletonComponentDrop, new SpawnMenuData {
|
||
|
skeletonDataAsset = skeletonDataAsset,
|
||
|
spawnPoint = spawnPoint,
|
||
|
instantiateDelegate = (data) => InstantiateSkeletonAnimation(data),
|
||
|
isUI = false
|
||
|
});
|
||
|
|
||
|
// SkeletonGraphic
|
||
|
var skeletonGraphicInspectorType = System.Type.GetType("Spine.Unity.Editor.SkeletonGraphicInspector");
|
||
|
if (skeletonGraphicInspectorType != null) {
|
||
|
var graphicInstantiateDelegate = skeletonGraphicInspectorType.GetMethod("SpawnSkeletonGraphicFromDrop", BindingFlags.Static | BindingFlags.Public);
|
||
|
if (graphicInstantiateDelegate != null)
|
||
|
menu.AddItem(new GUIContent("SkeletonGraphic (UI)"), false, HandleSkeletonComponentDrop, new SpawnMenuData {
|
||
|
skeletonDataAsset = skeletonDataAsset,
|
||
|
spawnPoint = spawnPoint,
|
||
|
instantiateDelegate = System.Delegate.CreateDelegate(typeof(InstantiateDelegate), graphicInstantiateDelegate) as InstantiateDelegate,
|
||
|
isUI = true
|
||
|
});
|
||
|
}
|
||
|
|
||
|
#if SPINE_SKELETONANIMATOR
|
||
|
menu.AddSeparator("");
|
||
|
// SkeletonAnimator
|
||
|
menu.AddItem(new GUIContent("SkeletonAnimator"), false, HandleSkeletonComponentDrop, new SpawnMenuData {
|
||
|
skeletonDataAsset = skeletonDataAsset,
|
||
|
spawnPoint = spawnPoint,
|
||
|
instantiateDelegate = (data) => InstantiateSkeletonAnimator(data)
|
||
|
});
|
||
|
#endif
|
||
|
|
||
|
menu.ShowAsContext();
|
||
|
}
|
||
|
|
||
|
public static void HandleSkeletonComponentDrop (object spawnMenuData) {
|
||
|
var data = (SpawnMenuData)spawnMenuData;
|
||
|
|
||
|
if (data.skeletonDataAsset.GetSkeletonData(true) == null) {
|
||
|
EditorUtility.DisplayDialog("Invalid SkeletonDataAsset", "Unable to create Spine GameObject.\n\nPlease check your SkeletonDataAsset.", "Ok");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
bool isUI = data.isUI;
|
||
|
|
||
|
Component newSkeletonComponent = data.instantiateDelegate.Invoke(data.skeletonDataAsset);
|
||
|
GameObject newGameObject = newSkeletonComponent.gameObject;
|
||
|
Transform newTransform = newGameObject.transform;
|
||
|
|
||
|
var activeGameObject = Selection.activeGameObject;
|
||
|
if (isUI && activeGameObject != null)
|
||
|
newTransform.SetParent(activeGameObject.transform, false);
|
||
|
|
||
|
newTransform.position = isUI ? data.spawnPoint : RoundVector(data.spawnPoint, 2);
|
||
|
|
||
|
if (isUI && (activeGameObject == null || activeGameObject.GetComponent<RectTransform>() == null))
|
||
|
Debug.Log("Created a UI Skeleton GameObject not under a RectTransform. It may not be visible until you parent it to a canvas.");
|
||
|
|
||
|
if (!isUI && activeGameObject != null && activeGameObject.transform.localScale != Vector3.one)
|
||
|
Debug.Log("New Spine GameObject was parented to a scaled Transform. It may not be the intended size.");
|
||
|
|
||
|
Selection.activeGameObject = newGameObject;
|
||
|
//EditorGUIUtility.PingObject(newGameObject); // Doesn't work when setting activeGameObject.
|
||
|
Undo.RegisterCreatedObjectUndo(newGameObject, "Create Spine GameObject");
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Rounds off vector components to a number of decimal digits.
|
||
|
/// </summary>
|
||
|
public static Vector3 RoundVector (Vector3 vector, int digits) {
|
||
|
vector.x = (float)System.Math.Round(vector.x, digits);
|
||
|
vector.y = (float)System.Math.Round(vector.y, digits);
|
||
|
vector.z = (float)System.Math.Round(vector.z, digits);
|
||
|
return vector;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Converts a mouse point to a world point on a plane.
|
||
|
/// </summary>
|
||
|
static Vector3 MousePointToWorldPoint2D (Vector2 mousePosition, Camera camera, Plane plane) {
|
||
|
var screenPos = new Vector3(mousePosition.x, camera.pixelHeight - mousePosition.y, 0f);
|
||
|
var ray = camera.ScreenPointToRay(screenPos);
|
||
|
float distance;
|
||
|
bool hit = plane.Raycast(ray, out distance);
|
||
|
return ray.GetPoint(distance);
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Hierarchy
|
||
|
static class SpineEditorHierarchyHandler {
|
||
|
static Dictionary<int, GameObject> skeletonRendererTable = new Dictionary<int, GameObject>();
|
||
|
static Dictionary<int, SkeletonUtilityBone> skeletonUtilityBoneTable = new Dictionary<int, SkeletonUtilityBone>();
|
||
|
static Dictionary<int, BoundingBoxFollower> boundingBoxFollowerTable = new Dictionary<int, BoundingBoxFollower>();
|
||
|
|
||
|
#if UNITY_2017_2_OR_NEWER
|
||
|
internal static void HierarchyIconsOnPlaymodeStateChanged (PlayModeStateChange stateChange) {
|
||
|
#else
|
||
|
internal static void HierarchyIconsOnPlaymodeStateChanged () {
|
||
|
#endif
|
||
|
skeletonRendererTable.Clear();
|
||
|
skeletonUtilityBoneTable.Clear();
|
||
|
boundingBoxFollowerTable.Clear();
|
||
|
|
||
|
#if UNITY_2018
|
||
|
EditorApplication.hierarchyChanged -= HierarchyIconsOnChanged;
|
||
|
#else
|
||
|
EditorApplication.hierarchyWindowChanged -= HierarchyIconsOnChanged;
|
||
|
#endif
|
||
|
EditorApplication.hierarchyWindowItemOnGUI -= HierarchyIconsOnGUI;
|
||
|
|
||
|
if (!Application.isPlaying && showHierarchyIcons) {
|
||
|
#if UNITY_2018
|
||
|
EditorApplication.hierarchyChanged += HierarchyIconsOnChanged;
|
||
|
#else
|
||
|
EditorApplication.hierarchyWindowChanged += HierarchyIconsOnChanged;
|
||
|
#endif
|
||
|
EditorApplication.hierarchyWindowItemOnGUI += HierarchyIconsOnGUI;
|
||
|
HierarchyIconsOnChanged();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal static void HierarchyIconsOnChanged () {
|
||
|
skeletonRendererTable.Clear();
|
||
|
skeletonUtilityBoneTable.Clear();
|
||
|
boundingBoxFollowerTable.Clear();
|
||
|
|
||
|
SkeletonRenderer[] arr = Object.FindObjectsOfType<SkeletonRenderer>();
|
||
|
foreach (SkeletonRenderer r in arr)
|
||
|
skeletonRendererTable[r.gameObject.GetInstanceID()] = r.gameObject;
|
||
|
|
||
|
SkeletonUtilityBone[] boneArr = Object.FindObjectsOfType<SkeletonUtilityBone>();
|
||
|
foreach (SkeletonUtilityBone b in boneArr)
|
||
|
skeletonUtilityBoneTable[b.gameObject.GetInstanceID()] = b;
|
||
|
|
||
|
BoundingBoxFollower[] bbfArr = Object.FindObjectsOfType<BoundingBoxFollower>();
|
||
|
foreach (BoundingBoxFollower bbf in bbfArr)
|
||
|
boundingBoxFollowerTable[bbf.gameObject.GetInstanceID()] = bbf;
|
||
|
}
|
||
|
|
||
|
internal static void HierarchyIconsOnGUI (int instanceId, Rect selectionRect) {
|
||
|
Rect r = new Rect(selectionRect);
|
||
|
if (skeletonRendererTable.ContainsKey(instanceId)) {
|
||
|
r.x = r.width - 15;
|
||
|
r.width = 15;
|
||
|
GUI.Label(r, Icons.spine);
|
||
|
} else if (skeletonUtilityBoneTable.ContainsKey(instanceId)) {
|
||
|
r.x -= 26;
|
||
|
if (skeletonUtilityBoneTable[instanceId] != null) {
|
||
|
if (skeletonUtilityBoneTable[instanceId].transform.childCount == 0)
|
||
|
r.x += 13;
|
||
|
r.y += 2;
|
||
|
r.width = 13;
|
||
|
r.height = 13;
|
||
|
if (skeletonUtilityBoneTable[instanceId].mode == SkeletonUtilityBone.Mode.Follow)
|
||
|
GUI.DrawTexture(r, Icons.bone);
|
||
|
else
|
||
|
GUI.DrawTexture(r, Icons.poseBones);
|
||
|
}
|
||
|
} else if (boundingBoxFollowerTable.ContainsKey(instanceId)) {
|
||
|
r.x -= 26;
|
||
|
if (boundingBoxFollowerTable[instanceId] != null) {
|
||
|
if (boundingBoxFollowerTable[instanceId].transform.childCount == 0)
|
||
|
r.x += 13;
|
||
|
r.y += 2;
|
||
|
r.width = 13;
|
||
|
r.height = 13;
|
||
|
GUI.DrawTexture(r, Icons.boundingBox);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal static void HierarchyDragAndDrop (int instanceId, Rect selectionRect) {
|
||
|
// HACK: Uses EditorApplication.hierarchyWindowItemOnGUI.
|
||
|
// Only works when there is at least one item in the scene.
|
||
|
var current = UnityEngine.Event.current;
|
||
|
var eventType = current.type;
|
||
|
bool isDraggingEvent = eventType == EventType.DragUpdated;
|
||
|
bool isDropEvent = eventType == EventType.DragPerform;
|
||
|
if (isDraggingEvent || isDropEvent) {
|
||
|
var mouseOverWindow = EditorWindow.mouseOverWindow;
|
||
|
if (mouseOverWindow != null) {
|
||
|
|
||
|
// One, existing, valid SkeletonDataAsset
|
||
|
var references = DragAndDrop.objectReferences;
|
||
|
if (references.Length == 1) {
|
||
|
var skeletonDataAsset = references[0] as SkeletonDataAsset;
|
||
|
if (skeletonDataAsset != null && skeletonDataAsset.GetSkeletonData(true) != null) {
|
||
|
|
||
|
// Allow drag-and-dropping anywhere in the Hierarchy Window.
|
||
|
// HACK: string-compare because we can't get its type via reflection.
|
||
|
const string HierarchyWindow = "UnityEditor.SceneHierarchyWindow";
|
||
|
if (HierarchyWindow.Equals(mouseOverWindow.GetType().ToString(), System.StringComparison.Ordinal)) {
|
||
|
if (isDraggingEvent) {
|
||
|
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||
|
current.Use();
|
||
|
} else if (isDropEvent) {
|
||
|
ShowInstantiateContextMenu(skeletonDataAsset, Vector3.zero);
|
||
|
DragAndDrop.AcceptDrag();
|
||
|
current.Use();
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Auto-Import Entry Point
|
||
|
static void OnPostprocessAllAssets (string[] imported, string[] deleted, string[] moved, string[] movedFromAssetPaths) {
|
||
|
if (imported.Length == 0)
|
||
|
return;
|
||
|
|
||
|
// In case user used "Assets -> Reimport All", during the import process,
|
||
|
// asset database is not initialized until some point. During that period,
|
||
|
// all attempts to load any assets using API (i.e. AssetDatabase.LoadAssetAtPath)
|
||
|
// will return null, and as result, assets won't be loaded even if they actually exists,
|
||
|
// which may lead to numerous importing errors.
|
||
|
// This situation also happens if Library folder is deleted from the project, which is a pretty
|
||
|
// common case, since when using version control systems, the Library folder must be excluded.
|
||
|
//
|
||
|
// So to avoid this, in case asset database is not available, we delay loading the assets
|
||
|
// until next time.
|
||
|
//
|
||
|
// Unity *always* reimports some internal assets after the process is done, so this method
|
||
|
// is always called once again in a state when asset database is available.
|
||
|
//
|
||
|
// Checking whether AssetDatabase is initialized is done by attempting to load
|
||
|
// a known "marker" asset that should always be available. Failing to load this asset
|
||
|
// means that AssetDatabase is not initialized.
|
||
|
assetsImportedInWrongState.UnionWith(imported);
|
||
|
if (AssetDatabaseAvailabilityDetector.IsAssetDatabaseAvailable()) {
|
||
|
string[] combinedAssets = assetsImportedInWrongState.ToArray();
|
||
|
assetsImportedInWrongState.Clear();
|
||
|
ImportSpineContent(combinedAssets);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void ImportSpineContent (string[] imported, bool reimport = false) {
|
||
|
var atlasPaths = new List<string>();
|
||
|
var imagePaths = new List<string>();
|
||
|
var skeletonPaths = new List<string>();
|
||
|
|
||
|
foreach (string str in imported) {
|
||
|
string extension = Path.GetExtension(str).ToLower();
|
||
|
switch (extension) {
|
||
|
case ".txt":
|
||
|
if (str.EndsWith(".atlas.txt", System.StringComparison.Ordinal))
|
||
|
atlasPaths.Add(str);
|
||
|
break;
|
||
|
case ".png":
|
||
|
case ".jpg":
|
||
|
imagePaths.Add(str);
|
||
|
break;
|
||
|
case ".json":
|
||
|
var jsonAsset = (TextAsset)AssetDatabase.LoadAssetAtPath(str, typeof(TextAsset));
|
||
|
if (jsonAsset != null && SkeletonDataFileValidator.IsSpineData(jsonAsset))
|
||
|
skeletonPaths.Add(str);
|
||
|
break;
|
||
|
case ".bytes":
|
||
|
if (str.ToLower().EndsWith(".skel.bytes", System.StringComparison.Ordinal)) {
|
||
|
if (SkeletonDataFileValidator.IsSpineData((TextAsset)AssetDatabase.LoadAssetAtPath(str, typeof(TextAsset))))
|
||
|
skeletonPaths.Add(str);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Import atlases first.
|
||
|
var atlases = new List<AtlasAsset>();
|
||
|
foreach (string ap in atlasPaths) {
|
||
|
TextAsset atlasText = (TextAsset)AssetDatabase.LoadAssetAtPath(ap, typeof(TextAsset));
|
||
|
AtlasAsset atlas = IngestSpineAtlas(atlasText);
|
||
|
atlases.Add(atlas);
|
||
|
}
|
||
|
|
||
|
// Import skeletons and match them with atlases.
|
||
|
bool abortSkeletonImport = false;
|
||
|
foreach (string sp in skeletonPaths) {
|
||
|
if (!reimport && SkeletonDataFileValidator.CheckForValidSkeletonData(sp)) {
|
||
|
ReloadSkeletonData(sp);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
string dir = Path.GetDirectoryName(sp);
|
||
|
|
||
|
#if SPINE_TK2D
|
||
|
IngestSpineProject(AssetDatabase.LoadAssetAtPath(sp, typeof(TextAsset)) as TextAsset, null);
|
||
|
#else
|
||
|
var localAtlases = FindAtlasesAtPath(dir);
|
||
|
var requiredPaths = GetRequiredAtlasRegions(sp);
|
||
|
var atlasMatch = GetMatchingAtlas(requiredPaths, localAtlases);
|
||
|
if (atlasMatch != null || requiredPaths.Count == 0) {
|
||
|
IngestSpineProject(AssetDatabase.LoadAssetAtPath(sp, typeof(TextAsset)) as TextAsset, atlasMatch);
|
||
|
} else {
|
||
|
bool resolved = false;
|
||
|
while (!resolved) {
|
||
|
|
||
|
string filename = Path.GetFileNameWithoutExtension(sp);
|
||
|
int result = EditorUtility.DisplayDialogComplex(
|
||
|
string.Format("AtlasAsset for \"{0}\"", filename),
|
||
|
string.Format("Could not automatically set the AtlasAsset for \"{0}\". You may set it manually.", filename),
|
||
|
"Choose AtlasAssets...", "Skip this", "Stop importing all"
|
||
|
);
|
||
|
|
||
|
switch (result) {
|
||
|
case -1:
|
||
|
//Debug.Log("Select Atlas");
|
||
|
AtlasAsset selectedAtlas = GetAtlasDialog(Path.GetDirectoryName(sp));
|
||
|
if (selectedAtlas != null) {
|
||
|
localAtlases.Clear();
|
||
|
localAtlases.Add(selectedAtlas);
|
||
|
atlasMatch = GetMatchingAtlas(requiredPaths, localAtlases);
|
||
|
if (atlasMatch != null) {
|
||
|
resolved = true;
|
||
|
IngestSpineProject(AssetDatabase.LoadAssetAtPath(sp, typeof(TextAsset)) as TextAsset, atlasMatch);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0: // Choose AtlasAssets...
|
||
|
var atlasList = MultiAtlasDialog(requiredPaths, Path.GetDirectoryName(sp), Path.GetFileNameWithoutExtension(sp));
|
||
|
if (atlasList != null)
|
||
|
IngestSpineProject(AssetDatabase.LoadAssetAtPath(sp, typeof(TextAsset)) as TextAsset, atlasList.ToArray());
|
||
|
|
||
|
resolved = true;
|
||
|
break;
|
||
|
case 1: // Skip
|
||
|
Debug.Log("Skipped importing: " + Path.GetFileName(sp));
|
||
|
resolved = true;
|
||
|
break;
|
||
|
case 2: // Stop importing all
|
||
|
abortSkeletonImport = true;
|
||
|
resolved = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (abortSkeletonImport)
|
||
|
break;
|
||
|
#endif
|
||
|
}
|
||
|
// Any post processing of images
|
||
|
}
|
||
|
|
||
|
static void ReloadSkeletonData (string skeletonJSONPath) {
|
||
|
string dir = Path.GetDirectoryName(skeletonJSONPath);
|
||
|
TextAsset textAsset = (TextAsset)AssetDatabase.LoadAssetAtPath(skeletonJSONPath, typeof(TextAsset));
|
||
|
DirectoryInfo dirInfo = new DirectoryInfo(dir);
|
||
|
FileInfo[] files = dirInfo.GetFiles("*.asset");
|
||
|
|
||
|
foreach (var f in files) {
|
||
|
string localPath = dir + "/" + f.Name;
|
||
|
var obj = AssetDatabase.LoadAssetAtPath(localPath, typeof(Object));
|
||
|
var skeletonDataAsset = obj as SkeletonDataAsset;
|
||
|
if (skeletonDataAsset != null) {
|
||
|
if (skeletonDataAsset.skeletonJSON == textAsset) {
|
||
|
if (Selection.activeObject == skeletonDataAsset)
|
||
|
Selection.activeObject = null;
|
||
|
|
||
|
Debug.LogFormat("Changes to '{0}' detected. Clearing SkeletonDataAsset: {1}", skeletonJSONPath, localPath);
|
||
|
skeletonDataAsset.Clear();
|
||
|
|
||
|
string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(skeletonDataAsset));
|
||
|
string lastHash = EditorPrefs.GetString(guid + "_hash");
|
||
|
|
||
|
// For some weird reason sometimes Unity loses the internal Object pointer,
|
||
|
// and as a result, all comparisons with null returns true.
|
||
|
// But the C# wrapper is still alive, so we can "restore" the object
|
||
|
// by reloading it from its Instance ID.
|
||
|
AtlasAsset[] skeletonDataAtlasAssets = skeletonDataAsset.atlasAssets;
|
||
|
if (skeletonDataAtlasAssets != null) {
|
||
|
for (int i = 0; i < skeletonDataAtlasAssets.Length; i++) {
|
||
|
if (!ReferenceEquals(null, skeletonDataAtlasAssets[i]) &&
|
||
|
skeletonDataAtlasAssets[i].Equals(null) &&
|
||
|
skeletonDataAtlasAssets[i].GetInstanceID() != 0
|
||
|
) {
|
||
|
skeletonDataAtlasAssets[i] = EditorUtility.InstanceIDToObject(skeletonDataAtlasAssets[i].GetInstanceID()) as AtlasAsset;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(true);
|
||
|
string currentHash = skeletonData != null ? skeletonData.Hash : null;
|
||
|
|
||
|
#if SPINE_SKELETONANIMATOR
|
||
|
if (currentHash == null || lastHash != currentHash)
|
||
|
UpdateMecanimClips(skeletonDataAsset);
|
||
|
#endif
|
||
|
|
||
|
// if (currentHash == null || lastHash != currentHash)
|
||
|
// Do any upkeep on synchronized assets
|
||
|
|
||
|
if (currentHash != null)
|
||
|
EditorPrefs.SetString(guid + "_hash", currentHash);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Match SkeletonData with Atlases
|
||
|
static readonly AttachmentType[] AtlasTypes = { AttachmentType.Region, AttachmentType.Linkedmesh, AttachmentType.Mesh };
|
||
|
|
||
|
static List<AtlasAsset> MultiAtlasDialog (List<string> requiredPaths, string initialDirectory, string filename = "") {
|
||
|
List<AtlasAsset> atlasAssets = new List<AtlasAsset>();
|
||
|
bool resolved = false;
|
||
|
string lastAtlasPath = initialDirectory;
|
||
|
while (!resolved) {
|
||
|
|
||
|
// Build dialog box message.
|
||
|
var missingRegions = new List<string>(requiredPaths);
|
||
|
var dialogText = new StringBuilder();
|
||
|
{
|
||
|
dialogText.AppendLine(string.Format("SkeletonDataAsset for \"{0}\"", filename));
|
||
|
dialogText.AppendLine("has missing regions.");
|
||
|
dialogText.AppendLine();
|
||
|
dialogText.AppendLine("Current Atlases:");
|
||
|
|
||
|
if (atlasAssets.Count == 0)
|
||
|
dialogText.AppendLine("\t--none--");
|
||
|
|
||
|
for (int i = 0; i < atlasAssets.Count; i++)
|
||
|
dialogText.AppendLine("\t" + atlasAssets[i].name);
|
||
|
|
||
|
dialogText.AppendLine();
|
||
|
dialogText.AppendLine("Missing Regions:");
|
||
|
|
||
|
foreach (var atlasAsset in atlasAssets) {
|
||
|
var atlas = atlasAsset.GetAtlas();
|
||
|
for (int i = 0; i < missingRegions.Count; i++) {
|
||
|
if (atlas.FindRegion(missingRegions[i]) != null) {
|
||
|
missingRegions.RemoveAt(i);
|
||
|
i--;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
int n = missingRegions.Count;
|
||
|
if (n == 0) break;
|
||
|
|
||
|
const int MaxListLength = 15;
|
||
|
for (int i = 0; (i < n && i < MaxListLength); i++)
|
||
|
dialogText.AppendLine("\t" + missingRegions[i]);
|
||
|
|
||
|
if (n > MaxListLength) dialogText.AppendLine(string.Format("\t... {0} more...", n - MaxListLength));
|
||
|
}
|
||
|
|
||
|
// Show dialog box.
|
||
|
int result = EditorUtility.DisplayDialogComplex(
|
||
|
"SkeletonDataAsset has missing Atlas.",
|
||
|
dialogText.ToString(),
|
||
|
"Browse...", "Import anyway", "Cancel"
|
||
|
);
|
||
|
|
||
|
switch (result) {
|
||
|
case 0: // Browse...
|
||
|
AtlasAsset selectedAtlasAsset = GetAtlasDialog(lastAtlasPath);
|
||
|
if (selectedAtlasAsset != null) {
|
||
|
var atlas = selectedAtlasAsset.GetAtlas();
|
||
|
bool hasValidRegion = false;
|
||
|
foreach (string str in missingRegions) {
|
||
|
if (atlas.FindRegion(str) != null) {
|
||
|
hasValidRegion = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
atlasAssets.Add(selectedAtlasAsset);
|
||
|
}
|
||
|
break;
|
||
|
case 1: // Import anyway
|
||
|
resolved = true;
|
||
|
break;
|
||
|
case 2: // Cancel
|
||
|
atlasAssets = null;
|
||
|
resolved = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return atlasAssets;
|
||
|
}
|
||
|
|
||
|
static AtlasAsset GetAtlasDialog (string dirPath) {
|
||
|
string path = EditorUtility.OpenFilePanel("Select AtlasAsset...", dirPath, "asset");
|
||
|
if (path == "") return null; // Canceled or closed by user.
|
||
|
|
||
|
int subLen = Application.dataPath.Length - 6;
|
||
|
string assetRelativePath = path.Substring(subLen, path.Length - subLen).Replace("\\", "/");
|
||
|
|
||
|
Object obj = AssetDatabase.LoadAssetAtPath(assetRelativePath, typeof(AtlasAsset));
|
||
|
|
||
|
if (obj == null || obj.GetType() != typeof(AtlasAsset))
|
||
|
return null;
|
||
|
|
||
|
return (AtlasAsset)obj;
|
||
|
}
|
||
|
|
||
|
static void AddRequiredAtlasRegionsFromBinary (string skeletonDataPath, List<string> requiredPaths) {
|
||
|
SkeletonBinary binary = new SkeletonBinary(new AtlasRequirementLoader(requiredPaths));
|
||
|
TextAsset data = (TextAsset)AssetDatabase.LoadAssetAtPath(skeletonDataPath, typeof(TextAsset));
|
||
|
MemoryStream input = new MemoryStream(data.bytes);
|
||
|
binary.ReadSkeletonData(input);
|
||
|
binary = null;
|
||
|
}
|
||
|
|
||
|
public static List<string> GetRequiredAtlasRegions (string skeletonDataPath) {
|
||
|
List<string> requiredPaths = new List<string>();
|
||
|
|
||
|
if (skeletonDataPath.Contains(".skel")) {
|
||
|
AddRequiredAtlasRegionsFromBinary(skeletonDataPath, requiredPaths);
|
||
|
return requiredPaths;
|
||
|
}
|
||
|
|
||
|
TextAsset spineJson = (TextAsset)AssetDatabase.LoadAssetAtPath(skeletonDataPath, typeof(TextAsset));
|
||
|
|
||
|
StringReader reader = new StringReader(spineJson.text);
|
||
|
var root = Json.Deserialize(reader) as Dictionary<string, object>;
|
||
|
|
||
|
if (!root.ContainsKey("skins"))
|
||
|
return requiredPaths;
|
||
|
|
||
|
foreach (KeyValuePair<string, object> entry in (Dictionary<string, object>)root["skins"]) {
|
||
|
foreach (KeyValuePair<string, object> slotEntry in (Dictionary<string, object>)entry.Value) {
|
||
|
|
||
|
foreach (KeyValuePair<string, object> attachmentEntry in ((Dictionary<string, object>)slotEntry.Value)) {
|
||
|
var data = ((Dictionary<string, object>)attachmentEntry.Value);
|
||
|
|
||
|
// Ignore non-atlas-requiring types.
|
||
|
if (data.ContainsKey("type")) {
|
||
|
AttachmentType attachmentType;
|
||
|
string typeString = (string)data["type"];
|
||
|
try {
|
||
|
attachmentType = (AttachmentType)System.Enum.Parse(typeof(AttachmentType), typeString, true);
|
||
|
} catch (System.ArgumentException e) {
|
||
|
// For more info, visit: http://esotericsoftware.com/forum/Spine-editor-and-runtime-version-management-6534
|
||
|
Debug.LogWarning(string.Format("Unidentified Attachment type: \"{0}\". Skeleton may have been exported from an incompatible Spine version.", typeString));
|
||
|
throw e;
|
||
|
}
|
||
|
|
||
|
if (!AtlasTypes.Contains(attachmentType))
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (data.ContainsKey("path"))
|
||
|
requiredPaths.Add((string)data["path"]);
|
||
|
else if (data.ContainsKey("name"))
|
||
|
requiredPaths.Add((string)data["name"]);
|
||
|
else
|
||
|
requiredPaths.Add(attachmentEntry.Key);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return requiredPaths;
|
||
|
}
|
||
|
|
||
|
static AtlasAsset GetMatchingAtlas (List<string> requiredPaths, List<AtlasAsset> atlasAssets) {
|
||
|
AtlasAsset atlasAssetMatch = null;
|
||
|
|
||
|
foreach (AtlasAsset a in atlasAssets) {
|
||
|
Atlas atlas = a.GetAtlas();
|
||
|
bool failed = false;
|
||
|
foreach (string regionPath in requiredPaths) {
|
||
|
if (atlas.FindRegion(regionPath) == null) {
|
||
|
failed = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!failed) {
|
||
|
atlasAssetMatch = a;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return atlasAssetMatch;
|
||
|
}
|
||
|
|
||
|
public class AtlasRequirementLoader : AttachmentLoader {
|
||
|
List<string> requirementList;
|
||
|
|
||
|
public AtlasRequirementLoader (List<string> requirementList) {
|
||
|
this.requirementList = requirementList;
|
||
|
}
|
||
|
|
||
|
public RegionAttachment NewRegionAttachment (Skin skin, string name, string path) {
|
||
|
requirementList.Add(path);
|
||
|
return new RegionAttachment(name);
|
||
|
}
|
||
|
|
||
|
public MeshAttachment NewMeshAttachment (Skin skin, string name, string path) {
|
||
|
requirementList.Add(path);
|
||
|
return new MeshAttachment(name);
|
||
|
}
|
||
|
|
||
|
public BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, string name) {
|
||
|
return new BoundingBoxAttachment(name);
|
||
|
}
|
||
|
|
||
|
public PathAttachment NewPathAttachment (Skin skin, string name) {
|
||
|
return new PathAttachment(name);
|
||
|
}
|
||
|
|
||
|
public PointAttachment NewPointAttachment (Skin skin, string name) {
|
||
|
return new PointAttachment(name);
|
||
|
}
|
||
|
|
||
|
public ClippingAttachment NewClippingAttachment (Skin skin, string name) {
|
||
|
return new ClippingAttachment(name);
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Import Atlases
|
||
|
static List<AtlasAsset> FindAtlasesAtPath (string path) {
|
||
|
List<AtlasAsset> arr = new List<AtlasAsset>();
|
||
|
DirectoryInfo dir = new DirectoryInfo(path);
|
||
|
FileInfo[] assetInfoArr = dir.GetFiles("*.asset");
|
||
|
|
||
|
int subLen = Application.dataPath.Length - 6;
|
||
|
foreach (var f in assetInfoArr) {
|
||
|
string assetRelativePath = f.FullName.Substring(subLen, f.FullName.Length - subLen).Replace("\\", "/");
|
||
|
Object obj = AssetDatabase.LoadAssetAtPath(assetRelativePath, typeof(AtlasAsset));
|
||
|
if (obj != null)
|
||
|
arr.Add(obj as AtlasAsset);
|
||
|
}
|
||
|
|
||
|
return arr;
|
||
|
}
|
||
|
|
||
|
static AtlasAsset IngestSpineAtlas (TextAsset atlasText) {
|
||
|
if (atlasText == null) {
|
||
|
Debug.LogWarning("Atlas source cannot be null!");
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
string primaryName = Path.GetFileNameWithoutExtension(atlasText.name).Replace(".atlas", "");
|
||
|
string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(atlasText));
|
||
|
|
||
|
string atlasPath = assetPath + "/" + primaryName + "_Atlas.asset";
|
||
|
|
||
|
AtlasAsset atlasAsset = (AtlasAsset)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAsset));
|
||
|
|
||
|
List<Material> vestigialMaterials = new List<Material>();
|
||
|
|
||
|
if (atlasAsset == null)
|
||
|
atlasAsset = AtlasAsset.CreateInstance<AtlasAsset>();
|
||
|
else {
|
||
|
foreach (Material m in atlasAsset.materials)
|
||
|
vestigialMaterials.Add(m);
|
||
|
}
|
||
|
|
||
|
protectFromStackGarbageCollection.Add(atlasAsset);
|
||
|
atlasAsset.atlasFile = atlasText;
|
||
|
|
||
|
//strip CR
|
||
|
string atlasStr = atlasText.text;
|
||
|
atlasStr = atlasStr.Replace("\r", "");
|
||
|
|
||
|
string[] atlasLines = atlasStr.Split('\n');
|
||
|
List<string> pageFiles = new List<string>();
|
||
|
for (int i = 0; i < atlasLines.Length - 1; i++) {
|
||
|
if (atlasLines[i].Trim().Length == 0)
|
||
|
pageFiles.Add(atlasLines[i + 1].Trim());
|
||
|
}
|
||
|
|
||
|
var populatingMaterials = new List<Material>(pageFiles.Count);//atlasAsset.materials = new Material[pageFiles.Count];
|
||
|
|
||
|
for (int i = 0; i < pageFiles.Count; i++) {
|
||
|
string texturePath = assetPath + "/" + pageFiles[i];
|
||
|
Texture2D texture = (Texture2D)AssetDatabase.LoadAssetAtPath(texturePath, typeof(Texture2D));
|
||
|
|
||
|
if (setTextureImporterSettings) {
|
||
|
TextureImporter texImporter = (TextureImporter)TextureImporter.GetAtPath(texturePath);
|
||
|
if (texImporter == null) {
|
||
|
Debug.LogWarning(string.Format("{0} ::: Texture asset \"{1}\" not found. Skipping. Please check your atlas file for renamed files.", atlasAsset.name, texturePath));
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
texImporter.textureCompression = TextureImporterCompression.Uncompressed;
|
||
|
texImporter.alphaSource = TextureImporterAlphaSource.FromInput;
|
||
|
texImporter.mipmapEnabled = false;
|
||
|
texImporter.alphaIsTransparency = false; // Prevent the texture importer from applying bleed to the transparent parts for PMA.
|
||
|
texImporter.spriteImportMode = SpriteImportMode.None;
|
||
|
texImporter.maxTextureSize = 2048;
|
||
|
|
||
|
EditorUtility.SetDirty(texImporter);
|
||
|
AssetDatabase.ImportAsset(texturePath);
|
||
|
AssetDatabase.SaveAssets();
|
||
|
}
|
||
|
|
||
|
string pageName = Path.GetFileNameWithoutExtension(pageFiles[i]);
|
||
|
|
||
|
//because this looks silly
|
||
|
if (pageName == primaryName && pageFiles.Count == 1)
|
||
|
pageName = "Material";
|
||
|
|
||
|
string materialPath = assetPath + "/" + primaryName + "_" + pageName + ".mat";
|
||
|
Material mat = (Material)AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material));
|
||
|
|
||
|
if (mat == null) {
|
||
|
mat = new Material(Shader.Find(defaultShader));
|
||
|
AssetDatabase.CreateAsset(mat, materialPath);
|
||
|
} else {
|
||
|
vestigialMaterials.Remove(mat);
|
||
|
}
|
||
|
|
||
|
mat.mainTexture = texture;
|
||
|
EditorUtility.SetDirty(mat);
|
||
|
AssetDatabase.SaveAssets();
|
||
|
|
||
|
populatingMaterials.Add(mat); //atlasAsset.materials[i] = mat;
|
||
|
}
|
||
|
|
||
|
atlasAsset.materials = populatingMaterials.ToArray();
|
||
|
|
||
|
for (int i = 0; i < vestigialMaterials.Count; i++)
|
||
|
AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(vestigialMaterials[i]));
|
||
|
|
||
|
if (AssetDatabase.GetAssetPath(atlasAsset) == "")
|
||
|
AssetDatabase.CreateAsset(atlasAsset, atlasPath);
|
||
|
else
|
||
|
atlasAsset.Clear();
|
||
|
|
||
|
EditorUtility.SetDirty(atlasAsset);
|
||
|
AssetDatabase.SaveAssets();
|
||
|
|
||
|
if (pageFiles.Count != atlasAsset.materials.Length)
|
||
|
Debug.LogWarning(string.Format("{0} ::: Not all atlas pages were imported. If you rename your image files, please make sure you also edit the filenames specified in the atlas file.", atlasAsset.name));
|
||
|
else
|
||
|
Debug.Log(string.Format("{0} ::: Imported with {1} material", atlasAsset.name, atlasAsset.materials.Length));
|
||
|
|
||
|
// Iterate regions and bake marked.
|
||
|
Atlas atlas = atlasAsset.GetAtlas();
|
||
|
FieldInfo field = typeof(Atlas).GetField("regions", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.NonPublic);
|
||
|
List<AtlasRegion> regions = (List<AtlasRegion>)field.GetValue(atlas);
|
||
|
string atlasAssetPath = AssetDatabase.GetAssetPath(atlasAsset);
|
||
|
string atlasAssetDirPath = Path.GetDirectoryName(atlasAssetPath);
|
||
|
string bakedDirPath = Path.Combine(atlasAssetDirPath, atlasAsset.name);
|
||
|
|
||
|
bool hasBakedRegions = false;
|
||
|
for (int i = 0; i < regions.Count; i++) {
|
||
|
AtlasRegion region = regions[i];
|
||
|
string bakedPrefabPath = Path.Combine(bakedDirPath, SpineEditorUtilities.GetPathSafeName(region.name) + ".prefab").Replace("\\", "/");
|
||
|
GameObject prefab = (GameObject)AssetDatabase.LoadAssetAtPath(bakedPrefabPath, typeof(GameObject));
|
||
|
if (prefab != null) {
|
||
|
BakeRegion(atlasAsset, region, false);
|
||
|
hasBakedRegions = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (hasBakedRegions) {
|
||
|
AssetDatabase.SaveAssets();
|
||
|
AssetDatabase.Refresh();
|
||
|
}
|
||
|
|
||
|
protectFromStackGarbageCollection.Remove(atlasAsset);
|
||
|
return (AtlasAsset)AssetDatabase.LoadAssetAtPath(atlasPath, typeof(AtlasAsset));
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Bake Atlas Region
|
||
|
public static GameObject BakeRegion (AtlasAsset atlasAsset, AtlasRegion region, bool autoSave = true) {
|
||
|
Atlas atlas = atlasAsset.GetAtlas();
|
||
|
string atlasAssetPath = AssetDatabase.GetAssetPath(atlasAsset);
|
||
|
string atlasAssetDirPath = Path.GetDirectoryName(atlasAssetPath);
|
||
|
string bakedDirPath = Path.Combine(atlasAssetDirPath, atlasAsset.name);
|
||
|
string bakedPrefabPath = Path.Combine(bakedDirPath, GetPathSafeName(region.name) + ".prefab").Replace("\\", "/");
|
||
|
|
||
|
GameObject prefab = (GameObject)AssetDatabase.LoadAssetAtPath(bakedPrefabPath, typeof(GameObject));
|
||
|
GameObject root;
|
||
|
Mesh mesh;
|
||
|
bool isNewPrefab = false;
|
||
|
|
||
|
if (!Directory.Exists(bakedDirPath))
|
||
|
Directory.CreateDirectory(bakedDirPath);
|
||
|
|
||
|
if (prefab == null) {
|
||
|
root = new GameObject("temp", typeof(MeshFilter), typeof(MeshRenderer));
|
||
|
prefab = PrefabUtility.CreatePrefab(bakedPrefabPath, root);
|
||
|
isNewPrefab = true;
|
||
|
Object.DestroyImmediate(root);
|
||
|
}
|
||
|
|
||
|
mesh = (Mesh)AssetDatabase.LoadAssetAtPath(bakedPrefabPath, typeof(Mesh));
|
||
|
|
||
|
Material mat = null;
|
||
|
mesh = atlasAsset.GenerateMesh(region.name, mesh, out mat);
|
||
|
if (isNewPrefab) {
|
||
|
AssetDatabase.AddObjectToAsset(mesh, prefab);
|
||
|
prefab.GetComponent<MeshFilter>().sharedMesh = mesh;
|
||
|
}
|
||
|
|
||
|
EditorUtility.SetDirty(mesh);
|
||
|
EditorUtility.SetDirty(prefab);
|
||
|
|
||
|
if (autoSave) {
|
||
|
AssetDatabase.SaveAssets();
|
||
|
AssetDatabase.Refresh();
|
||
|
}
|
||
|
|
||
|
prefab.GetComponent<MeshRenderer>().sharedMaterial = mat;
|
||
|
|
||
|
return prefab;
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Import SkeletonData (json or binary)
|
||
|
public const string SkeletonDataSuffix = "_SkeletonData";
|
||
|
static SkeletonDataAsset IngestSpineProject (TextAsset spineJson, params AtlasAsset[] atlasAssets) {
|
||
|
string primaryName = Path.GetFileNameWithoutExtension(spineJson.name);
|
||
|
string assetPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(spineJson));
|
||
|
string filePath = assetPath + "/" + primaryName + SkeletonDataSuffix + ".asset";
|
||
|
|
||
|
#if SPINE_TK2D
|
||
|
if (spineJson != null) {
|
||
|
SkeletonDataAsset skeletonDataAsset = (SkeletonDataAsset)AssetDatabase.LoadAssetAtPath(filePath, typeof(SkeletonDataAsset));
|
||
|
if (skeletonDataAsset == null) {
|
||
|
skeletonDataAsset = SkeletonDataAsset.CreateInstance<SkeletonDataAsset>();
|
||
|
skeletonDataAsset.skeletonJSON = spineJson;
|
||
|
skeletonDataAsset.fromAnimation = new string[0];
|
||
|
skeletonDataAsset.toAnimation = new string[0];
|
||
|
skeletonDataAsset.duration = new float[0];
|
||
|
skeletonDataAsset.defaultMix = defaultMix;
|
||
|
skeletonDataAsset.scale = defaultScale;
|
||
|
|
||
|
AssetDatabase.CreateAsset(skeletonDataAsset, filePath);
|
||
|
AssetDatabase.SaveAssets();
|
||
|
} else {
|
||
|
skeletonDataAsset.Clear();
|
||
|
skeletonDataAsset.GetSkeletonData(true);
|
||
|
}
|
||
|
|
||
|
return skeletonDataAsset;
|
||
|
} else {
|
||
|
EditorUtility.DisplayDialog("Error!", "Tried to ingest null Spine data.", "OK");
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
#else
|
||
|
if (spineJson != null && atlasAssets != null) {
|
||
|
SkeletonDataAsset skeletonDataAsset = (SkeletonDataAsset)AssetDatabase.LoadAssetAtPath(filePath, typeof(SkeletonDataAsset));
|
||
|
if (skeletonDataAsset == null) {
|
||
|
skeletonDataAsset = ScriptableObject.CreateInstance<SkeletonDataAsset>(); {
|
||
|
skeletonDataAsset.atlasAssets = atlasAssets;
|
||
|
skeletonDataAsset.skeletonJSON = spineJson;
|
||
|
skeletonDataAsset.defaultMix = defaultMix;
|
||
|
skeletonDataAsset.scale = defaultScale;
|
||
|
}
|
||
|
|
||
|
AssetDatabase.CreateAsset(skeletonDataAsset, filePath);
|
||
|
AssetDatabase.SaveAssets();
|
||
|
} else {
|
||
|
skeletonDataAsset.atlasAssets = atlasAssets;
|
||
|
skeletonDataAsset.Clear();
|
||
|
skeletonDataAsset.GetSkeletonData(true);
|
||
|
}
|
||
|
|
||
|
return skeletonDataAsset;
|
||
|
} else {
|
||
|
EditorUtility.DisplayDialog("Error!", "Must specify both Spine JSON and AtlasAsset array", "OK");
|
||
|
return null;
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region SkeletonDataFileValidator
|
||
|
internal static class SkeletonDataFileValidator {
|
||
|
static int[][] compatibleBinaryVersions = { new[] { 3, 6, 0 }, new[] { 3, 5, 0 } };
|
||
|
static int[][] compatibleJsonVersions = { new[] { 3, 6, 0 }, new[] { 3, 7, 0 }, new[] { 3, 5, 0 } };
|
||
|
//static bool isFixVersionRequired = false;
|
||
|
|
||
|
public static bool CheckForValidSkeletonData (string skeletonJSONPath) {
|
||
|
string dir = Path.GetDirectoryName(skeletonJSONPath);
|
||
|
TextAsset textAsset = (TextAsset)AssetDatabase.LoadAssetAtPath(skeletonJSONPath, typeof(TextAsset));
|
||
|
DirectoryInfo dirInfo = new DirectoryInfo(dir);
|
||
|
FileInfo[] files = dirInfo.GetFiles("*.asset");
|
||
|
|
||
|
foreach (var path in files) {
|
||
|
string localPath = dir + "/" + path.Name;
|
||
|
var obj = AssetDatabase.LoadAssetAtPath(localPath, typeof(Object));
|
||
|
var skeletonDataAsset = obj as SkeletonDataAsset;
|
||
|
if (skeletonDataAsset != null && skeletonDataAsset.skeletonJSON == textAsset)
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
public static bool IsSpineData (TextAsset asset) {
|
||
|
if (asset == null)
|
||
|
return false;
|
||
|
|
||
|
bool isSpineData = false;
|
||
|
string rawVersion = null;
|
||
|
|
||
|
int[][] compatibleVersions;
|
||
|
if (asset.name.Contains(".skel")) {
|
||
|
try {
|
||
|
rawVersion = SkeletonBinary.GetVersionString(new MemoryStream(asset.bytes));
|
||
|
isSpineData = !(string.IsNullOrEmpty(rawVersion));
|
||
|
compatibleVersions = compatibleBinaryVersions;
|
||
|
} catch (System.Exception e) {
|
||
|
Debug.LogErrorFormat("Failed to read '{0}'. It is likely not a binary Spine SkeletonData file.\n{1}", asset.name, e);
|
||
|
return false;
|
||
|
}
|
||
|
} else {
|
||
|
object obj = Json.Deserialize(new StringReader(asset.text));
|
||
|
if (obj == null) {
|
||
|
Debug.LogErrorFormat("'{0}' is not valid JSON.", asset.name);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var root = obj as Dictionary<string, object>;
|
||
|
if (root == null) {
|
||
|
Debug.LogError("Parser returned an incorrect type.");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
isSpineData = root.ContainsKey("skeleton");
|
||
|
if (isSpineData) {
|
||
|
var skeletonInfo = (Dictionary<string, object>)root["skeleton"];
|
||
|
object jv;
|
||
|
skeletonInfo.TryGetValue("spine", out jv);
|
||
|
rawVersion = jv as string;
|
||
|
}
|
||
|
|
||
|
compatibleVersions = compatibleJsonVersions;
|
||
|
}
|
||
|
|
||
|
// Version warning
|
||
|
if (isSpineData) {
|
||
|
string primaryRuntimeVersionDebugString = compatibleVersions[0][0] + "." + compatibleVersions[0][1];
|
||
|
|
||
|
if (string.IsNullOrEmpty(rawVersion)) {
|
||
|
Debug.LogWarningFormat("Skeleton '{0}' has no version information. It may be incompatible with your runtime version: spine-unity v{1}", asset.name, primaryRuntimeVersionDebugString);
|
||
|
} else {
|
||
|
string[] versionSplit = rawVersion.Split('.');
|
||
|
bool match = false;
|
||
|
foreach (var version in compatibleVersions) {
|
||
|
bool primaryMatch = version[0] == int.Parse(versionSplit[0]);
|
||
|
bool secondaryMatch = version[1] == int.Parse(versionSplit[1]);
|
||
|
|
||
|
// if (isFixVersionRequired) secondaryMatch &= version[2] <= int.Parse(jsonVersionSplit[2]);
|
||
|
|
||
|
if (primaryMatch && secondaryMatch) {
|
||
|
match = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!match)
|
||
|
Debug.LogWarningFormat("Skeleton '{0}' (exported with Spine {1}) may be incompatible with your runtime version: spine-unity v{2}", asset.name, rawVersion, primaryRuntimeVersionDebugString);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return isSpineData;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region SkeletonAnimation Menu
|
||
|
public static void IngestAdvancedRenderSettings (SkeletonRenderer skeletonRenderer) {
|
||
|
const string PMAShaderQuery = "Spine/Skeleton";
|
||
|
const string TintBlackShaderQuery = "Tint Black";
|
||
|
|
||
|
if (skeletonRenderer == null) return;
|
||
|
var skeletonDataAsset = skeletonRenderer.skeletonDataAsset;
|
||
|
if (skeletonDataAsset == null) return;
|
||
|
|
||
|
bool pmaVertexColors = false;
|
||
|
bool tintBlack = false;
|
||
|
foreach (AtlasAsset atlasAsset in skeletonDataAsset.atlasAssets) {
|
||
|
if (!pmaVertexColors) {
|
||
|
foreach (Material m in atlasAsset.materials) {
|
||
|
if (m.shader.name.Contains(PMAShaderQuery)) {
|
||
|
pmaVertexColors = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!tintBlack) {
|
||
|
foreach (Material m in atlasAsset.materials) {
|
||
|
if (m.shader.name.Contains(TintBlackShaderQuery)) {
|
||
|
tintBlack = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
skeletonRenderer.pmaVertexColors = pmaVertexColors;
|
||
|
skeletonRenderer.tintBlack = tintBlack;
|
||
|
}
|
||
|
|
||
|
public static SkeletonAnimation InstantiateSkeletonAnimation (SkeletonDataAsset skeletonDataAsset, string skinName, bool destroyInvalid = true) {
|
||
|
var skeletonData = skeletonDataAsset.GetSkeletonData(true);
|
||
|
var skin = skeletonData != null ? skeletonData.FindSkin(skinName) : null;
|
||
|
return InstantiateSkeletonAnimation(skeletonDataAsset, skin, destroyInvalid);
|
||
|
}
|
||
|
|
||
|
public static SkeletonAnimation InstantiateSkeletonAnimation (SkeletonDataAsset skeletonDataAsset, Skin skin = null, bool destroyInvalid = true) {
|
||
|
SkeletonData data = skeletonDataAsset.GetSkeletonData(true);
|
||
|
|
||
|
if (data == null) {
|
||
|
for (int i = 0; i < skeletonDataAsset.atlasAssets.Length; i++) {
|
||
|
string reloadAtlasPath = AssetDatabase.GetAssetPath(skeletonDataAsset.atlasAssets[i]);
|
||
|
skeletonDataAsset.atlasAssets[i] = (AtlasAsset)AssetDatabase.LoadAssetAtPath(reloadAtlasPath, typeof(AtlasAsset));
|
||
|
}
|
||
|
data = skeletonDataAsset.GetSkeletonData(false);
|
||
|
}
|
||
|
|
||
|
if (data == null) {
|
||
|
Debug.LogWarning("InstantiateSkeletonAnimation tried to instantiate a skeleton from an invalid SkeletonDataAsset.");
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
string spineGameObjectName = string.Format("Spine GameObject ({0})", skeletonDataAsset.name.Replace("_SkeletonData", ""));
|
||
|
GameObject go = new GameObject(spineGameObjectName, typeof(MeshFilter), typeof(MeshRenderer), typeof(SkeletonAnimation));
|
||
|
SkeletonAnimation newSkeletonAnimation = go.GetComponent<SkeletonAnimation>();
|
||
|
newSkeletonAnimation.skeletonDataAsset = skeletonDataAsset;
|
||
|
IngestAdvancedRenderSettings(newSkeletonAnimation);
|
||
|
|
||
|
try {
|
||
|
newSkeletonAnimation.Initialize(false);
|
||
|
} catch (System.Exception e) {
|
||
|
if (destroyInvalid) {
|
||
|
Debug.LogWarning("Editor-instantiated SkeletonAnimation threw an Exception. Destroying GameObject to prevent orphaned GameObject.");
|
||
|
GameObject.DestroyImmediate(go);
|
||
|
}
|
||
|
Debug.Log(e);
|
||
|
}
|
||
|
|
||
|
// Set Defaults
|
||
|
bool noSkins = data.DefaultSkin == null && (data.Skins == null || data.Skins.Count == 0); // Support attachmentless/skinless SkeletonData.
|
||
|
skin = skin ?? data.DefaultSkin ?? (noSkins ? null : data.Skins.Items[0]);
|
||
|
if (skin != null) {
|
||
|
newSkeletonAnimation.initialSkinName = skin.Name;
|
||
|
newSkeletonAnimation.skeleton.SetSkin(skin);
|
||
|
}
|
||
|
|
||
|
newSkeletonAnimation.zSpacing = defaultZSpacing;
|
||
|
|
||
|
newSkeletonAnimation.skeleton.Update(0);
|
||
|
newSkeletonAnimation.state.Update(0);
|
||
|
newSkeletonAnimation.state.Apply(newSkeletonAnimation.skeleton);
|
||
|
newSkeletonAnimation.skeleton.UpdateWorldTransform();
|
||
|
|
||
|
return newSkeletonAnimation;
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region SkeletonAnimator
|
||
|
#if SPINE_SKELETONANIMATOR
|
||
|
static void UpdateMecanimClips (SkeletonDataAsset skeletonDataAsset) {
|
||
|
if (skeletonDataAsset.controller == null)
|
||
|
return;
|
||
|
|
||
|
SkeletonBaker.GenerateMecanimAnimationClips(skeletonDataAsset);
|
||
|
}
|
||
|
|
||
|
public static SkeletonAnimator InstantiateSkeletonAnimator (SkeletonDataAsset skeletonDataAsset, string skinName) {
|
||
|
return InstantiateSkeletonAnimator(skeletonDataAsset, skeletonDataAsset.GetSkeletonData(true).FindSkin(skinName));
|
||
|
}
|
||
|
|
||
|
public static SkeletonAnimator InstantiateSkeletonAnimator (SkeletonDataAsset skeletonDataAsset, Skin skin = null) {
|
||
|
string spineGameObjectName = string.Format("Spine Mecanim GameObject ({0})", skeletonDataAsset.name.Replace("_SkeletonData", ""));
|
||
|
GameObject go = new GameObject(spineGameObjectName, typeof(MeshFilter), typeof(MeshRenderer), typeof(Animator), typeof(SkeletonAnimator));
|
||
|
|
||
|
if (skeletonDataAsset.controller == null) {
|
||
|
SkeletonBaker.GenerateMecanimAnimationClips(skeletonDataAsset);
|
||
|
Debug.Log(string.Format("Mecanim controller was automatically generated and assigned for {0}", skeletonDataAsset.name));
|
||
|
}
|
||
|
|
||
|
go.GetComponent<Animator>().runtimeAnimatorController = skeletonDataAsset.controller;
|
||
|
|
||
|
SkeletonAnimator anim = go.GetComponent<SkeletonAnimator>();
|
||
|
anim.skeletonDataAsset = skeletonDataAsset;
|
||
|
IngestAdvancedRenderSettings(anim);
|
||
|
|
||
|
SkeletonData data = skeletonDataAsset.GetSkeletonData(true);
|
||
|
if (data == null) {
|
||
|
for (int i = 0; i < skeletonDataAsset.atlasAssets.Length; i++) {
|
||
|
string reloadAtlasPath = AssetDatabase.GetAssetPath(skeletonDataAsset.atlasAssets[i]);
|
||
|
skeletonDataAsset.atlasAssets[i] = (AtlasAsset)AssetDatabase.LoadAssetAtPath(reloadAtlasPath, typeof(AtlasAsset));
|
||
|
}
|
||
|
data = skeletonDataAsset.GetSkeletonData(true);
|
||
|
}
|
||
|
|
||
|
// Set defaults
|
||
|
skin = skin ?? data.DefaultSkin ?? data.Skins.Items[0];
|
||
|
anim.zSpacing = defaultZSpacing;
|
||
|
|
||
|
anim.Initialize(false);
|
||
|
anim.skeleton.SetSkin(skin);
|
||
|
anim.initialSkinName = skin.Name;
|
||
|
|
||
|
anim.skeleton.Update(0);
|
||
|
anim.skeleton.UpdateWorldTransform();
|
||
|
anim.LateUpdate();
|
||
|
|
||
|
return anim;
|
||
|
}
|
||
|
#endif
|
||
|
#endregion
|
||
|
|
||
|
#region SpineTK2DEditorUtility
|
||
|
internal static class SpineTK2DEditorUtility {
|
||
|
const string SPINE_TK2D_DEFINE = "SPINE_TK2D";
|
||
|
|
||
|
static bool IsInvalidGroup (BuildTargetGroup group) {
|
||
|
int gi = (int)group;
|
||
|
return
|
||
|
gi == 15 || gi == 16
|
||
|
||
|
||
|
group == BuildTargetGroup.Unknown;
|
||
|
}
|
||
|
|
||
|
internal static void EnableTK2D () {
|
||
|
bool added = false;
|
||
|
foreach (BuildTargetGroup group in System.Enum.GetValues(typeof(BuildTargetGroup))) {
|
||
|
if (IsInvalidGroup(group))
|
||
|
continue;
|
||
|
|
||
|
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
|
||
|
if (!defines.Contains(SPINE_TK2D_DEFINE)) {
|
||
|
added = true;
|
||
|
if (defines.EndsWith(";", System.StringComparison.Ordinal))
|
||
|
defines = defines + SPINE_TK2D_DEFINE;
|
||
|
else
|
||
|
defines = defines + ";" + SPINE_TK2D_DEFINE;
|
||
|
|
||
|
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (added) {
|
||
|
Debug.LogWarning("Setting Scripting Define Symbol " + SPINE_TK2D_DEFINE);
|
||
|
} else {
|
||
|
Debug.LogWarning("Already Set Scripting Define Symbol " + SPINE_TK2D_DEFINE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
internal static void DisableTK2D () {
|
||
|
bool removed = false;
|
||
|
foreach (BuildTargetGroup group in System.Enum.GetValues(typeof(BuildTargetGroup))) {
|
||
|
if (IsInvalidGroup(group))
|
||
|
continue;
|
||
|
|
||
|
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
|
||
|
if (defines.Contains(SPINE_TK2D_DEFINE)) {
|
||
|
removed = true;
|
||
|
if (defines.Contains(SPINE_TK2D_DEFINE + ";"))
|
||
|
defines = defines.Replace(SPINE_TK2D_DEFINE + ";", "");
|
||
|
else
|
||
|
defines = defines.Replace(SPINE_TK2D_DEFINE, "");
|
||
|
|
||
|
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (removed) {
|
||
|
Debug.LogWarning("Removing Scripting Define Symbol " + SPINE_TK2D_DEFINE);
|
||
|
} else {
|
||
|
Debug.LogWarning("Already Removed Scripting Define Symbol " + SPINE_TK2D_DEFINE);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
public static string GetPathSafeName (string name) {
|
||
|
foreach (char c in System.IO.Path.GetInvalidFileNameChars()) { // Doesn't handle more obscure file name limitations.
|
||
|
name = name.Replace(c, '_');
|
||
|
}
|
||
|
return name;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static class SpineHandles {
|
||
|
internal static float handleScale = 1f;
|
||
|
public static Color BoneColor { get { return new Color(0.8f, 0.8f, 0.8f, 0.4f); } }
|
||
|
public static Color PathColor { get { return new Color(254/255f, 127/255f, 0); } }
|
||
|
public static Color TransformContraintColor { get { return new Color(170/255f, 226/255f, 35/255f); } }
|
||
|
public static Color IkColor { get { return new Color(228/255f,90/255f,43/255f); } }
|
||
|
public static Color PointColor { get { return new Color(1f, 1f, 0f, 1f); } }
|
||
|
|
||
|
static Vector3[] _boneMeshVerts = {
|
||
|
new Vector3(0, 0, 0),
|
||
|
new Vector3(0.1f, 0.1f, 0),
|
||
|
new Vector3(1, 0, 0),
|
||
|
new Vector3(0.1f, -0.1f, 0)
|
||
|
};
|
||
|
static Mesh _boneMesh;
|
||
|
public static Mesh BoneMesh {
|
||
|
get {
|
||
|
if (_boneMesh == null) {
|
||
|
_boneMesh = new Mesh {
|
||
|
vertices = _boneMeshVerts,
|
||
|
uv = new Vector2[4],
|
||
|
triangles = new [] { 0, 1, 2, 2, 3, 0 }
|
||
|
};
|
||
|
_boneMesh.RecalculateBounds();
|
||
|
_boneMesh.RecalculateNormals();
|
||
|
}
|
||
|
return _boneMesh;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static Mesh _arrowheadMesh;
|
||
|
public static Mesh ArrowheadMesh {
|
||
|
get {
|
||
|
if (_arrowheadMesh == null) {
|
||
|
_arrowheadMesh = new Mesh {
|
||
|
vertices = new [] {
|
||
|
new Vector3(0, 0),
|
||
|
new Vector3(-0.1f, 0.05f),
|
||
|
new Vector3(-0.1f, -0.05f)
|
||
|
},
|
||
|
uv = new Vector2[3],
|
||
|
triangles = new [] { 0, 1, 2 }
|
||
|
};
|
||
|
_arrowheadMesh.RecalculateBounds();
|
||
|
_arrowheadMesh.RecalculateNormals();
|
||
|
}
|
||
|
return _arrowheadMesh;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static Material _boneMaterial;
|
||
|
static Material BoneMaterial {
|
||
|
get {
|
||
|
if (_boneMaterial == null) {
|
||
|
_boneMaterial = new Material(Shader.Find("Hidden/Spine/Bones"));
|
||
|
_boneMaterial.SetColor("_Color", SpineHandles.BoneColor);
|
||
|
}
|
||
|
|
||
|
return _boneMaterial;
|
||
|
}
|
||
|
}
|
||
|
public static Material GetBoneMaterial () {
|
||
|
BoneMaterial.SetColor("_Color", SpineHandles.BoneColor);
|
||
|
return BoneMaterial;
|
||
|
}
|
||
|
|
||
|
public static Material GetBoneMaterial (Color color) {
|
||
|
BoneMaterial.SetColor("_Color", color);
|
||
|
return BoneMaterial;
|
||
|
}
|
||
|
|
||
|
static Material _ikMaterial;
|
||
|
public static Material IKMaterial {
|
||
|
get {
|
||
|
if (_ikMaterial == null) {
|
||
|
_ikMaterial = new Material(Shader.Find("Hidden/Spine/Bones"));
|
||
|
_ikMaterial.SetColor("_Color", SpineHandles.IkColor);
|
||
|
}
|
||
|
return _ikMaterial;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static GUIStyle _boneNameStyle;
|
||
|
public static GUIStyle BoneNameStyle {
|
||
|
get {
|
||
|
if (_boneNameStyle == null) {
|
||
|
_boneNameStyle = new GUIStyle(EditorStyles.whiteMiniLabel) {
|
||
|
alignment = TextAnchor.MiddleCenter,
|
||
|
stretchWidth = true,
|
||
|
padding = new RectOffset(0, 0, 0, 0),
|
||
|
contentOffset = new Vector2(-5f, 0f)
|
||
|
};
|
||
|
}
|
||
|
return _boneNameStyle;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static GUIStyle _pathNameStyle;
|
||
|
public static GUIStyle PathNameStyle {
|
||
|
get {
|
||
|
if (_pathNameStyle == null) {
|
||
|
_pathNameStyle = new GUIStyle(SpineHandles.BoneNameStyle);
|
||
|
_pathNameStyle.normal.textColor = SpineHandles.PathColor;
|
||
|
}
|
||
|
return _pathNameStyle;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static GUIStyle _pointNameStyle;
|
||
|
public static GUIStyle PointNameStyle {
|
||
|
get {
|
||
|
if (_pointNameStyle == null) {
|
||
|
_pointNameStyle = new GUIStyle(SpineHandles.BoneNameStyle);
|
||
|
_pointNameStyle.normal.textColor = SpineHandles.PointColor;
|
||
|
}
|
||
|
return _pointNameStyle;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void DrawBoneNames (Transform transform, Skeleton skeleton, float positionScale = 1f) {
|
||
|
GUIStyle style = BoneNameStyle;
|
||
|
foreach (Bone b in skeleton.Bones) {
|
||
|
var pos = new Vector3(b.WorldX * positionScale, b.WorldY * positionScale, 0) + (new Vector3(b.A, b.C) * (b.Data.Length * 0.5f));
|
||
|
pos = transform.TransformPoint(pos);
|
||
|
Handles.Label(pos, b.Data.Name, style);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void DrawBones (Transform transform, Skeleton skeleton, float positionScale = 1f) {
|
||
|
float boneScale = 1.8f; // Draw the root bone largest;
|
||
|
DrawCrosshairs2D(skeleton.Bones.Items[0].GetWorldPosition(transform), 0.08f, positionScale);
|
||
|
|
||
|
foreach (Bone b in skeleton.Bones) {
|
||
|
DrawBone(transform, b, boneScale, positionScale);
|
||
|
boneScale = 1f;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static Vector3[] _boneWireBuffer = new Vector3[5];
|
||
|
static Vector3[] GetBoneWireBuffer (Matrix4x4 m) {
|
||
|
for (int i = 0, n = _boneMeshVerts.Length; i < n; i++)
|
||
|
_boneWireBuffer[i] = m.MultiplyPoint(_boneMeshVerts[i]);
|
||
|
|
||
|
_boneWireBuffer[4] = _boneWireBuffer[0]; // closed polygon.
|
||
|
return _boneWireBuffer;
|
||
|
}
|
||
|
public static void DrawBoneWireframe (Transform transform, Bone b, Color color, float skeletonRenderScale = 1f) {
|
||
|
Handles.color = color;
|
||
|
var pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
|
||
|
float length = b.Data.Length;
|
||
|
|
||
|
if (length > 0) {
|
||
|
Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
|
||
|
Vector3 scale = Vector3.one * length * b.WorldScaleX * skeletonRenderScale;
|
||
|
const float my = 1.5f;
|
||
|
scale.y *= (SpineHandles.handleScale + 1) * 0.5f;
|
||
|
scale.y = Mathf.Clamp(scale.x, -my * skeletonRenderScale, my * skeletonRenderScale);
|
||
|
Handles.DrawPolyLine(GetBoneWireBuffer(transform.localToWorldMatrix * Matrix4x4.TRS(pos, rot, scale)));
|
||
|
var wp = transform.TransformPoint(pos);
|
||
|
DrawBoneCircle(wp, color, transform.forward, skeletonRenderScale);
|
||
|
} else {
|
||
|
var wp = transform.TransformPoint(pos);
|
||
|
DrawBoneCircle(wp, color, transform.forward, skeletonRenderScale);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void DrawBone (Transform transform, Bone b, float boneScale, float skeletonRenderScale = 1f) {
|
||
|
var pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
|
||
|
float length = b.Data.Length;
|
||
|
if (length > 0) {
|
||
|
Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
|
||
|
Vector3 scale = Vector3.one * length * b.WorldScaleX * skeletonRenderScale;
|
||
|
const float my = 1.5f;
|
||
|
scale.y *= (SpineHandles.handleScale + 1f) * 0.5f;
|
||
|
scale.y = Mathf.Clamp(scale.x, -my * skeletonRenderScale, my * skeletonRenderScale);
|
||
|
SpineHandles.GetBoneMaterial().SetPass(0);
|
||
|
Graphics.DrawMeshNow(SpineHandles.BoneMesh, transform.localToWorldMatrix * Matrix4x4.TRS(pos, rot, scale));
|
||
|
} else {
|
||
|
var wp = transform.TransformPoint(pos);
|
||
|
DrawBoneCircle(wp, SpineHandles.BoneColor, transform.forward, boneScale * skeletonRenderScale);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void DrawBone (Transform transform, Bone b, float boneScale, Color color, float skeletonRenderScale = 1f) {
|
||
|
var pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
|
||
|
float length = b.Data.Length;
|
||
|
if (length > 0) {
|
||
|
Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
|
||
|
Vector3 scale = Vector3.one * length * b.WorldScaleX;
|
||
|
const float my = 1.5f;
|
||
|
scale.y *= (SpineHandles.handleScale + 1f) * 0.5f;
|
||
|
scale.y = Mathf.Clamp(scale.x, -my, my);
|
||
|
SpineHandles.GetBoneMaterial(color).SetPass(0);
|
||
|
Graphics.DrawMeshNow(SpineHandles.BoneMesh, transform.localToWorldMatrix * Matrix4x4.TRS(pos, rot, scale));
|
||
|
} else {
|
||
|
var wp = transform.TransformPoint(pos);
|
||
|
DrawBoneCircle(wp, color, transform.forward, boneScale * skeletonRenderScale);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void DrawPaths (Transform transform, Skeleton skeleton) {
|
||
|
foreach (Slot s in skeleton.DrawOrder) {
|
||
|
var p = s.Attachment as PathAttachment;
|
||
|
if (p != null) SpineHandles.DrawPath(s, p, transform, true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static float[] pathVertexBuffer;
|
||
|
public static void DrawPath (Slot s, PathAttachment p, Transform t, bool includeName) {
|
||
|
int worldVerticesLength = p.WorldVerticesLength;
|
||
|
|
||
|
if (pathVertexBuffer == null || pathVertexBuffer.Length < worldVerticesLength)
|
||
|
pathVertexBuffer = new float[worldVerticesLength];
|
||
|
|
||
|
float[] pv = pathVertexBuffer;
|
||
|
p.ComputeWorldVertices(s, pv);
|
||
|
|
||
|
var ocolor = Handles.color;
|
||
|
Handles.color = SpineHandles.PathColor;
|
||
|
|
||
|
Matrix4x4 m = t.localToWorldMatrix;
|
||
|
const int step = 6;
|
||
|
int n = worldVerticesLength - step;
|
||
|
Vector3 p0, p1, p2, p3;
|
||
|
for (int i = 2; i < n; i += step) {
|
||
|
p0 = m.MultiplyPoint(new Vector3(pv[i], pv[i+1]));
|
||
|
p1 = m.MultiplyPoint(new Vector3(pv[i+2], pv[i+3]));
|
||
|
p2 = m.MultiplyPoint(new Vector3(pv[i+4], pv[i+5]));
|
||
|
p3 = m.MultiplyPoint(new Vector3(pv[i+6], pv[i+7]));
|
||
|
DrawCubicBezier(p0, p1, p2, p3);
|
||
|
}
|
||
|
|
||
|
n += step;
|
||
|
if (p.Closed) {
|
||
|
p0 = m.MultiplyPoint(new Vector3(pv[n - 4], pv[n - 3]));
|
||
|
p1 = m.MultiplyPoint(new Vector3(pv[n - 2], pv[n - 1]));
|
||
|
p2 = m.MultiplyPoint(new Vector3(pv[0], pv[1]));
|
||
|
p3 = m.MultiplyPoint(new Vector3(pv[2], pv[3]));
|
||
|
DrawCubicBezier(p0, p1, p2, p3);
|
||
|
}
|
||
|
|
||
|
const float endCapSize = 0.05f;
|
||
|
Vector3 firstPoint = m.MultiplyPoint(new Vector3(pv[2], pv[3]));
|
||
|
SpineHandles.DrawDot(firstPoint, endCapSize);
|
||
|
|
||
|
//if (!p.Closed) SpineHandles.DrawDot(m.MultiplyPoint(new Vector3(pv[n - 4], pv[n - 3])), endCapSize);
|
||
|
if (includeName) Handles.Label(firstPoint + new Vector3(0,0.1f), p.Name, PathNameStyle);
|
||
|
|
||
|
Handles.color = ocolor;
|
||
|
}
|
||
|
|
||
|
public static void DrawDot (Vector3 position, float size) {
|
||
|
Handles.DotHandleCap(0, position, Quaternion.identity, size * HandleUtility.GetHandleSize(position), EventType.Ignore); //Handles.DotCap(0, position, Quaternion.identity, size * HandleUtility.GetHandleSize(position));
|
||
|
}
|
||
|
|
||
|
public static void DrawBoundingBoxes (Transform transform, Skeleton skeleton) {
|
||
|
foreach (var slot in skeleton.Slots) {
|
||
|
var bba = slot.Attachment as BoundingBoxAttachment;
|
||
|
if (bba != null) SpineHandles.DrawBoundingBox(slot, bba, transform);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void DrawBoundingBox (Slot slot, BoundingBoxAttachment box, Transform t) {
|
||
|
if (box.Vertices.Length <= 2) return; // Handle cases where user creates a BoundingBoxAttachment but doesn't actually define it.
|
||
|
|
||
|
var worldVerts = new float[box.WorldVerticesLength];
|
||
|
box.ComputeWorldVertices(slot, worldVerts);
|
||
|
|
||
|
Handles.color = Color.green;
|
||
|
Vector3 lastVert = Vector3.zero;
|
||
|
Vector3 vert = Vector3.zero;
|
||
|
Vector3 firstVert = t.TransformPoint(new Vector3(worldVerts[0], worldVerts[1], 0));
|
||
|
for (int i = 0; i < worldVerts.Length; i += 2) {
|
||
|
vert.x = worldVerts[i];
|
||
|
vert.y = worldVerts[i + 1];
|
||
|
vert.z = 0;
|
||
|
|
||
|
vert = t.TransformPoint(vert);
|
||
|
|
||
|
if (i > 0)
|
||
|
Handles.DrawLine(lastVert, vert);
|
||
|
|
||
|
lastVert = vert;
|
||
|
}
|
||
|
|
||
|
Handles.DrawLine(lastVert, firstVert);
|
||
|
}
|
||
|
|
||
|
public static void DrawPointAttachment (Bone bone, PointAttachment pointAttachment, Transform skeletonTransform) {
|
||
|
if (bone == null) return;
|
||
|
if (pointAttachment == null) return;
|
||
|
|
||
|
Vector2 localPos;
|
||
|
pointAttachment.ComputeWorldPosition(bone, out localPos.x, out localPos.y);
|
||
|
float localRotation = pointAttachment.ComputeWorldRotation(bone);
|
||
|
Matrix4x4 m = Matrix4x4.TRS(localPos, Quaternion.Euler(0, 0, localRotation), Vector3.one) * Matrix4x4.TRS(Vector3.right * 0.25f, Quaternion.identity, Vector3.one);
|
||
|
|
||
|
DrawBoneCircle(skeletonTransform.TransformPoint(localPos), SpineHandles.PointColor, Vector3.back, 1.3f);
|
||
|
DrawArrowhead(skeletonTransform.localToWorldMatrix * m);
|
||
|
}
|
||
|
|
||
|
public static void DrawConstraints (Transform transform, Skeleton skeleton, float skeletonRenderScale = 1f) {
|
||
|
Vector3 targetPos;
|
||
|
Vector3 pos;
|
||
|
bool active;
|
||
|
Color handleColor;
|
||
|
const float Thickness = 4f;
|
||
|
Vector3 normal = transform.forward;
|
||
|
|
||
|
// Transform Constraints
|
||
|
handleColor = SpineHandles.TransformContraintColor;
|
||
|
foreach (var tc in skeleton.TransformConstraints) {
|
||
|
var targetBone = tc.Target;
|
||
|
targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale);
|
||
|
|
||
|
if (tc.TranslateMix > 0) {
|
||
|
if (tc.TranslateMix != 1f) {
|
||
|
Handles.color = handleColor;
|
||
|
foreach (var b in tc.Bones) {
|
||
|
pos = b.GetWorldPosition(transform, skeletonRenderScale);
|
||
|
Handles.DrawDottedLine(targetPos, pos, Thickness);
|
||
|
}
|
||
|
}
|
||
|
SpineHandles.DrawBoneCircle(targetPos, handleColor, normal, 1.3f * skeletonRenderScale);
|
||
|
Handles.color = handleColor;
|
||
|
SpineHandles.DrawCrosshairs(targetPos, 0.2f, targetBone.A, targetBone.B, targetBone.C, targetBone.D, transform, skeletonRenderScale);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// IK Constraints
|
||
|
handleColor = SpineHandles.IkColor;
|
||
|
foreach (var ikc in skeleton.IkConstraints) {
|
||
|
Bone targetBone = ikc.Target;
|
||
|
targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale);
|
||
|
var bones = ikc.Bones;
|
||
|
active = ikc.Mix > 0;
|
||
|
if (active) {
|
||
|
pos = bones.Items[0].GetWorldPosition(transform, skeletonRenderScale);
|
||
|
switch (bones.Count) {
|
||
|
case 1: {
|
||
|
Handles.color = handleColor;
|
||
|
Handles.DrawLine(targetPos, pos);
|
||
|
SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
|
||
|
var m = bones.Items[0].GetMatrix4x4();
|
||
|
m.m03 = targetBone.WorldX * skeletonRenderScale;
|
||
|
m.m13 = targetBone.WorldY * skeletonRenderScale;
|
||
|
SpineHandles.DrawArrowhead(transform.localToWorldMatrix * m);
|
||
|
break;
|
||
|
}
|
||
|
case 2: {
|
||
|
Bone childBone = bones.Items[1];
|
||
|
Vector3 child = childBone.GetWorldPosition(transform, skeletonRenderScale);
|
||
|
Handles.color = handleColor;
|
||
|
Handles.DrawLine(child, pos);
|
||
|
Handles.DrawLine(targetPos, child);
|
||
|
SpineHandles.DrawBoneCircle(pos, handleColor, normal, 0.5f);
|
||
|
SpineHandles.DrawBoneCircle(child, handleColor, normal, 0.5f);
|
||
|
SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
|
||
|
var m = childBone.GetMatrix4x4();
|
||
|
m.m03 = targetBone.WorldX * skeletonRenderScale;
|
||
|
m.m13 = targetBone.WorldY * skeletonRenderScale;
|
||
|
SpineHandles.DrawArrowhead(transform.localToWorldMatrix * m);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
//Handles.Label(targetPos, ikc.Data.Name, SpineHandles.BoneNameStyle);
|
||
|
}
|
||
|
|
||
|
// Path Constraints
|
||
|
handleColor = SpineHandles.PathColor;
|
||
|
foreach (var pc in skeleton.PathConstraints) {
|
||
|
active = pc.TranslateMix > 0;
|
||
|
if (active)
|
||
|
foreach (var b in pc.Bones)
|
||
|
SpineHandles.DrawBoneCircle(b.GetWorldPosition(transform, skeletonRenderScale), handleColor, normal, 1f * skeletonRenderScale);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void DrawCrosshairs2D (Vector3 position, float scale, float skeletonRenderScale = 1f) {
|
||
|
scale *= SpineHandles.handleScale * skeletonRenderScale;
|
||
|
Handles.DrawLine(position + new Vector3(-scale, 0), position + new Vector3(scale, 0));
|
||
|
Handles.DrawLine(position + new Vector3(0, -scale), position + new Vector3(0, scale));
|
||
|
}
|
||
|
|
||
|
static void DrawCrosshairs (Vector3 position, float scale, float a, float b, float c, float d, Transform transform, float skeletonRenderScale = 1f) {
|
||
|
scale *= SpineHandles.handleScale * skeletonRenderScale;
|
||
|
|
||
|
var xOffset = (Vector3)(new Vector2(a, c).normalized * scale);
|
||
|
var yOffset = (Vector3)(new Vector2(b, d).normalized * scale);
|
||
|
xOffset = transform.TransformDirection(xOffset);
|
||
|
yOffset = transform.TransformDirection(yOffset);
|
||
|
|
||
|
Handles.DrawLine(position + xOffset, position - xOffset);
|
||
|
Handles.DrawLine(position + yOffset, position - yOffset);
|
||
|
}
|
||
|
|
||
|
static void DrawArrowhead2D (Vector3 pos, float localRotation, float scale = 1f) {
|
||
|
scale *= SpineHandles.handleScale;
|
||
|
|
||
|
SpineHandles.IKMaterial.SetPass(0);
|
||
|
Graphics.DrawMeshNow(SpineHandles.ArrowheadMesh, Matrix4x4.TRS(pos, Quaternion.Euler(0, 0, localRotation), new Vector3(scale, scale, scale)));
|
||
|
}
|
||
|
|
||
|
static void DrawArrowhead (Vector3 pos, Quaternion worldQuaternion) {
|
||
|
Graphics.DrawMeshNow(SpineHandles.ArrowheadMesh, pos, worldQuaternion, 0);
|
||
|
}
|
||
|
|
||
|
static void DrawArrowhead (Matrix4x4 m) {
|
||
|
float s = SpineHandles.handleScale;
|
||
|
m.m00 *= s;
|
||
|
m.m01 *= s;
|
||
|
m.m02 *= s;
|
||
|
m.m10 *= s;
|
||
|
m.m11 *= s;
|
||
|
m.m12 *= s;
|
||
|
m.m20 *= s;
|
||
|
m.m21 *= s;
|
||
|
m.m22 *= s;
|
||
|
|
||
|
SpineHandles.IKMaterial.SetPass(0);
|
||
|
Graphics.DrawMeshNow(SpineHandles.ArrowheadMesh, m);
|
||
|
}
|
||
|
|
||
|
static void DrawBoneCircle (Vector3 pos, Color outlineColor, Vector3 normal, float scale = 1f) {
|
||
|
scale *= SpineHandles.handleScale;
|
||
|
|
||
|
Color o = Handles.color;
|
||
|
Handles.color = outlineColor;
|
||
|
float firstScale = 0.08f * scale;
|
||
|
Handles.DrawSolidDisc(pos, normal, firstScale);
|
||
|
const float Thickness = 0.03f;
|
||
|
float secondScale = firstScale - (Thickness * SpineHandles.handleScale * scale);
|
||
|
|
||
|
if (secondScale > 0f) {
|
||
|
Handles.color = new Color(0.3f, 0.3f, 0.3f, 0.5f);
|
||
|
Handles.DrawSolidDisc(pos, normal, secondScale);
|
||
|
}
|
||
|
|
||
|
Handles.color = o;
|
||
|
}
|
||
|
|
||
|
internal static void DrawCubicBezier (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3) {
|
||
|
Handles.DrawBezier(p0, p3, p1, p2, Handles.color, Texture2D.whiteTexture, 2f);
|
||
|
// const float dotSize = 0.01f;
|
||
|
// Quaternion q = Quaternion.identity;
|
||
|
// Handles.DotCap(0, p0, q, dotSize);
|
||
|
// Handles.DotCap(0, p1, q, dotSize);
|
||
|
// Handles.DotCap(0, p2, q, dotSize);
|
||
|
// Handles.DotCap(0, p3, q, dotSize);
|
||
|
// Handles.DrawLine(p0, p1);
|
||
|
// Handles.DrawLine(p3, p2);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|