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