638 lines
26 KiB
C#
638 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Newtonsoft.Json.Linq;
|
|
using MCPForUnity.Editor.Helpers;
|
|
using MCPForUnity.Runtime.Helpers;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace MCPForUnity.Editor.Tools.Animation
|
|
{
|
|
internal static class ClipCreate
|
|
{
|
|
public static object Create(JObject @params)
|
|
{
|
|
string clipPath = @params["clipPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(clipPath))
|
|
return new { success = false, message = "'clipPath' is required (e.g. 'Assets/Animations/Walk.anim')" };
|
|
|
|
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
|
if (clipPath == null)
|
|
return new { success = false, message = "Invalid asset path" };
|
|
|
|
if (!clipPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase))
|
|
clipPath += ".anim";
|
|
|
|
// Ensure directory exists
|
|
string dir = Path.GetDirectoryName(clipPath)?.Replace('\\', '/');
|
|
if (!string.IsNullOrEmpty(dir) && !AssetDatabase.IsValidFolder(dir))
|
|
{
|
|
CreateFoldersRecursive(dir);
|
|
}
|
|
|
|
// Check if already exists
|
|
var existing = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
if (existing != null)
|
|
return new { success = false, message = $"AnimationClip already exists at '{clipPath}'. Delete it first or use a different path." };
|
|
|
|
var clip = new AnimationClip();
|
|
string name = @params["name"]?.ToString();
|
|
clip.name = !string.IsNullOrEmpty(name)
|
|
? name
|
|
: Path.GetFileNameWithoutExtension(clipPath);
|
|
|
|
float length = @params["length"]?.ToObject<float>() ?? 1f;
|
|
clip.frameRate = @params["frameRate"]?.ToObject<float>() ?? 60f;
|
|
|
|
bool loop = @params["loop"]?.ToObject<bool>() ?? false;
|
|
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
|
settings.loopTime = loop;
|
|
settings.stopTime = length;
|
|
AnimationUtility.SetAnimationClipSettings(clip, settings);
|
|
|
|
AssetDatabase.CreateAsset(clip, clipPath);
|
|
|
|
// Set m_WrapMode via SerializedObject — clip.wrapMode is a runtime property
|
|
// that doesn't serialize to m_WrapMode, so we set it directly for the legacy system
|
|
if (loop)
|
|
{
|
|
var so = new SerializedObject(clip);
|
|
var wrapProp = so.FindProperty("m_WrapMode");
|
|
if (wrapProp != null)
|
|
{
|
|
wrapProp.intValue = (int)WrapMode.Loop;
|
|
so.ApplyModifiedProperties();
|
|
}
|
|
}
|
|
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"Created AnimationClip at '{clipPath}'",
|
|
data = new
|
|
{
|
|
path = clipPath,
|
|
name = clip.name,
|
|
length,
|
|
frameRate = clip.frameRate,
|
|
isLooping = loop
|
|
}
|
|
};
|
|
}
|
|
|
|
public static object GetInfo(JObject @params)
|
|
{
|
|
string clipPath = @params["clipPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(clipPath))
|
|
return new { success = false, message = "'clipPath' is required" };
|
|
|
|
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
|
if (clipPath == null)
|
|
return new { success = false, message = "Invalid asset path" };
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
if (clip == null)
|
|
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
|
|
|
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
|
var bindings = AnimationUtility.GetCurveBindings(clip);
|
|
|
|
var curves = new List<object>();
|
|
foreach (var binding in bindings)
|
|
{
|
|
var curve = AnimationUtility.GetEditorCurve(clip, binding);
|
|
curves.Add(new
|
|
{
|
|
path = binding.path,
|
|
propertyName = binding.propertyName,
|
|
type = binding.type.Name,
|
|
keyCount = curve?.length ?? 0
|
|
});
|
|
}
|
|
|
|
var events = AnimationUtility.GetAnimationEvents(clip);
|
|
var eventList = events.Select(e => new
|
|
{
|
|
time = e.time,
|
|
functionName = e.functionName,
|
|
stringParameter = e.stringParameter,
|
|
floatParameter = e.floatParameter,
|
|
intParameter = e.intParameter
|
|
}).ToArray();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
data = new
|
|
{
|
|
path = clipPath,
|
|
name = clip.name,
|
|
length = clip.length,
|
|
frameRate = clip.frameRate,
|
|
isLooping = settings.loopTime,
|
|
wrapMode = clip.wrapMode.ToString(),
|
|
curveCount = bindings.Length,
|
|
curves,
|
|
eventCount = events.Length,
|
|
events = eventList
|
|
}
|
|
};
|
|
}
|
|
|
|
public static object AddCurve(JObject @params)
|
|
{
|
|
return SetOrAddCurve(@params, append: true);
|
|
}
|
|
|
|
public static object SetCurve(JObject @params)
|
|
{
|
|
return SetOrAddCurve(@params, append: false);
|
|
}
|
|
|
|
private static object SetOrAddCurve(JObject @params, bool append)
|
|
{
|
|
string clipPath = @params["clipPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(clipPath))
|
|
return new { success = false, message = "'clipPath' is required" };
|
|
|
|
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
|
if (clipPath == null)
|
|
return new { success = false, message = "Invalid asset path" };
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
if (clip == null)
|
|
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
|
|
|
string propertyPath = @params["propertyPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(propertyPath))
|
|
return new { success = false, message = "'propertyPath' is required (e.g. 'localPosition.x')" };
|
|
|
|
string typeName = @params["type"]?.ToString() ?? "Transform";
|
|
Type componentType = ResolveType(typeName);
|
|
if (componentType == null)
|
|
return new { success = false, message = $"Could not resolve type '{typeName}'" };
|
|
|
|
string relativePath = @params["relativePath"]?.ToString() ?? "";
|
|
|
|
JToken keysToken = @params["keys"];
|
|
if (keysToken == null)
|
|
return new { success = false, message = "'keys' is required" };
|
|
|
|
var keyframes = ParseKeyframes(keysToken);
|
|
if (keyframes == null || keyframes.Length == 0)
|
|
return new { success = false, message = "Failed to parse keyframes. Use [{\"time\":0,\"value\":0},...] or [[0,0],[1,1],...]" };
|
|
|
|
AnimationCurve curve;
|
|
var binding = EditorCurveBinding.FloatCurve(relativePath, componentType, propertyPath);
|
|
|
|
if (append)
|
|
{
|
|
curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve();
|
|
foreach (var kf in keyframes)
|
|
{
|
|
curve.AddKey(kf);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
curve = new AnimationCurve(keyframes);
|
|
}
|
|
|
|
// Use AnimationUtility.SetEditorCurve instead of clip.SetCurve to avoid
|
|
// marking the clip as legacy — legacy clips cannot be used in Mecanim BlendTrees.
|
|
Undo.RecordObject(clip, append ? "Add Animation Curve" : "Set Animation Curve");
|
|
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
|
EditorUtility.SetDirty(clip);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
string verb = append ? "Added" : "Set";
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"{verb} curve on '{propertyPath}' ({typeName}) with {keyframes.Length} keyframes",
|
|
data = new
|
|
{
|
|
clipPath,
|
|
propertyPath,
|
|
type = typeName,
|
|
keyframeCount = curve.length
|
|
}
|
|
};
|
|
}
|
|
|
|
public static object SetVectorCurve(JObject @params)
|
|
{
|
|
string clipPath = @params["clipPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(clipPath))
|
|
return new { success = false, message = "'clipPath' is required" };
|
|
|
|
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
|
if (clipPath == null)
|
|
return new { success = false, message = "Invalid asset path" };
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
if (clip == null)
|
|
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
|
|
|
// Accept both 'property' and 'propertyPath' for consistency with add_curve/set_curve
|
|
string property = @params["property"]?.ToString() ?? @params["propertyPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(property))
|
|
return new { success = false, message = "'property' (or 'propertyPath') is required (e.g. 'localPosition', 'localEulerAngles', 'localScale')" };
|
|
|
|
string typeName = @params["type"]?.ToString() ?? "Transform";
|
|
Type componentType = ResolveType(typeName);
|
|
if (componentType == null)
|
|
return new { success = false, message = $"Could not resolve type '{typeName}'" };
|
|
|
|
string relativePath = @params["relativePath"]?.ToString() ?? "";
|
|
|
|
JToken keysToken = @params["keys"];
|
|
if (keysToken == null || keysToken is not JArray keysArray || keysArray.Count == 0)
|
|
return new { success = false, message = "'keys' is required. Use [{\"time\":0,\"value\":[0,1,0]},...]" };
|
|
|
|
// Map property group to axis suffixes
|
|
string[] suffixes;
|
|
switch (property.ToLowerInvariant())
|
|
{
|
|
case "localposition":
|
|
property = "localPosition";
|
|
suffixes = new[] { ".x", ".y", ".z" };
|
|
break;
|
|
case "localeulerangles":
|
|
property = "localEulerAngles";
|
|
suffixes = new[] { ".x", ".y", ".z" };
|
|
break;
|
|
case "localscale":
|
|
property = "localScale";
|
|
suffixes = new[] { ".x", ".y", ".z" };
|
|
break;
|
|
default:
|
|
suffixes = new[] { ".x", ".y", ".z" };
|
|
break;
|
|
}
|
|
|
|
var xKeys = new List<Keyframe>();
|
|
var yKeys = new List<Keyframe>();
|
|
var zKeys = new List<Keyframe>();
|
|
|
|
foreach (var item in keysArray)
|
|
{
|
|
if (item is not JObject keyObj)
|
|
return new { success = false, message = "Each key must be an object with 'time' and 'value' (Vector3 array)" };
|
|
|
|
float time = keyObj["time"]?.ToObject<float>() ?? 0f;
|
|
JToken valueToken = keyObj["value"];
|
|
if (valueToken is not JArray valArray || valArray.Count < 3)
|
|
return new { success = false, message = $"Key at time {time}: 'value' must be a 3-element array [x, y, z]" };
|
|
|
|
float vx = valArray[0].ToObject<float>();
|
|
float vy = valArray[1].ToObject<float>();
|
|
float vz = valArray[2].ToObject<float>();
|
|
|
|
xKeys.Add(new Keyframe(time, vx));
|
|
yKeys.Add(new Keyframe(time, vy));
|
|
zKeys.Add(new Keyframe(time, vz));
|
|
}
|
|
|
|
// Use AnimationUtility.SetEditorCurve instead of clip.SetCurve to avoid
|
|
// marking the clip as legacy — legacy clips cannot be used in Mecanim BlendTrees.
|
|
Undo.RecordObject(clip, "Set Vector Curve");
|
|
var bindingX = EditorCurveBinding.FloatCurve(relativePath, componentType, property + suffixes[0]);
|
|
var bindingY = EditorCurveBinding.FloatCurve(relativePath, componentType, property + suffixes[1]);
|
|
var bindingZ = EditorCurveBinding.FloatCurve(relativePath, componentType, property + suffixes[2]);
|
|
AnimationUtility.SetEditorCurve(clip, bindingX, new AnimationCurve(xKeys.ToArray()));
|
|
AnimationUtility.SetEditorCurve(clip, bindingY, new AnimationCurve(yKeys.ToArray()));
|
|
AnimationUtility.SetEditorCurve(clip, bindingZ, new AnimationCurve(zKeys.ToArray()));
|
|
EditorUtility.SetDirty(clip);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"Set 3 curves on '{property}' ({typeName}) with {keysArray.Count} vector keyframes",
|
|
data = new
|
|
{
|
|
clipPath,
|
|
property,
|
|
type = typeName,
|
|
curves = new[] { property + suffixes[0], property + suffixes[1], property + suffixes[2] },
|
|
keyframeCount = keysArray.Count
|
|
}
|
|
};
|
|
}
|
|
|
|
public static object Assign(JObject @params)
|
|
{
|
|
var go = ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
|
|
if (go == null)
|
|
return new { success = false, message = "Target GameObject not found" };
|
|
|
|
string clipPath = @params["clipPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(clipPath))
|
|
return new { success = false, message = "'clipPath' is required" };
|
|
|
|
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
|
if (clipPath == null)
|
|
return new { success = false, message = "Invalid asset path" };
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
if (clip == null)
|
|
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
|
|
|
// Try legacy Animation component first
|
|
var legacyAnim = go.GetComponent<UnityEngine.Animation>();
|
|
if (legacyAnim != null)
|
|
{
|
|
var wasLegacy = clip.legacy;
|
|
SetupLegacyClip(clip);
|
|
Undo.RecordObject(legacyAnim, "Assign Animation Clip");
|
|
legacyAnim.clip = clip;
|
|
legacyAnim.AddClip(clip, clip.name);
|
|
legacyAnim.playAutomatically = true;
|
|
EditorUtility.SetDirty(legacyAnim);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
// Warn about AnimationEvents if present — they require a MonoBehaviour receiver
|
|
var events = AnimationUtility.GetAnimationEvents(clip);
|
|
string warning = "";
|
|
if (events != null && events.Length > 0)
|
|
{
|
|
var eventNames = new System.Collections.Generic.List<string>();
|
|
foreach (var e in events)
|
|
eventNames.Add(e.functionName);
|
|
warning = $" Warning: This clip has {events.Length} AnimationEvent(s) ({string.Join(", ", eventNames)}). " +
|
|
$"'{go.name}' must have a MonoBehaviour with matching method(s) to receive them, " +
|
|
"otherwise Unity will log 'AnimationEvent has no receiver' errors.";
|
|
}
|
|
|
|
if (!wasLegacy) warning += " Warning: clip was converted to legacy and will not be usable in Mecanim/BlendTrees.";
|
|
|
|
return new { success = true, message = $"Assigned clip '{clip.name}' to Animation component on '{go.name}'.{warning}" };
|
|
}
|
|
|
|
// Add Animation component if no Animator or Animation exists
|
|
var animator = go.GetComponent<Animator>();
|
|
if (animator == null)
|
|
{
|
|
var wasLegacy = clip.legacy;
|
|
SetupLegacyClip(clip);
|
|
Undo.RecordObject(go, "Add Animation Component");
|
|
legacyAnim = Undo.AddComponent<UnityEngine.Animation>(go);
|
|
legacyAnim.clip = clip;
|
|
legacyAnim.AddClip(clip, clip.name);
|
|
legacyAnim.playAutomatically = true;
|
|
EditorUtility.SetDirty(go);
|
|
AssetDatabase.SaveAssets();
|
|
var legacyWarning = !wasLegacy ? " Warning: clip was converted to legacy and will not be usable in Mecanim/BlendTrees." : "";
|
|
return new { success = true, message = $"Added Animation component and assigned clip '{clip.name}' to '{go.name}'.{legacyWarning}" };
|
|
}
|
|
|
|
// Has Animator - we can't programmatically assign clips to Animator states easily,
|
|
// so report what the user should do
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"GameObject '{go.name}' has an Animator component. The clip '{clip.name}' is available at '{clipPath}'. " +
|
|
"Assign it to an Animator Controller state via the Animator window or create an AnimatorOverrideController."
|
|
};
|
|
}
|
|
|
|
private static void SetupLegacyClip(AnimationClip clip)
|
|
{
|
|
var so = new SerializedObject(clip);
|
|
bool changed = false;
|
|
|
|
if (!clip.legacy)
|
|
{
|
|
var legacyProp = so.FindProperty("m_Legacy");
|
|
if (legacyProp != null)
|
|
{
|
|
legacyProp.boolValue = true;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
|
if (settings.loopTime)
|
|
{
|
|
var wrapProp = so.FindProperty("m_WrapMode");
|
|
if (wrapProp != null && wrapProp.intValue != (int)WrapMode.Loop)
|
|
{
|
|
wrapProp.intValue = (int)WrapMode.Loop;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
so.ApplyModifiedProperties();
|
|
}
|
|
|
|
private static Keyframe[] ParseKeyframes(JToken keysToken)
|
|
{
|
|
if (keysToken is JArray array && array.Count > 0)
|
|
{
|
|
var keyframes = new List<Keyframe>();
|
|
|
|
foreach (var item in array)
|
|
{
|
|
if (item is JArray pair && pair.Count >= 2)
|
|
{
|
|
// Shorthand: [time, value]
|
|
float time = pair[0].ToObject<float>();
|
|
float value = pair[1].ToObject<float>();
|
|
keyframes.Add(new Keyframe(time, value));
|
|
}
|
|
else if (item is JObject obj)
|
|
{
|
|
// Full form: {"time":0, "value":0, "inTangent":0, "outTangent":0}
|
|
float time = obj["time"]?.ToObject<float>() ?? 0f;
|
|
float value = obj["value"]?.ToObject<float>() ?? 0f;
|
|
|
|
var kf = new Keyframe(time, value);
|
|
if (obj["inTangent"] != null)
|
|
kf.inTangent = obj["inTangent"].ToObject<float>();
|
|
if (obj["outTangent"] != null)
|
|
kf.outTangent = obj["outTangent"].ToObject<float>();
|
|
if (obj["inWeight"] != null)
|
|
kf.inWeight = obj["inWeight"].ToObject<float>();
|
|
if (obj["outWeight"] != null)
|
|
kf.outWeight = obj["outWeight"].ToObject<float>();
|
|
|
|
keyframes.Add(kf);
|
|
}
|
|
}
|
|
|
|
return keyframes.ToArray();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static Type ResolveType(string typeName)
|
|
{
|
|
if (string.IsNullOrEmpty(typeName))
|
|
return typeof(Transform);
|
|
|
|
// Try common Unity types
|
|
Type type = Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule");
|
|
if (type != null) return type;
|
|
|
|
type = Type.GetType($"UnityEngine.{typeName}, UnityEngine.AnimationModule");
|
|
if (type != null) return type;
|
|
|
|
type = Type.GetType($"UnityEngine.{typeName}, UnityEngine");
|
|
if (type != null) return type;
|
|
|
|
// Try fully qualified
|
|
type = Type.GetType(typeName);
|
|
if (type != null) return type;
|
|
|
|
// Fallback: search all loaded assemblies
|
|
foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
|
|
{
|
|
type = assembly.GetType(typeName);
|
|
if (type != null) return type;
|
|
|
|
type = assembly.GetType($"UnityEngine.{typeName}");
|
|
if (type != null) return type;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static object AddEvent(JObject @params)
|
|
{
|
|
string clipPath = @params["clipPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(clipPath))
|
|
return new { success = false, message = "'clipPath' is required" };
|
|
|
|
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
|
if (clipPath == null)
|
|
return new { success = false, message = "Invalid asset path" };
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
if (clip == null)
|
|
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
|
|
|
float time = @params["time"]?.ToObject<float>() ?? 0f;
|
|
string functionName = @params["functionName"]?.ToString();
|
|
if (string.IsNullOrEmpty(functionName))
|
|
return new { success = false, message = "'functionName' is required" };
|
|
|
|
var animEvent = new AnimationEvent
|
|
{
|
|
time = time,
|
|
functionName = functionName,
|
|
stringParameter = @params["stringParameter"]?.ToString() ?? "",
|
|
floatParameter = @params["floatParameter"]?.ToObject<float>() ?? 0f,
|
|
intParameter = @params["intParameter"]?.ToObject<int>() ?? 0
|
|
};
|
|
|
|
var events = AnimationUtility.GetAnimationEvents(clip).ToList();
|
|
events.Add(animEvent);
|
|
AnimationUtility.SetAnimationEvents(clip, events.ToArray());
|
|
|
|
EditorUtility.SetDirty(clip);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"Added event '{functionName}' at time {time} to '{clipPath}'",
|
|
data = new
|
|
{
|
|
clipPath,
|
|
time,
|
|
functionName,
|
|
stringParameter = animEvent.stringParameter,
|
|
floatParameter = animEvent.floatParameter,
|
|
intParameter = animEvent.intParameter
|
|
}
|
|
};
|
|
}
|
|
|
|
public static object RemoveEvent(JObject @params)
|
|
{
|
|
string clipPath = @params["clipPath"]?.ToString();
|
|
if (string.IsNullOrEmpty(clipPath))
|
|
return new { success = false, message = "'clipPath' is required" };
|
|
|
|
clipPath = AssetPathUtility.SanitizeAssetPath(clipPath);
|
|
if (clipPath == null)
|
|
return new { success = false, message = "Invalid asset path" };
|
|
|
|
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(clipPath);
|
|
if (clip == null)
|
|
return new { success = false, message = $"AnimationClip not found at '{clipPath}'" };
|
|
|
|
var events = AnimationUtility.GetAnimationEvents(clip).ToList();
|
|
int originalCount = events.Count;
|
|
|
|
int? eventIndex = @params["eventIndex"]?.ToObject<int?>();
|
|
if (eventIndex.HasValue)
|
|
{
|
|
if (eventIndex.Value < 0 || eventIndex.Value >= events.Count)
|
|
return new { success = false, message = $"Event index {eventIndex.Value} out of range (0-{events.Count - 1})" };
|
|
|
|
events.RemoveAt(eventIndex.Value);
|
|
}
|
|
else
|
|
{
|
|
string functionName = @params["functionName"]?.ToString();
|
|
if (string.IsNullOrEmpty(functionName))
|
|
return new { success = false, message = "Either 'eventIndex' or 'functionName' is required" };
|
|
|
|
float? timeFilter = @params["time"]?.ToObject<float?>();
|
|
events.RemoveAll(e =>
|
|
{
|
|
bool matchesFunction = e.functionName == functionName;
|
|
bool matchesTime = !timeFilter.HasValue || Mathf.Approximately(e.time, timeFilter.Value);
|
|
return matchesFunction && matchesTime;
|
|
});
|
|
}
|
|
|
|
int removedCount = originalCount - events.Count;
|
|
if (removedCount == 0)
|
|
return new { success = false, message = "No matching events found to remove" };
|
|
|
|
AnimationUtility.SetAnimationEvents(clip, events.ToArray());
|
|
EditorUtility.SetDirty(clip);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
return new
|
|
{
|
|
success = true,
|
|
message = $"Removed {removedCount} event(s) from '{clipPath}'",
|
|
data = new
|
|
{
|
|
clipPath,
|
|
removedCount,
|
|
remainingCount = events.Count
|
|
}
|
|
};
|
|
}
|
|
|
|
private static void CreateFoldersRecursive(string folderPath)
|
|
{
|
|
if (AssetDatabase.IsValidFolder(folderPath))
|
|
return;
|
|
|
|
string parent = Path.GetDirectoryName(folderPath)?.Replace('\\', '/');
|
|
if (!string.IsNullOrEmpty(parent) && parent != "Assets" && !AssetDatabase.IsValidFolder(parent))
|
|
{
|
|
CreateFoldersRecursive(parent);
|
|
}
|
|
|
|
string folderName = Path.GetFileName(folderPath);
|
|
if (!string.IsNullOrEmpty(parent) && !string.IsNullOrEmpty(folderName))
|
|
{
|
|
AssetDatabase.CreateFolder(parent, folderName);
|
|
}
|
|
}
|
|
}
|
|
}
|