yum/3ner

A toon shader for Unity's BIRP.

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

yumFold: add "ease linear spans" button to do eased lerping1fc2239

master
30.2 KiB553 linesraw
1using System.Collections.Generic;
2using UnityEditor;
3using UnityEngine;
4
5namespace Yum.Fold
6{
7    public static class FoldKeyframes
8    {
9        public struct Field
10        {
11            public string name;
12            public float value;
13            public bool discrete;
14            public Field(string name, float value, bool discrete = false)
15            { this.name = name; this.value = value; this.discrete = discrete; }
16        }
17
18        // Unity 2022.3 discrete integer curves store integer bits in the float key value.
19        // Numeric conversion would play opcode 12 as 1094713344.
20        [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit)]
21        struct DiscreteValue
22        {
23            [System.Runtime.InteropServices.FieldOffset(0)] public int integer;
24            [System.Runtime.InteropServices.FieldOffset(0)] public float floating;
25        }
26        static float CurveValue(Field field) => field.name == "opcode"
27            ? new DiscreteValue { integer = Mathf.RoundToInt(field.value) }.floating : field.value;
28
29        public static List<Field> Fields(FoldOperator op)
30        {
31            var fields = new List<Field> {
32                new Field("enabled", op.enabled ? 1 : 0, true), new Field("opcode", op.opcode, true),
33                new Field("float0", op.float0), new Field("float1", op.float1),
34                new Field("float2", op.float2), new Field("float3", op.float3)
35            };
36            var vectors = new[] { op.vector0, op.vector1, op.vector2, op.vector3 };
37            for (int i = 0; i < 4; i++)
38                for (int c = 0; c < 4; c++) fields.Add(new Field("vector" + i + "." + "xyzw"[c], vectors[i][c]));
39            return fields;
40        }
41
42        public static string Prefix(int material, int slot) => "slot" + slot + "_";
43        static EditorCurveBinding Binding(string path, string prefix, Field field) => field.name == "opcode"
44            ? EditorCurveBinding.DiscreteCurve(path, typeof(FoldMaterialAnimator), prefix + field.name)
45            : EditorCurveBinding.FloatCurve(path, typeof(FoldMaterialAnimator), prefix + field.name);
46
47        // Compare actual preceding keys, not interpolated values or transient window state.
48        // Also repair partial operator keys made by the Animation window's record mode.
49        public static bool Changed(AnimationClip clip, string path, string prefix, List<Field> fields, float time)
50        {
51            float latest = float.NegativeInfinity;
52            var times = new List<float>();
53            foreach (var field in fields)
54            {
55                var curve = AnimationUtility.GetEditorCurve(clip, Binding(path, prefix, field));
56                bool found = false;
57                float keyTime = float.NegativeInfinity;
58                float value = 0;
59                if (curve != null)
60                    foreach (var key in curve.keys)
61                        if (key.time <= time + .00001f) { found = true; keyTime = key.time; value = key.value; }
62                if (!found || !value.Equals(CurveValue(field))) return true;
63                times.Add(keyTime); latest = Mathf.Max(latest, keyTime);
64            }
65            foreach (float keyTime in times) if (Mathf.Abs(keyTime - latest) > .00001f) return true;
66            return false;
67        }
68
69        // Clear the current frame across every track in the clip, including object references.
70        public static int Clear(AnimationClip clip, float time)
71        {
72            int frame = Mathf.RoundToInt(Mathf.Max(0, time) * clip.frameRate);
73            var bindings = new List<EditorCurveBinding>();
74            var curves = new List<AnimationCurve>();
75            var objectBindings = new List<EditorCurveBinding>();
76            var objectCurves = new List<ObjectReferenceKeyframe[]>();
77            int count = 0;
78            foreach (var binding in AnimationUtility.GetCurveBindings(clip))
79            {
80                var curve = AnimationUtility.GetEditorCurve(clip, binding);
81                int before = curve.length;
82                for (int i = curve.length - 1; i >= 0; i--)
83                    if (Mathf.RoundToInt(curve[i].time * clip.frameRate) == frame) curve.RemoveKey(i);
84                if (curve.length == before) continue;
85                count += before - curve.length;
86                bindings.Add(binding);
87                curves.Add(curve.length == 0 ? null : curve);
88            }
89            foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip))
90            {
91                var keys = AnimationUtility.GetObjectReferenceCurve(clip, binding);
92                var remaining = new List<ObjectReferenceKeyframe>();
93                foreach (var key in keys)
94                    if (Mathf.RoundToInt(key.time * clip.frameRate) != frame) remaining.Add(key);
95                if (remaining.Count == keys.Length) continue;
96                count += keys.Length - remaining.Count;
97                objectBindings.Add(binding);
98                objectCurves.Add(remaining.Count == 0 ? null : remaining.ToArray());
99            }
100            if (count == 0) return 0;
101            Undo.RegisterCompleteObjectUndo(clip, "Clear keyframe");
102            AnimationUtility.SetEditorCurves(clip, bindings.ToArray(), curves.ToArray());
103            for (int i = 0; i < objectBindings.Count; i++)
104                AnimationUtility.SetObjectReferenceCurve(clip, objectBindings[i], objectCurves[i]);
105            EditorUtility.SetDirty(clip);
106            return count;
107        }
108
109        // Convert only complete linear numeric spans. Tangents belong to a side of
110        // a key: a handoff can keep its stepped incoming side and ease out normally.
111        public static int EaseLinearSpans(AnimationClip clip)
112        {
113            var numeric = new HashSet<string>();
114            foreach (var field in Fields(new FoldOperator()))
115                if (!field.discrete)
116                    for (int slot = 0; slot < 16; slot++) numeric.Add(Prefix(0, slot) + field.name);
117            var bindings = new List<EditorCurveBinding>();
118            var curves = new List<AnimationCurve>();
119            int count = 0;
120            foreach (var binding in AnimationUtility.GetCurveBindings(clip))
121            {
122                if (binding.type != typeof(FoldMaterialAnimator) || binding.isDiscreteCurve ||
123                    !numeric.Contains(binding.propertyName)) continue;
124                var curve = AnimationUtility.GetEditorCurve(clip, binding);
125                bool changed = false;
126                for (int i = 0; i + 1 < curve.length; i++)
127                {
128                    if (curve[i].value.Equals(curve[i + 1].value) ||
129                        AnimationUtility.GetKeyRightTangentMode(curve, i) != AnimationUtility.TangentMode.Linear ||
130                        AnimationUtility.GetKeyLeftTangentMode(curve, i + 1) != AnimationUtility.TangentMode.Linear ||
131                        float.IsInfinity(curve[i].outTangent) || float.IsInfinity(curve[i + 1].inTangent)) continue;
132                    FlattenSide(curve, i, false);
133                    FlattenSide(curve, i + 1, true);
134                    changed = true;
135                    count++;
136                }
137                if (changed) { bindings.Add(binding); curves.Add(curve); }
138            }
139            if (count > 0)
140            {
141                Undo.RegisterCompleteObjectUndo(clip, "Ease Fold animation");
142                AnimationUtility.SetEditorCurves(clip, bindings.ToArray(), curves.ToArray());
143                EditorUtility.SetDirty(clip);
144            }
145            return count;
146        }
147
148        static void FlattenSide(AnimationCurve curve, int index, bool incoming)
149        {
150            AnimationUtility.SetKeyBroken(curve, index, true);
151            if (incoming) AnimationUtility.SetKeyLeftTangentMode(curve, index, AnimationUtility.TangentMode.Free);
152            else AnimationUtility.SetKeyRightTangentMode(curve, index, AnimationUtility.TangentMode.Free);
153            var key = curve[index];
154            if (incoming) { key.inTangent = 0; key.weightedMode &= ~WeightedMode.In; }
155            else { key.outTangent = 0; key.weightedMode &= ~WeightedMode.Out; }
156            curve.MoveKey(index, key);
157        }
158
159        static float PreviousAppearanceTime(AnimationClip clip, float time)
160        {
161            float previous = float.NegativeInfinity;
162            foreach (var binding in AnimationUtility.GetCurveBindings(clip))
163            {
164                if (binding.type != typeof(FoldMaterialAnimator) || !binding.propertyName.StartsWith("slot")) continue;
165                foreach (var key in AnimationUtility.GetEditorCurve(clip, binding).keys)
166                    if (key.time < time - .00001f) previous = Mathf.Max(previous, key.time);
167            }
168            return previous;
169        }
170
171        static void Key(AnimationCurve curve, float time, float value, bool discrete)
172        {
173            var left = discrete ? AnimationUtility.TangentMode.Constant : AnimationUtility.TangentMode.Linear;
174            var right = left;
175            for (int i = curve.length - 1; i >= 0; i--)
176                if (Mathf.Abs(curve[i].time - time) < .00001f)
177                {
178                    // Re-keying a pose must not erase an existing stepped reorder boundary.
179                    if (AnimationUtility.GetKeyLeftTangentMode(curve, i) == AnimationUtility.TangentMode.Constant)
180                        left = AnimationUtility.TangentMode.Constant;
181                    if (AnimationUtility.GetKeyRightTangentMode(curve, i) == AnimationUtility.TangentMode.Constant)
182                        right = AnimationUtility.TangentMode.Constant;
183                    curve.RemoveKey(i);
184                }
185            int index = curve.AddKey(new Keyframe(time, value));
186            AnimationUtility.SetKeyBroken(curve, index, left != right);
187            AnimationUtility.SetKeyLeftTangentMode(curve, index, left);
188            AnimationUtility.SetKeyRightTangentMode(curve, index, right);
189            // Refresh neighboring cached slopes after insertion while preserving their tangent modes.
190            if (index > 0) AnimationUtility.SetKeyRightTangentMode(curve, index - 1, AnimationUtility.GetKeyRightTangentMode(curve, index - 1));
191            if (index + 1 < curve.length) AnimationUtility.SetKeyLeftTangentMode(curve, index + 1, AnimationUtility.GetKeyLeftTangentMode(curve, index + 1));
192        }
193
194        static void HoldPreviousState(AnimationCurve curve, float time, bool discrete)
195        {
196            float value = 0;
197            foreach (var key in curve.keys)
198            {
199                // Keep existing keys and their tangents. Never sample interpolation towards a future pose.
200                if (Mathf.Abs(key.time - time) < .00001f) return;
201                if (key.time > time) break;
202                value = key.value;
203            }
204            Key(curve, time, value, discrete);
205        }
206
207        static float LastValue(AnimationCurve curve, float time)
208        {
209            float value = 0;
210            if (curve != null)
211                foreach (var key in curve.keys)
212                {
213                    if (key.time > time + .00001f) break;
214                    value = key.value;
215                }
216            return value;
217        }
218
219        // Snapshot all sources before writing destination curves. A swap must read the
220        // original state on both sides, never the state just written into the first slot.
221        static float[][] PreviousStates(AnimationClip clip, string path, float time)
222        {
223            var fields = Fields(new FoldOperator());
224            var states = new float[16][];
225            for (int slot = 0; slot < states.Length; slot++)
226            {
227                states[slot] = new float[fields.Count];
228                for (int f = 0; f < fields.Count; f++)
229                    states[slot][f] = LastValue(AnimationUtility.GetEditorCurve(clip,
230                        Binding(path, Prefix(0, slot), fields[f])), time);
231            }
232            return states;
233        }
234
235        static bool SameState(float[] previous, List<Field> current)
236        {
237            for (int f = 0; f < current.Count; f++)
238                if (!previous[f].Equals(CurveValue(current[f]))) return false;
239            return true;
240        }
241
242        static int[] ReorderSources(FoldMaterialAnimator material, float[][] previous, out bool[] affected)
243        {
244            var sources = new int[16];
245            var used = new bool[16];
246            var oldOpcodes = new int[16];
247            var current = new List<Field>[16];
248            affected = new bool[16];
249            for (int slot = 0; slot < 16; slot++)
250            {
251                sources[slot] = -1;
252                oldOpcodes[slot] = new DiscreteValue { floating = previous[slot][1] }.integer;
253                current[slot] = Fields(material.slots[slot]);
254            }
255
256            // Preserve the longest unchanged sequence first. A newly inserted operator
257            // can equal an existing one farther down the stack; greedy matching would
258            // steal that source and reset the real survivor to its jumpback defaults.
259            var matches = new bool[16, 16];
260            var lengths = new int[17, 17];
261            for (int source = 15; source >= 0; source--)
262                for (int destination = 15; destination >= 0; destination--)
263                {
264                    matches[source, destination] = oldOpcodes[source] != 0 &&
265                        SameState(previous[source], current[destination]);
266                    lengths[source, destination] = matches[source, destination]
267                        ? 1 + lengths[source + 1, destination + 1]
268                        : System.Math.Max(lengths[source + 1, destination], lengths[source, destination + 1]);
269                }
270            for (int source = 0, destination = 0; source < 16 && destination < 16;)
271            {
272                if (matches[source, destination])
273                {
274                    sources[destination] = source;
275                    used[source] = true;
276                    source++; destination++;
277                }
278                else if (lengths[source + 1, destination] > lengths[source, destination + 1]) source++;
279                else destination++;
280            }
281            // Match the remaining exact states to support arbitrary reorders too.
282            for (int destination = 0; destination < 16; destination++)
283            {
284                if (sources[destination] >= 0 || current[destination][1].value == 0) continue;
285                for (int source = 0; source < 16; source++)
286                    if (!used[source] && SameState(previous[source], current[destination]))
287                    { sources[destination] = source; used[source] = true; break; }
288            }
289
290            // Remaining same-slot operators may have ordinary parameter edits.
291            for (int slot = 0; slot < 16; slot++)
292                if (sources[slot] < 0 && !used[slot] && oldOpcodes[slot] == current[slot][1].value)
293                { sources[slot] = slot; used[slot] = true; }
294
295            // A moved operator may also have been edited. Match by opcode only when
296            // both its remaining source and destination are unambiguous.
297            for (int destination = 0; destination < 16; destination++)
298            {
299                int opcode = (int)current[destination][1].value;
300                if (opcode == 0 || sources[destination] >= 0) continue;
301                int source = -1, candidates = 0, destinations = 0;
302                for (int slot = 0; slot < 16; slot++)
303                {
304                    if (!used[slot] && oldOpcodes[slot] == opcode) { source = slot; candidates++; }
305                    if (sources[slot] < 0 && current[slot][1].value == opcode) destinations++;
306                }
307                if (candidates == 1 && destinations == 1)
308                { sources[destination] = source; used[source] = true; }
309            }
310            for (int destination = 0; destination < 16; destination++)
311            {
312                int source = sources[destination];
313                if (source >= 0 && source != destination)
314                { affected[destination] = true; affected[source] = true; }
315                // Includes a same-opcode replacement whose old occupant moved elsewhere.
316                if (oldOpcodes[destination] != 0 && source != destination) affected[destination] = true;
317            }
318            return sources;
319        }
320
321        static void StepInto(AnimationCurve curve, float previousTime, float transitionTime, bool discrete)
322        {
323            for (int i = 0; i < curve.length; i++)
324            {
325                if (Mathf.Abs(curve[i].time - previousTime) < .00001f)
326                {
327                    // Only change the outgoing side; the preceding animation keeps its tangent.
328                    AnimationUtility.SetKeyBroken(curve, i, true);
329                    AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
330                }
331                if (Mathf.Abs(curve[i].time - transitionTime) < .00001f)
332                {
333                    AnimationUtility.SetKeyBroken(curve, i, true);
334                    AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
335                    AnimationUtility.SetKeyRightTangentMode(curve, i, discrete
336                        ? AnimationUtility.TangentMode.Constant : AnimationUtility.TangentMode.Linear);
337                }
338            }
339        }
340
341        static void InterpolateBetween(AnimationCurve curve, float a, float b)
342        {
343            float start = Mathf.Min(a, b), end = Mathf.Max(a, b);
344            if (end - start < .00001f) return;
345            for (int i = 0; i < curve.length; i++)
346            {
347                if (Mathf.Abs(curve[i].time - start) < .00001f)
348                {
349                    AnimationUtility.SetKeyBroken(curve, i, true);
350                    AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Linear);
351                }
352                if (Mathf.Abs(curve[i].time - end) < .00001f)
353                {
354                    AnimationUtility.SetKeyBroken(curve, i, true);
355                    AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Linear);
356                }
357            }
358        }
359
360        static bool HasNonDefaultInputs(FoldOperator op, List<Field> fields, List<Field> defaults)
361        {
362            // Identity scale and zero translation are neutral regardless of t. Ignore
363            // unused vector components/fields for these identity configurations.
364            if (op.opcode == 11 && ((Vector3)op.vector0).Equals(Vector3.one)) return false;
365            if (op.opcode == 12 && ((Vector3)op.vector0).Equals(Vector3.zero)) return false;
366            for (int f = 0; f < fields.Count; f++)
367                if (fields[f].name != "opcode" && fields[f].name != "enabled" &&
368                    !fields[f].value.Equals(defaults[f].value)) return true;
369            return false;
370        }
371
372        // Recover an existing generated boundary from the saved curves, so this also
373        // works after reopening the editor and after Undo/Redo. Require complete keys
374        // at both ends; partial keys must not be mistaken for a generated boundary.
375        static bool ExistingJumpback(AnimationClip clip, string path, int slot, float time,
376            out float anchor, out bool handoff)
377        {
378            anchor = float.NaN;
379            handoff = false;
380            var fields = Fields(new FoldOperator());
381            var end = new float[fields.Count];
382            var start = new float[fields.Count];
383            int beforeOpcode = 0;
384            for (int f = 0; f < fields.Count; f++)
385            {
386                var curve = AnimationUtility.GetEditorCurve(clip, Binding(path, Prefix(0, slot), fields[f]));
387                if (curve == null) return false;
388                int index = -1;
389                for (int k = 0; k < curve.length; k++)
390                    if (Mathf.Abs(curve[k].time - time) < .00001f) { index = k; break; }
391                if (index < 1) return false;
392                float candidate = curve[index - 1].time;
393                if (f == 0) anchor = candidate;
394                else if (Mathf.Abs(candidate - anchor) > .00001f) return false;
395                end[f] = curve[index].value;
396                start[f] = curve[index - 1].value;
397                if (f == 1 && index > 1)
398                    beforeOpcode = new DiscreteValue { floating = curve[index - 2].value }.integer;
399                if (!fields[f].discrete && index > 1 &&
400                    AnimationUtility.GetKeyLeftTangentMode(curve, index - 1) == AnimationUtility.TangentMode.Constant &&
401                    AnimationUtility.GetKeyRightTangentMode(curve, index - 2) == AnimationUtility.TangentMode.Constant)
402                    handoff = true;
403            }
404            if (handoff) return true;
405            int opcode = new DiscreteValue { floating = end[1] }.integer;
406            if (opcode == 0 || beforeOpcode != 0 || !start[1].Equals(end[1])) return false;
407            var saved = new FoldOperator {
408                enabled = end[0] > .5f, opcode = opcode,
409                float0 = end[2], float1 = end[3], float2 = end[4], float3 = end[5],
410                vector0 = new Vector4(end[6], end[7], end[8], end[9]),
411                vector1 = new Vector4(end[10], end[11], end[12], end[13]),
412                vector2 = new Vector4(end[14], end[15], end[16], end[17]),
413                vector3 = new Vector4(end[18], end[19], end[20], end[21])
414            };
415            // Ordinary additions have a neutral key, without the stepped handoff.
416            return SameState(start, Fields(saved.JumpbackDefaults()));
417        }
418
419        public static int Record(AnimationClip clip, Transform root, FoldStack stack, float time, int advanceFrames = 0,
420            System.Func<FoldMaterialAnimator, int, bool> disableJumpBackKeyframing = null)
421        {
422            time = Mathf.Round(Mathf.Max(0, time) * clip.frameRate) / clip.frameRate;
423            float appearanceTime = PreviousAppearanceTime(clip, time);
424            float requestedTransitionTime = time;
425            float previousTime = appearanceTime;
426            if (!float.IsNegativeInfinity(previousTime))
427            {
428                long frame = Mathf.RoundToInt(time * clip.frameRate);
429                long previousFrame = Mathf.RoundToInt(previousTime * clip.frameRate);
430                // Advance can be negative for backwards authoring. Playback transitions still
431                // belong after the preceding pose, never before it or beyond the current pose.
432                long requested = advanceFrames == 0 ? previousFrame + 1
433                    : frame - System.Math.Abs((long)advanceFrames) + 1;
434                requestedTransitionTime = System.Math.Min(frame, System.Math.Max(previousFrame + 1, requested)) / clip.frameRate;
435            }
436        
437            var bindings = new List<EditorCurveBinding>();
438            var curves = new List<AnimationCurve>();
439            int count = 0;
440            for (int m = 0; m < stack.materials.Length; m++)
441            {
442                var material = stack.materials[m];
443                if (!material || !material.material) continue;
444                string path = AnimationUtility.CalculateTransformPath(material.transform, root);
445                previousTime = appearanceTime;
446                float transitionTime = requestedTransitionTime;
447                var reuseDefault = new bool[16];
448                var anchors = new float[16];
449                for (int slot = 0; slot < 16; slot++)
450                {
451                    if (disableJumpBackKeyframing?.Invoke(material, slot) ?? false) continue;
452                    if (!ExistingJumpback(clip, path, slot, time, out anchors[slot], out bool handoff)) continue;
453                    reuseDefault[slot] = !handoff;
454                    if (handoff && Mathf.Abs(anchors[slot] - appearanceTime) < .00001f)
455                    {
456                        transitionTime = anchors[slot];
457                        previousTime = PreviousAppearanceTime(clip, anchors[slot]);
458                    }
459                }
460                var previous = PreviousStates(clip, path, previousTime);
461                var sources = ReorderSources(material, previous, out var affected);
462                for (int s = 0; s < material.slots.Length; s++)
463                {
464                    var op = material.slots[s];
465                    float slotTransitionTime = reuseDefault[s] ? anchors[s] : transitionTime;
466                    bool cursorOnly = disableJumpBackKeyframing?.Invoke(material, s) ?? false;
467                    var fields = Fields(op);
468                    int previousOpcode = new DiscreteValue { floating = previous[s][1] }.integer;
469                    bool added = op.opcode != 0 && (reuseDefault[s] || (sources[s] < 0 && (previousOpcode == 0 || affected[s])));
470                    var defaults = added ? Fields(op.JumpbackDefaults()) : null;
471                    bool needsDefaultKey = added && HasNonDefaultInputs(op, fields, defaults);
472                    float defaultTime = Mathf.Max(0, time - advanceFrames / clip.frameRate);
473                    // A reordered insertion cannot occupy its destination before the old
474                    // operator has moved out. Its default pose belongs at the handoff instead.
475                    if (added && affected[s]) defaultTime = slotTransitionTime;
476                    if (reuseDefault[s]) defaultTime = anchors[s];
477                    string prefix = Prefix(m, s);
478                    var opcodeCurve = added ? AnimationUtility.GetEditorCurve(clip, Binding(path, prefix, fields[1])) : null;
479                    if (needsDefaultKey && !reuseDefault[s] && !affected[s] && defaultTime < time && previousOpcode == 0 && opcodeCurve != null)
480                    {
481                        // Do not replace a different operator's earlier use of this material slot.
482                        bool occupied = new DiscreteValue { floating = LastValue(opcodeCurve, defaultTime) }.integer != 0;
483                        foreach (var key in opcodeCurve.keys)
484                            if (key.time > defaultTime && key.time < time && new DiscreteValue { floating = key.value }.integer != 0) occupied = true;
485                        if (occupied && !float.IsNegativeInfinity(previousTime)) defaultTime = Mathf.Max(defaultTime, previousTime);
486                    }
487                    if ((cursorOnly || !affected[s]) && !Changed(clip, path, prefix, fields, time)) continue;
488                    count++;
489                    for (int f = 0; f < fields.Count; f++)
490                    {
491                        var field = fields[f];
492                        var binding = Binding(path, prefix, field);
493                        var curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve();
494                        if (cursorOnly)
495                        {
496                            Key(curve, time, CurveValue(field), field.discrete);
497                            bindings.Add(binding); curves.Add(curve);
498                            continue;
499                        }
500                        if (added && (needsDefaultKey || affected[s] || reuseDefault[s]))
501                        {
502                            // Reorders still need their structural handoff even for a default
503                            // occupant. Ordinary additions at their opcode defaults only get the current key.
504                            // Initial empty-stack snapshots inside the new span would interrupt
505                            // the default-to-user interpolation. Remove only keys from times when
506                            // this slot had no operator, and only inside this operator's new span.
507                            float start = Mathf.Min(defaultTime, time), end = Mathf.Max(defaultTime, time);
508                            for (int i = curve.length - 1; i >= 0; i--)
509                                if (curve[i].time > start + .00001f && curve[i].time < end - .00001f &&
510                                    new DiscreteValue { floating = LastValue(opcodeCurve, curve[i].time) }.integer == 0) curve.RemoveKey(i);
511                            if (!float.IsNegativeInfinity(previousTime) &&
512                                (affected[s] || Mathf.Abs(defaultTime - time) < .00001f))
513                                HoldPreviousState(curve, previousTime, field.discrete);
514                            if (Mathf.Abs(defaultTime - time) > .00001f)
515                                Key(curve, defaultTime, CurveValue(defaults[f]), field.discrete);
516                        }
517                        else if (!added)
518                        {
519                            if (!float.IsNegativeInfinity(previousTime))
520                                HoldPreviousState(curve, previousTime, field.discrete);
521                            if (affected[s] && slotTransitionTime < time - .00001f)
522                            {
523                                float previousValue = sources[s] >= 0 ? previous[sources[s]][f] : 0;
524                                if (field.name == "opcode") previousValue = CurveValue(field);
525                                Key(curve, slotTransitionTime, previousValue, field.discrete);
526                            }
527                        }
528                        Key(curve, time, CurveValue(field), field.discrete);
529                        if (affected[s]) StepInto(curve, previousTime, slotTransitionTime, field.discrete);
530                        if (!field.discrete)
531                        {
532                            // Restore BOTH ends of the interpolated span. Key() preserves
533                            // Constant tangents, which may be stale after replacing/clearing
534                            // reorder keys. Preserve only the step into the reordered layout.
535                            if (affected[s]) InterpolateBetween(curve, slotTransitionTime, time);
536                            else if (needsDefaultKey || reuseDefault[s]) InterpolateBetween(curve, defaultTime, time);
537                            else if (!added && previousOpcode == op.opcode && !float.IsNegativeInfinity(previousTime))
538                                InterpolateBetween(curve, previousTime, time);
539                        }
540                        bindings.Add(binding); curves.Add(curve);
541                    }
542                }
543            }
544            if (count > 0)
545            {
546                Undo.RegisterCompleteObjectUndo(clip, "Keyframe Fold appearance");
547                AnimationUtility.SetEditorCurves(clip, bindings.ToArray(), curves.ToArray());
548                EditorUtility.SetDirty(clip);
549            }
550            return count;
551        }
552    }
553}