using System; using System.Linq; using UnityEditor; using UnityEngine; namespace Yum.Fold { public static class FoldEditorValidation { static void Check(bool ok, string message) { if (!ok) throw new Exception(message); Debug.Log("PASS: " + message); } static void ValidateStaggeredKeys(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var first = stack.materials[0]; var second = stack.materials[1]; first.Write(0, new FoldOperator { enabled = true, opcode = 12, vector0 = Vector3.up }); second.Write(0, new FoldOperator { enabled = true, opcode = 12, vector0 = Vector3.right }); FoldKeyframes.Record(clip, root.transform, stack, 0); first.slot0_float0 = 1; FoldKeyframes.Record(clip, root.transform, stack, 1); second.slot0_float0 = 1; second.Write(1, new FoldOperator { enabled = true, opcode = 15, float0 = 1, float1 = 1, vector1 = Vector3.up }); Undo.IncrementCurrentGroup(); Check(FoldKeyframes.Record(clip, root.transform, stack, 2) == 2, "Later pose records only the two changed operators"); string path = AnimationUtility.CalculateTransformPath(second.transform, root.transform); var tBinding = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float0"); var tCurve = AnimationUtility.GetEditorCurve(clip, tBinding); Check(tCurve.length == 3 && tCurve.keys[1].time == 1 && tCurve.keys[1].value == 0, "Later operator gets its explicit previous state at t1"); var secondBindings = AnimationUtility.GetCurveBindings(clip).Where(binding => binding.path == path && (binding.propertyName.StartsWith("slot0_") || binding.propertyName.StartsWith("slot1_"))).ToArray(); Check(secondBindings.Length == 44 && secondBindings.All(binding => AnimationUtility.GetEditorCurve(clip, binding).keys.Any(key => key.time == 1)), "Backfill keys all fields of each changed operator across materials"); Check(secondBindings.Where(binding => binding.propertyName.StartsWith("slot1_")).All(binding => AnimationUtility.GetEditorCurve(clip, binding).keys.First(key => key.time == 1).value == 0), "A newly introduced operator gets explicit zeroes at the previous pose"); Undo.PerformUndo(); Check(AnimationUtility.GetEditorCurve(clip, tBinding).length == 1, "One Undo removes both the previous-state keys and the new pose"); Undo.PerformRedo(); clip.SampleAnimation(root, .5f); Check(Mathf.Abs(first.slot0_float0 - .5f) < .001f && second.slot0_float0 == 0, "t0–t1 animates only the first operator"); clip.SampleAnimation(root, 1.5f); Check(first.slot0_float0 == 1 && Mathf.Abs(second.slot0_float0 - .5f) < .001f && second.slot0_opcode == 12, "t1–t2 holds the first operator and interpolates the second with the correct opcode"); clip.SampleAnimation(root, 2); first.slot0_float0 = .25f; FoldKeyframes.Record(clip, root.transform, stack, 3); var firstBinding = EditorCurveBinding.FloatCurve(AnimationUtility.CalculateTransformPath(first.transform, root.transform), typeof(FoldMaterialAnimator), "slot0_float0"); Check(AnimationUtility.GetEditorCurve(clip, firstBinding).keys.First(key => key.time == 2).value == 1, "Backfill preserves a nonzero last known state"); var missing = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float3"); AnimationUtility.SetEditorCurve(clip, missing, null); second.slot0_float3 = 7; FoldKeyframes.Record(clip, root.transform, stack, 4); Check(AnimationUtility.GetEditorCurve(clip, missing).keys.First(key => key.time == 3).value == 0, "Fields with no history backfill zero rather than the new value"); second.slot0_float3 = 8; FoldKeyframes.Record(clip, root.transform, stack, 4); var replaced = AnimationUtility.GetEditorCurve(clip, missing); Check(replaced.length == 2 && replaced.keys[0].value == 0 && replaced.keys[1].value == 8, "Replacing a pose preserves its previous-state anchor"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } static string CurveSignature(AnimationCurve curve) { return string.Join(";", curve.keys.Select((key, i) => string.Join(",", new[] { key.time.ToString("R"), key.value.ToString("R"), key.inTangent.ToString("R"), key.outTangent.ToString("R"), key.inWeight.ToString("R"), key.outWeight.ToString("R"), key.weightedMode.ToString(), AnimationUtility.GetKeyLeftTangentMode(curve, i).ToString(), AnimationUtility.GetKeyRightTangentMode(curve, i).ToString() }))); } static void ValidateReorder(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; var shortClip = new AnimationClip { frameRate = 60 }; var customClip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; var translate = new FoldOperator { enabled = true, opcode = 12, float0 = .2f, vector0 = Vector3.up }; var scale = new FoldOperator { enabled = true, opcode = 11, float0 = .4f, vector0 = Vector3.one * 2 }; driver0.Write(0, translate); driver0.Write(1, scale); driver0.Write(2, new FoldOperator { enabled = true, opcode = 15, float0 = .7f, float1 = .25f, vector1 = Vector3.up }); FoldKeyframes.Record(clip, root.transform, stack, 0); FoldKeyframes.Record(shortClip, root.transform, stack, 0); FoldKeyframes.Record(customClip, root.transform, stack, 0); string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform); var untouched = AnimationUtility.GetCurveBindings(clip).Where(binding => binding.path != path || (!binding.propertyName.StartsWith("slot0_") && !binding.propertyName.StartsWith("slot1_"))) .ToDictionary(binding => binding, binding => CurveSignature(AnimationUtility.GetEditorCurve(clip, binding))); driver0.Write(0, scale); driver0.Write(1, translate); driver0.slot0_float0 = .8f; driver0.slot1_float0 = .6f; Undo.IncrementCurrentGroup(); Check(FoldKeyframes.Record(clip, root.transform, stack, 1, 60) == 2, "A swap keys only its two affected operator slots"); Check(untouched.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "Reorder leaves every other slot and material curve unchanged"); var binding0 = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float0"); var curve0 = AnimationUtility.GetEditorCurve(clip, binding0); Check(curve0.length == 3 && Mathf.Abs(curve0.keys[1].time - 1f / 60) < .00001f && curve0.keys[1].value == .4f, "Reorder inserts the moved operator's old values at current minus (Advance - 1) frames"); Undo.PerformUndo(); Check(AnimationUtility.GetEditorCurve(clip, binding0).length == 1 && AnimationUtility.GetKeyRightTangentMode(AnimationUtility.GetEditorCurve(clip, binding0), 0) == AnimationUtility.TangentMode.Linear, "One Undo restores the original keys and tangents before the reorder"); Undo.PerformRedo(); clip.SampleAnimation(root, .5f / 60); Check(driver0.slot0_opcode == 12 && driver0.slot1_opcode == 11 && driver0.slot0_float0 == .2f && driver0.slot1_float0 == .4f, "The first frame holds all old operator settings without interpolation"); clip.SampleAnimation(root, 1f / 60); Check(driver0.slot0_opcode == 11 && driver0.slot1_opcode == 12 && driver0.slot0_float0 == .4f && driver0.slot1_float0 == .2f, "At the transition frame both operators switch order with their own previous settings"); clip.SampleAnimation(root, (1 + 1f / 60) * .5f); Check(Mathf.Abs(driver0.slot0_float0 - .6f) < .001f && Mathf.Abs(driver0.slot1_float0 - .4f) < .001f, "After the reorder parameters interpolate normally toward the new pose"); clip.SampleAnimation(root, 1); FoldKeyframes.Record(shortClip, root.transform, stack, 1f / 60, 60); shortClip.SampleAnimation(root, .5f / 60); Check(driver0.slot0_opcode == 12 && driver0.slot0_float0 == .2f, "Adjacent poses use a step without an invalid intermediate frame"); shortClip.SampleAnimation(root, 1f / 60); Check(driver0.slot0_opcode == 11 && driver0.slot0_float0 == .8f, "Adjacent reordered pose reaches the final state at its key"); FoldKeyframes.Record(customClip, root.transform, stack, 1, -30); Check(Mathf.Abs(AnimationUtility.GetEditorCurve(customClip, binding0).keys[1].time - 31f / 60) < .00001f, "Reorder honors the configured Advance magnitude when authoring backwards"); driver0.slot0_float0 = .9f; FoldKeyframes.Record(clip, root.transform, stack, 2, 60); curve0 = AnimationUtility.GetEditorCurve(clip, binding0); Check(AnimationUtility.GetKeyRightTangentMode(curve0, 0) == AnimationUtility.TangentMode.Constant && AnimationUtility.GetKeyLeftTangentMode(curve0, 1) == AnimationUtility.TangentMode.Constant && AnimationUtility.GetKeyRightTangentMode(curve0, 1) == AnimationUtility.TangentMode.Linear, "Later keyframes preserve the stepped entry and smooth exit of the reorder"); clip.SampleAnimation(root, 1); driver0.slot0_float0 = .7f; FoldKeyframes.Record(clip, root.transform, stack, 1, 60); curve0 = AnimationUtility.GetEditorCurve(clip, binding0); Check(curve0.length == 4 && curve0.keys[1].value == .4f && AnimationUtility.GetKeyLeftTangentMode(curve0, 1) == AnimationUtility.TangentMode.Constant, "Replacing the destination pose does not move or overwrite its reorder anchor"); Check(untouched.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "Subsequent edits still leave unrelated operator curves untouched"); } finally { UnityEngine.Object.DestroyImmediate(clip); UnityEngine.Object.DestroyImmediate(shortClip); UnityEngine.Object.DestroyImmediate(customClip); } } static void ValidateAddedOperator(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; var backwards = new AnimationClip { frameRate = 60 }; var clamped = new AnimationClip { frameRate = 60 }; var insertion = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; driver0.Write(0, FoldOperator.Defaults(11)); FoldKeyframes.Record(clip, root.transform, stack, 0); string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform); var untouched = AnimationUtility.GetCurveBindings(clip).Where(binding => binding.path != path || !binding.propertyName.StartsWith("slot1_")) .ToDictionary(binding => binding, binding => CurveSignature(AnimationUtility.GetEditorCurve(clip, binding))); var added = FoldOperator.Defaults(12); added.vector0 = new Vector4(0, 4, 0, 0); driver0.Write(1, added); Undo.IncrementCurrentGroup(); Check(FoldKeyframes.Record(clip, root.transform, stack, 1, 60) == 1, "Adding an operator keys only its own slot"); var defaults = FoldKeyframes.Fields(FoldOperator.AnimationDefaults(12)); foreach (var field in defaults) { var binding = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot1_" + field.name); if (field.name == "opcode") binding = EditorCurveBinding.DiscreteCurve(path, typeof(FoldMaterialAnimator), "slot1_opcode"); Check(AnimationUtility.GetEditorCurve(clip, binding).length == 2, "New operator has a default and user key for " + field.name); } clip.SampleAnimation(root, 0); Check(driver0.slot1_enabled && driver0.slot1_opcode == 12 && driver0.slot1_float0 == 0 && driver0.slot1_vector0 == Vector4.zero, "The earlier key keeps the opcode active but starts its interpolator at zero"); clip.SampleAnimation(root, .5f); stack.enabled = true; stack.Apply(); var block = new MaterialPropertyBlock(); stack.GetComponent().GetPropertyBlock(block, 0); Check(driver0.slot1_enabled && driver0.slot1_opcode == 12 && Mathf.Abs(driver0.slot1_float0 - .5f) < .001f && Mathf.Abs(block.GetFloat(FoldOperator.Prefix(1) + "Float_0") - .5f) < .001f && Mathf.Abs(driver0.slot1_vector0.y - 2) < .001f && Mathf.Abs(block.GetVector(FoldOperator.Prefix(1) + "Vector_0").y - 2) < .001f, "The new operator interpolates from defaults to user settings in native playback and the material"); Check(untouched.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "New-operator defaults leave other slots and material curves untouched"); Undo.PerformUndo(); clip.SampleAnimation(root, 0); Check(driver0.slot1_opcode == 0 && !driver0.slot1_enabled, "One Undo restores the previously empty slot"); Undo.PerformRedo(); clip.SampleAnimation(root, 1); driver0.slot1_vector0 = Vector3.up * 6; FoldKeyframes.Record(clip, root.transform, stack, 1, 60); clip.SampleAnimation(root, .5f); Check(Mathf.Abs(driver0.slot1_vector0.y - 3) < .001f, "Re-keying the user pose preserves interpolation from the default pose"); driver0.Write(1, added); FoldKeyframes.Record(backwards, root.transform, stack, 1, -60); backwards.SampleAnimation(root, 1.5f); Check(Mathf.Abs(driver0.slot1_vector0.y - 2) < .001f, "Negative Advance places the default key forward by the signed offset and interpolates"); driver0.Write(1, added); FoldKeyframes.Record(clamped, root.transform, stack, .5f, 60); clamped.SampleAnimation(root, .25f); Check(Mathf.Abs(driver0.slot1_vector0.y - 2) < .001f, "Default keyframes clamp to frame zero and still interpolate"); foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); driver0.Write(0, FoldOperator.Defaults(11)); FoldKeyframes.Record(insertion, root.transform, stack, 0); driver0.Write(1, driver0.slots[0]); driver0.Write(0, added); FoldKeyframes.Record(insertion, root.transform, stack, 1, 60); insertion.SampleAnimation(root, .5f / 60); Check(driver0.slot0_opcode == 11, "Insertion preserves the old slot occupant until the reorder handoff"); insertion.SampleAnimation(root, 1f / 60); Check(driver0.slot0_opcode == 12 && driver0.slot0_enabled && driver0.slot0_vector0 == Vector4.zero && driver0.slot1_opcode == 11, "An inserted operator starts with active defaults at the reorder handoff"); insertion.SampleAnimation(root, (1 + 1f / 60) * .5f); Check(Mathf.Abs(driver0.slot0_vector0.y - 2) < .001f, "Inserted operators interpolate from defaults after the stepped reorder"); } finally { UnityEngine.Object.DestroyImmediate(clip); UnityEngine.Object.DestroyImmediate(backwards); UnityEngine.Object.DestroyImmediate(clamped); UnityEngine.Object.DestroyImmediate(insertion); } } static void ValidateOpcodeDefaults(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; var changedClip = new AnimationClip { frameRate = 60 }; var historyClip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; FoldKeyframes.Record(clip, root.transform, stack, 0); FoldKeyframes.Record(changedClip, root.transform, stack, 0); for (int opcode = 1; opcode <= 15; opcode++) driver0.Write(opcode - 1, FoldOperator.AnimationDefaults(opcode)); driver0.slot10_float0 = 1; // Scale (1,1,1) remains neutral even at t=1. driver0.slot11_float0 = 1; // Translate (0,0,0) likewise. Check(FoldKeyframes.Record(clip, root.transform, stack, 2, 60) == 15, "Opcode defaults key new operators at the cursor"); Check(AnimationUtility.GetCurveBindings(clip).All(binding => AnimationUtility.GetEditorCurve(clip, binding).keys.All(key => key.time == 0 || key.time == 2)), "Opcode defaults, identity scale and zero translation do not insert earlier keys"); clip.SampleAnimation(root, 0); Check(Enumerable.Range(0, 15).All(slot => driver0.slots[slot].opcode == 0), "Neutral additions preserve the earlier empty stack"); clip.SampleAnimation(root, 2); Check(driver0.slot10_vector0 == (Vector4)Vector3.one && driver0.slot10_float0 == 1, "Scale uses one rather than zero as its neutral input"); driver0.slot10_vector0 = Vector4.zero; FoldKeyframes.Record(changedClip, root.transform, stack, 2, 60); string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform); var earlier = AnimationUtility.GetCurveBindings(changedClip).Where(binding => AnimationUtility.GetEditorCurve(changedClip, binding).keys.Any(key => key.time == 1)).ToArray(); Check(earlier.Length == 22 && earlier.All(binding => binding.path == path && binding.propertyName.StartsWith("slot10_")), "Zero scale is non-default and only that operator gets an earlier key"); changedClip.SampleAnimation(root, 1); Check(driver0.slot10_opcode == 11 && driver0.slot10_vector0 == (Vector4)Vector3.one && driver0.slot10_float0 == 0, "The earlier Scale key uses identity scale and t=0"); changedClip.SampleAnimation(root, 1.5f); Check(Mathf.Abs(driver0.slot10_vector0.x - .5f) < .001f && Mathf.Abs(driver0.slot10_float0 - .5f) < .001f, "Non-default Scale interpolates from its neutral defaults"); foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var op = FoldOperator.Defaults(12); op.vector0 = Vector3.up * 3; op.opcode = 0; driver0.Write(0, op); FoldKeyframes.Record(historyClip, root.transform, stack, 0); op.opcode = 12; driver0.Write(0, op); FoldKeyframes.Record(historyClip, root.transform, stack, 2, 60); Check(AnimationUtility.GetCurveBindings(historyClip).Count(binding => AnimationUtility.GetEditorCurve(historyClip, binding).keys.Any(key => key.time == 1)) == 22, "Non-default inputs backfill even when unchanged from the slot's previous inputs"); } finally { UnityEngine.Object.DestroyImmediate(clip); UnityEngine.Object.DestroyImmediate(changedClip); UnityEngine.Object.DestroyImmediate(historyClip); } } static void ValidateInterpolationAfterReorder(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; var translate = FoldOperator.AnimationDefaults(12); translate.float0 = .2f; var scale = FoldOperator.AnimationDefaults(11); scale.float0 = .4f; driver0.Write(0, translate); driver0.Write(1, scale); FoldKeyframes.Record(clip, root.transform, stack, 0); driver0.Write(0, scale); driver0.Write(1, translate); driver0.slot0_float0 = .8f; FoldKeyframes.Record(clip, root.transform, stack, 1, 60); // Clearing the transition and target leaves a Constant outgoing tangent at t0. FoldKeyframes.Clear(clip, 1f / 60); FoldKeyframes.Clear(clip, 1); clip.SampleAnimation(root, 0); driver0.slot0_float0 = .8f; FoldKeyframes.Record(clip, root.transform, stack, 1, 60); clip.SampleAnimation(root, .5f); Check(Mathf.Abs(driver0.slot0_float0 - .5f) < .001f, "A normal key after clearing a reorder restores interpolation instead of retaining its orphaned step"); string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform); var binding = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float0"); // Mimic a destination key retaining Constant tangents from an earlier edit. var curve = AnimationUtility.GetEditorCurve(clip, binding); AnimationUtility.SetKeyBroken(curve, 1, true); AnimationUtility.SetKeyLeftTangentMode(curve, 1, AnimationUtility.TangentMode.Constant); AnimationUtility.SetEditorCurve(clip, binding, curve); driver0.Write(0, scale); driver0.Write(1, translate); driver0.slot0_float0 = .8f; FoldKeyframes.Record(clip, root.transform, stack, 1, 60); clip.SampleAnimation(root, (1 + 1f / 60) * .5f); Check(Mathf.Abs(driver0.slot0_float0 - .6f) < .001f, "A reorder clears a stale Constant incoming tangent at its destination and resumes interpolation"); curve = AnimationUtility.GetEditorCurve(clip, binding); Check(AnimationUtility.GetKeyRightTangentMode(curve, 0) == AnimationUtility.TangentMode.Constant && AnimationUtility.GetKeyLeftTangentMode(curve, 1) == AnimationUtility.TangentMode.Constant && AnimationUtility.GetKeyRightTangentMode(curve, 1) == AnimationUtility.TangentMode.Linear && AnimationUtility.GetKeyLeftTangentMode(curve, 2) == AnimationUtility.TangentMode.Linear, "Only the entry to the reorder is stepped; both sides of the following interpolation are linear"); AnimationUtility.SetKeyLeftTangentMode(curve, 2, AnimationUtility.TangentMode.Constant); AnimationUtility.SetEditorCurve(clip, binding, curve); clip.SampleAnimation(root, 1); driver0.slot0_float0 = 1; FoldKeyframes.Record(clip, root.transform, stack, 1, 60); clip.SampleAnimation(root, (1 + 1f / 60) * .5f); Check(Mathf.Abs(driver0.slot0_float0 - .7f) < .001f, "Re-keying an existing reordered pose also repairs its interpolation without changing its stepped entry"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } static void ValidateStackReplacement(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; var translate = FoldOperator.Defaults(12); translate.vector0 = Vector3.up * 4; var scale = FoldOperator.Defaults(11); scale.vector0 = Vector3.one * 2; driver0.Write(0, translate); driver0.Write(1, scale); driver0.Write(2, translate); FoldKeyframes.Record(clip, root.transform, stack, 0); driver0.slot0_float0 = driver0.slot1_float0 = driver0.slot2_float0 = 0; FoldKeyframes.Record(clip, root.transform, stack, 1); var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b, b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b))); var rotate = FoldOperator.Defaults(15); rotate.float0 = Mathf.PI; driver0.Write(0, rotate); driver0.Write(1, FoldOperator.Defaults(5)); driver0.Write(2, new FoldOperator()); Undo.IncrementCurrentGroup(); Check(FoldKeyframes.Record(clip, root.transform, stack, 2, 60) == 3, "Replacing a stack keys only replaced and removed slots"); clip.SampleAnimation(root, 1 + .5f / 60); Check(driver0.slot0_opcode == 12 && driver0.slot0_float0 == 0 && driver0.slot0_vector0.y == 4 && driver0.slot1_opcode == 11 && driver0.slot1_float0 == 0 && driver0.slot2_enabled, "Replacement holds every old resting parameter until the handoff"); float handoff = 1 + 1f / 60; clip.SampleAnimation(root, handoff); Check(driver0.slot0_opcode == 15 && driver0.slot0_float1 == 0 && driver0.slot1_opcode == 5 && driver0.slot1_float2 == 0 && !driver0.slot2_enabled && driver0.slot2_opcode == 0, "Replacement steps to new active operators at t=0 and removes old operators at Advance minus one"); clip.SampleAnimation(root, (handoff + 2) / 2); Check(Mathf.Abs(driver0.slot0_float1 - .5f) < .001f && Mathf.Abs(driver0.slot1_float2 - .5f) < .001f, "Replacement resumes interpolation after the stepped handoff"); clip.SampleAnimation(root, .5f); Check(Mathf.Abs(driver0.slot0_float0 - .5f) < .001f && Mathf.Abs(driver0.slot1_float0 - .5f) < .001f, "Replacement preserves the earlier animation into rest"); Undo.PerformUndo(); Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "One Undo restores all curves before stack replacement"); Undo.PerformRedo(); clip.SampleAnimation(root, 2); Check(driver0.slot0_opcode == 15 && driver0.slot0_float1 == 1 && driver0.slot1_float2 == 1, "Redo restores the replacement destination pose"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } static void ValidateCursorOnlyKeyframing(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int slot = 0; slot < 16; slot++) driver.Write(slot, new FoldOperator()); var driver0 = stack.materials[0]; driver0.Write(0, FoldOperator.AnimationDefaults(12)); FoldKeyframes.Record(clip, root.transform, stack, 0); driver0.Write(0, FoldOperator.Defaults(15)); driver0.Write(1, FoldOperator.Defaults(5)); stack.materials[1].Write(0, FoldOperator.Defaults(5)); Undo.IncrementCurrentGroup(); Check(FoldKeyframes.Record(clip, root.transform, stack, 2, 60, (driver, slot) => driver == driver0 && slot == 0) == 3, "Cursor-only preference records changed operators across materials"); string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform); var bindings = AnimationUtility.GetCurveBindings(clip); Check(bindings.Where(b => b.path == path && b.propertyName.StartsWith("slot0_")).All(b => AnimationUtility.GetEditorCurve(clip, b).keys.Select(k => k.time).SequenceEqual(new[] { 0f, 2f })), "Disabling jump-back adds only cursor keys for a replacement, with no earlier hold or handoff"); Check(bindings.Where(b => (b.path == path && b.propertyName.StartsWith("slot1_")) || (b.path != path && b.propertyName.StartsWith("slot0_"))).All(b => AnimationUtility.GetEditorCurve(clip, b).keys.Any(k => k.time == 1)), "Other operators and materials retain automatic jump-back defaults"); Undo.PerformUndo(); Check(AnimationUtility.GetCurveBindings(clip).All(b => AnimationUtility.GetEditorCurve(clip, b).length == 1), "Cursor-only and automatic keys share one Undo step"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } static void ValidateOverwriteJumpback(GameObject root, FoldStack stack) { foreach (bool replacement in new[] { false, true }) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; if (replacement) driver0.Write(0, FoldOperator.Defaults(11)); FoldKeyframes.Record(clip, root.transform, stack, 0); driver0.Write(0, FoldOperator.Defaults(3)); FoldKeyframes.Record(clip, root.transform, stack, 1, 60); float anchor = replacement ? 1f / 60 : 0; string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform); var binding = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float0"); var originalTimes = AnimationUtility.GetEditorCurve(clip, binding).keys.Select(k => k.time).ToArray(); // Change the operator type, then edit its copied settings twice at the same endpoint. var edited = FoldOperator.Defaults(2); for (int edit = 0; edit < 5; edit++) { edited.vector0 = new Vector3(0, 0, -2 - edit); edited.float0 = edit == 3 ? 0 : 1; driver0.Write(0, edited); var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b, b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b))); Undo.IncrementCurrentGroup(); FoldKeyframes.Record(clip, root.transform, stack, 1, 60); Check(AnimationUtility.GetEditorCurve(clip, binding).keys.Select(k => k.time).SequenceEqual(originalTimes), "Re-keying reuses the jumpback time without frame creep (replacement=" + replacement + ", edit=" + edit + ")"); clip.SampleAnimation(root, anchor); Check(driver0.slot0_opcode == 2 && driver0.slot0_float0 == 0 && driver0.slot0_vector0 == edited.vector0, "Re-keying overwrites the jumpback with the edited opcode and Copy fields"); clip.SampleAnimation(root, (anchor + 1) / 2); Check(Mathf.Abs(driver0.slot0_float0 - .5f * edited.float0) < .001f && driver0.slot0_vector0 == edited.vector0, "Edited jumpback still interpolates toward the endpoint"); Undo.PerformUndo(); Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "Undo restores the prior jumpback values and all curve times"); Undo.PerformRedo(); clip.SampleAnimation(root, 1); } // A genuinely later pose must retain its own history. edited.float0 = .25f; driver0.Write(0, edited); FoldKeyframes.Record(clip, root.transform, stack, 2, 60); clip.SampleAnimation(root, 1); Check(driver0.slot0_float0 == 1, "A later authored pose preserves the preceding endpoint"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } } static void ValidateEaseLinearSpans(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; driver0.Write(0, FoldOperator.Defaults(12)); FoldKeyframes.Record(clip, root.transform, stack, 0); driver0.slot0_vector0 = Vector3.right * 4; FoldKeyframes.Record(clip, root.transform, stack, 1, 60); driver0.Write(0, FoldOperator.Defaults(15)); FoldKeyframes.Record(clip, root.transform, stack, 2, 60); string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform); var custom = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot1_float0"); AnimationUtility.SetEditorCurve(clip, custom, AnimationCurve.EaseInOut(0, 0, 2, 1)); var unrelated = EditorCurveBinding.FloatCurve("", typeof(Transform), "m_LocalPosition.x"); AnimationUtility.SetEditorCurve(clip, unrelated, AnimationCurve.Linear(0, 0, 2, 1)); var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b, b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b))); var keys = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b, b => AnimationUtility.GetEditorCurve(clip, b).keys.Select(k => (k.time, k.value)).ToArray()); Undo.IncrementCurrentGroup(); Check(FoldKeyframes.EaseLinearSpans(clip) > 0, "Ease converts existing linear Fold spans"); Check(keys.All(pair => pair.Value.SequenceEqual(AnimationUtility.GetEditorCurve(clip, pair.Key).keys.Select(k => (k.time, k.value)))), "Ease preserves every key time and value"); Check(before.Where(pair => pair.Key == custom || pair.Key == unrelated || pair.Key.propertyName.EndsWith("opcode") || pair.Key.propertyName.EndsWith("enabled")) .All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "Ease preserves custom curves, other tracks and discrete fields"); clip.SampleAnimation(root, .25f); Check(Mathf.Abs(driver0.slot0_vector0.x - .625f) < .0001f, "Former linear translation uses zero-slope ease in/out"); clip.SampleAnimation(root, 1 + .5f / 60); Check(driver0.slot0_opcode == 12 && driver0.slot0_vector0.x == 4, "Ease preserves the stepped hold before a replacement"); float handoff = 1 + 1f / 60; clip.SampleAnimation(root, handoff); Check(driver0.slot0_opcode == 15 && driver0.slot0_float1 == 0, "Ease preserves the neutral replacement handoff"); clip.SampleAnimation(root, Mathf.Lerp(handoff, 2, .25f)); Check(Mathf.Abs(driver0.slot0_float1 - .15625f) < .0001f, "A stepped handoff can ease out without smoothing its incoming step"); Check(FoldKeyframes.EaseLinearSpans(clip) == 0, "Repeated easing is a no-op"); Undo.PerformUndo(); Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "One Undo restores all original curves after easing"); Undo.PerformRedo(); clip.SampleAnimation(root, .25f); Check(Mathf.Abs(driver0.slot0_vector0.x - .625f) < .0001f, "Redo restores easing"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } static void ValidateInsertionMatchingExistingRotate(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; var translate = FoldOperator.Defaults(12); translate.vector0 = Vector3.right; var rotate = FoldOperator.Defaults(15); driver0.Write(0, translate); driver0.Write(1, rotate); FoldKeyframes.Record(clip, root.transform, stack, 0); translate.vector0 = Vector3.right * 2; driver0.Write(0, translate); FoldKeyframes.Record(clip, root.transform, stack, 10, 60); clip.SampleAnimation(root, 11); var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b, b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b))); driver0.Write(2, driver0.slots[1]); driver0.Write(1, driver0.slots[0]); driver0.Write(0, rotate.Copy()); Undo.IncrementCurrentGroup(); FoldKeyframes.Record(clip, root.transform, stack, 11, 60); clip.SampleAnimation(root, 10 + 1f / 60); Check(driver0.slot0_opcode == 15 && driver0.slot0_float1 == 0, "An inserted Rotate matching a later Rotate starts neutral"); Check(driver0.slot1_opcode == 12 && driver0.slot1_vector0 == translate.vector0 && driver0.slot1_float0 == 1 && driver0.slot2_opcode == 15 && driver0.slot2_float1 == 1, "Insertion preserves the translation then rotation sequence at the jumpback"); clip.SampleAnimation(root, 10.5f); Check(driver0.slot2_float1 == 1, "The existing Rotate stays fully applied throughout the new span"); clip.SampleAnimation(root, 10); Check(driver0.slot0_opcode == 12 && driver0.slot1_opcode == 15 && driver0.slot1_float1 == 1, "Late Rotate insertion preserves the preceding pose"); Undo.PerformUndo(); Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "Undo restores all curves before matching-Rotate insertion"); Undo.PerformRedo(); clip.SampleAnimation(root, 10 + 1f / 60); Check(driver0.slot0_float1 == 0 && driver0.slot2_float1 == 1, "Redo keeps only the new Rotate neutral"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } static void ValidateInsertionWithRepeatedOperators(GameObject root, FoldStack stack) { // Separated duplicates, adjacent duplicates, and identical duplicates. for (int scenario = 0; scenario < 3; scenario++) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; var firstTube = FoldOperator.Defaults(2); firstTube.vector0 = new Vector3(0, 0, -1); firstTube.vector1 = Vector3.forward; firstTube.vector2 = Vector3.up; var secondTube = scenario == 2 ? firstTube.Copy() : FoldOperator.Defaults(2); var original = scenario == 0 ? new[] { FoldOperator.Defaults(15), firstTube, FoldOperator.Defaults(3), secondTube, FoldOperator.Defaults(12) } : new[] { FoldOperator.Defaults(15), firstTube, secondTube, FoldOperator.Defaults(12) }; for (int i = 0; i < original.Length; i++) driver0.Write(i, original[i]); FoldKeyframes.Record(clip, root.transform, stack, 0); string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform); var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b, b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b))); var untouched = before.Where(pair => pair.Key.path != path || pair.Key.propertyName.StartsWith("slot0_")) .ToDictionary(pair => pair.Key, pair => pair.Value); for (int i = original.Length; i > 1; i--) driver0.Write(i, original[i - 1]); var fbm = FoldOperator.Defaults(8); driver0.Write(1, fbm); Undo.IncrementCurrentGroup(); FoldKeyframes.Record(clip, root.transform, stack, 1, 60); foreach (float sampleTime in new[] { 1f / 60, .5f, 1f }) { clip.SampleAnimation(root, sampleTime); for (int i = 1; i < original.Length; i++) { var expected = FoldKeyframes.Fields(original[i]); var actual = FoldKeyframes.Fields(driver0.slots[i + 1]); Check(expected.Zip(actual, (a, b) => a.discrete ? a.value.Equals(b.value) : Mathf.Abs(a.value - b.value) < .0001f).All(equal => equal), "Inserting FBM preserves moved operator " + i + " in duplicate scenario " + scenario + " at " + sampleTime); } } clip.SampleAnimation(root, 1f / 60); Check(driver0.slot1_opcode == 8 && (Vector3)driver0.slot1_vector0 == Vector3.zero, "Only the inserted FBM starts from its jumpback amplitude"); clip.SampleAnimation(root, .5f / 60); Check(driver0.slot1_opcode == 2 && driver0.slot1_float0 == 1 && driver0.slot1_vector0 == firstTube.vector0, "The original tube remains intact before the insertion handoff"); Check(untouched.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "Insertion leaves the prefix and other material curves unchanged"); Undo.PerformUndo(); Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))), "Undo restores the complete stack before duplicate-operator insertion"); Undo.PerformRedo(); clip.SampleAnimation(root, .5f); Check(driver0.slot2_float0 == 1 && driver0.slots[original.Length - 1].float0 == 1, "Redo preserves both completed tube folds"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } } static void ValidateJumpbackPolicies(GameObject root, FoldStack stack) { var clip = new AnimationClip { frameRate = 60 }; try { foreach (var driver in stack.materials) for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator()); var driver0 = stack.materials[0]; FoldKeyframes.Record(clip, root.transform, stack, 0); var align = FoldOperator.Defaults(3); align.vector0 = new Vector4(0, 0, -3, 7); align.vector1 = new Vector4(0, 0, 2, 9); align.vector2 = Vector3.up; align.float3 = 17; // Unused fields must also be preserved by Copy. var fbm = FoldOperator.Defaults(8); fbm.vector0 = new Vector4(.3f, .2f, .1f, 8); fbm.vector1 = new Vector3(1, 2, 3); fbm.vector2 = new Vector3(6, 2, 4); fbm.float0 = .7f; fbm.float1 = 3; fbm.float2 = 6; driver0.Write(0, align); driver0.Write(1, fbm); FoldKeyframes.Record(clip, root.transform, stack, 2, 60); clip.SampleAnimation(root, 1); Check(driver0.slot0_float0 == 0 && driver0.slot0_vector0 == align.vector0 && driver0.slot0_vector1 == align.vector1 && driver0.slot0_vector2 == align.vector2 && driver0.slot0_float3 == 17, "Jumpback uses Zero for alignment T and Copy for authored origin, target, axis and unused fields"); Check((Vector3)driver0.slot1_vector0 == Vector3.zero && driver0.slot1_vector0.w == 8 && driver0.slot1_vector1 == fbm.vector1 && driver0.slot1_vector2 == fbm.vector2 && driver0.slot1_float0 == .7f && driver0.slot1_float1 == 3 && driver0.slot1_float2 == 6, "FBM jumpback zeroes amplitude xyz while copying velocity, period, gain, lacunarity, octaves and w"); clip.SampleAnimation(root, 1.5f); Check(Mathf.Abs(driver0.slot1_vector0.x - .15f) < .001f && driver0.slot1_vector2 == fbm.vector2, "FBM amplitude interpolates while its authored period remains fixed"); Check(align.float0 == 1 && fbm.vector0.x == .3f, "Jumpback generation does not mutate authored operators"); var scale = FoldOperator.Defaults(11); scale.vector0 = new Vector4(2, 3, 4, 5); Check(scale.JumpbackDefaults().vector0 == new Vector4(1, 1, 1, 5), "Scale uses its One annotation for xyz and preserves w"); var rotate = FoldOperator.Defaults(15); rotate.float0 = 2; rotate.vector1 = Vector3.forward; Check(rotate.JumpbackDefaults().float0 == 2 && rotate.JumpbackDefaults().float1 == 0 && rotate.JumpbackDefaults().vector1 == rotate.vector1, "Rotate copies authored angle and axis and zeroes T"); var sine = FoldOperator.Defaults(7); Check((Vector3)sine.JumpbackDefaults().vector0 == Vector3.zero && sine.JumpbackDefaults().vector2 == sine.vector2, "Sine Waves zeroes amplitude and preserves wave number"); var seal = FoldOperator.Defaults(6); seal.float0 = .8f; seal.float1 = 3; Check(seal.float3 == 1 && seal.JumpbackDefaults().float3 == 0 && seal.JumpbackDefaults().float0 == .8f && seal.JumpbackDefaults().float1 == 3 && seal.JumpbackDefaults().float2 == seal.float2, "Seal starts interpolation at zero while copying its authored shape and time controls"); align.enabled = false; Check(!align.JumpbackDefaults().enabled, "Copy preserves the authored enabled state"); } finally { UnityEngine.Object.DestroyImmediate(clip); } } [MenuItem("Tools/yum_food/FoldEditor Validation")] public static void Run() { GameObject go = null; Material a = null, b = null; AnimationClip clip = null; if (AnimationMode.InAnimationMode()) throw new InvalidOperationException("Stop Animation preview before running validation."); Undo.IncrementCurrentGroup(); int undoGroup = Undo.GetCurrentGroup(); try { var shader = Shader.Find("yum_food/3ner"); a = new Material(shader); b = new Material(shader); Check(FoldStack.Compatible(a), "Detect complete shader slot layout"); go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.AddComponent(); var renderer = go.GetComponent(); renderer.sharedMaterials = new[] { a, b }; var original = new MaterialPropertyBlock(); original.SetFloat("_Unrelated", 42); renderer.SetPropertyBlock(original, 1); var stack = go.AddComponent(); FoldEditorWindow.Initialize(stack); stack.materials[0].Write(2, new FoldOperator { enabled = true, opcode = 12, float0 = 0, vector0 = Vector3.up }); stack.materials[1].Write(0, new FoldOperator { enabled = true, opcode = 15, float0 = 1, float1 = 0, vector1 = Vector3.up }); FoldEditorWindow.Initialize(stack); Check(stack.materials[0].slots[2].opcode == 12 && stack.materials[1].slots[0].opcode == 15, "Automatic setup preserves existing stacks"); clip = new AnimationClip { frameRate = 60 }; Check(FoldKeyframes.Record(clip, go.transform, stack, 0) == 32, "First pose keys all 16 slots on both materials, including disabled slots"); Check(AnimationUtility.GetCurveBindings(clip).All(binding => stack.materials.SelectMany(driver => AnimationUtility.GetAnimatableBindings(driver.gameObject, go)).Any(actual => actual.path == binding.path && actual.type == binding.type && actual.propertyName == binding.propertyName)), "Every saved curve has a native animatable binding"); Check(AnimationUtility.GetCurveBindings(clip).Length == 32 * 22, "All 22 fields per operator are keyed"); Check(FoldKeyframes.Record(clip, go.transform, stack, .5f) == 0, "Unchanged pose creates no redundant keys"); stack.materials[0].slot2_float0 = 1; Check(FoldKeyframes.Record(clip, go.transform, stack, 1) == 1, "One changed interpolator keys exactly one complete operator"); Check(AnimationUtility.GetCurveBindings(clip).Count(x => AnimationUtility.GetEditorCurve(clip, x).length == 2) == 22, "Changed operator gets all 22 fields"); clip.SampleAnimation(go, .5f); stack.Apply(); Check(Mathf.Abs(stack.materials[0].slots[2].float0 - .5f) < .001f, "Built-in animation samples driver fields"); var block = new MaterialPropertyBlock(); renderer.GetPropertyBlock(block, 0); Check(Mathf.Abs(block.GetFloat(FoldOperator.Prefix(2) + "Float_0") - .5f) < .001f, "Animation drives live material interpolator"); Check(block.GetInteger(FoldOperator.Prefix(2) + "Opcode") == 12, "Animation drives actual integer shader opcode"); renderer.GetPropertyBlock(block, 1); Check(block.GetInteger(FoldOperator.Prefix(0) + "Opcode") == 15 && block.GetFloat("_Unrelated") == 42, "Material indices remain independent and preserve other properties"); stack.materials[0].slot2_opcode = 11; FoldKeyframes.Record(clip, go.transform, stack, 2); clip.SampleAnimation(go, 1 + .5f / 60); Check(stack.materials[0].slots[2].opcode == 12, "Opcode curves hold until the replacement handoff"); clip.SampleAnimation(go, 1 + 1f / 60); Check(stack.materials[0].slots[2].opcode == 11, "Advance zero steps opcode one frame after the previous pose"); clip.SampleAnimation(go, 2); Check(stack.materials[0].slots[2].opcode == 11, "Opcode changes at its keyframe"); stack.enabled = false; renderer.GetPropertyBlock(block, 1); Check(block.GetFloat("_Unrelated") == 42 && block.GetInteger(FoldOperator.Prefix(0) + "Opcode") == 0, "Disabling bridge restores original indexed property block"); Check(a.GetInteger(FoldOperator.Prefix(2) + "Opcode") == 0, "Animation does not mutate shared material assets"); var transformBinding = EditorCurveBinding.FloatCurve("", typeof(Transform), "m_LocalPosition.x"); AnimationUtility.SetEditorCurve(clip, transformBinding, new AnimationCurve( new Keyframe(1 - 1f / 60, 3), new Keyframe(1, 4), new Keyframe(1 + 1f / 60, 5))); var objectBinding = EditorCurveBinding.PPtrCurve("", typeof(MeshRenderer), "m_Materials.Array.data[0]"); AnimationUtility.SetObjectReferenceCurve(clip, objectBinding, new[] { new ObjectReferenceKeyframe { time = 1, value = a } }); Undo.IncrementCurrentGroup(); Check(FoldKeyframes.Clear(clip, 1) == 24, "Clear deletes float, discrete and object-reference keys at the cursor across all tracks"); Check(AnimationUtility.GetEditorCurve(clip, transformBinding).length == 2, "Clear preserves keys on adjacent frames"); Check(AnimationUtility.GetObjectReferenceCurveBindings(clip).Length == 0, "Clear removes empty tracks"); Check(FoldKeyframes.Clear(clip, 1) == 0, "Clearing an empty frame is a no-op"); Undo.PerformUndo(); Check(AnimationUtility.GetEditorCurve(clip, transformBinding).length == 3 && AnimationUtility.GetObjectReferenceCurveBindings(clip).Length == 1, "Undo restores deleted keys and tracks"); ValidateStaggeredKeys(go, stack); ValidateReorder(go, stack); ValidateAddedOperator(go, stack); ValidateOpcodeDefaults(go, stack); ValidateJumpbackPolicies(go, stack); ValidateInsertionWithRepeatedOperators(go, stack); ValidateInsertionMatchingExistingRotate(go, stack); ValidateEaseLinearSpans(go, stack); ValidateOverwriteJumpback(go, stack); ValidateInterpolationAfterReorder(go, stack); ValidateStackReplacement(go, stack); ValidateCursorOnlyKeyframing(go, stack); Check(FoldOperator.AnimationDefaults(5).float2 == 0 && FoldOperator.AnimationDefaults(5).float0 == 2 && FoldOperator.AnimationDefaults(5).float1 == 1, "Norm Conversion starts with t=0 and preserves valid norm exponents"); Check(FoldOperator.AnimationDefaults(15).float1 == 0 && FoldOperator.AnimationDefaults(15).float0 == Mathf.PI / 2 && FoldOperator.AnimationDefaults(15).vector1 == (Vector4)Vector3.up, "Rotate starts with t=0 and preserves its angle and axis"); Debug.Log("FoldEditor validation passed."); } finally { Undo.RevertAllDownToGroup(undoGroup); if (go) UnityEngine.Object.DestroyImmediate(go); if (clip) UnityEngine.Object.DestroyImmediate(clip); if (a) UnityEngine.Object.DestroyImmediate(a); if (b) UnityEngine.Object.DestroyImmediate(b); } } } }