yum/3ner

A toon shader for Unity's BIRP.

git clone https://git.yummers.dev/yum/3ner

yumAdd seal interpolatorb3a533b

master
56.3 KiB792 linesraw
1using System;
2using System.Linq;
3using UnityEditor;
4using UnityEngine;
5namespace Yum.Fold
6{
7    public static class FoldEditorValidation
8    {
9        static void Check(bool ok, string message) { if (!ok) throw new Exception(message); Debug.Log("PASS: " + message); }
10        static void ValidateStaggeredKeys(GameObject root, FoldStack stack)
11        {
12            var clip = new AnimationClip { frameRate = 60 };
13            try
14            {
15                foreach (var driver in stack.materials)
16                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
17                var first = stack.materials[0];
18                var second = stack.materials[1];
19                first.Write(0, new FoldOperator { enabled = true, opcode = 12, vector0 = Vector3.up });
20                second.Write(0, new FoldOperator { enabled = true, opcode = 12, vector0 = Vector3.right });
21                FoldKeyframes.Record(clip, root.transform, stack, 0);
22                first.slot0_float0 = 1;
23                FoldKeyframes.Record(clip, root.transform, stack, 1);
24                second.slot0_float0 = 1;
25                second.Write(1, new FoldOperator { enabled = true, opcode = 15, float0 = 1, float1 = 1, vector1 = Vector3.up });
26                Undo.IncrementCurrentGroup();
27                Check(FoldKeyframes.Record(clip, root.transform, stack, 2) == 2, "Later pose records only the two changed operators");
28                string path = AnimationUtility.CalculateTransformPath(second.transform, root.transform);
29                var tBinding = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float0");
30                var tCurve = AnimationUtility.GetEditorCurve(clip, tBinding);
31                Check(tCurve.length == 3 && tCurve.keys[1].time == 1 && tCurve.keys[1].value == 0, "Later operator gets its explicit previous state at t1");
32                var secondBindings = AnimationUtility.GetCurveBindings(clip).Where(binding => binding.path == path &&
33                    (binding.propertyName.StartsWith("slot0_") || binding.propertyName.StartsWith("slot1_"))).ToArray();
34                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");
35                Check(secondBindings.Where(binding => binding.propertyName.StartsWith("slot1_")).All(binding =>
36                    AnimationUtility.GetEditorCurve(clip, binding).keys.First(key => key.time == 1).value == 0), "A newly introduced operator gets explicit zeroes at the previous pose");
37                Undo.PerformUndo();
38                Check(AnimationUtility.GetEditorCurve(clip, tBinding).length == 1, "One Undo removes both the previous-state keys and the new pose");
39                Undo.PerformRedo();
40                clip.SampleAnimation(root, .5f);
41                Check(Mathf.Abs(first.slot0_float0 - .5f) < .001f && second.slot0_float0 == 0, "t0–t1 animates only the first operator");
42                clip.SampleAnimation(root, 1.5f);
43                Check(first.slot0_float0 == 1 && Mathf.Abs(second.slot0_float0 - .5f) < .001f && second.slot0_opcode == 12,
44                    "t1–t2 holds the first operator and interpolates the second with the correct opcode");
45                clip.SampleAnimation(root, 2);
46                first.slot0_float0 = .25f;
47                FoldKeyframes.Record(clip, root.transform, stack, 3);
48                var firstBinding = EditorCurveBinding.FloatCurve(AnimationUtility.CalculateTransformPath(first.transform, root.transform), typeof(FoldMaterialAnimator), "slot0_float0");
49                Check(AnimationUtility.GetEditorCurve(clip, firstBinding).keys.First(key => key.time == 2).value == 1, "Backfill preserves a nonzero last known state");
50                var missing = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float3");
51                AnimationUtility.SetEditorCurve(clip, missing, null);
52                second.slot0_float3 = 7;
53                FoldKeyframes.Record(clip, root.transform, stack, 4);
54                Check(AnimationUtility.GetEditorCurve(clip, missing).keys.First(key => key.time == 3).value == 0, "Fields with no history backfill zero rather than the new value");
55                second.slot0_float3 = 8;
56                FoldKeyframes.Record(clip, root.transform, stack, 4);
57                var replaced = AnimationUtility.GetEditorCurve(clip, missing);
58                Check(replaced.length == 2 && replaced.keys[0].value == 0 && replaced.keys[1].value == 8, "Replacing a pose preserves its previous-state anchor");
59            }
60            finally { UnityEngine.Object.DestroyImmediate(clip); }
61        }
62
63        static string CurveSignature(AnimationCurve curve)
64        {
65            return string.Join(";", curve.keys.Select((key, i) => string.Join(",", new[] {
66                key.time.ToString("R"), key.value.ToString("R"), key.inTangent.ToString("R"), key.outTangent.ToString("R"),
67                key.inWeight.ToString("R"), key.outWeight.ToString("R"), key.weightedMode.ToString(),
68                AnimationUtility.GetKeyLeftTangentMode(curve, i).ToString(), AnimationUtility.GetKeyRightTangentMode(curve, i).ToString()
69            })));
70        }
71
72        static void ValidateReorder(GameObject root, FoldStack stack)
73        {
74            var clip = new AnimationClip { frameRate = 60 };
75            var shortClip = new AnimationClip { frameRate = 60 };
76            var customClip = new AnimationClip { frameRate = 60 };
77            try
78            {
79                foreach (var driver in stack.materials)
80                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
81                var driver0 = stack.materials[0];
82                var translate = new FoldOperator { enabled = true, opcode = 12, float0 = .2f, vector0 = Vector3.up };
83                var scale = new FoldOperator { enabled = true, opcode = 11, float0 = .4f, vector0 = Vector3.one * 2 };
84                driver0.Write(0, translate);
85                driver0.Write(1, scale);
86                driver0.Write(2, new FoldOperator { enabled = true, opcode = 15, float0 = .7f, float1 = .25f, vector1 = Vector3.up });
87                FoldKeyframes.Record(clip, root.transform, stack, 0);
88                FoldKeyframes.Record(shortClip, root.transform, stack, 0);
89                FoldKeyframes.Record(customClip, root.transform, stack, 0);
90                string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform);
91                var untouched = AnimationUtility.GetCurveBindings(clip).Where(binding => binding.path != path ||
92                    (!binding.propertyName.StartsWith("slot0_") && !binding.propertyName.StartsWith("slot1_")))
93                    .ToDictionary(binding => binding, binding => CurveSignature(AnimationUtility.GetEditorCurve(clip, binding)));
94                driver0.Write(0, scale);
95                driver0.Write(1, translate);
96                driver0.slot0_float0 = .8f;
97                driver0.slot1_float0 = .6f;
98                Undo.IncrementCurrentGroup();
99                Check(FoldKeyframes.Record(clip, root.transform, stack, 1, 60) == 2, "A swap keys only its two affected operator slots");
100                Check(untouched.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
101                    "Reorder leaves every other slot and material curve unchanged");
102                var binding0 = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float0");
103                var curve0 = AnimationUtility.GetEditorCurve(clip, binding0);
104                Check(curve0.length == 3 && Mathf.Abs(curve0.keys[1].time - 1f / 60) < .00001f && curve0.keys[1].value == .4f,
105                    "Reorder inserts the moved operator's old values at current minus (Advance - 1) frames");
106                Undo.PerformUndo();
107                Check(AnimationUtility.GetEditorCurve(clip, binding0).length == 1 &&
108                    AnimationUtility.GetKeyRightTangentMode(AnimationUtility.GetEditorCurve(clip, binding0), 0) == AnimationUtility.TangentMode.Linear,
109                    "One Undo restores the original keys and tangents before the reorder");
110                Undo.PerformRedo();
111                clip.SampleAnimation(root, .5f / 60);
112                Check(driver0.slot0_opcode == 12 && driver0.slot1_opcode == 11 && driver0.slot0_float0 == .2f && driver0.slot1_float0 == .4f,
113                    "The first frame holds all old operator settings without interpolation");
114                clip.SampleAnimation(root, 1f / 60);
115                Check(driver0.slot0_opcode == 11 && driver0.slot1_opcode == 12 && driver0.slot0_float0 == .4f && driver0.slot1_float0 == .2f,
116                    "At the transition frame both operators switch order with their own previous settings");
117                clip.SampleAnimation(root, (1 + 1f / 60) * .5f);
118                Check(Mathf.Abs(driver0.slot0_float0 - .6f) < .001f && Mathf.Abs(driver0.slot1_float0 - .4f) < .001f,
119                    "After the reorder parameters interpolate normally toward the new pose");
120                clip.SampleAnimation(root, 1);
121                FoldKeyframes.Record(shortClip, root.transform, stack, 1f / 60, 60);
122                shortClip.SampleAnimation(root, .5f / 60);
123                Check(driver0.slot0_opcode == 12 && driver0.slot0_float0 == .2f, "Adjacent poses use a step without an invalid intermediate frame");
124                shortClip.SampleAnimation(root, 1f / 60);
125                Check(driver0.slot0_opcode == 11 && driver0.slot0_float0 == .8f, "Adjacent reordered pose reaches the final state at its key");
126                FoldKeyframes.Record(customClip, root.transform, stack, 1, -30);
127                Check(Mathf.Abs(AnimationUtility.GetEditorCurve(customClip, binding0).keys[1].time - 31f / 60) < .00001f,
128                    "Reorder honors the configured Advance magnitude when authoring backwards");
129                driver0.slot0_float0 = .9f;
130                FoldKeyframes.Record(clip, root.transform, stack, 2, 60);
131                curve0 = AnimationUtility.GetEditorCurve(clip, binding0);
132                Check(AnimationUtility.GetKeyRightTangentMode(curve0, 0) == AnimationUtility.TangentMode.Constant &&
133                    AnimationUtility.GetKeyLeftTangentMode(curve0, 1) == AnimationUtility.TangentMode.Constant &&
134                    AnimationUtility.GetKeyRightTangentMode(curve0, 1) == AnimationUtility.TangentMode.Linear,
135                    "Later keyframes preserve the stepped entry and smooth exit of the reorder");
136                clip.SampleAnimation(root, 1);
137                driver0.slot0_float0 = .7f;
138                FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
139                curve0 = AnimationUtility.GetEditorCurve(clip, binding0);
140                Check(curve0.length == 4 && curve0.keys[1].value == .4f &&
141                    AnimationUtility.GetKeyLeftTangentMode(curve0, 1) == AnimationUtility.TangentMode.Constant,
142                    "Replacing the destination pose does not move or overwrite its reorder anchor");
143                Check(untouched.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
144                    "Subsequent edits still leave unrelated operator curves untouched");
145            }
146            finally
147            {
148                UnityEngine.Object.DestroyImmediate(clip);
149                UnityEngine.Object.DestroyImmediate(shortClip);
150                UnityEngine.Object.DestroyImmediate(customClip);
151            }
152        }
153
154        static void ValidateAddedOperator(GameObject root, FoldStack stack)
155        {
156            var clip = new AnimationClip { frameRate = 60 };
157            var backwards = new AnimationClip { frameRate = 60 };
158            var clamped = new AnimationClip { frameRate = 60 };
159            var insertion = new AnimationClip { frameRate = 60 };
160            try
161            {
162                foreach (var driver in stack.materials)
163                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
164                var driver0 = stack.materials[0];
165                driver0.Write(0, FoldOperator.Defaults(11));
166                FoldKeyframes.Record(clip, root.transform, stack, 0);
167                string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform);
168                var untouched = AnimationUtility.GetCurveBindings(clip).Where(binding => binding.path != path || !binding.propertyName.StartsWith("slot1_"))
169                    .ToDictionary(binding => binding, binding => CurveSignature(AnimationUtility.GetEditorCurve(clip, binding)));
170                var added = FoldOperator.Defaults(12);
171                added.vector0 = new Vector4(0, 4, 0, 0);
172                driver0.Write(1, added);
173                Undo.IncrementCurrentGroup();
174                Check(FoldKeyframes.Record(clip, root.transform, stack, 1, 60) == 1, "Adding an operator keys only its own slot");
175                var defaults = FoldKeyframes.Fields(FoldOperator.AnimationDefaults(12));
176                foreach (var field in defaults)
177                {
178                    var binding = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot1_" + field.name);
179                    if (field.name == "opcode") binding = EditorCurveBinding.DiscreteCurve(path, typeof(FoldMaterialAnimator), "slot1_opcode");
180                    Check(AnimationUtility.GetEditorCurve(clip, binding).length == 2, "New operator has a default and user key for " + field.name);
181                }
182                clip.SampleAnimation(root, 0);
183                Check(driver0.slot1_enabled && driver0.slot1_opcode == 12 && driver0.slot1_float0 == 0 && driver0.slot1_vector0 == Vector4.zero,
184                    "The earlier key keeps the opcode active but starts its interpolator at zero");
185                clip.SampleAnimation(root, .5f); stack.enabled = true; stack.Apply();
186                var block = new MaterialPropertyBlock(); stack.GetComponent<Renderer>().GetPropertyBlock(block, 0);
187                Check(driver0.slot1_enabled && driver0.slot1_opcode == 12 && Mathf.Abs(driver0.slot1_float0 - .5f) < .001f &&
188                    Mathf.Abs(block.GetFloat(FoldOperator.Prefix(1) + "Float_0") - .5f) < .001f && Mathf.Abs(driver0.slot1_vector0.y - 2) < .001f &&
189                    Mathf.Abs(block.GetVector(FoldOperator.Prefix(1) + "Vector_0").y - 2) < .001f,
190                    "The new operator interpolates from defaults to user settings in native playback and the material");
191                Check(untouched.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
192                    "New-operator defaults leave other slots and material curves untouched");
193                Undo.PerformUndo();
194                clip.SampleAnimation(root, 0);
195                Check(driver0.slot1_opcode == 0 && !driver0.slot1_enabled, "One Undo restores the previously empty slot");
196                Undo.PerformRedo();
197                clip.SampleAnimation(root, 1);
198                driver0.slot1_vector0 = Vector3.up * 6;
199                FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
200                clip.SampleAnimation(root, .5f);
201                Check(Mathf.Abs(driver0.slot1_vector0.y - 3) < .001f, "Re-keying the user pose preserves interpolation from the default pose");
202                driver0.Write(1, added);
203                FoldKeyframes.Record(backwards, root.transform, stack, 1, -60);
204                backwards.SampleAnimation(root, 1.5f);
205                Check(Mathf.Abs(driver0.slot1_vector0.y - 2) < .001f, "Negative Advance places the default key forward by the signed offset and interpolates");
206                driver0.Write(1, added);
207                FoldKeyframes.Record(clamped, root.transform, stack, .5f, 60);
208                clamped.SampleAnimation(root, .25f);
209                Check(Mathf.Abs(driver0.slot1_vector0.y - 2) < .001f, "Default keyframes clamp to frame zero and still interpolate");
210                foreach (var driver in stack.materials)
211                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
212                driver0.Write(0, FoldOperator.Defaults(11));
213                FoldKeyframes.Record(insertion, root.transform, stack, 0);
214                driver0.Write(1, driver0.slots[0]);
215                driver0.Write(0, added);
216                FoldKeyframes.Record(insertion, root.transform, stack, 1, 60);
217                insertion.SampleAnimation(root, .5f / 60);
218                Check(driver0.slot0_opcode == 11, "Insertion preserves the old slot occupant until the reorder handoff");
219                insertion.SampleAnimation(root, 1f / 60);
220                Check(driver0.slot0_opcode == 12 && driver0.slot0_enabled && driver0.slot0_vector0 == Vector4.zero && driver0.slot1_opcode == 11,
221                    "An inserted operator starts with active defaults at the reorder handoff");
222                insertion.SampleAnimation(root, (1 + 1f / 60) * .5f);
223                Check(Mathf.Abs(driver0.slot0_vector0.y - 2) < .001f, "Inserted operators interpolate from defaults after the stepped reorder");
224            }
225            finally
226            {
227                UnityEngine.Object.DestroyImmediate(clip);
228                UnityEngine.Object.DestroyImmediate(backwards);
229                UnityEngine.Object.DestroyImmediate(clamped);
230                UnityEngine.Object.DestroyImmediate(insertion);
231            }
232        }
233
234        static void ValidateOpcodeDefaults(GameObject root, FoldStack stack)
235        {
236            var clip = new AnimationClip { frameRate = 60 };
237            var changedClip = new AnimationClip { frameRate = 60 };
238            var historyClip = new AnimationClip { frameRate = 60 };
239            try
240            {
241                foreach (var driver in stack.materials)
242                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
243                var driver0 = stack.materials[0];
244                FoldKeyframes.Record(clip, root.transform, stack, 0);
245                FoldKeyframes.Record(changedClip, root.transform, stack, 0);
246                for (int opcode = 1; opcode <= 15; opcode++) driver0.Write(opcode - 1, FoldOperator.AnimationDefaults(opcode));
247                driver0.slot10_float0 = 1; // Scale (1,1,1) remains neutral even at t=1.
248                driver0.slot11_float0 = 1; // Translate (0,0,0) likewise.
249                Check(FoldKeyframes.Record(clip, root.transform, stack, 2, 60) == 15, "Opcode defaults key new operators at the cursor");
250                Check(AnimationUtility.GetCurveBindings(clip).All(binding =>
251                    AnimationUtility.GetEditorCurve(clip, binding).keys.All(key => key.time == 0 || key.time == 2)),
252                    "Opcode defaults, identity scale and zero translation do not insert earlier keys");
253                clip.SampleAnimation(root, 0);
254                Check(Enumerable.Range(0, 15).All(slot => driver0.slots[slot].opcode == 0),
255                    "Neutral additions preserve the earlier empty stack");
256                clip.SampleAnimation(root, 2);
257                Check(driver0.slot10_vector0 == (Vector4)Vector3.one && driver0.slot10_float0 == 1,
258                    "Scale uses one rather than zero as its neutral input");
259                driver0.slot10_vector0 = Vector4.zero;
260                FoldKeyframes.Record(changedClip, root.transform, stack, 2, 60);
261                string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform);
262                var earlier = AnimationUtility.GetCurveBindings(changedClip).Where(binding =>
263                    AnimationUtility.GetEditorCurve(changedClip, binding).keys.Any(key => key.time == 1)).ToArray();
264                Check(earlier.Length == 22 && earlier.All(binding => binding.path == path && binding.propertyName.StartsWith("slot10_")),
265                    "Zero scale is non-default and only that operator gets an earlier key");
266                changedClip.SampleAnimation(root, 1);
267                Check(driver0.slot10_opcode == 11 && driver0.slot10_vector0 == (Vector4)Vector3.one && driver0.slot10_float0 == 0,
268                    "The earlier Scale key uses identity scale and t=0");
269                changedClip.SampleAnimation(root, 1.5f);
270                Check(Mathf.Abs(driver0.slot10_vector0.x - .5f) < .001f && Mathf.Abs(driver0.slot10_float0 - .5f) < .001f,
271                    "Non-default Scale interpolates from its neutral defaults");
272                foreach (var driver in stack.materials)
273                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
274                var op = FoldOperator.Defaults(12); op.vector0 = Vector3.up * 3; op.opcode = 0;
275                driver0.Write(0, op);
276                FoldKeyframes.Record(historyClip, root.transform, stack, 0);
277                op.opcode = 12; driver0.Write(0, op);
278                FoldKeyframes.Record(historyClip, root.transform, stack, 2, 60);
279                Check(AnimationUtility.GetCurveBindings(historyClip).Count(binding =>
280                    AnimationUtility.GetEditorCurve(historyClip, binding).keys.Any(key => key.time == 1)) == 22,
281                    "Non-default inputs backfill even when unchanged from the slot's previous inputs");
282            }
283            finally
284            {
285                UnityEngine.Object.DestroyImmediate(clip);
286                UnityEngine.Object.DestroyImmediate(changedClip);
287                UnityEngine.Object.DestroyImmediate(historyClip);
288            }
289        }
290
291        static void ValidateInterpolationAfterReorder(GameObject root, FoldStack stack)
292        {
293            var clip = new AnimationClip { frameRate = 60 };
294            try
295            {
296                foreach (var driver in stack.materials)
297                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
298                var driver0 = stack.materials[0];
299                var translate = FoldOperator.AnimationDefaults(12);
300                translate.float0 = .2f;
301                var scale = FoldOperator.AnimationDefaults(11);
302                scale.float0 = .4f;
303                driver0.Write(0, translate);
304                driver0.Write(1, scale);
305                FoldKeyframes.Record(clip, root.transform, stack, 0);
306                driver0.Write(0, scale);
307                driver0.Write(1, translate);
308                driver0.slot0_float0 = .8f;
309                FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
310                // Clearing the transition and target leaves a Constant outgoing tangent at t0.
311                FoldKeyframes.Clear(clip, 1f / 60);
312                FoldKeyframes.Clear(clip, 1);
313                clip.SampleAnimation(root, 0);
314                driver0.slot0_float0 = .8f;
315                FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
316                clip.SampleAnimation(root, .5f);
317                Check(Mathf.Abs(driver0.slot0_float0 - .5f) < .001f,
318                    "A normal key after clearing a reorder restores interpolation instead of retaining its orphaned step");
319                string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform);
320                var binding = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float0");
321                // Mimic a destination key retaining Constant tangents from an earlier edit.
322                var curve = AnimationUtility.GetEditorCurve(clip, binding);
323                AnimationUtility.SetKeyBroken(curve, 1, true);
324                AnimationUtility.SetKeyLeftTangentMode(curve, 1, AnimationUtility.TangentMode.Constant);
325                AnimationUtility.SetEditorCurve(clip, binding, curve);
326                driver0.Write(0, scale);
327                driver0.Write(1, translate);
328                driver0.slot0_float0 = .8f;
329                FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
330                clip.SampleAnimation(root, (1 + 1f / 60) * .5f);
331                Check(Mathf.Abs(driver0.slot0_float0 - .6f) < .001f,
332                    "A reorder clears a stale Constant incoming tangent at its destination and resumes interpolation");
333                curve = AnimationUtility.GetEditorCurve(clip, binding);
334                Check(AnimationUtility.GetKeyRightTangentMode(curve, 0) == AnimationUtility.TangentMode.Constant &&
335                    AnimationUtility.GetKeyLeftTangentMode(curve, 1) == AnimationUtility.TangentMode.Constant &&
336                    AnimationUtility.GetKeyRightTangentMode(curve, 1) == AnimationUtility.TangentMode.Linear &&
337                    AnimationUtility.GetKeyLeftTangentMode(curve, 2) == AnimationUtility.TangentMode.Linear,
338                    "Only the entry to the reorder is stepped; both sides of the following interpolation are linear");
339                AnimationUtility.SetKeyLeftTangentMode(curve, 2, AnimationUtility.TangentMode.Constant);
340                AnimationUtility.SetEditorCurve(clip, binding, curve);
341                clip.SampleAnimation(root, 1);
342                driver0.slot0_float0 = 1;
343                FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
344                clip.SampleAnimation(root, (1 + 1f / 60) * .5f);
345                Check(Mathf.Abs(driver0.slot0_float0 - .7f) < .001f,
346                    "Re-keying an existing reordered pose also repairs its interpolation without changing its stepped entry");
347            }
348            finally { UnityEngine.Object.DestroyImmediate(clip); }
349        }
350
351        static void ValidateStackReplacement(GameObject root, FoldStack stack)
352        {
353            var clip = new AnimationClip { frameRate = 60 };
354            try
355            {
356                foreach (var driver in stack.materials)
357                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
358                var driver0 = stack.materials[0];
359                var translate = FoldOperator.Defaults(12); translate.vector0 = Vector3.up * 4;
360                var scale = FoldOperator.Defaults(11); scale.vector0 = Vector3.one * 2;
361                driver0.Write(0, translate); driver0.Write(1, scale); driver0.Write(2, translate);
362                FoldKeyframes.Record(clip, root.transform, stack, 0);
363                driver0.slot0_float0 = driver0.slot1_float0 = driver0.slot2_float0 = 0;
364                FoldKeyframes.Record(clip, root.transform, stack, 1);
365                var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b,
366                    b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b)));
367                var rotate = FoldOperator.Defaults(15); rotate.float0 = Mathf.PI;
368                driver0.Write(0, rotate);
369                driver0.Write(1, FoldOperator.Defaults(5));
370                driver0.Write(2, new FoldOperator());
371                Undo.IncrementCurrentGroup();
372                Check(FoldKeyframes.Record(clip, root.transform, stack, 2, 60) == 3,
373                    "Replacing a stack keys only replaced and removed slots");
374                clip.SampleAnimation(root, 1 + .5f / 60);
375                Check(driver0.slot0_opcode == 12 && driver0.slot0_float0 == 0 && driver0.slot0_vector0.y == 4 &&
376                    driver0.slot1_opcode == 11 && driver0.slot1_float0 == 0 && driver0.slot2_enabled,
377                    "Replacement holds every old resting parameter until the handoff");
378                float handoff = 1 + 1f / 60;
379                clip.SampleAnimation(root, handoff);
380                Check(driver0.slot0_opcode == 15 && driver0.slot0_float1 == 0 &&
381                    driver0.slot1_opcode == 5 && driver0.slot1_float2 == 0 && !driver0.slot2_enabled && driver0.slot2_opcode == 0,
382                    "Replacement steps to new active operators at t=0 and removes old operators at Advance minus one");
383                clip.SampleAnimation(root, (handoff + 2) / 2);
384                Check(Mathf.Abs(driver0.slot0_float1 - .5f) < .001f && Mathf.Abs(driver0.slot1_float2 - .5f) < .001f,
385                    "Replacement resumes interpolation after the stepped handoff");
386                clip.SampleAnimation(root, .5f);
387                Check(Mathf.Abs(driver0.slot0_float0 - .5f) < .001f && Mathf.Abs(driver0.slot1_float0 - .5f) < .001f,
388                    "Replacement preserves the earlier animation into rest");
389                Undo.PerformUndo();
390                Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
391                    "One Undo restores all curves before stack replacement");
392                Undo.PerformRedo();
393                clip.SampleAnimation(root, 2);
394                Check(driver0.slot0_opcode == 15 && driver0.slot0_float1 == 1 && driver0.slot1_float2 == 1,
395                    "Redo restores the replacement destination pose");
396            }
397            finally { UnityEngine.Object.DestroyImmediate(clip); }
398        }
399
400        static void ValidateCursorOnlyKeyframing(GameObject root, FoldStack stack)
401        {
402            var clip = new AnimationClip { frameRate = 60 };
403            try
404            {
405                foreach (var driver in stack.materials)
406                    for (int slot = 0; slot < 16; slot++) driver.Write(slot, new FoldOperator());
407                var driver0 = stack.materials[0];
408                driver0.Write(0, FoldOperator.AnimationDefaults(12));
409                FoldKeyframes.Record(clip, root.transform, stack, 0);
410                driver0.Write(0, FoldOperator.Defaults(15));
411                driver0.Write(1, FoldOperator.Defaults(5));
412                stack.materials[1].Write(0, FoldOperator.Defaults(5));
413                Undo.IncrementCurrentGroup();
414                Check(FoldKeyframes.Record(clip, root.transform, stack, 2, 60,
415                    (driver, slot) => driver == driver0 && slot == 0) == 3,
416                    "Cursor-only preference records changed operators across materials");
417                string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform);
418                var bindings = AnimationUtility.GetCurveBindings(clip);
419                Check(bindings.Where(b => b.path == path && b.propertyName.StartsWith("slot0_")).All(b =>
420                    AnimationUtility.GetEditorCurve(clip, b).keys.Select(k => k.time).SequenceEqual(new[] { 0f, 2f })),
421                    "Disabling jump-back adds only cursor keys for a replacement, with no earlier hold or handoff");
422                Check(bindings.Where(b => (b.path == path && b.propertyName.StartsWith("slot1_")) ||
423                    (b.path != path && b.propertyName.StartsWith("slot0_"))).All(b =>
424                    AnimationUtility.GetEditorCurve(clip, b).keys.Any(k => k.time == 1)),
425                    "Other operators and materials retain automatic jump-back defaults");
426                Undo.PerformUndo();
427                Check(AnimationUtility.GetCurveBindings(clip).All(b => AnimationUtility.GetEditorCurve(clip, b).length == 1),
428                    "Cursor-only and automatic keys share one Undo step");
429            }
430            finally { UnityEngine.Object.DestroyImmediate(clip); }
431        }
432
433        static void ValidateOverwriteJumpback(GameObject root, FoldStack stack)
434        {
435            foreach (bool replacement in new[] { false, true })
436            {
437                var clip = new AnimationClip { frameRate = 60 };
438                try
439                {
440                    foreach (var driver in stack.materials)
441                        for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
442                    var driver0 = stack.materials[0];
443                    if (replacement) driver0.Write(0, FoldOperator.Defaults(11));
444                    FoldKeyframes.Record(clip, root.transform, stack, 0);
445                    driver0.Write(0, FoldOperator.Defaults(3));
446                    FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
447                    float anchor = replacement ? 1f / 60 : 0;
448                    string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform);
449                    var binding = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot0_float0");
450                    var originalTimes = AnimationUtility.GetEditorCurve(clip, binding).keys.Select(k => k.time).ToArray();
451                    // Change the operator type, then edit its copied settings twice at the same endpoint.
452                    var edited = FoldOperator.Defaults(2);
453                    for (int edit = 0; edit < 5; edit++)
454                    {
455                        edited.vector0 = new Vector3(0, 0, -2 - edit);
456                        edited.float0 = edit == 3 ? 0 : 1;
457                        driver0.Write(0, edited);
458                        var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b,
459                            b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b)));
460                        Undo.IncrementCurrentGroup();
461                        FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
462                        Check(AnimationUtility.GetEditorCurve(clip, binding).keys.Select(k => k.time).SequenceEqual(originalTimes),
463                            "Re-keying reuses the jumpback time without frame creep (replacement=" + replacement + ", edit=" + edit + ")");
464                        clip.SampleAnimation(root, anchor);
465                        Check(driver0.slot0_opcode == 2 && driver0.slot0_float0 == 0 && driver0.slot0_vector0 == edited.vector0,
466                            "Re-keying overwrites the jumpback with the edited opcode and Copy fields");
467                        clip.SampleAnimation(root, (anchor + 1) / 2);
468                        Check(Mathf.Abs(driver0.slot0_float0 - .5f * edited.float0) < .001f && driver0.slot0_vector0 == edited.vector0,
469                            "Edited jumpback still interpolates toward the endpoint");
470                        Undo.PerformUndo();
471                        Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
472                            "Undo restores the prior jumpback values and all curve times");
473                        Undo.PerformRedo();
474                        clip.SampleAnimation(root, 1);
475                    }
476                    // A genuinely later pose must retain its own history.
477                    edited.float0 = .25f;
478                    driver0.Write(0, edited);
479                    FoldKeyframes.Record(clip, root.transform, stack, 2, 60);
480                    clip.SampleAnimation(root, 1);
481                    Check(driver0.slot0_float0 == 1, "A later authored pose preserves the preceding endpoint");
482                }
483                finally { UnityEngine.Object.DestroyImmediate(clip); }
484            }
485        }
486
487        static void ValidateEaseLinearSpans(GameObject root, FoldStack stack)
488        {
489            var clip = new AnimationClip { frameRate = 60 };
490            try
491            {
492                foreach (var driver in stack.materials)
493                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
494                var driver0 = stack.materials[0];
495                driver0.Write(0, FoldOperator.Defaults(12));
496                FoldKeyframes.Record(clip, root.transform, stack, 0);
497                driver0.slot0_vector0 = Vector3.right * 4;
498                FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
499                driver0.Write(0, FoldOperator.Defaults(15));
500                FoldKeyframes.Record(clip, root.transform, stack, 2, 60);
501                string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform);
502                var custom = EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), "slot1_float0");
503                AnimationUtility.SetEditorCurve(clip, custom, AnimationCurve.EaseInOut(0, 0, 2, 1));
504                var unrelated = EditorCurveBinding.FloatCurve("", typeof(Transform), "m_LocalPosition.x");
505                AnimationUtility.SetEditorCurve(clip, unrelated, AnimationCurve.Linear(0, 0, 2, 1));
506                var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b,
507                    b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b)));
508                var keys = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b,
509                    b => AnimationUtility.GetEditorCurve(clip, b).keys.Select(k => (k.time, k.value)).ToArray());
510                Undo.IncrementCurrentGroup();
511                Check(FoldKeyframes.EaseLinearSpans(clip) > 0, "Ease converts existing linear Fold spans");
512                Check(keys.All(pair => pair.Value.SequenceEqual(AnimationUtility.GetEditorCurve(clip, pair.Key).keys.Select(k => (k.time, k.value)))),
513                    "Ease preserves every key time and value");
514                Check(before.Where(pair => pair.Key == custom || pair.Key == unrelated ||
515                    pair.Key.propertyName.EndsWith("opcode") || pair.Key.propertyName.EndsWith("enabled"))
516                    .All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
517                    "Ease preserves custom curves, other tracks and discrete fields");
518                clip.SampleAnimation(root, .25f);
519                Check(Mathf.Abs(driver0.slot0_vector0.x - .625f) < .0001f,
520                    "Former linear translation uses zero-slope ease in/out");
521                clip.SampleAnimation(root, 1 + .5f / 60);
522                Check(driver0.slot0_opcode == 12 && driver0.slot0_vector0.x == 4,
523                    "Ease preserves the stepped hold before a replacement");
524                float handoff = 1 + 1f / 60;
525                clip.SampleAnimation(root, handoff);
526                Check(driver0.slot0_opcode == 15 && driver0.slot0_float1 == 0,
527                    "Ease preserves the neutral replacement handoff");
528                clip.SampleAnimation(root, Mathf.Lerp(handoff, 2, .25f));
529                Check(Mathf.Abs(driver0.slot0_float1 - .15625f) < .0001f,
530                    "A stepped handoff can ease out without smoothing its incoming step");
531                Check(FoldKeyframes.EaseLinearSpans(clip) == 0, "Repeated easing is a no-op");
532                Undo.PerformUndo();
533                Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
534                    "One Undo restores all original curves after easing");
535                Undo.PerformRedo();
536                clip.SampleAnimation(root, .25f);
537                Check(Mathf.Abs(driver0.slot0_vector0.x - .625f) < .0001f, "Redo restores easing");
538            }
539            finally { UnityEngine.Object.DestroyImmediate(clip); }
540        }
541
542        static void ValidateInsertionMatchingExistingRotate(GameObject root, FoldStack stack)
543        {
544            var clip = new AnimationClip { frameRate = 60 };
545            try
546            {
547                foreach (var driver in stack.materials)
548                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
549                var driver0 = stack.materials[0];
550                var translate = FoldOperator.Defaults(12);
551                translate.vector0 = Vector3.right;
552                var rotate = FoldOperator.Defaults(15);
553                driver0.Write(0, translate);
554                driver0.Write(1, rotate);
555                FoldKeyframes.Record(clip, root.transform, stack, 0);
556                translate.vector0 = Vector3.right * 2;
557                driver0.Write(0, translate);
558                FoldKeyframes.Record(clip, root.transform, stack, 10, 60);
559                clip.SampleAnimation(root, 11);
560                var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b,
561                    b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b)));
562                driver0.Write(2, driver0.slots[1]);
563                driver0.Write(1, driver0.slots[0]);
564                driver0.Write(0, rotate.Copy());
565                Undo.IncrementCurrentGroup();
566                FoldKeyframes.Record(clip, root.transform, stack, 11, 60);
567                clip.SampleAnimation(root, 10 + 1f / 60);
568                Check(driver0.slot0_opcode == 15 && driver0.slot0_float1 == 0,
569                    "An inserted Rotate matching a later Rotate starts neutral");
570                Check(driver0.slot1_opcode == 12 && driver0.slot1_vector0 == translate.vector0 &&
571                    driver0.slot1_float0 == 1 && driver0.slot2_opcode == 15 && driver0.slot2_float1 == 1,
572                    "Insertion preserves the translation then rotation sequence at the jumpback");
573                clip.SampleAnimation(root, 10.5f);
574                Check(driver0.slot2_float1 == 1, "The existing Rotate stays fully applied throughout the new span");
575                clip.SampleAnimation(root, 10);
576                Check(driver0.slot0_opcode == 12 && driver0.slot1_opcode == 15 && driver0.slot1_float1 == 1,
577                    "Late Rotate insertion preserves the preceding pose");
578                Undo.PerformUndo();
579                Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
580                    "Undo restores all curves before matching-Rotate insertion");
581                Undo.PerformRedo();
582                clip.SampleAnimation(root, 10 + 1f / 60);
583                Check(driver0.slot0_float1 == 0 && driver0.slot2_float1 == 1,
584                    "Redo keeps only the new Rotate neutral");
585            }
586            finally { UnityEngine.Object.DestroyImmediate(clip); }
587        }
588
589        static void ValidateInsertionWithRepeatedOperators(GameObject root, FoldStack stack)
590        {
591            // Separated duplicates, adjacent duplicates, and identical duplicates.
592            for (int scenario = 0; scenario < 3; scenario++)
593            {
594                var clip = new AnimationClip { frameRate = 60 };
595                try
596                {
597                    foreach (var driver in stack.materials)
598                        for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
599                    var driver0 = stack.materials[0];
600                    var firstTube = FoldOperator.Defaults(2);
601                    firstTube.vector0 = new Vector3(0, 0, -1);
602                    firstTube.vector1 = Vector3.forward;
603                    firstTube.vector2 = Vector3.up;
604                    var secondTube = scenario == 2 ? firstTube.Copy() : FoldOperator.Defaults(2);
605                    var original = scenario == 0
606                        ? new[] { FoldOperator.Defaults(15), firstTube, FoldOperator.Defaults(3), secondTube, FoldOperator.Defaults(12) }
607                        : new[] { FoldOperator.Defaults(15), firstTube, secondTube, FoldOperator.Defaults(12) };
608                    for (int i = 0; i < original.Length; i++) driver0.Write(i, original[i]);
609                    FoldKeyframes.Record(clip, root.transform, stack, 0);
610                    string path = AnimationUtility.CalculateTransformPath(driver0.transform, root.transform);
611                    var before = AnimationUtility.GetCurveBindings(clip).ToDictionary(b => b,
612                        b => CurveSignature(AnimationUtility.GetEditorCurve(clip, b)));
613                    var untouched = before.Where(pair => pair.Key.path != path || pair.Key.propertyName.StartsWith("slot0_"))
614                        .ToDictionary(pair => pair.Key, pair => pair.Value);
615                    for (int i = original.Length; i > 1; i--) driver0.Write(i, original[i - 1]);
616                    var fbm = FoldOperator.Defaults(8);
617                    driver0.Write(1, fbm);
618                    Undo.IncrementCurrentGroup();
619                    FoldKeyframes.Record(clip, root.transform, stack, 1, 60);
620                    foreach (float sampleTime in new[] { 1f / 60, .5f, 1f })
621                    {
622                        clip.SampleAnimation(root, sampleTime);
623                        for (int i = 1; i < original.Length; i++)
624                        {
625                            var expected = FoldKeyframes.Fields(original[i]);
626                            var actual = FoldKeyframes.Fields(driver0.slots[i + 1]);
627                            Check(expected.Zip(actual, (a, b) => a.discrete ? a.value.Equals(b.value) : Mathf.Abs(a.value - b.value) < .0001f).All(equal => equal),
628                                "Inserting FBM preserves moved operator " + i + " in duplicate scenario " + scenario + " at " + sampleTime);
629                        }
630                    }
631                    clip.SampleAnimation(root, 1f / 60);
632                    Check(driver0.slot1_opcode == 8 && (Vector3)driver0.slot1_vector0 == Vector3.zero,
633                        "Only the inserted FBM starts from its jumpback amplitude");
634                    clip.SampleAnimation(root, .5f / 60);
635                    Check(driver0.slot1_opcode == 2 && driver0.slot1_float0 == 1 && driver0.slot1_vector0 == firstTube.vector0,
636                        "The original tube remains intact before the insertion handoff");
637                    Check(untouched.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
638                        "Insertion leaves the prefix and other material curves unchanged");
639                    Undo.PerformUndo();
640                    Check(before.All(pair => pair.Value == CurveSignature(AnimationUtility.GetEditorCurve(clip, pair.Key))),
641                        "Undo restores the complete stack before duplicate-operator insertion");
642                    Undo.PerformRedo();
643                    clip.SampleAnimation(root, .5f);
644                    Check(driver0.slot2_float0 == 1 && driver0.slots[original.Length - 1].float0 == 1,
645                        "Redo preserves both completed tube folds");
646                }
647                finally { UnityEngine.Object.DestroyImmediate(clip); }
648            }
649        }
650
651        static void ValidateJumpbackPolicies(GameObject root, FoldStack stack)
652        {
653            var clip = new AnimationClip { frameRate = 60 };
654            try
655            {
656                foreach (var driver in stack.materials)
657                    for (int i = 0; i < 16; i++) driver.Write(i, new FoldOperator());
658                var driver0 = stack.materials[0];
659                FoldKeyframes.Record(clip, root.transform, stack, 0);
660                var align = FoldOperator.Defaults(3);
661                align.vector0 = new Vector4(0, 0, -3, 7);
662                align.vector1 = new Vector4(0, 0, 2, 9);
663                align.vector2 = Vector3.up;
664                align.float3 = 17; // Unused fields must also be preserved by Copy.
665                var fbm = FoldOperator.Defaults(8);
666                fbm.vector0 = new Vector4(.3f, .2f, .1f, 8);
667                fbm.vector1 = new Vector3(1, 2, 3);
668                fbm.vector2 = new Vector3(6, 2, 4);
669                fbm.float0 = .7f; fbm.float1 = 3; fbm.float2 = 6;
670                driver0.Write(0, align); driver0.Write(1, fbm);
671                FoldKeyframes.Record(clip, root.transform, stack, 2, 60);
672                clip.SampleAnimation(root, 1);
673                Check(driver0.slot0_float0 == 0 && driver0.slot0_vector0 == align.vector0 &&
674                    driver0.slot0_vector1 == align.vector1 && driver0.slot0_vector2 == align.vector2 && driver0.slot0_float3 == 17,
675                    "Jumpback uses Zero for alignment T and Copy for authored origin, target, axis and unused fields");
676                Check((Vector3)driver0.slot1_vector0 == Vector3.zero && driver0.slot1_vector0.w == 8 &&
677                    driver0.slot1_vector1 == fbm.vector1 && driver0.slot1_vector2 == fbm.vector2 &&
678                    driver0.slot1_float0 == .7f && driver0.slot1_float1 == 3 && driver0.slot1_float2 == 6,
679                    "FBM jumpback zeroes amplitude xyz while copying velocity, period, gain, lacunarity, octaves and w");
680                clip.SampleAnimation(root, 1.5f);
681                Check(Mathf.Abs(driver0.slot1_vector0.x - .15f) < .001f && driver0.slot1_vector2 == fbm.vector2,
682                    "FBM amplitude interpolates while its authored period remains fixed");
683                Check(align.float0 == 1 && fbm.vector0.x == .3f, "Jumpback generation does not mutate authored operators");
684                var scale = FoldOperator.Defaults(11); scale.vector0 = new Vector4(2, 3, 4, 5);
685                Check(scale.JumpbackDefaults().vector0 == new Vector4(1, 1, 1, 5), "Scale uses its One annotation for xyz and preserves w");
686                var rotate = FoldOperator.Defaults(15); rotate.float0 = 2; rotate.vector1 = Vector3.forward;
687                Check(rotate.JumpbackDefaults().float0 == 2 && rotate.JumpbackDefaults().float1 == 0 &&
688                    rotate.JumpbackDefaults().vector1 == rotate.vector1, "Rotate copies authored angle and axis and zeroes T");
689                var sine = FoldOperator.Defaults(7);
690                Check((Vector3)sine.JumpbackDefaults().vector0 == Vector3.zero &&
691                    sine.JumpbackDefaults().vector2 == sine.vector2, "Sine Waves zeroes amplitude and preserves wave number");
692                var seal = FoldOperator.Defaults(6); seal.float0 = .8f; seal.float1 = 3;
693                Check(seal.float3 == 1 && seal.JumpbackDefaults().float3 == 0 &&
694                    seal.JumpbackDefaults().float0 == .8f && seal.JumpbackDefaults().float1 == 3 &&
695                    seal.JumpbackDefaults().float2 == seal.float2,
696                    "Seal starts interpolation at zero while copying its authored shape and time controls");
697                align.enabled = false;
698                Check(!align.JumpbackDefaults().enabled, "Copy preserves the authored enabled state");
699            }
700            finally { UnityEngine.Object.DestroyImmediate(clip); }
701        }
702
703        [MenuItem("Tools/yum_food/FoldEditor Validation")]
704        public static void Run()
705        {
706            GameObject go = null;
707            Material a = null, b = null;
708            AnimationClip clip = null;
709            if (AnimationMode.InAnimationMode()) throw new InvalidOperationException("Stop Animation preview before running validation.");
710            Undo.IncrementCurrentGroup();
711            int undoGroup = Undo.GetCurrentGroup();
712            try
713            {
714                var shader = Shader.Find("yum_food/3ner");
715                a = new Material(shader); b = new Material(shader);
716                Check(FoldStack.Compatible(a), "Detect complete shader slot layout");
717                go = GameObject.CreatePrimitive(PrimitiveType.Cube);
718                go.AddComponent<Animator>();
719                var renderer = go.GetComponent<Renderer>(); renderer.sharedMaterials = new[] { a, b };
720                var original = new MaterialPropertyBlock(); original.SetFloat("_Unrelated", 42); renderer.SetPropertyBlock(original, 1);
721                var stack = go.AddComponent<FoldStack>(); FoldEditorWindow.Initialize(stack);
722                stack.materials[0].Write(2, new FoldOperator { enabled = true, opcode = 12, float0 = 0, vector0 = Vector3.up });
723                stack.materials[1].Write(0, new FoldOperator { enabled = true, opcode = 15, float0 = 1, float1 = 0, vector1 = Vector3.up });
724                FoldEditorWindow.Initialize(stack);
725                Check(stack.materials[0].slots[2].opcode == 12 && stack.materials[1].slots[0].opcode == 15, "Automatic setup preserves existing stacks");
726                clip = new AnimationClip { frameRate = 60 };
727                Check(FoldKeyframes.Record(clip, go.transform, stack, 0) == 32, "First pose keys all 16 slots on both materials, including disabled slots");
728                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");
729                Check(AnimationUtility.GetCurveBindings(clip).Length == 32 * 22, "All 22 fields per operator are keyed");
730                Check(FoldKeyframes.Record(clip, go.transform, stack, .5f) == 0, "Unchanged pose creates no redundant keys");
731                stack.materials[0].slot2_float0 = 1;
732                Check(FoldKeyframes.Record(clip, go.transform, stack, 1) == 1, "One changed interpolator keys exactly one complete operator");
733                Check(AnimationUtility.GetCurveBindings(clip).Count(x => AnimationUtility.GetEditorCurve(clip, x).length == 2) == 22, "Changed operator gets all 22 fields");
734                clip.SampleAnimation(go, .5f); stack.Apply();
735                Check(Mathf.Abs(stack.materials[0].slots[2].float0 - .5f) < .001f, "Built-in animation samples driver fields");
736                var block = new MaterialPropertyBlock(); renderer.GetPropertyBlock(block, 0);
737                Check(Mathf.Abs(block.GetFloat(FoldOperator.Prefix(2) + "Float_0") - .5f) < .001f, "Animation drives live material interpolator");
738                Check(block.GetInteger(FoldOperator.Prefix(2) + "Opcode") == 12, "Animation drives actual integer shader opcode");
739                renderer.GetPropertyBlock(block, 1);
740                Check(block.GetInteger(FoldOperator.Prefix(0) + "Opcode") == 15 && block.GetFloat("_Unrelated") == 42, "Material indices remain independent and preserve other properties");
741                stack.materials[0].slot2_opcode = 11;
742                FoldKeyframes.Record(clip, go.transform, stack, 2);
743                clip.SampleAnimation(go, 1 + .5f / 60);
744                Check(stack.materials[0].slots[2].opcode == 12, "Opcode curves hold until the replacement handoff");
745                clip.SampleAnimation(go, 1 + 1f / 60);
746                Check(stack.materials[0].slots[2].opcode == 11, "Advance zero steps opcode one frame after the previous pose");
747                clip.SampleAnimation(go, 2);
748                Check(stack.materials[0].slots[2].opcode == 11, "Opcode changes at its keyframe");
749                stack.enabled = false; renderer.GetPropertyBlock(block, 1);
750                Check(block.GetFloat("_Unrelated") == 42 && block.GetInteger(FoldOperator.Prefix(0) + "Opcode") == 0, "Disabling bridge restores original indexed property block");
751                Check(a.GetInteger(FoldOperator.Prefix(2) + "Opcode") == 0, "Animation does not mutate shared material assets");
752                var transformBinding = EditorCurveBinding.FloatCurve("", typeof(Transform), "m_LocalPosition.x");
753                AnimationUtility.SetEditorCurve(clip, transformBinding, new AnimationCurve(
754                    new Keyframe(1 - 1f / 60, 3), new Keyframe(1, 4), new Keyframe(1 + 1f / 60, 5)));
755                var objectBinding = EditorCurveBinding.PPtrCurve("", typeof(MeshRenderer), "m_Materials.Array.data[0]");
756                AnimationUtility.SetObjectReferenceCurve(clip, objectBinding, new[] { new ObjectReferenceKeyframe { time = 1, value = a } });
757                Undo.IncrementCurrentGroup();
758                Check(FoldKeyframes.Clear(clip, 1) == 24, "Clear deletes float, discrete and object-reference keys at the cursor across all tracks");
759                Check(AnimationUtility.GetEditorCurve(clip, transformBinding).length == 2, "Clear preserves keys on adjacent frames");
760                Check(AnimationUtility.GetObjectReferenceCurveBindings(clip).Length == 0, "Clear removes empty tracks");
761                Check(FoldKeyframes.Clear(clip, 1) == 0, "Clearing an empty frame is a no-op");
762                Undo.PerformUndo();
763                Check(AnimationUtility.GetEditorCurve(clip, transformBinding).length == 3 && AnimationUtility.GetObjectReferenceCurveBindings(clip).Length == 1, "Undo restores deleted keys and tracks");
764                ValidateStaggeredKeys(go, stack);
765                ValidateReorder(go, stack);
766                ValidateAddedOperator(go, stack);
767                ValidateOpcodeDefaults(go, stack);
768                ValidateJumpbackPolicies(go, stack);
769                ValidateInsertionWithRepeatedOperators(go, stack);
770                ValidateInsertionMatchingExistingRotate(go, stack);
771                ValidateEaseLinearSpans(go, stack);
772                ValidateOverwriteJumpback(go, stack);
773                ValidateInterpolationAfterReorder(go, stack);
774                ValidateStackReplacement(go, stack);
775                ValidateCursorOnlyKeyframing(go, stack);
776                Check(FoldOperator.AnimationDefaults(5).float2 == 0 && FoldOperator.AnimationDefaults(5).float0 == 2 &&
777                    FoldOperator.AnimationDefaults(5).float1 == 1, "Norm Conversion starts with t=0 and preserves valid norm exponents");
778                Check(FoldOperator.AnimationDefaults(15).float1 == 0 && FoldOperator.AnimationDefaults(15).float0 == Mathf.PI / 2 &&
779                    FoldOperator.AnimationDefaults(15).vector1 == (Vector4)Vector3.up, "Rotate starts with t=0 and preserves its angle and axis");
780                Debug.Log("FoldEditor validation passed.");
781            }
782            finally
783            {
784                Undo.RevertAllDownToGroup(undoGroup);
785                if (go) UnityEngine.Object.DestroyImmediate(go);
786                if (clip) UnityEngine.Object.DestroyImmediate(clip);
787                if (a) UnityEngine.Object.DestroyImmediate(a);
788                if (b) UnityEngine.Object.DestroyImmediate(b);
789            }
790        }
791    }
792}