using System; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Text.RegularExpressions; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; namespace RosettaUI { /// /// Unity inspector compatible parser /// /// 同名だが型違いのデータがある場合、UnityEditorと挙動が異なる /// RosettaUIではパースできないものとする /// /// ref: https://github.com/Unity-Technologies/UnityCsReference/blob/6000.0/Editor/Mono/Clipboard/ClipboardParser.cs /// public static partial class ClipboardParser { public const string PrefixEnum = "Enum:"; public const string PrefixGradient = nameof(UnityEditor) + ".GradientWrapperJSON:"; public const string PrefixAnimationCurve = nameof(UnityEditor) + ".AnimationCurveWrapperJSON:"; public const string PrefixGeneric = "GenericPropertyJSON:"; private delegate bool DeserializeFunc(string text, out T value); private delegate T InitializeWithSpanFunc(Span values); public static string Serialize(T value) { var type = typeof(T); return type.Name switch { nameof(Boolean) => value.ToString(), nameof(Int32) => value.ToString(), nameof(UInt32) => value.ToString(), nameof(String) => value?.ToString(), nameof(Single) => SerializeFloat(To(ref value)), nameof(Vector2) => SerializeVector2(To(ref value)), nameof(Vector3) => SerializeVector3(To(ref value)), nameof(Vector4) => SerializeVector4(To(ref value)), nameof(Vector2Int) => SerializeVector2(To(ref value)), nameof(Vector3Int) => SerializeVector3(To(ref value)), nameof(Rect) => SerializeRect(To(ref value)), nameof(RectInt) => SerializeRect(ClipboardParserUtility.FromInt(To(ref value))), nameof(Bounds) => SerializeBounds(To(ref value)), nameof(BoundsInt) => SerializeBounds(ClipboardParserUtility.FromIntKeepValueLook(To(ref value))), nameof(Quaternion) => SerializeQuaternion(To(ref value)), nameof(Color) => SerializeColor(To(ref value)), nameof(Gradient) => SerializeGradient(To(ref value)), nameof(AnimationCurve) => SerializeAnimationCurve(To(ref value)), _ when type.IsEnum => SerializeEnum(value), _ => SerializeGeneric(in value) }; static TOriginal To(ref T value) => UnsafeUtility.As(ref value); } public static (bool success, T value) Deserialize(string text) { var type = typeof(T); return type.Name switch { nameof(Boolean) => (DeserializeBool(text, out var v), From(ref v)), nameof(Int32) => (DeserializeInt(text, out var v), From(ref v)), nameof(UInt32) => (DeserializeUInt(text, out var v), From(ref v)), nameof(String) => (true, From(ref text)), nameof(Single) => (DeserializeFloat(text, out var v), From(ref v)), nameof(Vector2) => (DeserializeVector2(text, out var v), From(ref v)), nameof(Vector3) => (DeserializeVector3(text, out var v), From(ref v)), nameof(Vector4) => (DeserializeVector4(text, out var v), From(ref v)), nameof(Vector2Int) => Cast(text, DeserializeVector2, ClipboardParserUtility.ToInt), nameof(Vector3Int) => Cast(text, DeserializeVector3, ClipboardParserUtility.ToInt), nameof(Rect) => (DeserializeRect(text, out var v), From(ref v)), nameof(RectInt) => Cast(text, DeserializeRect, ClipboardParserUtility.ToInt), nameof(Bounds) => (DeserializeBounds(text, out var v), From(ref v)), nameof(BoundsInt) => Cast(text, DeserializeBounds, ClipboardParserUtility.ToIntKeepValueLook), nameof(Quaternion) => (DeserializeQuaternion(text, out var v), From(ref v)), nameof(Color) => (DeserializeColor(text, out var v), From(ref v)), nameof(Gradient) => (DeserializeGradient(text, out var v), From(ref v)), nameof(AnimationCurve) => (DeserializeAnimationCurve(text, out var v), From(ref v)), _ when type.IsEnum => (DeserializeEnum(text, out var v), v), _ => (DeserializeGeneric(text, out var v), v) }; static T From(ref TOriginal value) => UnsafeUtility.As(ref value); static (bool, T) Cast(string text, DeserializeFunc deserialize, Func castFunc) { var s = deserialize(text, out var v); if(!s) return(false, default); var value = castFunc(v); return (true, From(ref value)); } } public static bool DeserializeBool(string text, out bool value) { value = default; return !string.IsNullOrEmpty(text) && bool.TryParse(text, out value); } public static bool DeserializeInt(string text, out int value) { value = default; return !string.IsNullOrEmpty(text) && int.TryParse(text, out value); } public static bool DeserializeUInt(string text, out uint value) { value = default; return !string.IsNullOrEmpty(text) && uint.TryParse(text, out value); } public static string SerializeFloat(float value) => value.ToString(CultureInfo.InvariantCulture); public static bool DeserializeFloat(string text, out float value) { value = default; return !string.IsNullOrEmpty(text) && float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value); } //TODO: supports flag enum public static string SerializeEnum(object value) { var displayName = RuntimeObjectNames.NicifyVariableName(value.ToString()); return $"{PrefixEnum}{displayName}"; } //TODO: supports flag enum public static bool DeserializeEnum(string text, out TEnum value) { value = default; if (!DeserializeEnum(text, typeof(TEnum), out var obj)) { return false; } value = (TEnum)obj; return true; } public static bool DeserializeEnum(string text, Type type, out object value) { value = default; if (string.IsNullOrEmpty(text)) return false; if (!text.StartsWith(PrefixEnum)) return false; var val = text[PrefixEnum.Length..]; if (string.IsNullOrEmpty(val)) return false; var names = Enum.GetNames(type); foreach (var name in names) { var displayName = RuntimeObjectNames.NicifyVariableName(name); if (displayName != val) continue; value = Enum.Parse(type, name); return true; } return false; } public static string SerializeVector2(Vector2 value) => SerializeFloats(nameof(Vector2), value.x, value.y); public static string SerializeVector3(Vector3 value) => SerializeFloats(nameof(Vector3),value.x, value.y, value.z); public static string SerializeVector4(Vector4 value) => SerializeFloats(nameof(Vector4),value.x, value.y, value.z, value.w); public static string SerializeRect(Rect value) => SerializeFloats(nameof(Rect),value.x, value.y, value.width, value.height); public static string SerializeQuaternion(Quaternion value) => SerializeFloats(nameof(Quaternion),value.x, value.y, value.z, value.w); public static string SerializeBounds(Bounds value) => SerializeFloats(nameof(Bounds), value.center.x, value.center.y, value.center.z, value.extents.x, value.extents.y, value.extents.z); public static bool DeserializeVector2(string text, out Vector2 value) { return DeserializeFloats(text, 2, out value, values => new Vector2(values[0], values[1])); } public static bool DeserializeVector3(string text, out Vector3 value) { return DeserializeFloats(text, 3, out value, values => new Vector3(values[0], values[1], values[2])); } public static bool DeserializeVector4(string text, out Vector4 value) { return DeserializeFloats(text, 4, out value, values => new Vector4(values[0], values[1], values[2], values[3])); } public static bool DeserializeRect(string text, out Rect value) { return DeserializeFloats(text, 4, out value, values => new Rect(values[0], values[1], values[2], values[3])); } public static bool DeserializeQuaternion(string text, out Quaternion value) { return DeserializeFloats(text, 4, out value, values => new Quaternion(values[0], values[1], values[2], values[3]), Quaternion.identity); } public static bool DeserializeBounds(string text, out Bounds value) { return DeserializeFloats(text, 6, out value, values => new Bounds( new Vector3(values[0], values[1], values[2]), new Vector3(values[3], values[4], values[5]) * 2f )); } public static string SerializeColor(Color value) { Color32 ldr = value; Color hdrFromLdr = ldr; if (((Vector4)value - (Vector4)hdrFromLdr).sqrMagnitude < 0.0001f) { return ldr.a == 255 ? $"#{ColorUtility.ToHtmlStringRGB(value)}" : $"#{ColorUtility.ToHtmlStringRGBA(value)}"; } return SerializeFloats(nameof(Color), value.r, value.g, value.b, value.a); } public static bool DeserializeColor(string text, out Color value) { return ColorUtility.TryParseHtmlString(text, out value) || DeserializeFloats(text, 4, out value, values => new Color(values[0], values[1], values[2], values[3]), Color.black); } private static string SerializeFloats(string prefix, params float[] values) { var sb = new StringBuilder(); sb.Append(prefix); sb.Append('('); for (var i = 0; i < values.Length; ++i) { if (i != 0) sb.Append(','); sb.Append(values[i].ToString("g9", CultureInfo.InvariantCulture)); } sb.Append(')'); return sb.ToString(); } private static bool DeserializeFloats(string text, int count, out T value, InitializeWithSpanFunc initializeFunc, T? defaultValue = null) where T : struct { Span values = stackalloc float[count]; var success = DeserializeFloats(text, typeof(T).Name, ref values); value = success ? initializeFunc(values) : defaultValue ?? default; return success; } private static bool DeserializeFloats(string text, string prefix, ref Span values) { if (string.IsNullOrEmpty(text)) return false; var count = values.Length; // build a regex that matches "Prefix(a,b,c,...)" at start of text var sb = new StringBuilder(); sb.Append('^'); sb.Append(prefix); sb.Append("\\("); for (var i = 0; i < count; ++i) { if (i != 0) sb.Append(','); sb.Append("([^,]+)"); } sb.Append("\\)"); var match = Regex.Match(text, sb.ToString()); if (!match.Success || match.Groups.Count <= count) return false; for (var i = 0; i < count; ++i) { if (float.TryParse(match.Groups[i + 1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var f)) values[i] = f; else return false; } return true; } // Gradient // インスペクタはCustomPrefix付きでEditorJsonUtilityでシリアライズしている // https://github.com/Unity-Technologies/UnityCsReference/blob/5406f17521a16bb37880960352990229987aa676/Editor/Mono/Clipboard/ClipboardParser.cs#L353 // // さらにGradientWrapperでかぶせている public static string SerializeGradient(Gradient gradient) { var wrapper = new GradientWrapper { gradient = gradient }; return $"{PrefixGradient}{JsonUtility.ToJson(wrapper)}"; } // Gradient // インスペクタはCustomPrefix付きでEditorJsonUtilityでシリアライズしている // https://github.com/Unity-Technologies/UnityCsReference/blob/5406f17521a16bb37880960352990229987aa676/Editor/Mono/Clipboard/ClipboardParser.cs#L353 // // さらにAnimationCurveWrapperでかぶせている public static string SerializeAnimationCurve(AnimationCurve curve) { var wrapper = new AnimationCurveWrapper() { curve = curve }; return $"{PrefixAnimationCurve}{JsonUtility.ToJson(wrapper)}"; } public static bool DeserializeGradient(string text, out Gradient gradient) { gradient = new Gradient(); if (string.IsNullOrEmpty(text)) return false; if (!text.StartsWith(PrefixGradient, StringComparison.OrdinalIgnoreCase)) return false; try { var wrapper = new GradientWrapper(); JsonUtility.FromJsonOverwrite(text.Remove(0, PrefixGradient.Length), wrapper); gradient = wrapper.gradient; } catch (Exception e) { Debug.LogException(e); return false; } return true; } public static bool DeserializeAnimationCurve(string text, out AnimationCurve curve) { curve = new AnimationCurve(); if (string.IsNullOrEmpty(text)) return false; if (!text.StartsWith(PrefixAnimationCurve, StringComparison.OrdinalIgnoreCase)) return false; try { var wrapper = new AnimationCurveWrapper(); JsonUtility.FromJsonOverwrite(text.Remove(0, PrefixAnimationCurve.Length), wrapper); curve = wrapper.curve; } catch (Exception e) { Debug.LogException(e); return false; } return true; } [Serializable] private class GradientWrapper { public Gradient gradient; } [Serializable] private class AnimationCurveWrapper { public AnimationCurve curve; } public static string SerializeGeneric(in T value) { // 第2引数 fieldName はルートの名前はなんでもいいが // string.IsNullOrEmpty() == true だとインスペクタが無視するのでなにかしら名前をいれておく var dic = ObjectToDictionary(value, typeof(T).Name); var json = Json.Serialize(dic); return $"{PrefixGeneric}{json}"; } public static bool DeserializeGeneric(string text, out T value) { value = default; if ( text == null) return false; if (!text.StartsWith(PrefixGeneric)) return false; if (Json.Deserialize(text[PrefixGeneric.Length..]) is not Dictionary jsonObjects) return false; if (!DictionaryToObject(typeof(T), jsonObjects, out var obj)) return false; value = (T)obj; return true; } } }