diff --git a/README.md b/README.md index f77e076..df0897b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ a tool can view spine files with different spine-runtimes version and export gif * **3.7.94** * **3.8.95** * **4.0.31** + * **4.0.64** + * **4.1.00** * Export animation to gif or png file. * Can view Animation with different options. diff --git a/README_zhTW.md b/README_zhTW.md index 7c63903..9b27338 100644 --- a/README_zhTW.md +++ b/README_zhTW.md @@ -21,6 +21,8 @@ * **3.7.94** * **3.8.95** * **4.0.31** + * **4.0.64** + * **4.1.00** * 將Spine動畫匯出成png或gif。 * 能用不同功能選項瀏覽Spine動畫。 diff --git a/SpineViewerWPF/Properties/AssemblyInfo.cs b/SpineViewerWPF/Properties/AssemblyInfo.cs index 8943c51..424ed66 100644 --- a/SpineViewerWPF/Properties/AssemblyInfo.cs +++ b/SpineViewerWPF/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // 您可以指定所有的值,或將組建編號或修訂編號設為預設值 // 指定為預設值: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.3.1.0")] -[assembly: AssemblyFileVersion("2.3.1.0")] +[assembly: AssemblyVersion("2.4.0.0")] +[assembly: AssemblyFileVersion("2.4.0.0")] diff --git a/SpineViewerWPF/PublicFunction/Common.cs b/SpineViewerWPF/PublicFunction/Common.cs index c13e59a..fb95268 100644 --- a/SpineViewerWPF/PublicFunction/Common.cs +++ b/SpineViewerWPF/PublicFunction/Common.cs @@ -175,7 +175,10 @@ public static void SaveToGif(List lms, float time = 0) fileName += $"_{App.globalValues.SelectSkin}"; saveFileDialog.FileName = fileName; - saveFileDialog.ShowDialog(); + if (saveFileDialog.ShowDialog() == false) + { + return; + } int delay = 0; if (time == 0) { @@ -216,7 +219,7 @@ public static void SaveToGif(List lms, float time = 0) { gif.SaveAsGif(fs, new GifEncoder() { ColorTableMode = GifColorTableMode.Global }); } - + gif.Dispose(); } else { @@ -248,7 +251,10 @@ public static void SaveToGif2(float time = 0) fileName += $"_{App.globalValues.SelectSkin}"; saveFileDialog.FileName = fileName; - saveFileDialog.ShowDialog(); + if(saveFileDialog.ShowDialog() == false ) + { + return; + } string[] pngList = Directory.GetFiles($"{App.rootDir}\\Temp\\", "*.png",SearchOption.TopDirectoryOnly); @@ -293,6 +299,7 @@ public static void SaveToGif2(float time = 0) { gif.SaveAsGif(fs,new GifEncoder() { ColorTableMode = GifColorTableMode.Global}); } + gif.Dispose(); } else @@ -411,6 +418,7 @@ public static void TakeScreenshot() Texture2D texture = new Texture2D(_graphicsDevice, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight, false, _graphicsDevice.PresentationParameters.BackBufferFormat); texture.SetData(screenData); Common.SaveToPng(texture); + texture.Dispose(); } App.globalValues.TimeScale = bakTimeScale; } diff --git a/SpineViewerWPF/PublicFunction/Player/IPlayer.cs b/SpineViewerWPF/PublicFunction/Player/IPlayer.cs index ca42157..0b3ce43 100644 --- a/SpineViewerWPF/PublicFunction/Player/IPlayer.cs +++ b/SpineViewerWPF/PublicFunction/Player/IPlayer.cs @@ -22,5 +22,7 @@ public interface IPlayer void ChangeSet(); void SizeChange(); + + void Dispose(); } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_2_1_08.cs b/SpineViewerWPF/PublicFunction/Player/Player_2_1_08.cs index 3f05789..68e3272 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_2_1_08.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_2_1_08.cs @@ -202,5 +202,11 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } + } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_2_1_25.cs b/SpineViewerWPF/PublicFunction/Player/Player_2_1_25.cs index 22f78dd..d1a3000 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_2_1_25.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_2_1_25.cs @@ -220,5 +220,10 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_1_07.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_1_07.cs index 1dff4db..1b58a03 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_1_07.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_1_07.cs @@ -221,5 +221,10 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_2_xx.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_2_xx.cs index 8b11ca2..c28f6cf 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_2_xx.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_2_xx.cs @@ -221,5 +221,10 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_4_02.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_4_02.cs index fa76f8a..ea74209 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_4_02.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_4_02.cs @@ -221,5 +221,10 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_5_51.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_5_51.cs index 01cb87e..d9dd65e 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_5_51.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_5_51.cs @@ -220,5 +220,10 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_6_32.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_6_32.cs index 9d35779..7fd7c53 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_6_32.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_6_32.cs @@ -228,5 +228,10 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_6_39.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_6_39.cs index 55ef64a..ccd2536 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_6_39.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_6_39.cs @@ -213,5 +213,10 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_6_53.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_6_53.cs index 02ac7f3..da310eb 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_6_53.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_6_53.cs @@ -228,5 +228,10 @@ public void SizeChange() if (App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_7_94.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_7_94.cs index 376313e..75c9364 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_7_94.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_7_94.cs @@ -212,5 +212,10 @@ public void SizeChange() if(App.graphicsDevice != null) Player.UserControl_SizeChanged(ref App.graphicsDevice); } + + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_3_8_95.cs b/SpineViewerWPF/PublicFunction/Player/Player_3_8_95.cs index a725521..66029e9 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_3_8_95.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_3_8_95.cs @@ -239,5 +239,9 @@ public void SizeChange() Player.UserControl_SizeChanged(ref App.graphicsDevice); } + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_4_0_31.cs b/SpineViewerWPF/PublicFunction/Player/Player_4_0_31.cs index b4562bd..20cb2a7 100644 --- a/SpineViewerWPF/PublicFunction/Player/Player_4_0_31.cs +++ b/SpineViewerWPF/PublicFunction/Player/Player_4_0_31.cs @@ -238,5 +238,9 @@ public void SizeChange() Player.UserControl_SizeChanged(ref App.graphicsDevice); } + public void Dispose() + { + ChangeSet(); + } } diff --git a/SpineViewerWPF/PublicFunction/Player/Player_4_0_64.cs b/SpineViewerWPF/PublicFunction/Player/Player_4_0_64.cs new file mode 100644 index 0000000..c2c87ec --- /dev/null +++ b/SpineViewerWPF/PublicFunction/Player/Player_4_0_64.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SpineViewerWPF; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Spine4_0_64; + +public class Player_4_0_64 : IPlayer +{ + private Skeleton skeleton; + private AnimationState state; + private SkeletonRenderer skeletonRenderer; + private ExposedList listAnimation; + private ExposedList listSkin; + private Atlas atlas; + private SkeletonData skeletonData; + private AnimationStateData stateData; + private SkeletonBinary binary; + private SkeletonJson json; + + public void Initialize() + { + Player.Initialize(ref App.graphicsDevice, ref App.spriteBatch); + } + + public void LoadContent(ContentManager contentManager) + { + skeletonRenderer = new SkeletonRenderer(App.graphicsDevice); + skeletonRenderer.PremultipliedAlpha = App.globalValues.Alpha; + + if (App.mulitTexture != null && App.mulitTexture.Length == 0) + { + atlas = new Atlas(App.globalValues.SelectAtlasFile, new XnaTextureLoader(App.graphicsDevice)); + } + else + { + atlas = new Atlas(App.globalValues.SelectAtlasFile, new XnaTextureLoader(App.graphicsDevice, true, App.mulitTexture)); + } + + if (Common.IsBinaryData(App.globalValues.SelectSpineFile)) + { + binary = new SkeletonBinary(atlas); + binary.Scale = App.globalValues.Scale; + skeletonData = binary.ReadSkeletonData(App.globalValues.SelectSpineFile); + } + else + { + json = new SkeletonJson(atlas); + json.Scale = App.globalValues.Scale; + skeletonData = json.ReadSkeletonData(App.globalValues.SelectSpineFile); + } + App.globalValues.SpineVersion = skeletonData.Version; + skeleton = new Skeleton(skeletonData); + + + + + Common.SetInitLocation(skeleton.Data.Height); + App.globalValues.FileHash = skeleton.Data.Hash; + + stateData = new AnimationStateData(skeleton.Data); + + state = new AnimationState(stateData); + + List AnimationNames = new List(); + listAnimation = state.Data.skeletonData.Animations; + foreach (Animation An in listAnimation) + { + AnimationNames.Add(An.name); + } + App.globalValues.AnimeList = AnimationNames; + + List SkinNames = new List(); + listSkin = state.Data.skeletonData.Skins; + foreach (Skin Sk in listSkin) + { + SkinNames.Add(Sk.name); + } + App.globalValues.SkinList = SkinNames; + + if (App.globalValues.SelectAnimeName != "") + { + state.SetAnimation(0, App.globalValues.SelectAnimeName, App.globalValues.IsLoop); + } + else + { + state.SetAnimation(0, state.Data.skeletonData.animations.Items[0].name, App.globalValues.IsLoop); + } + + if (App.isNew) + { + App.globalValues.PosX = (float)App.canvasWidth/2; + App.globalValues.PosY = (float)App.canvasHeight/2; + MainWindow.SetCBAnimeName(); + } + App.isNew = false; + + } + + + + public void Update(GameTime gameTime) + { + if (App.globalValues.SelectAnimeName != "" && App.globalValues.SetAnime) + { + state.ClearTracks(); + skeleton.SetToSetupPose(); + state.SetAnimation(0, App.globalValues.SelectAnimeName, App.globalValues.IsLoop); + App.globalValues.SetAnime = false; + } + + if (App.globalValues.SelectSkin != "" && App.globalValues.SetSkin) + { + skeleton.SetSkin(App.globalValues.SelectSkin); + skeleton.SetSlotsToSetupPose(); + App.globalValues.SetSkin = false; + } + + + } + + public void Draw() + { + if (App.globalValues.SelectSpineVersion != "4.0.64" || App.globalValues.FileHash != skeleton.Data.Hash) + { + state = null; + skeletonRenderer = null; + return; + } + App.graphicsDevice.Clear(Color.Transparent); + + Player.DrawBG(ref App.spriteBatch); + + + state.Update(App.globalValues.Speed / 1000f); + state.Apply(skeleton); + state.TimeScale = App.globalValues.TimeScale; + if (binary != null) + { + if (App.globalValues.Scale != binary.Scale) + { + binary.Scale = App.globalValues.Scale; + skeletonData = binary.ReadSkeletonData(App.globalValues.SelectSpineFile); + skeleton = new Skeleton(skeletonData); + } + } + else if (json != null) + { + if (App.globalValues.Scale != json.Scale) + { + json.Scale = App.globalValues.Scale; + skeletonData = json.ReadSkeletonData(App.globalValues.SelectSpineFile); + skeleton = new Skeleton(skeletonData); + } + } + + skeleton.X = App.globalValues.PosX; + skeleton.Y = App.globalValues.PosY; + skeleton.ScaleX = (App.globalValues.FilpX ? -1 : 1) ; + skeleton.ScaleY = (App.globalValues.FilpY ? -1 : 1) ; + + + skeleton.RootBone.Rotation = App.globalValues.Rotation; + skeleton.UpdateWorldTransform(); + skeletonRenderer.PremultipliedAlpha = App.globalValues.Alpha; + if (skeletonRenderer.Effect is BasicEffect) + { + ((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, App.graphicsDevice.Viewport.Width, App.graphicsDevice.Viewport.Height, 0, 1, 0); + } + else + { + skeletonRenderer.Effect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, App.graphicsDevice.Viewport.Width, App.graphicsDevice.Viewport.Height, 0, 1, 0)); + } + skeletonRenderer.Begin(); + skeletonRenderer.Draw(skeleton); + skeletonRenderer.End(); + + if (state != null) + { + TrackEntry entry = state.GetCurrent(0); + if (entry != null) + { + if (App.globalValues.IsRecoding && (App.globalValues.GifList != null || App.recordImageCount >0 ) && !entry.IsComplete) + { + if (App.recordImageCount == 1) + { + TrackEntry te = state.GetCurrent(0); + te.trackTime = 0; + App.globalValues.TimeScale = 1; + App.globalValues.Lock = 0; + } + + Common.TakeRecodeScreenshot(App.graphicsDevice); + } + + if (App.globalValues.IsRecoding && entry.IsComplete) + { + state.TimeScale = 0; + App.globalValues.IsRecoding = false; + Common.RecodingEnd(entry.AnimationEnd); + + state.TimeScale = 1; + App.globalValues.TimeScale = 1; + } + + if (App.globalValues.TimeScale == 0) + { + entry.TrackTime = entry.AnimationEnd * App.globalValues.Lock; + entry.TimeScale = 0; + } + else + { + App.globalValues.Lock = entry.AnimationTime / entry.AnimationEnd; + entry.TimeScale = 1; + } + App.globalValues.LoadingProcess = $"{ Math.Round(entry.AnimationTime / entry.AnimationEnd * 100, 2)}%"; + } + } + + + } + + public void ChangeSet() + { + App.appXC.ContentManager.Dispose(); + atlas.Dispose(); + atlas = null; + App.appXC.LoadContent.Invoke(App.appXC.ContentManager); + } + + public void SizeChange() + { + if (App.graphicsDevice != null) + Player.UserControl_SizeChanged(ref App.graphicsDevice); + } + + public void Dispose() + { + ChangeSet(); + } +} + diff --git a/SpineViewerWPF/PublicFunction/Player/Player_4_1_00.cs b/SpineViewerWPF/PublicFunction/Player/Player_4_1_00.cs new file mode 100644 index 0000000..2820c15 --- /dev/null +++ b/SpineViewerWPF/PublicFunction/Player/Player_4_1_00.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SpineViewerWPF; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Spine4_1_00; + +public class Player_4_1_00 : IPlayer +{ + private Skeleton skeleton; + private AnimationState state; + private SkeletonRenderer skeletonRenderer; + private ExposedList listAnimation; + private ExposedList listSkin; + private Atlas atlas; + private SkeletonData skeletonData; + private AnimationStateData stateData; + private SkeletonBinary binary; + private SkeletonJson json; + + public void Initialize() + { + Player.Initialize(ref App.graphicsDevice, ref App.spriteBatch); + } + + public void LoadContent(ContentManager contentManager) + { + skeletonRenderer = new SkeletonRenderer(App.graphicsDevice); + skeletonRenderer.PremultipliedAlpha = App.globalValues.Alpha; + + if (App.mulitTexture != null && App.mulitTexture.Length == 0) + { + atlas = new Atlas(App.globalValues.SelectAtlasFile, new XnaTextureLoader(App.graphicsDevice)); + } + else + { + atlas = new Atlas(App.globalValues.SelectAtlasFile, new XnaTextureLoader(App.graphicsDevice, true, App.mulitTexture)); + } + + if (Common.IsBinaryData(App.globalValues.SelectSpineFile)) + { + binary = new SkeletonBinary(atlas); + binary.Scale = App.globalValues.Scale; + skeletonData = binary.ReadSkeletonData(App.globalValues.SelectSpineFile); + } + else + { + json = new SkeletonJson(atlas); + json.Scale = App.globalValues.Scale; + skeletonData = json.ReadSkeletonData(App.globalValues.SelectSpineFile); + } + App.globalValues.SpineVersion = skeletonData.Version; + skeleton = new Skeleton(skeletonData); + + + + + Common.SetInitLocation(skeleton.Data.Height); + App.globalValues.FileHash = skeleton.Data.Hash; + + stateData = new AnimationStateData(skeleton.Data); + + state = new AnimationState(stateData); + + List AnimationNames = new List(); + listAnimation = state.Data.skeletonData.Animations; + foreach (Animation An in listAnimation) + { + AnimationNames.Add(An.name); + } + App.globalValues.AnimeList = AnimationNames; + + List SkinNames = new List(); + listSkin = state.Data.skeletonData.Skins; + foreach (Skin Sk in listSkin) + { + SkinNames.Add(Sk.name); + } + App.globalValues.SkinList = SkinNames; + + if (App.globalValues.SelectAnimeName != "") + { + state.SetAnimation(0, App.globalValues.SelectAnimeName, App.globalValues.IsLoop); + } + else + { + state.SetAnimation(0, state.Data.skeletonData.animations.Items[0].name, App.globalValues.IsLoop); + } + + if (App.isNew) + { + App.globalValues.PosX = (float)App.canvasWidth/2; + App.globalValues.PosY = (float)App.canvasHeight/2; + MainWindow.SetCBAnimeName(); + } + App.isNew = false; + + } + + + + public void Update(GameTime gameTime) + { + if (App.globalValues.SelectAnimeName != "" && App.globalValues.SetAnime) + { + state.ClearTracks(); + skeleton.SetToSetupPose(); + state.SetAnimation(0, App.globalValues.SelectAnimeName, App.globalValues.IsLoop); + App.globalValues.SetAnime = false; + } + + if (App.globalValues.SelectSkin != "" && App.globalValues.SetSkin) + { + skeleton.SetSkin(App.globalValues.SelectSkin); + skeleton.SetSlotsToSetupPose(); + App.globalValues.SetSkin = false; + } + + + } + + public void Draw() + { + if (App.globalValues.SelectSpineVersion != "4.1.00" || App.globalValues.FileHash != skeleton.Data.Hash) + { + state = null; + skeletonRenderer = null; + return; + } + App.graphicsDevice.Clear(Color.Transparent); + + Player.DrawBG(ref App.spriteBatch); + + + state.Update(App.globalValues.Speed / 1000f); + state.Apply(skeleton); + state.TimeScale = App.globalValues.TimeScale; + if (binary != null) + { + if (App.globalValues.Scale != binary.Scale) + { + binary.Scale = App.globalValues.Scale; + skeletonData = binary.ReadSkeletonData(App.globalValues.SelectSpineFile); + skeleton = new Skeleton(skeletonData); + } + } + else if (json != null) + { + if (App.globalValues.Scale != json.Scale) + { + json.Scale = App.globalValues.Scale; + skeletonData = json.ReadSkeletonData(App.globalValues.SelectSpineFile); + skeleton = new Skeleton(skeletonData); + } + } + + skeleton.X = App.globalValues.PosX; + skeleton.Y = App.globalValues.PosY; + skeleton.ScaleX = (App.globalValues.FilpX ? -1 : 1) ; + skeleton.ScaleY = (App.globalValues.FilpY ? -1 : 1) ; + + + skeleton.RootBone.Rotation = App.globalValues.Rotation; + skeleton.UpdateWorldTransform(); + skeletonRenderer.PremultipliedAlpha = App.globalValues.Alpha; + if (skeletonRenderer.Effect is BasicEffect) + { + ((BasicEffect)skeletonRenderer.Effect).Projection = Matrix.CreateOrthographicOffCenter(0, App.graphicsDevice.Viewport.Width, App.graphicsDevice.Viewport.Height, 0, 1, 0); + } + else + { + skeletonRenderer.Effect.Parameters["Projection"].SetValue(Matrix.CreateOrthographicOffCenter(0, App.graphicsDevice.Viewport.Width, App.graphicsDevice.Viewport.Height, 0, 1, 0)); + } + skeletonRenderer.Begin(); + skeletonRenderer.Draw(skeleton); + skeletonRenderer.End(); + + if (state != null) + { + TrackEntry entry = state.GetCurrent(0); + if (entry != null) + { + if (App.globalValues.IsRecoding && (App.globalValues.GifList != null || App.recordImageCount >0 ) && !entry.IsComplete) + { + if (App.recordImageCount == 1) + { + TrackEntry te = state.GetCurrent(0); + te.trackTime = 0; + App.globalValues.TimeScale = 1; + App.globalValues.Lock = 0; + } + + Common.TakeRecodeScreenshot(App.graphicsDevice); + } + + if (App.globalValues.IsRecoding && entry.IsComplete) + { + state.TimeScale = 0; + App.globalValues.IsRecoding = false; + Common.RecodingEnd(entry.AnimationEnd); + + state.TimeScale = 1; + App.globalValues.TimeScale = 1; + } + + if (App.globalValues.TimeScale == 0) + { + entry.TrackTime = entry.AnimationEnd * App.globalValues.Lock; + entry.TimeScale = 0; + } + else + { + App.globalValues.Lock = entry.AnimationTime / entry.AnimationEnd; + entry.TimeScale = 1; + } + App.globalValues.LoadingProcess = $"{ Math.Round(entry.AnimationTime / entry.AnimationEnd * 100, 2)}%"; + } + } + + + } + + public void ChangeSet() + { + App.appXC.ContentManager.Dispose(); + atlas.Dispose(); + atlas = null; + App.appXC.LoadContent.Invoke(App.appXC.ContentManager); + } + + public void SizeChange() + { + if (App.graphicsDevice != null) + Player.UserControl_SizeChanged(ref App.graphicsDevice); + } + + public void Dispose() + { + ChangeSet(); + } +} + diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Animation.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Animation.cs new file mode 100644 index 0000000..ba689da --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Animation.cs @@ -0,0 +1,2586 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_0_64 { + + /// + /// Stores a list of timelines to animate a skeleton's pose over time. + public class Animation { + internal String name; + internal ExposedList timelines; + internal HashSet timelineIds; + internal float duration; + + public Animation (string name, ExposedList timelines, float duration) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + + this.name = name; + SetTimelines(timelines); + this.duration = duration; + } + + public ExposedList Timelines { + get { return timelines; } + set { SetTimelines(value); } + } + + public void SetTimelines (ExposedList timelines) { + if (timelines == null) throw new ArgumentNullException("timelines", "timelines cannot be null."); + this.timelines = timelines; + // Note: avoiding reallocations by adding all hash set entries at + // once (EnsureCapacity() is only available in newer .Net versions). + int idCount = 0; + int timelinesCount = timelines.Count; + Timeline[] timelinesItems = timelines.Items; + for (int t = 0; t < timelinesCount; ++t) + idCount += timelinesItems[t].PropertyIds.Length; + string[] propertyIds = new string[idCount]; + int currentId = 0; + for (int t = 0; t < timelinesCount; ++t) { + var ids = timelinesItems[t].PropertyIds; + for (int i = 0, idsLength = ids.Length; i < idsLength; ++i) + propertyIds[currentId++] = ids[i]; + } + this.timelineIds = new HashSet(propertyIds); + } + + /// The duration of the animation in seconds, which is usually the highest time of all frames in the timeline. The duration is + /// used to know when it has completed and when it should loop back to the start. + public float Duration { get { return duration; } set { duration = value; } } + + /// The animation's name, which is unique across all animations in the skeleton. + public string Name { get { return name; } } + + /// Returns true if this animation contains a timeline with any of the specified property IDs. + public bool HasTimeline (string[] propertyIds) { + foreach (string id in propertyIds) + if (timelineIds.Contains(id)) return true; + return false; + } + + /// Applies the animation's timelines to the specified skeleton. + /// + /// The skeleton the animation is being applied to. This provides access to the bones, slots, and other skeleton + /// components the timelines may change. + /// The last time in seconds this animation was applied. Some timelines trigger only at specific times rather + /// than every frame. Pass -1 the first time an animation is applied to ensure frame 0 is triggered. + /// The time in seconds the skeleton is being posed for. Most timelines find the frame before and the frame after + /// this time and interpolate between the frame values. If beyond the and loop is + /// true then the animation will repeat, else the last frame will be applied. + /// If true, the animation repeats after the . + /// If any events are fired, they are added to this list. Can be null to ignore fired events or if no timelines + /// fire events. + /// 0 applies the current or setup values (depending on blend). 1 applies the timeline values. Between + /// 0 and 1 applies values between the current or setup values and the timeline values. By adjusting + /// alpha over time, an animation can be mixed in or out. alpha can also be useful to apply + /// animations on top of each other (layering). + /// Controls how mixing is applied when alpha < 1. + /// Indicates whether the timelines are mixing in or out. Used by timelines which perform instant transitions, + /// such as or . + public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, ExposedList events, float alpha, + MixBlend blend, MixDirection direction) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + + if (loop && duration != 0) { + time %= duration; + if (lastTime > 0) lastTime %= duration; + } + + var timelines = this.timelines.Items; + for (int i = 0, n = this.timelines.Count; i < n; i++) + timelines[i].Apply(skeleton, lastTime, time, events, alpha, blend, direction); + } + + override public string ToString () { + return name; + } + } + + /// + /// Controls how timeline values are mixed with setup pose values or current pose values when a timeline is applied with + /// alpha < 1. + /// + public enum MixBlend { + /// Transitions from the setup value to the timeline value (the current value is not used). Before the first frame, the + /// setup value is set. + Setup, + + /// + /// + /// Transitions from the current value to the timeline value. Before the first frame, transitions from the current value to + /// the setup value. Timelines which perform instant transitions, such as or + /// , use the setup value before the first frame. + /// + /// First is intended for the first animations applied, not for animations layered on top of those. + /// + First, + + /// + /// + /// Transitions from the current value to the timeline value. No change is made before the first frame (the current value is + /// kept until the first frame). + /// + /// Replace is intended for animations layered on top of others, not for the first animations applied. + /// + Replace, + + /// + /// + /// Transitions from the current value to the current value plus the timeline value. No change is made before the first frame + /// (the current value is kept until the first frame). + /// + /// Add is intended for animations layered on top of others, not for the first animations applied. Properties + /// set by additive animations must be set manually or by another animation before applying the additive animations, else the + /// property values will increase each time the additive animations are applied. + /// + /// + Add + } + + /// + /// Indicates whether a timeline's alpha is mixing out over time toward 0 (the setup or current pose value) or + /// mixing in toward 1 (the timeline's value). Some timelines use this to decide how values are applied. + /// + public enum MixDirection { + In, + Out + } + + internal enum Property { + Rotate = 0, X, Y, ScaleX, ScaleY, ShearX, ShearY, // + RGB, Alpha, RGB2, // + Attachment, Deform, // + Event, DrawOrder, // + IkConstraint, TransformConstraint, // + PathConstraintPosition, PathConstraintSpacing, PathConstraintMix + } + + /// + /// The base class for all timelines. + public abstract class Timeline { + private readonly string[] propertyIds; + internal readonly float[] frames; + + /// Unique identifiers for the properties the timeline modifies. + public Timeline (int frameCount, params string[] propertyIds) { + if (propertyIds == null) throw new System.ArgumentNullException("propertyIds", "propertyIds cannot be null."); + this.propertyIds = propertyIds; + frames = new float[frameCount * FrameEntries]; + } + + /// Uniquely encodes both the type of this timeline and the skeleton properties that it affects. + public string[] PropertyIds { + get { return propertyIds; } + } + + /// The time in seconds and any other values for each frame. + public float[] Frames { + get { return frames; } + } + + /// The number of entries stored per frame. + public virtual int FrameEntries { + get { return 1; } + } + + /// The number of frames for this timeline. + public int FrameCount { + get { return frames.Length / FrameEntries; } + } + + public float Duration { + get { + return frames[frames.Length - FrameEntries]; + } + } + + /// Applies this timeline to the skeleton. + /// The skeleton the timeline is being applied to. This provides access to the bones, slots, and other + /// skeleton components the timeline may change. + /// The time this timeline was last applied. Timelines such as trigger only + /// at specific times rather than every frame. In that case, the timeline triggers everything between + /// lastTime (exclusive) and time (inclusive). Pass -1 the first time an animation is + /// applied to ensure frame 0 is triggered. + /// The time in seconds that the skeleton is being posed for. Most timelines find the frame before and the frame + /// after this time and interpolate between the frame values.If beyond the last frame, the last frame will be + /// applied. + /// If any events are fired, they are added to this list. Can be null to ignore fired events or if the timeline + /// does not fire events. + /// 0 applies the current or setup value (depending on blend). 1 applies the timeline value. + /// Between 0 and 1 applies a value between the current or setup value and the timeline value.By adjusting + /// alpha over time, an animation can be mixed in or out. alpha can also be useful to + /// apply animations on top of each other (layering). + /// Controls how mixing is applied when alpha < 1. + /// Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, + /// such as or , and other such as . + public abstract void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, + MixBlend blend, MixDirection direction); + + /// Search using a stride of 1. + /// Must be >= the first value in frames. + /// The index of the first value <= time. + internal static int Search (float[] frames, float time) { + int n = frames.Length; + for (int i = 1; i < n; i++) + if (frames[i] > time) return i - 1; + return n - 1; + } + + /// Search using the specified stride. + /// Must be >= the first value in frames. + /// The index of the first value <= time. + internal static int Search (float[] frames, float time, int step) { + int n = frames.Length; + for (int i = step; i < n; i += step) + if (frames[i] > time) return i - step; + return n - step; + } + } + + /// An interface for timelines which change the property of a bone. + public interface IBoneTimeline { + /// The index of the bone in that will be changed when this timeline is applied. + int BoneIndex { get; } + } + + /// An interface for timelines which change the property of a slot. + public interface ISlotTimeline { + /// The index of the slot in that will be changed when this timeline is applied. + int SlotIndex { get; } + } + + /// The base class for timelines that interpolate between frame values using stepped, linear, or a Bezier curve. + public abstract class CurveTimeline : Timeline { + public const int LINEAR = 0, STEPPED = 1, BEZIER = 2, BEZIER_SIZE = 18; + + internal float[] curves; + /// The number of key frames for this timeline. + + /// The maximum number of Bezier curves. See . + /// Unique identifiers for the properties the timeline modifies. + public CurveTimeline (int frameCount, int bezierCount, params string[] propertyIds) + : base(frameCount, propertyIds) { + curves = new float[frameCount + bezierCount * BEZIER_SIZE]; + curves[frameCount - 1] = STEPPED; + } + + /// Sets the specified frame to linear interpolation. + /// Between 0 and frameCount - 1, inclusive. + public void SetLinear (int frame) { + curves[frame] = LINEAR; + } + + /// Sets the specified frame to stepped interpolation. + /// Between 0 and frameCount - 1, inclusive. + public void SetStepped (int frame) { + curves[frame] = STEPPED; + } + + /// Returns the interpolation type for the specified frame. + /// Between 0 and frameCount - 1, inclusive. + /// , or + the index of the Bezier segments. + public float GetCurveType (int frame) { + return (int)curves[frame]; + } + + /// Shrinks the storage for Bezier curves, for use when bezierCount (specified in the constructor) was larger + /// than the actual number of Bezier curves. + public void Shrink (int bezierCount) { + int size = FrameCount + bezierCount * BEZIER_SIZE; + if (curves.Length > size) { + float[] newCurves = new float[size]; + Array.Copy(curves, 0, newCurves, 0, size); + curves = newCurves; + } + } + + /// + /// Stores the segments for the specified Bezier curve. For timelines that modify multiple values, there may be more than + /// one curve per frame. + /// The ordinal of this Bezier curve for this timeline, between 0 and bezierCount - 1 (specified + /// in the constructor), inclusive. + /// Between 0 and frameCount - 1, inclusive. + /// The index of the value for the frame this curve is used for. + /// The time for the first key. + /// The value for the first key. + /// The time for the first Bezier handle. + /// The value for the first Bezier handle. + /// The time of the second Bezier handle. + /// The value for the second Bezier handle. + /// The time for the second key. + /// The value for the second key. + public void SetBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2, + float cy2, float time2, float value2) { + + float[] curves = this.curves; + int i = FrameCount + bezier * BEZIER_SIZE; + if (value == 0) curves[frame] = BEZIER + i; + float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = (value1 - cy1 * 2 + cy2) * 0.03f; + float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = ((cy1 - cy2) * 3 - value1 + value2) * 0.006f; + float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy; + float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = (cy1 - value1) * 0.3f + tmpy + dddy * 0.16666667f; + float x = time1 + dx, y = value1 + dy; + for (int n = i + BEZIER_SIZE; i < n; i += 2) { + curves[i] = x; + curves[i + 1] = y; + dx += ddx; + dy += ddy; + ddx += dddx; + ddy += dddy; + x += dx; + y += dy; + } + } + + /// + /// Returns the Bezier interpolated value for the specified time. + /// The index into for the values of the frame before time. + /// The offset from frameIndex to the value this curve is used for. + /// The index of the Bezier segments. See . + public float GetBezierValue (float time, int frameIndex, int valueOffset, int i) { + float[] curves = this.curves; + if (curves[i] > time) { + float x = frames[frameIndex], y = frames[frameIndex + valueOffset]; + return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); + } + int n = i + BEZIER_SIZE; + for (i += 2; i < n; i += 2) { + if (curves[i] >= time) { + float x = curves[i - 2], y = curves[i - 1]; + return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); + } + } + frameIndex += FrameEntries; + { // scope added to prevent compile error "float x and y declared in enclosing scope" + float x = curves[n - 2], y = curves[n - 1]; + return y + (time - x) / (frames[frameIndex] - x) * (frames[frameIndex + valueOffset] - y); + } + } + } + + /// The base class for a that sets one property. + public abstract class CurveTimeline1 : CurveTimeline { + public const int ENTRIES = 2; + internal const int VALUE = 1; + + /// The maximum number of Bezier curves. See . + /// Unique identifiers for the properties the timeline modifies. + public CurveTimeline1 (int frameCount, int bezierCount, string propertyId) + : base(frameCount, bezierCount, propertyId) { + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + /// Sets the time and value for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds + public void SetFrame (int frame, float time, float value) { + frame <<= 1; + frames[frame] = time; + frames[frame + VALUE] = value; + } + + /// Returns the interpolated value for the specified time. + public float GetCurveValue (float time) { + float[] frames = this.frames; + int i = frames.Length - 2; + for (int ii = 2; ii <= i; ii += 2) { + if (frames[ii] > time) { + i = ii - 2; + break; + } + } + + int curveType = (int)curves[i >> 1]; + switch (curveType) { + case LINEAR: + float before = frames[i], value = frames[i + VALUE]; + return value + (time - before) / (frames[i + ENTRIES] - before) * (frames[i + ENTRIES + VALUE] - value); + case STEPPED: + return frames[i + VALUE]; + } + return GetBezierValue(time, i, VALUE, curveType - BEZIER); + } + } + + /// The base class for a which sets two properties. + public abstract class CurveTimeline2 : CurveTimeline { + public const int ENTRIES = 3; + internal const int VALUE1 = 1, VALUE2 = 2; + + /// The maximum number of Bezier curves. See . + /// Unique identifiers for the properties the timeline modifies. + public CurveTimeline2 (int frameCount, int bezierCount, string propertyId1, string propertyId2) + : base(frameCount, bezierCount, propertyId1, propertyId2) { + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + /// Sets the time and values for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float value1, float value2) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + VALUE1] = value1; + frames[frame + VALUE2] = value2; + } + } + + /// Changes a bone's local . + public class RotateTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public RotateTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.Rotate + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation; + return; + case MixBlend.First: + bone.rotation += (bone.data.rotation - bone.rotation) * alpha; + return; + } + return; + } + + float r = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation + r * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + r += bone.data.rotation - bone.rotation; + goto case MixBlend.Add; // Fall through. + case MixBlend.Add: + bone.rotation += r * alpha; + break; + } + } + } + + /// Changes a bone's local and . + public class TranslateTimeline : CurveTimeline2, IBoneTimeline { + readonly int boneIndex; + + public TranslateTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, // + (int)Property.X + "|" + boneIndex, // + (int)Property.Y + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x; + bone.y = bone.data.y; + return; + case MixBlend.First: + bone.x += (bone.data.x - bone.x) * alpha; + bone.y += (bone.data.y - bone.y) * alpha; + return; + } + return; + } + + float x, y; + GetCurveValue(out x, out y, time); // note: reference implementation has code inlined + + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x + x * alpha; + bone.y = bone.data.y + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.x += (bone.data.x + x - bone.x) * alpha; + bone.y += (bone.data.y + y - bone.y) * alpha; + break; + case MixBlend.Add: + bone.x += x * alpha; + bone.y += y * alpha; + break; + } + } + + public void GetCurveValue (out float x, out float y, float time) { + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + x += (frames[i + ENTRIES + VALUE1] - x) * t; + y += (frames[i + ENTRIES + VALUE2] - y) * t; + break; + case STEPPED: + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + break; + default: + x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); + y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); + break; + } + } + } + + /// Changes a bone's local . + public class TranslateXTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public TranslateXTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.X + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x; + return; + case MixBlend.First: + bone.x += (bone.data.x - bone.x) * alpha; + return; + } + return; + } + + float x = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x + x * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.x += (bone.data.x + x - bone.x) * alpha; + break; + case MixBlend.Add: + bone.x += x * alpha; + break; + } + } + } + + /// Changes a bone's local . + public class TranslateYTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public TranslateYTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.Y + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.y = bone.data.y; + return; + case MixBlend.First: + bone.y += (bone.data.y - bone.y) * alpha; + return; + } + return; + } + + float y = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.y = bone.data.y + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.y += (bone.data.y + y - bone.y) * alpha; + break; + case MixBlend.Add: + bone.y += y * alpha; + break; + } + } + } + + /// Changes a bone's local and . + public class ScaleTimeline : CurveTimeline2, IBoneTimeline { + readonly int boneIndex; + + public ScaleTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, // + (int)Property.ScaleX + "|" + boneIndex, // + (int)Property.ScaleY + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.scaleX = bone.data.scaleX; + bone.scaleY = bone.data.scaleY; + return; + case MixBlend.First: + bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; + return; + } + return; + } + + float x, y; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + x += (frames[i + ENTRIES + VALUE1] - x) * t; + y += (frames[i + ENTRIES + VALUE2] - y) * t; + break; + case STEPPED: + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + break; + default: + x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); + y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); + break; + } + x *= bone.data.scaleX; + y *= bone.data.scaleY; + + if (alpha == 1) { + if (blend == MixBlend.Add) { + bone.scaleX += x - bone.data.scaleX; + bone.scaleY += y - bone.data.scaleY; + } else { + bone.scaleX = x; + bone.scaleY = y; + } + } else { + // Mixing out uses sign of setup or current pose, else use sign of key. + float bx, by; + if (direction == MixDirection.Out) { + switch (blend) { + case MixBlend.Setup: + bx = bone.data.scaleX; + by = bone.data.scaleY; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = bone.scaleX; + by = bone.scaleY; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.Add: + bone.scaleX += (x - bone.data.scaleX) * alpha; + bone.scaleY += (y - bone.data.scaleY) * alpha; + break; + } + } else { + switch (blend) { + case MixBlend.Setup: + bx = Math.Abs(bone.data.scaleX) * Math.Sign(x); + by = Math.Abs(bone.data.scaleY) * Math.Sign(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = Math.Abs(bone.scaleX) * Math.Sign(x); + by = Math.Abs(bone.scaleY) * Math.Sign(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.Add: + bone.scaleX += (x - bone.data.scaleX) * alpha; + bone.scaleY += (y - bone.data.scaleY) * alpha; + break; + } + } + } + } + } + + /// Changes a bone's local . + public class ScaleXTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public ScaleXTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.ScaleX + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.scaleX = bone.data.scaleX; + return; + case MixBlend.First: + bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; + return; + } + return; + } + + float x = GetCurveValue(time) * bone.data.scaleX; + if (alpha == 1) { + if (blend == MixBlend.Add) + bone.scaleX += x - bone.data.scaleX; + else + bone.scaleX = x; + } else { + // Mixing out uses sign of setup or current pose, else use sign of key. + float bx; + if (direction == MixDirection.Out) { + switch (blend) { + case MixBlend.Setup: + bx = bone.data.scaleX; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = bone.scaleX; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + break; + case MixBlend.Add: + bone.scaleX += (x - bone.data.scaleX) * alpha; + break; + } + } else { + switch (blend) { + case MixBlend.Setup: + bx = Math.Abs(bone.data.scaleX) * Math.Sign(x); + bone.scaleX = bx + (x - bx) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = Math.Abs(bone.scaleX) * Math.Sign(x); + bone.scaleX = bx + (x - bx) * alpha; + break; + case MixBlend.Add: + bone.scaleX += (x - bone.data.scaleX) * alpha; + break; + } + } + } + } + } + + /// Changes a bone's local . + public class ScaleYTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public ScaleYTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.ScaleY + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.scaleY = bone.data.scaleY; + return; + case MixBlend.First: + bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; + return; + } + return; + } + + float y = GetCurveValue(time) * bone.data.scaleY; + if (alpha == 1) { + if (blend == MixBlend.Add) + bone.scaleY += y - bone.data.scaleY; + else + bone.scaleY = y; + } else { + // Mixing out uses sign of setup or current pose, else use sign of key. + float by; + if (direction == MixDirection.Out) { + switch (blend) { + case MixBlend.Setup: + by = bone.data.scaleY; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + by = bone.scaleY; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.Add: + bone.scaleY += (y - bone.data.scaleY) * alpha; + break; + } + } else { + switch (blend) { + case MixBlend.Setup: + by = Math.Abs(bone.data.scaleY) * Math.Sign(y); + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + by = Math.Abs(bone.scaleY) * Math.Sign(y); + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.Add: + bone.scaleY += (y - bone.data.scaleY) * alpha; + break; + } + } + } + } + } + + /// Changes a bone's local and . + public class ShearTimeline : CurveTimeline2, IBoneTimeline { + readonly int boneIndex; + + public ShearTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, // + (int)Property.ShearX + "|" + boneIndex, // + (int)Property.ShearY + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX; + bone.shearY = bone.data.shearY; + return; + case MixBlend.First: + bone.shearX += (bone.data.shearX - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY - bone.shearY) * alpha; + return; + } + return; + } + + float x, y; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + x += (frames[i + ENTRIES + VALUE1] - x) * t; + y += (frames[i + ENTRIES + VALUE2] - y) * t; + break; + case STEPPED: + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + break; + default: + x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); + y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); + break; + } + + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX + x * alpha; + bone.shearY = bone.data.shearY + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + break; + case MixBlend.Add: + bone.shearX += x * alpha; + bone.shearY += y * alpha; + break; + } + } + } + + /// Changes a bone's local . + public class ShearXTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public ShearXTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.ShearX + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX; + return; + case MixBlend.First: + bone.shearX += (bone.data.shearX - bone.shearX) * alpha; + return; + } + return; + } + + float x = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX + x * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; + break; + case MixBlend.Add: + bone.shearX += x * alpha; + break; + } + } + } + + /// Changes a bone's local . + public class ShearYTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public ShearYTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.ShearY + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.shearY = bone.data.shearY; + return; + case MixBlend.First: + bone.shearY += (bone.data.shearY - bone.shearY) * alpha; + return; + } + return; + } + + float y = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.shearY = bone.data.shearY + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + break; + case MixBlend.Add: + bone.shearY += y * alpha; + break; + } + } + } + + /// Changes a slot's . + public class RGBATimeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 5; + protected const int R = 1, G = 2, B = 3, A = 4; + + readonly int slotIndex; + + public RGBATimeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, // + (int)Property.RGB + "|" + slotIndex, // + (int)Property.Alpha + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + public override int FrameEntries { + get { return ENTRIES; } + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + /// Sets the time and color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float r, float g, float b, float a) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + R] = r; + frames[frame + G] = g; + frames[frame + B] = b; + frames[frame + A] = a; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + var setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = setup.r; + slot.g = setup.g; + slot.b = setup.b; + slot.a = setup.a; + return; + case MixBlend.First: + slot.r += (setup.r - slot.r) * alpha; + slot.g += (setup.g - slot.g) * alpha; + slot.b += (setup.b - slot.b) * alpha; + slot.a += (setup.a - slot.a) * alpha; + slot.ClampColor(); + return; + } + return; + } + + float r, g, b, a; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + a = frames[i + A]; + float t = (time - before) / (frames[i + ENTRIES] - before); + r += (frames[i + ENTRIES + R] - r) * t; + g += (frames[i + ENTRIES + G] - g) * t; + b += (frames[i + ENTRIES + B] - b) * t; + a += (frames[i + ENTRIES + A] - a) * t; + break; + case STEPPED: + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + a = frames[i + A]; + break; + default: + r = GetBezierValue(time, i, R, curveType - BEZIER); + g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); + b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); + a = GetBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER); + break; + } + + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + slot.a = a; + } else { + float br, bg, bb, ba; + if (blend == MixBlend.Setup) { + br = slot.data.r; + bg = slot.data.g; + bb = slot.data.b; + ba = slot.data.a; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + ba = slot.a; + } + slot.r = br + (r - br) * alpha; + slot.g = bg + (g - bg) * alpha; + slot.b = bb + (b - bb) * alpha; + slot.a = ba + (a - ba) * alpha; + } + slot.ClampColor(); + } + } + + /// Changes the RGB for a slot's . + public class RGBTimeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 4; + protected const int R = 1, G = 2, B = 3; + + readonly int slotIndex; + + public RGBTimeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, // + (int)Property.RGB + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + /// Sets the time and color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float r, float g, float b) { + frame <<= 2; + frames[frame] = time; + frames[frame + R] = r; + frames[frame + G] = g; + frames[frame + B] = b; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + var setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = setup.r; + slot.g = setup.g; + slot.b = setup.b; + return; + case MixBlend.First: + slot.r += (setup.r - slot.r) * alpha; + slot.g += (setup.g - slot.g) * alpha; + slot.b += (setup.b - slot.b) * alpha; + slot.ClampColor(); + return; + } + return; + } + + float r, g, b; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 2]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + float t = (time - before) / (frames[i + ENTRIES] - before); + r += (frames[i + ENTRIES + R] - r) * t; + g += (frames[i + ENTRIES + G] - g) * t; + b += (frames[i + ENTRIES + B] - b) * t; + break; + case STEPPED: + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + break; + default: + r = GetBezierValue(time, i, R, curveType - BEZIER); + g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); + b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); + break; + } + + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + } else { + float br, bg, bb; + if (blend == MixBlend.Setup) { + var setup = slot.data; + br = setup.r; + bg = setup.g; + bb = setup.b; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + } + slot.r = br + (r - br) * alpha; + slot.g = bg + (g - bg) * alpha; + slot.b = bb + (b - bb) * alpha; + } + slot.ClampColor(); + } + } + + /// Changes the alpha for a slot's . + public class AlphaTimeline : CurveTimeline1, ISlotTimeline { + readonly int slotIndex; + + public AlphaTimeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, (int)Property.Alpha + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + var setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.a = setup.a; + return; + case MixBlend.First: + slot.a += (setup.a - slot.a) * alpha; + slot.ClampColor(); + return; + } + return; + } + + float a = GetCurveValue(time); + if (alpha == 1) + slot.a = a; + else { + if (blend == MixBlend.Setup) slot.a = slot.data.a; + slot.a += (a - slot.a) * alpha; + } + slot.ClampColor(); + } + } + + /// Changes a slot's and for two color tinting. + public class RGBA2Timeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 8; + protected const int R = 1, G = 2, B = 3, A = 4, R2 = 5, G2 = 6, B2 = 7; + + readonly int slotIndex; + + public RGBA2Timeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, // + (int)Property.RGB + "|" + slotIndex, // + (int)Property.Alpha + "|" + slotIndex, // + (int)Property.RGB2 + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + + public override int FrameEntries { + get { + return ENTRIES; + } + } + + /// + /// The index of the slot in that will be changed when this timeline is applied. The + /// must have a dark color available. + public int SlotIndex { + get { + return slotIndex; + } + } + + /// Sets the time, light color, and dark color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float r, float g, float b, float a, float r2, float g2, float b2) { + frame <<= 3; + frames[frame] = time; + frames[frame + R] = r; + frames[frame + G] = g; + frames[frame + B] = b; + frames[frame + A] = a; + frames[frame + R2] = r2; + frames[frame + G2] = g2; + frames[frame + B2] = b2; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + SlotData setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = setup.r; + slot.g = setup.g; + slot.b = setup.b; + slot.a = setup.a; + slot.ClampColor(); + slot.r2 = setup.r2; + slot.g2 = setup.g2; + slot.b2 = setup.b2; + slot.ClampSecondColor(); + return; + case MixBlend.First: + slot.r += (slot.r - setup.r) * alpha; + slot.g += (slot.g - setup.g) * alpha; + slot.b += (slot.b - setup.b) * alpha; + slot.a += (slot.a - setup.a) * alpha; + slot.ClampColor(); + slot.r2 += (slot.r2 - setup.r2) * alpha; + slot.g2 += (slot.g2 - setup.g2) * alpha; + slot.b2 += (slot.b2 - setup.b2) * alpha; + slot.ClampSecondColor(); + return; + } + return; + } + + float r, g, b, a, r2, g2, b2; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 3]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + a = frames[i + A]; + r2 = frames[i + R2]; + g2 = frames[i + G2]; + b2 = frames[i + B2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + r += (frames[i + ENTRIES + R] - r) * t; + g += (frames[i + ENTRIES + G] - g) * t; + b += (frames[i + ENTRIES + B] - b) * t; + a += (frames[i + ENTRIES + A] - a) * t; + r2 += (frames[i + ENTRIES + R2] - r2) * t; + g2 += (frames[i + ENTRIES + G2] - g2) * t; + b2 += (frames[i + ENTRIES + B2] - b2) * t; + break; + case STEPPED: + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + a = frames[i + A]; + r2 = frames[i + R2]; + g2 = frames[i + G2]; + b2 = frames[i + B2]; + break; + default: + r = GetBezierValue(time, i, R, curveType - BEZIER); + g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); + b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); + a = GetBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER); + r2 = GetBezierValue(time, i, R2, curveType + BEZIER_SIZE * 4 - BEZIER); + g2 = GetBezierValue(time, i, G2, curveType + BEZIER_SIZE * 5 - BEZIER); + b2 = GetBezierValue(time, i, B2, curveType + BEZIER_SIZE * 6 - BEZIER); + break; + } + + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + slot.a = a; + slot.r2 = r2; + slot.g2 = g2; + slot.b2 = b2; + } else { + float br, bg, bb, ba, br2, bg2, bb2; + if (blend == MixBlend.Setup) { + br = slot.data.r; + bg = slot.data.g; + bb = slot.data.b; + ba = slot.data.a; + br2 = slot.data.r2; + bg2 = slot.data.g2; + bb2 = slot.data.b2; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + ba = slot.a; + br2 = slot.r2; + bg2 = slot.g2; + bb2 = slot.b2; + } + slot.r = br + (r - br) * alpha; + slot.g = bg + (g - bg) * alpha; + slot.b = bb + (b - bb) * alpha; + slot.a = ba + (a - ba) * alpha; + slot.r2 = br2 + (r2 - br2) * alpha; + slot.g2 = bg2 + (g2 - bg2) * alpha; + slot.b2 = bb2 + (b2 - bb2) * alpha; + } + slot.ClampColor(); + slot.ClampSecondColor(); + } + } + + /// Changes the RGB for a slot's and for two color tinting. + public class RGB2Timeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 7; + protected const int R = 1, G = 2, B = 3, R2 = 4, G2 = 5, B2 = 6; + + readonly int slotIndex; + + public RGB2Timeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, // + (int)Property.RGB + "|" + slotIndex, // + (int)Property.RGB2 + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + + public override int FrameEntries { + get { + return ENTRIES; + } + } + + /// + /// The index of the slot in that will be changed when this timeline is applied. The + /// must have a dark color available. + public int SlotIndex { + get { + return slotIndex; + } + } + + /// Sets the time, light color, and dark color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float r, float g, float b, float r2, float g2, float b2) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + R] = r; + frames[frame + G] = g; + frames[frame + B] = b; + frames[frame + R2] = r2; + frames[frame + G2] = g2; + frames[frame + B2] = b2; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + SlotData setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = setup.r; + slot.g = setup.g; + slot.b = setup.b; + slot.ClampColor(); + slot.r2 = setup.r2; + slot.g2 = setup.g2; + slot.b2 = setup.b2; + slot.ClampSecondColor(); + return; + case MixBlend.First: + slot.r += (slot.r - setup.r) * alpha; + slot.g += (slot.g - setup.g) * alpha; + slot.b += (slot.b - setup.b) * alpha; + slot.ClampColor(); + slot.r2 += (slot.r2 - setup.r2) * alpha; + slot.g2 += (slot.g2 - setup.g2) * alpha; + slot.b2 += (slot.b2 - setup.b2) * alpha; + slot.ClampSecondColor(); + return; + } + return; + } + + float r, g, b, r2, g2, b2; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + r2 = frames[i + R2]; + g2 = frames[i + G2]; + b2 = frames[i + B2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + r += (frames[i + ENTRIES + R] - r) * t; + g += (frames[i + ENTRIES + G] - g) * t; + b += (frames[i + ENTRIES + B] - b) * t; + r2 += (frames[i + ENTRIES + R2] - r2) * t; + g2 += (frames[i + ENTRIES + G2] - g2) * t; + b2 += (frames[i + ENTRIES + B2] - b2) * t; + break; + case STEPPED: + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + r2 = frames[i + R2]; + g2 = frames[i + G2]; + b2 = frames[i + B2]; + break; + default: + r = GetBezierValue(time, i, R, curveType - BEZIER); + g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); + b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); + r2 = GetBezierValue(time, i, R2, curveType + BEZIER_SIZE * 3 - BEZIER); + g2 = GetBezierValue(time, i, G2, curveType + BEZIER_SIZE * 4 - BEZIER); + b2 = GetBezierValue(time, i, B2, curveType + BEZIER_SIZE * 5 - BEZIER); + break; + } + + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + slot.r2 = r2; + slot.g2 = g2; + slot.b2 = b2; + } else { + float br, bg, bb, br2, bg2, bb2; + if (blend == MixBlend.Setup) { + SlotData setup = slot.data; + br = setup.r; + bg = setup.g; + bb = setup.b; + br2 = setup.r2; + bg2 = setup.g2; + bb2 = setup.b2; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + br2 = slot.r2; + bg2 = slot.g2; + bb2 = slot.b2; + } + slot.r = br + (r - br) * alpha; + slot.g = bg + (g - bg) * alpha; + slot.b = bb + (b - bb) * alpha; + slot.r2 = br2 + (r2 - br2) * alpha; + slot.g2 = bg2 + (g2 - bg2) * alpha; + slot.b2 = bb2 + (b2 - bb2) * alpha; + } + slot.ClampColor(); + slot.ClampSecondColor(); + } + } + + /// Changes a slot's . + public class AttachmentTimeline : Timeline, ISlotTimeline { + readonly int slotIndex; + readonly string[] attachmentNames; + + public AttachmentTimeline (int frameCount, int slotIndex) + : base(frameCount, (int)Property.Attachment + "|" + slotIndex) { + this.slotIndex = slotIndex; + attachmentNames = new String[frameCount]; + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + /// The attachment name for each frame. May contain null values to clear the attachment. + public string[] AttachmentNames { + get { + return attachmentNames; + } + } + + /// Sets the time and attachment name for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, String attachmentName) { + frames[frame] = time; + attachmentNames[frame] = attachmentName; + } + + public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + if (direction == MixDirection.Out) { + if (blend == MixBlend.Setup) SetAttachment(skeleton, slot, slot.data.attachmentName); + return; + } + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) SetAttachment(skeleton, slot, slot.data.attachmentName); + return; + } + + SetAttachment(skeleton, slot, attachmentNames[Search(frames, time)]); + } + + private void SetAttachment (Skeleton skeleton, Slot slot, string attachmentName) { + slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); + } + } + + /// Changes a slot's to deform a . + public class DeformTimeline : CurveTimeline, ISlotTimeline { + readonly int slotIndex; + readonly VertexAttachment attachment; + internal float[][] vertices; + + public DeformTimeline (int frameCount, int bezierCount, int slotIndex, VertexAttachment attachment) + : base(frameCount, bezierCount, (int)Property.Deform + "|" + slotIndex + "|" + attachment.Id) { + this.slotIndex = slotIndex; + this.attachment = attachment; + vertices = new float[frameCount][]; + } + + public int SlotIndex { + get { + return slotIndex; + } + } + /// The attachment that will be deformed. + /// + public VertexAttachment Attachment { + get { + return attachment; + } + } + + /// The vertices for each frame. + public float[][] Vertices { + get { + return vertices; + } + } + + /// Sets the time and vertices for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + /// Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. + public void SetFrame (int frame, float time, float[] vertices) { + frames[frame] = time; + this.vertices[frame] = vertices; + } + + /// Ignored (0 is used for a deform timeline). + /// Ignored (1 is used for a deform timeline). + public void setBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2, + float cy2, float time2, float value2) { + float[] curves = this.curves; + int i = FrameCount + bezier * BEZIER_SIZE; + if (value == 0) curves[frame] = BEZIER + i; + float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = cy2 * 0.03f - cy1 * 0.06f; + float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = (cy1 - cy2 + 0.33333333f) * 0.018f; + float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy; + float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = cy1 * 0.3f + tmpy + dddy * 0.16666667f; + float x = time1 + dx, y = dy; + for (int n = i + BEZIER_SIZE; i < n; i += 2) { + curves[i] = x; + curves[i + 1] = y; + dx += ddx; + dy += ddy; + ddx += dddx; + ddy += dddy; + x += dx; + y += dy; + } + } + + /// Returns the interpolated percentage for the specified time. + /// The frame before time. + private float GetCurvePercent (float time, int frame) { + float[] curves = this.curves; + int i = (int)curves[frame]; + switch (i) { + case LINEAR: + float x = frames[frame]; + return (time - x) / (frames[frame + FrameEntries] - x); + case STEPPED: + return 0; + } + i -= BEZIER; + if (curves[i] > time) { + float x = frames[frame]; + return curves[i + 1] * (time - x) / (curves[i] - x); + } + int n = i + BEZIER_SIZE; + for (i += 2; i < n; i += 2) { + if (curves[i] >= time) { + float x = curves[i - 2], y = curves[i - 1]; + return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); + } + } + { // scope added to prevent compile error "float x and y declared in enclosing scope" + float x = curves[n - 2], y = curves[n - 1]; + return y + (1 - y) * (time - x) / (frames[frame + FrameEntries] - x); + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + var vertexAttachment = slot.attachment as VertexAttachment; + if (vertexAttachment == null || vertexAttachment.DeformAttachment != attachment) return; + + var deformArray = slot.Deform; + if (deformArray.Count == 0) blend = MixBlend.Setup; + + float[][] vertices = this.vertices; + int vertexCount = vertices[0].Length; + + float[] deform; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + deformArray.Clear(); + return; + case MixBlend.First: + if (alpha == 1) { + deformArray.Clear(); + return; + } + + // Ensure size and preemptively set count. + if (deformArray.Capacity < vertexCount) deformArray.Capacity = vertexCount; + deformArray.Count = vertexCount; + deform = deformArray.Items; + + if (vertexAttachment.bones == null) { + // Unweighted vertex positions. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + deform[i] += (setupVertices[i] - deform[i]) * alpha; + } else { + // Weighted deform offsets. + alpha = 1 - alpha; + for (int i = 0; i < vertexCount; i++) + deform[i] *= alpha; + } + return; + } + return; + } + + // Ensure size and preemptively set count. + if (deformArray.Capacity < vertexCount) deformArray.Capacity = vertexCount; + deformArray.Count = vertexCount; + deform = deformArray.Items; + + if (time >= frames[frames.Length - 1]) { // Time is after last frame. + float[] lastVertices = vertices[frames.Length - 1]; + if (alpha == 1) { + if (blend == MixBlend.Add) { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + deform[i] += lastVertices[i] - setupVertices[i]; + } else { + // Weighted deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) + deform[i] += lastVertices[i]; + } + } else { + // Vertex positions or deform offsets, no alpha. + Array.Copy(lastVertices, 0, deform, 0, vertexCount); + } + } else { + switch (blend) { + case MixBlend.Setup: { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float setup = setupVertices[i]; + deform[i] = setup + (lastVertices[i] - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) + deform[i] = lastVertices[i] * alpha; + } + break; + } + case MixBlend.First: + case MixBlend.Replace: + // Vertex positions or deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) + deform[i] += (lastVertices[i] - deform[i]) * alpha; + break; + case MixBlend.Add: + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + deform[i] += (lastVertices[i] - setupVertices[i]) * alpha; + } else { + // Weighted deform offsets, alpha. + for (int i = 0; i < vertexCount; i++) + deform[i] += lastVertices[i] * alpha; + } + break; + } + } + return; + } + + int frame = Search(frames, time); + float percent = GetCurvePercent(time, frame); + float[] prevVertices = vertices[frame]; + float[] nextVertices = vertices[frame + 1]; + + if (alpha == 1) { + if (blend == MixBlend.Add) { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i]; + } + } else { + // Weighted deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += prev + (nextVertices[i] - prev) * percent; + } + } + } else { + // Vertex positions or deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] = prev + (nextVertices[i] - prev) * percent; + } + } + } else { + switch (blend) { + case MixBlend.Setup: { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i], setup = setupVertices[i]; + deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + break; + } + case MixBlend.First: + case MixBlend.Replace: { + // Vertex positions or deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha; + } + break; + } + case MixBlend.Add: + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + break; + } + } + } + } + + /// Fires an when specific animation times are reached. + public class EventTimeline : Timeline { + readonly static string[] propertyIds = { ((int)Property.Event).ToString() }; + readonly Event[] events; + + public EventTimeline (int frameCount) + : base(frameCount, propertyIds) { + events = new Event[frameCount]; + } + + /// The event for each frame. + public Event[] Events { + get { + return events; + } + } + + /// Sets the time and event for the specified frame. + /// Between 0 and frameCount, inclusive. + public void SetFrame (int frame, Event e) { + frames[frame] = e.time; + events[frame] = e; + } + + /// Fires events for frames > lastTime and <= time. + public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, + MixBlend blend, MixDirection direction) { + + if (firedEvents == null) return; + + float[] frames = this.frames; + int frameCount = frames.Length; + + if (lastTime > time) { // Fire events after last time for looped animations. + Apply(skeleton, lastTime, int.MaxValue, firedEvents, alpha, blend, direction); + lastTime = -1f; + } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. + + int i; + if (lastTime < frames[0]) + i = 0; + else { + i = Search(frames, lastTime) + 1; + float frameTime = frames[i]; + while (i > 0) { // Fire multiple events with the same frame. + if (frames[i - 1] != frameTime) break; + i--; + } + } + for (; i < frameCount && time >= frames[i]; i++) + firedEvents.Add(events[i]); + } + } + + /// Changes a skeleton's . + public class DrawOrderTimeline : Timeline { + static readonly string[] propertyIds = { ((int)Property.DrawOrder).ToString() }; + + readonly int[][] drawOrders; + + public DrawOrderTimeline (int frameCount) + : base(frameCount, propertyIds) { + drawOrders = new int[frameCount][]; + } + + /// The draw order for each frame. + /// . + public int[][] DrawOrders { + get { + return drawOrders; + } + } + + /// Sets the time and draw order for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + /// For each slot in , the index of the slot in the new draw order. May be null to use + /// setup pose draw order. + public void SetFrame (int frame, float time, int[] drawOrder) { + frames[frame] = time; + drawOrders[frame] = drawOrder; + } + + public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + + if (direction == MixDirection.Out) { + if (blend == MixBlend.Setup) Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); + return; + } + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); + return; + } + + int[] drawOrderToSetupIndex = drawOrders[Search(frames, time)]; + if (drawOrderToSetupIndex == null) + Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); + else { + Slot[] slots = skeleton.slots.Items; + Slot[] drawOrder = skeleton.drawOrder.Items; + for (int i = 0, n = drawOrderToSetupIndex.Length; i < n; i++) + drawOrder[i] = slots[drawOrderToSetupIndex[i]]; + } + } + } + + /// Changes an IK constraint's , , + /// , , and . + public class IkConstraintTimeline : CurveTimeline { + public const int ENTRIES = 6; + private const int MIX = 1, SOFTNESS = 2, BEND_DIRECTION = 3, COMPRESS = 4, STRETCH = 5; + + readonly int ikConstraintIndex; + + public IkConstraintTimeline (int frameCount, int bezierCount, int ikConstraintIndex) + : base(frameCount, bezierCount, (int)Property.IkConstraint + "|" + ikConstraintIndex) { + this.ikConstraintIndex = ikConstraintIndex; + } + + public override int FrameEntries { + get { + return ENTRIES; + } + } + + /// The index of the IK constraint slot in that will be changed when this timeline is + /// applied. + public int IkConstraintIndex { + get { + return ikConstraintIndex; + } + } + + /// Sets the time, mix, softness, bend direction, compress, and stretch for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + /// 1 or -1. + public void SetFrame (int frame, float time, float mix, float softness, int bendDirection, bool compress, + bool stretch) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + MIX] = mix; + frames[frame + SOFTNESS] = softness; + frames[frame + BEND_DIRECTION] = bendDirection; + frames[frame + COMPRESS] = compress ? 1 : 0; + frames[frame + STRETCH] = stretch ? 1 : 0; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + IkConstraint constraint = skeleton.ikConstraints.Items[ikConstraintIndex]; + if (!constraint.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.mix = constraint.data.mix; + constraint.softness = constraint.data.softness; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + return; + case MixBlend.First: + constraint.mix += (constraint.data.mix - constraint.mix) * alpha; + constraint.softness += (constraint.data.softness - constraint.softness) * alpha; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + return; + } + return; + } + + float mix, softness; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + mix = frames[i + MIX]; + softness = frames[i + SOFTNESS]; + float t = (time - before) / (frames[i + ENTRIES] - before); + mix += (frames[i + ENTRIES + MIX] - mix) * t; + softness += (frames[i + ENTRIES + SOFTNESS] - softness) * t; + break; + case STEPPED: + mix = frames[i + MIX]; + softness = frames[i + SOFTNESS]; + break; + default: + mix = GetBezierValue(time, i, MIX, curveType - BEZIER); + softness = GetBezierValue(time, i, SOFTNESS, curveType + BEZIER_SIZE - BEZIER); + break; + } + + if (blend == MixBlend.Setup) { + constraint.mix = constraint.data.mix + (mix - constraint.data.mix) * alpha; + constraint.softness = constraint.data.softness + (softness - constraint.data.softness) * alpha; + if (direction == MixDirection.Out) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = (int)frames[i + BEND_DIRECTION]; + constraint.compress = frames[i + COMPRESS] != 0; + constraint.stretch = frames[i + STRETCH] != 0; + } + } else { + constraint.mix += (mix - constraint.mix) * alpha; + constraint.softness += (softness - constraint.softness) * alpha; + if (direction == MixDirection.In) { + constraint.bendDirection = (int)frames[i + BEND_DIRECTION]; + constraint.compress = frames[i + COMPRESS] != 0; + constraint.stretch = frames[i + STRETCH] != 0; + } + } + } + } + + /// Changes a transform constraint's mixes. + public class TransformConstraintTimeline : CurveTimeline { + public const int ENTRIES = 7; + private const int ROTATE = 1, X = 2, Y = 3, SCALEX = 4, SCALEY = 5, SHEARY = 6; + + readonly int transformConstraintIndex; + + public TransformConstraintTimeline (int frameCount, int bezierCount, int transformConstraintIndex) + : base(frameCount, bezierCount, (int)Property.TransformConstraint + "|" + transformConstraintIndex) { + this.transformConstraintIndex = transformConstraintIndex; + } + + public override int FrameEntries { + get { + return ENTRIES; + } + } + + /// The index of the transform constraint slot in that will be changed when this + /// timeline is applied. + public int TransformConstraintIndex { + get { + return transformConstraintIndex; + } + } + + /// Sets the time, rotate mix, translate mix, scale mix, and shear mix for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float mixRotate, float mixX, float mixY, float mixScaleX, float mixScaleY, + float mixShearY) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + ROTATE] = mixRotate; + frames[frame + X] = mixX; + frames[frame + Y] = mixY; + frames[frame + SCALEX] = mixScaleX; + frames[frame + SCALEY] = mixScaleY; + frames[frame + SHEARY] = mixShearY; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + TransformConstraint constraint = skeleton.transformConstraints.Items[transformConstraintIndex]; + if (!constraint.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + TransformConstraintData data = constraint.data; + switch (blend) { + case MixBlend.Setup: + constraint.mixRotate = data.mixRotate; + constraint.mixX = data.mixX; + constraint.mixY = data.mixY; + constraint.mixScaleX = data.mixScaleX; + constraint.mixScaleY = data.mixScaleY; + constraint.mixShearY = data.mixShearY; + return; + case MixBlend.First: + constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha; + constraint.mixX += (data.mixX - constraint.mixX) * alpha; + constraint.mixY += (data.mixY - constraint.mixY) * alpha; + constraint.mixScaleX += (data.mixScaleX - constraint.mixScaleX) * alpha; + constraint.mixScaleY += (data.mixScaleY - constraint.mixScaleY) * alpha; + constraint.mixShearY += (data.mixShearY - constraint.mixShearY) * alpha; + return; + } + return; + } + + float rotate, x, y, scaleX, scaleY, shearY; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + rotate = frames[i + ROTATE]; + x = frames[i + X]; + y = frames[i + Y]; + scaleX = frames[i + SCALEX]; + scaleY = frames[i + SCALEY]; + shearY = frames[i + SHEARY]; + float t = (time - before) / (frames[i + ENTRIES] - before); + rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t; + x += (frames[i + ENTRIES + X] - x) * t; + y += (frames[i + ENTRIES + Y] - y) * t; + scaleX += (frames[i + ENTRIES + SCALEX] - scaleX) * t; + scaleY += (frames[i + ENTRIES + SCALEY] - scaleY) * t; + shearY += (frames[i + ENTRIES + SHEARY] - shearY) * t; + break; + case STEPPED: + rotate = frames[i + ROTATE]; + x = frames[i + X]; + y = frames[i + Y]; + scaleX = frames[i + SCALEX]; + scaleY = frames[i + SCALEY]; + shearY = frames[i + SHEARY]; + break; + default: + rotate = GetBezierValue(time, i, ROTATE, curveType - BEZIER); + x = GetBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER); + y = GetBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER); + scaleX = GetBezierValue(time, i, SCALEX, curveType + BEZIER_SIZE * 3 - BEZIER); + scaleY = GetBezierValue(time, i, SCALEY, curveType + BEZIER_SIZE * 4 - BEZIER); + shearY = GetBezierValue(time, i, SHEARY, curveType + BEZIER_SIZE * 5 - BEZIER); + break; + } + + if (blend == MixBlend.Setup) { + TransformConstraintData data = constraint.data; + constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha; + constraint.mixX = data.mixX + (x - data.mixX) * alpha; + constraint.mixY = data.mixY + (y - data.mixY) * alpha; + constraint.mixScaleX = data.mixScaleX + (scaleX - data.mixScaleX) * alpha; + constraint.mixScaleY = data.mixScaleY + (scaleY - data.mixScaleY) * alpha; + constraint.mixShearY = data.mixShearY + (shearY - data.mixShearY) * alpha; + } else { + constraint.mixRotate += (rotate - constraint.mixRotate) * alpha; + constraint.mixX += (x - constraint.mixX) * alpha; + constraint.mixY += (y - constraint.mixY) * alpha; + constraint.mixScaleX += (scaleX - constraint.mixScaleX) * alpha; + constraint.mixScaleY += (scaleY - constraint.mixScaleY) * alpha; + constraint.mixShearY += (shearY - constraint.mixShearY) * alpha; + } + } + } + + /// Changes a path constraint's . + public class PathConstraintPositionTimeline : CurveTimeline1 { + readonly int pathConstraintIndex; + + public PathConstraintPositionTimeline (int frameCount, int bezierCount, int pathConstraintIndex) + : base(frameCount, bezierCount, (int)Property.PathConstraintPosition + "|" + pathConstraintIndex) { + this.pathConstraintIndex = pathConstraintIndex; + } + + /// The index of the path constraint slot in that will be changed when this timeline + /// is applied. + public int PathConstraintIndex { + get { + return pathConstraintIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + if (!constraint.active) return; + + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.position = constraint.data.position; + return; + case MixBlend.First: + constraint.position += (constraint.data.position - constraint.position) * alpha; + return; + } + return; + } + + float position = GetCurveValue(time); + if (blend == MixBlend.Setup) + constraint.position = constraint.data.position + (position - constraint.data.position) * alpha; + else + constraint.position += (position - constraint.position) * alpha; + } + } + + /// Changes a path constraint's . + public class PathConstraintSpacingTimeline : CurveTimeline1 { + readonly int pathConstraintIndex; + + public PathConstraintSpacingTimeline (int frameCount, int bezierCount, int pathConstraintIndex) + : base(frameCount, bezierCount, (int)Property.PathConstraintSpacing + "|" + pathConstraintIndex) { + this.pathConstraintIndex = pathConstraintIndex; + } + + /// The index of the path constraint slot in that will be changed when this timeline + /// is applied. + public int PathConstraintIndex { + get { + return pathConstraintIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixBlend blend, + MixDirection direction) { + + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + if (!constraint.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.spacing = constraint.data.spacing; + return; + case MixBlend.First: + constraint.spacing += (constraint.data.spacing - constraint.spacing) * alpha; + return; + } + return; + } + + float spacing = GetCurveValue(time); + if (blend == MixBlend.Setup) + constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha; + else + constraint.spacing += (spacing - constraint.spacing) * alpha; + } + } + + /// Changes a transform constraint's , , and + /// . + public class PathConstraintMixTimeline : CurveTimeline { + public const int ENTRIES = 4; + private const int ROTATE = 1, X = 2, Y = 3; + + readonly int pathConstraintIndex; + + public PathConstraintMixTimeline (int frameCount, int bezierCount, int pathConstraintIndex) + : base(frameCount, bezierCount, (int)Property.PathConstraintMix + "|" + pathConstraintIndex) { + this.pathConstraintIndex = pathConstraintIndex; + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + /// The index of the path constraint slot in that will be changed when this timeline + /// is applied. + public int PathConstraintIndex { + get { + return pathConstraintIndex; + } + } + + /// Sets the time and color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float mixRotate, float mixX, float mixY) { + frame <<= 2; + frames[frame] = time; + frames[frame + ROTATE] = mixRotate; + frames[frame + X] = mixX; + frames[frame + Y] = mixY; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + if (!constraint.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.mixRotate = constraint.data.mixRotate; + constraint.mixX = constraint.data.mixX; + constraint.mixY = constraint.data.mixY; + return; + case MixBlend.First: + constraint.mixRotate += (constraint.data.mixRotate - constraint.mixRotate) * alpha; + constraint.mixX += (constraint.data.mixX - constraint.mixX) * alpha; + constraint.mixY += (constraint.data.mixY - constraint.mixY) * alpha; + return; + } + return; + } + + float rotate, x, y; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 2]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + rotate = frames[i + ROTATE]; + x = frames[i + X]; + y = frames[i + Y]; + float t = (time - before) / (frames[i + ENTRIES] - before); + rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t; + x += (frames[i + ENTRIES + X] - x) * t; + y += (frames[i + ENTRIES + Y] - y) * t; + break; + case STEPPED: + rotate = frames[i + ROTATE]; + x = frames[i + X]; + y = frames[i + Y]; + break; + default: + rotate = GetBezierValue(time, i, ROTATE, curveType - BEZIER); + x = GetBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER); + y = GetBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER); + break; + } + + if (blend == MixBlend.Setup) { + PathConstraintData data = constraint.data; + constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha; + constraint.mixX = data.mixX + (x - data.mixX) * alpha; + constraint.mixY = data.mixY + (y - data.mixY) * alpha; + } else { + constraint.mixRotate += (rotate - constraint.mixRotate) * alpha; + constraint.mixX += (x - constraint.mixX) * alpha; + constraint.mixY += (y - constraint.mixY) * alpha; + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/AnimationState.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/AnimationState.cs new file mode 100644 index 0000000..7a07b5a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/AnimationState.cs @@ -0,0 +1,1443 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_0_64 { + + /// + /// + /// Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies + /// multiple animations on top of each other (layering). + /// + /// See Applying Animations in the Spine Runtimes Guide. + /// + public class AnimationState { + internal static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0); + + /// 1) A previously applied timeline has set this property. + /// Result: Mix from the current pose to the timeline pose. + internal const int Subsequent = 0; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry applied after this one does not have a timeline to set this property. + /// Result: Mix from the setup pose to the timeline pose. + internal const int First = 1; + /// 1) A previously applied timeline has set this property.
+ /// 2) The next track entry to be applied does have a timeline to set this property.
+ /// 3) The next track entry after that one does not have a timeline to set this property.
+ /// Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading + /// animations that key the same property. A subsequent timeline will set this property using a mix. + internal const int HoldSubsequent = 2; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry to be applied does have a timeline to set this property. + /// 3) The next track entry after that one does not have a timeline to set this property. + /// Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations + /// that key the same property. A subsequent timeline will set this property using a mix. + internal const int HoldFirst = 3; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry to be applied does have a timeline to set this property. + /// 3) The next track entry after that one does have a timeline to set this property. + /// 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property. + /// Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than + /// 2 track entries in a row have a timeline that sets the same property. + /// Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid + /// "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A + /// (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap to the mixed + /// out position. + internal const int HoldMix = 4; + + internal const int Setup = 1, Current = 2; + + protected AnimationStateData data; + private readonly ExposedList tracks = new ExposedList(); + private readonly ExposedList events = new ExposedList(); + // difference to libgdx reference: delegates are used for event callbacks instead of 'final SnapshotArray listeners'. + internal void OnStart (TrackEntry entry) { if (Start != null) Start(entry); } + internal void OnInterrupt (TrackEntry entry) { if (Interrupt != null) Interrupt(entry); } + internal void OnEnd (TrackEntry entry) { if (End != null) End(entry); } + internal void OnDispose (TrackEntry entry) { if (Dispose != null) Dispose(entry); } + internal void OnComplete (TrackEntry entry) { if (Complete != null) Complete(entry); } + internal void OnEvent (TrackEntry entry, Event e) { if (Event != null) Event(entry, e); } + + public delegate void TrackEntryDelegate (TrackEntry trackEntry); + /// See + /// API Reference documentation pages here for details. Usage in C# and spine-unity is explained + /// here + /// on the spine-unity documentation pages. + public event TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; + + public delegate void TrackEntryEventDelegate (TrackEntry trackEntry, Event e); + public event TrackEntryEventDelegate Event; + + public void AssignEventSubscribersFrom (AnimationState src) { + Event = src.Event; + Start = src.Start; + Interrupt = src.Interrupt; + End = src.End; + Dispose = src.Dispose; + Complete = src.Complete; + } + + public void AddEventSubscribersFrom (AnimationState src) { + Event += src.Event; + Start += src.Start; + Interrupt += src.Interrupt; + End += src.End; + Dispose += src.Dispose; + Complete += src.Complete; + } + + // end of difference + private readonly EventQueue queue; // Initialized by constructor. + private readonly HashSet propertyIds = new HashSet(); + private bool animationsChanged; + private float timeScale = 1; + private int unkeyedState; + + private readonly Pool trackEntryPool = new Pool(); + + public AnimationState (AnimationStateData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = data; + this.queue = new EventQueue( + this, + delegate { this.animationsChanged = true; }, + trackEntryPool + ); + } + + /// + /// Increments the track entry , setting queued animations as current if needed. + /// delta time + public void Update (float delta) { + delta *= timeScale; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current == null) continue; + + current.animationLast = current.nextAnimationLast; + current.trackLast = current.nextTrackLast; + + float currentDelta = delta * current.timeScale; + + if (current.delay > 0) { + current.delay -= currentDelta; + if (current.delay > 0) continue; + currentDelta = -current.delay; + current.delay = 0; + } + + TrackEntry next = current.next; + if (next != null) { + // When the next entry's delay is passed, change to the next entry, preserving leftover time. + float nextTime = current.trackLast - next.delay; + if (nextTime >= 0) { + next.delay = 0; + next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale; + current.trackTime += currentDelta; + SetCurrent(i, next, true); + while (next.mixingFrom != null) { + next.mixTime += delta; + next = next.mixingFrom; + } + continue; + } + } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { + // Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom. + tracksItems[i] = null; + queue.End(current); + ClearNext(current); + continue; + } + if (current.mixingFrom != null && UpdateMixingFrom(current, delta)) { + // End mixing from entries once all have completed. + TrackEntry from = current.mixingFrom; + current.mixingFrom = null; + if (from != null) from.mixingTo = null; + while (from != null) { + queue.End(from); + from = from.mixingFrom; + } + } + + current.trackTime += currentDelta; + } + + queue.Drain(); + } + + /// Returns true when all mixing from entries are complete. + private bool UpdateMixingFrom (TrackEntry to, float delta) { + TrackEntry from = to.mixingFrom; + if (from == null) return true; + + bool finished = UpdateMixingFrom(from, delta); + + from.animationLast = from.nextAnimationLast; + from.trackLast = from.nextTrackLast; + + // Require mixTime > 0 to ensure the mixing from entry was applied at least once. + if (to.mixTime > 0 && to.mixTime >= to.mixDuration) { + // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame). + if (from.totalAlpha == 0 || to.mixDuration == 0) { + to.mixingFrom = from.mixingFrom; + if (from.mixingFrom != null) from.mixingFrom.mixingTo = to; + to.interruptAlpha = from.interruptAlpha; + queue.End(from); + } + return finished; + } + + from.trackTime += delta * from.timeScale; + to.mixTime += delta; + return false; + } + + /// + /// Poses the skeleton using the track entry animations. The animation state is not changed, so can be applied to multiple + /// skeletons to pose them identically. + /// True if any animations were applied. + public bool Apply (Skeleton skeleton) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + if (animationsChanged) AnimationsChanged(); + + ExposedList events = this.events; + bool applied = false; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current == null || current.delay > 0) continue; + applied = true; + + // Track 0 animations aren't for layering, so do not show the previously applied animations before the first key. + MixBlend blend = i == 0 ? MixBlend.First : current.mixBlend; + + // Apply mixing from entries first. + float mix = current.alpha; + if (current.mixingFrom != null) + mix *= ApplyMixingFrom(current, skeleton, blend); + else if (current.trackTime >= current.trackEnd && current.next == null) // + mix = 0; // Set to setup pose the last time the entry will be applied. + + // Apply current entry. + float animationLast = current.animationLast, animationTime = current.AnimationTime, applyTime = animationTime; + ExposedList applyEvents = events; + if (current.reverse) { + applyTime = current.animation.duration - applyTime; + applyEvents = null; + } + + int timelineCount = current.animation.timelines.Count; + Timeline[] timelines = current.animation.timelines.Items; + if ((i == 0 && mix == 1) || blend == MixBlend.Add) { + for (int ii = 0; ii < timelineCount; ii++) { + Timeline timeline = timelines[ii]; + if (timeline is AttachmentTimeline) + ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true); + else + timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.In); + } + } else { + int[] timelineMode = current.timelineMode.Items; + + bool firstFrame = current.timelinesRotation.Count != timelineCount << 1; + if (firstFrame) current.timelinesRotation.Resize(timelineCount << 1); + float[] timelinesRotation = current.timelinesRotation.Items; + + for (int ii = 0; ii < timelineCount; ii++) { + Timeline timeline = timelines[ii]; + MixBlend timelineBlend = timelineMode[ii] == AnimationState.Subsequent ? blend : MixBlend.Setup; + var rotateTimeline = timeline as RotateTimeline; + if (rotateTimeline != null) + ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, mix, timelineBlend, timelinesRotation, + ii << 1, firstFrame); + else if (timeline is AttachmentTimeline) + ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true); + else + timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.In); + } + } + QueueEvents(current, animationTime); + events.Clear(false); + current.nextAnimationLast = animationTime; + current.nextTrackLast = current.trackTime; + } + + // Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so + // subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or + // the time is before the first key). + int setupState = unkeyedState + Setup; + Slot[] slots = skeleton.slots.Items; + for (int i = 0, n = skeleton.slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.attachmentState == setupState) { + string attachmentName = slot.data.attachmentName; + slot.Attachment = (attachmentName == null ? null : skeleton.GetAttachment(slot.data.index, attachmentName)); + } + } + unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot. + + queue.Drain(); + return applied; + } + + /// Version of only applying and updating time at + /// EventTimelines for lightweight off-screen updates. + /// When set to false, only animation times of TrackEntries are updated. + // Note: This method is not part of the libgdx reference implementation. + public bool ApplyEventTimelinesOnly (Skeleton skeleton, bool issueEvents = true) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + + ExposedList events = this.events; + bool applied = false; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current == null || current.delay > 0) continue; + applied = true; + + // Apply mixing from entries first. + if (current.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(current, skeleton, issueEvents); + + // Apply current entry. + float animationLast = current.animationLast, animationTime = current.AnimationTime; + + if (issueEvents) { + int timelineCount = current.animation.timelines.Count; + Timeline[] timelines = current.animation.timelines.Items; + for (int ii = 0; ii < timelineCount; ii++) { + Timeline timeline = timelines[ii]; + if (timeline is EventTimeline) + timeline.Apply(skeleton, animationLast, animationTime, events, 1.0f, MixBlend.Setup, MixDirection.In); + } + QueueEvents(current, animationTime); + events.Clear(false); + } + current.nextAnimationLast = animationTime; + current.nextTrackLast = current.trackTime; + } + + if (issueEvents) + queue.Drain(); + return applied; + } + + private float ApplyMixingFrom (TrackEntry to, Skeleton skeleton, MixBlend blend) { + TrackEntry from = to.mixingFrom; + if (from.mixingFrom != null) ApplyMixingFrom(from, skeleton, blend); + + float mix; + if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes. + mix = 1; + if (blend == MixBlend.First) blend = MixBlend.Setup; // Tracks > 0 are transparent and can't reset to setup pose. + } else { + mix = to.mixTime / to.mixDuration; + if (mix > 1) mix = 1; + if (blend != MixBlend.First) blend = from.mixBlend; // Track 0 ignores track mix blend. + } + + bool attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; + int timelineCount = from.animation.timelines.Count; + Timeline[] timelines = from.animation.timelines.Items; + float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix); + float animationLast = from.animationLast, animationTime = from.AnimationTime, applyTime = animationTime; + ExposedList events = null; + if (from.reverse) + applyTime = from.animation.duration - applyTime; + else { + if (mix < from.eventThreshold) events = this.events; + } + + if (blend == MixBlend.Add) { + for (int i = 0; i < timelineCount; i++) + timelines[i].Apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.Out); + } else { + int[] timelineMode = from.timelineMode.Items; + TrackEntry[] timelineHoldMix = from.timelineHoldMix.Items; + + bool firstFrame = from.timelinesRotation.Count != timelineCount << 1; + if (firstFrame) from.timelinesRotation.Resize(timelineCount << 1); + float[] timelinesRotation = from.timelinesRotation.Items; + + from.totalAlpha = 0; + for (int i = 0; i < timelineCount; i++) { + Timeline timeline = timelines[i]; + MixDirection direction = MixDirection.Out; + MixBlend timelineBlend; + float alpha; + switch (timelineMode[i]) { + case AnimationState.Subsequent: + if (!drawOrder && timeline is DrawOrderTimeline) continue; + timelineBlend = blend; + alpha = alphaMix; + break; + case AnimationState.First: + timelineBlend = MixBlend.Setup; + alpha = alphaMix; + break; + case AnimationState.HoldSubsequent: + timelineBlend = blend; + alpha = alphaHold; + break; + case AnimationState.HoldFirst: + timelineBlend = MixBlend.Setup; + alpha = alphaHold; + break; + default: // HoldMix + timelineBlend = MixBlend.Setup; + TrackEntry holdMix = timelineHoldMix[i]; + alpha = alphaHold * Math.Max(0, 1 - holdMix.mixTime / holdMix.mixDuration); + break; + } + from.totalAlpha += alpha; + var rotateTimeline = timeline as RotateTimeline; + if (rotateTimeline != null) { + ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, i << 1, + firstFrame); + } else if (timeline is AttachmentTimeline) { + ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, timelineBlend, attachments); + } else { + if (drawOrder && timeline is DrawOrderTimeline && timelineBlend == MixBlend.Setup) + direction = MixDirection.In; + timeline.Apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction); + } + } + } + + if (to.mixDuration > 0) QueueEvents(from, animationTime); + this.events.Clear(false); + from.nextAnimationLast = animationTime; + from.nextTrackLast = from.trackTime; + + return mix; + } + + /// Version of only applying and updating time at + /// EventTimelines for lightweight off-screen updates. + /// When set to false, only animation times of TrackEntries are updated. + // Note: This method is not part of the libgdx reference implementation. + private float ApplyMixingFromEventTimelinesOnly (TrackEntry to, Skeleton skeleton, bool issueEvents) { + TrackEntry from = to.mixingFrom; + if (from.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(from, skeleton, issueEvents); + + + float mix; + if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes. + mix = 1; + } else { + mix = to.mixTime / to.mixDuration; + if (mix > 1) mix = 1; + } + + ExposedList eventBuffer = mix < from.eventThreshold ? this.events : null; + if (eventBuffer == null) return mix; + + float animationLast = from.animationLast, animationTime = from.AnimationTime; + if (issueEvents) { + int timelineCount = from.animation.timelines.Count; + Timeline[] timelines = from.animation.timelines.Items; + for (int i = 0; i < timelineCount; i++) { + Timeline timeline = timelines[i]; + if (timeline is EventTimeline) + timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, 0, MixBlend.Setup, MixDirection.Out); + } + + if (to.mixDuration > 0) QueueEvents(from, animationTime); + this.events.Clear(false); + } + from.nextAnimationLast = animationTime; + from.nextTrackLast = from.trackTime; + + return mix; + } + + /// Applies the attachment timeline and sets . + /// False when: 1) the attachment timeline is mixing out, 2) mix < attachmentThreshold, and 3) the timeline + /// is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent + /// timelines see any deform. + private void ApplyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend, + bool attachments) { + + Slot slot = skeleton.slots.Items[timeline.SlotIndex]; + if (!slot.bone.active) return; + + float[] frames = timeline.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) + SetAttachment(skeleton, slot, slot.data.attachmentName, attachments); + } else + SetAttachment(skeleton, slot, timeline.AttachmentNames[Timeline.Search(frames, time)], attachments); + + // If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later. + if (slot.attachmentState <= unkeyedState) slot.attachmentState = unkeyedState + Setup; + } + + private void SetAttachment (Skeleton skeleton, Slot slot, String attachmentName, bool attachments) { + slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slot.data.index, attachmentName); + if (attachments) slot.attachmentState = unkeyedState + Current; + } + + /// + /// Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest + /// the first time the mixing was applied. + static private void ApplyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, MixBlend blend, + float[] timelinesRotation, int i, bool firstFrame) { + + if (firstFrame) timelinesRotation[i] = 0; + + if (alpha == 1) { + timeline.Apply(skeleton, 0, time, null, 1, blend, MixDirection.In); + return; + } + + Bone bone = skeleton.bones.Items[timeline.BoneIndex]; + if (!bone.active) return; + + float[] frames = timeline.frames; + float r1, r2; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation; + goto default; // Fall through. + default: + return; + case MixBlend.First: + r1 = bone.rotation; + r2 = bone.data.rotation; + break; + } + } else { + r1 = blend == MixBlend.Setup ? bone.data.rotation : bone.rotation; + r2 = bone.data.rotation + timeline.GetCurveValue(time); + } + + // Mix between rotations using the direction of the shortest route on the first frame. + float total, diff = r2 - r1; + diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360; + if (diff == 0) { + total = timelinesRotation[i]; + } else { + float lastTotal, lastDiff; + if (firstFrame) { + lastTotal = 0; + lastDiff = diff; + } else { + lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops. + lastDiff = timelinesRotation[i + 1]; // Difference between bones. + } + bool current = diff > 0, dir = lastTotal >= 0; + // Detect cross at 0 (not 180). + if (Math.Sign(lastDiff) != Math.Sign(diff) && Math.Abs(lastDiff) <= 90) { + // A cross after a 360 rotation is a loop. + if (Math.Abs(lastTotal) > 180) lastTotal += 360 * Math.Sign(lastTotal); + dir = current; + } + total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal. + if (dir != current) total += 360 * Math.Sign(lastTotal); + timelinesRotation[i] = total; + } + timelinesRotation[i + 1] = diff; + bone.rotation = r1 + total * alpha; + } + + private void QueueEvents (TrackEntry entry, float animationTime) { + float animationStart = entry.animationStart, animationEnd = entry.animationEnd; + float duration = animationEnd - animationStart; + float trackLastWrapped = entry.trackLast % duration; + + // Queue events before complete. + Event[] eventsItems = this.events.Items; + int i = 0, n = events.Count; + for (; i < n; i++) { + Event e = eventsItems[i]; + if (e.time < trackLastWrapped) break; + if (e.time > animationEnd) continue; // Discard events outside animation start/end. + queue.Event(entry, e); + } + + // Queue complete if completed a loop iteration or the animation. + bool complete = false; + if (entry.loop) + complete = duration == 0 || (trackLastWrapped > entry.trackTime % duration); + else + complete = animationTime >= animationEnd && entry.animationLast < animationEnd; + if (complete) queue.Complete(entry); + + // Queue events after complete. + for (; i < n; i++) { + Event e = eventsItems[i]; + if (e.time < animationStart) continue; // Discard events outside animation start/end. + queue.Event(entry, eventsItems[i]); + } + } + + /// + /// Removes all animations from all tracks, leaving skeletons in their current pose. + /// + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their current pose. + /// + public void ClearTracks () { + bool oldDrainDisabled = queue.drainDisabled; + queue.drainDisabled = true; + for (int i = 0, n = tracks.Count; i < n; i++) { + ClearTrack(i); + } + tracks.Clear(); + queue.drainDisabled = oldDrainDisabled; + queue.Drain(); + } + + /// + /// Removes all animations from the track, leaving skeletons in their current pose. + /// + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their current pose. + /// + public void ClearTrack (int trackIndex) { + if (trackIndex >= tracks.Count) return; + TrackEntry current = tracks.Items[trackIndex]; + if (current == null) return; + + queue.End(current); + + ClearNext(current); + + TrackEntry entry = current; + while (true) { + TrackEntry from = entry.mixingFrom; + if (from == null) break; + queue.End(from); + entry.mixingFrom = null; + entry.mixingTo = null; + entry = from; + } + + tracks.Items[current.trackIndex] = null; + + queue.Drain(); + } + + /// Sets the active TrackEntry for a given track number. + private void SetCurrent (int index, TrackEntry current, bool interrupt) { + TrackEntry from = ExpandToIndex(index); + tracks.Items[index] = current; + current.previous = null; + + if (from != null) { + if (interrupt) queue.Interrupt(from); + current.mixingFrom = from; + from.mixingTo = current; + current.mixTime = 0; + + // Store the interrupted mix percentage. + if (from.mixingFrom != null && from.mixDuration > 0) + current.interruptAlpha *= Math.Min(1, from.mixTime / from.mixDuration); + + from.timelinesRotation.Clear(); // Reset rotation for mixing out, in case entry was mixed in. + } + + queue.Start(current); // triggers AnimationsChanged + } + + /// Sets an animation by name. + public TrackEntry SetAnimation (int trackIndex, string animationName, bool loop) { + Animation animation = data.skeletonData.FindAnimation(animationName); + if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName"); + return SetAnimation(trackIndex, animation, loop); + } + + /// Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never + /// applied to a skeleton, it is replaced (not mixed from). + /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + /// duration. In either case determines when the track is cleared. + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) { + if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); + bool interrupt = true; + TrackEntry current = ExpandToIndex(trackIndex); + if (current != null) { + if (current.nextTrackLast == -1) { + // Don't mix from an entry that was never applied. + tracks.Items[trackIndex] = current.mixingFrom; + queue.Interrupt(current); + queue.End(current); + ClearNext(current); + current = current.mixingFrom; + interrupt = false; // mixingFrom is current again, but don't interrupt it twice. + } else + ClearNext(current); + } + TrackEntry entry = NewTrackEntry(trackIndex, animation, loop, current); + SetCurrent(trackIndex, entry, interrupt); + queue.Drain(); + return entry; + } + + /// Queues an animation by name. + /// + public TrackEntry AddAnimation (int trackIndex, string animationName, bool loop, float delay) { + Animation animation = data.skeletonData.FindAnimation(animationName); + if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName"); + return AddAnimation(trackIndex, animation, loop, delay); + } + + /// Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is + /// equivalent to calling . + /// + /// If > 0, sets . If <= 0, the delay set is the duration of the previous track entry + /// minus any mix duration (from the plus the specified Delay (ie the mix + /// ends at (Delay = 0) or before (Delay < 0) the previous track entry duration). If the + /// previous entry is looping, its next loop completion is used instead of its duration. + /// + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) { + if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); + + TrackEntry last = ExpandToIndex(trackIndex); + if (last != null) { + while (last.next != null) + last = last.next; + } + + TrackEntry entry = NewTrackEntry(trackIndex, animation, loop, last); + + if (last == null) { + SetCurrent(trackIndex, entry, true); + queue.Drain(); + } else { + last.next = entry; + entry.previous = last; + if (delay <= 0) delay += last.TrackComplete - entry.mixDuration; + } + + entry.delay = delay; + return entry; + } + + /// + /// Sets an empty animation for a track, discarding any queued animations, and sets the track entry's + /// . An empty animation has no timelines and serves as a placeholder for mixing in or out. + /// + /// Mixing out is done by setting an empty animation with a mix duration using either , + /// , or . Mixing to an empty animation causes + /// the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation + /// transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of + /// 0 still mixes out over one frame. + /// + /// Mixing in is done by first setting an empty animation, then adding an animation using + /// with the desired delay (an empty animation has a duration of 0) and on + /// the returned track entry, set the . Mixing from an empty animation causes the new + /// animation to be applied more and more over the mix duration. Properties keyed in the new animation transition from the value + /// from lower tracks or from the setup pose value if no lower tracks key the property to the value keyed in the new + /// animation. + public TrackEntry SetEmptyAnimation (int trackIndex, float mixDuration) { + TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false); + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /// + /// Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's + /// . If the track is empty, it is equivalent to calling + /// . + /// + /// Track number. + /// Mix duration. + /// If > 0, sets . If <= 0, the delay set is the duration of the previous track entry + /// minus any mix duration plus the specified Delay (ie the mix ends at (Delay = 0) or + /// before (Delay < 0) the previous track entry duration). If the previous entry is looping, its next + /// loop completion is used instead of its duration. + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + /// + public TrackEntry AddEmptyAnimation (int trackIndex, float mixDuration, float delay) { + TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay); + if (delay <= 0) entry.delay += entry.mixDuration - mixDuration; + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /// + /// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix + /// duration. + public void SetEmptyAnimations (float mixDuration) { + bool oldDrainDisabled = queue.drainDisabled; + queue.drainDisabled = true; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current != null) SetEmptyAnimation(current.trackIndex, mixDuration); + } + queue.drainDisabled = oldDrainDisabled; + queue.Drain(); + } + + private TrackEntry ExpandToIndex (int index) { + if (index < tracks.Count) return tracks.Items[index]; + tracks.Resize(index + 1); + return null; + } + + /// Object-pooling version of new TrackEntry. Obtain an unused TrackEntry from the pool and clear/initialize its values. + /// May be null. + private TrackEntry NewTrackEntry (int trackIndex, Animation animation, bool loop, TrackEntry last) { + TrackEntry entry = trackEntryPool.Obtain(); + entry.trackIndex = trackIndex; + entry.animation = animation; + entry.loop = loop; + entry.holdPrevious = false; + + entry.eventThreshold = 0; + entry.attachmentThreshold = 0; + entry.drawOrderThreshold = 0; + + entry.animationStart = 0; + entry.animationEnd = animation.Duration; + entry.animationLast = -1; + entry.nextAnimationLast = -1; + + entry.delay = 0; + entry.trackTime = 0; + entry.trackLast = -1; + entry.nextTrackLast = -1; + entry.trackEnd = float.MaxValue; + entry.timeScale = 1; + + entry.alpha = 1; + entry.interruptAlpha = 1; + entry.mixTime = 0; + entry.mixDuration = last == null ? 0 : data.GetMix(last.animation, animation); + entry.mixBlend = MixBlend.Replace; + return entry; + } + + /// Removes the next entry and all entries after it for the specified entry. + public void ClearNext (TrackEntry entry) { + TrackEntry next = entry.next; + while (next != null) { + queue.Dispose(next); + next = next.next; + } + entry.next = null; + } + + private void AnimationsChanged () { + animationsChanged = false; + + // Process in the order that animations are applied. + propertyIds.Clear(); + int n = tracks.Count; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0; i < n; i++) { + TrackEntry entry = tracksItems[i]; + if (entry == null) continue; + while (entry.mixingFrom != null) // Move to last entry, then iterate in reverse. + entry = entry.mixingFrom; + do { + if (entry.mixingTo == null || entry.mixBlend != MixBlend.Add) ComputeHold(entry); + entry = entry.mixingTo; + } while (entry != null); + } + } + + private void ComputeHold (TrackEntry entry) { + TrackEntry to = entry.mixingTo; + Timeline[] timelines = entry.animation.timelines.Items; + int timelinesCount = entry.animation.timelines.Count; + int[] timelineMode = entry.timelineMode.Resize(timelinesCount).Items; + entry.timelineHoldMix.Clear(); + TrackEntry[] timelineHoldMix = entry.timelineHoldMix.Resize(timelinesCount).Items; + HashSet propertyIds = this.propertyIds; + + if (to != null && to.holdPrevious) { + for (int i = 0; i < timelinesCount; i++) + timelineMode[i] = propertyIds.AddAll(timelines[i].PropertyIds) ? AnimationState.HoldFirst : AnimationState.HoldSubsequent; + + return; + } + + // outer: + for (int i = 0; i < timelinesCount; i++) { + Timeline timeline = timelines[i]; + String[] ids = timeline.PropertyIds; + if (!propertyIds.AddAll(ids)) + timelineMode[i] = AnimationState.Subsequent; + else if (to == null || timeline is AttachmentTimeline || timeline is DrawOrderTimeline + || timeline is EventTimeline || !to.animation.HasTimeline(ids)) { + timelineMode[i] = AnimationState.First; + } else { + for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) { + if (next.animation.HasTimeline(ids)) continue; + if (next.mixDuration > 0) { + timelineMode[i] = AnimationState.HoldMix; + timelineHoldMix[i] = next; + goto continue_outer; // continue outer; + } + break; + } + timelineMode[i] = AnimationState.HoldFirst; + } + continue_outer: { } + } + } + + /// The track entry for the animation currently playing on the track, or null if no animation is currently playing. + public TrackEntry GetCurrent (int trackIndex) { + if (trackIndex >= tracks.Count) return null; + return tracks.Items[trackIndex]; + } + + /// Discards all listener notifications that have not yet been delivered. This can be useful to call from an + /// AnimationState event subscriber when it is known that further notifications that may have been already queued for delivery + /// are not wanted because new animations are being set. + public void ClearListenerNotifications () { + queue.Clear(); + } + + /// + /// Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower + /// or faster. Defaults to 1. + /// + /// See TrackEntry for affecting a single animation. + /// + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + /// The AnimationStateData to look up mix durations. + public AnimationStateData Data { + get { + return data; + } + set { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = value; + } + } + + /// A list of tracks that have animations, which may contain nulls. + public ExposedList Tracks { get { return tracks; } } + + override public string ToString () { + var buffer = new System.Text.StringBuilder(); + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry entry = tracksItems[i]; + if (entry == null) continue; + if (buffer.Length > 0) buffer.Append(", "); + buffer.Append(entry.ToString()); + } + if (buffer.Length == 0) return ""; + return buffer.ToString(); + } + } + + /// + /// + /// Stores settings and other state for the playback of an animation on an track. + /// + /// References to a track entry must not be kept after the event occurs. + /// + public class TrackEntry : Pool.IPoolable { + internal Animation animation; + + internal TrackEntry previous, next, mixingFrom, mixingTo; + // difference to libgdx reference: delegates are used for event callbacks instead of 'AnimationStateListener listener'. + /// See + /// API Reference documentation pages here for details. Usage in C# and spine-unity is explained + /// here + /// on the spine-unity documentation pages. + public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; + public event AnimationState.TrackEntryEventDelegate Event; + internal void OnStart () { if (Start != null) Start(this); } + internal void OnInterrupt () { if (Interrupt != null) Interrupt(this); } + internal void OnEnd () { if (End != null) End(this); } + internal void OnDispose () { if (Dispose != null) Dispose(this); } + internal void OnComplete () { if (Complete != null) Complete(this); } + internal void OnEvent (Event e) { if (Event != null) Event(this, e); } + + internal int trackIndex; + + internal bool loop, holdPrevious, reverse; + internal float eventThreshold, attachmentThreshold, drawOrderThreshold; + internal float animationStart, animationEnd, animationLast, nextAnimationLast; + internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f; + internal float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha; + internal MixBlend mixBlend = MixBlend.Replace; + internal readonly ExposedList timelineMode = new ExposedList(); + internal readonly ExposedList timelineHoldMix = new ExposedList(); + internal readonly ExposedList timelinesRotation = new ExposedList(); + + // IPoolable.Reset() + public void Reset () { + previous = null; + next = null; + mixingFrom = null; + mixingTo = null; + animation = null; + // replaces 'listener = null;' since delegates are used for event callbacks + Start = null; + Interrupt = null; + End = null; + Dispose = null; + Complete = null; + Event = null; + timelineMode.Clear(); + timelineHoldMix.Clear(); + timelinesRotation.Clear(); + } + + /// The index of the track where this entry is either current or queued. + /// + public int TrackIndex { get { return trackIndex; } } + + /// The animation to apply for this track entry. + public Animation Animation { get { return animation; } } + + /// + /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + /// duration. + public bool Loop { get { return loop; } set { loop = value; } } + + /// + /// + /// Seconds to postpone playing the animation. When this track entry is the current track entry, Delay + /// postpones incrementing the . When this track entry is queued, Delay is the time from + /// the start of the previous animation to when this track entry will become the current track entry (ie when the previous + /// track entry >= this track entry's Delay). + /// + /// affects the delay. + /// + /// When using with a delay <= 0, the delay + /// is set using the mix duration from the . If is set afterward, the delay + /// may need to be adjusted. + public float Delay { get { return delay; } set { delay = value; } } + + /// + /// Current time in seconds this track entry has been the current track entry. The track time determines + /// . The track time can be set to start the animation at a time other than 0, without affecting + /// looping. + public float TrackTime { get { return trackTime; } set { trackTime = value; } } + + /// + /// + /// The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float + /// value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time + /// is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the + /// properties keyed by the animation are set to the setup pose and the track is cleared. + /// + /// It may be desired to use rather than have the animation + /// abruptly cease being applied. + /// + public float TrackEnd { get { return trackEnd; } set { trackEnd = value; } } + + /// + /// If this track entry is non-looping, the track time in seconds when is reached, or the current + /// if it has already been reached. If this track entry is looping, the track time when this + /// animation will reach its next (the next loop completion). + public float TrackComplete { + get { + float duration = animationEnd - animationStart; + if (duration != 0) { + if (loop) return duration * (1 + (int)(trackTime / duration)); // Completion of next loop. + if (trackTime < duration) return duration; // Before duration. + } + return trackTime; // Next update. + } + } + + /// + /// + /// Seconds when this animation starts, both initially and after looping. Defaults to 0. + /// + /// When changing the AnimationStart time, it often makes sense to set to the same + /// value to prevent timeline keys before the start time from triggering. + /// + public float AnimationStart { get { return animationStart; } set { animationStart = value; } } + + /// + /// Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will + /// loop back to at this time. Defaults to the animation . + /// + public float AnimationEnd { get { return animationEnd; } set { animationEnd = value; } } + + /// + /// The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this + /// animation is applied, event timelines will fire all events between the AnimationLast time (exclusive) and + /// AnimationTime (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation + /// is applied. + public float AnimationLast { + get { return animationLast; } + set { + animationLast = value; + nextAnimationLast = value; + } + } + + /// + /// Uses to compute the AnimationTime, which is between + /// and . When the TrackTime is 0, the AnimationTime is equal to the + /// AnimationStart time. + /// + public float AnimationTime { + get { + if (loop) { + float duration = animationEnd - animationStart; + if (duration == 0) return animationStart; + return (trackTime % duration) + animationStart; + } + return Math.Min(trackTime + animationStart, animationEnd); + } + } + + /// + /// + /// Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or + /// faster. Defaults to 1. + /// + /// Values < 0 are not supported. To play an animation in reverse, use . + /// + /// is not affected by track entry time scale, so may need to be adjusted to + /// match the animation speed. + /// + /// When using with a Delay <= 0, the + /// is set using the mix duration from the , assuming time scale to be 1. If + /// the time scale is not 1, the delay may need to be adjusted. + /// + /// See AnimationState for affecting all animations. + /// + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + /// + /// + /// Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults + /// to 1, which overwrites the skeleton's current pose with this animation. + /// + /// Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to + /// use alpha on track 0 if the skeleton pose is from the last frame render. + /// + public float Alpha { get { return alpha; } set { alpha = value; } } + + public float InterruptAlpha { get { return interruptAlpha; } } + + /// + /// When the mix percentage ( / ) is less than the + /// EventThreshold, event timelines are applied while this animation is being mixed out. Defaults to 0, so event + /// timelines are not applied while this animation is being mixed out. + /// + public float EventThreshold { get { return eventThreshold; } set { eventThreshold = value; } } + + /// + /// When the mix percentage ( / ) is less than the + /// AttachmentThreshold, attachment timelines are applied while this animation is being mixed out. Defaults to + /// 0, so attachment timelines are not applied while this animation is being mixed out. + /// + public float AttachmentThreshold { get { return attachmentThreshold; } set { attachmentThreshold = value; } } + + /// + /// When the mix percentage ( / ) is less than the + /// DrawOrderThreshold, draw order timelines are applied while this animation is being mixed out. Defaults to 0, + /// so draw order timelines are not applied while this animation is being mixed out. + /// + public float DrawOrderThreshold { get { return drawOrderThreshold; } set { drawOrderThreshold = value; } } + + /// + /// The animation queued to start after this animation, or null if there is none. next makes up a doubly linked + /// list. + /// + /// See to truncate the list. + public TrackEntry Next { get { return next; } } + + /// + /// The animation queued to play before this animation, or null. previous makes up a doubly linked list. + public TrackEntry Previous { get { return previous; } } + + /// + /// Returns true if at least one loop has been completed. + /// + public bool IsComplete { + get { return trackTime >= animationEnd - animationStart; } + } + + /// + /// Seconds from 0 to the when mixing from the previous animation to this animation. May be + /// slightly more than MixDuration when the mix is complete. + public float MixTime { get { return mixTime; } set { mixTime = value; } } + + /// + /// + /// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData + /// based on the animation before this animation (if any). + /// + /// The MixDuration can be set manually rather than use the value from + /// . In that case, the MixDuration can be set for a new + /// track entry only before is first called. + /// + /// When using with a Delay <= 0, the + /// is set using the mix duration from the . If mixDuration is set + /// afterward, the delay may need to be adjusted. For example: + /// entry.Delay = entry.previous.TrackComplete - entry.MixDuration; + /// + public float MixDuration { get { return mixDuration; } set { mixDuration = value; } } + + /// + /// + /// Controls how properties keyed in the animation are mixed with lower tracks. Defaults to . + /// + /// Track entries on track 0 ignore this setting and always use . + /// + /// The MixBlend can be set for a new track entry only before is first + /// called. + /// + public MixBlend MixBlend { get { return mixBlend; } set { mixBlend = value; } } + + /// + /// The track entry for the previous animation when mixing from the previous animation to this animation, or null if no + /// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a linked list. + public TrackEntry MixingFrom { get { return mixingFrom; } } + + /// + /// The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is + /// currently occuring. When mixing to multiple animations, MixingTo makes up a linked list. + public TrackEntry MixingTo { get { return mixingTo; } } + + /// + /// + /// If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead + /// of being mixed out. + /// + /// When mixing between animations that key the same property, if a lower track also keys that property then the value will + /// briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0% + /// while the second animation mixes from 0% to 100%. Setting HoldPrevious to true applies the first animation + /// at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which + /// keys the property, only when a higher track also keys the property. + /// + /// Snapping will occur if HoldPrevious is true and this animation does not key all the same properties as the + /// previous animation. + /// + public bool HoldPrevious { get { return holdPrevious; } set { holdPrevious = value; } } + + /// + /// If true, the animation will be applied in reverse. Events are not fired when an animation is applied in reverse. + public bool Reverse { get { return reverse; } set { reverse = value; } } + + /// Returns true if this entry is for the empty animation. See , + /// , and . + public bool IsEmptyAnimation { get { return animation == AnimationState.EmptyAnimation; } } + + /// + /// + /// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the + /// long way around when using and starting animations on other tracks. + /// + /// Mixing with involves finding a rotation between two others, which has two possible solutions: + /// the short way or the long way around. The two rotations likely change over time, so which direction is the short or long + /// way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the + /// long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. + /// + public void ResetRotationDirections () { + timelinesRotation.Clear(); + } + + override public string ToString () { + return animation == null ? "" : animation.name; + } + + // Note: This method is required by SpineAnimationStateMixerBehaviour, + // which is part of the timeline extension package. Thus the internal member variable + // nextTrackLast is not accessible. We favor providing this method + // over exposing nextTrackLast as public property, which would rather confuse users. + public void AllowImmediateQueue () { + if (nextTrackLast < 0) nextTrackLast = 0; + } + } + + class EventQueue { + private readonly List eventQueueEntries = new List(); + internal bool drainDisabled; + + private readonly AnimationState state; + private readonly Pool trackEntryPool; + internal event Action AnimationsChanged; + + internal EventQueue (AnimationState state, Action HandleAnimationsChanged, Pool trackEntryPool) { + this.state = state; + this.AnimationsChanged += HandleAnimationsChanged; + this.trackEntryPool = trackEntryPool; + } + + internal void Start (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Start, entry)); + if (AnimationsChanged != null) AnimationsChanged(); + } + + internal void Interrupt (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Interrupt, entry)); + } + + internal void End (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.End, entry)); + if (AnimationsChanged != null) AnimationsChanged(); + } + + internal void Dispose (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Dispose, entry)); + } + + internal void Complete (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Complete, entry)); + } + + internal void Event (TrackEntry entry, Event e) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Event, entry, e)); + } + + /// Raises all events in the queue and drains the queue. + internal void Drain () { + if (drainDisabled) return; + drainDisabled = true; + + List eventQueueEntries = this.eventQueueEntries; + AnimationState state = this.state; + + // Don't cache eventQueueEntries.Count so callbacks can queue their own events (eg, call SetAnimation in AnimationState_Complete). + for (int i = 0; i < eventQueueEntries.Count; i++) { + EventQueueEntry queueEntry = eventQueueEntries[i]; + TrackEntry trackEntry = queueEntry.entry; + + switch (queueEntry.type) { + case EventType.Start: + trackEntry.OnStart(); + state.OnStart(trackEntry); + break; + case EventType.Interrupt: + trackEntry.OnInterrupt(); + state.OnInterrupt(trackEntry); + break; + case EventType.End: + trackEntry.OnEnd(); + state.OnEnd(trackEntry); + goto case EventType.Dispose; // Fall through. (C#) + case EventType.Dispose: + trackEntry.OnDispose(); + state.OnDispose(trackEntry); + trackEntryPool.Free(trackEntry); + break; + case EventType.Complete: + trackEntry.OnComplete(); + state.OnComplete(trackEntry); + break; + case EventType.Event: + trackEntry.OnEvent(queueEntry.e); + state.OnEvent(trackEntry, queueEntry.e); + break; + } + } + eventQueueEntries.Clear(); + + drainDisabled = false; + } + + internal void Clear () { + eventQueueEntries.Clear(); + } + + struct EventQueueEntry { + public EventType type; + public TrackEntry entry; + public Event e; + + public EventQueueEntry (EventType eventType, TrackEntry trackEntry, Event e = null) { + this.type = eventType; + this.entry = trackEntry; + this.e = e; + } + } + + enum EventType { + Start, Interrupt, End, Dispose, Complete, Event + } + } + + class Pool where T : class, new() { + public readonly int max; + readonly Stack freeObjects; + + public int Count { get { return freeObjects.Count; } } + public int Peak { get; private set; } + + public Pool (int initialCapacity = 16, int max = int.MaxValue) { + freeObjects = new Stack(initialCapacity); + this.max = max; + } + + public T Obtain () { + return freeObjects.Count == 0 ? new T() : freeObjects.Pop(); + } + + public void Free (T obj) { + if (obj == null) throw new ArgumentNullException("obj", "obj cannot be null"); + if (freeObjects.Count < max) { + freeObjects.Push(obj); + Peak = Math.Max(Peak, freeObjects.Count); + } + Reset(obj); + } + + public void Clear () { + freeObjects.Clear(); + } + + protected void Reset (T obj) { + var poolable = obj as IPoolable; + if (poolable != null) poolable.Reset(); + } + + public interface IPoolable { + void Reset (); + } + } + + public static class HashSetExtensions { + public static bool AddAll (this HashSet set, T[] addSet) { + bool anyItemAdded = false; + for (int i = 0, n = addSet.Length; i < n; ++i) { + T item = addSet[i]; + anyItemAdded |= set.Add(item); + } + return anyItemAdded; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/AnimationStateData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/AnimationStateData.cs new file mode 100644 index 0000000..9fd9b26 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/AnimationStateData.cs @@ -0,0 +1,114 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_0_64 { + + /// Stores mix (crossfade) durations to be applied when AnimationState animations are changed. + public class AnimationStateData { + internal SkeletonData skeletonData; + readonly Dictionary animationToMixTime = new Dictionary(AnimationPairComparer.Instance); + internal float defaultMix; + + /// The SkeletonData to look up animations when they are specified by name. + public SkeletonData SkeletonData { get { return skeletonData; } } + + /// + /// The mix duration to use when no mix duration has been specifically defined between two animations. + public float DefaultMix { get { return defaultMix; } set { defaultMix = value; } } + + public AnimationStateData (SkeletonData skeletonData) { + if (skeletonData == null) throw new ArgumentException("skeletonData cannot be null.", "skeletonData"); + this.skeletonData = skeletonData; + } + + /// Sets a mix duration by animation names. + public void SetMix (string fromName, string toName, float duration) { + Animation from = skeletonData.FindAnimation(fromName); + if (from == null) throw new ArgumentException("Animation not found: " + fromName, "fromName"); + Animation to = skeletonData.FindAnimation(toName); + if (to == null) throw new ArgumentException("Animation not found: " + toName, "toName"); + SetMix(from, to, duration); + } + + /// Sets a mix duration when changing from the specified animation to the other. + /// See TrackEntry.MixDuration. + public void SetMix (Animation from, Animation to, float duration) { + if (from == null) throw new ArgumentNullException("from", "from cannot be null."); + if (to == null) throw new ArgumentNullException("to", "to cannot be null."); + AnimationPair key = new AnimationPair(from, to); + animationToMixTime.Remove(key); + animationToMixTime.Add(key, duration); + } + + /// + /// The mix duration to use when changing from the specified animation to the other, + /// or the DefaultMix if no mix duration has been set. + /// + public float GetMix (Animation from, Animation to) { + if (from == null) throw new ArgumentNullException("from", "from cannot be null."); + if (to == null) throw new ArgumentNullException("to", "to cannot be null."); + AnimationPair key = new AnimationPair(from, to); + float duration; + if (animationToMixTime.TryGetValue(key, out duration)) return duration; + return defaultMix; + } + + public struct AnimationPair { + public readonly Animation a1; + public readonly Animation a2; + + public AnimationPair (Animation a1, Animation a2) { + this.a1 = a1; + this.a2 = a2; + } + + public override string ToString () { + return a1.name + "->" + a2.name; + } + } + + // Avoids boxing in the dictionary. + public class AnimationPairComparer : IEqualityComparer { + public static readonly AnimationPairComparer Instance = new AnimationPairComparer(); + + bool IEqualityComparer.Equals (AnimationPair x, AnimationPair y) { + return ReferenceEquals(x.a1, y.a1) && ReferenceEquals(x.a2, y.a2); + } + + int IEqualityComparer.GetHashCode (AnimationPair obj) { + // from Tuple.CombineHashCodes // return (((h1 << 5) + h1) ^ h2); + int h1 = obj.a1.GetHashCode(); + return (((h1 << 5) + h1) ^ obj.a2.GetHashCode()); + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Atlas.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Atlas.cs new file mode 100644 index 0000000..906b8c8 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Atlas.cs @@ -0,0 +1,364 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace Spine4_0_64 { + public class Atlas : IEnumerable { + readonly List pages = new List(); + List regions = new List(); + TextureLoader textureLoader; + + #region IEnumerable implementation + public IEnumerator GetEnumerator () { + return regions.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () { + return regions.GetEnumerator(); + } + #endregion + + public List Regions { get { return regions; } } + public List Pages { get { return pages; } } + +#if !(IS_UNITY) +#if WINDOWS_STOREAPP + private async Task ReadFile(string path, TextureLoader textureLoader) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false); + using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) { + try { + Atlas atlas = new Atlas(reader, Path.GetDirectoryName(path), textureLoader); + this.pages = atlas.pages; + this.regions = atlas.regions; + this.textureLoader = atlas.textureLoader; + } catch (Exception ex) { + throw new Exception("Error reading atlas file: " + path, ex); + } + } + } + + public Atlas(string path, TextureLoader textureLoader) { + this.ReadFile(path, textureLoader).Wait(); + } +#else + public Atlas (string path, TextureLoader textureLoader) { +#if WINDOWS_PHONE + Stream stream = Microsoft.Xna.Framework.TitleContainer.OpenStream(path); + using (StreamReader reader = new StreamReader(stream)) { +#else + using (StreamReader reader = new StreamReader(path)) { +#endif // WINDOWS_PHONE + try { + Atlas atlas = new Atlas(reader, Path.GetDirectoryName(path), textureLoader); + this.pages = atlas.pages; + this.regions = atlas.regions; + this.textureLoader = atlas.textureLoader; + } catch (Exception ex) { + throw new Exception("Error reading atlas file: " + path, ex); + } + } + } +#endif // WINDOWS_STOREAPP +#endif + + public Atlas (List pages, List regions) { + if (pages == null) throw new ArgumentNullException("pages", "pages cannot be null."); + if (regions == null) throw new ArgumentNullException("regions", "regions cannot be null."); + this.pages = pages; + this.regions = regions; + this.textureLoader = null; + } + + public Atlas (TextReader reader, string imagesDir, TextureLoader textureLoader) { + if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null."); + if (imagesDir == null) throw new ArgumentNullException("imagesDir", "imagesDir cannot be null."); + if (textureLoader == null) throw new ArgumentNullException("textureLoader", "textureLoader cannot be null."); + this.textureLoader = textureLoader; + + string[] entry = new string[5]; + AtlasPage page = null; + AtlasRegion region = null; + + var pageFields = new Dictionary(5); + pageFields.Add("size", () => { + page.width = int.Parse(entry[1], CultureInfo.InvariantCulture); + page.height = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + pageFields.Add("format", () => { + page.format = (Format)Enum.Parse(typeof(Format), entry[1], false); + }); + pageFields.Add("filter", () => { + page.minFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), entry[1], false); + page.magFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), entry[2], false); + }); + pageFields.Add("repeat", () => { + if (entry[1].IndexOf('x') != -1) page.uWrap = TextureWrap.Repeat; + if (entry[1].IndexOf('y') != -1) page.vWrap = TextureWrap.Repeat; + }); + pageFields.Add("pma", () => { + page.pma = entry[1] == "true"; + }); + + var regionFields = new Dictionary(8); + regionFields.Add("xy", () => { // Deprecated, use bounds. + region.x = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.y = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + regionFields.Add("size", () => { // Deprecated, use bounds. + region.width = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.height = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + regionFields.Add("bounds", () => { + region.x = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.y = int.Parse(entry[2], CultureInfo.InvariantCulture); + region.width = int.Parse(entry[3], CultureInfo.InvariantCulture); + region.height = int.Parse(entry[4], CultureInfo.InvariantCulture); + }); + regionFields.Add("offset", () => { // Deprecated, use offsets. + region.offsetX = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.offsetY = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + regionFields.Add("orig", () => { // Deprecated, use offsets. + region.originalWidth = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.originalHeight = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + regionFields.Add("offsets", () => { + region.offsetX = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.offsetY = int.Parse(entry[2], CultureInfo.InvariantCulture); + region.originalWidth = int.Parse(entry[3], CultureInfo.InvariantCulture); + region.originalHeight = int.Parse(entry[4], CultureInfo.InvariantCulture); + }); + regionFields.Add("rotate", () => { + string value = entry[1]; + if (value == "true") + region.degrees = 90; + else if (value != "false") + region.degrees = int.Parse(value, CultureInfo.InvariantCulture); + }); + regionFields.Add("index", () => { + region.index = int.Parse(entry[1], CultureInfo.InvariantCulture); + }); + + string line = reader.ReadLine(); + // Ignore empty lines before first entry. + while (line != null && line.Trim().Length == 0) + line = reader.ReadLine(); + // Header entries. + while (true) { + if (line == null || line.Trim().Length == 0) break; + if (ReadEntry(entry, line) == 0) break; // Silently ignore all header fields. + line = reader.ReadLine(); + } + // Page and region entries. + List names = null; + List values = null; + while (true) { + if (line == null) break; + if (line.Trim().Length == 0) { + page = null; + line = reader.ReadLine(); + } else if (page == null) { + page = new AtlasPage(); + page.name = line.Trim(); + while (true) { + if (ReadEntry(entry, line = reader.ReadLine()) == 0) break; + Action field; + if (pageFields.TryGetValue(entry[0], out field)) field(); // Silently ignore unknown page fields. + } + textureLoader.Load(page, Path.Combine(imagesDir, page.name)); + pages.Add(page); + } else { + region = new AtlasRegion(); + region.page = page; + region.name = line; + while (true) { + int count = ReadEntry(entry, line = reader.ReadLine()); + if (count == 0) break; + Action field; + if (regionFields.TryGetValue(entry[0], out field)) + field(); + else { + if (names == null) { + names = new List(8); + values = new List(8); + } + names.Add(entry[0]); + int[] entryValues = new int[count]; + for (int i = 0; i < count; i++) + int.TryParse(entry[i + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out entryValues[i]); // Silently ignore non-integer values. + values.Add(entryValues); + } + } + if (region.originalWidth == 0 && region.originalHeight == 0) { + region.originalWidth = region.width; + region.originalHeight = region.height; + } + if (names != null && names.Count > 0) { + region.names = names.ToArray(); + region.values = values.ToArray(); + names.Clear(); + values.Clear(); + } + region.u = region.x / (float)page.width; + region.v = region.y / (float)page.height; + if (region.degrees == 90) { + region.u2 = (region.x + region.height) / (float)page.width; + region.v2 = (region.y + region.width) / (float)page.height; + } else { + region.u2 = (region.x + region.width) / (float)page.width; + region.v2 = (region.y + region.height) / (float)page.height; + } + regions.Add(region); + } + } + } + + static private int ReadEntry (string[] entry, string line) { + if (line == null) return 0; + line = line.Trim(); + if (line.Length == 0) return 0; + int colon = line.IndexOf(':'); + if (colon == -1) return 0; + entry[0] = line.Substring(0, colon).Trim(); + for (int i = 1, lastMatch = colon + 1; ; i++) { + int comma = line.IndexOf(',', lastMatch); + if (comma == -1) { + entry[i] = line.Substring(lastMatch).Trim(); + return i; + } + entry[i] = line.Substring(lastMatch, comma - lastMatch).Trim(); + lastMatch = comma + 1; + if (i == 4) return 4; + } + } + + public void FlipV () { + for (int i = 0, n = regions.Count; i < n; i++) { + AtlasRegion region = regions[i]; + region.v = 1 - region.v; + region.v2 = 1 - region.v2; + } + } + + /// Returns the first region found with the specified name. This method uses string comparison to find the region, so the result + /// should be cached rather than calling this method multiple times. + /// The region, or null. + public AtlasRegion FindRegion (string name) { + for (int i = 0, n = regions.Count; i < n; i++) + if (regions[i].name == name) return regions[i]; + return null; + } + + public void Dispose () { + if (textureLoader == null) return; + for (int i = 0, n = pages.Count; i < n; i++) + textureLoader.Unload(pages[i].rendererObject); + } + } + + public enum Format { + Alpha, + Intensity, + LuminanceAlpha, + RGB565, + RGBA4444, + RGB888, + RGBA8888 + } + + public enum TextureFilter { + Nearest, + Linear, + MipMap, + MipMapNearestNearest, + MipMapLinearNearest, + MipMapNearestLinear, + MipMapLinearLinear + } + + public enum TextureWrap { + MirroredRepeat, + ClampToEdge, + Repeat + } + + public class AtlasPage { + public string name; + public int width, height; + public Format format = Format.RGBA8888; + public TextureFilter minFilter = TextureFilter.Nearest; + public TextureFilter magFilter = TextureFilter.Nearest; + public TextureWrap uWrap = TextureWrap.ClampToEdge; + public TextureWrap vWrap = TextureWrap.ClampToEdge; + public bool pma; + public object rendererObject; + + public AtlasPage Clone () { + return MemberwiseClone() as AtlasPage; + } + } + + public class AtlasRegion { + public AtlasPage page; + public string name; + public int x, y, width, height; + public float u, v, u2, v2; + public float offsetX, offsetY; + public int originalWidth, originalHeight; + public int degrees; + public bool rotate; + public int index; + public string[] names; + public int[][] values; + + public AtlasRegion Clone () { + return MemberwiseClone() as AtlasRegion; + } + } + + public interface TextureLoader { + void Load (AtlasPage page, string path); + void Unload (Object texture); + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AtlasAttachmentLoader.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AtlasAttachmentLoader.cs new file mode 100644 index 0000000..db887b7 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AtlasAttachmentLoader.cs @@ -0,0 +1,108 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + + /// + /// An AttachmentLoader that configures attachments using texture regions from an Atlas. + /// See Loading Skeleton Data in the Spine Runtimes Guide. + /// + public class AtlasAttachmentLoader : AttachmentLoader { + private Atlas[] atlasArray; + + public AtlasAttachmentLoader (params Atlas[] atlasArray) { + if (atlasArray == null) throw new ArgumentNullException("atlas", "atlas array cannot be null."); + this.atlasArray = atlasArray; + } + + public RegionAttachment NewRegionAttachment (Skin skin, string name, string path) { + AtlasRegion region = FindRegion(path); + if (region == null) throw new ArgumentException(string.Format("Region not found in atlas: {0} (region attachment: {1})", path, name)); + RegionAttachment attachment = new RegionAttachment(name); + attachment.RendererObject = region; + attachment.SetUVs(region.u, region.v, region.u2, region.v2, region.degrees); + attachment.regionOffsetX = region.offsetX; + attachment.regionOffsetY = region.offsetY; + attachment.regionWidth = region.width; + attachment.regionHeight = region.height; + attachment.regionOriginalWidth = region.originalWidth; + attachment.regionOriginalHeight = region.originalHeight; + return attachment; + } + + public MeshAttachment NewMeshAttachment (Skin skin, string name, string path) { + AtlasRegion region = FindRegion(path); + if (region == null) throw new ArgumentException(string.Format("Region not found in atlas: {0} (region attachment: {1})", path, name)); + MeshAttachment attachment = new MeshAttachment(name); + attachment.RendererObject = region; + attachment.RegionU = region.u; + attachment.RegionV = region.v; + attachment.RegionU2 = region.u2; + attachment.RegionV2 = region.v2; + attachment.RegionDegrees = region.degrees; + attachment.regionOffsetX = region.offsetX; + attachment.regionOffsetY = region.offsetY; + attachment.regionWidth = region.width; + attachment.regionHeight = region.height; + attachment.regionOriginalWidth = region.originalWidth; + attachment.regionOriginalHeight = region.originalHeight; + return attachment; + } + + public BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, string name) { + return new BoundingBoxAttachment(name); + } + + public PathAttachment NewPathAttachment (Skin skin, string name) { + return new PathAttachment(name); + } + + public PointAttachment NewPointAttachment (Skin skin, string name) { + return new PointAttachment(name); + } + + public ClippingAttachment NewClippingAttachment (Skin skin, string name) { + return new ClippingAttachment(name); + } + + public AtlasRegion FindRegion (string name) { + AtlasRegion region; + + for (int i = 0; i < atlasArray.Length; i++) { + region = atlasArray[i].FindRegion(name); + if (region != null) + return region; + } + + return null; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/Attachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/Attachment.cs new file mode 100644 index 0000000..141b4e2 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/Attachment.cs @@ -0,0 +1,52 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + abstract public class Attachment { + public string Name { get; private set; } + + protected Attachment (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null"); + Name = name; + } + + override public string ToString () { + return Name; + } + + ///Returns a copy of the attachment. + public abstract Attachment Copy (); + } + + public interface IHasRendererObject { + object RendererObject { get; set; } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AttachmentLoader.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AttachmentLoader.cs new file mode 100644 index 0000000..44605ae --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AttachmentLoader.cs @@ -0,0 +1,48 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_0_64 { + public interface AttachmentLoader { + /// May be null to not load any attachment. + RegionAttachment NewRegionAttachment (Skin skin, string name, string path); + + /// May be null to not load any attachment. + MeshAttachment NewMeshAttachment (Skin skin, string name, string path); + + /// May be null to not load any attachment. + BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, string name); + + /// May be null to not load any attachment + PathAttachment NewPathAttachment (Skin skin, string name); + + PointAttachment NewPointAttachment (Skin skin, string name); + + ClippingAttachment NewClippingAttachment (Skin skin, string name); + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AttachmentType.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AttachmentType.cs new file mode 100644 index 0000000..52a9f11 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/AttachmentType.cs @@ -0,0 +1,34 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_0_64 { + public enum AttachmentType { + Region, Boundingbox, Mesh, Linkedmesh, Path, Point, Clipping + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/BoundingBoxAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/BoundingBoxAttachment.cs new file mode 100644 index 0000000..6480ff3 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/BoundingBoxAttachment.cs @@ -0,0 +1,45 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// Attachment that has a polygon for bounds checking. + public class BoundingBoxAttachment : VertexAttachment { + public BoundingBoxAttachment (string name) + : base(name) { + } + + public override Attachment Copy () { + BoundingBoxAttachment copy = new BoundingBoxAttachment(this.Name); + CopyTo(copy); + return copy; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/ClippingAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/ClippingAttachment.cs new file mode 100644 index 0000000..fe6623a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/ClippingAttachment.cs @@ -0,0 +1,48 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + public class ClippingAttachment : VertexAttachment { + internal SlotData endSlot; + + public SlotData EndSlot { get { return endSlot; } set { endSlot = value; } } + + public ClippingAttachment (string name) : base(name) { + } + + public override Attachment Copy () { + ClippingAttachment copy = new ClippingAttachment(this.Name); + CopyTo(copy); + copy.endSlot = endSlot; + return copy; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/MeshAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/MeshAttachment.cs new file mode 100644 index 0000000..6af922d --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/MeshAttachment.cs @@ -0,0 +1,221 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// Attachment that displays a texture region using a mesh. + public class MeshAttachment : VertexAttachment, IHasRendererObject { + internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight; + private MeshAttachment parentMesh; + internal float[] uvs, regionUVs; + internal int[] triangles; + internal float r = 1, g = 1, b = 1, a = 1; + internal int hulllength; + + public int HullLength { get { return hulllength; } set { hulllength = value; } } + public float[] RegionUVs { get { return regionUVs; } set { regionUVs = value; } } + /// The UV pair for each vertex, normalized within the entire texture. + public float[] UVs { get { return uvs; } set { uvs = value; } } + public int[] Triangles { get { return triangles; } set { triangles = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public string Path { get; set; } + public object RendererObject { get; set; } + public float RegionU { get; set; } + public float RegionV { get; set; } + public float RegionU2 { get; set; } + public float RegionV2 { get; set; } + public int RegionDegrees { get; set; } + public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } } + public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated. + public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } } + public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size. + public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } } + public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size. + + public MeshAttachment ParentMesh { + get { return parentMesh; } + set { + parentMesh = value; + if (value != null) { + bones = value.bones; + vertices = value.vertices; + worldVerticesLength = value.worldVerticesLength; + regionUVs = value.regionUVs; + triangles = value.triangles; + HullLength = value.HullLength; + Edges = value.Edges; + Width = value.Width; + Height = value.Height; + } + } + } + + // Nonessential. + public int[] Edges { get; set; } + public float Width { get; set; } + public float Height { get; set; } + + public MeshAttachment (string name) + : base(name) { + } + + public void UpdateUVs () { + float[] regionUVs = this.regionUVs; + if (this.uvs == null || this.uvs.Length != regionUVs.Length) this.uvs = new float[regionUVs.Length]; + float[] uvs = this.uvs; + float u = RegionU, v = RegionV, width = 0, height = 0; + + if (RegionDegrees == 90) { + float textureHeight = this.regionWidth / (RegionV2 - RegionV); + float textureWidth = this.regionHeight / (RegionU2 - RegionU); + u -= (RegionOriginalHeight - RegionOffsetY - RegionHeight) / textureWidth; + v -= (RegionOriginalWidth - RegionOffsetX - RegionWidth) / textureHeight; + width = RegionOriginalHeight / textureWidth; + height = RegionOriginalWidth / textureHeight; + + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + regionUVs[i + 1] * width; + uvs[i + 1] = v + (1 - regionUVs[i]) * height; + } + } else if (RegionDegrees == 180) { + float textureWidth = this.regionWidth / (RegionU2 - RegionU); + float textureHeight = this.regionHeight / (RegionV2 - RegionV); + u -= (RegionOriginalWidth - RegionOffsetX - RegionWidth) / textureWidth; + v -= RegionOffsetY / textureHeight; + width = RegionOriginalWidth / textureWidth; + height = RegionOriginalHeight / textureHeight; + + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + (1 - regionUVs[i]) * width; + uvs[i + 1] = v + (1 - regionUVs[i + 1]) * height; + } + } else if (RegionDegrees == 270) { + float textureWidth = this.regionWidth / (RegionU2 - RegionU); + float textureHeight = this.regionHeight / (RegionV2 - RegionV); + u -= RegionOffsetY / textureWidth; + v -= RegionOffsetX / textureHeight; + width = RegionOriginalHeight / textureWidth; + height = RegionOriginalWidth / textureHeight; + + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + (1 - regionUVs[i + 1]) * width; + uvs[i + 1] = v + regionUVs[i] * height; + } + } else { + float textureWidth = this.regionWidth / (RegionU2 - RegionU); + float textureHeight = this.regionHeight / (RegionV2 - RegionV); + u -= RegionOffsetX / textureWidth; + v -= (RegionOriginalHeight - RegionOffsetY - RegionHeight) / textureHeight; + width = RegionOriginalWidth / textureWidth; + height = RegionOriginalHeight / textureHeight; + + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + regionUVs[i] * width; + uvs[i + 1] = v + regionUVs[i + 1] * height; + } + } + } + + public override Attachment Copy () { + if (parentMesh != null) return NewLinkedMesh(); + + MeshAttachment copy = new MeshAttachment(this.Name); + copy.RendererObject = RendererObject; + copy.regionOffsetX = regionOffsetX; + copy.regionOffsetY = regionOffsetY; + copy.regionWidth = regionWidth; + copy.regionHeight = regionHeight; + copy.regionOriginalWidth = regionOriginalWidth; + copy.regionOriginalHeight = regionOriginalHeight; + copy.RegionDegrees = RegionDegrees; + copy.RegionU = RegionU; + copy.RegionV = RegionV; + copy.RegionU2 = RegionU2; + copy.RegionV2 = RegionV2; + + copy.Path = Path; + copy.r = r; + copy.g = g; + copy.b = b; + copy.a = a; + + CopyTo(copy); + copy.regionUVs = new float[regionUVs.Length]; + Array.Copy(regionUVs, 0, copy.regionUVs, 0, regionUVs.Length); + copy.uvs = new float[uvs.Length]; + Array.Copy(uvs, 0, copy.uvs, 0, uvs.Length); + copy.triangles = new int[triangles.Length]; + Array.Copy(triangles, 0, copy.triangles, 0, triangles.Length); + copy.HullLength = HullLength; + + // Nonessential. + if (Edges != null) { + copy.Edges = new int[Edges.Length]; + Array.Copy(Edges, 0, copy.Edges, 0, Edges.Length); + } + copy.Width = Width; + copy.Height = Height; + return copy; + } + + ///Returns a new mesh with this mesh set as the . + public MeshAttachment NewLinkedMesh () { + MeshAttachment mesh = new MeshAttachment(Name); + mesh.RendererObject = RendererObject; + mesh.regionOffsetX = regionOffsetX; + mesh.regionOffsetY = regionOffsetY; + mesh.regionWidth = regionWidth; + mesh.regionHeight = regionHeight; + mesh.regionOriginalWidth = regionOriginalWidth; + mesh.regionOriginalHeight = regionOriginalHeight; + mesh.RegionDegrees = RegionDegrees; + mesh.RegionU = RegionU; + mesh.RegionV = RegionV; + mesh.RegionU2 = RegionU2; + mesh.RegionV2 = RegionV2; + + mesh.Path = Path; + mesh.r = r; + mesh.g = g; + mesh.b = b; + mesh.a = a; + + mesh.deformAttachment = deformAttachment; + mesh.ParentMesh = parentMesh != null ? parentMesh : this; + mesh.UpdateUVs(); + return mesh; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/PathAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/PathAttachment.cs new file mode 100644 index 0000000..e45c5e3 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/PathAttachment.cs @@ -0,0 +1,60 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_0_64 { + public class PathAttachment : VertexAttachment { + internal float[] lengths; + internal bool closed, constantSpeed; + + /// The length in the setup pose from the start of the path to the end of each curve. + public float[] Lengths { get { return lengths; } set { lengths = value; } } + /// If true, the start and end knots are connected. + public bool Closed { get { return closed; } set { closed = value; } } + /// If true, additional calculations are performed to make computing positions along the path more accurate and movement along + /// the path have a constant speed. + public bool ConstantSpeed { get { return constantSpeed; } set { constantSpeed = value; } } + + public PathAttachment (String name) + : base(name) { + } + + public override Attachment Copy () { + PathAttachment copy = new PathAttachment(this.Name); + CopyTo(copy); + copy.lengths = new float[lengths.Length]; + Array.Copy(lengths, 0, copy.lengths, 0, lengths.Length); + copy.closed = closed; + copy.constantSpeed = constantSpeed; + return copy; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/PointAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/PointAttachment.cs new file mode 100644 index 0000000..6e32473 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/PointAttachment.cs @@ -0,0 +1,67 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_0_64 { + /// + /// An attachment which is a single point and a rotation. This can be used to spawn projectiles, particles, etc. A bone can be + /// used in similar ways, but a PointAttachment is slightly less expensive to compute and can be hidden, shown, and placed in a + /// skin. + ///

+ /// See Point Attachments in the Spine User Guide. + ///

+ public class PointAttachment : Attachment { + internal float x, y, rotation; + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Rotation { get { return rotation; } set { rotation = value; } } + + public PointAttachment (string name) + : base(name) { + } + + public void ComputeWorldPosition (Bone bone, out float ox, out float oy) { + bone.LocalToWorld(this.x, this.y, out ox, out oy); + } + + public float ComputeWorldRotation (Bone bone) { + float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation); + float ix = cos * bone.a + sin * bone.b; + float iy = cos * bone.c + sin * bone.d; + return MathUtils.Atan2(iy, ix) * MathUtils.RadDeg; + } + + public override Attachment Copy () { + PointAttachment copy = new PointAttachment(this.Name); + copy.x = x; + copy.y = y; + copy.rotation = rotation; + return copy; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/RegionAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/RegionAttachment.cs new file mode 100644 index 0000000..18eb298 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/RegionAttachment.cs @@ -0,0 +1,194 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// Attachment that displays a texture region. + public class RegionAttachment : Attachment, IHasRendererObject { + public const int BLX = 0; + public const int BLY = 1; + public const int ULX = 2; + public const int ULY = 3; + public const int URX = 4; + public const int URY = 5; + public const int BRX = 6; + public const int BRY = 7; + + internal float x, y, rotation, scaleX = 1, scaleY = 1, width, height; + internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight; + internal float[] offset = new float[8], uvs = new float[8]; + internal float r = 1, g = 1, b = 1, a = 1; + + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Rotation { get { return rotation; } set { rotation = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + public float Width { get { return width; } set { width = value; } } + public float Height { get { return height; } set { height = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public string Path { get; set; } + public object RendererObject { get; set; } + public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } } + public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated. + public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } } + public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size. + public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } } + public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size. + + public float[] Offset { get { return offset; } } + public float[] UVs { get { return uvs; } } + + public RegionAttachment (string name) + : base(name) { + } + + public void UpdateOffset () { + float regionScaleX = width / regionOriginalWidth * scaleX; + float regionScaleY = height / regionOriginalHeight * scaleY; + float localX = -width / 2 * scaleX + regionOffsetX * regionScaleX; + float localY = -height / 2 * scaleY + regionOffsetY * regionScaleY; + float localX2 = localX + regionWidth * regionScaleX; + float localY2 = localY + regionHeight * regionScaleY; + float cos = MathUtils.CosDeg(this.rotation); + float sin = MathUtils.SinDeg(this.rotation); + float x = this.x, y = this.y; + float localXCos = localX * cos + x; + float localXSin = localX * sin; + float localYCos = localY * cos + y; + float localYSin = localY * sin; + float localX2Cos = localX2 * cos + x; + float localX2Sin = localX2 * sin; + float localY2Cos = localY2 * cos + y; + float localY2Sin = localY2 * sin; + float[] offset = this.offset; + offset[BLX] = localXCos - localYSin; + offset[BLY] = localYCos + localXSin; + offset[ULX] = localXCos - localY2Sin; + offset[ULY] = localY2Cos + localXSin; + offset[URX] = localX2Cos - localY2Sin; + offset[URY] = localY2Cos + localX2Sin; + offset[BRX] = localX2Cos - localYSin; + offset[BRY] = localYCos + localX2Sin; + } + + public void SetUVs (float u, float v, float u2, float v2, int degrees) { + float[] uvs = this.uvs; + // UV values differ from spine-libgdx. + if (degrees == 90) { + uvs[URX] = u; + uvs[URY] = v2; + uvs[BRX] = u; + uvs[BRY] = v; + uvs[BLX] = u2; + uvs[BLY] = v; + uvs[ULX] = u2; + uvs[ULY] = v2; + } else { + uvs[ULX] = u; + uvs[ULY] = v2; + uvs[URX] = u; + uvs[URY] = v; + uvs[BRX] = u2; + uvs[BRY] = v; + uvs[BLX] = u2; + uvs[BLY] = v2; + } + } + + /// Transforms the attachment's four vertices to world coordinates. + /// The parent bone. + /// The output world vertices. Must have a length greater than or equal to offset + 8. + /// The worldVertices index to begin writing values. + /// The number of worldVertices entries between the value pairs written. + public void ComputeWorldVertices (Bone bone, float[] worldVertices, int offset, int stride = 2) { + float[] vertexOffset = this.offset; + float bwx = bone.worldX, bwy = bone.worldY; + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float offsetX, offsetY; + + // Vertex order is different from RegionAttachment.java + offsetX = vertexOffset[BRX]; // 0 + offsetY = vertexOffset[BRY]; // 1 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // bl + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[BLX]; // 2 + offsetY = vertexOffset[BLY]; // 3 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // ul + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[ULX]; // 4 + offsetY = vertexOffset[ULY]; // 5 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // ur + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[URX]; // 6 + offsetY = vertexOffset[URY]; // 7 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // br + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + //offset += stride; + } + + public override Attachment Copy () { + RegionAttachment copy = new RegionAttachment(this.Name); + copy.RendererObject = RendererObject; + copy.regionOffsetX = regionOffsetX; + copy.regionOffsetY = regionOffsetY; + copy.regionWidth = regionWidth; + copy.regionHeight = regionHeight; + copy.regionOriginalWidth = regionOriginalWidth; + copy.regionOriginalHeight = regionOriginalHeight; + copy.Path = Path; + copy.x = x; + copy.y = y; + copy.scaleX = scaleX; + copy.scaleY = scaleY; + copy.rotation = rotation; + copy.width = width; + copy.height = height; + Array.Copy(uvs, 0, copy.uvs, 0, 8); + Array.Copy(offset, 0, copy.offset, 0, 8); + copy.r = r; + copy.g = g; + copy.b = b; + copy.a = a; + return copy; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/VertexAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/VertexAttachment.cs new file mode 100644 index 0000000..e1c12f1 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Attachments/VertexAttachment.cs @@ -0,0 +1,153 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// >An attachment with vertices that are transformed by one or more bones and can be deformed by a slot's + /// . + public abstract class VertexAttachment : Attachment { + static int nextID = 0; + static readonly Object nextIdLock = new Object(); + + internal readonly int id; + internal int[] bones; + internal float[] vertices; + internal int worldVerticesLength; + internal VertexAttachment deformAttachment; + + /// Gets a unique ID for this attachment. + public int Id { get { return id; } } + public int[] Bones { get { return bones; } set { bones = value; } } + public float[] Vertices { get { return vertices; } set { vertices = value; } } + public int WorldVerticesLength { get { return worldVerticesLength; } set { worldVerticesLength = value; } } + ///Deform keys for the deform attachment are also applied to this attachment. + /// May be null if no deform keys should be applied. + public VertexAttachment DeformAttachment { get { return deformAttachment; } set { deformAttachment = value; } } + + public VertexAttachment (string name) + : base(name) { + + deformAttachment = this; + lock (VertexAttachment.nextIdLock) { + id = VertexAttachment.nextID++; + } + } + + public void ComputeWorldVertices (Slot slot, float[] worldVertices) { + ComputeWorldVertices(slot, 0, worldVerticesLength, worldVertices, 0); + } + + /// + /// Transforms the attachment's local to world coordinates. If the slot's is + /// not empty, it is used to deform the vertices. + /// + /// See World transforms in the Spine + /// Runtimes Guide. + /// + /// The index of the first value to transform. Each vertex has 2 values, x and y. + /// The number of world vertex values to output. Must be less than or equal to - start. + /// The output world vertices. Must have a length greater than or equal to + . + /// The index to begin writing values. + /// The number of entries between the value pairs written. + public void ComputeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride = 2) { + count = offset + (count >> 1) * stride; + ExposedList deformArray = slot.deform; + float[] vertices = this.vertices; + int[] bones = this.bones; + if (bones == null) { + if (deformArray.Count > 0) vertices = deformArray.Items; + Bone bone = slot.bone; + float x = bone.worldX, y = bone.worldY; + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + for (int vv = start, w = offset; w < count; vv += 2, w += stride) { + float vx = vertices[vv], vy = vertices[vv + 1]; + worldVertices[w] = vx * a + vy * b + x; + worldVertices[w + 1] = vx * c + vy * d + y; + } + return; + } + int v = 0, skip = 0; + for (int i = 0; i < start; i += 2) { + int n = bones[v]; + v += n + 1; + skip += n; + } + Bone[] skeletonBones = slot.bone.skeleton.bones.Items; + if (deformArray.Count == 0) { + for (int w = offset, b = skip * 3; w < count; w += stride) { + float wx = 0, wy = 0; + int n = bones[v++]; + n += v; + for (; v < n; v++, b += 3) { + Bone bone = skeletonBones[bones[v]]; + float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } else { + float[] deform = deformArray.Items; + for (int w = offset, b = skip * 3, f = skip << 1; w < count; w += stride) { + float wx = 0, wy = 0; + int n = bones[v++]; + n += v; + for (; v < n; v++, b += 3, f += 2) { + Bone bone = skeletonBones[bones[v]]; + float vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } + } + + ///Does not copy id (generated) or name (set on construction). + internal void CopyTo (VertexAttachment attachment) { + if (bones != null) { + attachment.bones = new int[bones.Length]; + Array.Copy(bones, 0, attachment.bones, 0, bones.Length); + } else + attachment.bones = null; + + if (vertices != null) { + attachment.vertices = new float[vertices.Length]; + Array.Copy(vertices, 0, attachment.vertices, 0, vertices.Length); + } else + attachment.vertices = null; + + attachment.worldVerticesLength = worldVerticesLength; + attachment.deformAttachment = deformAttachment; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/BlendMode.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/BlendMode.cs new file mode 100644 index 0000000..ddf8953 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/BlendMode.cs @@ -0,0 +1,34 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_0_64 { + public enum BlendMode { + Normal, Additive, Multiply, Screen + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Bone.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Bone.cs new file mode 100644 index 0000000..641c7d5 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Bone.cs @@ -0,0 +1,378 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// + /// Stores a bone's current pose. + /// + /// A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a + /// local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a + /// constraint or application code modifies the world transform after it was computed from the local transform. + /// + /// + public class Bone : IUpdatable { + static public bool yDown; + + internal BoneData data; + internal Skeleton skeleton; + internal Bone parent; + internal ExposedList children = new ExposedList(); + internal float x, y, rotation, scaleX, scaleY, shearX, shearY; + internal float ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY; + + internal float a, b, worldX; + internal float c, d, worldY; + + internal bool sorted, active; + + public BoneData Data { get { return data; } } + public Skeleton Skeleton { get { return skeleton; } } + public Bone Parent { get { return parent; } } + public ExposedList Children { get { return children; } } + /// Returns false when the bone has not been computed because is true and the + /// active skin does not contain this bone. + public bool Active { get { return active; } } + /// The local X translation. + public float X { get { return x; } set { x = value; } } + /// The local Y translation. + public float Y { get { return y; } set { y = value; } } + /// The local rotation. + public float Rotation { get { return rotation; } set { rotation = value; } } + + /// The local scaleX. + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + + /// The local scaleY. + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + + /// The local shearX. + public float ShearX { get { return shearX; } set { shearX = value; } } + + /// The local shearY. + public float ShearY { get { return shearY; } set { shearY = value; } } + + /// The rotation, as calculated by any constraints. + public float AppliedRotation { get { return arotation; } set { arotation = value; } } + + /// The applied local x translation. + public float AX { get { return ax; } set { ax = value; } } + + /// The applied local y translation. + public float AY { get { return ay; } set { ay = value; } } + + /// The applied local scaleX. + public float AScaleX { get { return ascaleX; } set { ascaleX = value; } } + + /// The applied local scaleY. + public float AScaleY { get { return ascaleY; } set { ascaleY = value; } } + + /// The applied local shearX. + public float AShearX { get { return ashearX; } set { ashearX = value; } } + + /// The applied local shearY. + public float AShearY { get { return ashearY; } set { ashearY = value; } } + + /// Part of the world transform matrix for the X axis. If changed, should be called. + public float A { get { return a; } set { a = value; } } + /// Part of the world transform matrix for the Y axis. If changed, should be called. + public float B { get { return b; } set { b = value; } } + /// Part of the world transform matrix for the X axis. If changed, should be called. + public float C { get { return c; } set { c = value; } } + /// Part of the world transform matrix for the Y axis. If changed, should be called. + public float D { get { return d; } set { d = value; } } + + /// The world X position. If changed, should be called. + public float WorldX { get { return worldX; } set { worldX = value; } } + /// The world Y position. If changed, should be called. + public float WorldY { get { return worldY; } set { worldY = value; } } + public float WorldRotationX { get { return MathUtils.Atan2(c, a) * MathUtils.RadDeg; } } + public float WorldRotationY { get { return MathUtils.Atan2(d, b) * MathUtils.RadDeg; } } + + /// Returns the magnitide (always positive) of the world scale X. + public float WorldScaleX { get { return (float)Math.Sqrt(a * a + c * c); } } + /// Returns the magnitide (always positive) of the world scale Y. + public float WorldScaleY { get { return (float)Math.Sqrt(b * b + d * d); } } + + /// Copy constructor. Does not copy the bones. + /// May be null. + public Bone (BoneData data, Skeleton skeleton, Bone parent) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + this.skeleton = skeleton; + this.parent = parent; + SetToSetupPose(); + } + + /// Computes the world transform using the parent bone and this bone's local applied transform. + public void Update () { + UpdateWorldTransform(ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY); + } + + /// Computes the world transform using the parent bone and this bone's local transform. + public void UpdateWorldTransform () { + UpdateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY); + } + + /// Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the + /// specified local transform. Child bones are not updated. + /// + /// See World transforms in the Spine + /// Runtimes Guide. + public void UpdateWorldTransform (float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY) { + ax = x; + ay = y; + arotation = rotation; + ascaleX = scaleX; + ascaleY = scaleY; + ashearX = shearX; + ashearY = shearY; + + Bone parent = this.parent; + if (parent == null) { // Root bone. + float rotationY = rotation + 90 + shearY, sx = skeleton.ScaleX, sy = skeleton.ScaleY; + a = MathUtils.CosDeg(rotation + shearX) * scaleX * sx; + b = MathUtils.CosDeg(rotationY) * scaleY * sx; + c = MathUtils.SinDeg(rotation + shearX) * scaleX * sy; + d = MathUtils.SinDeg(rotationY) * scaleY * sy; + worldX = x * sx + skeleton.x; + worldY = y * sy + skeleton.y; + return; + } + + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; + worldX = pa * x + pb * y + parent.worldX; + worldY = pc * x + pd * y + parent.worldY; + + switch (data.transformMode) { + case TransformMode.Normal: { + float rotationY = rotation + 90 + shearY; + float la = MathUtils.CosDeg(rotation + shearX) * scaleX; + float lb = MathUtils.CosDeg(rotationY) * scaleY; + float lc = MathUtils.SinDeg(rotation + shearX) * scaleX; + float ld = MathUtils.SinDeg(rotationY) * scaleY; + a = pa * la + pb * lc; + b = pa * lb + pb * ld; + c = pc * la + pd * lc; + d = pc * lb + pd * ld; + return; + } + case TransformMode.OnlyTranslation: { + float rotationY = rotation + 90 + shearY; + a = MathUtils.CosDeg(rotation + shearX) * scaleX; + b = MathUtils.CosDeg(rotationY) * scaleY; + c = MathUtils.SinDeg(rotation + shearX) * scaleX; + d = MathUtils.SinDeg(rotationY) * scaleY; + break; + } + case TransformMode.NoRotationOrReflection: { + float s = pa * pa + pc * pc, prx; + if (s > 0.0001f) { + s = Math.Abs(pa * pd - pb * pc) / s; + pa /= skeleton.ScaleX; + pc /= skeleton.ScaleY; + pb = pc * s; + pd = pa * s; + prx = MathUtils.Atan2(pc, pa) * MathUtils.RadDeg; + } else { + pa = 0; + pc = 0; + prx = 90 - MathUtils.Atan2(pd, pb) * MathUtils.RadDeg; + } + float rx = rotation + shearX - prx; + float ry = rotation + shearY - prx + 90; + float la = MathUtils.CosDeg(rx) * scaleX; + float lb = MathUtils.CosDeg(ry) * scaleY; + float lc = MathUtils.SinDeg(rx) * scaleX; + float ld = MathUtils.SinDeg(ry) * scaleY; + a = pa * la - pb * lc; + b = pa * lb - pb * ld; + c = pc * la + pd * lc; + d = pc * lb + pd * ld; + break; + } + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: { + float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation); + float za = (pa * cos + pb * sin) / skeleton.ScaleX; + float zc = (pc * cos + pd * sin) / skeleton.ScaleY; + float s = (float)Math.Sqrt(za * za + zc * zc); + if (s > 0.00001f) s = 1 / s; + za *= s; + zc *= s; + s = (float)Math.Sqrt(za * za + zc * zc); + if (data.transformMode == TransformMode.NoScale + && (pa * pd - pb * pc < 0) != (skeleton.ScaleX < 0 != skeleton.ScaleY < 0)) s = -s; + + float r = MathUtils.PI / 2 + MathUtils.Atan2(zc, za); + float zb = MathUtils.Cos(r) * s; + float zd = MathUtils.Sin(r) * s; + float la = MathUtils.CosDeg(shearX) * scaleX; + float lb = MathUtils.CosDeg(90 + shearY) * scaleY; + float lc = MathUtils.SinDeg(shearX) * scaleX; + float ld = MathUtils.SinDeg(90 + shearY) * scaleY; + a = za * la + zb * lc; + b = za * lb + zb * ld; + c = zc * la + zd * lc; + d = zc * lb + zd * ld; + break; + } + } + + a *= skeleton.ScaleX; + b *= skeleton.ScaleX; + c *= skeleton.ScaleY; + d *= skeleton.ScaleY; + } + + public void SetToSetupPose () { + BoneData data = this.data; + x = data.x; + y = data.y; + rotation = data.rotation; + scaleX = data.scaleX; + scaleY = data.scaleY; + shearX = data.shearX; + shearY = data.shearY; + } + + /// + /// Computes the applied transform values from the world transform. + /// + /// If the world transform is modified (by a constraint, , etc) then this method should be called so + /// the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply another + /// constraint). + /// + /// Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after + /// calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. + /// + public void UpdateAppliedTransform () { + Bone parent = this.parent; + if (parent == null) { + ax = worldX - skeleton.x; + ay = worldY - skeleton.y; + arotation = MathUtils.Atan2(c, a) * MathUtils.RadDeg; + ascaleX = (float)Math.Sqrt(a * a + c * c); + ascaleY = (float)Math.Sqrt(b * b + d * d); + ashearX = 0; + ashearY = MathUtils.Atan2(a * b + c * d, a * d - b * c) * MathUtils.RadDeg; + return; + } + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; + float pid = 1 / (pa * pd - pb * pc); + float dx = worldX - parent.worldX, dy = worldY - parent.worldY; + ax = (dx * pd * pid - dy * pb * pid); + ay = (dy * pa * pid - dx * pc * pid); + float ia = pid * pd; + float id = pid * pa; + float ib = pid * pb; + float ic = pid * pc; + float ra = ia * a - ib * c; + float rb = ia * b - ib * d; + float rc = id * c - ic * a; + float rd = id * d - ic * b; + ashearX = 0; + ascaleX = (float)Math.Sqrt(ra * ra + rc * rc); + if (ascaleX > 0.0001f) { + float det = ra * rd - rb * rc; + ascaleY = det / ascaleX; + ashearY = MathUtils.Atan2(ra * rb + rc * rd, det) * MathUtils.RadDeg; + arotation = MathUtils.Atan2(rc, ra) * MathUtils.RadDeg; + } else { + ascaleX = 0; + ascaleY = (float)Math.Sqrt(rb * rb + rd * rd); + ashearY = 0; + arotation = 90 - MathUtils.Atan2(rd, rb) * MathUtils.RadDeg; + } + } + + public void WorldToLocal (float worldX, float worldY, out float localX, out float localY) { + float a = this.a, b = this.b, c = this.c, d = this.d; + float det = a * d - b * c; + float x = worldX - this.worldX, y = worldY - this.worldY; + localX = (x * d - y * b) / det; + localY = (y * a - x * c) / det; + } + + public void LocalToWorld (float localX, float localY, out float worldX, out float worldY) { + worldX = localX * a + localY * b + this.worldX; + worldY = localX * c + localY * d + this.worldY; + } + + public float WorldToLocalRotationX { + get { + Bone parent = this.parent; + if (parent == null) return arotation; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, a = this.a, c = this.c; + return MathUtils.Atan2(pa * c - pc * a, pd * a - pb * c) * MathUtils.RadDeg; + } + } + + public float WorldToLocalRotationY { + get { + Bone parent = this.parent; + if (parent == null) return arotation; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, b = this.b, d = this.d; + return MathUtils.Atan2(pa * d - pc * b, pd * b - pb * d) * MathUtils.RadDeg; + } + } + + public float WorldToLocalRotation (float worldRotation) { + float sin = MathUtils.SinDeg(worldRotation), cos = MathUtils.CosDeg(worldRotation); + return MathUtils.Atan2(a * sin - c * cos, d * cos - b * sin) * MathUtils.RadDeg + rotation - shearX; + } + + public float LocalToWorldRotation (float localRotation) { + localRotation -= rotation - shearX; + float sin = MathUtils.SinDeg(localRotation), cos = MathUtils.CosDeg(localRotation); + return MathUtils.Atan2(cos * c + sin * d, cos * a + sin * b) * MathUtils.RadDeg; + } + + /// + /// Rotates the world transform the specified amount. + /// + /// After changes are made to the world transform, should be called and will + /// need to be called on any child bones, recursively. + /// + public void RotateWorld (float degrees) { + float a = this.a, b = this.b, c = this.c, d = this.d; + float cos = MathUtils.CosDeg(degrees), sin = MathUtils.SinDeg(degrees); + this.a = cos * a - sin * c; + this.b = cos * b - sin * d; + this.c = sin * a + cos * c; + this.d = sin * b + cos * d; + } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/BoneData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/BoneData.cs new file mode 100644 index 0000000..ef67d6c --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/BoneData.cs @@ -0,0 +1,105 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + public class BoneData { + internal int index; + internal string name; + internal BoneData parent; + internal float length; + internal float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY; + internal TransformMode transformMode = TransformMode.Normal; + internal bool skinRequired; + + /// The index of the bone in Skeleton.Bones + public int Index { get { return index; } } + + /// The name of the bone, which is unique across all bones in the skeleton. + public string Name { get { return name; } } + + /// May be null. + public BoneData Parent { get { return parent; } } + + public float Length { get { return length; } set { length = value; } } + + /// Local X translation. + public float X { get { return x; } set { x = value; } } + + /// Local Y translation. + public float Y { get { return y; } set { y = value; } } + + /// Local rotation. + public float Rotation { get { return rotation; } set { rotation = value; } } + + /// Local scaleX. + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + + /// Local scaleY. + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + + /// Local shearX. + public float ShearX { get { return shearX; } set { shearX = value; } } + + /// Local shearY. + public float ShearY { get { return shearY; } set { shearY = value; } } + + /// The transform mode for how parent world transforms affect this bone. + public TransformMode TransformMode { get { return transformMode; } set { transformMode = value; } } + + ///When true, only updates this bone if the contains this + /// bone. + /// + public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } } + + /// May be null. + public BoneData (int index, string name, BoneData parent) { + if (index < 0) throw new ArgumentException("index must be >= 0", "index"); + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.index = index; + this.name = name; + this.parent = parent; + } + + override public string ToString () { + return name; + } + } + + [Flags] + public enum TransformMode { + //0000 0 Flip Scale Rotation + Normal = 0, // 0000 + OnlyTranslation = 7, // 0111 + NoRotationOrReflection = 1, // 0001 + NoScale = 2, // 0010 + NoScaleOrReflection = 6, // 0110 + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/ConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/ConstraintData.cs new file mode 100644 index 0000000..a69181e --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/ConstraintData.cs @@ -0,0 +1,61 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_0_64 { + /// The base class for all constraint datas. + public abstract class ConstraintData { + internal readonly string name; + internal int order; + internal bool skinRequired; + + public ConstraintData (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + /// The constraint's name, which is unique across all constraints in the skeleton of the same type. + public string Name { get { return name; } } + + ///The ordinal of this constraint for the order a skeleton's constraints will be applied by + /// . + public int Order { get { return order; } set { order = value; } } + + ///When true, only updates this constraint if the contains + /// this constraint. + /// + public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } } + + override public string ToString () { + return name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Event.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Event.cs new file mode 100644 index 0000000..feb65f2 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Event.cs @@ -0,0 +1,64 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// Stores the current pose values for an Event. + public class Event { + internal readonly EventData data; + internal readonly float time; + internal int intValue; + internal float floatValue; + internal string stringValue; + internal float volume; + internal float balance; + + public EventData Data { get { return data; } } + /// The animation time this event was keyed. + public float Time { get { return time; } } + + public int Int { get { return intValue; } set { intValue = value; } } + public float Float { get { return floatValue; } set { floatValue = value; } } + public string String { get { return stringValue; } set { stringValue = value; } } + + public float Volume { get { return volume; } set { volume = value; } } + public float Balance { get { return balance; } set { balance = value; } } + + public Event (float time, EventData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.time = time; + this.data = data; + } + + override public string ToString () { + return this.data.Name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/EventData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/EventData.cs new file mode 100644 index 0000000..26379c5 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/EventData.cs @@ -0,0 +1,56 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// Stores the setup pose values for an Event. + public class EventData { + internal string name; + + /// The name of the event, which is unique across all events in the skeleton. + public string Name { get { return name; } } + public int Int { get; set; } + public float Float { get; set; } + public string @String { get; set; } + + public string AudioPath { get; set; } + public float Volume { get; set; } + public float Balance { get; set; } + + public EventData (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + override public string ToString () { + return Name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/ExposedList.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/ExposedList.cs new file mode 100644 index 0000000..8f4ab4f --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/ExposedList.cs @@ -0,0 +1,637 @@ +// +// System.Collections.Generic.List +// +// Authors: +// Ben Maurer (bmaurer@ximian.com) +// Martin Baulig (martin@ximian.com) +// Carlos Alberto Cortez (calberto.cortez@gmail.com) +// David Waite (mass@akuma.org) +// +// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com) +// Copyright (C) 2005 David Waite +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Spine4_0_64 { + [DebuggerDisplay("Count={Count}")] + public class ExposedList : IEnumerable { + public T[] Items; + public int Count; + private const int DefaultCapacity = 4; + private static readonly T[] EmptyArray = new T[0]; + private int version; + + public ExposedList () { + Items = EmptyArray; + } + + public ExposedList (IEnumerable collection) { + CheckCollection(collection); + + // initialize to needed size (if determinable) + ICollection c = collection as ICollection; + if (c == null) { + Items = EmptyArray; + AddEnumerable(collection); + } else { + Items = new T[c.Count]; + AddCollection(c); + } + } + + public ExposedList (int capacity) { + if (capacity < 0) + throw new ArgumentOutOfRangeException("capacity"); + Items = new T[capacity]; + } + + internal ExposedList (T[] data, int size) { + Items = data; + Count = size; + } + + public void Add (T item) { + // If we check to see if we need to grow before trying to grow + // we can speed things up by 25% + if (Count == Items.Length) + GrowIfNeeded(1); + Items[Count++] = item; + version++; + } + + public void GrowIfNeeded (int addedCount) { + int minimumSize = Count + addedCount; + if (minimumSize > Items.Length) + Capacity = Math.Max(Math.Max(Capacity * 2, DefaultCapacity), minimumSize); + } + + public ExposedList Resize (int newSize) { + int itemsLength = Items.Length; + var oldItems = Items; + if (newSize > itemsLength) { + Array.Resize(ref Items, newSize); + } else if (newSize < itemsLength) { + // Allow nulling of T reference type to allow GC. + for (int i = newSize; i < itemsLength; i++) + oldItems[i] = default(T); + } + Count = newSize; + return this; + } + + public void EnsureCapacity (int min) { + if (Items.Length < min) { + int newCapacity = Items.Length == 0 ? DefaultCapacity : Items.Length * 2; + //if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; + if (newCapacity < min) newCapacity = min; + Capacity = newCapacity; + } + } + + private void CheckRange (int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + if ((uint)index + (uint)count > (uint)Count) + throw new ArgumentException("index and count exceed length of list"); + } + + private void AddCollection (ICollection collection) { + int collectionCount = collection.Count; + if (collectionCount == 0) + return; + + GrowIfNeeded(collectionCount); + collection.CopyTo(Items, Count); + Count += collectionCount; + } + + private void AddEnumerable (IEnumerable enumerable) { + foreach (T t in enumerable) { + Add(t); + } + } + + // Additional overload provided because ExposedList only implements IEnumerable, + // leading to sub-optimal behavior: It grows multiple times as it assumes not + // to know the final size ahead of insertion. + public void AddRange (ExposedList list) { + CheckCollection(list); + + int collectionCount = list.Count; + if (collectionCount == 0) + return; + + GrowIfNeeded(collectionCount); + list.CopyTo(Items, Count); + Count += collectionCount; + + version++; + } + + public void AddRange (IEnumerable collection) { + CheckCollection(collection); + + ICollection c = collection as ICollection; + if (c != null) + AddCollection(c); + else + AddEnumerable(collection); + version++; + } + + public int BinarySearch (T item) { + return Array.BinarySearch(Items, 0, Count, item); + } + + public int BinarySearch (T item, IComparer comparer) { + return Array.BinarySearch(Items, 0, Count, item, comparer); + } + + public int BinarySearch (int index, int count, T item, IComparer comparer) { + CheckRange(index, count); + return Array.BinarySearch(Items, index, count, item, comparer); + } + + public void Clear (bool clearArray = true) { + if (clearArray) + Array.Clear(Items, 0, Items.Length); + + Count = 0; + version++; + } + + public bool Contains (T item) { + return Array.IndexOf(Items, item, 0, Count) != -1; + } + + public ExposedList ConvertAll (Converter converter) { + if (converter == null) + throw new ArgumentNullException("converter"); + ExposedList u = new ExposedList(Count); + u.Count = Count; + T[] items = Items; + TOutput[] uItems = u.Items; + for (int i = 0; i < Count; i++) + uItems[i] = converter(items[i]); + return u; + } + + public void CopyTo (T[] array) { + Array.Copy(Items, 0, array, 0, Count); + } + + public void CopyTo (T[] array, int arrayIndex) { + Array.Copy(Items, 0, array, arrayIndex, Count); + } + + public void CopyTo (int index, T[] array, int arrayIndex, int count) { + CheckRange(index, count); + Array.Copy(Items, index, array, arrayIndex, count); + } + + public bool Exists (Predicate match) { + CheckMatch(match); + return GetIndex(0, Count, match) != -1; + } + + public T Find (Predicate match) { + CheckMatch(match); + int i = GetIndex(0, Count, match); + return (i != -1) ? Items[i] : default(T); + } + + private static void CheckMatch (Predicate match) { + if (match == null) + throw new ArgumentNullException("match"); + } + + public ExposedList FindAll (Predicate match) { + CheckMatch(match); + return FindAllList(match); + } + + private ExposedList FindAllList (Predicate match) { + ExposedList results = new ExposedList(); + for (int i = 0; i < Count; i++) + if (match(Items[i])) + results.Add(Items[i]); + + return results; + } + + public int FindIndex (Predicate match) { + CheckMatch(match); + return GetIndex(0, Count, match); + } + + public int FindIndex (int startIndex, Predicate match) { + CheckMatch(match); + CheckIndex(startIndex); + return GetIndex(startIndex, Count - startIndex, match); + } + + public int FindIndex (int startIndex, int count, Predicate match) { + CheckMatch(match); + CheckRange(startIndex, count); + return GetIndex(startIndex, count, match); + } + + private int GetIndex (int startIndex, int count, Predicate match) { + int end = startIndex + count; + for (int i = startIndex; i < end; i++) + if (match(Items[i])) + return i; + + return -1; + } + + public T FindLast (Predicate match) { + CheckMatch(match); + int i = GetLastIndex(0, Count, match); + return i == -1 ? default(T) : Items[i]; + } + + public int FindLastIndex (Predicate match) { + CheckMatch(match); + return GetLastIndex(0, Count, match); + } + + public int FindLastIndex (int startIndex, Predicate match) { + CheckMatch(match); + CheckIndex(startIndex); + return GetLastIndex(0, startIndex + 1, match); + } + + public int FindLastIndex (int startIndex, int count, Predicate match) { + CheckMatch(match); + int start = startIndex - count + 1; + CheckRange(start, count); + return GetLastIndex(start, count, match); + } + + private int GetLastIndex (int startIndex, int count, Predicate match) { + // unlike FindLastIndex, takes regular params for search range + for (int i = startIndex + count; i != startIndex;) + if (match(Items[--i])) + return i; + return -1; + } + + public void ForEach (Action action) { + if (action == null) + throw new ArgumentNullException("action"); + for (int i = 0; i < Count; i++) + action(Items[i]); + } + + public Enumerator GetEnumerator () { + return new Enumerator(this); + } + + public ExposedList GetRange (int index, int count) { + CheckRange(index, count); + T[] tmpArray = new T[count]; + Array.Copy(Items, index, tmpArray, 0, count); + return new ExposedList(tmpArray, count); + } + + public int IndexOf (T item) { + return Array.IndexOf(Items, item, 0, Count); + } + + public int IndexOf (T item, int index) { + CheckIndex(index); + return Array.IndexOf(Items, item, index, Count - index); + } + + public int IndexOf (T item, int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + if ((uint)index + (uint)count > (uint)Count) + throw new ArgumentOutOfRangeException("index and count exceed length of list"); + + return Array.IndexOf(Items, item, index, count); + } + + private void Shift (int start, int delta) { + if (delta < 0) + start -= delta; + + if (start < Count) + Array.Copy(Items, start, Items, start + delta, Count - start); + + Count += delta; + + if (delta < 0) + Array.Clear(Items, Count, -delta); + } + + private void CheckIndex (int index) { + if (index < 0 || (uint)index > (uint)Count) + throw new ArgumentOutOfRangeException("index"); + } + + public void Insert (int index, T item) { + CheckIndex(index); + if (Count == Items.Length) + GrowIfNeeded(1); + Shift(index, 1); + Items[index] = item; + version++; + } + + private void CheckCollection (IEnumerable collection) { + if (collection == null) + throw new ArgumentNullException("collection"); + } + + public void InsertRange (int index, IEnumerable collection) { + CheckCollection(collection); + CheckIndex(index); + if (collection == this) { + T[] buffer = new T[Count]; + CopyTo(buffer, 0); + GrowIfNeeded(Count); + Shift(index, buffer.Length); + Array.Copy(buffer, 0, Items, index, buffer.Length); + } else { + ICollection c = collection as ICollection; + if (c != null) + InsertCollection(index, c); + else + InsertEnumeration(index, collection); + } + version++; + } + + private void InsertCollection (int index, ICollection collection) { + int collectionCount = collection.Count; + GrowIfNeeded(collectionCount); + + Shift(index, collectionCount); + collection.CopyTo(Items, index); + } + + private void InsertEnumeration (int index, IEnumerable enumerable) { + foreach (T t in enumerable) + Insert(index++, t); + } + + public int LastIndexOf (T item) { + return Array.LastIndexOf(Items, item, Count - 1, Count); + } + + public int LastIndexOf (T item, int index) { + CheckIndex(index); + return Array.LastIndexOf(Items, item, index, index + 1); + } + + public int LastIndexOf (T item, int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index", index, "index is negative"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count", count, "count is negative"); + + if (index - count + 1 < 0) + throw new ArgumentOutOfRangeException("count", count, "count is too large"); + + return Array.LastIndexOf(Items, item, index, count); + } + + public bool Remove (T item) { + int loc = IndexOf(item); + if (loc != -1) + RemoveAt(loc); + + return loc != -1; + } + + public int RemoveAll (Predicate match) { + CheckMatch(match); + int i = 0; + int j = 0; + + // Find the first item to remove + for (i = 0; i < Count; i++) + if (match(Items[i])) + break; + + if (i == Count) + return 0; + + version++; + + // Remove any additional items + for (j = i + 1; j < Count; j++) { + if (!match(Items[j])) + Items[i++] = Items[j]; + } + if (j - i > 0) + Array.Clear(Items, i, j - i); + + Count = i; + return (j - i); + } + + public void RemoveAt (int index) { + if (index < 0 || (uint)index >= (uint)Count) + throw new ArgumentOutOfRangeException("index"); + Shift(index, -1); + Array.Clear(Items, Count, 1); + version++; + } + + // Spine Added Method + // Based on Stack.Pop(); https://referencesource.microsoft.com/#mscorlib/system/collections/stack.cs + /// Pops the last item of the list. If the list is empty, Pop throws an InvalidOperationException. + public T Pop () { + if (Count == 0) + throw new InvalidOperationException("List is empty. Nothing to pop."); + + int i = Count - 1; + T item = Items[i]; + Items[i] = default(T); + Count--; + version++; + return item; + } + + public void RemoveRange (int index, int count) { + CheckRange(index, count); + if (count > 0) { + Shift(index, -count); + Array.Clear(Items, Count, count); + version++; + } + } + + public void Reverse () { + Array.Reverse(Items, 0, Count); + version++; + } + + public void Reverse (int index, int count) { + CheckRange(index, count); + Array.Reverse(Items, index, count); + version++; + } + + public void Sort () { + Array.Sort(Items, 0, Count, Comparer.Default); + version++; + } + + public void Sort (IComparer comparer) { + Array.Sort(Items, 0, Count, comparer); + version++; + } + + public void Sort (Comparison comparison) { + Array.Sort(Items, comparison); + version++; + } + + public void Sort (int index, int count, IComparer comparer) { + CheckRange(index, count); + Array.Sort(Items, index, count, comparer); + version++; + } + + public T[] ToArray () { + T[] t = new T[Count]; + Array.Copy(Items, t, Count); + + return t; + } + + public void TrimExcess () { + Capacity = Count; + } + + public bool TrueForAll (Predicate match) { + CheckMatch(match); + + for (int i = 0; i < Count; i++) + if (!match(Items[i])) + return false; + + return true; + } + + public int Capacity { + get { + return Items.Length; + } + set { + if ((uint)value < (uint)Count) + throw new ArgumentOutOfRangeException(); + + Array.Resize(ref Items, value); + } + } + + #region Interface implementations. + + IEnumerator IEnumerable.GetEnumerator () { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator () { + return GetEnumerator(); + } + + #endregion + + public struct Enumerator : IEnumerator, IDisposable { + private ExposedList l; + private int next; + private int ver; + private T current; + + internal Enumerator (ExposedList l) + : this() { + this.l = l; + ver = l.version; + } + + public void Dispose () { + l = null; + } + + private void VerifyState () { + if (l == null) + throw new ObjectDisposedException(GetType().FullName); + if (ver != l.version) + throw new InvalidOperationException( + "Collection was modified; enumeration operation may not execute."); + } + + public bool MoveNext () { + VerifyState(); + + if (next < 0) + return false; + + if (next < l.Count) { + current = l.Items[next++]; + return true; + } + + next = -1; + return false; + } + + public T Current { + get { + return current; + } + } + + void IEnumerator.Reset () { + VerifyState(); + next = 0; + } + + object IEnumerator.Current { + get { + VerifyState(); + if (next <= 0) + throw new InvalidOperationException(); + return current; + } + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IUpdatable.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IUpdatable.cs new file mode 100644 index 0000000..d2a47be --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IUpdatable.cs @@ -0,0 +1,42 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_0_64 { + + ///The interface for items updated by . + public interface IUpdatable { + void Update (); + + ///Returns false when this item has not been updated because a skin is required and the active + /// skin does not contain this item. + /// + /// + bool Active { get; } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IkConstraint.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IkConstraint.cs new file mode 100644 index 0000000..9f33a06 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IkConstraint.cs @@ -0,0 +1,368 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// + /// + /// Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of + /// the last bone is as close to the target bone as possible. + /// + /// See IK constraints in the Spine User Guide. + /// + public class IkConstraint : IUpdatable { + internal IkConstraintData data; + internal ExposedList bones = new ExposedList(); + internal Bone target; + internal int bendDirection; + internal bool compress, stretch; + internal float mix = 1, softness; + + internal bool active; + + public IkConstraint (IkConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + mix = data.mix; + softness = data.softness; + bendDirection = data.bendDirection; + compress = data.compress; + stretch = data.stretch; + + bones = new ExposedList(data.bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.FindBone(boneData.name)); + target = skeleton.FindBone(data.target.name); + } + + /// Copy constructor. + public IkConstraint (IkConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.Bones.Count); + foreach (Bone bone in constraint.Bones) + bones.Add(skeleton.Bones.Items[bone.data.index]); + target = skeleton.Bones.Items[constraint.target.data.index]; + mix = constraint.mix; + softness = constraint.softness; + bendDirection = constraint.bendDirection; + compress = constraint.compress; + stretch = constraint.stretch; + } + + public void Update () { + if (mix == 0) return; + Bone target = this.target; + var bones = this.bones.Items; + switch (this.bones.Count) { + case 1: + Apply(bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix); + break; + case 2: + Apply(bones[0], bones[1], target.worldX, target.worldY, bendDirection, stretch, data.uniform, softness, mix); + break; + } + } + + /// The bones that will be modified by this IK constraint. + public ExposedList Bones { + get { return bones; } + } + + /// The bone that is the IK target. + public Bone Target { + get { return target; } + set { target = value; } + } + + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + /// + /// For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. + /// + public float Mix { + get { return mix; } + set { mix = value; } + } + + /// For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones + /// will not straighten completely until the target is this far out of range. + public float Softness { + get { return softness; } + set { softness = value; } + } + + /// For two bone IK, controls the bend direction of the IK bones, either 1 or -1. + public int BendDirection { + get { return bendDirection; } + set { bendDirection = value; } + } + + /// For one bone IK, when true and the target is too close, the bone is scaled to reach it. + public bool Compress { + get { return compress; } + set { compress = value; } + } + + /// When true and the target is out of range, the parent bone is scaled to reach it. + /// + /// For two bone IK: 1) the child bone's local Y translation is set to 0, + /// 2) stretch is not applied if is > 0, + /// and 3) if the parent bone has local nonuniform scale, stretch is not applied. + /// + public bool Stretch { + get { return stretch; } + set { stretch = value; } + } + + public bool Active { + get { return active; } + } + + /// The IK constraint's setup pose data. + public IkConstraintData Data { + get { return data; } + } + + override public string ToString () { + return data.name; + } + + /// Applies 1 bone IK. The target is specified in the world coordinate system. + static public void Apply (Bone bone, float targetX, float targetY, bool compress, bool stretch, bool uniform, + float alpha) { + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + Bone p = bone.parent; + + float pa = p.a, pb = p.b, pc = p.c, pd = p.d; + float rotationIK = -bone.ashearX - bone.arotation; + float tx = 0, ty = 0; + + switch (bone.data.transformMode) { + case TransformMode.OnlyTranslation: + tx = targetX - bone.worldX; + ty = targetY - bone.worldY; + break; + case TransformMode.NoRotationOrReflection: { + float s = Math.Abs(pa * pd - pb * pc) / (pa * pa + pc * pc); + float sa = pa / bone.skeleton.ScaleX; + float sc = pc / bone.skeleton.ScaleY; + pb = -sc * s * bone.skeleton.ScaleX; + pd = sa * s * bone.skeleton.ScaleY; + rotationIK += (float)Math.Atan2(sc, sa) * MathUtils.RadDeg; + goto default; // Fall through. + } + default: { + float x = targetX - p.worldX, y = targetY - p.worldY; + float d = pa * pd - pb * pc; + tx = (x * pd - y * pb) / d - bone.ax; + ty = (y * pa - x * pc) / d - bone.ay; + break; + } + } + + rotationIK += (float)Math.Atan2(ty, tx) * MathUtils.RadDeg; + if (bone.ascaleX < 0) rotationIK += 180; + if (rotationIK > 180) + rotationIK -= 360; + else if (rotationIK < -180) // + rotationIK += 360; + + float sx = bone.ascaleX, sy = bone.ascaleY; + if (compress || stretch) { + switch (bone.data.transformMode) { + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: + tx = targetX - bone.worldX; + ty = targetY - bone.worldY; + break; + } + float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty); + if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) { + float s = (dd / b - 1) * alpha + 1; + sx *= s; + if (uniform) sy *= s; + } + } + bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY); + } + + /// Applies 2 bone IK. The target is specified in the world coordinate system. + /// A direct descendant of the parent bone. + static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, bool uniform, + float softness, float alpha) { + if (parent == null) throw new ArgumentNullException("parent", "parent cannot be null."); + if (child == null) throw new ArgumentNullException("child", "child cannot be null."); + float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, sx = psx, sy = psy, csx = child.ascaleX; + int os1, os2, s2; + if (psx < 0) { + psx = -psx; + os1 = 180; + s2 = -1; + } else { + os1 = 0; + s2 = 1; + } + if (psy < 0) { + psy = -psy; + s2 = -s2; + } + if (csx < 0) { + csx = -csx; + os2 = 180; + } else + os2 = 0; + float cx = child.ax, cy, cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d; + bool u = Math.Abs(psx - psy) <= 0.0001f; + if (!u || stretch) { + cy = 0; + cwx = a * cx + parent.worldX; + cwy = c * cx + parent.worldY; + } else { + cy = child.ay; + cwx = a * cx + b * cy + parent.worldX; + cwy = c * cx + d * cy + parent.worldY; + } + Bone pp = parent.parent; + a = pp.a; + b = pp.b; + c = pp.c; + d = pp.d; + float id = 1 / (a * d - b * c), x = cwx - pp.worldX, y = cwy - pp.worldY; + float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py; + float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2; + if (l1 < 0.0001f) { + Apply(parent, targetX, targetY, false, stretch, false, alpha); + child.UpdateWorldTransform(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + return; + } + x = targetX - pp.worldX; + y = targetY - pp.worldY; + float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py; + float dd = tx * tx + ty * ty; + if (softness != 0) { + softness *= psx * (csx + 1) * 0.5f; + float td = (float)Math.Sqrt(dd), sd = td - l1 - l2 * psx + softness; + if (sd > 0) { + float p = Math.Min(1, sd / (softness * 2)) - 1; + p = (sd - softness * (1 - p * p)) / td; + tx -= p * tx; + ty -= p * ty; + dd = tx * tx + ty * ty; + } + } + if (u) { + l2 *= psx; + float cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2); + if (cos < -1) { + cos = -1; + a2 = MathUtils.PI * bendDir; + } else if (cos > 1) { + cos = 1; + a2 = 0; + if (stretch) { + a = ((float)Math.Sqrt(dd) / (l1 + l2) - 1) * alpha + 1; + sx *= a; + if (uniform) sy *= a; + } + } else + a2 = (float)Math.Acos(cos) * bendDir; + a = l1 + l2 * cos; + b = l2 * (float)Math.Sin(a2); + a1 = (float)Math.Atan2(ty * a - tx * b, tx * a + ty * b); + } else { + a = psx * l2; + b = psy * l2; + float aa = a * a, bb = b * b, ta = (float)Math.Atan2(ty, tx); + c = bb * l1 * l1 + aa * dd - aa * bb; + float c1 = -2 * bb * l1, c2 = bb - aa; + d = c1 * c1 - 4 * c2 * c; + if (d >= 0) { + float q = (float)Math.Sqrt(d); + if (c1 < 0) q = -q; + q = -(c1 + q) * 0.5f; + float r0 = q / c2, r1 = c / q; + float r = Math.Abs(r0) < Math.Abs(r1) ? r0 : r1; + if (r * r <= dd) { + y = (float)Math.Sqrt(dd - r * r) * bendDir; + a1 = ta - (float)Math.Atan2(y, r); + a2 = (float)Math.Atan2(y / psy, (r - l1) / psx); + goto break_outer; // break outer; + } + } + float minAngle = MathUtils.PI, minX = l1 - a, minDist = minX * minX, minY = 0; + float maxAngle = 0, maxX = l1 + a, maxDist = maxX * maxX, maxY = 0; + c = -a * l1 / (aa - bb); + if (c >= -1 && c <= 1) { + c = (float)Math.Acos(c); + x = a * (float)Math.Cos(c) + l1; + y = b * (float)Math.Sin(c); + d = x * x + y * y; + if (d < minDist) { + minAngle = c; + minDist = d; + minX = x; + minY = y; + } + if (d > maxDist) { + maxAngle = c; + maxDist = d; + maxX = x; + maxY = y; + } + } + if (dd <= (minDist + maxDist) * 0.5f) { + a1 = ta - (float)Math.Atan2(minY * bendDir, minX); + a2 = minAngle * bendDir; + } else { + a1 = ta - (float)Math.Atan2(maxY * bendDir, maxX); + a2 = maxAngle * bendDir; + } + } + break_outer: + float os = (float)Math.Atan2(cy, cx) * s2; + float rotation = parent.arotation; + a1 = (a1 - os) * MathUtils.RadDeg + os1 - rotation; + if (a1 > 180) + a1 -= 360; + else if (a1 < -180) + a1 += 360; + parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, sx, sy, 0, 0); + rotation = child.arotation; + a2 = ((a2 + os) * MathUtils.RadDeg - child.ashearX) * s2 + os2 - rotation; + if (a2 > 180) + a2 -= 360; + else if (a2 < -180) + a2 += 360; + child.UpdateWorldTransform(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IkConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IkConstraintData.cs new file mode 100644 index 0000000..3281d1a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/IkConstraintData.cs @@ -0,0 +1,103 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_0_64 { + /// Stores the setup pose for an IkConstraint. + public class IkConstraintData : ConstraintData { + internal ExposedList bones = new ExposedList(); + internal BoneData target; + internal int bendDirection = 1; + internal bool compress, stretch, uniform; + internal float mix = 1, softness; + + public IkConstraintData (string name) : base(name) { + } + + /// The bones that are constrained by this IK Constraint. + public ExposedList Bones { + get { return bones; } + } + + /// The bone that is the IK target. + public BoneData Target { + get { return target; } + set { target = value; } + } + + /// + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + /// + /// For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. + /// + public float Mix { + get { return mix; } + set { mix = value; } + } + + /// For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones + /// will not straighten completely until the target is this far out of range. + public float Softness { + get { return softness; } + set { softness = value; } + } + + /// For two bone IK, controls the bend direction of the IK bones, either 1 or -1. + public int BendDirection { + get { return bendDirection; } + set { bendDirection = value; } + } + + /// For one bone IK, when true and the target is too close, the bone is scaled to reach it. + public bool Compress { + get { return compress; } + set { compress = value; } + } + + /// When true and the target is out of range, the parent bone is scaled to reach it. + /// + /// For two bone IK: 1) the child bone's local Y translation is set to 0, + /// 2) stretch is not applied if is > 0, + /// and 3) if the parent bone has local nonuniform scale, stretch is not applied. + public bool Stretch { + get { return stretch; } + set { stretch = value; } + } + + /// + /// When true and or is used, the bone is scaled on both the X and Y axes. + /// + public bool Uniform { + get { return uniform; } + set { uniform = value; } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Json.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Json.cs new file mode 100644 index 0000000..894ffdc --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Json.cs @@ -0,0 +1,517 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + +namespace Spine4_0_64 { + public static class Json { + public static object Deserialize (TextReader text) { + var parser = new SharpJson.JsonDecoder(); + parser.parseNumbersAsFloat = true; + return parser.Decode(text.ReadToEnd()); + } + } +} + +/** + * Copyright (c) 2016 Adriano Tinoco d'Oliveira Rezende + * + * Based on the JSON parser by Patrick van Bergen + * http://techblog.procurios.nl/k/news/view/14605/14863/how-do-i-write-my-own-parser-(for-json).html + * + * Changes made: + * + * - Optimized parser speed (deserialize roughly near 3x faster than original) + * - Added support to handle lexer/parser error messages with line numbers + * - Added more fine grained control over type conversions during the parsing + * - Refactory API (Separate Lexer code from Parser code and the Encoder from Decoder) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +namespace Spine4_0_64 { + class Lexer { + public enum Token { + None, + Null, + True, + False, + Colon, + Comma, + String, + Number, + CurlyOpen, + CurlyClose, + SquaredOpen, + SquaredClose, + }; + + public bool hasError { + get { + return !success; + } + } + + public int lineNumber { + get; + private set; + } + + public bool parseNumbersAsFloat { + get; + set; + } + + char[] json; + int index = 0; + bool success = true; + char[] stringBuffer = new char[4096]; + + public Lexer (string text) { + Reset(); + + json = text.ToCharArray(); + parseNumbersAsFloat = false; + } + + public void Reset () { + index = 0; + lineNumber = 1; + success = true; + } + + public string ParseString () { + int idx = 0; + StringBuilder builder = null; + + SkipWhiteSpaces(); + + // " + char c = json[index++]; + + bool failed = false; + bool complete = false; + + while (!complete && !failed) { + if (index == json.Length) + break; + + c = json[index++]; + if (c == '"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.Length) + break; + + c = json[index++]; + + switch (c) { + case '"': + stringBuffer[idx++] = '"'; + break; + case '\\': + stringBuffer[idx++] = '\\'; + break; + case '/': + stringBuffer[idx++] = '/'; + break; + case 'b': + stringBuffer[idx++] = '\b'; + break; + case 'f': + stringBuffer[idx++] = '\f'; + break; + case 'n': + stringBuffer[idx++] = '\n'; + break; + case 'r': + stringBuffer[idx++] = '\r'; + break; + case 't': + stringBuffer[idx++] = '\t'; + break; + case 'u': + int remainingLength = json.Length - index; + if (remainingLength >= 4) { + var hex = new string(json, index, 4); + + // XXX: handle UTF + stringBuffer[idx++] = (char)Convert.ToInt32(hex, 16); + + // skip 4 chars + index += 4; + } else { + failed = true; + } + break; + } + } else { + stringBuffer[idx++] = c; + } + + if (idx >= stringBuffer.Length) { + if (builder == null) + builder = new StringBuilder(); + + builder.Append(stringBuffer, 0, idx); + idx = 0; + } + } + + if (!complete) { + success = false; + return null; + } + + if (builder != null) + return builder.ToString(); + else + return new string(stringBuffer, 0, idx); + } + + string GetNumberString () { + SkipWhiteSpaces(); + + int lastIndex = GetLastIndexOfNumber(index); + int charLength = (lastIndex - index) + 1; + + var result = new string(json, index, charLength); + + index = lastIndex + 1; + + return result; + } + + public float ParseFloatNumber () { + float number; + var str = GetNumberString(); + + if (!float.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out number)) + return 0; + + return number; + } + + public double ParseDoubleNumber () { + double number; + var str = GetNumberString(); + + if (!double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) + return 0; + + return number; + } + + int GetLastIndexOfNumber (int index) { + int lastIndex; + + for (lastIndex = index; lastIndex < json.Length; lastIndex++) { + char ch = json[lastIndex]; + + if ((ch < '0' || ch > '9') && ch != '+' && ch != '-' + && ch != '.' && ch != 'e' && ch != 'E') + break; + } + + return lastIndex - 1; + } + + void SkipWhiteSpaces () { + for (; index < json.Length; index++) { + char ch = json[index]; + + if (ch == '\n') + lineNumber++; + + if (!char.IsWhiteSpace(json[index])) + break; + } + } + + public Token LookAhead () { + SkipWhiteSpaces(); + + int savedIndex = index; + return NextToken(json, ref savedIndex); + } + + public Token NextToken () { + SkipWhiteSpaces(); + return NextToken(json, ref index); + } + + static Token NextToken (char[] json, ref int index) { + if (index == json.Length) + return Token.None; + + char c = json[index++]; + + switch (c) { + case '{': + return Token.CurlyOpen; + case '}': + return Token.CurlyClose; + case '[': + return Token.SquaredOpen; + case ']': + return Token.SquaredClose; + case ',': + return Token.Comma; + case '"': + return Token.String; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return Token.Number; + case ':': + return Token.Colon; + } + + index--; + + int remainingLength = json.Length - index; + + // false + if (remainingLength >= 5) { + if (json[index] == 'f' && + json[index + 1] == 'a' && + json[index + 2] == 'l' && + json[index + 3] == 's' && + json[index + 4] == 'e') { + index += 5; + return Token.False; + } + } + + // true + if (remainingLength >= 4) { + if (json[index] == 't' && + json[index + 1] == 'r' && + json[index + 2] == 'u' && + json[index + 3] == 'e') { + index += 4; + return Token.True; + } + } + + // null + if (remainingLength >= 4) { + if (json[index] == 'n' && + json[index + 1] == 'u' && + json[index + 2] == 'l' && + json[index + 3] == 'l') { + index += 4; + return Token.Null; + } + } + + return Token.None; + } + } + + public class JsonDecoder { + public string errorMessage { + get; + private set; + } + + public bool parseNumbersAsFloat { + get; + set; + } + + Lexer lexer; + + public JsonDecoder () { + errorMessage = null; + parseNumbersAsFloat = false; + } + + public object Decode (string text) { + errorMessage = null; + + lexer = new Lexer(text); + lexer.parseNumbersAsFloat = parseNumbersAsFloat; + + return ParseValue(); + } + + public static object DecodeText (string text) { + var builder = new JsonDecoder(); + return builder.Decode(text); + } + + IDictionary ParseObject () { + var table = new Dictionary(); + + // { + lexer.NextToken(); + + while (true) { + var token = lexer.LookAhead(); + + switch (token) { + case Lexer.Token.None: + TriggerError("Invalid token"); + return null; + case Lexer.Token.Comma: + lexer.NextToken(); + break; + case Lexer.Token.CurlyClose: + lexer.NextToken(); + return table; + default: + // name + string name = EvalLexer(lexer.ParseString()); + + if (errorMessage != null) + return null; + + // : + token = lexer.NextToken(); + + if (token != Lexer.Token.Colon) { + TriggerError("Invalid token; expected ':'"); + return null; + } + + // value + object value = ParseValue(); + + if (errorMessage != null) + return null; + + table[name] = value; + break; + } + } + + //return null; // Unreachable code + } + + IList ParseArray () { + var array = new List(); + + // [ + lexer.NextToken(); + + while (true) { + var token = lexer.LookAhead(); + + switch (token) { + case Lexer.Token.None: + TriggerError("Invalid token"); + return null; + case Lexer.Token.Comma: + lexer.NextToken(); + break; + case Lexer.Token.SquaredClose: + lexer.NextToken(); + return array; + default: + object value = ParseValue(); + + if (errorMessage != null) + return null; + + array.Add(value); + break; + } + } + + //return null; // Unreachable code + } + + object ParseValue () { + switch (lexer.LookAhead()) { + case Lexer.Token.String: + return EvalLexer(lexer.ParseString()); + case Lexer.Token.Number: + if (parseNumbersAsFloat) + return EvalLexer(lexer.ParseFloatNumber()); + else + return EvalLexer(lexer.ParseDoubleNumber()); + case Lexer.Token.CurlyOpen: + return ParseObject(); + case Lexer.Token.SquaredOpen: + return ParseArray(); + case Lexer.Token.True: + lexer.NextToken(); + return true; + case Lexer.Token.False: + lexer.NextToken(); + return false; + case Lexer.Token.Null: + lexer.NextToken(); + return null; + case Lexer.Token.None: + break; + } + + TriggerError("Unable to parse value"); + return null; + } + + void TriggerError (string message) { + errorMessage = string.Format("Error: '{0}' at line {1}", + message, lexer.lineNumber); + } + + T EvalLexer (T value) { + if (lexer.hasError) + TriggerError("Lexical error ocurred"); + + return value; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/MathUtils.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/MathUtils.cs new file mode 100644 index 0000000..7ccf780 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/MathUtils.cs @@ -0,0 +1,173 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +//#define USE_FAST_SIN_COS_ATAN2_APPROXIMATIONS + +using System; + +namespace Spine4_0_64 { + public static class MathUtils { + public const float PI = 3.1415927f; + public const float PI2 = PI * 2; + public const float RadDeg = 180f / PI; + public const float DegRad = PI / 180; + + static Random random = new Random(); + +#if USE_FAST_SIN_COS_ATAN2_APPROXIMATIONS + const int SIN_BITS = 14; // 16KB. Adjust for accuracy. + const int SIN_MASK = ~(-1 << SIN_BITS); + const int SIN_COUNT = SIN_MASK + 1; + const float RadFull = PI * 2; + const float DegFull = 360; + const float RadToIndex = SIN_COUNT / RadFull; + const float DegToIndex = SIN_COUNT / DegFull; + static float[] sin = new float[SIN_COUNT]; + + static MathUtils () { + for (int i = 0; i < SIN_COUNT; i++) + sin[i] = (float)Math.Sin((i + 0.5f) / SIN_COUNT * RadFull); + for (int i = 0; i < 360; i += 90) + sin[(int)(i * DegToIndex) & SIN_MASK] = (float)Math.Sin(i * DegRad); + } + + /// Returns the sine of a given angle in radians from a lookup table. + static public float Sin (float radians) { + return sin[(int)(radians * RadToIndex) & SIN_MASK]; + } + + /// Returns the cosine of a given angle in radians from a lookup table. + static public float Cos (float radians) { + return sin[(int)((radians + PI / 2) * RadToIndex) & SIN_MASK]; + } + + /// Returns the sine of a given angle in degrees from a lookup table. + static public float SinDeg (float degrees) { + return sin[(int)(degrees * DegToIndex) & SIN_MASK]; + } + + /// Returns the cosine of a given angle in degrees from a lookup table. + static public float CosDeg (float degrees) { + return sin[(int)((degrees + 90) * DegToIndex) & SIN_MASK]; + } + + /// Returns atan2 in radians, faster but less accurate than Math.Atan2. Average error of 0.00231 radians (0.1323 + /// degrees), largest error of 0.00488 radians (0.2796 degrees). + static public float Atan2 (float y, float x) { + if (x == 0f) { + if (y > 0f) return PI / 2; + if (y == 0f) return 0f; + return -PI / 2; + } + float atan, z = y / x; + if (Math.Abs(z) < 1f) { + atan = z / (1f + 0.28f * z * z); + if (x < 0f) return atan + (y < 0f ? -PI : PI); + return atan; + } + atan = PI / 2 - z / (z * z + 0.28f); + return y < 0f ? atan - PI : atan; + } +#else + /// Returns the sine of a given angle in radians. + static public float Sin (float radians) { + return (float)Math.Sin(radians); + } + + /// Returns the cosine of a given angle in radians. + static public float Cos (float radians) { + return (float)Math.Cos(radians); + } + + /// Returns the sine of a given angle in degrees. + static public float SinDeg (float degrees) { + return (float)Math.Sin(degrees * DegRad); + } + + /// Returns the cosine of a given angle in degrees. + static public float CosDeg (float degrees) { + return (float)Math.Cos(degrees * DegRad); + } + + /// Returns the atan2 using Math.Atan2. + static public float Atan2 (float y, float x) { + return (float)Math.Atan2(y, x); + } +#endif + static public float Clamp (float value, float min, float max) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + static public float RandomTriangle (float min, float max) { + return RandomTriangle(min, max, (min + max) * 0.5f); + } + + static public float RandomTriangle (float min, float max, float mode) { + float u = (float)random.NextDouble(); + float d = max - min; + if (u <= (mode - min) / d) return min + (float)Math.Sqrt(u * d * (mode - min)); + return max - (float)Math.Sqrt((1 - u) * d * (max - mode)); + } + } + + public abstract class IInterpolation { + public static IInterpolation Pow2 = new Pow(2); + public static IInterpolation Pow2Out = new PowOut(2); + + protected abstract float Apply (float a); + + public float Apply (float start, float end, float a) { + return start + (end - start) * Apply(a); + } + } + + public class Pow : IInterpolation { + public float Power { get; set; } + + public Pow (float power) { + Power = power; + } + + protected override float Apply (float a) { + if (a <= 0.5f) return (float)Math.Pow(a * 2, Power) / 2; + return (float)Math.Pow((a - 1) * 2, Power) / (Power % 2 == 0 ? -2 : 2) + 1; + } + } + + public class PowOut : Pow { + public PowOut (float power) : base(power) { + } + + protected override float Apply (float a) { + return (float)Math.Pow(a - 1, Power) * (Power % 2 == 0 ? -1 : 1) + 1; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/PathConstraint.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/PathConstraint.cs new file mode 100644 index 0000000..7469b40 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/PathConstraint.cs @@ -0,0 +1,510 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + + /// + /// + /// Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the + /// constrained bones so they follow a . + /// + /// See Path constraints in the Spine User Guide. + /// + public class PathConstraint : IUpdatable { + const int NONE = -1, BEFORE = -2, AFTER = -3; + const float Epsilon = 0.00001f; + + internal PathConstraintData data; + internal ExposedList bones; + internal Slot target; + internal float position, spacing, mixRotate, mixX, mixY; + + internal bool active; + + internal ExposedList spaces = new ExposedList(), positions = new ExposedList(); + internal ExposedList world = new ExposedList(), curves = new ExposedList(), lengths = new ExposedList(); + internal float[] segments = new float[10]; + + public PathConstraint (PathConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + bones = new ExposedList(data.Bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.FindBone(boneData.name)); + target = skeleton.FindSlot(data.target.name); + position = data.position; + spacing = data.spacing; + mixRotate = data.mixRotate; + mixX = data.mixX; + mixY = data.mixY; + } + + /// Copy constructor. + public PathConstraint (PathConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.Bones.Count); + foreach (Bone bone in constraint.Bones) + bones.Add(skeleton.Bones.Items[bone.data.index]); + target = skeleton.slots.Items[constraint.target.data.index]; + position = constraint.position; + spacing = constraint.spacing; + mixRotate = constraint.mixRotate; + mixX = constraint.mixX; + mixY = constraint.mixY; + } + + public static void ArraysFill (float[] a, int fromIndex, int toIndex, float val) { + for (int i = fromIndex; i < toIndex; i++) + a[i] = val; + } + + public void Update () { + PathAttachment attachment = target.Attachment as PathAttachment; + if (attachment == null) return; + + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY; + if (mixRotate == 0 && mixX == 0 && mixY == 0) return; + + PathConstraintData data = this.data; + bool tangents = data.rotateMode == RotateMode.Tangent, scale = data.rotateMode == RotateMode.ChainScale; + int boneCount = this.bones.Count, spacesCount = tangents ? boneCount : boneCount + 1; + Bone[] bonesItems = this.bones.Items; + float[] spaces = this.spaces.Resize(spacesCount).Items, lengths = scale ? this.lengths.Resize(boneCount).Items : null; + float spacing = this.spacing; + switch (data.spacingMode) { + case SpacingMode.Percent: + if (scale) { + for (int i = 0, n = spacesCount - 1; i < n; i++) { + Bone bone = bonesItems[i]; + float setupLength = bone.data.length; + if (setupLength < PathConstraint.Epsilon) + lengths[i] = 0; + else { + float x = setupLength * bone.a, y = setupLength * bone.c; + lengths[i] = (float)Math.Sqrt(x * x + y * y); + } + } + } + ArraysFill(spaces, 1, spacesCount, spacing); + break; + case SpacingMode.Proportional: { + float sum = 0; + for (int i = 0, n = spacesCount - 1; i < n;) { + Bone bone = bonesItems[i]; + float setupLength = bone.data.length; + if (setupLength < PathConstraint.Epsilon) { + if (scale) lengths[i] = 0; + spaces[++i] = spacing; + } else { + float x = setupLength * bone.a, y = setupLength * bone.c; + float length = (float)Math.Sqrt(x * x + y * y); + if (scale) lengths[i] = length; + spaces[++i] = length; + sum += length; + } + } + if (sum > 0) { + sum = spacesCount / sum * spacing; + for (int i = 1; i < spacesCount; i++) + spaces[i] *= sum; + } + break; + } + default: { + bool lengthSpacing = data.spacingMode == SpacingMode.Length; + for (int i = 0, n = spacesCount - 1; i < n;) { + Bone bone = bonesItems[i]; + float setupLength = bone.data.length; + if (setupLength < PathConstraint.Epsilon) { + if (scale) lengths[i] = 0; + spaces[++i] = spacing; + } else { + float x = setupLength * bone.a, y = setupLength * bone.c; + float length = (float)Math.Sqrt(x * x + y * y); + if (scale) lengths[i] = length; + spaces[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength; + } + } + break; + } + } + + float[] positions = ComputeWorldPositions(attachment, spacesCount, tangents); + float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation; + bool tip; + if (offsetRotation == 0) { + tip = data.rotateMode == RotateMode.Chain; + } else { + tip = false; + Bone p = target.bone; + offsetRotation *= p.a * p.d - p.b * p.c > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + } + for (int i = 0, p = 3; i < boneCount; i++, p += 3) { + Bone bone = bonesItems[i]; + bone.worldX += (boneX - bone.worldX) * mixX; + bone.worldY += (boneY - bone.worldY) * mixY; + float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY; + if (scale) { + float length = lengths[i]; + if (length >= PathConstraint.Epsilon) { + float s = ((float)Math.Sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1; + bone.a *= s; + bone.c *= s; + } + } + boneX = x; + boneY = y; + if (mixRotate > 0) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d, r, cos, sin; + if (tangents) + r = positions[p - 1]; + else if (spaces[i + 1] < PathConstraint.Epsilon) + r = positions[p + 2]; + else + r = MathUtils.Atan2(dy, dx); + r -= MathUtils.Atan2(c, a); + if (tip) { + cos = MathUtils.Cos(r); + sin = MathUtils.Sin(r); + float length = bone.data.length; + boneX += (length * (cos * a - sin * c) - dx) * mixRotate; + boneY += (length * (sin * a + cos * c) - dy) * mixRotate; + } else + r += offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r *= mixRotate; + cos = MathUtils.Cos(r); + sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + bone.UpdateAppliedTransform(); + } + } + + float[] ComputeWorldPositions (PathAttachment path, int spacesCount, bool tangents) { + Slot target = this.target; + float position = this.position; + float[] spaces = this.spaces.Items, output = this.positions.Resize(spacesCount * 3 + 2).Items, world; + bool closed = path.Closed; + int verticesLength = path.WorldVerticesLength, curveCount = verticesLength / 6, prevCurve = NONE; + + float pathLength, multiplier; + if (!path.ConstantSpeed) { + float[] lengths = path.Lengths; + curveCount -= closed ? 1 : 2; + pathLength = lengths[curveCount]; + + if (data.positionMode == PositionMode.Percent) position *= pathLength; + + switch (data.spacingMode) { + case SpacingMode.Percent: + multiplier = pathLength; + break; + case SpacingMode.Proportional: + multiplier = pathLength / spacesCount; + break; + default: + multiplier = 1; + break; + } + + world = this.world.Resize(8).Items; + for (int i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) { + float space = spaces[i] * multiplier; + position += space; + float p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + if (prevCurve != BEFORE) { + prevCurve = BEFORE; + path.ComputeWorldVertices(target, 2, 4, world, 0, 2); + } + AddBeforePosition(p, world, 0, output, o); + continue; + } else if (p > pathLength) { + if (prevCurve != AFTER) { + prevCurve = AFTER; + path.ComputeWorldVertices(target, verticesLength - 6, 4, world, 0, 2); + } + AddAfterPosition(p - pathLength, world, 0, output, o); + continue; + } + + // Determine curve containing position. + for (; ; curve++) { + float length = lengths[curve]; + if (p > length) continue; + if (curve == 0) + p /= length; + else { + float prev = lengths[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + if (curve != prevCurve) { + prevCurve = curve; + if (closed && curve == curveCount) { + path.ComputeWorldVertices(target, verticesLength - 4, 4, world, 0, 2); + path.ComputeWorldVertices(target, 0, 4, world, 4, 2); + } else + path.ComputeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2); + } + AddCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], output, o, + tangents || (i > 0 && space < PathConstraint.Epsilon)); + } + return output; + } + + // World vertices. + if (closed) { + verticesLength += 2; + world = this.world.Resize(verticesLength).Items; + path.ComputeWorldVertices(target, 2, verticesLength - 4, world, 0, 2); + path.ComputeWorldVertices(target, 0, 2, world, verticesLength - 4, 2); + world[verticesLength - 2] = world[0]; + world[verticesLength - 1] = world[1]; + } else { + curveCount--; + verticesLength -= 4; + world = this.world.Resize(verticesLength).Items; + path.ComputeWorldVertices(target, 2, verticesLength, world, 0, 2); + } + + // Curve lengths. + float[] curves = this.curves.Resize(curveCount).Items; + pathLength = 0; + float x1 = world[0], y1 = world[1], cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0; + float tmpx, tmpy, dddfx, dddfy, ddfx, ddfy, dfx, dfy; + for (int i = 0, w = 2; i < curveCount; i++, w += 6) { + cx1 = world[w]; + cy1 = world[w + 1]; + cx2 = world[w + 2]; + cy2 = world[w + 3]; + x2 = world[w + 4]; + y2 = world[w + 5]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.1875f; + tmpy = (y1 - cy1 * 2 + cy2) * 0.1875f; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375f; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375f; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.75f + tmpx + dddfx * 0.16666667f; + dfy = (cy1 - y1) * 0.75f + tmpy + dddfy * 0.16666667f; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + curves[i] = pathLength; + x1 = x2; + y1 = y2; + } + + if (data.positionMode == PositionMode.Percent) position *= pathLength; + + switch (data.spacingMode) { + case SpacingMode.Percent: + multiplier = pathLength; + break; + case SpacingMode.Proportional: + multiplier = pathLength / spacesCount; + break; + default: + multiplier = 1; + break; + } + + float[] segments = this.segments; + float curveLength = 0; + for (int i = 0, o = 0, curve = 0, segment = 0; i < spacesCount; i++, o += 3) { + float space = spaces[i] * multiplier; + position += space; + float p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + AddBeforePosition(p, world, 0, output, o); + continue; + } else if (p > pathLength) { + AddAfterPosition(p - pathLength, world, verticesLength - 4, output, o); + continue; + } + + // Determine curve containing position. + for (; ; curve++) { + float length = curves[curve]; + if (p > length) continue; + if (curve == 0) + p /= length; + else { + float prev = curves[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + + // Curve segment lengths. + if (curve != prevCurve) { + prevCurve = curve; + int ii = curve * 6; + x1 = world[ii]; + y1 = world[ii + 1]; + cx1 = world[ii + 2]; + cy1 = world[ii + 3]; + cx2 = world[ii + 4]; + cy2 = world[ii + 5]; + x2 = world[ii + 6]; + y2 = world[ii + 7]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.03f; + tmpy = (y1 - cy1 * 2 + cy2) * 0.03f; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006f; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006f; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.3f + tmpx + dddfx * 0.16666667f; + dfy = (cy1 - y1) * 0.3f + tmpy + dddfy * 0.16666667f; + curveLength = (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[0] = curveLength; + for (ii = 1; ii < 8; ii++) { + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[ii] = curveLength; + } + dfx += ddfx; + dfy += ddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[8] = curveLength; + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[9] = curveLength; + segment = 0; + } + + // Weight by segment length. + p *= curveLength; + for (; ; segment++) { + float length = segments[segment]; + if (p > length) continue; + if (segment == 0) + p /= length; + else { + float prev = segments[segment - 1]; + p = segment + (p - prev) / (length - prev); + } + break; + } + AddCurvePosition(p * 0.1f, x1, y1, cx1, cy1, cx2, cy2, x2, y2, output, o, tangents || (i > 0 && space < PathConstraint.Epsilon)); + } + return output; + } + + static void AddBeforePosition (float p, float[] temp, int i, float[] output, int o) { + float x1 = temp[i], y1 = temp[i + 1], dx = temp[i + 2] - x1, dy = temp[i + 3] - y1, r = MathUtils.Atan2(dy, dx); + output[o] = x1 + p * MathUtils.Cos(r); + output[o + 1] = y1 + p * MathUtils.Sin(r); + output[o + 2] = r; + } + + static void AddAfterPosition (float p, float[] temp, int i, float[] output, int o) { + float x1 = temp[i + 2], y1 = temp[i + 3], dx = x1 - temp[i], dy = y1 - temp[i + 1], r = MathUtils.Atan2(dy, dx); + output[o] = x1 + p * MathUtils.Cos(r); + output[o + 1] = y1 + p * MathUtils.Sin(r); + output[o + 2] = r; + } + + static void AddCurvePosition (float p, float x1, float y1, float cx1, float cy1, float cx2, float cy2, float x2, float y2, + float[] output, int o, bool tangents) { + if (p < PathConstraint.Epsilon || float.IsNaN(p)) { + output[o] = x1; + output[o + 1] = y1; + output[o + 2] = (float)Math.Atan2(cy1 - y1, cx1 - x1); + return; + } + float tt = p * p, ttt = tt * p, u = 1 - p, uu = u * u, uuu = uu * u; + float ut = u * p, ut3 = ut * 3, uut3 = u * ut3, utt3 = ut3 * p; + float x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt, y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt; + output[o] = x; + output[o + 1] = y; + if (tangents) { + if (p < 0.001f) + output[o + 2] = (float)Math.Atan2(cy1 - y1, cx1 - x1); + else + output[o + 2] = (float)Math.Atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)); + } + } + + /// The position along the path. + public float Position { get { return position; } set { position = value; } } + /// The spacing between bones. + public float Spacing { get { return spacing; } set { spacing = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. + public float MixRotate { get { return mixRotate; } set { mixRotate = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. + public float MixX { get { return mixX; } set { mixX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. + public float MixY { get { return mixY; } set { mixY = value; } } + /// The bones that will be modified by this path constraint. + public ExposedList Bones { get { return bones; } } + /// The slot whose path attachment will be used to constrained the bones. + public Slot Target { get { return target; } set { target = value; } } + public bool Active { get { return active; } } + /// The path constraint's setup pose data. + public PathConstraintData Data { get { return data; } } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/PathConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/PathConstraintData.cs new file mode 100644 index 0000000..99a3587 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/PathConstraintData.cs @@ -0,0 +1,72 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + public class PathConstraintData : ConstraintData { + internal ExposedList bones = new ExposedList(); + internal SlotData target; + internal PositionMode positionMode; + internal SpacingMode spacingMode; + internal RotateMode rotateMode; + internal float offsetRotation; + internal float position, spacing, mixRotate, mixX, mixY; + + public PathConstraintData (string name) : base(name) { + } + + public ExposedList Bones { get { return bones; } } + public SlotData Target { get { return target; } set { target = value; } } + public PositionMode PositionMode { get { return positionMode; } set { positionMode = value; } } + public SpacingMode SpacingMode { get { return spacingMode; } set { spacingMode = value; } } + public RotateMode RotateMode { get { return rotateMode; } set { rotateMode = value; } } + public float OffsetRotation { get { return offsetRotation; } set { offsetRotation = value; } } + public float Position { get { return position; } set { position = value; } } + public float Spacing { get { return spacing; } set { spacing = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + public float RotateMix { get { return mixRotate; } set { mixRotate = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. + public float MixX { get { return mixX; } set { mixX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. + public float MixY { get { return mixY; } set { mixY = value; } } + } + + public enum PositionMode { + Fixed, Percent + } + + public enum SpacingMode { + Length, Fixed, Percent, Proportional + } + + public enum RotateMode { + Tangent, Chain, ChainScale + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Skeleton.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Skeleton.cs new file mode 100644 index 0000000..5fca86a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Skeleton.cs @@ -0,0 +1,614 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + public class Skeleton { + internal SkeletonData data; + internal ExposedList bones; + internal ExposedList slots; + internal ExposedList drawOrder; + internal ExposedList ikConstraints; + internal ExposedList transformConstraints; + internal ExposedList pathConstraints; + internal ExposedList updateCache = new ExposedList(); + internal Skin skin; + internal float r = 1, g = 1, b = 1, a = 1; + internal float time; + private float scaleX = 1, scaleY = 1; + internal float x, y; + + public SkeletonData Data { get { return data; } } + public ExposedList Bones { get { return bones; } } + public ExposedList UpdateCacheList { get { return updateCache; } } + public ExposedList Slots { get { return slots; } } + public ExposedList DrawOrder { get { return drawOrder; } } + public ExposedList IkConstraints { get { return ikConstraints; } } + public ExposedList PathConstraints { get { return pathConstraints; } } + public ExposedList TransformConstraints { get { return transformConstraints; } } + + public Skin Skin { + /// The skeleton's current skin. May be null. + get { return skin; } + /// Sets a skin, . + set { SetSkin(value); } + } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + public float Time { get { return time; } set { time = value; } } + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY * (Bone.yDown ? -1 : 1); } set { scaleY = value; } } + + [Obsolete("Use ScaleX instead. FlipX is when ScaleX is negative.")] + public bool FlipX { get { return scaleX < 0; } set { scaleX = value ? -1f : 1f; } } + + [Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")] + public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } } + + /// Returns the root bone, or null if the skeleton has no bones. + public Bone RootBone { + get { return bones.Count == 0 ? null : bones.Items[0]; } + } + + public Skeleton (SkeletonData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = data; + + bones = new ExposedList(data.bones.Count); + Bone[] bonesItems = this.bones.Items; + foreach (BoneData boneData in data.bones) { + Bone bone; + if (boneData.parent == null) { + bone = new Bone(boneData, this, null); + } else { + Bone parent = bonesItems[boneData.parent.index]; + bone = new Bone(boneData, this, parent); + parent.children.Add(bone); + } + this.bones.Add(bone); + } + + slots = new ExposedList(data.slots.Count); + drawOrder = new ExposedList(data.slots.Count); + foreach (SlotData slotData in data.slots) { + Bone bone = bonesItems[slotData.boneData.index]; + Slot slot = new Slot(slotData, bone); + slots.Add(slot); + drawOrder.Add(slot); + } + + ikConstraints = new ExposedList(data.ikConstraints.Count); + foreach (IkConstraintData ikConstraintData in data.ikConstraints) + ikConstraints.Add(new IkConstraint(ikConstraintData, this)); + + transformConstraints = new ExposedList(data.transformConstraints.Count); + foreach (TransformConstraintData transformConstraintData in data.transformConstraints) + transformConstraints.Add(new TransformConstraint(transformConstraintData, this)); + + pathConstraints = new ExposedList(data.pathConstraints.Count); + foreach (PathConstraintData pathConstraintData in data.pathConstraints) + pathConstraints.Add(new PathConstraint(pathConstraintData, this)); + + UpdateCache(); + } + + /// Caches information about bones and constraints. Must be called if the is modified or if bones, constraints, or + /// constraints, or weighted path attachments are added or removed. + public void UpdateCache () { + var updateCache = this.updateCache; + updateCache.Clear(); + + int boneCount = this.bones.Count; + Bone[] bones = this.bones.Items; + for (int i = 0; i < boneCount; i++) { + Bone bone = bones[i]; + bone.sorted = bone.data.skinRequired; + bone.active = !bone.sorted; + } + if (skin != null) { + BoneData[] skinBones = skin.bones.Items; + for (int i = 0, n = skin.bones.Count; i < n; i++) { + var bone = bones[skinBones[i].index]; + do { + bone.sorted = false; + bone.active = true; + bone = bone.parent; + } while (bone != null); + } + } + + int ikCount = this.ikConstraints.Count, transformCount = this.transformConstraints.Count, pathCount = this.pathConstraints.Count; + IkConstraint[] ikConstraints = this.ikConstraints.Items; + TransformConstraint[] transformConstraints = this.transformConstraints.Items; + PathConstraint[] pathConstraints = this.pathConstraints.Items; + int constraintCount = ikCount + transformCount + pathCount; + for (int i = 0; i < constraintCount; i++) { + for (int ii = 0; ii < ikCount; ii++) { + IkConstraint constraint = ikConstraints[ii]; + if (constraint.data.order == i) { + SortIkConstraint(constraint); + goto continue_outer; + } + } + for (int ii = 0; ii < transformCount; ii++) { + TransformConstraint constraint = transformConstraints[ii]; + if (constraint.data.order == i) { + SortTransformConstraint(constraint); + goto continue_outer; + } + } + for (int ii = 0; ii < pathCount; ii++) { + PathConstraint constraint = pathConstraints[ii]; + if (constraint.data.order == i) { + SortPathConstraint(constraint); + goto continue_outer; + } + } + continue_outer: { } + } + + for (int i = 0; i < boneCount; i++) + SortBone(bones[i]); + } + + private void SortIkConstraint (IkConstraint constraint) { + constraint.active = constraint.target.active + && (!constraint.data.skinRequired || (skin != null && skin.constraints.Contains(constraint.data))); + if (!constraint.active) return; + + Bone target = constraint.target; + SortBone(target); + + var constrained = constraint.bones; + Bone parent = constrained.Items[0]; + SortBone(parent); + + if (constrained.Count == 1) { + updateCache.Add(constraint); + SortReset(parent.children); + } else { + Bone child = constrained.Items[constrained.Count - 1]; + SortBone(child); + + updateCache.Add(constraint); + + SortReset(parent.children); + child.sorted = true; + } + } + + private void SortPathConstraint (PathConstraint constraint) { + constraint.active = constraint.target.bone.active + && (!constraint.data.skinRequired || (skin != null && skin.constraints.Contains(constraint.data))); + if (!constraint.active) return; + + Slot slot = constraint.target; + int slotIndex = slot.data.index; + Bone slotBone = slot.bone; + if (skin != null) SortPathConstraintAttachment(skin, slotIndex, slotBone); + if (data.defaultSkin != null && data.defaultSkin != skin) + SortPathConstraintAttachment(data.defaultSkin, slotIndex, slotBone); + + Attachment attachment = slot.attachment; + if (attachment is PathAttachment) SortPathConstraintAttachment(attachment, slotBone); + + var constrained = constraint.bones.Items; + int boneCount = constraint.bones.Count; + for (int i = 0; i < boneCount; i++) + SortBone(constrained[i]); + + updateCache.Add(constraint); + + for (int i = 0; i < boneCount; i++) + SortReset(constrained[i].children); + for (int i = 0; i < boneCount; i++) + constrained[i].sorted = true; + } + + private void SortTransformConstraint (TransformConstraint constraint) { + constraint.active = constraint.target.active + && (!constraint.data.skinRequired || (skin != null && skin.constraints.Contains(constraint.data))); + if (!constraint.active) return; + + SortBone(constraint.target); + + var constrained = constraint.bones.Items; + int boneCount = constraint.bones.Count; + if (constraint.data.local) { + for (int i = 0; i < boneCount; i++) { + Bone child = constrained[i]; + SortBone(child.parent); + SortBone(child); + } + } else { + for (int i = 0; i < boneCount; i++) + SortBone(constrained[i]); + } + + updateCache.Add(constraint); + + for (int i = 0; i < boneCount; i++) + SortReset(constrained[i].children); + for (int i = 0; i < boneCount; i++) + constrained[i].sorted = true; + } + + private void SortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) { + foreach (var entry in skin.Attachments) + if (entry.SlotIndex == slotIndex) SortPathConstraintAttachment(entry.Attachment, slotBone); + } + + private void SortPathConstraintAttachment (Attachment attachment, Bone slotBone) { + if (!(attachment is PathAttachment)) return; + int[] pathBones = ((PathAttachment)attachment).bones; + if (pathBones == null) + SortBone(slotBone); + else { + var bones = this.bones.Items; + for (int i = 0, n = pathBones.Length; i < n;) { + int nn = pathBones[i++]; + nn += i; + while (i < nn) + SortBone(bones[pathBones[i++]]); + } + } + } + + private void SortBone (Bone bone) { + if (bone.sorted) return; + Bone parent = bone.parent; + if (parent != null) SortBone(parent); + bone.sorted = true; + updateCache.Add(bone); + } + + private static void SortReset (ExposedList bones) { + Bone[] bonesItems = bones.Items; + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bonesItems[i]; + if (!bone.active) continue; + if (bone.sorted) SortReset(bone.children); + bone.sorted = false; + } + } + + /// + /// Updates the world transform for each bone and applies all constraints. + /// + /// See World transforms in the Spine + /// Runtimes Guide. + /// + public void UpdateWorldTransform () { + Bone[] bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + bone.ax = bone.x; + bone.ay = bone.y; + bone.arotation = bone.rotation; + bone.ascaleX = bone.scaleX; + bone.ascaleY = bone.scaleY; + bone.ashearX = bone.shearX; + bone.ashearY = bone.shearY; + } + + var updateCache = this.updateCache.Items; + for (int i = 0, n = this.updateCache.Count; i < n; i++) + updateCache[i].Update(); + } + + /// + /// Temporarily sets the root bone as a child of the specified bone, then updates the world transform for each bone and applies + /// all constraints. + /// + public void UpdateWorldTransform (Bone parent) { + if (parent == null) throw new ArgumentNullException("parent", "parent cannot be null."); + + // Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection. + Bone rootBone = this.RootBone; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; + rootBone.worldX = pa * x + pb * y + parent.worldX; + rootBone.worldY = pc * x + pd * y + parent.worldY; + + float rotationY = rootBone.rotation + 90 + rootBone.shearY; + float la = MathUtils.CosDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX; + float lb = MathUtils.CosDeg(rotationY) * rootBone.scaleY; + float lc = MathUtils.SinDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX; + float ld = MathUtils.SinDeg(rotationY) * rootBone.scaleY; + rootBone.a = (pa * la + pb * lc) * scaleX; + rootBone.b = (pa * lb + pb * ld) * scaleX; + rootBone.c = (pc * la + pd * lc) * scaleY; + rootBone.d = (pc * lb + pd * ld) * scaleY; + + // Update everything except root bone. + var updateCache = this.updateCache.Items; + for (int i = 0, n = this.updateCache.Count; i < n; i++) { + var updatable = updateCache[i]; + if (updatable != rootBone) updatable.Update(); + } + } + + /// Sets the bones, constraints, and slots to their setup pose values. + public void SetToSetupPose () { + SetBonesToSetupPose(); + SetSlotsToSetupPose(); + } + + /// Sets the bones and constraints to their setup pose values. + public void SetBonesToSetupPose () { + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) + bones[i].SetToSetupPose(); + + var ikConstraints = this.ikConstraints.Items; + for (int i = 0, n = this.ikConstraints.Count; i < n; i++) { + IkConstraint constraint = ikConstraints[i]; + IkConstraintData data = constraint.data; + constraint.mix = data.mix; + constraint.softness = data.softness; + constraint.bendDirection = data.bendDirection; + constraint.compress = data.compress; + constraint.stretch = data.stretch; + } + + var transformConstraints = this.transformConstraints.Items; + for (int i = 0, n = this.transformConstraints.Count; i < n; i++) { + TransformConstraint constraint = transformConstraints[i]; + TransformConstraintData data = constraint.data; + constraint.mixRotate = data.mixRotate; + constraint.mixX = data.mixX; + constraint.mixY = data.mixY; + constraint.mixScaleX = data.mixScaleX; + constraint.mixScaleY = data.mixScaleY; + constraint.mixShearY = data.mixShearY; + } + + var pathConstraints = this.pathConstraints.Items; + for (int i = 0, n = this.pathConstraints.Count; i < n; i++) { + PathConstraint constraint = pathConstraints[i]; + PathConstraintData data = constraint.data; + constraint.position = data.position; + constraint.spacing = data.spacing; + constraint.mixRotate = data.mixRotate; + constraint.mixX = data.mixX; + constraint.mixY = data.mixY; + } + } + + public void SetSlotsToSetupPose () { + var slots = this.slots.Items; + int n = this.slots.Count; + Array.Copy(slots, 0, drawOrder.Items, 0, n); + for (int i = 0; i < n; i++) + slots[i].SetToSetupPose(); + } + + /// Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it + /// repeatedly. + /// May be null. + public Bone FindBone (string boneName) { + if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + if (bone.data.name == boneName) return bone; + } + return null; + } + + /// Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it + /// repeatedly. + /// May be null. + public Slot FindSlot (string slotName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + var slots = this.slots.Items; + for (int i = 0, n = this.slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.data.name == slotName) return slot; + } + return null; + } + + /// Sets a skin by name (). + public void SetSkin (string skinName) { + Skin foundSkin = data.FindSkin(skinName); + if (foundSkin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName"); + SetSkin(foundSkin); + } + + /// + /// Sets the skin used to look up attachments before looking in the . If the + /// skin is changed, is called. + /// + /// Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. + /// If there was no old skin, each slot's setup mode attachment is attached from the new skin. + /// + /// After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling + /// . + /// Also, often is called before the next time the + /// skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. + /// + /// May be null. + public void SetSkin (Skin newSkin) { + if (newSkin == skin) return; + if (newSkin != null) { + if (skin != null) + newSkin.AttachAll(this, skin); + else { + Slot[] slots = this.slots.Items; + for (int i = 0, n = this.slots.Count; i < n; i++) { + Slot slot = slots[i]; + string name = slot.data.attachmentName; + if (name != null) { + Attachment attachment = newSkin.GetAttachment(i, name); + if (attachment != null) slot.Attachment = attachment; + } + } + } + } + skin = newSkin; + UpdateCache(); + } + + /// Finds an attachment by looking in the and using the slot name and attachment name. + /// May be null. + public Attachment GetAttachment (string slotName, string attachmentName) { + return GetAttachment(data.FindSlot(slotName).index, attachmentName); + } + + /// Finds an attachment by looking in the skin and skeletonData.defaultSkin using the slot index and attachment name.First the skin is checked and if the attachment was not found, the default skin is checked. + /// May be null. + public Attachment GetAttachment (int slotIndex, string attachmentName) { + if (attachmentName == null) throw new ArgumentNullException("attachmentName", "attachmentName cannot be null."); + if (skin != null) { + Attachment attachment = skin.GetAttachment(slotIndex, attachmentName); + if (attachment != null) return attachment; + } + return data.defaultSkin != null ? data.defaultSkin.GetAttachment(slotIndex, attachmentName) : null; + } + + /// A convenience method to set an attachment by finding the slot with FindSlot, finding the attachment with GetAttachment, then setting the slot's slot.Attachment. + /// May be null to clear the slot's attachment. + public void SetAttachment (string slotName, string attachmentName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + Slot[] slots = this.slots.Items; + for (int i = 0, n = this.slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.data.name == slotName) { + Attachment attachment = null; + if (attachmentName != null) { + attachment = GetAttachment(i, attachmentName); + if (attachment == null) throw new Exception("Attachment not found: " + attachmentName + ", for slot: " + slotName); + } + slot.Attachment = attachment; + return; + } + } + throw new Exception("Slot not found: " + slotName); + } + + /// Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method + /// than to call it repeatedly. + /// May be null. + public IkConstraint FindIkConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var ikConstraints = this.ikConstraints.Items; + for (int i = 0, n = this.ikConstraints.Count; i < n; i++) { + IkConstraint ikConstraint = ikConstraints[i]; + if (ikConstraint.data.name == constraintName) return ikConstraint; + } + return null; + } + + /// Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of + /// this method than to call it repeatedly. + /// May be null. + public TransformConstraint FindTransformConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var transformConstraints = this.transformConstraints.Items; + for (int i = 0, n = this.transformConstraints.Count; i < n; i++) { + TransformConstraint transformConstraint = transformConstraints[i]; + if (transformConstraint.data.Name == constraintName) return transformConstraint; + } + return null; + } + + /// Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method + /// than to call it repeatedly. + /// May be null. + public PathConstraint FindPathConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var pathConstraints = this.pathConstraints.Items; + for (int i = 0, n = this.pathConstraints.Count; i < n; i++) { + PathConstraint constraint = pathConstraints[i]; + if (constraint.data.Name.Equals(constraintName)) return constraint; + } + return null; + } + + public void Update (float delta) { + time += delta; + } + + /// Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose. + /// The horizontal distance between the skeleton origin and the left side of the AABB. + /// The vertical distance between the skeleton origin and the bottom side of the AABB. + /// The width of the AABB + /// The height of the AABB. + /// Reference to hold a float[]. May be a null reference. This method will assign it a new float[] with the appropriate size as needed. + public void GetBounds (out float x, out float y, out float width, out float height, ref float[] vertexBuffer) { + float[] temp = vertexBuffer; + temp = temp ?? new float[8]; + var drawOrder = this.drawOrder.Items; + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + for (int i = 0, n = this.drawOrder.Count; i < n; i++) { + Slot slot = drawOrder[i]; + if (!slot.bone.active) continue; + int verticesLength = 0; + float[] vertices = null; + Attachment attachment = slot.attachment; + var regionAttachment = attachment as RegionAttachment; + if (regionAttachment != null) { + verticesLength = 8; + vertices = temp; + if (vertices.Length < 8) vertices = temp = new float[8]; + regionAttachment.ComputeWorldVertices(slot.bone, temp, 0); + } else { + var meshAttachment = attachment as MeshAttachment; + if (meshAttachment != null) { + MeshAttachment mesh = meshAttachment; + verticesLength = mesh.WorldVerticesLength; + vertices = temp; + if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength]; + mesh.ComputeWorldVertices(slot, 0, verticesLength, temp, 0); + } + } + + if (vertices != null) { + for (int ii = 0; ii < verticesLength; ii += 2) { + float vx = vertices[ii], vy = vertices[ii + 1]; + minX = Math.Min(minX, vx); + minY = Math.Min(minY, vy); + maxX = Math.Max(maxX, vx); + maxY = Math.Max(maxY, vy); + } + } + } + x = minX; + y = minY; + width = maxX - minX; + height = maxY - minY; + vertexBuffer = temp; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonBinary.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonBinary.cs new file mode 100644 index 0000000..b8ce817 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonBinary.cs @@ -0,0 +1,1225 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace Spine4_0_64 { + public class SkeletonBinary : SkeletonLoader { + public const int BONE_ROTATE = 0; + public const int BONE_TRANSLATE = 1; + public const int BONE_TRANSLATEX = 2; + public const int BONE_TRANSLATEY = 3; + public const int BONE_SCALE = 4; + public const int BONE_SCALEX = 5; + public const int BONE_SCALEY = 6; + public const int BONE_SHEAR = 7; + public const int BONE_SHEARX = 8; + public const int BONE_SHEARY = 9; + + public const int SLOT_ATTACHMENT = 0; + public const int SLOT_RGBA = 1; + public const int SLOT_RGB = 2; + public const int SLOT_RGBA2 = 3; + public const int SLOT_RGB2 = 4; + public const int SLOT_ALPHA = 5; + + public const int PATH_POSITION = 0; + public const int PATH_SPACING = 1; + public const int PATH_MIX = 2; + + public const int CURVE_LINEAR = 0; + public const int CURVE_STEPPED = 1; + public const int CURVE_BEZIER = 2; + + public SkeletonBinary (AttachmentLoader attachmentLoader) + : base(attachmentLoader) { + } + + public SkeletonBinary (params Atlas[] atlasArray) + : base(atlasArray) { + } + +#if !ISUNITY && WINDOWS_STOREAPP + private async Task ReadFile(string path) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + using (var input = new BufferedStream(await folder.GetFileAsync(path).AsTask().ConfigureAwait(false))) { + SkeletonData skeletonData = ReadSkeletonData(input); + skeletonData.Name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + + public override SkeletonData ReadSkeletonData (string path) { + return this.ReadFile(path).Result; + } +#else + public override SkeletonData ReadSkeletonData (string path) { +#if WINDOWS_PHONE + using (var input = new BufferedStream(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) { +#else + using (var input = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { +#endif + SkeletonData skeletonData = ReadSkeletonData(input); + skeletonData.name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } +#endif // WINDOWS_STOREAPP + + public static readonly TransformMode[] TransformModeValues = { + TransformMode.Normal, + TransformMode.OnlyTranslation, + TransformMode.NoRotationOrReflection, + TransformMode.NoScale, + TransformMode.NoScaleOrReflection + }; + + /// Returns the version string of binary skeleton data. + public static string GetVersionString (Stream file) { + if (file == null) throw new ArgumentNullException("file"); + + SkeletonInput input = new SkeletonInput(file); + return input.GetVersionString(); + } + + public SkeletonData ReadSkeletonData (Stream file) { + if (file == null) throw new ArgumentNullException("file"); + float scale = this.scale; + + var skeletonData = new SkeletonData(); + SkeletonInput input = new SkeletonInput(file); + + long hash = input.ReadLong(); + skeletonData.hash = hash == 0 ? null : hash.ToString(); + skeletonData.version = input.ReadString(); + if (skeletonData.version.Length == 0) skeletonData.version = null; + // early return for old 3.8 format instead of reading past the end + if (skeletonData.version.Length > 13) return null; + skeletonData.x = input.ReadFloat(); + skeletonData.y = input.ReadFloat(); + skeletonData.width = input.ReadFloat(); + skeletonData.height = input.ReadFloat(); + + bool nonessential = input.ReadBoolean(); + + if (nonessential) { + skeletonData.fps = input.ReadFloat(); + + skeletonData.imagesPath = input.ReadString(); + if (string.IsNullOrEmpty(skeletonData.imagesPath)) skeletonData.imagesPath = null; + + skeletonData.audioPath = input.ReadString(); + if (string.IsNullOrEmpty(skeletonData.audioPath)) skeletonData.audioPath = null; + } + + int n; + Object[] o; + + // Strings. + o = input.strings = new String[n = input.ReadInt(true)]; + for (int i = 0; i < n; i++) + o[i] = input.ReadString(); + + // Bones. + var bones = skeletonData.bones.Resize(n = input.ReadInt(true)).Items; + for (int i = 0; i < n; i++) { + String name = input.ReadString(); + BoneData parent = i == 0 ? null : bones[input.ReadInt(true)]; + BoneData data = new BoneData(i, name, parent); + data.rotation = input.ReadFloat(); + data.x = input.ReadFloat() * scale; + data.y = input.ReadFloat() * scale; + data.scaleX = input.ReadFloat(); + data.scaleY = input.ReadFloat(); + data.shearX = input.ReadFloat(); + data.shearY = input.ReadFloat(); + data.Length = input.ReadFloat() * scale; + data.transformMode = TransformModeValues[input.ReadInt(true)]; + data.skinRequired = input.ReadBoolean(); + if (nonessential) input.ReadInt(); // Skip bone color. + bones[i] = data; + } + + // Slots. + var slots = skeletonData.slots.Resize(n = input.ReadInt(true)).Items; + for (int i = 0; i < n; i++) { + String slotName = input.ReadString(); + BoneData boneData = bones[input.ReadInt(true)]; + SlotData slotData = new SlotData(i, slotName, boneData); + int color = input.ReadInt(); + slotData.r = ((color & 0xff000000) >> 24) / 255f; + slotData.g = ((color & 0x00ff0000) >> 16) / 255f; + slotData.b = ((color & 0x0000ff00) >> 8) / 255f; + slotData.a = ((color & 0x000000ff)) / 255f; + + int darkColor = input.ReadInt(); // 0x00rrggbb + if (darkColor != -1) { + slotData.hasSecondColor = true; + slotData.r2 = ((darkColor & 0x00ff0000) >> 16) / 255f; + slotData.g2 = ((darkColor & 0x0000ff00) >> 8) / 255f; + slotData.b2 = ((darkColor & 0x000000ff)) / 255f; + } + + slotData.attachmentName = input.ReadStringRef(); + slotData.blendMode = (BlendMode)input.ReadInt(true); + slots[i] = slotData; + } + + // IK constraints. + o = skeletonData.ikConstraints.Resize(n = input.ReadInt(true)).Items; + for (int i = 0, nn; i < n; i++) { + IkConstraintData data = new IkConstraintData(input.ReadString()); + data.order = input.ReadInt(true); + data.skinRequired = input.ReadBoolean(); + var constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items; + for (int ii = 0; ii < nn; ii++) + constraintBones[ii] = bones[input.ReadInt(true)]; + data.target = bones[input.ReadInt(true)]; + data.mix = input.ReadFloat(); + data.softness = input.ReadFloat() * scale; + data.bendDirection = input.ReadSByte(); + data.compress = input.ReadBoolean(); + data.stretch = input.ReadBoolean(); + data.uniform = input.ReadBoolean(); + o[i] = data; + } + + // Transform constraints. + o = skeletonData.transformConstraints.Resize(n = input.ReadInt(true)).Items; + for (int i = 0, nn; i < n; i++) { + TransformConstraintData data = new TransformConstraintData(input.ReadString()); + data.order = input.ReadInt(true); + data.skinRequired = input.ReadBoolean(); + var constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items; + for (int ii = 0; ii < nn; ii++) + constraintBones[ii] = bones[input.ReadInt(true)]; + data.target = bones[input.ReadInt(true)]; + data.local = input.ReadBoolean(); + data.relative = input.ReadBoolean(); + data.offsetRotation = input.ReadFloat(); + data.offsetX = input.ReadFloat() * scale; + data.offsetY = input.ReadFloat() * scale; + data.offsetScaleX = input.ReadFloat(); + data.offsetScaleY = input.ReadFloat(); + data.offsetShearY = input.ReadFloat(); + data.mixRotate = input.ReadFloat(); + data.mixX = input.ReadFloat(); + data.mixY = input.ReadFloat(); + data.mixScaleX = input.ReadFloat(); + data.mixScaleY = input.ReadFloat(); + data.mixShearY = input.ReadFloat(); + o[i] = data; + } + + // Path constraints + o = skeletonData.pathConstraints.Resize(n = input.ReadInt(true)).Items; + for (int i = 0, nn; i < n; i++) { + PathConstraintData data = new PathConstraintData(input.ReadString()); + data.order = input.ReadInt(true); + data.skinRequired = input.ReadBoolean(); + Object[] constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items; + for (int ii = 0; ii < nn; ii++) + constraintBones[ii] = bones[input.ReadInt(true)]; + data.target = slots[input.ReadInt(true)]; + data.positionMode = (PositionMode)Enum.GetValues(typeof(PositionMode)).GetValue(input.ReadInt(true)); + data.spacingMode = (SpacingMode)Enum.GetValues(typeof(SpacingMode)).GetValue(input.ReadInt(true)); + data.rotateMode = (RotateMode)Enum.GetValues(typeof(RotateMode)).GetValue(input.ReadInt(true)); + data.offsetRotation = input.ReadFloat(); + data.position = input.ReadFloat(); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = input.ReadFloat(); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.mixRotate = input.ReadFloat(); + data.mixX = input.ReadFloat(); + data.mixY = input.ReadFloat(); + o[i] = data; + } + + // Default skin. + Skin defaultSkin = ReadSkin(input, skeletonData, true, nonessential); + if (defaultSkin != null) { + skeletonData.defaultSkin = defaultSkin; + skeletonData.skins.Add(defaultSkin); + } + + // Skins. + { + int i = skeletonData.skins.Count; + o = skeletonData.skins.Resize(n = i + input.ReadInt(true)).Items; + for (; i < n; i++) + o[i] = ReadSkin(input, skeletonData, false, nonessential); + } + + // Linked meshes. + n = linkedMeshes.Count; + for (int i = 0; i < n; i++) { + LinkedMesh linkedMesh = linkedMeshes[i]; + Skin skin = linkedMesh.skin == null ? skeletonData.DefaultSkin : skeletonData.FindSkin(linkedMesh.skin); + if (skin == null) throw new Exception("Skin not found: " + linkedMesh.skin); + Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Exception("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.DeformAttachment = linkedMesh.inheritDeform ? (VertexAttachment)parent : linkedMesh.mesh; + linkedMesh.mesh.ParentMesh = (MeshAttachment)parent; + linkedMesh.mesh.UpdateUVs(); + } + linkedMeshes.Clear(); + + // Events. + o = skeletonData.events.Resize(n = input.ReadInt(true)).Items; + for (int i = 0; i < n; i++) { + EventData data = new EventData(input.ReadStringRef()); + data.Int = input.ReadInt(false); + data.Float = input.ReadFloat(); + data.String = input.ReadString(); + data.AudioPath = input.ReadString(); + if (data.AudioPath != null) { + data.Volume = input.ReadFloat(); + data.Balance = input.ReadFloat(); + } + o[i] = data; + } + + // Animations. + o = skeletonData.animations.Resize(n = input.ReadInt(true)).Items; + for (int i = 0; i < n; i++) + o[i] = ReadAnimation(input.ReadString(), input, skeletonData); + + return skeletonData; + } + + /// May be null. + private Skin ReadSkin (SkeletonInput input, SkeletonData skeletonData, bool defaultSkin, bool nonessential) { + + Skin skin; + int slotCount; + + if (defaultSkin) { + slotCount = input.ReadInt(true); + if (slotCount == 0) return null; + skin = new Skin("default"); + } else { + skin = new Skin(input.ReadStringRef()); + Object[] bones = skin.bones.Resize(input.ReadInt(true)).Items; + var bonesItems = skeletonData.bones.Items; + for (int i = 0, n = skin.bones.Count; i < n; i++) + bones[i] = bonesItems[input.ReadInt(true)]; + + var ikConstraintsItems = skeletonData.ikConstraints.Items; + for (int i = 0, n = input.ReadInt(true); i < n; i++) + skin.constraints.Add(ikConstraintsItems[input.ReadInt(true)]); + var transformConstraintsItems = skeletonData.transformConstraints.Items; + for (int i = 0, n = input.ReadInt(true); i < n; i++) + skin.constraints.Add(transformConstraintsItems[input.ReadInt(true)]); + var pathConstraintsItems = skeletonData.pathConstraints.Items; + for (int i = 0, n = input.ReadInt(true); i < n; i++) + skin.constraints.Add(pathConstraintsItems[input.ReadInt(true)]); + skin.constraints.TrimExcess(); + + slotCount = input.ReadInt(true); + } + for (int i = 0; i < slotCount; i++) { + int slotIndex = input.ReadInt(true); + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + String name = input.ReadStringRef(); + Attachment attachment = ReadAttachment(input, skeletonData, skin, slotIndex, name, nonessential); + if (attachment != null) skin.SetAttachment(slotIndex, name, attachment); + } + } + return skin; + } + + private Attachment ReadAttachment (SkeletonInput input, SkeletonData skeletonData, Skin skin, int slotIndex, + String attachmentName, bool nonessential) { + float scale = this.scale; + + String name = input.ReadStringRef(); + if (name == null) name = attachmentName; + + switch ((AttachmentType)input.ReadByte()) { + case AttachmentType.Region: { + String path = input.ReadStringRef(); + float rotation = input.ReadFloat(); + float x = input.ReadFloat(); + float y = input.ReadFloat(); + float scaleX = input.ReadFloat(); + float scaleY = input.ReadFloat(); + float width = input.ReadFloat(); + float height = input.ReadFloat(); + int color = input.ReadInt(); + + if (path == null) path = name; + RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path); + if (region == null) return null; + region.Path = path; + region.x = x * scale; + region.y = y * scale; + region.scaleX = scaleX; + region.scaleY = scaleY; + region.rotation = rotation; + region.width = width * scale; + region.height = height * scale; + region.r = ((color & 0xff000000) >> 24) / 255f; + region.g = ((color & 0x00ff0000) >> 16) / 255f; + region.b = ((color & 0x0000ff00) >> 8) / 255f; + region.a = ((color & 0x000000ff)) / 255f; + region.UpdateOffset(); + return region; + } + case AttachmentType.Boundingbox: { + int vertexCount = input.ReadInt(true); + Vertices vertices = ReadVertices(input, vertexCount); + if (nonessential) input.ReadInt(); //int color = nonessential ? input.ReadInt() : 0; // Avoid unused local warning. + + BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name); + if (box == null) return null; + box.worldVerticesLength = vertexCount << 1; + box.vertices = vertices.vertices; + box.bones = vertices.bones; + // skipped porting: if (nonessential) Color.rgba8888ToColor(box.getColor(), color); + return box; + } + case AttachmentType.Mesh: { + String path = input.ReadStringRef(); + int color = input.ReadInt(); + int vertexCount = input.ReadInt(true); + float[] uvs = ReadFloatArray(input, vertexCount << 1, 1); + int[] triangles = ReadShortArray(input); + Vertices vertices = ReadVertices(input, vertexCount); + int hullLength = input.ReadInt(true); + int[] edges = null; + float width = 0, height = 0; + if (nonessential) { + edges = ReadShortArray(input); + width = input.ReadFloat(); + height = input.ReadFloat(); + } + + if (path == null) path = name; + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.Path = path; + mesh.r = ((color & 0xff000000) >> 24) / 255f; + mesh.g = ((color & 0x00ff0000) >> 16) / 255f; + mesh.b = ((color & 0x0000ff00) >> 8) / 255f; + mesh.a = ((color & 0x000000ff)) / 255f; + mesh.bones = vertices.bones; + mesh.vertices = vertices.vertices; + mesh.WorldVerticesLength = vertexCount << 1; + mesh.triangles = triangles; + mesh.regionUVs = uvs; + mesh.UpdateUVs(); + mesh.HullLength = hullLength << 1; + if (nonessential) { + mesh.Edges = edges; + mesh.Width = width * scale; + mesh.Height = height * scale; + } + return mesh; + } + case AttachmentType.Linkedmesh: { + String path = input.ReadStringRef(); + int color = input.ReadInt(); + String skinName = input.ReadStringRef(); + String parent = input.ReadStringRef(); + bool inheritDeform = input.ReadBoolean(); + float width = 0, height = 0; + if (nonessential) { + width = input.ReadFloat(); + height = input.ReadFloat(); + } + + if (path == null) path = name; + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.Path = path; + mesh.r = ((color & 0xff000000) >> 24) / 255f; + mesh.g = ((color & 0x00ff0000) >> 16) / 255f; + mesh.b = ((color & 0x0000ff00) >> 8) / 255f; + mesh.a = ((color & 0x000000ff)) / 255f; + if (nonessential) { + mesh.Width = width * scale; + mesh.Height = height * scale; + } + linkedMeshes.Add(new SkeletonJson.LinkedMesh(mesh, skinName, slotIndex, parent, inheritDeform)); + return mesh; + } + case AttachmentType.Path: { + bool closed = input.ReadBoolean(); + bool constantSpeed = input.ReadBoolean(); + int vertexCount = input.ReadInt(true); + Vertices vertices = ReadVertices(input, vertexCount); + float[] lengths = new float[vertexCount / 3]; + for (int i = 0, n = lengths.Length; i < n; i++) + lengths[i] = input.ReadFloat() * scale; + if (nonessential) input.ReadInt(); //int color = nonessential ? input.ReadInt() : 0; + + PathAttachment path = attachmentLoader.NewPathAttachment(skin, name); + if (path == null) return null; + path.closed = closed; + path.constantSpeed = constantSpeed; + path.worldVerticesLength = vertexCount << 1; + path.vertices = vertices.vertices; + path.bones = vertices.bones; + path.lengths = lengths; + // skipped porting: if (nonessential) Color.rgba8888ToColor(path.getColor(), color); + return path; + } + case AttachmentType.Point: { + float rotation = input.ReadFloat(); + float x = input.ReadFloat(); + float y = input.ReadFloat(); + if (nonessential) input.ReadInt(); //int color = nonessential ? input.ReadInt() : 0; + + PointAttachment point = attachmentLoader.NewPointAttachment(skin, name); + if (point == null) return null; + point.x = x * scale; + point.y = y * scale; + point.rotation = rotation; + // skipped porting: if (nonessential) point.color = color; + return point; + } + case AttachmentType.Clipping: { + int endSlotIndex = input.ReadInt(true); + int vertexCount = input.ReadInt(true); + Vertices vertices = ReadVertices(input, vertexCount); + if (nonessential) input.ReadInt(); + + ClippingAttachment clip = attachmentLoader.NewClippingAttachment(skin, name); + if (clip == null) return null; + clip.EndSlot = skeletonData.slots.Items[endSlotIndex]; + clip.worldVerticesLength = vertexCount << 1; + clip.vertices = vertices.vertices; + clip.bones = vertices.bones; + // skipped porting: if (nonessential) Color.rgba8888ToColor(clip.getColor(), color); + return clip; + } + } + return null; + } + + private Vertices ReadVertices (SkeletonInput input, int vertexCount) { + float scale = this.scale; + int verticesLength = vertexCount << 1; + Vertices vertices = new Vertices(); + if (!input.ReadBoolean()) { + vertices.vertices = ReadFloatArray(input, verticesLength, scale); + return vertices; + } + var weights = new ExposedList(verticesLength * 3 * 3); + var bonesArray = new ExposedList(verticesLength * 3); + for (int i = 0; i < vertexCount; i++) { + int boneCount = input.ReadInt(true); + bonesArray.Add(boneCount); + for (int ii = 0; ii < boneCount; ii++) { + bonesArray.Add(input.ReadInt(true)); + weights.Add(input.ReadFloat() * scale); + weights.Add(input.ReadFloat() * scale); + weights.Add(input.ReadFloat()); + } + } + + vertices.vertices = weights.ToArray(); + vertices.bones = bonesArray.ToArray(); + return vertices; + } + + private float[] ReadFloatArray (SkeletonInput input, int n, float scale) { + float[] array = new float[n]; + if (scale == 1) { + for (int i = 0; i < n; i++) + array[i] = input.ReadFloat(); + } else { + for (int i = 0; i < n; i++) + array[i] = input.ReadFloat() * scale; + } + return array; + } + + private int[] ReadShortArray (SkeletonInput input) { + int n = input.ReadInt(true); + int[] array = new int[n]; + for (int i = 0; i < n; i++) + array[i] = (input.ReadByte() << 8) | input.ReadByte(); + return array; + } + + /// SerializationException will be thrown when a Vertex attachment is not found. + /// Throws IOException when a read operation fails. + private Animation ReadAnimation (String name, SkeletonInput input, SkeletonData skeletonData) { + var timelines = new ExposedList(input.ReadInt(true)); + float scale = this.scale; + + // Slot timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int slotIndex = input.ReadInt(true); + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + int timelineType = input.ReadByte(), frameCount = input.ReadInt(true), frameLast = frameCount - 1; + switch (timelineType) { + case SLOT_ATTACHMENT: { + AttachmentTimeline timeline = new AttachmentTimeline(frameCount, slotIndex); + for (int frame = 0; frame < frameCount; frame++) + timeline.SetFrame(frame, input.ReadFloat(), input.ReadStringRef()); + timelines.Add(timeline); + break; + } + case SLOT_RGBA: { + RGBATimeline timeline = new RGBATimeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(); + float r = input.Read() / 255f, g = input.Read() / 255f; + float b = input.Read() / 255f, a = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, a); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float r2 = input.Read() / 255f, g2 = input.Read() / 255f; + float b2 = input.Read() / 255f, a2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1); + SetBezier(input, timeline, bezier++, frame, 3, time, time2, a, a2, 1); + break; + } + time = time2; + r = r2; + g = g2; + b = b2; + a = a2; + } + timelines.Add(timeline); + break; + } + case SLOT_RGB: { + RGBTimeline timeline = new RGBTimeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(); + float r = input.Read() / 255f, g = input.Read() / 255f, b = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float r2 = input.Read() / 255f, g2 = input.Read() / 255f, b2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1); + break; + } + time = time2; + r = r2; + g = g2; + b = b2; + } + timelines.Add(timeline); + break; + } + case SLOT_RGBA2: { + RGBA2Timeline timeline = new RGBA2Timeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(); + float r = input.Read() / 255f, g = input.Read() / 255f; + float b = input.Read() / 255f, a = input.Read() / 255f; + float r2 = input.Read() / 255f, g2 = input.Read() / 255f, b2 = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, a, r2, g2, b2); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float nr = input.Read() / 255f, ng = input.Read() / 255f; + float nb = input.Read() / 255f, na = input.Read() / 255f; + float nr2 = input.Read() / 255f, ng2 = input.Read() / 255f, nb2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1); + SetBezier(input, timeline, bezier++, frame, 3, time, time2, a, na, 1); + SetBezier(input, timeline, bezier++, frame, 4, time, time2, r2, nr2, 1); + SetBezier(input, timeline, bezier++, frame, 5, time, time2, g2, ng2, 1); + SetBezier(input, timeline, bezier++, frame, 6, time, time2, b2, nb2, 1); + break; + } + time = time2; + r = nr; + g = ng; + b = nb; + a = na; + r2 = nr2; + g2 = ng2; + b2 = nb2; + } + timelines.Add(timeline); + break; + } + case SLOT_RGB2: { + RGB2Timeline timeline = new RGB2Timeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(); + float r = input.Read() / 255f, g = input.Read() / 255f, b = input.Read() / 255f; + float r2 = input.Read() / 255f, g2 = input.Read() / 255f, b2 = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, r2, g2, b2); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float nr = input.Read() / 255f, ng = input.Read() / 255f, nb = input.Read() / 255f; + float nr2 = input.Read() / 255f, ng2 = input.Read() / 255f, nb2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1); + SetBezier(input, timeline, bezier++, frame, 3, time, time2, r2, nr2, 1); + SetBezier(input, timeline, bezier++, frame, 4, time, time2, g2, ng2, 1); + SetBezier(input, timeline, bezier++, frame, 5, time, time2, b2, nb2, 1); + break; + } + time = time2; + r = nr; + g = ng; + b = nb; + r2 = nr2; + g2 = ng2; + b2 = nb2; + } + timelines.Add(timeline); + break; + } + case SLOT_ALPHA: { + AlphaTimeline timeline = new AlphaTimeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(), a = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, a); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float a2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, a, a2, 1); + break; + } + time = time2; + a = a2; + } + timelines.Add(timeline); + break; + } + } + } + } + + // Bone timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int boneIndex = input.ReadInt(true); + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + int type = input.ReadByte(), frameCount = input.ReadInt(true), bezierCount = input.ReadInt(true); + switch (type) { + case BONE_ROTATE: + timelines.Add(ReadTimeline(input, new RotateTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_TRANSLATE: + timelines.Add(ReadTimeline(input, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale)); + break; + case BONE_TRANSLATEX: + timelines.Add(ReadTimeline(input, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale)); + break; + case BONE_TRANSLATEY: + timelines.Add(ReadTimeline(input, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale)); + break; + case BONE_SCALE: + timelines.Add(ReadTimeline(input, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SCALEX: + timelines.Add(ReadTimeline(input, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SCALEY: + timelines.Add(ReadTimeline(input, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SHEAR: + timelines.Add(ReadTimeline(input, new ShearTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SHEARX: + timelines.Add(ReadTimeline(input, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SHEARY: + timelines.Add(ReadTimeline(input, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + } + } + } + + // IK constraint timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int index = input.ReadInt(true), frameCount = input.ReadInt(true), frameLast = frameCount - 1; + IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount, input.ReadInt(true), index); + float time = input.ReadFloat(), mix = input.ReadFloat(), softness = input.ReadFloat() * scale; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mix, softness, input.ReadSByte(), input.ReadBoolean(), input.ReadBoolean()); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), mix2 = input.ReadFloat(), softness2 = input.ReadFloat() * scale; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale); + break; + } + time = time2; + mix = mix2; + softness = softness2; + } + timelines.Add(timeline); + } + + // Transform constraint timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int index = input.ReadInt(true), frameCount = input.ReadInt(true), frameLast = frameCount - 1; + TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount, input.ReadInt(true), index); + float time = input.ReadFloat(), mixRotate = input.ReadFloat(), mixX = input.ReadFloat(), mixY = input.ReadFloat(), + mixScaleX = input.ReadFloat(), mixScaleY = input.ReadFloat(), mixShearY = input.ReadFloat(); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), mixRotate2 = input.ReadFloat(), mixX2 = input.ReadFloat(), mixY2 = input.ReadFloat(), + mixScaleX2 = input.ReadFloat(), mixScaleY2 = input.ReadFloat(), mixShearY2 = input.ReadFloat(); + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, mixRotate, mixRotate2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, mixX, mixX2, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, mixY, mixY2, 1); + SetBezier(input, timeline, bezier++, frame, 3, time, time2, mixScaleX, mixScaleX2, 1); + SetBezier(input, timeline, bezier++, frame, 4, time, time2, mixScaleY, mixScaleY2, 1); + SetBezier(input, timeline, bezier++, frame, 5, time, time2, mixShearY, mixShearY2, 1); + break; + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + mixScaleX = mixScaleX2; + mixScaleY = mixScaleY2; + mixShearY = mixShearY2; + } + timelines.Add(timeline); + } + + // Path constraint timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int index = input.ReadInt(true); + PathConstraintData data = skeletonData.pathConstraints.Items[index]; + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + switch (input.ReadByte()) { + case PATH_POSITION: + timelines + .Add(ReadTimeline(input, new PathConstraintPositionTimeline(input.ReadInt(true), input.ReadInt(true), index), + data.positionMode == PositionMode.Fixed ? scale : 1)); + break; + case PATH_SPACING: + timelines + .Add(ReadTimeline(input, new PathConstraintSpacingTimeline(input.ReadInt(true), input.ReadInt(true), index), + data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1)); + break; + case PATH_MIX: + PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(input.ReadInt(true), input.ReadInt(true), + index); + float time = input.ReadFloat(), mixRotate = input.ReadFloat(), mixX = input.ReadFloat(), mixY = input.ReadFloat(); + for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) { + timeline.SetFrame(frame, time, mixRotate, mixX, mixY); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), mixRotate2 = input.ReadFloat(), mixX2 = input.ReadFloat(), + mixY2 = input.ReadFloat(); + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, mixRotate, mixRotate2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, mixX, mixX2, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, mixY, mixY2, 1); + break; + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + } + timelines.Add(timeline); + break; + } + } + } + + // Deform timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + Skin skin = skeletonData.skins.Items[input.ReadInt(true)]; + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + int slotIndex = input.ReadInt(true); + for (int iii = 0, nnn = input.ReadInt(true); iii < nnn; iii++) { + String attachmentName = input.ReadStringRef(); + VertexAttachment attachment = (VertexAttachment)skin.GetAttachment(slotIndex, attachmentName); + if (attachment == null) throw new SerializationException("Vertex attachment not found: " + attachmentName); + bool weighted = attachment.Bones != null; + float[] vertices = attachment.Vertices; + int deformLength = weighted ? (vertices.Length / 3) << 1 : vertices.Length; + + int frameCount = input.ReadInt(true), frameLast = frameCount - 1; + DeformTimeline timeline = new DeformTimeline(frameCount, input.ReadInt(true), slotIndex, attachment); + + float time = input.ReadFloat(); + for (int frame = 0, bezier = 0; ; frame++) { + float[] deform; + int end = input.ReadInt(true); + if (end == 0) + deform = weighted ? new float[deformLength] : vertices; + else { + deform = new float[deformLength]; + int start = input.ReadInt(true); + end += start; + if (scale == 1) { + for (int v = start; v < end; v++) + deform[v] = input.ReadFloat(); + } else { + for (int v = start; v < end; v++) + deform[v] = input.ReadFloat() * scale; + } + if (!weighted) { + for (int v = 0, vn = deform.Length; v < vn; v++) + deform[v] += vertices[v]; + } + } + timeline.SetFrame(frame, time, deform); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1); + break; + } + time = time2; + } + timelines.Add(timeline); + } + } + } + + // Draw order timeline. + int drawOrderCount = input.ReadInt(true); + if (drawOrderCount > 0) { + DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrderCount); + int slotCount = skeletonData.slots.Count; + for (int i = 0; i < drawOrderCount; i++) { + float time = input.ReadFloat(); + int offsetCount = input.ReadInt(true); + int[] drawOrder = new int[slotCount]; + for (int ii = slotCount - 1; ii >= 0; ii--) + drawOrder[ii] = -1; + int[] unchanged = new int[slotCount - offsetCount]; + int originalIndex = 0, unchangedIndex = 0; + for (int ii = 0; ii < offsetCount; ii++) { + int slotIndex = input.ReadInt(true); + // Collect unchanged items. + while (originalIndex != slotIndex) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + input.ReadInt(true)] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) + unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (int ii = slotCount - 1; ii >= 0; ii--) + if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex]; + timeline.SetFrame(i, time, drawOrder); + } + timelines.Add(timeline); + } + + // Event timeline. + int eventCount = input.ReadInt(true); + if (eventCount > 0) { + EventTimeline timeline = new EventTimeline(eventCount); + for (int i = 0; i < eventCount; i++) { + float time = input.ReadFloat(); + EventData eventData = skeletonData.events.Items[input.ReadInt(true)]; + Event e = new Event(time, eventData); + e.intValue = input.ReadInt(false); + e.floatValue = input.ReadFloat(); + e.stringValue = input.ReadBoolean() ? input.ReadString() : eventData.String; + if (e.Data.AudioPath != null) { + e.volume = input.ReadFloat(); + e.balance = input.ReadFloat(); + } + timeline.SetFrame(i, e); + } + timelines.Add(timeline); + } + + float duration = 0; + var items = timelines.Items; + for (int i = 0, n = timelines.Count; i < n; i++) + duration = Math.Max(duration, items[i].Duration); + return new Animation(name, timelines, duration); + } + + /// Throws IOException when a read operation fails. + private Timeline ReadTimeline (SkeletonInput input, CurveTimeline1 timeline, float scale) { + float time = input.ReadFloat(), value = input.ReadFloat() * scale; + for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) { + timeline.SetFrame(frame, time, value); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), value2 = input.ReadFloat() * scale; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, value, value2, scale); + break; + } + time = time2; + value = value2; + } + return timeline; + } + + /// Throws IOException when a read operation fails. + private Timeline ReadTimeline (SkeletonInput input, CurveTimeline2 timeline, float scale) { + float time = input.ReadFloat(), value1 = input.ReadFloat() * scale, value2 = input.ReadFloat() * scale; + for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) { + timeline.SetFrame(frame, time, value1, value2); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), nvalue1 = input.ReadFloat() * scale, nvalue2 = input.ReadFloat() * scale; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, value1, nvalue1, scale); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, value2, nvalue2, scale); + break; + } + time = time2; + value1 = nvalue1; + value2 = nvalue2; + } + return timeline; + } + + /// Throws IOException when a read operation fails. + void SetBezier (SkeletonInput input, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2, + float value1, float value2, float scale) { + timeline.SetBezier(bezier, frame, value, time1, value1, input.ReadFloat(), input.ReadFloat() * scale, input.ReadFloat(), + input.ReadFloat() * scale, time2, value2); + } + + internal class Vertices { + public int[] bones; + public float[] vertices; + } + + internal class SkeletonInput { + private byte[] chars = new byte[32]; + private byte[] bytesBigEndian = new byte[8]; + internal string[] strings; + Stream input; + + public SkeletonInput (Stream input) { + this.input = input; + } + + public int Read () { + return input.ReadByte(); + } + + public byte ReadByte () { + return (byte)input.ReadByte(); + } + + public sbyte ReadSByte () { + int value = input.ReadByte(); + if (value == -1) throw new EndOfStreamException(); + return (sbyte)value; + } + + public bool ReadBoolean () { + return input.ReadByte() != 0; + } + + public float ReadFloat () { + input.Read(bytesBigEndian, 0, 4); + chars[3] = bytesBigEndian[0]; + chars[2] = bytesBigEndian[1]; + chars[1] = bytesBigEndian[2]; + chars[0] = bytesBigEndian[3]; + return BitConverter.ToSingle(chars, 0); + } + + public int ReadInt () { + input.Read(bytesBigEndian, 0, 4); + return (bytesBigEndian[0] << 24) + + (bytesBigEndian[1] << 16) + + (bytesBigEndian[2] << 8) + + bytesBigEndian[3]; + } + + public long ReadLong () { + input.Read(bytesBigEndian, 0, 8); + return ((long)(bytesBigEndian[0]) << 56) + + ((long)(bytesBigEndian[1]) << 48) + + ((long)(bytesBigEndian[2]) << 40) + + ((long)(bytesBigEndian[3]) << 32) + + ((long)(bytesBigEndian[4]) << 24) + + ((long)(bytesBigEndian[5]) << 16) + + ((long)(bytesBigEndian[6]) << 8) + + (long)(bytesBigEndian[7]); + } + + public int ReadInt (bool optimizePositive) { + int b = input.ReadByte(); + int result = b & 0x7F; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) result |= (input.ReadByte() & 0x7F) << 28; + } + } + } + return optimizePositive ? result : ((result >> 1) ^ -(result & 1)); + } + + public string ReadString () { + int byteCount = ReadInt(true); + switch (byteCount) { + case 0: + return null; + case 1: + return ""; + } + byteCount--; + byte[] buffer = this.chars; + if (buffer.Length < byteCount) buffer = new byte[byteCount]; + ReadFully(buffer, 0, byteCount); + return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount); + } + + ///May be null. + public String ReadStringRef () { + int index = ReadInt(true); + return index == 0 ? null : strings[index - 1]; + } + + public void ReadFully (byte[] buffer, int offset, int length) { + while (length > 0) { + int count = input.Read(buffer, offset, length); + if (count <= 0) throw new EndOfStreamException(); + offset += count; + length -= count; + } + } + + /// Returns the version string of binary skeleton data. + public string GetVersionString () { + try { + // try reading 4.0+ format + var initialPosition = input.Position; + ReadLong(); // long hash + + var stringPosition = input.Position; + int stringByteCount = ReadInt(true); + input.Position = stringPosition; + if (stringByteCount <= 13) { + string version = ReadString(); + if (char.IsDigit(version[0])) + return version; + } + // fallback to old version format + input.Position = initialPosition; + return GetVersionStringOld3X(); + } catch (Exception e) { + throw new ArgumentException("Stream does not contain valid binary Skeleton Data.\n" + e, "input"); + } + } + + /// Returns old 3.8 and earlier format version string of binary skeleton data. + public string GetVersionStringOld3X () { + // Hash. + int byteCount = ReadInt(true); + if (byteCount > 1) input.Position += byteCount - 1; + + // Version. + byteCount = ReadInt(true); + if (byteCount > 1 && byteCount <= 13) { + byteCount--; + var buffer = new byte[byteCount]; + ReadFully(buffer, 0, byteCount); + return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount); + } + throw new ArgumentException("Stream does not contain valid binary Skeleton Data."); + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonBounds.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonBounds.cs new file mode 100644 index 0000000..e1f811c --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonBounds.cs @@ -0,0 +1,233 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + + /// + /// Collects each BoundingBoxAttachment that is visible and computes the world vertices for its polygon. + /// The polygon vertices are provided along with convenience methods for doing hit detection. + /// + public class SkeletonBounds { + private ExposedList polygonPool = new ExposedList(); + private float minX, minY, maxX, maxY; + + public ExposedList BoundingBoxes { get; private set; } + public ExposedList Polygons { get; private set; } + public float MinX { get { return minX; } set { minX = value; } } + public float MinY { get { return minY; } set { minY = value; } } + public float MaxX { get { return maxX; } set { maxX = value; } } + public float MaxY { get { return maxY; } set { maxY = value; } } + public float Width { get { return maxX - minX; } } + public float Height { get { return maxY - minY; } } + + public SkeletonBounds () { + BoundingBoxes = new ExposedList(); + Polygons = new ExposedList(); + } + + /// + /// Clears any previous polygons, finds all visible bounding box attachments, + /// and computes the world vertices for each bounding box's polygon. + /// The skeleton. + /// + /// If true, the axis aligned bounding box containing all the polygons is computed. + /// If false, the SkeletonBounds AABB methods will always return true. + /// + public void Update (Skeleton skeleton, bool updateAabb) { + ExposedList boundingBoxes = BoundingBoxes; + ExposedList polygons = Polygons; + Slot[] slots = skeleton.slots.Items; + int slotCount = skeleton.slots.Count; + + boundingBoxes.Clear(); + for (int i = 0, n = polygons.Count; i < n; i++) + polygonPool.Add(polygons.Items[i]); + polygons.Clear(); + + for (int i = 0; i < slotCount; i++) { + Slot slot = slots[i]; + if (!slot.bone.active) continue; + BoundingBoxAttachment boundingBox = slot.attachment as BoundingBoxAttachment; + if (boundingBox == null) continue; + boundingBoxes.Add(boundingBox); + + Polygon polygon = null; + int poolCount = polygonPool.Count; + if (poolCount > 0) { + polygon = polygonPool.Items[poolCount - 1]; + polygonPool.RemoveAt(poolCount - 1); + } else + polygon = new Polygon(); + polygons.Add(polygon); + + int count = boundingBox.worldVerticesLength; + polygon.Count = count; + if (polygon.Vertices.Length < count) polygon.Vertices = new float[count]; + boundingBox.ComputeWorldVertices(slot, polygon.Vertices); + } + + if (updateAabb) { + AabbCompute(); + } else { + minX = int.MinValue; + minY = int.MinValue; + maxX = int.MaxValue; + maxY = int.MaxValue; + } + } + + private void AabbCompute () { + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + Polygon[] polygons = Polygons.Items; + for (int i = 0, n = Polygons.Count; i < n; i++) { + Polygon polygon = polygons[i]; + float[] vertices = polygon.Vertices; + for (int ii = 0, nn = polygon.Count; ii < nn; ii += 2) { + float x = vertices[ii]; + float y = vertices[ii + 1]; + minX = Math.Min(minX, x); + minY = Math.Min(minY, y); + maxX = Math.Max(maxX, x); + maxY = Math.Max(maxY, y); + } + } + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + /// Returns true if the axis aligned bounding box contains the point. + public bool AabbContainsPoint (float x, float y) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + /// Returns true if the axis aligned bounding box intersects the line segment. + public bool AabbIntersectsSegment (float x1, float y1, float x2, float y2) { + float minX = this.minX; + float minY = this.minY; + float maxX = this.maxX; + float maxY = this.maxY; + if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) + return false; + float m = (y2 - y1) / (x2 - x1); + float y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + float x = (minY - y1) / m + x1; + if (x > minX && x < maxX) return true; + x = (maxY - y1) / m + x1; + if (x > minX && x < maxX) return true; + return false; + } + + /// Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. + public bool AabbIntersectsSkeleton (SkeletonBounds bounds) { + return minX < bounds.maxX && maxX > bounds.minX && minY < bounds.maxY && maxY > bounds.minY; + } + + /// Returns true if the polygon contains the point. + public bool ContainsPoint (Polygon polygon, float x, float y) { + float[] vertices = polygon.Vertices; + int nn = polygon.Count; + + int prevIndex = nn - 2; + bool inside = false; + for (int ii = 0; ii < nn; ii += 2) { + float vertexY = vertices[ii + 1]; + float prevY = vertices[prevIndex + 1]; + if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { + float vertexX = vertices[ii]; + if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; + } + prevIndex = ii; + } + return inside; + } + + /// Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more + /// efficient to only call this method if returns true. + public BoundingBoxAttachment ContainsPoint (float x, float y) { + Polygon[] polygons = Polygons.Items; + for (int i = 0, n = Polygons.Count; i < n; i++) + if (ContainsPoint(polygons[i], x, y)) return BoundingBoxes.Items[i]; + return null; + } + + /// Returns the first bounding box attachment that contains the line segment, or null. When doing many checks, it is usually + /// more efficient to only call this method if returns true. + public BoundingBoxAttachment IntersectsSegment (float x1, float y1, float x2, float y2) { + Polygon[] polygons = Polygons.Items; + for (int i = 0, n = Polygons.Count; i < n; i++) + if (IntersectsSegment(polygons[i], x1, y1, x2, y2)) return BoundingBoxes.Items[i]; + return null; + } + + /// Returns true if the polygon contains the line segment. + public bool IntersectsSegment (Polygon polygon, float x1, float y1, float x2, float y2) { + float[] vertices = polygon.Vertices; + int nn = polygon.Count; + + float width12 = x1 - x2, height12 = y1 - y2; + float det1 = x1 * y2 - y1 * x2; + float x3 = vertices[nn - 2], y3 = vertices[nn - 1]; + for (int ii = 0; ii < nn; ii += 2) { + float x4 = vertices[ii], y4 = vertices[ii + 1]; + float det2 = x3 * y4 - y3 * x4; + float width34 = x3 - x4, height34 = y3 - y4; + float det3 = width12 * height34 - height12 * width34; + float x = (det1 * width34 - width12 * det2) / det3; + if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { + float y = (det1 * height34 - height12 * det2) / det3; + if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; + } + x3 = x4; + y3 = y4; + } + return false; + } + + public Polygon GetPolygon (BoundingBoxAttachment attachment) { + int index = BoundingBoxes.IndexOf(attachment); + return index == -1 ? null : Polygons.Items[index]; + } + } + + public class Polygon { + public float[] Vertices { get; set; } + public int Count { get; set; } + + public Polygon () { + Vertices = new float[16]; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonClipping.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonClipping.cs new file mode 100644 index 0000000..1172fbc --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonClipping.cs @@ -0,0 +1,292 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + public class SkeletonClipping { + internal readonly Triangulator triangulator = new Triangulator(); + internal readonly ExposedList clippingPolygon = new ExposedList(); + internal readonly ExposedList clipOutput = new ExposedList(128); + internal readonly ExposedList clippedVertices = new ExposedList(128); + internal readonly ExposedList clippedTriangles = new ExposedList(128); + internal readonly ExposedList clippedUVs = new ExposedList(128); + internal readonly ExposedList scratch = new ExposedList(); + + internal ClippingAttachment clipAttachment; + internal ExposedList> clippingPolygons; + + public ExposedList ClippedVertices { get { return clippedVertices; } } + public ExposedList ClippedTriangles { get { return clippedTriangles; } } + public ExposedList ClippedUVs { get { return clippedUVs; } } + + public bool IsClipping { get { return clipAttachment != null; } } + + public int ClipStart (Slot slot, ClippingAttachment clip) { + if (clipAttachment != null) return 0; + clipAttachment = clip; + + int n = clip.worldVerticesLength; + float[] vertices = clippingPolygon.Resize(n).Items; + clip.ComputeWorldVertices(slot, 0, n, vertices, 0, 2); + MakeClockwise(clippingPolygon); + clippingPolygons = triangulator.Decompose(clippingPolygon, triangulator.Triangulate(clippingPolygon)); + foreach (var polygon in clippingPolygons) { + MakeClockwise(polygon); + polygon.Add(polygon.Items[0]); + polygon.Add(polygon.Items[1]); + } + return clippingPolygons.Count; + } + + public void ClipEnd (Slot slot) { + if (clipAttachment != null && clipAttachment.endSlot == slot.data) ClipEnd(); + } + + public void ClipEnd () { + if (clipAttachment == null) return; + clipAttachment = null; + clippingPolygons = null; + clippedVertices.Clear(); + clippedTriangles.Clear(); + clippingPolygon.Clear(); + } + + public void ClipTriangles (float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs) { + ExposedList clipOutput = this.clipOutput, clippedVertices = this.clippedVertices; + var clippedTriangles = this.clippedTriangles; + var polygons = clippingPolygons.Items; + int polygonsCount = clippingPolygons.Count; + + int index = 0; + clippedVertices.Clear(); + clippedUVs.Clear(); + clippedTriangles.Clear(); + //outer: + for (int i = 0; i < trianglesLength; i += 3) { + int vertexOffset = triangles[i] << 1; + float x1 = vertices[vertexOffset], y1 = vertices[vertexOffset + 1]; + float u1 = uvs[vertexOffset], v1 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 1] << 1; + float x2 = vertices[vertexOffset], y2 = vertices[vertexOffset + 1]; + float u2 = uvs[vertexOffset], v2 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 2] << 1; + float x3 = vertices[vertexOffset], y3 = vertices[vertexOffset + 1]; + float u3 = uvs[vertexOffset], v3 = uvs[vertexOffset + 1]; + + for (int p = 0; p < polygonsCount; p++) { + int s = clippedVertices.Count; + if (Clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) { + int clipOutputLength = clipOutput.Count; + if (clipOutputLength == 0) continue; + float d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1; + float d = 1 / (d0 * d2 + d1 * (y1 - y3)); + + int clipOutputCount = clipOutputLength >> 1; + float[] clipOutputItems = clipOutput.Items; + float[] clippedVerticesItems = clippedVertices.Resize(s + clipOutputCount * 2).Items; + float[] clippedUVsItems = clippedUVs.Resize(s + clipOutputCount * 2).Items; + for (int ii = 0; ii < clipOutputLength; ii += 2) { + float x = clipOutputItems[ii], y = clipOutputItems[ii + 1]; + clippedVerticesItems[s] = x; + clippedVerticesItems[s + 1] = y; + float c0 = x - x3, c1 = y - y3; + float a = (d0 * c0 + d1 * c1) * d; + float b = (d4 * c0 + d2 * c1) * d; + float c = 1 - a - b; + clippedUVsItems[s] = u1 * a + u2 * b + u3 * c; + clippedUVsItems[s + 1] = v1 * a + v2 * b + v3 * c; + s += 2; + } + + s = clippedTriangles.Count; + int[] clippedTrianglesItems = clippedTriangles.Resize(s + 3 * (clipOutputCount - 2)).Items; + clipOutputCount--; + for (int ii = 1; ii < clipOutputCount; ii++) { + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + ii; + clippedTrianglesItems[s + 2] = index + ii + 1; + s += 3; + } + index += clipOutputCount + 1; + } else { + float[] clippedVerticesItems = clippedVertices.Resize(s + 3 * 2).Items; + float[] clippedUVsItems = clippedUVs.Resize(s + 3 * 2).Items; + clippedVerticesItems[s] = x1; + clippedVerticesItems[s + 1] = y1; + clippedVerticesItems[s + 2] = x2; + clippedVerticesItems[s + 3] = y2; + clippedVerticesItems[s + 4] = x3; + clippedVerticesItems[s + 5] = y3; + + clippedUVsItems[s] = u1; + clippedUVsItems[s + 1] = v1; + clippedUVsItems[s + 2] = u2; + clippedUVsItems[s + 3] = v2; + clippedUVsItems[s + 4] = u3; + clippedUVsItems[s + 5] = v3; + + s = clippedTriangles.Count; + int[] clippedTrianglesItems = clippedTriangles.Resize(s + 3).Items; + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + 1; + clippedTrianglesItems[s + 2] = index + 2; + index += 3; + break; //continue outer; + } + } + } + + } + + /** Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping + * area, false is returned. The clipping area must duplicate the first vertex at the end of the vertices list. */ + internal bool Clip (float x1, float y1, float x2, float y2, float x3, float y3, ExposedList clippingArea, ExposedList output) { + var originalOutput = output; + var clipped = false; + + // Avoid copy at the end. + ExposedList input = null; + if (clippingArea.Count % 4 >= 2) { + input = output; + output = scratch; + } else { + input = scratch; + } + + input.Clear(); + input.Add(x1); + input.Add(y1); + input.Add(x2); + input.Add(y2); + input.Add(x3); + input.Add(y3); + input.Add(x1); + input.Add(y1); + output.Clear(); + + float[] clippingVertices = clippingArea.Items; + int clippingVerticesLast = clippingArea.Count - 4; + for (int i = 0; ; i += 2) { + float edgeX = clippingVertices[i], edgeY = clippingVertices[i + 1]; + float edgeX2 = clippingVertices[i + 2], edgeY2 = clippingVertices[i + 3]; + float deltaX = edgeX - edgeX2, deltaY = edgeY - edgeY2; + + float[] inputVertices = input.Items; + int inputVerticesLength = input.Count - 2, outputStart = output.Count; + for (int ii = 0; ii < inputVerticesLength; ii += 2) { + float inputX = inputVertices[ii], inputY = inputVertices[ii + 1]; + float inputX2 = inputVertices[ii + 2], inputY2 = inputVertices[ii + 3]; + bool side2 = deltaX * (inputY2 - edgeY2) - deltaY * (inputX2 - edgeX2) > 0; + if (deltaX * (inputY - edgeY2) - deltaY * (inputX - edgeX2) > 0) { + if (side2) { // v1 inside, v2 inside + output.Add(inputX2); + output.Add(inputY2); + continue; + } + // v1 inside, v2 outside + float c0 = inputY2 - inputY, c2 = inputX2 - inputX; + float s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.Abs(s) > 0.000001f) { + float ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.Add(edgeX + (edgeX2 - edgeX) * ua); + output.Add(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.Add(edgeX); + output.Add(edgeY); + } + } else if (side2) { // v1 outside, v2 inside + float c0 = inputY2 - inputY, c2 = inputX2 - inputX; + float s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.Abs(s) > 0.000001f) { + float ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.Add(edgeX + (edgeX2 - edgeX) * ua); + output.Add(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.Add(edgeX); + output.Add(edgeY); + } + output.Add(inputX2); + output.Add(inputY2); + } + clipped = true; + } + + if (outputStart == output.Count) { // All edges outside. + originalOutput.Clear(); + return true; + } + + output.Add(output.Items[0]); + output.Add(output.Items[1]); + + if (i == clippingVerticesLast) break; + var temp = output; + output = input; + output.Clear(); + input = temp; + } + + if (originalOutput != output) { + originalOutput.Clear(); + for (int i = 0, n = output.Count - 2; i < n; i++) + originalOutput.Add(output.Items[i]); + } else + originalOutput.Resize(originalOutput.Count - 2); + + return clipped; + } + + public static void MakeClockwise (ExposedList polygon) { + float[] vertices = polygon.Items; + int verticeslength = polygon.Count; + + float area = vertices[verticeslength - 2] * vertices[1] - vertices[0] * vertices[verticeslength - 1], p1x, p1y, p2x, p2y; + for (int i = 0, n = verticeslength - 3; i < n; i += 2) { + p1x = vertices[i]; + p1y = vertices[i + 1]; + p2x = vertices[i + 2]; + p2y = vertices[i + 3]; + area += p1x * p2y - p2x * p1y; + } + if (area < 0) return; + + for (int i = 0, lastX = verticeslength - 2, n = verticeslength >> 1; i < n; i += 2) { + float x = vertices[i], y = vertices[i + 1]; + int other = lastX - i; + vertices[i] = vertices[other]; + vertices[i + 1] = vertices[other + 1]; + vertices[other] = x; + vertices[other + 1] = y; + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonData.cs new file mode 100644 index 0000000..20b6b19 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonData.cs @@ -0,0 +1,205 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + + /// Stores the setup pose and all of the stateless data for a skeleton. + public class SkeletonData { + internal string name; + internal ExposedList bones = new ExposedList(); // Ordered parents first + internal ExposedList slots = new ExposedList(); // Setup pose draw order. + internal ExposedList skins = new ExposedList(); + internal Skin defaultSkin; + internal ExposedList events = new ExposedList(); + internal ExposedList animations = new ExposedList(); + internal ExposedList ikConstraints = new ExposedList(); + internal ExposedList transformConstraints = new ExposedList(); + internal ExposedList pathConstraints = new ExposedList(); + internal float x, y, width, height; + internal string version, hash; + + // Nonessential. + internal float fps; + internal string imagesPath, audioPath; + + ///The skeleton's name, which by default is the name of the skeleton data file when possible, or null when a name hasn't been + ///set. + public string Name { get { return name; } set { name = value; } } + + /// The skeleton's bones, sorted parent first. The root bone is always the first bone. + public ExposedList Bones { get { return bones; } } + + public ExposedList Slots { get { return slots; } } + + /// All skins, including the default skin. + public ExposedList Skins { get { return skins; } set { skins = value; } } + + /// + /// The skeleton's default skin. + /// By default this skin contains all attachments that were not in a skin in Spine. + /// + /// May be null. + public Skin DefaultSkin { get { return defaultSkin; } set { defaultSkin = value; } } + + public ExposedList Events { get { return events; } set { events = value; } } + public ExposedList Animations { get { return animations; } set { animations = value; } } + public ExposedList IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } } + public ExposedList TransformConstraints { get { return transformConstraints; } set { transformConstraints = value; } } + public ExposedList PathConstraints { get { return pathConstraints; } set { pathConstraints = value; } } + + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Width { get { return width; } set { width = value; } } + public float Height { get { return height; } set { height = value; } } + /// The Spine version used to export this data, or null. + public string Version { get { return version; } set { version = value; } } + + ///The skeleton data hash. This value will change if any of the skeleton data has changed. + ///May be null. + public string Hash { get { return hash; } set { hash = value; } } + + public string ImagesPath { get { return imagesPath; } set { imagesPath = value; } } + + /// The path to the audio directory as defined in Spine. Available only when nonessential data was exported. + /// May be null. + public string AudioPath { get { return audioPath; } set { audioPath = value; } } + + /// The dopesheet FPS in Spine, or zero if nonessential data was not exported. + public float Fps { get { return fps; } set { fps = value; } } + + // --- Bones. + + /// + /// Finds a bone by comparing each bone's name. + /// It is more efficient to cache the results of this method than to call it multiple times. + /// May be null. + public BoneData FindBone (string boneName) { + if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + BoneData bone = bones[i]; + if (bone.name == boneName) return bone; + } + return null; + } + + // --- Slots. + + /// May be null. + public SlotData FindSlot (string slotName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + var slots = this.slots.Items; + for (int i = 0, n = this.slots.Count; i < n; i++) { + SlotData slot = slots[i]; + if (slot.name == slotName) return slot; + } + return null; + } + + // --- Skins. + + /// May be null. + public Skin FindSkin (string skinName) { + if (skinName == null) throw new ArgumentNullException("skinName", "skinName cannot be null."); + foreach (Skin skin in skins) + if (skin.name == skinName) return skin; + return null; + } + + // --- Events. + + /// May be null. + public EventData FindEvent (string eventDataName) { + if (eventDataName == null) throw new ArgumentNullException("eventDataName", "eventDataName cannot be null."); + foreach (EventData eventData in events) + if (eventData.name == eventDataName) return eventData; + return null; + } + + // --- Animations. + + /// May be null. + public Animation FindAnimation (string animationName) { + if (animationName == null) throw new ArgumentNullException("animationName", "animationName cannot be null."); + var animations = this.animations.Items; + for (int i = 0, n = this.animations.Count; i < n; i++) { + Animation animation = animations[i]; + if (animation.name == animationName) return animation; + } + return null; + } + + // --- IK constraints. + + /// May be null. + public IkConstraintData FindIkConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var ikConstraints = this.ikConstraints.Items; + for (int i = 0, n = this.ikConstraints.Count; i < n; i++) { + IkConstraintData ikConstraint = ikConstraints[i]; + if (ikConstraint.name == constraintName) return ikConstraint; + } + return null; + } + + // --- Transform constraints. + + /// May be null. + public TransformConstraintData FindTransformConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var transformConstraints = this.transformConstraints.Items; + for (int i = 0, n = this.transformConstraints.Count; i < n; i++) { + TransformConstraintData transformConstraint = transformConstraints[i]; + if (transformConstraint.name == constraintName) return transformConstraint; + } + return null; + } + + // --- Path constraints. + + /// May be null. + public PathConstraintData FindPathConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var pathConstraints = this.pathConstraints.Items; + for (int i = 0, n = this.pathConstraints.Count; i < n; i++) { + PathConstraintData constraint = pathConstraints[i]; + if (constraint.name.Equals(constraintName)) return constraint; + } + return null; + } + + // --- + + override public string ToString () { + return name ?? base.ToString(); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonJson.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonJson.cs new file mode 100644 index 0000000..f9081e9 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonJson.cs @@ -0,0 +1,1193 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.Collections.Generic; +using System.IO; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace Spine4_0_64 { + + /// + /// Loads skeleton data in the Spine JSON format. + /// + /// JSON is human readable but the binary format is much smaller on disk and faster to load. See . + /// + /// See Spine JSON format and + /// JSON and binary data in the Spine + /// Runtimes Guide. + /// + public class SkeletonJson : SkeletonLoader { + + public SkeletonJson (AttachmentLoader attachmentLoader) + : base(attachmentLoader) { + } + + public SkeletonJson (params Atlas[] atlasArray) + : base(atlasArray) { + } + +#if !IS_UNITY && WINDOWS_STOREAPP + private async Task ReadFile(string path) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false); + using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) { + SkeletonData skeletonData = ReadSkeletonData(reader); + skeletonData.Name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + + public override SkeletonData ReadSkeletonData (string path) { + return this.ReadFile(path).Result; + } +#else + public override SkeletonData ReadSkeletonData (string path) { +#if WINDOWS_PHONE + using (var reader = new StreamReader(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) { +#else + using (var reader = new StreamReader(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))) { +#endif + SkeletonData skeletonData = ReadSkeletonData(reader); + skeletonData.name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } +#endif + + public SkeletonData ReadSkeletonData (TextReader reader) { + if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null."); + + float scale = this.scale; + var skeletonData = new SkeletonData(); + + var root = Json.Deserialize(reader) as Dictionary; + if (root == null) throw new Exception("Invalid JSON."); + + // Skeleton. + if (root.ContainsKey("skeleton")) { + var skeletonMap = (Dictionary)root["skeleton"]; + skeletonData.hash = (string)skeletonMap["hash"]; + skeletonData.version = (string)skeletonMap["spine"]; + skeletonData.x = GetFloat(skeletonMap, "x", 0); + skeletonData.y = GetFloat(skeletonMap, "y", 0); + skeletonData.width = GetFloat(skeletonMap, "width", 0); + skeletonData.height = GetFloat(skeletonMap, "height", 0); + skeletonData.fps = GetFloat(skeletonMap, "fps", 30); + skeletonData.imagesPath = GetString(skeletonMap, "images", null); + skeletonData.audioPath = GetString(skeletonMap, "audio", null); + } + + // Bones. + if (root.ContainsKey("bones")) { + foreach (Dictionary boneMap in (List)root["bones"]) { + BoneData parent = null; + if (boneMap.ContainsKey("parent")) { + parent = skeletonData.FindBone((string)boneMap["parent"]); + if (parent == null) + throw new Exception("Parent bone not found: " + boneMap["parent"]); + } + var data = new BoneData(skeletonData.Bones.Count, (string)boneMap["name"], parent); + data.length = GetFloat(boneMap, "length", 0) * scale; + data.x = GetFloat(boneMap, "x", 0) * scale; + data.y = GetFloat(boneMap, "y", 0) * scale; + data.rotation = GetFloat(boneMap, "rotation", 0); + data.scaleX = GetFloat(boneMap, "scaleX", 1); + data.scaleY = GetFloat(boneMap, "scaleY", 1); + data.shearX = GetFloat(boneMap, "shearX", 0); + data.shearY = GetFloat(boneMap, "shearY", 0); + + string tm = GetString(boneMap, "transform", TransformMode.Normal.ToString()); + data.transformMode = (TransformMode)Enum.Parse(typeof(TransformMode), tm, true); + data.skinRequired = GetBoolean(boneMap, "skin", false); + + skeletonData.bones.Add(data); + } + } + + // Slots. + if (root.ContainsKey("slots")) { + foreach (Dictionary slotMap in (List)root["slots"]) { + var slotName = (string)slotMap["name"]; + var boneName = (string)slotMap["bone"]; + BoneData boneData = skeletonData.FindBone(boneName); + if (boneData == null) throw new Exception("Slot bone not found: " + boneName); + var data = new SlotData(skeletonData.Slots.Count, slotName, boneData); + + if (slotMap.ContainsKey("color")) { + string color = (string)slotMap["color"]; + data.r = ToColor(color, 0); + data.g = ToColor(color, 1); + data.b = ToColor(color, 2); + data.a = ToColor(color, 3); + } + + if (slotMap.ContainsKey("dark")) { + var color2 = (string)slotMap["dark"]; + data.r2 = ToColor(color2, 0, 6); // expectedLength = 6. ie. "RRGGBB" + data.g2 = ToColor(color2, 1, 6); + data.b2 = ToColor(color2, 2, 6); + data.hasSecondColor = true; + } + + data.attachmentName = GetString(slotMap, "attachment", null); + if (slotMap.ContainsKey("blend")) + data.blendMode = (BlendMode)Enum.Parse(typeof(BlendMode), (string)slotMap["blend"], true); + else + data.blendMode = BlendMode.Normal; + skeletonData.slots.Add(data); + } + } + + // IK constraints. + if (root.ContainsKey("ik")) { + foreach (Dictionary constraintMap in (List)root["ik"]) { + IkConstraintData data = new IkConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + data.skinRequired = GetBoolean(constraintMap, "skin", false); + + if (constraintMap.ContainsKey("bones")) { + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("IK bone not found: " + boneName); + data.bones.Add(bone); + } + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindBone(targetName); + if (data.target == null) throw new Exception("IK target bone not found: " + targetName); + data.mix = GetFloat(constraintMap, "mix", 1); + data.softness = GetFloat(constraintMap, "softness", 0) * scale; + data.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1; + data.compress = GetBoolean(constraintMap, "compress", false); + data.stretch = GetBoolean(constraintMap, "stretch", false); + data.uniform = GetBoolean(constraintMap, "uniform", false); + + skeletonData.ikConstraints.Add(data); + } + } + + // Transform constraints. + if (root.ContainsKey("transform")) { + foreach (Dictionary constraintMap in (List)root["transform"]) { + TransformConstraintData data = new TransformConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + data.skinRequired = GetBoolean(constraintMap, "skin", false); + + if (constraintMap.ContainsKey("bones")) { + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("Transform constraint bone not found: " + boneName); + data.bones.Add(bone); + } + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindBone(targetName); + if (data.target == null) throw new Exception("Transform constraint target bone not found: " + targetName); + + data.local = GetBoolean(constraintMap, "local", false); + data.relative = GetBoolean(constraintMap, "relative", false); + + data.offsetRotation = GetFloat(constraintMap, "rotation", 0); + data.offsetX = GetFloat(constraintMap, "x", 0) * scale; + data.offsetY = GetFloat(constraintMap, "y", 0) * scale; + data.offsetScaleX = GetFloat(constraintMap, "scaleX", 0); + data.offsetScaleY = GetFloat(constraintMap, "scaleY", 0); + data.offsetShearY = GetFloat(constraintMap, "shearY", 0); + + data.mixRotate = GetFloat(constraintMap, "mixRotate", 1); + data.mixX = GetFloat(constraintMap, "mixX", 1); + data.mixY = GetFloat(constraintMap, "mixY", data.mixX); + data.mixScaleX = GetFloat(constraintMap, "mixScaleX", 1); + data.mixScaleY = GetFloat(constraintMap, "mixScaleY", data.mixScaleX); + data.mixShearY = GetFloat(constraintMap, "mixShearY", 1); + + skeletonData.transformConstraints.Add(data); + } + } + + // Path constraints. + if (root.ContainsKey("path")) { + foreach (Dictionary constraintMap in (List)root["path"]) { + PathConstraintData data = new PathConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + data.skinRequired = GetBoolean(constraintMap, "skin", false); + + if (constraintMap.ContainsKey("bones")) { + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("Path bone not found: " + boneName); + data.bones.Add(bone); + } + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindSlot(targetName); + if (data.target == null) throw new Exception("Path target slot not found: " + targetName); + + data.positionMode = (PositionMode)Enum.Parse(typeof(PositionMode), GetString(constraintMap, "positionMode", "percent"), true); + data.spacingMode = (SpacingMode)Enum.Parse(typeof(SpacingMode), GetString(constraintMap, "spacingMode", "length"), true); + data.rotateMode = (RotateMode)Enum.Parse(typeof(RotateMode), GetString(constraintMap, "rotateMode", "tangent"), true); + data.offsetRotation = GetFloat(constraintMap, "rotation", 0); + data.position = GetFloat(constraintMap, "position", 0); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = GetFloat(constraintMap, "spacing", 0); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.mixRotate = GetFloat(constraintMap, "mixRotate", 1); + data.mixX = GetFloat(constraintMap, "mixX", 1); + data.mixY = GetFloat(constraintMap, "mixY", data.mixX); + + skeletonData.pathConstraints.Add(data); + } + } + + // Skins. + if (root.ContainsKey("skins")) { + foreach (Dictionary skinMap in (List)root["skins"]) { + Skin skin = new Skin((string)skinMap["name"]); + if (skinMap.ContainsKey("bones")) { + foreach (string entryName in (List)skinMap["bones"]) { + BoneData bone = skeletonData.FindBone(entryName); + if (bone == null) throw new Exception("Skin bone not found: " + entryName); + skin.bones.Add(bone); + } + } + skin.bones.TrimExcess(); + if (skinMap.ContainsKey("ik")) { + foreach (string entryName in (List)skinMap["ik"]) { + IkConstraintData constraint = skeletonData.FindIkConstraint(entryName); + if (constraint == null) throw new Exception("Skin IK constraint not found: " + entryName); + skin.constraints.Add(constraint); + } + } + if (skinMap.ContainsKey("transform")) { + foreach (string entryName in (List)skinMap["transform"]) { + TransformConstraintData constraint = skeletonData.FindTransformConstraint(entryName); + if (constraint == null) throw new Exception("Skin transform constraint not found: " + entryName); + skin.constraints.Add(constraint); + } + } + if (skinMap.ContainsKey("path")) { + foreach (string entryName in (List)skinMap["path"]) { + PathConstraintData constraint = skeletonData.FindPathConstraint(entryName); + if (constraint == null) throw new Exception("Skin path constraint not found: " + entryName); + skin.constraints.Add(constraint); + } + } + skin.constraints.TrimExcess(); + if (skinMap.ContainsKey("attachments")) { + foreach (KeyValuePair slotEntry in (Dictionary)skinMap["attachments"]) { + int slotIndex = FindSlotIndex(skeletonData, slotEntry.Key); + foreach (KeyValuePair entry in ((Dictionary)slotEntry.Value)) { + try { + Attachment attachment = ReadAttachment((Dictionary)entry.Value, skin, slotIndex, entry.Key, skeletonData); + if (attachment != null) skin.SetAttachment(slotIndex, entry.Key, attachment); + } catch (Exception e) { + throw new Exception("Error reading attachment: " + entry.Key + ", skin: " + skin, e); + } + } + } + } + skeletonData.skins.Add(skin); + if (skin.name == "default") skeletonData.defaultSkin = skin; + } + } + + // Linked meshes. + for (int i = 0, n = linkedMeshes.Count; i < n; i++) { + LinkedMesh linkedMesh = linkedMeshes[i]; + Skin skin = linkedMesh.skin == null ? skeletonData.defaultSkin : skeletonData.FindSkin(linkedMesh.skin); + if (skin == null) throw new Exception("Slot not found: " + linkedMesh.skin); + Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Exception("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.DeformAttachment = linkedMesh.inheritDeform ? (VertexAttachment)parent : linkedMesh.mesh; + linkedMesh.mesh.ParentMesh = (MeshAttachment)parent; + linkedMesh.mesh.UpdateUVs(); + } + linkedMeshes.Clear(); + + // Events. + if (root.ContainsKey("events")) { + foreach (KeyValuePair entry in (Dictionary)root["events"]) { + var entryMap = (Dictionary)entry.Value; + var data = new EventData(entry.Key); + data.Int = GetInt(entryMap, "int", 0); + data.Float = GetFloat(entryMap, "float", 0); + data.String = GetString(entryMap, "string", string.Empty); + data.AudioPath = GetString(entryMap, "audio", null); + if (data.AudioPath != null) { + data.Volume = GetFloat(entryMap, "volume", 1); + data.Balance = GetFloat(entryMap, "balance", 0); + } + skeletonData.events.Add(data); + } + } + + // Animations. + if (root.ContainsKey("animations")) { + foreach (KeyValuePair entry in (Dictionary)root["animations"]) { + try { + ReadAnimation((Dictionary)entry.Value, entry.Key, skeletonData); + } catch (Exception e) { + throw new Exception("Error reading animation: " + entry.Key + "\n" + e.Message, e); + } + } + } + + skeletonData.bones.TrimExcess(); + skeletonData.slots.TrimExcess(); + skeletonData.skins.TrimExcess(); + skeletonData.events.TrimExcess(); + skeletonData.animations.TrimExcess(); + skeletonData.ikConstraints.TrimExcess(); + return skeletonData; + } + + private Attachment ReadAttachment (Dictionary map, Skin skin, int slotIndex, string name, SkeletonData skeletonData) { + float scale = this.scale; + name = GetString(map, "name", name); + + var typeName = GetString(map, "type", "region"); + var type = (AttachmentType)Enum.Parse(typeof(AttachmentType), typeName, true); + + string path = GetString(map, "path", name); + + switch (type) { + case AttachmentType.Region: + RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path); + if (region == null) return null; + region.Path = path; + region.x = GetFloat(map, "x", 0) * scale; + region.y = GetFloat(map, "y", 0) * scale; + region.scaleX = GetFloat(map, "scaleX", 1); + region.scaleY = GetFloat(map, "scaleY", 1); + region.rotation = GetFloat(map, "rotation", 0); + region.width = GetFloat(map, "width", 32) * scale; + region.height = GetFloat(map, "height", 32) * scale; + + if (map.ContainsKey("color")) { + var color = (string)map["color"]; + region.r = ToColor(color, 0); + region.g = ToColor(color, 1); + region.b = ToColor(color, 2); + region.a = ToColor(color, 3); + } + + region.UpdateOffset(); + return region; + case AttachmentType.Boundingbox: + BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name); + if (box == null) return null; + ReadVertices(map, box, GetInt(map, "vertexCount", 0) << 1); + return box; + case AttachmentType.Mesh: + case AttachmentType.Linkedmesh: { + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.Path = path; + + if (map.ContainsKey("color")) { + var color = (string)map["color"]; + mesh.r = ToColor(color, 0); + mesh.g = ToColor(color, 1); + mesh.b = ToColor(color, 2); + mesh.a = ToColor(color, 3); + } + + mesh.Width = GetFloat(map, "width", 0) * scale; + mesh.Height = GetFloat(map, "height", 0) * scale; + + string parent = GetString(map, "parent", null); + if (parent != null) { + linkedMeshes.Add(new LinkedMesh(mesh, GetString(map, "skin", null), slotIndex, parent, GetBoolean(map, "deform", true))); + return mesh; + } + + float[] uvs = GetFloatArray(map, "uvs", 1); + ReadVertices(map, mesh, uvs.Length); + mesh.triangles = GetIntArray(map, "triangles"); + mesh.regionUVs = uvs; + mesh.UpdateUVs(); + + if (map.ContainsKey("hull")) mesh.HullLength = GetInt(map, "hull", 0) << 1; + if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges"); + return mesh; + } + case AttachmentType.Path: { + PathAttachment pathAttachment = attachmentLoader.NewPathAttachment(skin, name); + if (pathAttachment == null) return null; + pathAttachment.closed = GetBoolean(map, "closed", false); + pathAttachment.constantSpeed = GetBoolean(map, "constantSpeed", true); + + int vertexCount = GetInt(map, "vertexCount", 0); + ReadVertices(map, pathAttachment, vertexCount << 1); + + // potential BOZO see Java impl + pathAttachment.lengths = GetFloatArray(map, "lengths", scale); + return pathAttachment; + } + case AttachmentType.Point: { + PointAttachment point = attachmentLoader.NewPointAttachment(skin, name); + if (point == null) return null; + point.x = GetFloat(map, "x", 0) * scale; + point.y = GetFloat(map, "y", 0) * scale; + point.rotation = GetFloat(map, "rotation", 0); + + //string color = GetString(map, "color", null); + //if (color != null) point.color = color; + return point; + } + case AttachmentType.Clipping: { + ClippingAttachment clip = attachmentLoader.NewClippingAttachment(skin, name); + if (clip == null) return null; + + string end = GetString(map, "end", null); + if (end != null) { + SlotData slot = skeletonData.FindSlot(end); + if (slot == null) throw new Exception("Clipping end slot not found: " + end); + clip.EndSlot = slot; + } + + ReadVertices(map, clip, GetInt(map, "vertexCount", 0) << 1); + + //string color = GetString(map, "color", null); + // if (color != null) clip.color = color; + return clip; + } + } + return null; + } + + private void ReadVertices (Dictionary map, VertexAttachment attachment, int verticesLength) { + attachment.WorldVerticesLength = verticesLength; + float[] vertices = GetFloatArray(map, "vertices", 1); + float scale = Scale; + if (verticesLength == vertices.Length) { + if (scale != 1) { + for (int i = 0; i < vertices.Length; i++) { + vertices[i] *= scale; + } + } + attachment.vertices = vertices; + return; + } + ExposedList weights = new ExposedList(verticesLength * 3 * 3); + ExposedList bones = new ExposedList(verticesLength * 3); + for (int i = 0, n = vertices.Length; i < n;) { + int boneCount = (int)vertices[i++]; + bones.Add(boneCount); + for (int nn = i + (boneCount << 2); i < nn; i += 4) { + bones.Add((int)vertices[i]); + weights.Add(vertices[i + 1] * this.Scale); + weights.Add(vertices[i + 2] * this.Scale); + weights.Add(vertices[i + 3]); + } + } + attachment.bones = bones.ToArray(); + attachment.vertices = weights.ToArray(); + } + + private int FindSlotIndex (SkeletonData skeletonData, string slotName) { + SlotData[] slots = skeletonData.slots.Items; + for (int i = 0, n = skeletonData.slots.Count; i < n; i++) + if (slots[i].name == slotName) return i; + throw new Exception("Slot not found: " + slotName); + } + + private void ReadAnimation (Dictionary map, string name, SkeletonData skeletonData) { + var scale = this.scale; + var timelines = new ExposedList(); + + // Slot timelines. + if (map.ContainsKey("slots")) { + foreach (KeyValuePair entry in (Dictionary)map["slots"]) { + string slotName = entry.Key; + int slotIndex = FindSlotIndex(skeletonData, slotName); + var timelineMap = (Dictionary)entry.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + int frames = values.Count; + if (frames == 0) continue; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "attachment") { + var timeline = new AttachmentTimeline(frames, slotIndex); + int frame = 0; + foreach (Dictionary keyMap in values) { + timeline.SetFrame(frame++, GetFloat(keyMap, "time", 0), (string)keyMap["name"]); + } + timelines.Add(timeline); + + } else if (timelineName == "rgba") { + var timeline = new RGBATimeline(frames, frames << 2, slotIndex); + + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + string color = (string)keyMap["color"]; + float r = ToColor(color, 0); + float g = ToColor(color, 1); + float b = ToColor(color, 2); + float a = ToColor(color, 3); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, a); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + + float time2 = GetFloat(nextMap, "time", 0); + color = (string)nextMap["color"]; + float nr = ToColor(color, 0); + float ng = ToColor(color, 1); + float nb = ToColor(color, 2); + float na = ToColor(color, 3); + + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1); + } + time = time2; + r = nr; + g = ng; + b = nb; + a = na; + keyMap = nextMap; + } + timelines.Add(timeline); + + } else if (timelineName == "rgb") { + var timeline = new RGBTimeline(frames, frames * 3, slotIndex); + + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + string color = (string)keyMap["color"]; + float r = ToColor(color, 0, 6); + float g = ToColor(color, 1, 6); + float b = ToColor(color, 2, 6); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + + float time2 = GetFloat(nextMap, "time", 0); + color = (string)nextMap["color"]; + float nr = ToColor(color, 0, 6); + float ng = ToColor(color, 1, 6); + float nb = ToColor(color, 2, 6); + + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1); + } + time = time2; + r = nr; + g = ng; + b = nb; + keyMap = nextMap; + } + timelines.Add(timeline); + + } else if (timelineName == "alpha") { + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + timelines.Add(ReadTimeline(ref keyMapEnumerator, new AlphaTimeline(frames, frames, slotIndex), 0, 1)); + + } else if (timelineName == "rgba2") { + var timeline = new RGBA2Timeline(frames, frames * 7, slotIndex); + + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + string color = (string)keyMap["light"]; + float r = ToColor(color, 0); + float g = ToColor(color, 1); + float b = ToColor(color, 2); + float a = ToColor(color, 3); + color = (string)keyMap["dark"]; + float r2 = ToColor(color, 0, 6); + float g2 = ToColor(color, 1, 6); + float b2 = ToColor(color, 2, 6); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, a, r2, g2, b2); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + + float time2 = GetFloat(nextMap, "time", 0); + color = (string)nextMap["light"]; + float nr = ToColor(color, 0); + float ng = ToColor(color, 1); + float nb = ToColor(color, 2); + float na = ToColor(color, 3); + color = (string)nextMap["dark"]; + float nr2 = ToColor(color, 0, 6); + float ng2 = ToColor(color, 1, 6); + float nb2 = ToColor(color, 2, 6); + + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 4, time, time2, r2, nr2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 5, time, time2, g2, ng2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 6, time, time2, b2, nb2, 1); + } + time = time2; + r = nr; + g = ng; + b = nb; + a = na; + r2 = nr2; + g2 = ng2; + b2 = nb2; + keyMap = nextMap; + } + timelines.Add(timeline); + + } else if (timelineName == "rgb2") { + var timeline = new RGB2Timeline(frames, frames * 6, slotIndex); + + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + string color = (string)keyMap["light"]; + float r = ToColor(color, 0, 6); + float g = ToColor(color, 1, 6); + float b = ToColor(color, 2, 6); + color = (string)keyMap["dark"]; + float r2 = ToColor(color, 0, 6); + float g2 = ToColor(color, 1, 6); + float b2 = ToColor(color, 2, 6); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, r2, g2, b2); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + + float time2 = GetFloat(nextMap, "time", 0); + color = (string)nextMap["light"]; + float nr = ToColor(color, 0, 6); + float ng = ToColor(color, 1, 6); + float nb = ToColor(color, 2, 6); + color = (string)nextMap["dark"]; + float nr2 = ToColor(color, 0, 6); + float ng2 = ToColor(color, 1, 6); + float nb2 = ToColor(color, 2, 6); + + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, r2, nr2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 4, time, time2, g2, ng2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 5, time, time2, b2, nb2, 1); + } + time = time2; + r = nr; + g = ng; + b = nb; + r2 = nr2; + g2 = ng2; + b2 = nb2; + keyMap = nextMap; + } + timelines.Add(timeline); + + } else + throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")"); + } + } + } + + // Bone timelines. + if (map.ContainsKey("bones")) { + foreach (KeyValuePair entry in (Dictionary)map["bones"]) { + string boneName = entry.Key; + int boneIndex = -1; + var bones = skeletonData.bones.Items; + for (int i = 0, n = skeletonData.bones.Count; i < n; i++) { + if (bones[i].name == boneName) { + boneIndex = i; + break; + } + } + if (boneIndex == -1) throw new Exception("Bone not found: " + boneName); + var timelineMap = (Dictionary)entry.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + int frames = values.Count; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "rotate") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new RotateTimeline(frames, frames, boneIndex), 0, 1)); + else if (timelineName == "translate") { + TranslateTimeline timeline = new TranslateTimeline(frames, frames << 1, boneIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 0, scale)); + } else if (timelineName == "translatex") { + timelines + .Add(ReadTimeline(ref keyMapEnumerator, new TranslateXTimeline(frames, frames, boneIndex), 0, scale)); + } else if (timelineName == "translatey") { + timelines + .Add(ReadTimeline(ref keyMapEnumerator, new TranslateYTimeline(frames, frames, boneIndex), 0, scale)); + } else if (timelineName == "scale") { + ScaleTimeline timeline = new ScaleTimeline(frames, frames << 1, boneIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 1, 1)); + } else if (timelineName == "scalex") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new ScaleXTimeline(frames, frames, boneIndex), 1, 1)); + else if (timelineName == "scaley") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new ScaleYTimeline(frames, frames, boneIndex), 1, 1)); + else if (timelineName == "shear") { + ShearTimeline timeline = new ShearTimeline(frames, frames << 1, boneIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 0, 1)); + } else if (timelineName == "shearx") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new ShearXTimeline(frames, frames, boneIndex), 0, 1)); + else if (timelineName == "sheary") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new ShearYTimeline(frames, frames, boneIndex), 0, 1)); + else + throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")"); + } + } + } + + // IK constraint timelines. + if (map.ContainsKey("ik")) { + foreach (KeyValuePair timelineMap in (Dictionary)map["ik"]) { + var values = (List)timelineMap.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + var keyMap = (Dictionary)keyMapEnumerator.Current; + IkConstraintData constraint = skeletonData.FindIkConstraint(timelineMap.Key); + IkConstraintTimeline timeline = new IkConstraintTimeline(values.Count, values.Count << 1, + skeletonData.IkConstraints.IndexOf(constraint)); + float time = GetFloat(keyMap, "time", 0); + float mix = GetFloat(keyMap, "mix", 1), softness = GetFloat(keyMap, "softness", 0) * scale; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mix, softness, GetBoolean(keyMap, "bendPositive", true) ? 1 : -1, + GetBoolean(keyMap, "compress", false), GetBoolean(keyMap, "stretch", false)); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float mix2 = GetFloat(nextMap, "mix", 1), softness2 = GetFloat(nextMap, "softness", 0) * scale; + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, mix, mix2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, softness, softness2, scale); + } + time = time2; + mix = mix2; + softness = softness2; + keyMap = nextMap; + } + timelines.Add(timeline); + } + } + + // Transform constraint timelines. + if (map.ContainsKey("transform")) { + foreach (KeyValuePair timelineMap in (Dictionary)map["transform"]) { + var values = (List)timelineMap.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + var keyMap = (Dictionary)keyMapEnumerator.Current; + TransformConstraintData constraint = skeletonData.FindTransformConstraint(timelineMap.Key); + TransformConstraintTimeline timeline = new TransformConstraintTimeline(values.Count, values.Count * 6, + skeletonData.TransformConstraints.IndexOf(constraint)); + float time = GetFloat(keyMap, "time", 0); + float mixRotate = GetFloat(keyMap, "mixRotate", 1), mixShearY = GetFloat(keyMap, "mixShearY", 1); + float mixX = GetFloat(keyMap, "mixX", 1), mixY = GetFloat(keyMap, "mixY", mixX); + float mixScaleX = GetFloat(keyMap, "mixScaleX", 1), mixScaleY = GetFloat(keyMap, "mixScaleY", mixScaleX); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float mixRotate2 = GetFloat(nextMap, "mixRotate", 1), mixShearY2 = GetFloat(nextMap, "mixShearY", 1); + float mixX2 = GetFloat(nextMap, "mixX", 1), mixY2 = GetFloat(nextMap, "mixY", mixX2); + float mixScaleX2 = GetFloat(nextMap, "mixScaleX", 1), mixScaleY2 = GetFloat(nextMap, "mixScaleY", mixScaleX2); + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, mixScaleX, mixScaleX2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 4, time, time2, mixScaleY, mixScaleY2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 5, time, time2, mixShearY, mixShearY2, 1); + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + mixScaleX = mixScaleX2; + mixScaleY = mixScaleY2; + mixScaleX = mixScaleX2; + keyMap = nextMap; + } + timelines.Add(timeline); + } + } + + // Path constraint timelines. + if (map.ContainsKey("path")) { + foreach (KeyValuePair constraintMap in (Dictionary)map["path"]) { + PathConstraintData constraint = skeletonData.FindPathConstraint(constraintMap.Key); + if (constraint == null) throw new Exception("Path constraint not found: " + constraintMap.Key); + int constraintIndex = skeletonData.pathConstraints.IndexOf(constraint); + var timelineMap = (Dictionary)constraintMap.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + + int frames = values.Count; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "position") { + CurveTimeline1 timeline = new PathConstraintPositionTimeline(frames, frames, constraintIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, 0, constraint.positionMode == PositionMode.Fixed ? scale : 1)); + } else if (timelineName == "spacing") { + CurveTimeline1 timeline = new PathConstraintSpacingTimeline(frames, frames, constraintIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, 0, + constraint.spacingMode == SpacingMode.Length || constraint.spacingMode == SpacingMode.Fixed ? scale : 1)); + } else if (timelineName == "mix") { + PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(frames, frames * 3, constraintIndex); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + float mixRotate = GetFloat(keyMap, "mixRotate", 1); + float mixX = GetFloat(keyMap, "mixX", 1), mixY = GetFloat(keyMap, "mixY", mixX); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mixRotate, mixX, mixY); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float mixRotate2 = GetFloat(nextMap, "mixRotate", 1); + float mixX2 = GetFloat(nextMap, "mixX", 1), mixY2 = GetFloat(nextMap, "mixY", mixX2); + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1); + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + keyMap = nextMap; + } + timelines.Add(timeline); + } + } + } + } + + // Deform timelines. + if (map.ContainsKey("deform")) { + foreach (KeyValuePair deformMap in (Dictionary)map["deform"]) { + Skin skin = skeletonData.FindSkin(deformMap.Key); + foreach (KeyValuePair slotMap in (Dictionary)deformMap.Value) { + int slotIndex = FindSlotIndex(skeletonData, slotMap.Key); + foreach (KeyValuePair timelineMap in (Dictionary)slotMap.Value) { + var values = (List)timelineMap.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + var keyMap = (Dictionary)keyMapEnumerator.Current; + VertexAttachment attachment = (VertexAttachment)skin.GetAttachment(slotIndex, timelineMap.Key); + if (attachment == null) throw new Exception("Deform attachment not found: " + timelineMap.Key); + bool weighted = attachment.bones != null; + float[] vertices = attachment.vertices; + int deformLength = weighted ? (vertices.Length / 3) << 1 : vertices.Length; + DeformTimeline timeline = new DeformTimeline(values.Count, values.Count, slotIndex, attachment); + float time = GetFloat(keyMap, "time", 0); + for (int frame = 0, bezier = 0; ; frame++) { + float[] deform; + if (!keyMap.ContainsKey("vertices")) { + deform = weighted ? new float[deformLength] : vertices; + } else { + deform = new float[deformLength]; + int start = GetInt(keyMap, "offset", 0); + float[] verticesValue = GetFloatArray(keyMap, "vertices", 1); + Array.Copy(verticesValue, 0, deform, start, verticesValue.Length); + if (scale != 1) { + for (int i = start, n = i + verticesValue.Length; i < n; i++) + deform[i] *= scale; + } + + if (!weighted) { + for (int i = 0; i < deformLength; i++) + deform[i] += vertices[i]; + } + } + + timeline.SetFrame(frame, time, deform); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, 0, 1, 1); + } + time = time2; + keyMap = nextMap; + } + timelines.Add(timeline); + } + } + } + } + + // Draw order timeline. + if (map.ContainsKey("drawOrder")) { + var values = (List)map["drawOrder"]; + var timeline = new DrawOrderTimeline(values.Count); + int slotCount = skeletonData.slots.Count; + int frame = 0; + foreach (Dictionary drawOrderMap in values) { + int[] drawOrder = null; + if (drawOrderMap.ContainsKey("offsets")) { + drawOrder = new int[slotCount]; + for (int i = slotCount - 1; i >= 0; i--) + drawOrder[i] = -1; + var offsets = (List)drawOrderMap["offsets"]; + int[] unchanged = new int[slotCount - offsets.Count]; + int originalIndex = 0, unchangedIndex = 0; + foreach (Dictionary offsetMap in offsets) { + int slotIndex = FindSlotIndex(skeletonData, (string)offsetMap["slot"]); + // Collect unchanged items. + while (originalIndex != slotIndex) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + int index = originalIndex + (int)(float)offsetMap["offset"]; + drawOrder[index] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) + unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (int i = slotCount - 1; i >= 0; i--) + if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex]; + } + timeline.SetFrame(frame, GetFloat(drawOrderMap, "time", 0), drawOrder); + ++frame; + } + timelines.Add(timeline); + } + + // Event timeline. + if (map.ContainsKey("events")) { + var eventsMap = (List)map["events"]; + var timeline = new EventTimeline(eventsMap.Count); + int frame = 0; + foreach (Dictionary eventMap in eventsMap) { + EventData eventData = skeletonData.FindEvent((string)eventMap["name"]); + if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]); + var e = new Event(GetFloat(eventMap, "time", 0), eventData) { + intValue = GetInt(eventMap, "int", eventData.Int), + floatValue = GetFloat(eventMap, "float", eventData.Float), + stringValue = GetString(eventMap, "string", eventData.String) + }; + if (e.data.AudioPath != null) { + e.volume = GetFloat(eventMap, "volume", eventData.Volume); + e.balance = GetFloat(eventMap, "balance", eventData.Balance); + } + timeline.SetFrame(frame, e); + ++frame; + } + timelines.Add(timeline); + } + timelines.TrimExcess(); + float duration = 0; + var items = timelines.Items; + for (int i = 0, n = timelines.Count; i < n; i++) + duration = Math.Max(duration, items[i].Duration); + skeletonData.animations.Add(new Animation(name, timelines, duration)); + } + + static Timeline ReadTimeline (ref List.Enumerator keyMapEnumerator, CurveTimeline1 timeline, float defaultValue, float scale) { + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + float value = GetFloat(keyMap, "value", defaultValue) * scale; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, value); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + return timeline; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float value2 = GetFloat(nextMap, "value", defaultValue) * scale; + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, value, value2, scale); + } + time = time2; + value = value2; + keyMap = nextMap; + } + } + + static Timeline ReadTimeline (ref List.Enumerator keyMapEnumerator, CurveTimeline2 timeline, String name1, String name2, float defaultValue, + float scale) { + + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + float value1 = GetFloat(keyMap, name1, defaultValue) * scale, value2 = GetFloat(keyMap, name2, defaultValue) * scale; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, value1, value2); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + return timeline; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float nvalue1 = GetFloat(nextMap, name1, defaultValue) * scale, nvalue2 = GetFloat(nextMap, name2, defaultValue) * scale; + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, value1, nvalue1, scale); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, value2, nvalue2, scale); + } + time = time2; + value1 = nvalue1; + value2 = nvalue2; + keyMap = nextMap; + } + } + + static int ReadCurve (object curve, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2, + float value1, float value2, float scale) { + + string curveString = curve as string; + if (curveString != null) { + if (curveString == "stepped") timeline.SetStepped(frame); + return bezier; + } + var curveValues = (List)curve; + int i = value << 2; + float cx1 = (float)curveValues[i]; + float cy1 = (float)curveValues[i + 1] * scale; + float cx2 = (float)curveValues[i + 2]; + float cy2 = (float)curveValues[i + 3] * scale; + SetBezier(timeline, frame, value, bezier, time1, value1, cx1, cy1, cx2, cy2, time2, value2); + return bezier + 1; + } + + static void SetBezier (CurveTimeline timeline, int frame, int value, int bezier, float time1, float value1, float cx1, float cy1, + float cx2, float cy2, float time2, float value2) { + timeline.SetBezier(bezier, frame, value, time1, value1, cx1, cy1, cx2, cy2, time2, value2); + } + + static float[] GetFloatArray (Dictionary map, string name, float scale) { + var list = (List)map[name]; + var values = new float[list.Count]; + if (scale == 1) { + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (float)list[i]; + } else { + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (float)list[i] * scale; + } + return values; + } + + static int[] GetIntArray (Dictionary map, string name) { + var list = (List)map[name]; + var values = new int[list.Count]; + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (int)(float)list[i]; + return values; + } + + static float GetFloat (Dictionary map, string name, float defaultValue) { + if (!map.ContainsKey(name)) return defaultValue; + return (float)map[name]; + } + + static int GetInt (Dictionary map, string name, int defaultValue) { + if (!map.ContainsKey(name)) return defaultValue; + return (int)(float)map[name]; + } + + static bool GetBoolean (Dictionary map, string name, bool defaultValue) { + if (!map.ContainsKey(name)) return defaultValue; + return (bool)map[name]; + } + + static string GetString (Dictionary map, string name, string defaultValue) { + if (!map.ContainsKey(name)) return defaultValue; + return (string)map[name]; + } + + static float ToColor (string hexString, int colorIndex, int expectedLength = 8) { + if (hexString.Length != expectedLength) + throw new ArgumentException("Color hexidecimal length must be " + expectedLength + ", recieved: " + hexString, "hexString"); + return Convert.ToInt32(hexString.Substring(colorIndex * 2, 2), 16) / (float)255; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonLoader.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonLoader.cs new file mode 100644 index 0000000..b4d0025 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SkeletonLoader.cs @@ -0,0 +1,92 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Spine4_0_64 { + + /// + /// Base class for loading skeleton data from a file. + /// + /// SeeJSON and binary data in the + /// Spine Runtimes Guide. + /// + public abstract class SkeletonLoader { + protected readonly AttachmentLoader attachmentLoader; + protected float scale = 1; + protected readonly List linkedMeshes = new List(); + + /// Creates a skeleton loader that loads attachments using an with the specified atlas. + /// + public SkeletonLoader (params Atlas[] atlasArray) { + attachmentLoader = new AtlasAttachmentLoader(atlasArray); + } + + /// Creates a skeleton loader that loads attachments using the specified attachment loader. + /// See Loading skeleton data in the + /// Spine Runtimes Guide. + public SkeletonLoader (AttachmentLoader attachmentLoader) { + if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader", "attachmentLoader cannot be null."); + this.attachmentLoader = attachmentLoader; + } + + /// Scales bone positions, image sizes, and translations as they are loaded. This allows different size images to be used at + /// runtime than were used in Spine. + /// + /// See Scaling in the Spine Runtimes Guide. + /// + public float Scale { + get { return scale; } + set { + if (scale == 0) throw new ArgumentNullException("scale", "scale cannot be 0."); + this.scale = value; + } + } + + public abstract SkeletonData ReadSkeletonData (string path); + + protected class LinkedMesh { + internal string parent, skin; + internal int slotIndex; + internal MeshAttachment mesh; + internal bool inheritDeform; + + public LinkedMesh (MeshAttachment mesh, string skin, int slotIndex, string parent, bool inheritDeform) { + this.mesh = mesh; + this.skin = skin; + this.slotIndex = slotIndex; + this.parent = parent; + this.inheritDeform = inheritDeform; + } + } + + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Skin.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Skin.cs new file mode 100644 index 0000000..2065d11 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Skin.cs @@ -0,0 +1,206 @@ + +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Spine4_0_64 { + /// Stores attachments by slot index and attachment name. + /// See SkeletonData , Skeleton , and + /// Runtime skins in the Spine Runtimes Guide. + /// + public class Skin { + internal string name; + // Difference to reference implementation: using Dictionary instead of HashSet. + // Reason is that there is no efficient way to replace or access an already added element, losing any benefits. + private Dictionary attachments = new Dictionary(SkinKeyComparer.Instance); + internal readonly ExposedList bones = new ExposedList(); + internal readonly ExposedList constraints = new ExposedList(); + + public string Name { get { return name; } } + ///Returns all attachments contained in this skin. + public ICollection Attachments { get { return attachments.Values; } } + public ExposedList Bones { get { return bones; } } + public ExposedList Constraints { get { return constraints; } } + + public Skin (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + /// Adds an attachment to the skin for the specified slot index and name. + /// If the name already exists for the slot, the previous value is replaced. + public void SetAttachment (int slotIndex, string name, Attachment attachment) { + if (attachment == null) throw new ArgumentNullException("attachment", "attachment cannot be null."); + attachments[new SkinKey(slotIndex, name)] = new SkinEntry(slotIndex, name, attachment); + } + + ///Adds all attachments, bones, and constraints from the specified skin to this skin. + public void AddSkin (Skin skin) { + foreach (BoneData data in skin.bones) + if (!bones.Contains(data)) bones.Add(data); + + foreach (ConstraintData data in skin.constraints) + if (!constraints.Contains(data)) constraints.Add(data); + + foreach (var item in skin.attachments) { + SkinEntry entry = item.Value; + SetAttachment(entry.slotIndex, entry.name, entry.attachment); + } + } + + ///Adds all attachments from the specified skin to this skin. Attachments are deep copied. + public void CopySkin (Skin skin) { + foreach (BoneData data in skin.bones) + if (!bones.Contains(data)) bones.Add(data); + + foreach (ConstraintData data in skin.constraints) + if (!constraints.Contains(data)) constraints.Add(data); + + foreach (var item in skin.attachments) { + SkinEntry entry = item.Value; + if (entry.attachment is MeshAttachment) { + SetAttachment(entry.slotIndex, entry.name, + entry.attachment != null ? ((MeshAttachment)entry.attachment).NewLinkedMesh() : null); + } else + SetAttachment(entry.slotIndex, entry.name, entry.attachment != null ? entry.attachment.Copy() : null); + } + } + + /// Returns the attachment for the specified slot index and name, or null. + /// May be null. + public Attachment GetAttachment (int slotIndex, string name) { + SkinEntry entry; + bool containsKey = attachments.TryGetValue(new SkinKey(slotIndex, name), out entry); + return containsKey ? entry.attachment : null; + } + + /// Removes the attachment in the skin for the specified slot index and name, if any. + public void RemoveAttachment (int slotIndex, string name) { + attachments.Remove(new SkinKey(slotIndex, name)); + } + + /// Returns all attachments in this skin for the specified slot index. + /// The target slotIndex. To find the slot index, use and . + public void GetAttachments (int slotIndex, List attachments) { + if (slotIndex < 0) throw new ArgumentException("slotIndex must be >= 0."); + if (attachments == null) throw new ArgumentNullException("attachments", "attachments cannot be null."); + foreach (var item in this.attachments) { + SkinEntry entry = item.Value; + if (entry.slotIndex == slotIndex) attachments.Add(entry); + } + } + + ///Clears all attachments, bones, and constraints. + public void Clear () { + attachments.Clear(); + bones.Clear(); + constraints.Clear(); + } + + override public string ToString () { + return name; + } + + /// Attach all attachments from this skin if the corresponding attachment from the old skin is currently attached. + internal void AttachAll (Skeleton skeleton, Skin oldSkin) { + Slot[] slots = skeleton.slots.Items; + foreach (var item in oldSkin.attachments) { + SkinEntry entry = item.Value; + int slotIndex = entry.slotIndex; + Slot slot = slots[slotIndex]; + if (slot.Attachment == entry.attachment) { + Attachment attachment = GetAttachment(slotIndex, entry.name); + if (attachment != null) slot.Attachment = attachment; + } + } + } + + /// Stores an entry in the skin consisting of the slot index, name, and attachment. + public struct SkinEntry { + internal readonly int slotIndex; + internal readonly string name; + internal readonly Attachment attachment; + + public SkinEntry (int slotIndex, string name, Attachment attachment) { + this.slotIndex = slotIndex; + this.name = name; + this.attachment = attachment; + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + /// The name the attachment is associated with, equivalent to the skin placeholder name in the Spine editor. + public String Name { + get { + return name; + } + } + + public Attachment Attachment { + get { + return attachment; + } + } + } + + private struct SkinKey { + internal readonly int slotIndex; + internal readonly string name; + internal readonly int hashCode; + + public SkinKey (int slotIndex, string name) { + if (slotIndex < 0) throw new ArgumentException("slotIndex must be >= 0."); + if (name == null) throw new ArgumentNullException("name", "name cannot be null"); + this.slotIndex = slotIndex; + this.name = name; + this.hashCode = name.GetHashCode() + slotIndex * 37; + } + } + + class SkinKeyComparer : IEqualityComparer { + internal static readonly SkinKeyComparer Instance = new SkinKeyComparer(); + + bool IEqualityComparer.Equals (SkinKey e1, SkinKey e2) { + return e1.slotIndex == e2.slotIndex && string.Equals(e1.name, e2.name, StringComparison.Ordinal); + } + + int IEqualityComparer.GetHashCode (SkinKey e) { + return e.hashCode; + } + } + } +} + diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Slot.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Slot.cs new file mode 100644 index 0000000..81392e6 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Slot.cs @@ -0,0 +1,200 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + + /// + /// Stores a slot's current pose. Slots organize attachments for purposes and provide a place to store + /// state for an attachment.State cannot be stored in an attachment itself because attachments are stateless and may be shared + /// across multiple skeletons. + /// + public class Slot { + internal SlotData data; + internal Bone bone; + internal float r, g, b, a; + internal float r2, g2, b2; + internal bool hasSecondColor; + internal Attachment attachment; + internal float attachmentTime; + internal ExposedList deform = new ExposedList(); + internal int attachmentState; + + public Slot (SlotData data, Bone bone) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + this.data = data; + this.bone = bone; + + // darkColor = data.darkColor == null ? null : new Color(); + if (data.hasSecondColor) { + r2 = g2 = b2 = 0; + } + + SetToSetupPose(); + } + + /// Copy constructor. + public Slot (Slot slot, Bone bone) { + if (slot == null) throw new ArgumentNullException("slot", "slot cannot be null."); + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + data = slot.data; + this.bone = bone; + r = slot.r; + g = slot.g; + b = slot.b; + a = slot.a; + + // darkColor = slot.darkColor == null ? null : new Color(slot.darkColor); + if (slot.hasSecondColor) { + r2 = slot.r2; + g2 = slot.g2; + b2 = slot.b2; + } else { + r2 = g2 = b2 = 0; + } + hasSecondColor = slot.hasSecondColor; + + attachment = slot.attachment; + attachmentTime = slot.attachmentTime; + deform.AddRange(slot.deform); + } + + /// The slot's setup pose data. + public SlotData Data { get { return data; } } + /// The bone this slot belongs to. + public Bone Bone { get { return bone; } } + /// The skeleton this slot belongs to. + public Skeleton Skeleton { get { return bone.skeleton; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float R { get { return r; } set { r = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float G { get { return g; } set { g = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float B { get { return b; } set { b = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float A { get { return a; } set { a = value; } } + + public void ClampColor () { + r = MathUtils.Clamp(r, 0, 1); + g = MathUtils.Clamp(g, 0, 1); + b = MathUtils.Clamp(b, 0, 1); + a = MathUtils.Clamp(a, 0, 1); + } + + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float R2 { get { return r2; } set { r2 = value; } } + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float G2 { get { return g2; } set { g2 = value; } } + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float B2 { get { return b2; } set { b2 = value; } } + /// Whether R2 G2 B2 are used to tint the slot's attachment for two color tinting. False if two color tinting is not used. + public bool HasSecondColor { get { return data.hasSecondColor; } set { data.hasSecondColor = value; } } + + public void ClampSecondColor () { + r2 = MathUtils.Clamp(r2, 0, 1); + g2 = MathUtils.Clamp(g2, 0, 1); + b2 = MathUtils.Clamp(b2, 0, 1); + } + + public Attachment Attachment { + /// The current attachment for the slot, or null if the slot has no attachment. + get { return attachment; } + /// + /// Sets the slot's attachment and, if the attachment changed, resets and clears the . + /// The deform is not cleared if the old attachment has the same as the specified + /// attachment. + /// May be null. + set { + if (attachment == value) return; + if (!(value is VertexAttachment) || !(this.attachment is VertexAttachment) + || ((VertexAttachment)value).DeformAttachment != ((VertexAttachment)this.attachment).DeformAttachment) { + deform.Clear(); + } + this.attachment = value; + attachmentTime = bone.skeleton.time; + } + } + + /// The time that has elapsed since the last time the attachment was set or cleared. Relies on Skeleton + /// + public float AttachmentTime { + get { return bone.skeleton.time - attachmentTime; } + set { attachmentTime = bone.skeleton.time - value; } + } + + /// Vertices to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a + /// weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions. + /// + /// See and . + public ExposedList Deform { + get { + return deform; + } + set { + if (deform == null) throw new ArgumentNullException("deform", "deform cannot be null."); + deform = value; + } + } + + /// Sets this slot to the setup pose. + public void SetToSetupPose () { + r = data.r; + g = data.g; + b = data.b; + a = data.a; + + // if (darkColor != null) darkColor.set(data.darkColor); + if (HasSecondColor) { + r2 = data.r2; + g2 = data.g2; + b2 = data.b2; + } + + if (data.attachmentName == null) + Attachment = null; + else { + attachment = null; + Attachment = bone.skeleton.GetAttachment(data.index, data.attachmentName); + } + } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SlotData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SlotData.cs new file mode 100644 index 0000000..309e78d --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/SlotData.cs @@ -0,0 +1,77 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + public class SlotData { + internal int index; + internal string name; + internal BoneData boneData; + internal float r = 1, g = 1, b = 1, a = 1; + internal float r2 = 0, g2 = 0, b2 = 0; + internal bool hasSecondColor = false; + internal string attachmentName; + internal BlendMode blendMode; + + /// The index of the slot in . + public int Index { get { return index; } } + /// The name of the slot, which is unique across all slots in the skeleton. + public string Name { get { return name; } } + /// The bone this slot belongs to. + public BoneData BoneData { get { return boneData; } } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public float R2 { get { return r2; } set { r2 = value; } } + public float G2 { get { return g2; } set { g2 = value; } } + public float B2 { get { return b2; } set { b2 = value; } } + public bool HasSecondColor { get { return hasSecondColor; } set { hasSecondColor = value; } } + + /// The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. + public String AttachmentName { get { return attachmentName; } set { attachmentName = value; } } + /// The blend mode for drawing the slot's attachment. + public BlendMode BlendMode { get { return blendMode; } set { blendMode = value; } } + + public SlotData (int index, String name, BoneData boneData) { + if (index < 0) throw new ArgumentException("index must be >= 0.", "index"); + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + if (boneData == null) throw new ArgumentNullException("boneData", "boneData cannot be null."); + this.index = index; + this.name = name; + this.boneData = boneData; + } + + override public string ToString () { + return name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/TransformConstraint.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/TransformConstraint.cs new file mode 100644 index 0000000..6de599a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/TransformConstraint.cs @@ -0,0 +1,311 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + /// + /// + /// Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained + /// bones to match that of the target bone. + /// + /// See Transform constraints in the Spine User Guide. + /// + public class TransformConstraint : IUpdatable { + internal TransformConstraintData data; + internal ExposedList bones; + internal Bone target; + internal float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY; + + internal bool active; + + public TransformConstraint (TransformConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + mixRotate = data.mixRotate; + mixX = data.mixX; + mixY = data.mixY; + mixScaleX = data.mixScaleX; + mixScaleY = data.mixScaleY; + mixShearY = data.mixShearY; + bones = new ExposedList(); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.FindBone(boneData.name)); + + target = skeleton.FindBone(data.target.name); + } + + /// Copy constructor. + public TransformConstraint (TransformConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.Bones.Count); + foreach (Bone bone in constraint.Bones) + bones.Add(skeleton.Bones.Items[bone.data.index]); + target = skeleton.Bones.Items[constraint.target.data.index]; + mixRotate = constraint.mixRotate; + mixX = constraint.mixX; + mixY = constraint.mixY; + mixScaleX = constraint.mixScaleX; + mixScaleY = constraint.mixScaleY; + mixShearY = constraint.mixShearY; + } + + public void Update () { + if (mixRotate == 0 && mixX == 0 && mixY == 0 && mixScaleX == 0 && mixScaleX == 0 && mixShearY == 0) return; + if (data.local) { + if (data.relative) + ApplyRelativeLocal(); + else + ApplyAbsoluteLocal(); + } else { + if (data.relative) + ApplyRelativeWorld(); + else + ApplyAbsoluteWorld(); + } + } + + void ApplyAbsoluteWorld () { + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, + mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; + bool translate = mixX != 0 || mixY != 0; + + Bone target = this.target; + float ta = target.a, tb = target.b, tc = target.c, td = target.d; + float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect; + + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + + if (mixRotate != 0) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float r = MathUtils.Atan2(tc, ta) - MathUtils.Atan2(c, a) + offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r *= mixRotate; + float cos = MathUtils.Cos(r), sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + + if (translate) { + float tx, ty; //Vector2 temp = this.temp; + target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY)); + bone.worldX += (tx - bone.worldX) * mixX; + bone.worldY += (ty - bone.worldY) * mixY; + } + + if (mixScaleX != 0) { + float s = (float)Math.Sqrt(bone.a * bone.a + bone.c * bone.c); + if (s != 0) s = (s + ((float)Math.Sqrt(ta * ta + tc * tc) - s + data.offsetScaleX) * mixScaleX) / s; + bone.a *= s; + bone.c *= s; + } + if (mixScaleY != 0) { + float s = (float)Math.Sqrt(bone.b * bone.b + bone.d * bone.d); + if (s != 0) s = (s + ((float)Math.Sqrt(tb * tb + td * td) - s + data.offsetScaleY) * mixScaleY) / s; + bone.b *= s; + bone.d *= s; + } + + if (mixShearY > 0) { + float b = bone.b, d = bone.d; + float by = MathUtils.Atan2(d, b); + float r = MathUtils.Atan2(td, tb) - MathUtils.Atan2(tc, ta) - (by - MathUtils.Atan2(bone.c, bone.a)); + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r = by + (r + offsetShearY) * mixShearY; + float s = (float)Math.Sqrt(b * b + d * d); + bone.b = MathUtils.Cos(r) * s; + bone.d = MathUtils.Sin(r) * s; + } + + bone.UpdateAppliedTransform(); + } + } + + void ApplyRelativeWorld () { + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, + mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; + bool translate = mixX != 0 || mixY != 0; + + Bone target = this.target; + float ta = target.a, tb = target.b, tc = target.c, td = target.d; + float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect; + + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + + if (mixRotate != 0) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float r = MathUtils.Atan2(tc, ta) + offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r *= mixRotate; + float cos = MathUtils.Cos(r), sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + + if (translate) { + float tx, ty; //Vector2 temp = this.temp; + target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY)); + bone.worldX += tx * mixX; + bone.worldY += ty * mixY; + } + + if (mixScaleX != 0) { + float s = ((float)Math.Sqrt(ta * ta + tc * tc) - 1 + data.offsetScaleX) * mixScaleX + 1; + bone.a *= s; + bone.c *= s; + } + if (mixScaleY != 0) { + float s = ((float)Math.Sqrt(tb * tb + td * td) - 1 + data.offsetScaleY) * mixScaleY + 1; + bone.b *= s; + bone.d *= s; + } + + if (mixShearY > 0) { + float r = MathUtils.Atan2(td, tb) - MathUtils.Atan2(tc, ta); + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + float b = bone.b, d = bone.d; + r = MathUtils.Atan2(d, b) + (r - MathUtils.PI / 2 + offsetShearY) * mixShearY; + float s = (float)Math.Sqrt(b * b + d * d); + bone.b = MathUtils.Cos(r) * s; + bone.d = MathUtils.Sin(r) * s; + } + + bone.UpdateAppliedTransform(); + } + } + + void ApplyAbsoluteLocal () { + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, + mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; + + Bone target = this.target; + + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + + float rotation = bone.arotation; + if (mixRotate != 0) { + float r = target.arotation - rotation + data.offsetRotation; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + rotation += r * mixRotate; + } + + float x = bone.ax, y = bone.ay; + x += (target.ax - x + data.offsetX) * mixX; + y += (target.ay - y + data.offsetY) * mixY; + + float scaleX = bone.ascaleX, scaleY = bone.ascaleY; + if (mixScaleX != 0 && scaleX != 0) + scaleX = (scaleX + (target.ascaleX - scaleX + data.offsetScaleX) * mixScaleX) / scaleX; + if (mixScaleY != 0 && scaleY != 0) + scaleY = (scaleY + (target.ascaleY - scaleY + data.offsetScaleY) * mixScaleY) / scaleY; + + float shearY = bone.ashearY; + if (mixShearY != 0) { + float r = target.ashearY - shearY + data.offsetShearY; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + shearY += r * mixShearY; + } + + bone.UpdateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } + + void ApplyRelativeLocal () { + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, + mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; + + Bone target = this.target; + + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + + float rotation = bone.arotation + (target.arotation + data.offsetRotation) * mixRotate; + float x = bone.ax + (target.ax + data.offsetX) * mixX; + float y = bone.ay + (target.ay + data.offsetY) * mixY; + float scaleX = bone.ascaleX * (((target.ascaleX - 1 + data.offsetScaleX) * mixScaleX) + 1); + float scaleY = bone.ascaleY * (((target.ascaleY - 1 + data.offsetScaleY) * mixScaleY) + 1); + float shearY = bone.ashearY + (target.ashearY + data.offsetShearY) * mixShearY; + + bone.UpdateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } + + /// The bones that will be modified by this transform constraint. + public ExposedList Bones { get { return bones; } } + /// The target bone whose world transform will be copied to the constrained bones. + public Bone Target { get { return target; } set { target = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + public float MixRotate { get { return mixRotate; } set { mixRotate = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. + public float MixX { get { return mixX; } set { mixX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. + public float MixY { get { return mixY; } set { mixY = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. + public float MixScaleX { get { return mixScaleX; } set { mixScaleX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. + public float MixScaleY { get { return mixScaleY; } set { mixScaleY = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. + public float MixShearY { get { return mixShearY; } set { mixShearY = value; } } + public bool Active { get { return active; } } + /// The transform constraint's setup pose data. + public TransformConstraintData Data { get { return data; } } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/TransformConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/TransformConstraintData.cs new file mode 100644 index 0000000..6565f5a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/TransformConstraintData.cs @@ -0,0 +1,68 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + public class TransformConstraintData : ConstraintData { + internal ExposedList bones = new ExposedList(); + internal BoneData target; + internal float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY; + internal float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY; + internal bool relative, local; + + public ExposedList Bones { get { return bones; } } + public BoneData Target { get { return target; } set { target = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + public float MixRotate { get { return mixRotate; } set { mixRotate = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. + public float MixX { get { return mixX; } set { mixX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. + public float MixY { get { return mixY; } set { mixY = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. + public float MixScaleX { get { return mixScaleX; } set { mixScaleX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. + public float MixScaleY { get { return mixScaleY; } set { mixScaleY = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. + public float MixShearY { get { return mixShearY; } set { mixShearY = value; } } + + public float OffsetRotation { get { return offsetRotation; } set { offsetRotation = value; } } + public float OffsetX { get { return offsetX; } set { offsetX = value; } } + public float OffsetY { get { return offsetY; } set { offsetY = value; } } + public float OffsetScaleX { get { return offsetScaleX; } set { offsetScaleX = value; } } + public float OffsetScaleY { get { return offsetScaleY; } set { offsetScaleY = value; } } + public float OffsetShearY { get { return offsetShearY; } set { offsetShearY = value; } } + + public bool Relative { get { return relative; } set { relative = value; } } + public bool Local { get { return local; } set { local = value; } } + + public TransformConstraintData (string name) : base(name) { + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Triangulator.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Triangulator.cs new file mode 100644 index 0000000..2200e91 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/Triangulator.cs @@ -0,0 +1,275 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_0_64 { + public class Triangulator { + private readonly ExposedList> convexPolygons = new ExposedList>(); + private readonly ExposedList> convexPolygonsIndices = new ExposedList>(); + + private readonly ExposedList indicesArray = new ExposedList(); + private readonly ExposedList isConcaveArray = new ExposedList(); + private readonly ExposedList triangles = new ExposedList(); + + private readonly Pool> polygonPool = new Pool>(); + private readonly Pool> polygonIndicesPool = new Pool>(); + + public ExposedList Triangulate (ExposedList verticesArray) { + var vertices = verticesArray.Items; + int vertexCount = verticesArray.Count >> 1; + + var indicesArray = this.indicesArray; + indicesArray.Clear(); + int[] indices = indicesArray.Resize(vertexCount).Items; + for (int i = 0; i < vertexCount; i++) + indices[i] = i; + + var isConcaveArray = this.isConcaveArray; + bool[] isConcave = isConcaveArray.Resize(vertexCount).Items; + for (int i = 0, n = vertexCount; i < n; ++i) + isConcave[i] = IsConcave(i, vertexCount, vertices, indices); + + var triangles = this.triangles; + triangles.Clear(); + triangles.EnsureCapacity(Math.Max(0, vertexCount - 2) << 2); + + while (vertexCount > 3) { + // Find ear tip. + int previous = vertexCount - 1, i = 0, next = 1; + + // outer: + while (true) { + if (!isConcave[i]) { + int p1 = indices[previous] << 1, p2 = indices[i] << 1, p3 = indices[next] << 1; + float p1x = vertices[p1], p1y = vertices[p1 + 1]; + float p2x = vertices[p2], p2y = vertices[p2 + 1]; + float p3x = vertices[p3], p3y = vertices[p3 + 1]; + for (int ii = (next + 1) % vertexCount; ii != previous; ii = (ii + 1) % vertexCount) { + if (!isConcave[ii]) continue; + int v = indices[ii] << 1; + float vx = vertices[v], vy = vertices[v + 1]; + if (PositiveArea(p3x, p3y, p1x, p1y, vx, vy)) { + if (PositiveArea(p1x, p1y, p2x, p2y, vx, vy)) { + if (PositiveArea(p2x, p2y, p3x, p3y, vx, vy)) goto break_outer; // break outer; + } + } + } + break; + } + break_outer: + + if (next == 0) { + do { + if (!isConcave[i]) break; + i--; + } while (i > 0); + break; + } + + previous = i; + i = next; + next = (next + 1) % vertexCount; + } + + // Cut ear tip. + triangles.Add(indices[(vertexCount + i - 1) % vertexCount]); + triangles.Add(indices[i]); + triangles.Add(indices[(i + 1) % vertexCount]); + indicesArray.RemoveAt(i); + isConcaveArray.RemoveAt(i); + vertexCount--; + + int previousIndex = (vertexCount + i - 1) % vertexCount; + int nextIndex = i == vertexCount ? 0 : i; + isConcave[previousIndex] = IsConcave(previousIndex, vertexCount, vertices, indices); + isConcave[nextIndex] = IsConcave(nextIndex, vertexCount, vertices, indices); + } + + if (vertexCount == 3) { + triangles.Add(indices[2]); + triangles.Add(indices[0]); + triangles.Add(indices[1]); + } + + return triangles; + } + + public ExposedList> Decompose (ExposedList verticesArray, ExposedList triangles) { + var vertices = verticesArray.Items; + var convexPolygons = this.convexPolygons; + for (int i = 0, n = convexPolygons.Count; i < n; i++) + polygonPool.Free(convexPolygons.Items[i]); + convexPolygons.Clear(); + + var convexPolygonsIndices = this.convexPolygonsIndices; + for (int i = 0, n = convexPolygonsIndices.Count; i < n; i++) + polygonIndicesPool.Free(convexPolygonsIndices.Items[i]); + convexPolygonsIndices.Clear(); + + var polygonIndices = polygonIndicesPool.Obtain(); + polygonIndices.Clear(); + + var polygon = polygonPool.Obtain(); + polygon.Clear(); + + // Merge subsequent triangles if they form a triangle fan. + int fanBaseIndex = -1, lastWinding = 0; + int[] trianglesItems = triangles.Items; + for (int i = 0, n = triangles.Count; i < n; i += 3) { + int t1 = trianglesItems[i] << 1, t2 = trianglesItems[i + 1] << 1, t3 = trianglesItems[i + 2] << 1; + float x1 = vertices[t1], y1 = vertices[t1 + 1]; + float x2 = vertices[t2], y2 = vertices[t2 + 1]; + float x3 = vertices[t3], y3 = vertices[t3 + 1]; + + // If the base of the last triangle is the same as this triangle, check if they form a convex polygon (triangle fan). + var merged = false; + if (fanBaseIndex == t1) { + int o = polygon.Count - 4; + float[] p = polygon.Items; + int winding1 = Winding(p[o], p[o + 1], p[o + 2], p[o + 3], x3, y3); + int winding2 = Winding(x3, y3, p[0], p[1], p[2], p[3]); + if (winding1 == lastWinding && winding2 == lastWinding) { + polygon.Add(x3); + polygon.Add(y3); + polygonIndices.Add(t3); + merged = true; + } + } + + // Otherwise make this triangle the new base. + if (!merged) { + if (polygon.Count > 0) { + convexPolygons.Add(polygon); + convexPolygonsIndices.Add(polygonIndices); + } else { + polygonPool.Free(polygon); + polygonIndicesPool.Free(polygonIndices); + } + polygon = polygonPool.Obtain(); + polygon.Clear(); + polygon.Add(x1); + polygon.Add(y1); + polygon.Add(x2); + polygon.Add(y2); + polygon.Add(x3); + polygon.Add(y3); + polygonIndices = polygonIndicesPool.Obtain(); + polygonIndices.Clear(); + polygonIndices.Add(t1); + polygonIndices.Add(t2); + polygonIndices.Add(t3); + lastWinding = Winding(x1, y1, x2, y2, x3, y3); + fanBaseIndex = t1; + } + } + + if (polygon.Count > 0) { + convexPolygons.Add(polygon); + convexPolygonsIndices.Add(polygonIndices); + } + + // Go through the list of polygons and try to merge the remaining triangles with the found triangle fans. + for (int i = 0, n = convexPolygons.Count; i < n; i++) { + polygonIndices = convexPolygonsIndices.Items[i]; + if (polygonIndices.Count == 0) continue; + int firstIndex = polygonIndices.Items[0]; + int lastIndex = polygonIndices.Items[polygonIndices.Count - 1]; + + polygon = convexPolygons.Items[i]; + int o = polygon.Count - 4; + float[] p = polygon.Items; + float prevPrevX = p[o], prevPrevY = p[o + 1]; + float prevX = p[o + 2], prevY = p[o + 3]; + float firstX = p[0], firstY = p[1]; + float secondX = p[2], secondY = p[3]; + int winding = Winding(prevPrevX, prevPrevY, prevX, prevY, firstX, firstY); + + for (int ii = 0; ii < n; ii++) { + if (ii == i) continue; + var otherIndices = convexPolygonsIndices.Items[ii]; + if (otherIndices.Count != 3) continue; + int otherFirstIndex = otherIndices.Items[0]; + int otherSecondIndex = otherIndices.Items[1]; + int otherLastIndex = otherIndices.Items[2]; + + var otherPoly = convexPolygons.Items[ii]; + float x3 = otherPoly.Items[otherPoly.Count - 2], y3 = otherPoly.Items[otherPoly.Count - 1]; + + if (otherFirstIndex != firstIndex || otherSecondIndex != lastIndex) continue; + int winding1 = Winding(prevPrevX, prevPrevY, prevX, prevY, x3, y3); + int winding2 = Winding(x3, y3, firstX, firstY, secondX, secondY); + if (winding1 == winding && winding2 == winding) { + otherPoly.Clear(); + otherIndices.Clear(); + polygon.Add(x3); + polygon.Add(y3); + polygonIndices.Add(otherLastIndex); + prevPrevX = prevX; + prevPrevY = prevY; + prevX = x3; + prevY = y3; + ii = 0; + } + } + } + + // Remove empty polygons that resulted from the merge step above. + for (int i = convexPolygons.Count - 1; i >= 0; i--) { + polygon = convexPolygons.Items[i]; + if (polygon.Count == 0) { + convexPolygons.RemoveAt(i); + polygonPool.Free(polygon); + polygonIndices = convexPolygonsIndices.Items[i]; + convexPolygonsIndices.RemoveAt(i); + polygonIndicesPool.Free(polygonIndices); + } + } + + return convexPolygons; + } + + static private bool IsConcave (int index, int vertexCount, float[] vertices, int[] indices) { + int previous = indices[(vertexCount + index - 1) % vertexCount] << 1; + int current = indices[index] << 1; + int next = indices[(index + 1) % vertexCount] << 1; + return !PositiveArea(vertices[previous], vertices[previous + 1], vertices[current], vertices[current + 1], vertices[next], + vertices[next + 1]); + } + + static private bool PositiveArea (float p1x, float p1y, float p2x, float p2y, float p3x, float p3y) { + return p1x * (p3y - p2y) + p2x * (p1y - p3y) + p3x * (p2y - p1y) >= 0; + } + + static private int Winding (float p1x, float p1y, float p2x, float p2y, float p3x, float p3y) { + float px = p2x - p1x, py = p2y - p1y; + return p3x * py - p3y * px + px * p1y - p1x * py >= 0 ? 1 : -1; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/MeshBatcher.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/MeshBatcher.cs new file mode 100644 index 0000000..398c3f3 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/MeshBatcher.cs @@ -0,0 +1,195 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; + +namespace Spine4_0_64 { + public struct VertexPositionColorTextureColor : IVertexType { + public Vector3 Position; + public Color Color; + public Vector2 TextureCoordinate; + public Color Color2; + + public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration + ( + new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), + new VertexElement(12, VertexElementFormat.Color, VertexElementUsage.Color, 0), + new VertexElement(16, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + new VertexElement(24, VertexElementFormat.Color, VertexElementUsage.Color, 1) + ); + + VertexDeclaration IVertexType.VertexDeclaration { + get { return VertexDeclaration; } + } + } + + // #region License + // /* + // Microsoft Public License (Ms-PL) + // MonoGame - Copyright � 2009 The MonoGame Team + // + // All rights reserved. + // + // This license governs use of the accompanying software. If you use the software, you accept this license. If you do not + // accept the license, do not use the software. + // + // 1. Definitions + // The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under + // U.S. copyright law. + // + // A "contribution" is the original software, or any additions or changes to the software. + // A "contributor" is any person that distributes its contribution under this license. + // "Licensed patents" are a contributor's patent claims that read directly on its contribution. + // + // 2. Grant of Rights + // (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, + // each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + // (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, + // each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + // + // 3. Conditions and Limitations + // (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + // (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, + // your patent license from such contributor to the software ends automatically. + // (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution + // notices that are present in the software. + // (D) If you distribute any portion of the software in source code form, you may do so only under this license by including + // a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object + // code form, you may only do so under a license that complies with this license. + // (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees + // or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent + // permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular + // purpose and non-infringement. + // */ + // #endregion License + // + + /// Draws batched meshes. + public class MeshBatcher { + private readonly List items; + private readonly Queue freeItems; + private VertexPositionColorTextureColor[] vertexArray = { }; + private short[] triangles = { }; + + public MeshBatcher () { + items = new List(256); + freeItems = new Queue(256); + EnsureCapacity(256, 512); + } + + /// Returns a pooled MeshItem. + public MeshItem NextItem (int vertexCount, int triangleCount) { + MeshItem item = freeItems.Count > 0 ? freeItems.Dequeue() : new MeshItem(); + if (item.vertices.Length < vertexCount) item.vertices = new VertexPositionColorTextureColor[vertexCount]; + if (item.triangles.Length < triangleCount) item.triangles = new int[triangleCount]; + item.vertexCount = vertexCount; + item.triangleCount = triangleCount; + items.Add(item); + return item; + } + + private void EnsureCapacity (int vertexCount, int triangleCount) { + if (vertexArray.Length < vertexCount) vertexArray = new VertexPositionColorTextureColor[vertexCount]; + if (triangles.Length < triangleCount) triangles = new short[triangleCount]; + } + + public void Draw (GraphicsDevice device) { + if (items.Count == 0) return; + + int itemCount = items.Count; + int vertexCount = 0, triangleCount = 0; + for (int i = 0; i < itemCount; i++) { + MeshItem item = items[i]; + vertexCount += item.vertexCount; + triangleCount += item.triangleCount; + } + EnsureCapacity(vertexCount, triangleCount); + + vertexCount = 0; + triangleCount = 0; + Texture2D lastTexture = null; + for (int i = 0; i < itemCount; i++) { + MeshItem item = items[i]; + int itemVertexCount = item.vertexCount; + + if (item.texture != lastTexture || vertexCount + itemVertexCount > short.MaxValue) { + FlushVertexArray(device, vertexCount, triangleCount); + vertexCount = 0; + triangleCount = 0; + lastTexture = item.texture; + device.Textures[0] = lastTexture; + if (item.textureLayers != null) { + for (int layer = 1; layer < item.textureLayers.Length; ++layer) + device.Textures[layer] = item.textureLayers[layer]; + } + } + + int[] itemTriangles = item.triangles; + int itemTriangleCount = item.triangleCount; + for (int ii = 0, t = triangleCount; ii < itemTriangleCount; ii++, t++) + triangles[t] = (short)(itemTriangles[ii] + vertexCount); + triangleCount += itemTriangleCount; + + Array.Copy(item.vertices, 0, vertexArray, vertexCount, itemVertexCount); + vertexCount += itemVertexCount; + } + FlushVertexArray(device, vertexCount, triangleCount); + } + + public void AfterLastDrawPass () { + int itemCount = items.Count; + for (int i = 0; i < itemCount; i++) { + var item = items[i]; + item.texture = null; + freeItems.Enqueue(item); + } + items.Clear(); + } + + private void FlushVertexArray (GraphicsDevice device, int vertexCount, int triangleCount) { + if (vertexCount == 0) return; + device.DrawUserIndexedPrimitives( + PrimitiveType.TriangleList, + vertexArray, 0, vertexCount, + triangles, 0, triangleCount / 3, + VertexPositionColorTextureColor.VertexDeclaration); + } + } + + public class MeshItem { + public Texture2D texture = null; + public Texture2D[] textureLayers = null; + public int vertexCount, triangleCount; + public VertexPositionColorTextureColor[] vertices = { }; + public int[] triangles = { }; + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/ShapeRenderer.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/ShapeRenderer.cs new file mode 100644 index 0000000..6700687 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/ShapeRenderer.cs @@ -0,0 +1,165 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Spine4_0_64 { + /// + /// Batch drawing of lines and shapes that can be derived from lines. + /// + /// Call drawing methods in between Begin()/End() + /// + public class ShapeRenderer { + GraphicsDevice device; + List vertices = new List(); + Color color = Color.White; + BasicEffect effect; + public BasicEffect Effect { get { return effect; } set { effect = value; } } + + public ShapeRenderer (GraphicsDevice device) { + this.device = device; + this.effect = new BasicEffect(device); + effect.World = Matrix.Identity; + effect.View = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 1.0f), Vector3.Zero, Vector3.Up); + effect.TextureEnabled = false; + effect.VertexColorEnabled = true; + } + + public void SetColor (Color color) { + this.color = color; + } + + public void Begin () { + device.RasterizerState = new RasterizerState(); + device.BlendState = BlendState.AlphaBlend; + } + + public void Line (float x1, float y1, float x2, float y2) { + vertices.Add(new VertexPositionColor(new Vector3(x1, y1, 0), color)); + vertices.Add(new VertexPositionColor(new Vector3(x2, y2, 0), color)); + } + + /** Calls {@link #circle(float, float, float, int)} by estimating the number of segments needed for a smooth circle. */ + public void Circle (float x, float y, float radius) { + Circle(x, y, radius, Math.Max(1, (int)(6 * (float)Math.Pow(radius, 1.0f / 3.0f)))); + } + + /** Draws a circle using {@link ShapeType#Line} or {@link ShapeType#Filled}. */ + public void Circle (float x, float y, float radius, int segments) { + if (segments <= 0) throw new ArgumentException("segments must be > 0."); + float angle = 2 * MathUtils.PI / segments; + float cos = MathUtils.Cos(angle); + float sin = MathUtils.Sin(angle); + float cx = radius, cy = 0; + float temp = 0; + + for (int i = 0; i < segments; i++) { + vertices.Add(new VertexPositionColor(new Vector3(x + cx, y + cy, 0), color)); + temp = cx; + cx = cos * cx - sin * cy; + cy = sin * temp + cos * cy; + vertices.Add(new VertexPositionColor(new Vector3(x + cx, y + cy, 0), color)); + } + vertices.Add(new VertexPositionColor(new Vector3(x + cx, y + cy, 0), color)); + + temp = cx; + cx = radius; + cy = 0; + vertices.Add(new VertexPositionColor(new Vector3(x + cx, y + cy, 0), color)); + } + + public void Triangle (float x1, float y1, float x2, float y2, float x3, float y3) { + vertices.Add(new VertexPositionColor(new Vector3(x1, y1, 0), color)); + vertices.Add(new VertexPositionColor(new Vector3(x2, y2, 0), color)); + + vertices.Add(new VertexPositionColor(new Vector3(x2, y2, 0), color)); + vertices.Add(new VertexPositionColor(new Vector3(x3, y3, 0), color)); + + vertices.Add(new VertexPositionColor(new Vector3(x3, y3, 0), color)); + vertices.Add(new VertexPositionColor(new Vector3(x1, y1, 0), color)); + } + + public void X (float x, float y, float len) { + Line(x + len, y + len, x - len, y - len); + Line(x - len, y + len, x + len, y - len); + } + + public void Polygon (float[] polygonVertices, int offset, int count) { + if (count < 3) throw new ArgumentException("Polygon must contain at least 3 vertices"); + + offset <<= 1; + + var firstX = polygonVertices[offset]; + var firstY = polygonVertices[offset + 1]; + var last = offset + count; + + for (int i = offset, n = offset + count; i < n; i += 2) { + var x1 = polygonVertices[i]; + var y1 = polygonVertices[i + 1]; + + var x2 = 0f; + var y2 = 0f; + + if (i + 2 >= last) { + x2 = firstX; + y2 = firstY; + } else { + x2 = polygonVertices[i + 2]; + y2 = polygonVertices[i + 3]; + } + + Line(x1, y1, x2, y2); + } + } + + public void Rect (float x, float y, float width, float height) { + Line(x, y, x + width, y); + Line(x + width, y, x + width, y + height); + Line(x + width, y + height, x, y + height); + Line(x, y + height, x, y); + } + + public void End () { + if (vertices.Count == 0) return; + var verticesArray = vertices.ToArray(); + + foreach (EffectPass pass in effect.CurrentTechnique.Passes) { + pass.Apply(); + device.DrawUserPrimitives(PrimitiveType.LineList, verticesArray, 0, verticesArray.Length / 2); + } + + vertices.Clear(); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/SkeletonRenderer.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/SkeletonRenderer.cs new file mode 100644 index 0000000..04f2d05 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/SkeletonRenderer.cs @@ -0,0 +1,276 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; + +namespace Spine4_0_64 { + /// Draws region and mesh attachments. + public class SkeletonRenderer { + private const int TL = 0; + private const int TR = 1; + private const int BL = 2; + private const int BR = 3; + + SkeletonClipping clipper = new SkeletonClipping(); + GraphicsDevice device; + MeshBatcher batcher; + public MeshBatcher Batcher { get { return batcher; } } + RasterizerState rasterizerState; + float[] vertices = new float[8]; + int[] quadTriangles = { 0, 1, 2, 2, 3, 0 }; + BlendState defaultBlendState; + + Effect effect; + public Effect Effect { get { return effect; } set { effect = value; } } + public IVertexEffect VertexEffect { get; set; } + + private bool premultipliedAlpha; + public bool PremultipliedAlpha { get { return premultipliedAlpha; } set { premultipliedAlpha = value; } } + + /// Attachments are rendered back to front in the x/y plane by the SkeletonRenderer. + /// Each attachment is offset by a customizable z-spacing value on the z-axis to avoid z-fighting + /// in shaders with ZWrite enabled. Typical values lie in the range [-0.1, 0]. + private float zSpacing = 0.0f; + public float ZSpacing { get { return zSpacing; } set { zSpacing = value; } } + + /// A Z position offset added at each vertex. + private float z = 0.0f; + public float Z { get { return z; } set { z = value; } } + + public SkeletonRenderer (GraphicsDevice device) { + this.device = device; + + batcher = new MeshBatcher(); + + var basicEffect = new BasicEffect(device); + basicEffect.World = Matrix.Identity; + basicEffect.View = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 1.0f), Vector3.Zero, Vector3.Up); + basicEffect.TextureEnabled = true; + basicEffect.VertexColorEnabled = true; + effect = basicEffect; + + rasterizerState = new RasterizerState(); + rasterizerState.CullMode = CullMode.None; + + Bone.yDown = true; + } + + public void Begin () { + defaultBlendState = premultipliedAlpha ? BlendState.AlphaBlend : BlendState.NonPremultiplied; + + device.RasterizerState = rasterizerState; + device.BlendState = defaultBlendState; + } + + public void End () { + foreach (EffectPass pass in effect.CurrentTechnique.Passes) { + pass.Apply(); + batcher.Draw(device); + } + batcher.AfterLastDrawPass(); + } + + public void Draw (Skeleton skeleton) { + var drawOrder = skeleton.DrawOrder; + var drawOrderItems = skeleton.DrawOrder.Items; + float skeletonR = skeleton.R, skeletonG = skeleton.G, skeletonB = skeleton.B, skeletonA = skeleton.A; + Color color = new Color(); + + if (VertexEffect != null) VertexEffect.Begin(skeleton); + + for (int i = 0, n = drawOrder.Count; i < n; i++) { + Slot slot = drawOrderItems[i]; + Attachment attachment = slot.Attachment; + float attachmentZOffset = z + zSpacing * i; + + float attachmentColorR, attachmentColorG, attachmentColorB, attachmentColorA; + object textureObject = null; + int verticesCount = 0; + float[] vertices = this.vertices; + int indicesCount = 0; + int[] indices = null; + float[] uvs = null; + + if (attachment is RegionAttachment) { + RegionAttachment regionAttachment = (RegionAttachment)attachment; + attachmentColorR = regionAttachment.R; attachmentColorG = regionAttachment.G; attachmentColorB = regionAttachment.B; attachmentColorA = regionAttachment.A; + AtlasRegion region = (AtlasRegion)regionAttachment.RendererObject; + textureObject = region.page.rendererObject; + verticesCount = 4; + regionAttachment.ComputeWorldVertices(slot.Bone, vertices, 0, 2); + indicesCount = 6; + indices = quadTriangles; + uvs = regionAttachment.UVs; + } else if (attachment is MeshAttachment) { + MeshAttachment mesh = (MeshAttachment)attachment; + attachmentColorR = mesh.R; attachmentColorG = mesh.G; attachmentColorB = mesh.B; attachmentColorA = mesh.A; + AtlasRegion region = (AtlasRegion)mesh.RendererObject; + textureObject = region.page.rendererObject; + int vertexCount = mesh.WorldVerticesLength; + if (vertices.Length < vertexCount) vertices = new float[vertexCount]; + verticesCount = vertexCount >> 1; + mesh.ComputeWorldVertices(slot, vertices); + indicesCount = mesh.Triangles.Length; + indices = mesh.Triangles; + uvs = mesh.UVs; + } else if (attachment is ClippingAttachment) { + ClippingAttachment clip = (ClippingAttachment)attachment; + clipper.ClipStart(slot, clip); + continue; + } else { + continue; + } + + // set blend state + BlendState blendState = new BlendState(); + Blend blendSrc; + Blend blendDst; + if (premultipliedAlpha) + { + blendState.AlphaBlendFunction = BlendState.AlphaBlend.AlphaBlendFunction; + blendState.BlendFactor = BlendState.AlphaBlend.BlendFactor; + blendState.ColorBlendFunction = BlendState.AlphaBlend.ColorBlendFunction; + blendState.ColorWriteChannels = BlendState.AlphaBlend.ColorWriteChannels; + blendState.ColorWriteChannels1 = BlendState.AlphaBlend.ColorWriteChannels1; + blendState.ColorWriteChannels2 = BlendState.AlphaBlend.ColorWriteChannels2; + blendState.ColorWriteChannels3 = BlendState.AlphaBlend.ColorWriteChannels3; + blendState.MultiSampleMask = BlendState.AlphaBlend.MultiSampleMask; + } + else + { + blendState.AlphaBlendFunction = BlendState.NonPremultiplied.AlphaBlendFunction; + blendState.BlendFactor = BlendState.NonPremultiplied.BlendFactor; + blendState.ColorBlendFunction = BlendState.NonPremultiplied.ColorBlendFunction; + blendState.ColorWriteChannels = BlendState.NonPremultiplied.ColorWriteChannels; + blendState.ColorWriteChannels1 = BlendState.NonPremultiplied.ColorWriteChannels1; + blendState.ColorWriteChannels2 = BlendState.NonPremultiplied.ColorWriteChannels2; + blendState.ColorWriteChannels3 = BlendState.NonPremultiplied.ColorWriteChannels3; + blendState.MultiSampleMask = BlendState.NonPremultiplied.MultiSampleMask; + } + switch (slot.Data.BlendMode) + { + case BlendMode.Additive: + blendState = BlendState.Additive; + break; + case BlendMode.Multiply: + blendSrc = BlendXna.GetXNABlend(BlendXna.GL_DST_COLOR); + blendDst = BlendXna.GetXNABlend(BlendXna.GL_ONE_MINUS_SRC_ALPHA); + blendState.ColorSourceBlend = blendSrc; + blendState.AlphaSourceBlend = blendSrc; + blendState.ColorDestinationBlend = blendDst; + blendState.AlphaDestinationBlend = blendDst; + break; + case BlendMode.Screen: + blendSrc = BlendXna.GetXNABlend(premultipliedAlpha ? BlendXna.GL_ONE : BlendXna.GL_SRC_ALPHA); + blendDst = BlendXna.GetXNABlend(BlendXna.GL_ONE_MINUS_SRC_COLOR); + blendState.ColorSourceBlend = blendSrc; + blendState.AlphaSourceBlend = blendSrc; + blendState.ColorDestinationBlend = blendDst; + blendState.AlphaDestinationBlend = blendDst; + break; + default: + blendState = defaultBlendState; + break; + } + + if (device.BlendState != blendState) { + End(); + device.BlendState = blendState; + } + + // calculate color + float a = skeletonA * slot.A * attachmentColorA; + if (premultipliedAlpha) { + color = new Color( + skeletonR * slot.R * attachmentColorR * a, + skeletonG * slot.G * attachmentColorG * a, + skeletonB * slot.B * attachmentColorB * a, a); + } else { + color = new Color( + skeletonR * slot.R * attachmentColorR, + skeletonG * slot.G * attachmentColorG, + skeletonB * slot.B * attachmentColorB, a); + } + + Color darkColor = new Color(); + if (slot.HasSecondColor) { + if (premultipliedAlpha) { + darkColor = new Color(slot.R2 * a, slot.G2 * a, slot.B2 * a); + } else { + darkColor = new Color(slot.R2 * a, slot.G2 * a, slot.B2 * a); + } + } + darkColor.A = premultipliedAlpha ? (byte)255 : (byte)0; + + // clip + if (clipper.IsClipping) { + clipper.ClipTriangles(vertices, verticesCount << 1, indices, indicesCount, uvs); + vertices = clipper.ClippedVertices.Items; + verticesCount = clipper.ClippedVertices.Count >> 1; + indices = clipper.ClippedTriangles.Items; + indicesCount = clipper.ClippedTriangles.Count; + uvs = clipper.ClippedUVs.Items; + } + + if (verticesCount == 0 || indicesCount == 0) + continue; + + // submit to batch + MeshItem item = batcher.NextItem(verticesCount, indicesCount); + if (textureObject is Texture2D) + item.texture = (Texture2D)textureObject; + else { + item.textureLayers = (Texture2D[])textureObject; + item.texture = item.textureLayers[0]; + } + for (int ii = 0, nn = indicesCount; ii < nn; ii++) { + item.triangles[ii] = indices[ii]; + } + VertexPositionColorTextureColor[] itemVertices = item.vertices; + for (int ii = 0, v = 0, nn = verticesCount << 1; v < nn; ii++, v += 2) { + itemVertices[ii].Color = color; + itemVertices[ii].Color2 = darkColor; + itemVertices[ii].Position.X = vertices[v]; + itemVertices[ii].Position.Y = vertices[v + 1]; + itemVertices[ii].Position.Z = attachmentZOffset; + itemVertices[ii].TextureCoordinate.X = uvs[v]; + itemVertices[ii].TextureCoordinate.Y = uvs[v + 1]; + if (VertexEffect != null) VertexEffect.Transform(ref itemVertices[ii]); + } + + clipper.ClipEnd(slot); + } + clipper.ClipEnd(); + if (VertexEffect != null) VertexEffect.End(); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/VertexEffect.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/VertexEffect.cs new file mode 100644 index 0000000..1dc6a3e --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/VertexEffect.cs @@ -0,0 +1,97 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Spine4_0_64 { + public interface IVertexEffect { + void Begin (Skeleton skeleton); + void Transform (ref VertexPositionColorTextureColor vertex); + void End (); + } + + public class JitterEffect : IVertexEffect { + public float JitterX { get; set; } + public float JitterY { get; set; } + + public JitterEffect (float jitterX, float jitterY) { + JitterX = jitterX; + JitterY = jitterY; + } + + public void Begin (Skeleton skeleton) { + } + + public void End () { + } + + public void Transform (ref VertexPositionColorTextureColor vertex) { + vertex.Position.X += MathUtils.RandomTriangle(-JitterX, JitterY); + vertex.Position.Y += MathUtils.RandomTriangle(-JitterX, JitterY); + } + } + + public class SwirlEffect : IVertexEffect { + private float worldX, worldY, angle; + + public float Radius { get; set; } + public float Angle { get { return angle; } set { angle = value * MathUtils.DegRad; } } + public float CenterX { get; set; } + public float CenterY { get; set; } + public IInterpolation Interpolation { get; set; } + + public SwirlEffect (float radius) { + Radius = radius; + Interpolation = IInterpolation.Pow2; + } + + public void Begin (Skeleton skeleton) { + worldX = skeleton.X + CenterX; + worldY = skeleton.Y + CenterY; + } + + public void End () { + } + + public void Transform (ref VertexPositionColorTextureColor vertex) { + float x = vertex.Position.X - worldX; + float y = vertex.Position.Y - worldY; + float dist = (float)Math.Sqrt(x * x + y * y); + if (dist < Radius) { + float theta = Interpolation.Apply(0, angle, (Radius - dist) / Radius); + float cos = MathUtils.Cos(theta), sin = MathUtils.Sin(theta); + vertex.Position.X = cos * x - sin * y + worldX; + vertex.Position.Y = sin * x + cos * y + worldY; + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/XnaTextureLoader.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/XnaTextureLoader.cs new file mode 100644 index 0000000..e3b713f --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.0.64/XnaLoader/XnaTextureLoader.cs @@ -0,0 +1,110 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated January 1, 2020. Replaces all prior versions. + * + * Copyright (c) 2013-2020, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.IO; + +namespace Spine4_0_64 { + public class XnaTextureLoader : TextureLoader { + GraphicsDevice device; + string[] textureLayerSuffixes = null; + + /// + /// Constructor. + /// + /// The graphics device to be used. + /// If true multiple textures layers + /// (e.g. a diffuse/albedo texture and a normal map) are loaded instead of a single texture. + /// Names are constructed based on suffixes added according to the textureSuffixes parameter. + /// If loadMultipleTextureLayers is true, the strings of this array + /// define the path name suffix of each layer to be loaded. Array size must be equal to the number of layers to be loaded. + /// The first array entry is the suffix to be replaced (e.g. "_albedo", or "" for a first layer without a suffix), + /// subsequent array entries contain the suffix to replace the first entry with (e.g. "_normals"). + /// + /// An example would be: + /// new string[] { "", "_normals" } for loading a base diffuse texture named "skeletonname.png" and + /// a normalmap named "skeletonname_normals.png". + public XnaTextureLoader (GraphicsDevice device, bool loadMultipleTextureLayers = false, string[] textureSuffixes = null) { + this.device = device; + if (loadMultipleTextureLayers) + this.textureLayerSuffixes = textureSuffixes; + } + + public void Load (AtlasPage page, String path) { + Texture2D texture = Util.LoadTexture(device, path); + + if (page.width == 0 || page.height == 0) + { + page.width = texture.Width; + page.height = texture.Height; + } + if (textureLayerSuffixes == null) { + page.rendererObject = texture; + } else { + Texture2D[] textureLayersArray = new Texture2D[textureLayerSuffixes.Length]; + textureLayersArray[0] = texture; + for (int layer = 1; layer < textureLayersArray.Length; ++layer) { + string layerPath = GetLayerName(path, textureLayerSuffixes[0], textureLayerSuffixes[layer]); + textureLayersArray[layer] = Util.LoadTexture(device, layerPath); + } + page.rendererObject = textureLayersArray; + } + } + + public void Unload(Object texture) + { + string type = texture.GetType().FullName; + if (type == "Microsoft.Xna.Framework.Graphics.Texture2D[]") + { + foreach (Texture2D texture2d in (Texture2D[])texture) + { + texture2d.Dispose(); + } + + } + else + { + ((Texture2D)texture).Dispose(); + } + + } + + private string GetLayerName (string firstLayerPath, string firstLayerSuffix, string replacementSuffix) { + + int suffixLocation = firstLayerPath.LastIndexOf(firstLayerSuffix + "."); + if (suffixLocation == -1) { + throw new Exception(string.Concat("Error composing texture layer name: first texture layer name '", firstLayerPath, + "' does not contain suffix to be replaced: '", firstLayerSuffix, "'")); + } + return firstLayerPath.Remove(suffixLocation, firstLayerSuffix.Length).Insert(suffixLocation, replacementSuffix); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Animation.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Animation.cs new file mode 100644 index 0000000..b8c0830 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Animation.cs @@ -0,0 +1,2693 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_1_00 { + + /// + /// Stores a list of timelines to animate a skeleton's pose over time. + public class Animation { + internal String name; + internal ExposedList timelines; + internal HashSet timelineIds; + internal float duration; + + public Animation (string name, ExposedList timelines, float duration) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + + this.name = name; + SetTimelines(timelines); + this.duration = duration; + } + + public ExposedList Timelines { + get { return timelines; } + set { SetTimelines(value); } + } + + public void SetTimelines (ExposedList timelines) { + if (timelines == null) throw new ArgumentNullException("timelines", "timelines cannot be null."); + this.timelines = timelines; + // Note: avoiding reallocations by adding all hash set entries at + // once (EnsureCapacity() is only available in newer .Net versions). + int idCount = 0; + int timelinesCount = timelines.Count; + Timeline[] timelinesItems = timelines.Items; + for (int t = 0; t < timelinesCount; ++t) + idCount += timelinesItems[t].PropertyIds.Length; + string[] propertyIds = new string[idCount]; + int currentId = 0; + for (int t = 0; t < timelinesCount; ++t) { + var ids = timelinesItems[t].PropertyIds; + for (int i = 0, idsLength = ids.Length; i < idsLength; ++i) + propertyIds[currentId++] = ids[i]; + } + this.timelineIds = new HashSet(propertyIds); + } + + /// The duration of the animation in seconds, which is usually the highest time of all frames in the timeline. The duration is + /// used to know when it has completed and when it should loop back to the start. + public float Duration { get { return duration; } set { duration = value; } } + + /// The animation's name, which is unique across all animations in the skeleton. + public string Name { get { return name; } } + + /// Returns true if this animation contains a timeline with any of the specified property IDs. + public bool HasTimeline (string[] propertyIds) { + foreach (string id in propertyIds) + if (timelineIds.Contains(id)) return true; + return false; + } + + /// Applies the animation's timelines to the specified skeleton. + /// + /// The skeleton the animation is being applied to. This provides access to the bones, slots, and other skeleton + /// components the timelines may change. + /// The last time in seconds this animation was applied. Some timelines trigger only at specific times rather + /// than every frame. Pass -1 the first time an animation is applied to ensure frame 0 is triggered. + /// The time in seconds the skeleton is being posed for. Most timelines find the frame before and the frame after + /// this time and interpolate between the frame values. If beyond the and loop is + /// true then the animation will repeat, else the last frame will be applied. + /// If true, the animation repeats after the . + /// If any events are fired, they are added to this list. Can be null to ignore fired events or if no timelines + /// fire events. + /// 0 applies the current or setup values (depending on blend). 1 applies the timeline values. Between + /// 0 and 1 applies values between the current or setup values and the timeline values. By adjusting + /// alpha over time, an animation can be mixed in or out. alpha can also be useful to apply + /// animations on top of each other (layering). + /// Controls how mixing is applied when alpha < 1. + /// Indicates whether the timelines are mixing in or out. Used by timelines which perform instant transitions, + /// such as or . + public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, ExposedList events, float alpha, + MixBlend blend, MixDirection direction) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + + if (loop && duration != 0) { + time %= duration; + if (lastTime > 0) lastTime %= duration; + } + + var timelines = this.timelines.Items; + for (int i = 0, n = this.timelines.Count; i < n; i++) + timelines[i].Apply(skeleton, lastTime, time, events, alpha, blend, direction); + } + + override public string ToString () { + return name; + } + } + + /// + /// Controls how timeline values are mixed with setup pose values or current pose values when a timeline is applied with + /// alpha < 1. + /// + public enum MixBlend { + /// Transitions from the setup value to the timeline value (the current value is not used). Before the first frame, the + /// setup value is set. + Setup, + + /// + /// + /// Transitions from the current value to the timeline value. Before the first frame, transitions from the current value to + /// the setup value. Timelines which perform instant transitions, such as or + /// , use the setup value before the first frame. + /// + /// First is intended for the first animations applied, not for animations layered on top of those. + /// + First, + + /// + /// + /// Transitions from the current value to the timeline value. No change is made before the first frame (the current value is + /// kept until the first frame). + /// + /// Replace is intended for animations layered on top of others, not for the first animations applied. + /// + Replace, + + /// + /// + /// Transitions from the current value to the current value plus the timeline value. No change is made before the first frame + /// (the current value is kept until the first frame). + /// + /// Add is intended for animations layered on top of others, not for the first animations applied. Properties + /// set by additive animations must be set manually or by another animation before applying the additive animations, else the + /// property values will increase each time the additive animations are applied. + /// + /// + Add + } + + /// + /// Indicates whether a timeline's alpha is mixing out over time toward 0 (the setup or current pose value) or + /// mixing in toward 1 (the timeline's value). Some timelines use this to decide how values are applied. + /// + public enum MixDirection { + In, + Out + } + + internal enum Property { + Rotate = 0, X, Y, ScaleX, ScaleY, ShearX, ShearY, // + RGB, Alpha, RGB2, // + Attachment, Deform, // + Event, DrawOrder, // + IkConstraint, TransformConstraint, // + PathConstraintPosition, PathConstraintSpacing, PathConstraintMix, // + Sequence + } + + /// + /// The base class for all timelines. + public abstract class Timeline { + private readonly string[] propertyIds; + internal readonly float[] frames; + + /// Unique identifiers for the properties the timeline modifies. + public Timeline (int frameCount, params string[] propertyIds) { + if (propertyIds == null) throw new System.ArgumentNullException("propertyIds", "propertyIds cannot be null."); + this.propertyIds = propertyIds; + frames = new float[frameCount * FrameEntries]; + } + + /// Uniquely encodes both the type of this timeline and the skeleton properties that it affects. + public string[] PropertyIds { + get { return propertyIds; } + } + + /// The time in seconds and any other values for each frame. + public float[] Frames { + get { return frames; } + } + + /// The number of entries stored per frame. + public virtual int FrameEntries { + get { return 1; } + } + + /// The number of frames for this timeline. + public int FrameCount { + get { return frames.Length / FrameEntries; } + } + + public float Duration { + get { + return frames[frames.Length - FrameEntries]; + } + } + + /// Applies this timeline to the skeleton. + /// The skeleton the timeline is being applied to. This provides access to the bones, slots, and other + /// skeleton components the timeline may change. + /// The time this timeline was last applied. Timelines such as trigger only + /// at specific times rather than every frame. In that case, the timeline triggers everything between + /// lastTime (exclusive) and time (inclusive). Pass -1 the first time an animation is + /// applied to ensure frame 0 is triggered. + /// The time in seconds that the skeleton is being posed for. Most timelines find the frame before and the frame + /// after this time and interpolate between the frame values.If beyond the last frame, the last frame will be + /// applied. + /// If any events are fired, they are added to this list. Can be null to ignore fired events or if the timeline + /// does not fire events. + /// 0 applies the current or setup value (depending on blend). 1 applies the timeline value. + /// Between 0 and 1 applies a value between the current or setup value and the timeline value.By adjusting + /// alpha over time, an animation can be mixed in or out. alpha can also be useful to + /// apply animations on top of each other (layering). + /// Controls how mixing is applied when alpha < 1. + /// Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, + /// such as or , and other such as . + public abstract void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, + MixBlend blend, MixDirection direction); + + /// Search using a stride of 1. + /// Must be >= the first value in frames. + /// The index of the first value <= time. + internal static int Search (float[] frames, float time) { + int n = frames.Length; + for (int i = 1; i < n; i++) + if (frames[i] > time) return i - 1; + return n - 1; + } + + /// Search using the specified stride. + /// Must be >= the first value in frames. + /// The index of the first value <= time. + internal static int Search (float[] frames, float time, int step) { + int n = frames.Length; + for (int i = step; i < n; i += step) + if (frames[i] > time) return i - step; + return n - step; + } + } + + /// An interface for timelines which change the property of a bone. + public interface IBoneTimeline { + /// The index of the bone in that will be changed when this timeline is applied. + int BoneIndex { get; } + } + + /// An interface for timelines which change the property of a slot. + public interface ISlotTimeline { + /// The index of the slot in that will be changed when this timeline is applied. + int SlotIndex { get; } + } + + /// The base class for timelines that interpolate between frame values using stepped, linear, or a Bezier curve. + public abstract class CurveTimeline : Timeline { + public const int LINEAR = 0, STEPPED = 1, BEZIER = 2, BEZIER_SIZE = 18; + + internal float[] curves; + /// The number of key frames for this timeline. + + /// The maximum number of Bezier curves. See . + /// Unique identifiers for the properties the timeline modifies. + public CurveTimeline (int frameCount, int bezierCount, params string[] propertyIds) + : base(frameCount, propertyIds) { + curves = new float[frameCount + bezierCount * BEZIER_SIZE]; + curves[frameCount - 1] = STEPPED; + } + + /// Sets the specified frame to linear interpolation. + /// Between 0 and frameCount - 1, inclusive. + public void SetLinear (int frame) { + curves[frame] = LINEAR; + } + + /// Sets the specified frame to stepped interpolation. + /// Between 0 and frameCount - 1, inclusive. + public void SetStepped (int frame) { + curves[frame] = STEPPED; + } + + /// Returns the interpolation type for the specified frame. + /// Between 0 and frameCount - 1, inclusive. + /// , or + the index of the Bezier segments. + public float GetCurveType (int frame) { + return (int)curves[frame]; + } + + /// Shrinks the storage for Bezier curves, for use when bezierCount (specified in the constructor) was larger + /// than the actual number of Bezier curves. + public void Shrink (int bezierCount) { + int size = FrameCount + bezierCount * BEZIER_SIZE; + if (curves.Length > size) { + float[] newCurves = new float[size]; + Array.Copy(curves, 0, newCurves, 0, size); + curves = newCurves; + } + } + + /// + /// Stores the segments for the specified Bezier curve. For timelines that modify multiple values, there may be more than + /// one curve per frame. + /// The ordinal of this Bezier curve for this timeline, between 0 and bezierCount - 1 (specified + /// in the constructor), inclusive. + /// Between 0 and frameCount - 1, inclusive. + /// The index of the value for the frame this curve is used for. + /// The time for the first key. + /// The value for the first key. + /// The time for the first Bezier handle. + /// The value for the first Bezier handle. + /// The time of the second Bezier handle. + /// The value for the second Bezier handle. + /// The time for the second key. + /// The value for the second key. + public void SetBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2, + float cy2, float time2, float value2) { + + float[] curves = this.curves; + int i = FrameCount + bezier * BEZIER_SIZE; + if (value == 0) curves[frame] = BEZIER + i; + float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = (value1 - cy1 * 2 + cy2) * 0.03f; + float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = ((cy1 - cy2) * 3 - value1 + value2) * 0.006f; + float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy; + float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = (cy1 - value1) * 0.3f + tmpy + dddy * 0.16666667f; + float x = time1 + dx, y = value1 + dy; + for (int n = i + BEZIER_SIZE; i < n; i += 2) { + curves[i] = x; + curves[i + 1] = y; + dx += ddx; + dy += ddy; + ddx += dddx; + ddy += dddy; + x += dx; + y += dy; + } + } + + /// + /// Returns the Bezier interpolated value for the specified time. + /// The index into for the values of the frame before time. + /// The offset from frameIndex to the value this curve is used for. + /// The index of the Bezier segments. See . + public float GetBezierValue (float time, int frameIndex, int valueOffset, int i) { + float[] curves = this.curves; + if (curves[i] > time) { + float x = frames[frameIndex], y = frames[frameIndex + valueOffset]; + return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); + } + int n = i + BEZIER_SIZE; + for (i += 2; i < n; i += 2) { + if (curves[i] >= time) { + float x = curves[i - 2], y = curves[i - 1]; + return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); + } + } + frameIndex += FrameEntries; + { // scope added to prevent compile error "float x and y declared in enclosing scope" + float x = curves[n - 2], y = curves[n - 1]; + return y + (time - x) / (frames[frameIndex] - x) * (frames[frameIndex + valueOffset] - y); + } + } + } + + /// The base class for a that sets one property. + public abstract class CurveTimeline1 : CurveTimeline { + public const int ENTRIES = 2; + internal const int VALUE = 1; + + /// The maximum number of Bezier curves. See . + /// Unique identifiers for the properties the timeline modifies. + public CurveTimeline1 (int frameCount, int bezierCount, string propertyId) + : base(frameCount, bezierCount, propertyId) { + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + /// Sets the time and value for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds + public void SetFrame (int frame, float time, float value) { + frame <<= 1; + frames[frame] = time; + frames[frame + VALUE] = value; + } + + /// Returns the interpolated value for the specified time. + public float GetCurveValue (float time) { + float[] frames = this.frames; + int i = frames.Length - 2; + for (int ii = 2; ii <= i; ii += 2) { + if (frames[ii] > time) { + i = ii - 2; + break; + } + } + + int curveType = (int)curves[i >> 1]; + switch (curveType) { + case LINEAR: + float before = frames[i], value = frames[i + VALUE]; + return value + (time - before) / (frames[i + ENTRIES] - before) * (frames[i + ENTRIES + VALUE] - value); + case STEPPED: + return frames[i + VALUE]; + } + return GetBezierValue(time, i, VALUE, curveType - BEZIER); + } + } + + /// The base class for a which sets two properties. + public abstract class CurveTimeline2 : CurveTimeline { + public const int ENTRIES = 3; + internal const int VALUE1 = 1, VALUE2 = 2; + + /// The maximum number of Bezier curves. See . + /// Unique identifiers for the properties the timeline modifies. + public CurveTimeline2 (int frameCount, int bezierCount, string propertyId1, string propertyId2) + : base(frameCount, bezierCount, propertyId1, propertyId2) { + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + /// Sets the time and values for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float value1, float value2) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + VALUE1] = value1; + frames[frame + VALUE2] = value2; + } + } + + /// Changes a bone's local . + public class RotateTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public RotateTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.Rotate + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation; + return; + case MixBlend.First: + bone.rotation += (bone.data.rotation - bone.rotation) * alpha; + return; + } + return; + } + + float r = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation + r * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + r += bone.data.rotation - bone.rotation; + goto case MixBlend.Add; // Fall through. + case MixBlend.Add: + bone.rotation += r * alpha; + break; + } + } + } + + /// Changes a bone's local and . + public class TranslateTimeline : CurveTimeline2, IBoneTimeline { + readonly int boneIndex; + + public TranslateTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, // + (int)Property.X + "|" + boneIndex, // + (int)Property.Y + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x; + bone.y = bone.data.y; + return; + case MixBlend.First: + bone.x += (bone.data.x - bone.x) * alpha; + bone.y += (bone.data.y - bone.y) * alpha; + return; + } + return; + } + + float x, y; + GetCurveValue(out x, out y, time); // note: reference implementation has code inlined + + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x + x * alpha; + bone.y = bone.data.y + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.x += (bone.data.x + x - bone.x) * alpha; + bone.y += (bone.data.y + y - bone.y) * alpha; + break; + case MixBlend.Add: + bone.x += x * alpha; + bone.y += y * alpha; + break; + } + } + + public void GetCurveValue (out float x, out float y, float time) { + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + x += (frames[i + ENTRIES + VALUE1] - x) * t; + y += (frames[i + ENTRIES + VALUE2] - y) * t; + break; + case STEPPED: + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + break; + default: + x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); + y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); + break; + } + } + } + + /// Changes a bone's local . + public class TranslateXTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public TranslateXTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.X + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x; + return; + case MixBlend.First: + bone.x += (bone.data.x - bone.x) * alpha; + return; + } + return; + } + + float x = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.x = bone.data.x + x * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.x += (bone.data.x + x - bone.x) * alpha; + break; + case MixBlend.Add: + bone.x += x * alpha; + break; + } + } + } + + /// Changes a bone's local . + public class TranslateYTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public TranslateYTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.Y + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.y = bone.data.y; + return; + case MixBlend.First: + bone.y += (bone.data.y - bone.y) * alpha; + return; + } + return; + } + + float y = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.y = bone.data.y + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.y += (bone.data.y + y - bone.y) * alpha; + break; + case MixBlend.Add: + bone.y += y * alpha; + break; + } + } + } + + /// Changes a bone's local and . + public class ScaleTimeline : CurveTimeline2, IBoneTimeline { + readonly int boneIndex; + + public ScaleTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, // + (int)Property.ScaleX + "|" + boneIndex, // + (int)Property.ScaleY + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.scaleX = bone.data.scaleX; + bone.scaleY = bone.data.scaleY; + return; + case MixBlend.First: + bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; + return; + } + return; + } + + float x, y; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + x += (frames[i + ENTRIES + VALUE1] - x) * t; + y += (frames[i + ENTRIES + VALUE2] - y) * t; + break; + case STEPPED: + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + break; + default: + x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); + y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); + break; + } + x *= bone.data.scaleX; + y *= bone.data.scaleY; + + if (alpha == 1) { + if (blend == MixBlend.Add) { + bone.scaleX += x - bone.data.scaleX; + bone.scaleY += y - bone.data.scaleY; + } else { + bone.scaleX = x; + bone.scaleY = y; + } + } else { + // Mixing out uses sign of setup or current pose, else use sign of key. + float bx, by; + if (direction == MixDirection.Out) { + switch (blend) { + case MixBlend.Setup: + bx = bone.data.scaleX; + by = bone.data.scaleY; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = bone.scaleX; + by = bone.scaleY; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.Add: + bone.scaleX += (x - bone.data.scaleX) * alpha; + bone.scaleY += (y - bone.data.scaleY) * alpha; + break; + } + } else { + switch (blend) { + case MixBlend.Setup: + bx = Math.Abs(bone.data.scaleX) * Math.Sign(x); + by = Math.Abs(bone.data.scaleY) * Math.Sign(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = Math.Abs(bone.scaleX) * Math.Sign(x); + by = Math.Abs(bone.scaleY) * Math.Sign(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.Add: + bone.scaleX += (x - bone.data.scaleX) * alpha; + bone.scaleY += (y - bone.data.scaleY) * alpha; + break; + } + } + } + } + } + + /// Changes a bone's local . + public class ScaleXTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public ScaleXTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.ScaleX + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.scaleX = bone.data.scaleX; + return; + case MixBlend.First: + bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; + return; + } + return; + } + + float x = GetCurveValue(time) * bone.data.scaleX; + if (alpha == 1) { + if (blend == MixBlend.Add) + bone.scaleX += x - bone.data.scaleX; + else + bone.scaleX = x; + } else { + // Mixing out uses sign of setup or current pose, else use sign of key. + float bx; + if (direction == MixDirection.Out) { + switch (blend) { + case MixBlend.Setup: + bx = bone.data.scaleX; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = bone.scaleX; + bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; + break; + case MixBlend.Add: + bone.scaleX += (x - bone.data.scaleX) * alpha; + break; + } + } else { + switch (blend) { + case MixBlend.Setup: + bx = Math.Abs(bone.data.scaleX) * Math.Sign(x); + bone.scaleX = bx + (x - bx) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bx = Math.Abs(bone.scaleX) * Math.Sign(x); + bone.scaleX = bx + (x - bx) * alpha; + break; + case MixBlend.Add: + bone.scaleX += (x - bone.data.scaleX) * alpha; + break; + } + } + } + } + } + + /// Changes a bone's local . + public class ScaleYTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public ScaleYTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.ScaleY + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.scaleY = bone.data.scaleY; + return; + case MixBlend.First: + bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; + return; + } + return; + } + + float y = GetCurveValue(time) * bone.data.scaleY; + if (alpha == 1) { + if (blend == MixBlend.Add) + bone.scaleY += y - bone.data.scaleY; + else + bone.scaleY = y; + } else { + // Mixing out uses sign of setup or current pose, else use sign of key. + float by; + if (direction == MixDirection.Out) { + switch (blend) { + case MixBlend.Setup: + by = bone.data.scaleY; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + by = bone.scaleY; + bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; + break; + case MixBlend.Add: + bone.scaleY += (y - bone.data.scaleY) * alpha; + break; + } + } else { + switch (blend) { + case MixBlend.Setup: + by = Math.Abs(bone.data.scaleY) * Math.Sign(y); + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + by = Math.Abs(bone.scaleY) * Math.Sign(y); + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.Add: + bone.scaleY += (y - bone.data.scaleY) * alpha; + break; + } + } + } + } + } + + /// Changes a bone's local and . + public class ShearTimeline : CurveTimeline2, IBoneTimeline { + readonly int boneIndex; + + public ShearTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, // + (int)Property.ShearX + "|" + boneIndex, // + (int)Property.ShearY + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX; + bone.shearY = bone.data.shearY; + return; + case MixBlend.First: + bone.shearX += (bone.data.shearX - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY - bone.shearY) * alpha; + return; + } + return; + } + + float x, y; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + x += (frames[i + ENTRIES + VALUE1] - x) * t; + y += (frames[i + ENTRIES + VALUE2] - y) * t; + break; + case STEPPED: + x = frames[i + VALUE1]; + y = frames[i + VALUE2]; + break; + default: + x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); + y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); + break; + } + + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX + x * alpha; + bone.shearY = bone.data.shearY + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + break; + case MixBlend.Add: + bone.shearX += x * alpha; + bone.shearY += y * alpha; + break; + } + } + } + + /// Changes a bone's local . + public class ShearXTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public ShearXTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.ShearX + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX; + return; + case MixBlend.First: + bone.shearX += (bone.data.shearX - bone.shearX) * alpha; + return; + } + return; + } + + float x = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.shearX = bone.data.shearX + x * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; + break; + case MixBlend.Add: + bone.shearX += x * alpha; + break; + } + } + } + + /// Changes a bone's local . + public class ShearYTimeline : CurveTimeline1, IBoneTimeline { + readonly int boneIndex; + + public ShearYTimeline (int frameCount, int bezierCount, int boneIndex) + : base(frameCount, bezierCount, (int)Property.ShearY + "|" + boneIndex) { + this.boneIndex = boneIndex; + } + + public int BoneIndex { + get { + return boneIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Bone bone = skeleton.bones.Items[boneIndex]; + if (!bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.shearY = bone.data.shearY; + return; + case MixBlend.First: + bone.shearY += (bone.data.shearY - bone.shearY) * alpha; + return; + } + return; + } + + float y = GetCurveValue(time); + switch (blend) { + case MixBlend.Setup: + bone.shearY = bone.data.shearY + y * alpha; + break; + case MixBlend.First: + case MixBlend.Replace: + bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + break; + case MixBlend.Add: + bone.shearY += y * alpha; + break; + } + } + } + + /// Changes a slot's . + public class RGBATimeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 5; + protected const int R = 1, G = 2, B = 3, A = 4; + + readonly int slotIndex; + + public RGBATimeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, // + (int)Property.RGB + "|" + slotIndex, // + (int)Property.Alpha + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + public override int FrameEntries { + get { return ENTRIES; } + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + /// Sets the time and color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float r, float g, float b, float a) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + R] = r; + frames[frame + G] = g; + frames[frame + B] = b; + frames[frame + A] = a; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + var setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = setup.r; + slot.g = setup.g; + slot.b = setup.b; + slot.a = setup.a; + return; + case MixBlend.First: + slot.r += (setup.r - slot.r) * alpha; + slot.g += (setup.g - slot.g) * alpha; + slot.b += (setup.b - slot.b) * alpha; + slot.a += (setup.a - slot.a) * alpha; + slot.ClampColor(); + return; + } + return; + } + + float r, g, b, a; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + a = frames[i + A]; + float t = (time - before) / (frames[i + ENTRIES] - before); + r += (frames[i + ENTRIES + R] - r) * t; + g += (frames[i + ENTRIES + G] - g) * t; + b += (frames[i + ENTRIES + B] - b) * t; + a += (frames[i + ENTRIES + A] - a) * t; + break; + case STEPPED: + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + a = frames[i + A]; + break; + default: + r = GetBezierValue(time, i, R, curveType - BEZIER); + g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); + b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); + a = GetBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER); + break; + } + + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + slot.a = a; + } else { + float br, bg, bb, ba; + if (blend == MixBlend.Setup) { + br = slot.data.r; + bg = slot.data.g; + bb = slot.data.b; + ba = slot.data.a; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + ba = slot.a; + } + slot.r = br + (r - br) * alpha; + slot.g = bg + (g - bg) * alpha; + slot.b = bb + (b - bb) * alpha; + slot.a = ba + (a - ba) * alpha; + } + slot.ClampColor(); + } + } + + /// Changes the RGB for a slot's . + public class RGBTimeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 4; + protected const int R = 1, G = 2, B = 3; + + readonly int slotIndex; + + public RGBTimeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, // + (int)Property.RGB + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + /// Sets the time and color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float r, float g, float b) { + frame <<= 2; + frames[frame] = time; + frames[frame + R] = r; + frames[frame + G] = g; + frames[frame + B] = b; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + var setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = setup.r; + slot.g = setup.g; + slot.b = setup.b; + return; + case MixBlend.First: + slot.r += (setup.r - slot.r) * alpha; + slot.g += (setup.g - slot.g) * alpha; + slot.b += (setup.b - slot.b) * alpha; + slot.ClampColor(); + return; + } + return; + } + + float r, g, b; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 2]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + float t = (time - before) / (frames[i + ENTRIES] - before); + r += (frames[i + ENTRIES + R] - r) * t; + g += (frames[i + ENTRIES + G] - g) * t; + b += (frames[i + ENTRIES + B] - b) * t; + break; + case STEPPED: + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + break; + default: + r = GetBezierValue(time, i, R, curveType - BEZIER); + g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); + b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); + break; + } + + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + } else { + float br, bg, bb; + if (blend == MixBlend.Setup) { + var setup = slot.data; + br = setup.r; + bg = setup.g; + bb = setup.b; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + } + slot.r = br + (r - br) * alpha; + slot.g = bg + (g - bg) * alpha; + slot.b = bb + (b - bb) * alpha; + } + slot.ClampColor(); + } + } + + /// Changes the alpha for a slot's . + public class AlphaTimeline : CurveTimeline1, ISlotTimeline { + readonly int slotIndex; + + public AlphaTimeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, (int)Property.Alpha + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + var setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.a = setup.a; + return; + case MixBlend.First: + slot.a += (setup.a - slot.a) * alpha; + slot.ClampColor(); + return; + } + return; + } + + float a = GetCurveValue(time); + if (alpha == 1) + slot.a = a; + else { + if (blend == MixBlend.Setup) slot.a = slot.data.a; + slot.a += (a - slot.a) * alpha; + } + slot.ClampColor(); + } + } + + /// Changes a slot's and for two color tinting. + public class RGBA2Timeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 8; + protected const int R = 1, G = 2, B = 3, A = 4, R2 = 5, G2 = 6, B2 = 7; + + readonly int slotIndex; + + public RGBA2Timeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, // + (int)Property.RGB + "|" + slotIndex, // + (int)Property.Alpha + "|" + slotIndex, // + (int)Property.RGB2 + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + + public override int FrameEntries { + get { + return ENTRIES; + } + } + + /// + /// The index of the slot in that will be changed when this timeline is applied. The + /// must have a dark color available. + public int SlotIndex { + get { + return slotIndex; + } + } + + /// Sets the time, light color, and dark color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float r, float g, float b, float a, float r2, float g2, float b2) { + frame <<= 3; + frames[frame] = time; + frames[frame + R] = r; + frames[frame + G] = g; + frames[frame + B] = b; + frames[frame + A] = a; + frames[frame + R2] = r2; + frames[frame + G2] = g2; + frames[frame + B2] = b2; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + SlotData setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = setup.r; + slot.g = setup.g; + slot.b = setup.b; + slot.a = setup.a; + slot.ClampColor(); + slot.r2 = setup.r2; + slot.g2 = setup.g2; + slot.b2 = setup.b2; + slot.ClampSecondColor(); + return; + case MixBlend.First: + slot.r += (slot.r - setup.r) * alpha; + slot.g += (slot.g - setup.g) * alpha; + slot.b += (slot.b - setup.b) * alpha; + slot.a += (slot.a - setup.a) * alpha; + slot.ClampColor(); + slot.r2 += (slot.r2 - setup.r2) * alpha; + slot.g2 += (slot.g2 - setup.g2) * alpha; + slot.b2 += (slot.b2 - setup.b2) * alpha; + slot.ClampSecondColor(); + return; + } + return; + } + + float r, g, b, a, r2, g2, b2; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 3]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + a = frames[i + A]; + r2 = frames[i + R2]; + g2 = frames[i + G2]; + b2 = frames[i + B2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + r += (frames[i + ENTRIES + R] - r) * t; + g += (frames[i + ENTRIES + G] - g) * t; + b += (frames[i + ENTRIES + B] - b) * t; + a += (frames[i + ENTRIES + A] - a) * t; + r2 += (frames[i + ENTRIES + R2] - r2) * t; + g2 += (frames[i + ENTRIES + G2] - g2) * t; + b2 += (frames[i + ENTRIES + B2] - b2) * t; + break; + case STEPPED: + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + a = frames[i + A]; + r2 = frames[i + R2]; + g2 = frames[i + G2]; + b2 = frames[i + B2]; + break; + default: + r = GetBezierValue(time, i, R, curveType - BEZIER); + g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); + b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); + a = GetBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER); + r2 = GetBezierValue(time, i, R2, curveType + BEZIER_SIZE * 4 - BEZIER); + g2 = GetBezierValue(time, i, G2, curveType + BEZIER_SIZE * 5 - BEZIER); + b2 = GetBezierValue(time, i, B2, curveType + BEZIER_SIZE * 6 - BEZIER); + break; + } + + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + slot.a = a; + slot.r2 = r2; + slot.g2 = g2; + slot.b2 = b2; + } else { + float br, bg, bb, ba, br2, bg2, bb2; + if (blend == MixBlend.Setup) { + br = slot.data.r; + bg = slot.data.g; + bb = slot.data.b; + ba = slot.data.a; + br2 = slot.data.r2; + bg2 = slot.data.g2; + bb2 = slot.data.b2; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + ba = slot.a; + br2 = slot.r2; + bg2 = slot.g2; + bb2 = slot.b2; + } + slot.r = br + (r - br) * alpha; + slot.g = bg + (g - bg) * alpha; + slot.b = bb + (b - bb) * alpha; + slot.a = ba + (a - ba) * alpha; + slot.r2 = br2 + (r2 - br2) * alpha; + slot.g2 = bg2 + (g2 - bg2) * alpha; + slot.b2 = bb2 + (b2 - bb2) * alpha; + } + slot.ClampColor(); + slot.ClampSecondColor(); + } + } + + /// Changes the RGB for a slot's and for two color tinting. + public class RGB2Timeline : CurveTimeline, ISlotTimeline { + public const int ENTRIES = 7; + protected const int R = 1, G = 2, B = 3, R2 = 4, G2 = 5, B2 = 6; + + readonly int slotIndex; + + public RGB2Timeline (int frameCount, int bezierCount, int slotIndex) + : base(frameCount, bezierCount, // + (int)Property.RGB + "|" + slotIndex, // + (int)Property.RGB2 + "|" + slotIndex) { + this.slotIndex = slotIndex; + } + + public override int FrameEntries { + get { + return ENTRIES; + } + } + + /// + /// The index of the slot in that will be changed when this timeline is applied. The + /// must have a dark color available. + public int SlotIndex { + get { + return slotIndex; + } + } + + /// Sets the time, light color, and dark color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float r, float g, float b, float r2, float g2, float b2) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + R] = r; + frames[frame + G] = g; + frames[frame + B] = b; + frames[frame + R2] = r2; + frames[frame + G2] = g2; + frames[frame + B2] = b2; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + SlotData setup = slot.data; + switch (blend) { + case MixBlend.Setup: + slot.r = setup.r; + slot.g = setup.g; + slot.b = setup.b; + slot.ClampColor(); + slot.r2 = setup.r2; + slot.g2 = setup.g2; + slot.b2 = setup.b2; + slot.ClampSecondColor(); + return; + case MixBlend.First: + slot.r += (slot.r - setup.r) * alpha; + slot.g += (slot.g - setup.g) * alpha; + slot.b += (slot.b - setup.b) * alpha; + slot.ClampColor(); + slot.r2 += (slot.r2 - setup.r2) * alpha; + slot.g2 += (slot.g2 - setup.g2) * alpha; + slot.b2 += (slot.b2 - setup.b2) * alpha; + slot.ClampSecondColor(); + return; + } + return; + } + + float r, g, b, r2, g2, b2; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + r2 = frames[i + R2]; + g2 = frames[i + G2]; + b2 = frames[i + B2]; + float t = (time - before) / (frames[i + ENTRIES] - before); + r += (frames[i + ENTRIES + R] - r) * t; + g += (frames[i + ENTRIES + G] - g) * t; + b += (frames[i + ENTRIES + B] - b) * t; + r2 += (frames[i + ENTRIES + R2] - r2) * t; + g2 += (frames[i + ENTRIES + G2] - g2) * t; + b2 += (frames[i + ENTRIES + B2] - b2) * t; + break; + case STEPPED: + r = frames[i + R]; + g = frames[i + G]; + b = frames[i + B]; + r2 = frames[i + R2]; + g2 = frames[i + G2]; + b2 = frames[i + B2]; + break; + default: + r = GetBezierValue(time, i, R, curveType - BEZIER); + g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); + b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); + r2 = GetBezierValue(time, i, R2, curveType + BEZIER_SIZE * 3 - BEZIER); + g2 = GetBezierValue(time, i, G2, curveType + BEZIER_SIZE * 4 - BEZIER); + b2 = GetBezierValue(time, i, B2, curveType + BEZIER_SIZE * 5 - BEZIER); + break; + } + + if (alpha == 1) { + slot.r = r; + slot.g = g; + slot.b = b; + slot.r2 = r2; + slot.g2 = g2; + slot.b2 = b2; + } else { + float br, bg, bb, br2, bg2, bb2; + if (blend == MixBlend.Setup) { + SlotData setup = slot.data; + br = setup.r; + bg = setup.g; + bb = setup.b; + br2 = setup.r2; + bg2 = setup.g2; + bb2 = setup.b2; + } else { + br = slot.r; + bg = slot.g; + bb = slot.b; + br2 = slot.r2; + bg2 = slot.g2; + bb2 = slot.b2; + } + slot.r = br + (r - br) * alpha; + slot.g = bg + (g - bg) * alpha; + slot.b = bb + (b - bb) * alpha; + slot.r2 = br2 + (r2 - br2) * alpha; + slot.g2 = bg2 + (g2 - bg2) * alpha; + slot.b2 = bb2 + (b2 - bb2) * alpha; + } + slot.ClampColor(); + slot.ClampSecondColor(); + } + } + + /// Changes a slot's . + public class AttachmentTimeline : Timeline, ISlotTimeline { + readonly int slotIndex; + readonly string[] attachmentNames; + + public AttachmentTimeline (int frameCount, int slotIndex) + : base(frameCount, (int)Property.Attachment + "|" + slotIndex) { + this.slotIndex = slotIndex; + attachmentNames = new String[frameCount]; + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + /// The attachment name for each frame. May contain null values to clear the attachment. + public string[] AttachmentNames { + get { + return attachmentNames; + } + } + + /// Sets the time and attachment name for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, String attachmentName) { + frames[frame] = time; + attachmentNames[frame] = attachmentName; + } + + public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + + if (direction == MixDirection.Out) { + if (blend == MixBlend.Setup) SetAttachment(skeleton, slot, slot.data.attachmentName); + return; + } + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) SetAttachment(skeleton, slot, slot.data.attachmentName); + return; + } + + SetAttachment(skeleton, slot, attachmentNames[Search(frames, time)]); + } + + private void SetAttachment (Skeleton skeleton, Slot slot, string attachmentName) { + slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); + } + } + + /// Changes a slot's to deform a . + public class DeformTimeline : CurveTimeline, ISlotTimeline { + readonly int slotIndex; + readonly VertexAttachment attachment; + internal float[][] vertices; + + public DeformTimeline (int frameCount, int bezierCount, int slotIndex, VertexAttachment attachment) + : base(frameCount, bezierCount, (int)Property.Deform + "|" + slotIndex + "|" + attachment.Id) { + this.slotIndex = slotIndex; + this.attachment = attachment; + vertices = new float[frameCount][]; + } + + public int SlotIndex { + get { + return slotIndex; + } + } + /// The attachment that will be deformed. + /// + public VertexAttachment Attachment { + get { + return attachment; + } + } + + /// The vertices for each frame. + public float[][] Vertices { + get { + return vertices; + } + } + + /// Sets the time and vertices for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + /// Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. + public void SetFrame (int frame, float time, float[] vertices) { + frames[frame] = time; + this.vertices[frame] = vertices; + } + + /// Ignored (0 is used for a deform timeline). + /// Ignored (1 is used for a deform timeline). + public void setBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2, + float cy2, float time2, float value2) { + float[] curves = this.curves; + int i = FrameCount + bezier * BEZIER_SIZE; + if (value == 0) curves[frame] = BEZIER + i; + float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = cy2 * 0.03f - cy1 * 0.06f; + float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = (cy1 - cy2 + 0.33333333f) * 0.018f; + float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy; + float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = cy1 * 0.3f + tmpy + dddy * 0.16666667f; + float x = time1 + dx, y = dy; + for (int n = i + BEZIER_SIZE; i < n; i += 2) { + curves[i] = x; + curves[i + 1] = y; + dx += ddx; + dy += ddy; + ddx += dddx; + ddy += dddy; + x += dx; + y += dy; + } + } + + /// Returns the interpolated percentage for the specified time. + /// The frame before time. + private float GetCurvePercent (float time, int frame) { + float[] curves = this.curves; + int i = (int)curves[frame]; + switch (i) { + case LINEAR: + float x = frames[frame]; + return (time - x) / (frames[frame + FrameEntries] - x); + case STEPPED: + return 0; + } + i -= BEZIER; + if (curves[i] > time) { + float x = frames[frame]; + return curves[i + 1] * (time - x) / (curves[i] - x); + } + int n = i + BEZIER_SIZE; + for (i += 2; i < n; i += 2) { + if (curves[i] >= time) { + float x = curves[i - 2], y = curves[i - 1]; + return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); + } + } + { // scope added to prevent compile error "float x and y declared in enclosing scope" + float x = curves[n - 2], y = curves[n - 1]; + return y + (1 - y) * (time - x) / (frames[frame + FrameEntries] - x); + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + var vertexAttachment = slot.attachment as VertexAttachment; + if (vertexAttachment == null || vertexAttachment.TimelineAttachment != attachment) return; + + var deformArray = slot.deform; + if (deformArray.Count == 0) blend = MixBlend.Setup; + + float[][] vertices = this.vertices; + int vertexCount = vertices[0].Length; + + float[] deform; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + deformArray.Clear(); + return; + case MixBlend.First: + if (alpha == 1) { + deformArray.Clear(); + return; + } + + // Ensure size and preemptively set count. + if (deformArray.Capacity < vertexCount) deformArray.Capacity = vertexCount; + deformArray.Count = vertexCount; + deform = deformArray.Items; + + if (vertexAttachment.bones == null) { + // Unweighted vertex positions. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + deform[i] += (setupVertices[i] - deform[i]) * alpha; + } else { + // Weighted deform offsets. + alpha = 1 - alpha; + for (int i = 0; i < vertexCount; i++) + deform[i] *= alpha; + } + return; + } + return; + } + + // Ensure size and preemptively set count. + if (deformArray.Capacity < vertexCount) deformArray.Capacity = vertexCount; + deformArray.Count = vertexCount; + deform = deformArray.Items; + + if (time >= frames[frames.Length - 1]) { // Time is after last frame. + float[] lastVertices = vertices[frames.Length - 1]; + if (alpha == 1) { + if (blend == MixBlend.Add) { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + deform[i] += lastVertices[i] - setupVertices[i]; + } else { + // Weighted deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) + deform[i] += lastVertices[i]; + } + } else { + // Vertex positions or deform offsets, no alpha. + Array.Copy(lastVertices, 0, deform, 0, vertexCount); + } + } else { + switch (blend) { + case MixBlend.Setup: { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float setup = setupVertices[i]; + deform[i] = setup + (lastVertices[i] - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) + deform[i] = lastVertices[i] * alpha; + } + break; + } + case MixBlend.First: + case MixBlend.Replace: + // Vertex positions or deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) + deform[i] += (lastVertices[i] - deform[i]) * alpha; + break; + case MixBlend.Add: + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) + deform[i] += (lastVertices[i] - setupVertices[i]) * alpha; + } else { + // Weighted deform offsets, alpha. + for (int i = 0; i < vertexCount; i++) + deform[i] += lastVertices[i] * alpha; + } + break; + } + } + return; + } + + int frame = Search(frames, time); + float percent = GetCurvePercent(time, frame); + float[] prevVertices = vertices[frame]; + float[] nextVertices = vertices[frame + 1]; + + if (alpha == 1) { + if (blend == MixBlend.Add) { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, no alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i]; + } + } else { + // Weighted deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += prev + (nextVertices[i] - prev) * percent; + } + } + } else { + // Vertex positions or deform offsets, no alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] = prev + (nextVertices[i] - prev) * percent; + } + } + } else { + switch (blend) { + case MixBlend.Setup: { + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i], setup = setupVertices[i]; + deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + break; + } + case MixBlend.First: + case MixBlend.Replace: { + // Vertex positions or deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha; + } + break; + } + case MixBlend.Add: + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + float[] setupVertices = vertexAttachment.vertices; + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + break; + } + } + } + } + + /// Fires an when specific animation times are reached. + public class EventTimeline : Timeline { + readonly static string[] propertyIds = { ((int)Property.Event).ToString() }; + readonly Event[] events; + + public EventTimeline (int frameCount) + : base(frameCount, propertyIds) { + events = new Event[frameCount]; + } + + /// The event for each frame. + public Event[] Events { + get { + return events; + } + } + + /// Sets the time and event for the specified frame. + /// Between 0 and frameCount, inclusive. + public void SetFrame (int frame, Event e) { + frames[frame] = e.time; + events[frame] = e; + } + + /// Fires events for frames > lastTime and <= time. + public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, + MixBlend blend, MixDirection direction) { + + if (firedEvents == null) return; + + float[] frames = this.frames; + int frameCount = frames.Length; + + if (lastTime > time) { // Fire events after last time for looped animations. + Apply(skeleton, lastTime, int.MaxValue, firedEvents, alpha, blend, direction); + lastTime = -1f; + } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. + + int i; + if (lastTime < frames[0]) + i = 0; + else { + i = Search(frames, lastTime) + 1; + float frameTime = frames[i]; + while (i > 0) { // Fire multiple events with the same frame. + if (frames[i - 1] != frameTime) break; + i--; + } + } + for (; i < frameCount && time >= frames[i]; i++) + firedEvents.Add(events[i]); + } + } + + /// Changes a skeleton's . + public class DrawOrderTimeline : Timeline { + static readonly string[] propertyIds = { ((int)Property.DrawOrder).ToString() }; + + readonly int[][] drawOrders; + + public DrawOrderTimeline (int frameCount) + : base(frameCount, propertyIds) { + drawOrders = new int[frameCount][]; + } + + /// The draw order for each frame. + /// . + public int[][] DrawOrders { + get { + return drawOrders; + } + } + + /// Sets the time and draw order for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + /// For each slot in , the index of the slot in the new draw order. May be null to use + /// setup pose draw order. + public void SetFrame (int frame, float time, int[] drawOrder) { + frames[frame] = time; + drawOrders[frame] = drawOrder; + } + + public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + + if (direction == MixDirection.Out) { + if (blend == MixBlend.Setup) Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); + return; + } + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); + return; + } + + int[] drawOrderToSetupIndex = drawOrders[Search(frames, time)]; + if (drawOrderToSetupIndex == null) + Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); + else { + Slot[] slots = skeleton.slots.Items; + Slot[] drawOrder = skeleton.drawOrder.Items; + for (int i = 0, n = drawOrderToSetupIndex.Length; i < n; i++) + drawOrder[i] = slots[drawOrderToSetupIndex[i]]; + } + } + } + + /// Changes an IK constraint's , , + /// , , and . + public class IkConstraintTimeline : CurveTimeline { + public const int ENTRIES = 6; + private const int MIX = 1, SOFTNESS = 2, BEND_DIRECTION = 3, COMPRESS = 4, STRETCH = 5; + + readonly int ikConstraintIndex; + + public IkConstraintTimeline (int frameCount, int bezierCount, int ikConstraintIndex) + : base(frameCount, bezierCount, (int)Property.IkConstraint + "|" + ikConstraintIndex) { + this.ikConstraintIndex = ikConstraintIndex; + } + + public override int FrameEntries { + get { + return ENTRIES; + } + } + + /// The index of the IK constraint slot in that will be changed when this timeline is + /// applied. + public int IkConstraintIndex { + get { + return ikConstraintIndex; + } + } + + /// Sets the time, mix, softness, bend direction, compress, and stretch for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + /// 1 or -1. + public void SetFrame (int frame, float time, float mix, float softness, int bendDirection, bool compress, + bool stretch) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + MIX] = mix; + frames[frame + SOFTNESS] = softness; + frames[frame + BEND_DIRECTION] = bendDirection; + frames[frame + COMPRESS] = compress ? 1 : 0; + frames[frame + STRETCH] = stretch ? 1 : 0; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + IkConstraint constraint = skeleton.ikConstraints.Items[ikConstraintIndex]; + if (!constraint.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.mix = constraint.data.mix; + constraint.softness = constraint.data.softness; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + return; + case MixBlend.First: + constraint.mix += (constraint.data.mix - constraint.mix) * alpha; + constraint.softness += (constraint.data.softness - constraint.softness) * alpha; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + return; + } + return; + } + + float mix, softness; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + mix = frames[i + MIX]; + softness = frames[i + SOFTNESS]; + float t = (time - before) / (frames[i + ENTRIES] - before); + mix += (frames[i + ENTRIES + MIX] - mix) * t; + softness += (frames[i + ENTRIES + SOFTNESS] - softness) * t; + break; + case STEPPED: + mix = frames[i + MIX]; + softness = frames[i + SOFTNESS]; + break; + default: + mix = GetBezierValue(time, i, MIX, curveType - BEZIER); + softness = GetBezierValue(time, i, SOFTNESS, curveType + BEZIER_SIZE - BEZIER); + break; + } + + if (blend == MixBlend.Setup) { + constraint.mix = constraint.data.mix + (mix - constraint.data.mix) * alpha; + constraint.softness = constraint.data.softness + (softness - constraint.data.softness) * alpha; + if (direction == MixDirection.Out) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = (int)frames[i + BEND_DIRECTION]; + constraint.compress = frames[i + COMPRESS] != 0; + constraint.stretch = frames[i + STRETCH] != 0; + } + } else { + constraint.mix += (mix - constraint.mix) * alpha; + constraint.softness += (softness - constraint.softness) * alpha; + if (direction == MixDirection.In) { + constraint.bendDirection = (int)frames[i + BEND_DIRECTION]; + constraint.compress = frames[i + COMPRESS] != 0; + constraint.stretch = frames[i + STRETCH] != 0; + } + } + } + } + + /// Changes a transform constraint's mixes. + public class TransformConstraintTimeline : CurveTimeline { + public const int ENTRIES = 7; + private const int ROTATE = 1, X = 2, Y = 3, SCALEX = 4, SCALEY = 5, SHEARY = 6; + + readonly int transformConstraintIndex; + + public TransformConstraintTimeline (int frameCount, int bezierCount, int transformConstraintIndex) + : base(frameCount, bezierCount, (int)Property.TransformConstraint + "|" + transformConstraintIndex) { + this.transformConstraintIndex = transformConstraintIndex; + } + + public override int FrameEntries { + get { + return ENTRIES; + } + } + + /// The index of the transform constraint slot in that will be changed when this + /// timeline is applied. + public int TransformConstraintIndex { + get { + return transformConstraintIndex; + } + } + + /// Sets the time, rotate mix, translate mix, scale mix, and shear mix for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float mixRotate, float mixX, float mixY, float mixScaleX, float mixScaleY, + float mixShearY) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + ROTATE] = mixRotate; + frames[frame + X] = mixX; + frames[frame + Y] = mixY; + frames[frame + SCALEX] = mixScaleX; + frames[frame + SCALEY] = mixScaleY; + frames[frame + SHEARY] = mixShearY; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + TransformConstraint constraint = skeleton.transformConstraints.Items[transformConstraintIndex]; + if (!constraint.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + TransformConstraintData data = constraint.data; + switch (blend) { + case MixBlend.Setup: + constraint.mixRotate = data.mixRotate; + constraint.mixX = data.mixX; + constraint.mixY = data.mixY; + constraint.mixScaleX = data.mixScaleX; + constraint.mixScaleY = data.mixScaleY; + constraint.mixShearY = data.mixShearY; + return; + case MixBlend.First: + constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha; + constraint.mixX += (data.mixX - constraint.mixX) * alpha; + constraint.mixY += (data.mixY - constraint.mixY) * alpha; + constraint.mixScaleX += (data.mixScaleX - constraint.mixScaleX) * alpha; + constraint.mixScaleY += (data.mixScaleY - constraint.mixScaleY) * alpha; + constraint.mixShearY += (data.mixShearY - constraint.mixShearY) * alpha; + return; + } + return; + } + + float rotate, x, y, scaleX, scaleY, shearY; + GetCurveValue(out rotate, out x, out y, out scaleX, out scaleY, out shearY, time); + + if (blend == MixBlend.Setup) { + TransformConstraintData data = constraint.data; + constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha; + constraint.mixX = data.mixX + (x - data.mixX) * alpha; + constraint.mixY = data.mixY + (y - data.mixY) * alpha; + constraint.mixScaleX = data.mixScaleX + (scaleX - data.mixScaleX) * alpha; + constraint.mixScaleY = data.mixScaleY + (scaleY - data.mixScaleY) * alpha; + constraint.mixShearY = data.mixShearY + (shearY - data.mixShearY) * alpha; + } else { + constraint.mixRotate += (rotate - constraint.mixRotate) * alpha; + constraint.mixX += (x - constraint.mixX) * alpha; + constraint.mixY += (y - constraint.mixY) * alpha; + constraint.mixScaleX += (scaleX - constraint.mixScaleX) * alpha; + constraint.mixScaleY += (scaleY - constraint.mixScaleY) * alpha; + constraint.mixShearY += (shearY - constraint.mixShearY) * alpha; + } + } + + public void GetCurveValue (out float rotate, out float x, out float y, + out float scaleX, out float scaleY, out float shearY, float time) { + + float[] frames = this.frames; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + rotate = frames[i + ROTATE]; + x = frames[i + X]; + y = frames[i + Y]; + scaleX = frames[i + SCALEX]; + scaleY = frames[i + SCALEY]; + shearY = frames[i + SHEARY]; + float t = (time - before) / (frames[i + ENTRIES] - before); + rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t; + x += (frames[i + ENTRIES + X] - x) * t; + y += (frames[i + ENTRIES + Y] - y) * t; + scaleX += (frames[i + ENTRIES + SCALEX] - scaleX) * t; + scaleY += (frames[i + ENTRIES + SCALEY] - scaleY) * t; + shearY += (frames[i + ENTRIES + SHEARY] - shearY) * t; + break; + case STEPPED: + rotate = frames[i + ROTATE]; + x = frames[i + X]; + y = frames[i + Y]; + scaleX = frames[i + SCALEX]; + scaleY = frames[i + SCALEY]; + shearY = frames[i + SHEARY]; + break; + default: + rotate = GetBezierValue(time, i, ROTATE, curveType - BEZIER); + x = GetBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER); + y = GetBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER); + scaleX = GetBezierValue(time, i, SCALEX, curveType + BEZIER_SIZE * 3 - BEZIER); + scaleY = GetBezierValue(time, i, SCALEY, curveType + BEZIER_SIZE * 4 - BEZIER); + shearY = GetBezierValue(time, i, SHEARY, curveType + BEZIER_SIZE * 5 - BEZIER); + break; + } + } + } + + /// Changes a path constraint's . + public class PathConstraintPositionTimeline : CurveTimeline1 { + readonly int pathConstraintIndex; + + public PathConstraintPositionTimeline (int frameCount, int bezierCount, int pathConstraintIndex) + : base(frameCount, bezierCount, (int)Property.PathConstraintPosition + "|" + pathConstraintIndex) { + this.pathConstraintIndex = pathConstraintIndex; + } + + /// The index of the path constraint slot in that will be changed when this timeline + /// is applied. + public int PathConstraintIndex { + get { + return pathConstraintIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + if (!constraint.active) return; + + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.position = constraint.data.position; + return; + case MixBlend.First: + constraint.position += (constraint.data.position - constraint.position) * alpha; + return; + } + return; + } + + float position = GetCurveValue(time); + if (blend == MixBlend.Setup) + constraint.position = constraint.data.position + (position - constraint.data.position) * alpha; + else + constraint.position += (position - constraint.position) * alpha; + } + } + + /// Changes a path constraint's . + public class PathConstraintSpacingTimeline : CurveTimeline1 { + readonly int pathConstraintIndex; + + public PathConstraintSpacingTimeline (int frameCount, int bezierCount, int pathConstraintIndex) + : base(frameCount, bezierCount, (int)Property.PathConstraintSpacing + "|" + pathConstraintIndex) { + this.pathConstraintIndex = pathConstraintIndex; + } + + /// The index of the path constraint slot in that will be changed when this timeline + /// is applied. + public int PathConstraintIndex { + get { + return pathConstraintIndex; + } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixBlend blend, + MixDirection direction) { + + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + if (!constraint.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.spacing = constraint.data.spacing; + return; + case MixBlend.First: + constraint.spacing += (constraint.data.spacing - constraint.spacing) * alpha; + return; + } + return; + } + + float spacing = GetCurveValue(time); + if (blend == MixBlend.Setup) + constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha; + else + constraint.spacing += (spacing - constraint.spacing) * alpha; + } + } + + /// Changes a transform constraint's , , and + /// . + public class PathConstraintMixTimeline : CurveTimeline { + public const int ENTRIES = 4; + private const int ROTATE = 1, X = 2, Y = 3; + + readonly int pathConstraintIndex; + + public PathConstraintMixTimeline (int frameCount, int bezierCount, int pathConstraintIndex) + : base(frameCount, bezierCount, (int)Property.PathConstraintMix + "|" + pathConstraintIndex) { + this.pathConstraintIndex = pathConstraintIndex; + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + /// The index of the path constraint slot in that will be changed when this timeline + /// is applied. + public int PathConstraintIndex { + get { + return pathConstraintIndex; + } + } + + /// Sets the time and color for the specified frame. + /// Between 0 and frameCount, inclusive. + /// The frame time in seconds. + public void SetFrame (int frame, float time, float mixRotate, float mixX, float mixY) { + frame <<= 2; + frames[frame] = time; + frames[frame + ROTATE] = mixRotate; + frames[frame + X] = mixX; + frames[frame + Y] = mixY; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; + if (!constraint.active) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + constraint.mixRotate = constraint.data.mixRotate; + constraint.mixX = constraint.data.mixX; + constraint.mixY = constraint.data.mixY; + return; + case MixBlend.First: + constraint.mixRotate += (constraint.data.mixRotate - constraint.mixRotate) * alpha; + constraint.mixX += (constraint.data.mixX - constraint.mixX) * alpha; + constraint.mixY += (constraint.data.mixY - constraint.mixY) * alpha; + return; + } + return; + } + + float rotate, x, y; + int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 2]; + switch (curveType) { + case LINEAR: + float before = frames[i]; + rotate = frames[i + ROTATE]; + x = frames[i + X]; + y = frames[i + Y]; + float t = (time - before) / (frames[i + ENTRIES] - before); + rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t; + x += (frames[i + ENTRIES + X] - x) * t; + y += (frames[i + ENTRIES + Y] - y) * t; + break; + case STEPPED: + rotate = frames[i + ROTATE]; + x = frames[i + X]; + y = frames[i + Y]; + break; + default: + rotate = GetBezierValue(time, i, ROTATE, curveType - BEZIER); + x = GetBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER); + y = GetBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER); + break; + } + + if (blend == MixBlend.Setup) { + PathConstraintData data = constraint.data; + constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha; + constraint.mixX = data.mixX + (x - data.mixX) * alpha; + constraint.mixY = data.mixY + (y - data.mixY) * alpha; + } else { + constraint.mixRotate += (rotate - constraint.mixRotate) * alpha; + constraint.mixX += (x - constraint.mixX) * alpha; + constraint.mixY += (y - constraint.mixY) * alpha; + } + } + } + + /// Changes a slot's for an attachment's . + public class SequenceTimeline : Timeline, ISlotTimeline { + public const int ENTRIES = 3; + private const int MODE = 1, DELAY = 2; + + readonly int slotIndex; + readonly IHasTextureRegion attachment; + + public SequenceTimeline (int frameCount, int slotIndex, Attachment attachment) + : base(frameCount, (int)Property.Sequence + "|" + slotIndex + "|" + ((IHasTextureRegion)attachment).Sequence.Id) { + this.slotIndex = slotIndex; + this.attachment = (IHasTextureRegion)attachment; + } + + public override int FrameEntries { + get { return ENTRIES; } + } + + public int SlotIndex { + get { + return slotIndex; + } + } + public Attachment Attachment { + get { + return (Attachment)attachment; + } + } + + /// Sets the time, mode, index, and frame time for the specified frame. + /// Between 0 and frameCount, inclusive. + /// Seconds between frames. + public void SetFrame (int frame, float time, SequenceMode mode, int index, float delay) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + MODE] = (int)mode | (index << 4); + frames[frame + DELAY] = delay; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { + + Slot slot = skeleton.slots.Items[slotIndex]; + if (!slot.bone.active) return; + Attachment slotAttachment = slot.attachment; + if (slotAttachment != attachment) { + VertexAttachment vertexAttachment = slotAttachment as VertexAttachment; + if ((vertexAttachment == null) + || vertexAttachment.TimelineAttachment != attachment) return; + } + Sequence sequence = ((IHasTextureRegion)slotAttachment).Sequence; + if (sequence == null) return; + + float[] frames = this.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) slot.SequenceIndex = -1; + return; + } + + int i = Search(frames, time, ENTRIES); + float before = frames[i]; + int modeAndIndex = (int)frames[i + MODE]; + float delay = frames[i + DELAY]; + + int index = modeAndIndex >> 4, count = sequence.Regions.Length; + SequenceMode mode = (SequenceMode)(modeAndIndex & 0xf); + if (mode != SequenceMode.Hold) { + index += (int)((time - before) / delay + 0.00001f); + switch (mode) { + case SequenceMode.Once: + index = Math.Min(count - 1, index); + break; + case SequenceMode.Loop: + index %= count; + break; + case SequenceMode.Pingpong: { + int n = (count << 1) - 2; + index = n == 0 ? 0 : index % n; + if (index >= count) index = n - index; + break; + } + case SequenceMode.OnceReverse: + index = Math.Max(count - 1 - index, 0); + break; + case SequenceMode.LoopReverse: + index = count - 1 - (index % count); + break; + case SequenceMode.PingpongReverse: { + int n = (count << 1) - 2; + index = n == 0 ? 0 : (index + count - 1) % n; + if (index >= count) index = n - index; + break; + } // end case + } + } + slot.SequenceIndex = index; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/AnimationState.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/AnimationState.cs new file mode 100644 index 0000000..abf7e34 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/AnimationState.cs @@ -0,0 +1,1457 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_1_00 { + + /// + /// + /// Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies + /// multiple animations on top of each other (layering). + /// + /// See Applying Animations in the Spine Runtimes Guide. + /// + public class AnimationState { + internal static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0); + + /// 1) A previously applied timeline has set this property. + /// Result: Mix from the current pose to the timeline pose. + internal const int Subsequent = 0; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry applied after this one does not have a timeline to set this property. + /// Result: Mix from the setup pose to the timeline pose. + internal const int First = 1; + /// 1) A previously applied timeline has set this property.
+ /// 2) The next track entry to be applied does have a timeline to set this property.
+ /// 3) The next track entry after that one does not have a timeline to set this property.
+ /// Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading + /// animations that key the same property. A subsequent timeline will set this property using a mix. + internal const int HoldSubsequent = 2; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry to be applied does have a timeline to set this property. + /// 3) The next track entry after that one does not have a timeline to set this property. + /// Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations + /// that key the same property. A subsequent timeline will set this property using a mix. + internal const int HoldFirst = 3; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry to be applied does have a timeline to set this property. + /// 3) The next track entry after that one does have a timeline to set this property. + /// 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property. + /// Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than + /// 2 track entries in a row have a timeline that sets the same property. + /// Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid + /// "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A + /// (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap to the mixed + /// out position. + internal const int HoldMix = 4; + + internal const int Setup = 1, Current = 2; + + protected AnimationStateData data; + private readonly ExposedList tracks = new ExposedList(); + private readonly ExposedList events = new ExposedList(); + // difference to libgdx reference: delegates are used for event callbacks instead of 'final SnapshotArray listeners'. + internal void OnStart (TrackEntry entry) { if (Start != null) Start(entry); } + internal void OnInterrupt (TrackEntry entry) { if (Interrupt != null) Interrupt(entry); } + internal void OnEnd (TrackEntry entry) { if (End != null) End(entry); } + internal void OnDispose (TrackEntry entry) { if (Dispose != null) Dispose(entry); } + internal void OnComplete (TrackEntry entry) { if (Complete != null) Complete(entry); } + internal void OnEvent (TrackEntry entry, Event e) { if (Event != null) Event(entry, e); } + + public delegate void TrackEntryDelegate (TrackEntry trackEntry); + /// See + /// API Reference documentation pages here for details. Usage in C# and spine-unity is explained + /// here + /// on the spine-unity documentation pages. + public event TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; + + public delegate void TrackEntryEventDelegate (TrackEntry trackEntry, Event e); + public event TrackEntryEventDelegate Event; + + public void AssignEventSubscribersFrom (AnimationState src) { + Event = src.Event; + Start = src.Start; + Interrupt = src.Interrupt; + End = src.End; + Dispose = src.Dispose; + Complete = src.Complete; + } + + public void AddEventSubscribersFrom (AnimationState src) { + Event += src.Event; + Start += src.Start; + Interrupt += src.Interrupt; + End += src.End; + Dispose += src.Dispose; + Complete += src.Complete; + } + + // end of difference + private readonly EventQueue queue; // Initialized by constructor. + private readonly HashSet propertyIds = new HashSet(); + private bool animationsChanged; + private float timeScale = 1; + private int unkeyedState; + + private readonly Pool trackEntryPool = new Pool(); + + public AnimationState (AnimationStateData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = data; + this.queue = new EventQueue( + this, + delegate { this.animationsChanged = true; }, + trackEntryPool + ); + } + + /// + /// Increments the track entry , setting queued animations as current if needed. + /// delta time + public void Update (float delta) { + delta *= timeScale; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current == null) continue; + + current.animationLast = current.nextAnimationLast; + current.trackLast = current.nextTrackLast; + + float currentDelta = delta * current.timeScale; + + if (current.delay > 0) { + current.delay -= currentDelta; + if (current.delay > 0) continue; + currentDelta = -current.delay; + current.delay = 0; + } + + TrackEntry next = current.next; + if (next != null) { + // When the next entry's delay is passed, change to the next entry, preserving leftover time. + float nextTime = current.trackLast - next.delay; + if (nextTime >= 0) { + next.delay = 0; + next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale; + current.trackTime += currentDelta; + SetCurrent(i, next, true); + while (next.mixingFrom != null) { + next.mixTime += delta; + next = next.mixingFrom; + } + continue; + } + } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { + // Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom. + tracksItems[i] = null; + queue.End(current); + ClearNext(current); + continue; + } + if (current.mixingFrom != null && UpdateMixingFrom(current, delta)) { + // End mixing from entries once all have completed. + TrackEntry from = current.mixingFrom; + current.mixingFrom = null; + if (from != null) from.mixingTo = null; + while (from != null) { + queue.End(from); + from = from.mixingFrom; + } + } + + current.trackTime += currentDelta; + } + + queue.Drain(); + } + + /// Returns true when all mixing from entries are complete. + private bool UpdateMixingFrom (TrackEntry to, float delta) { + TrackEntry from = to.mixingFrom; + if (from == null) return true; + + bool finished = UpdateMixingFrom(from, delta); + + from.animationLast = from.nextAnimationLast; + from.trackLast = from.nextTrackLast; + + // Require mixTime > 0 to ensure the mixing from entry was applied at least once. + if (to.mixTime > 0 && to.mixTime >= to.mixDuration) { + // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame). + if (from.totalAlpha == 0 || to.mixDuration == 0) { + to.mixingFrom = from.mixingFrom; + if (from.mixingFrom != null) from.mixingFrom.mixingTo = to; + to.interruptAlpha = from.interruptAlpha; + queue.End(from); + } + return finished; + } + + from.trackTime += delta * from.timeScale; + to.mixTime += delta; + return false; + } + + /// + /// Poses the skeleton using the track entry animations. The animation state is not changed, so can be applied to multiple + /// skeletons to pose them identically. + /// True if any animations were applied. + public bool Apply (Skeleton skeleton) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + if (animationsChanged) AnimationsChanged(); + + ExposedList events = this.events; + bool applied = false; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current == null || current.delay > 0) continue; + applied = true; + + // Track 0 animations aren't for layering, so do not show the previously applied animations before the first key. + MixBlend blend = i == 0 ? MixBlend.First : current.mixBlend; + + // Apply mixing from entries first. + float mix = current.alpha; + if (current.mixingFrom != null) + mix *= ApplyMixingFrom(current, skeleton, blend); + else if (current.trackTime >= current.trackEnd && current.next == null) // + mix = 0; // Set to setup pose the last time the entry will be applied. + + // Apply current entry. + float animationLast = current.animationLast, animationTime = current.AnimationTime, applyTime = animationTime; + ExposedList applyEvents = events; + if (current.reverse) { + applyTime = current.animation.duration - applyTime; + applyEvents = null; + } + + int timelineCount = current.animation.timelines.Count; + Timeline[] timelines = current.animation.timelines.Items; + if ((i == 0 && mix == 1) || blend == MixBlend.Add) { + for (int ii = 0; ii < timelineCount; ii++) { + Timeline timeline = timelines[ii]; + if (timeline is AttachmentTimeline) + ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true); + else + timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.In); + } + } else { + int[] timelineMode = current.timelineMode.Items; + + bool shortestRotation = current.shortestRotation; + bool firstFrame = !shortestRotation && current.timelinesRotation.Count != timelineCount << 1; + if (firstFrame) current.timelinesRotation.Resize(timelineCount << 1); + float[] timelinesRotation = current.timelinesRotation.Items; + + for (int ii = 0; ii < timelineCount; ii++) { + Timeline timeline = timelines[ii]; + MixBlend timelineBlend = timelineMode[ii] == AnimationState.Subsequent ? blend : MixBlend.Setup; + var rotateTimeline = timeline as RotateTimeline; + if (!shortestRotation && rotateTimeline != null) + ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, mix, timelineBlend, timelinesRotation, + ii << 1, firstFrame); + else if (timeline is AttachmentTimeline) + ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true); + else + timeline.Apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.In); + } + } + QueueEvents(current, animationTime); + events.Clear(false); + current.nextAnimationLast = animationTime; + current.nextTrackLast = current.trackTime; + } + + // Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so + // subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or + // the time is before the first key). + int setupState = unkeyedState + Setup; + Slot[] slots = skeleton.slots.Items; + for (int i = 0, n = skeleton.slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.attachmentState == setupState) { + string attachmentName = slot.data.attachmentName; + slot.Attachment = (attachmentName == null ? null : skeleton.GetAttachment(slot.data.index, attachmentName)); + } + } + unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot. + + queue.Drain(); + return applied; + } + + /// Version of only applying and updating time at + /// EventTimelines for lightweight off-screen updates. + /// When set to false, only animation times of TrackEntries are updated. + // Note: This method is not part of the libgdx reference implementation. + public bool ApplyEventTimelinesOnly (Skeleton skeleton, bool issueEvents = true) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + + ExposedList events = this.events; + bool applied = false; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current == null || current.delay > 0) continue; + applied = true; + + // Apply mixing from entries first. + if (current.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(current, skeleton, issueEvents); + + // Apply current entry. + float animationLast = current.animationLast, animationTime = current.AnimationTime; + + if (issueEvents) { + int timelineCount = current.animation.timelines.Count; + Timeline[] timelines = current.animation.timelines.Items; + for (int ii = 0; ii < timelineCount; ii++) { + Timeline timeline = timelines[ii]; + if (timeline is EventTimeline) + timeline.Apply(skeleton, animationLast, animationTime, events, 1.0f, MixBlend.Setup, MixDirection.In); + } + QueueEvents(current, animationTime); + events.Clear(false); + } + current.nextAnimationLast = animationTime; + current.nextTrackLast = current.trackTime; + } + + if (issueEvents) + queue.Drain(); + return applied; + } + + private float ApplyMixingFrom (TrackEntry to, Skeleton skeleton, MixBlend blend) { + TrackEntry from = to.mixingFrom; + if (from.mixingFrom != null) ApplyMixingFrom(from, skeleton, blend); + + float mix; + if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes. + mix = 1; + if (blend == MixBlend.First) blend = MixBlend.Setup; // Tracks > 0 are transparent and can't reset to setup pose. + } else { + mix = to.mixTime / to.mixDuration; + if (mix > 1) mix = 1; + if (blend != MixBlend.First) blend = from.mixBlend; // Track 0 ignores track mix blend. + } + + bool attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; + int timelineCount = from.animation.timelines.Count; + Timeline[] timelines = from.animation.timelines.Items; + float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix); + float animationLast = from.animationLast, animationTime = from.AnimationTime, applyTime = animationTime; + ExposedList events = null; + if (from.reverse) + applyTime = from.animation.duration - applyTime; + else { + if (mix < from.eventThreshold) events = this.events; + } + + if (blend == MixBlend.Add) { + for (int i = 0; i < timelineCount; i++) + timelines[i].Apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.Out); + } else { + int[] timelineMode = from.timelineMode.Items; + TrackEntry[] timelineHoldMix = from.timelineHoldMix.Items; + + bool shortestRotation = from.shortestRotation; + bool firstFrame = !shortestRotation && from.timelinesRotation.Count != timelineCount << 1; + if (firstFrame) from.timelinesRotation.Resize(timelineCount << 1); + float[] timelinesRotation = from.timelinesRotation.Items; + + from.totalAlpha = 0; + for (int i = 0; i < timelineCount; i++) { + Timeline timeline = timelines[i]; + MixDirection direction = MixDirection.Out; + MixBlend timelineBlend; + float alpha; + switch (timelineMode[i]) { + case AnimationState.Subsequent: + if (!drawOrder && timeline is DrawOrderTimeline) continue; + timelineBlend = blend; + alpha = alphaMix; + break; + case AnimationState.First: + timelineBlend = MixBlend.Setup; + alpha = alphaMix; + break; + case AnimationState.HoldSubsequent: + timelineBlend = blend; + alpha = alphaHold; + break; + case AnimationState.HoldFirst: + timelineBlend = MixBlend.Setup; + alpha = alphaHold; + break; + default: // HoldMix + timelineBlend = MixBlend.Setup; + TrackEntry holdMix = timelineHoldMix[i]; + alpha = alphaHold * Math.Max(0, 1 - holdMix.mixTime / holdMix.mixDuration); + break; + } + from.totalAlpha += alpha; + var rotateTimeline = timeline as RotateTimeline; + if (!shortestRotation && rotateTimeline != null) { + ApplyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, i << 1, + firstFrame); + } else if (timeline is AttachmentTimeline) { + ApplyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, timelineBlend, attachments); + } else { + if (drawOrder && timeline is DrawOrderTimeline && timelineBlend == MixBlend.Setup) + direction = MixDirection.In; + timeline.Apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction); + } + } + } + + if (to.mixDuration > 0) QueueEvents(from, animationTime); + this.events.Clear(false); + from.nextAnimationLast = animationTime; + from.nextTrackLast = from.trackTime; + + return mix; + } + + /// Version of only applying and updating time at + /// EventTimelines for lightweight off-screen updates. + /// When set to false, only animation times of TrackEntries are updated. + // Note: This method is not part of the libgdx reference implementation. + private float ApplyMixingFromEventTimelinesOnly (TrackEntry to, Skeleton skeleton, bool issueEvents) { + TrackEntry from = to.mixingFrom; + if (from.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(from, skeleton, issueEvents); + + + float mix; + if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes. + mix = 1; + } else { + mix = to.mixTime / to.mixDuration; + if (mix > 1) mix = 1; + } + + ExposedList eventBuffer = mix < from.eventThreshold ? this.events : null; + if (eventBuffer == null) return mix; + + float animationLast = from.animationLast, animationTime = from.AnimationTime; + if (issueEvents) { + int timelineCount = from.animation.timelines.Count; + Timeline[] timelines = from.animation.timelines.Items; + for (int i = 0; i < timelineCount; i++) { + Timeline timeline = timelines[i]; + if (timeline is EventTimeline) + timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, 0, MixBlend.Setup, MixDirection.Out); + } + + if (to.mixDuration > 0) QueueEvents(from, animationTime); + this.events.Clear(false); + } + from.nextAnimationLast = animationTime; + from.nextTrackLast = from.trackTime; + + return mix; + } + + /// Applies the attachment timeline and sets . + /// False when: 1) the attachment timeline is mixing out, 2) mix < attachmentThreshold, and 3) the timeline + /// is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent + /// timelines see any deform. + private void ApplyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend, + bool attachments) { + + Slot slot = skeleton.slots.Items[timeline.SlotIndex]; + if (!slot.bone.active) return; + + float[] frames = timeline.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.Setup || blend == MixBlend.First) + SetAttachment(skeleton, slot, slot.data.attachmentName, attachments); + } else + SetAttachment(skeleton, slot, timeline.AttachmentNames[Timeline.Search(frames, time)], attachments); + + // If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later. + if (slot.attachmentState <= unkeyedState) slot.attachmentState = unkeyedState + Setup; + } + + private void SetAttachment (Skeleton skeleton, Slot slot, String attachmentName, bool attachments) { + slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slot.data.index, attachmentName); + if (attachments) slot.attachmentState = unkeyedState + Current; + } + + /// + /// Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest + /// the first time the mixing was applied. + static private void ApplyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, MixBlend blend, + float[] timelinesRotation, int i, bool firstFrame) { + + if (firstFrame) timelinesRotation[i] = 0; + + if (alpha == 1) { + timeline.Apply(skeleton, 0, time, null, 1, blend, MixDirection.In); + return; + } + + Bone bone = skeleton.bones.Items[timeline.BoneIndex]; + if (!bone.active) return; + + float[] frames = timeline.frames; + float r1, r2; + if (time < frames[0]) { // Time is before first frame. + switch (blend) { + case MixBlend.Setup: + bone.rotation = bone.data.rotation; + goto default; // Fall through. + default: + return; + case MixBlend.First: + r1 = bone.rotation; + r2 = bone.data.rotation; + break; + } + } else { + r1 = blend == MixBlend.Setup ? bone.data.rotation : bone.rotation; + r2 = bone.data.rotation + timeline.GetCurveValue(time); + } + + // Mix between rotations using the direction of the shortest route on the first frame. + float total, diff = r2 - r1; + diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360; + if (diff == 0) { + total = timelinesRotation[i]; + } else { + float lastTotal, lastDiff; + if (firstFrame) { + lastTotal = 0; + lastDiff = diff; + } else { + lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops. + lastDiff = timelinesRotation[i + 1]; // Difference between bones. + } + bool current = diff > 0, dir = lastTotal >= 0; + // Detect cross at 0 (not 180). + if (Math.Sign(lastDiff) != Math.Sign(diff) && Math.Abs(lastDiff) <= 90) { + // A cross after a 360 rotation is a loop. + if (Math.Abs(lastTotal) > 180) lastTotal += 360 * Math.Sign(lastTotal); + dir = current; + } + total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal. + if (dir != current) total += 360 * Math.Sign(lastTotal); + timelinesRotation[i] = total; + } + timelinesRotation[i + 1] = diff; + bone.rotation = r1 + total * alpha; + } + + private void QueueEvents (TrackEntry entry, float animationTime) { + float animationStart = entry.animationStart, animationEnd = entry.animationEnd; + float duration = animationEnd - animationStart; + float trackLastWrapped = entry.trackLast % duration; + + // Queue events before complete. + Event[] eventsItems = this.events.Items; + int i = 0, n = events.Count; + for (; i < n; i++) { + Event e = eventsItems[i]; + if (e.time < trackLastWrapped) break; + if (e.time > animationEnd) continue; // Discard events outside animation start/end. + queue.Event(entry, e); + } + + // Queue complete if completed a loop iteration or the animation. + bool complete = false; + if (entry.loop) + complete = duration == 0 || (trackLastWrapped > entry.trackTime % duration); + else + complete = animationTime >= animationEnd && entry.animationLast < animationEnd; + if (complete) queue.Complete(entry); + + // Queue events after complete. + for (; i < n; i++) { + Event e = eventsItems[i]; + if (e.time < animationStart) continue; // Discard events outside animation start/end. + queue.Event(entry, eventsItems[i]); + } + } + + /// + /// Removes all animations from all tracks, leaving skeletons in their current pose. + /// + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their current pose. + /// + public void ClearTracks () { + bool oldDrainDisabled = queue.drainDisabled; + queue.drainDisabled = true; + for (int i = 0, n = tracks.Count; i < n; i++) { + ClearTrack(i); + } + tracks.Clear(); + queue.drainDisabled = oldDrainDisabled; + queue.Drain(); + } + + /// + /// Removes all animations from the track, leaving skeletons in their current pose. + /// + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their current pose. + /// + public void ClearTrack (int trackIndex) { + if (trackIndex >= tracks.Count) return; + TrackEntry current = tracks.Items[trackIndex]; + if (current == null) return; + + queue.End(current); + + ClearNext(current); + + TrackEntry entry = current; + while (true) { + TrackEntry from = entry.mixingFrom; + if (from == null) break; + queue.End(from); + entry.mixingFrom = null; + entry.mixingTo = null; + entry = from; + } + + tracks.Items[current.trackIndex] = null; + + queue.Drain(); + } + + /// Sets the active TrackEntry for a given track number. + private void SetCurrent (int index, TrackEntry current, bool interrupt) { + TrackEntry from = ExpandToIndex(index); + tracks.Items[index] = current; + current.previous = null; + + if (from != null) { + if (interrupt) queue.Interrupt(from); + current.mixingFrom = from; + from.mixingTo = current; + current.mixTime = 0; + + // Store the interrupted mix percentage. + if (from.mixingFrom != null && from.mixDuration > 0) + current.interruptAlpha *= Math.Min(1, from.mixTime / from.mixDuration); + + from.timelinesRotation.Clear(); // Reset rotation for mixing out, in case entry was mixed in. + } + + queue.Start(current); // triggers AnimationsChanged + } + + /// Sets an animation by name. + public TrackEntry SetAnimation (int trackIndex, string animationName, bool loop) { + Animation animation = data.skeletonData.FindAnimation(animationName); + if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName"); + return SetAnimation(trackIndex, animation, loop); + } + + /// Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never + /// applied to a skeleton, it is replaced (not mixed from). + /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + /// duration. In either case determines when the track is cleared. + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) { + if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); + bool interrupt = true; + TrackEntry current = ExpandToIndex(trackIndex); + if (current != null) { + if (current.nextTrackLast == -1) { + // Don't mix from an entry that was never applied. + tracks.Items[trackIndex] = current.mixingFrom; + queue.Interrupt(current); + queue.End(current); + ClearNext(current); + current = current.mixingFrom; + interrupt = false; // mixingFrom is current again, but don't interrupt it twice. + } else + ClearNext(current); + } + TrackEntry entry = NewTrackEntry(trackIndex, animation, loop, current); + SetCurrent(trackIndex, entry, interrupt); + queue.Drain(); + return entry; + } + + /// Queues an animation by name. + /// + public TrackEntry AddAnimation (int trackIndex, string animationName, bool loop, float delay) { + Animation animation = data.skeletonData.FindAnimation(animationName); + if (animation == null) throw new ArgumentException("Animation not found: " + animationName, "animationName"); + return AddAnimation(trackIndex, animation, loop, delay); + } + + /// Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is + /// equivalent to calling . + /// + /// If > 0, sets . If <= 0, the delay set is the duration of the previous track entry + /// minus any mix duration (from the plus the specified Delay (ie the mix + /// ends at (Delay = 0) or before (Delay < 0) the previous track entry duration). If the + /// previous entry is looping, its next loop completion is used instead of its duration. + /// + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) { + if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); + + TrackEntry last = ExpandToIndex(trackIndex); + if (last != null) { + while (last.next != null) + last = last.next; + } + + TrackEntry entry = NewTrackEntry(trackIndex, animation, loop, last); + + if (last == null) { + SetCurrent(trackIndex, entry, true); + queue.Drain(); + } else { + last.next = entry; + entry.previous = last; + if (delay <= 0) delay += last.TrackComplete - entry.mixDuration; + } + + entry.delay = delay; + return entry; + } + + /// + /// Sets an empty animation for a track, discarding any queued animations, and sets the track entry's + /// . An empty animation has no timelines and serves as a placeholder for mixing in or out. + /// + /// Mixing out is done by setting an empty animation with a mix duration using either , + /// , or . Mixing to an empty animation causes + /// the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation + /// transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of + /// 0 still mixes out over one frame. + /// + /// Mixing in is done by first setting an empty animation, then adding an animation using + /// with the desired delay (an empty animation has a duration of 0) and on + /// the returned track entry, set the . Mixing from an empty animation causes the new + /// animation to be applied more and more over the mix duration. Properties keyed in the new animation transition from the value + /// from lower tracks or from the setup pose value if no lower tracks key the property to the value keyed in the new + /// animation. + public TrackEntry SetEmptyAnimation (int trackIndex, float mixDuration) { + TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false); + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /// + /// Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's + /// . If the track is empty, it is equivalent to calling + /// . + /// + /// Track number. + /// Mix duration. + /// If > 0, sets . If <= 0, the delay set is the duration of the previous track entry + /// minus any mix duration plus the specified Delay (ie the mix ends at (Delay = 0) or + /// before (Delay < 0) the previous track entry duration). If the previous entry is looping, its next + /// loop completion is used instead of its duration. + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + /// + public TrackEntry AddEmptyAnimation (int trackIndex, float mixDuration, float delay) { + TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay); + if (delay <= 0) entry.delay += entry.mixDuration - mixDuration; + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /// + /// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix + /// duration. + public void SetEmptyAnimations (float mixDuration) { + bool oldDrainDisabled = queue.drainDisabled; + queue.drainDisabled = true; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry current = tracksItems[i]; + if (current != null) SetEmptyAnimation(current.trackIndex, mixDuration); + } + queue.drainDisabled = oldDrainDisabled; + queue.Drain(); + } + + private TrackEntry ExpandToIndex (int index) { + if (index < tracks.Count) return tracks.Items[index]; + tracks.Resize(index + 1); + return null; + } + + /// Object-pooling version of new TrackEntry. Obtain an unused TrackEntry from the pool and clear/initialize its values. + /// May be null. + private TrackEntry NewTrackEntry (int trackIndex, Animation animation, bool loop, TrackEntry last) { + TrackEntry entry = trackEntryPool.Obtain(); + entry.trackIndex = trackIndex; + entry.animation = animation; + entry.loop = loop; + entry.holdPrevious = false; + + entry.eventThreshold = 0; + entry.attachmentThreshold = 0; + entry.drawOrderThreshold = 0; + + entry.animationStart = 0; + entry.animationEnd = animation.Duration; + entry.animationLast = -1; + entry.nextAnimationLast = -1; + + entry.delay = 0; + entry.trackTime = 0; + entry.trackLast = -1; + entry.nextTrackLast = -1; + entry.trackEnd = float.MaxValue; + entry.timeScale = 1; + + entry.alpha = 1; + entry.interruptAlpha = 1; + entry.mixTime = 0; + entry.mixDuration = last == null ? 0 : data.GetMix(last.animation, animation); + entry.mixBlend = MixBlend.Replace; + return entry; + } + + /// Removes the next entry and all entries after it for the specified entry. + public void ClearNext (TrackEntry entry) { + TrackEntry next = entry.next; + while (next != null) { + queue.Dispose(next); + next = next.next; + } + entry.next = null; + } + + private void AnimationsChanged () { + animationsChanged = false; + + // Process in the order that animations are applied. + propertyIds.Clear(); + int n = tracks.Count; + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0; i < n; i++) { + TrackEntry entry = tracksItems[i]; + if (entry == null) continue; + while (entry.mixingFrom != null) // Move to last entry, then iterate in reverse. + entry = entry.mixingFrom; + do { + if (entry.mixingTo == null || entry.mixBlend != MixBlend.Add) ComputeHold(entry); + entry = entry.mixingTo; + } while (entry != null); + } + } + + private void ComputeHold (TrackEntry entry) { + TrackEntry to = entry.mixingTo; + Timeline[] timelines = entry.animation.timelines.Items; + int timelinesCount = entry.animation.timelines.Count; + int[] timelineMode = entry.timelineMode.Resize(timelinesCount).Items; + entry.timelineHoldMix.Clear(); + TrackEntry[] timelineHoldMix = entry.timelineHoldMix.Resize(timelinesCount).Items; + HashSet propertyIds = this.propertyIds; + + if (to != null && to.holdPrevious) { + for (int i = 0; i < timelinesCount; i++) + timelineMode[i] = propertyIds.AddAll(timelines[i].PropertyIds) ? AnimationState.HoldFirst : AnimationState.HoldSubsequent; + + return; + } + + // outer: + for (int i = 0; i < timelinesCount; i++) { + Timeline timeline = timelines[i]; + String[] ids = timeline.PropertyIds; + if (!propertyIds.AddAll(ids)) + timelineMode[i] = AnimationState.Subsequent; + else if (to == null || timeline is AttachmentTimeline || timeline is DrawOrderTimeline + || timeline is EventTimeline || !to.animation.HasTimeline(ids)) { + timelineMode[i] = AnimationState.First; + } else { + for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) { + if (next.animation.HasTimeline(ids)) continue; + if (next.mixDuration > 0) { + timelineMode[i] = AnimationState.HoldMix; + timelineHoldMix[i] = next; + goto continue_outer; // continue outer; + } + break; + } + timelineMode[i] = AnimationState.HoldFirst; + } + continue_outer: { } + } + } + + /// The track entry for the animation currently playing on the track, or null if no animation is currently playing. + public TrackEntry GetCurrent (int trackIndex) { + if (trackIndex >= tracks.Count) return null; + return tracks.Items[trackIndex]; + } + + /// Discards all listener notifications that have not yet been delivered. This can be useful to call from an + /// AnimationState event subscriber when it is known that further notifications that may have been already queued for delivery + /// are not wanted because new animations are being set. + public void ClearListenerNotifications () { + queue.Clear(); + } + + /// + /// Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower + /// or faster. Defaults to 1. + /// + /// See TrackEntry for affecting a single animation. + /// + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + /// The AnimationStateData to look up mix durations. + public AnimationStateData Data { + get { + return data; + } + set { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = value; + } + } + + /// A list of tracks that have animations, which may contain nulls. + public ExposedList Tracks { get { return tracks; } } + + override public string ToString () { + var buffer = new System.Text.StringBuilder(); + TrackEntry[] tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry entry = tracksItems[i]; + if (entry == null) continue; + if (buffer.Length > 0) buffer.Append(", "); + buffer.Append(entry.ToString()); + } + if (buffer.Length == 0) return ""; + return buffer.ToString(); + } + } + + /// + /// + /// Stores settings and other state for the playback of an animation on an track. + /// + /// References to a track entry must not be kept after the event occurs. + /// + public class TrackEntry : Pool.IPoolable { + internal Animation animation; + + internal TrackEntry previous, next, mixingFrom, mixingTo; + // difference to libgdx reference: delegates are used for event callbacks instead of 'AnimationStateListener listener'. + /// See + /// API Reference documentation pages here for details. Usage in C# and spine-unity is explained + /// here + /// on the spine-unity documentation pages. + public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; + public event AnimationState.TrackEntryEventDelegate Event; + internal void OnStart () { if (Start != null) Start(this); } + internal void OnInterrupt () { if (Interrupt != null) Interrupt(this); } + internal void OnEnd () { if (End != null) End(this); } + internal void OnDispose () { if (Dispose != null) Dispose(this); } + internal void OnComplete () { if (Complete != null) Complete(this); } + internal void OnEvent (Event e) { if (Event != null) Event(this, e); } + + internal int trackIndex; + + internal bool loop, holdPrevious, reverse, shortestRotation; + internal float eventThreshold, attachmentThreshold, drawOrderThreshold; + internal float animationStart, animationEnd, animationLast, nextAnimationLast; + internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f; + internal float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha; + internal MixBlend mixBlend = MixBlend.Replace; + internal readonly ExposedList timelineMode = new ExposedList(); + internal readonly ExposedList timelineHoldMix = new ExposedList(); + internal readonly ExposedList timelinesRotation = new ExposedList(); + + // IPoolable.Reset() + public void Reset () { + previous = null; + next = null; + mixingFrom = null; + mixingTo = null; + animation = null; + // replaces 'listener = null;' since delegates are used for event callbacks + Start = null; + Interrupt = null; + End = null; + Dispose = null; + Complete = null; + Event = null; + timelineMode.Clear(); + timelineHoldMix.Clear(); + timelinesRotation.Clear(); + } + + /// The index of the track where this entry is either current or queued. + /// + public int TrackIndex { get { return trackIndex; } } + + /// The animation to apply for this track entry. + public Animation Animation { get { return animation; } } + + /// + /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + /// duration. + public bool Loop { get { return loop; } set { loop = value; } } + + /// + /// + /// Seconds to postpone playing the animation. When this track entry is the current track entry, Delay + /// postpones incrementing the . When this track entry is queued, Delay is the time from + /// the start of the previous animation to when this track entry will become the current track entry (ie when the previous + /// track entry >= this track entry's Delay). + /// + /// affects the delay. + /// + /// When using with a delay <= 0, the delay + /// is set using the mix duration from the . If is set afterward, the delay + /// may need to be adjusted. + public float Delay { get { return delay; } set { delay = value; } } + + /// + /// Current time in seconds this track entry has been the current track entry. The track time determines + /// . The track time can be set to start the animation at a time other than 0, without affecting + /// looping. + public float TrackTime { get { return trackTime; } set { trackTime = value; } } + + /// + /// + /// The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float + /// value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time + /// is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the + /// properties keyed by the animation are set to the setup pose and the track is cleared. + /// + /// It may be desired to use rather than have the animation + /// abruptly cease being applied. + /// + public float TrackEnd { get { return trackEnd; } set { trackEnd = value; } } + + /// + /// If this track entry is non-looping, the track time in seconds when is reached, or the current + /// if it has already been reached. If this track entry is looping, the track time when this + /// animation will reach its next (the next loop completion). + public float TrackComplete { + get { + float duration = animationEnd - animationStart; + if (duration != 0) { + if (loop) return duration * (1 + (int)(trackTime / duration)); // Completion of next loop. + if (trackTime < duration) return duration; // Before duration. + } + return trackTime; // Next update. + } + } + + /// + /// + /// Seconds when this animation starts, both initially and after looping. Defaults to 0. + /// + /// When changing the AnimationStart time, it often makes sense to set to the same + /// value to prevent timeline keys before the start time from triggering. + /// + public float AnimationStart { get { return animationStart; } set { animationStart = value; } } + + /// + /// Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will + /// loop back to at this time. Defaults to the animation . + /// + public float AnimationEnd { get { return animationEnd; } set { animationEnd = value; } } + + /// + /// The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this + /// animation is applied, event timelines will fire all events between the AnimationLast time (exclusive) and + /// AnimationTime (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation + /// is applied. + public float AnimationLast { + get { return animationLast; } + set { + animationLast = value; + nextAnimationLast = value; + } + } + + /// + /// Uses to compute the AnimationTime. When the TrackTime is 0, the + /// AnimationTime is equal to the AnimationStart time. + /// + /// The animationTime is between and , except if this + /// track entry is non-looping and is >= to the animation , then + /// animationTime continues to increase past . + /// + public float AnimationTime { + get { + if (loop) { + float duration = animationEnd - animationStart; + if (duration == 0) return animationStart; + return (trackTime % duration) + animationStart; + } + float animationTime = trackTime + animationStart; + return animationEnd >= animation.duration ? animationTime : Math.Min(animationTime, animationEnd); + } + } + + /// + /// + /// Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or + /// faster. Defaults to 1. + /// + /// Values < 0 are not supported. To play an animation in reverse, use . + /// + /// is not affected by track entry time scale, so may need to be adjusted to + /// match the animation speed. + /// + /// When using with a Delay <= 0, the + /// is set using the mix duration from the , assuming time scale to be 1. If + /// the time scale is not 1, the delay may need to be adjusted. + /// + /// See AnimationState for affecting all animations. + /// + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + /// + /// + /// Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults + /// to 1, which overwrites the skeleton's current pose with this animation. + /// + /// Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to + /// use alpha on track 0 if the skeleton pose is from the last frame render. + /// + public float Alpha { get { return alpha; } set { alpha = value; } } + + public float InterruptAlpha { get { return interruptAlpha; } } + + /// + /// When the mix percentage ( / ) is less than the + /// EventThreshold, event timelines are applied while this animation is being mixed out. Defaults to 0, so event + /// timelines are not applied while this animation is being mixed out. + /// + public float EventThreshold { get { return eventThreshold; } set { eventThreshold = value; } } + + /// + /// When the mix percentage ( / ) is less than the + /// AttachmentThreshold, attachment timelines are applied while this animation is being mixed out. Defaults to + /// 0, so attachment timelines are not applied while this animation is being mixed out. + /// + public float AttachmentThreshold { get { return attachmentThreshold; } set { attachmentThreshold = value; } } + + /// + /// When the mix percentage ( / ) is less than the + /// DrawOrderThreshold, draw order timelines are applied while this animation is being mixed out. Defaults to 0, + /// so draw order timelines are not applied while this animation is being mixed out. + /// + public float DrawOrderThreshold { get { return drawOrderThreshold; } set { drawOrderThreshold = value; } } + + /// + /// The animation queued to start after this animation, or null if there is none. next makes up a doubly linked + /// list. + /// + /// See to truncate the list. + public TrackEntry Next { get { return next; } } + + /// + /// The animation queued to play before this animation, or null. previous makes up a doubly linked list. + public TrackEntry Previous { get { return previous; } } + + /// + /// Returns true if at least one loop has been completed. + /// + public bool IsComplete { + get { return trackTime >= animationEnd - animationStart; } + } + + /// + /// Seconds from 0 to the when mixing from the previous animation to this animation. May be + /// slightly more than MixDuration when the mix is complete. + public float MixTime { get { return mixTime; } set { mixTime = value; } } + + /// + /// + /// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData + /// based on the animation before this animation (if any). + /// + /// The MixDuration can be set manually rather than use the value from + /// . In that case, the MixDuration can be set for a new + /// track entry only before is first called. + /// + /// When using with a Delay <= 0, the + /// is set using the mix duration from the . If mixDuration is set + /// afterward, the delay may need to be adjusted. For example: + /// entry.Delay = entry.previous.TrackComplete - entry.MixDuration; + /// + public float MixDuration { get { return mixDuration; } set { mixDuration = value; } } + + /// + /// + /// Controls how properties keyed in the animation are mixed with lower tracks. Defaults to . + /// + /// Track entries on track 0 ignore this setting and always use . + /// + /// The MixBlend can be set for a new track entry only before is first + /// called. + /// + public MixBlend MixBlend { get { return mixBlend; } set { mixBlend = value; } } + + /// + /// The track entry for the previous animation when mixing from the previous animation to this animation, or null if no + /// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a linked list. + public TrackEntry MixingFrom { get { return mixingFrom; } } + + /// + /// The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is + /// currently occuring. When mixing to multiple animations, MixingTo makes up a linked list. + public TrackEntry MixingTo { get { return mixingTo; } } + + /// + /// + /// If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead + /// of being mixed out. + /// + /// When mixing between animations that key the same property, if a lower track also keys that property then the value will + /// briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0% + /// while the second animation mixes from 0% to 100%. Setting HoldPrevious to true applies the first animation + /// at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which + /// keys the property, only when a higher track also keys the property. + /// + /// Snapping will occur if HoldPrevious is true and this animation does not key all the same properties as the + /// previous animation. + /// + public bool HoldPrevious { get { return holdPrevious; } set { holdPrevious = value; } } + + /// + /// If true, the animation will be applied in reverse. Events are not fired when an animation is applied in reverse. + public bool Reverse { get { return reverse; } set { reverse = value; } } + + /// + /// If true, mixing rotation between tracks always uses the shortest rotation direction. If the rotation is animated, the + /// shortest rotation direction may change during the mix. + /// + /// If false, the shortest rotation direction is remembered when the mix starts and the same direction is used for the rest + /// of the mix. Defaults to false. + public bool ShortestRotation { get { return shortestRotation; } set { shortestRotation = value; } } + + /// Returns true if this entry is for the empty animation. See , + /// , and . + public bool IsEmptyAnimation { get { return animation == AnimationState.EmptyAnimation; } } + + /// + /// + /// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the + /// long way around when using and starting animations on other tracks. + /// + /// Mixing with involves finding a rotation between two others, which has two possible solutions: + /// the short way or the long way around. The two rotations likely change over time, so which direction is the short or long + /// way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the + /// long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. + /// + public void ResetRotationDirections () { + timelinesRotation.Clear(); + } + + override public string ToString () { + return animation == null ? "" : animation.name; + } + + // Note: This method is required by SpineAnimationStateMixerBehaviour, + // which is part of the timeline extension package. Thus the internal member variable + // nextTrackLast is not accessible. We favor providing this method + // over exposing nextTrackLast as public property, which would rather confuse users. + public void AllowImmediateQueue () { + if (nextTrackLast < 0) nextTrackLast = 0; + } + } + + class EventQueue { + private readonly List eventQueueEntries = new List(); + internal bool drainDisabled; + + private readonly AnimationState state; + private readonly Pool trackEntryPool; + internal event Action AnimationsChanged; + + internal EventQueue (AnimationState state, Action HandleAnimationsChanged, Pool trackEntryPool) { + this.state = state; + this.AnimationsChanged += HandleAnimationsChanged; + this.trackEntryPool = trackEntryPool; + } + + internal void Start (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Start, entry)); + if (AnimationsChanged != null) AnimationsChanged(); + } + + internal void Interrupt (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Interrupt, entry)); + } + + internal void End (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.End, entry)); + if (AnimationsChanged != null) AnimationsChanged(); + } + + internal void Dispose (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Dispose, entry)); + } + + internal void Complete (TrackEntry entry) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Complete, entry)); + } + + internal void Event (TrackEntry entry, Event e) { + eventQueueEntries.Add(new EventQueueEntry(EventType.Event, entry, e)); + } + + /// Raises all events in the queue and drains the queue. + internal void Drain () { + if (drainDisabled) return; + drainDisabled = true; + + List eventQueueEntries = this.eventQueueEntries; + AnimationState state = this.state; + + // Don't cache eventQueueEntries.Count so callbacks can queue their own events (eg, call SetAnimation in AnimationState_Complete). + for (int i = 0; i < eventQueueEntries.Count; i++) { + EventQueueEntry queueEntry = eventQueueEntries[i]; + TrackEntry trackEntry = queueEntry.entry; + + switch (queueEntry.type) { + case EventType.Start: + trackEntry.OnStart(); + state.OnStart(trackEntry); + break; + case EventType.Interrupt: + trackEntry.OnInterrupt(); + state.OnInterrupt(trackEntry); + break; + case EventType.End: + trackEntry.OnEnd(); + state.OnEnd(trackEntry); + goto case EventType.Dispose; // Fall through. (C#) + case EventType.Dispose: + trackEntry.OnDispose(); + state.OnDispose(trackEntry); + trackEntryPool.Free(trackEntry); + break; + case EventType.Complete: + trackEntry.OnComplete(); + state.OnComplete(trackEntry); + break; + case EventType.Event: + trackEntry.OnEvent(queueEntry.e); + state.OnEvent(trackEntry, queueEntry.e); + break; + } + } + eventQueueEntries.Clear(); + + drainDisabled = false; + } + + internal void Clear () { + eventQueueEntries.Clear(); + } + + struct EventQueueEntry { + public EventType type; + public TrackEntry entry; + public Event e; + + public EventQueueEntry (EventType eventType, TrackEntry trackEntry, Event e = null) { + this.type = eventType; + this.entry = trackEntry; + this.e = e; + } + } + + enum EventType { + Start, Interrupt, End, Dispose, Complete, Event + } + } + + class Pool where T : class, new() { + public readonly int max; + readonly Stack freeObjects; + + public int Count { get { return freeObjects.Count; } } + public int Peak { get; private set; } + + public Pool (int initialCapacity = 16, int max = int.MaxValue) { + freeObjects = new Stack(initialCapacity); + this.max = max; + } + + public T Obtain () { + return freeObjects.Count == 0 ? new T() : freeObjects.Pop(); + } + + public void Free (T obj) { + if (obj == null) throw new ArgumentNullException("obj", "obj cannot be null"); + if (freeObjects.Count < max) { + freeObjects.Push(obj); + Peak = Math.Max(Peak, freeObjects.Count); + } + Reset(obj); + } + + public void Clear () { + freeObjects.Clear(); + } + + protected void Reset (T obj) { + var poolable = obj as IPoolable; + if (poolable != null) poolable.Reset(); + } + + public interface IPoolable { + void Reset (); + } + } + + public static class HashSetExtensions { + public static bool AddAll (this HashSet set, T[] addSet) { + bool anyItemAdded = false; + for (int i = 0, n = addSet.Length; i < n; ++i) { + T item = addSet[i]; + anyItemAdded |= set.Add(item); + } + return anyItemAdded; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/AnimationStateData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/AnimationStateData.cs new file mode 100644 index 0000000..d125cae --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/AnimationStateData.cs @@ -0,0 +1,114 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_1_00 { + + /// Stores mix (crossfade) durations to be applied when AnimationState animations are changed. + public class AnimationStateData { + internal SkeletonData skeletonData; + readonly Dictionary animationToMixTime = new Dictionary(AnimationPairComparer.Instance); + internal float defaultMix; + + /// The SkeletonData to look up animations when they are specified by name. + public SkeletonData SkeletonData { get { return skeletonData; } } + + /// + /// The mix duration to use when no mix duration has been specifically defined between two animations. + public float DefaultMix { get { return defaultMix; } set { defaultMix = value; } } + + public AnimationStateData (SkeletonData skeletonData) { + if (skeletonData == null) throw new ArgumentException("skeletonData cannot be null.", "skeletonData"); + this.skeletonData = skeletonData; + } + + /// Sets a mix duration by animation names. + public void SetMix (string fromName, string toName, float duration) { + Animation from = skeletonData.FindAnimation(fromName); + if (from == null) throw new ArgumentException("Animation not found: " + fromName, "fromName"); + Animation to = skeletonData.FindAnimation(toName); + if (to == null) throw new ArgumentException("Animation not found: " + toName, "toName"); + SetMix(from, to, duration); + } + + /// Sets a mix duration when changing from the specified animation to the other. + /// See TrackEntry.MixDuration. + public void SetMix (Animation from, Animation to, float duration) { + if (from == null) throw new ArgumentNullException("from", "from cannot be null."); + if (to == null) throw new ArgumentNullException("to", "to cannot be null."); + AnimationPair key = new AnimationPair(from, to); + animationToMixTime.Remove(key); + animationToMixTime.Add(key, duration); + } + + /// + /// The mix duration to use when changing from the specified animation to the other, + /// or the DefaultMix if no mix duration has been set. + /// + public float GetMix (Animation from, Animation to) { + if (from == null) throw new ArgumentNullException("from", "from cannot be null."); + if (to == null) throw new ArgumentNullException("to", "to cannot be null."); + AnimationPair key = new AnimationPair(from, to); + float duration; + if (animationToMixTime.TryGetValue(key, out duration)) return duration; + return defaultMix; + } + + public struct AnimationPair { + public readonly Animation a1; + public readonly Animation a2; + + public AnimationPair (Animation a1, Animation a2) { + this.a1 = a1; + this.a2 = a2; + } + + public override string ToString () { + return a1.name + "->" + a2.name; + } + } + + // Avoids boxing in the dictionary. + public class AnimationPairComparer : IEqualityComparer { + public static readonly AnimationPairComparer Instance = new AnimationPairComparer(); + + bool IEqualityComparer.Equals (AnimationPair x, AnimationPair y) { + return ReferenceEquals(x.a1, y.a1) && ReferenceEquals(x.a2, y.a2); + } + + int IEqualityComparer.GetHashCode (AnimationPair obj) { + // from Tuple.CombineHashCodes // return (((h1 << 5) + h1) ^ h2); + int h1 = obj.a1.GetHashCode(); + return (((h1 << 5) + h1) ^ obj.a2.GetHashCode()); + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Atlas.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Atlas.cs new file mode 100644 index 0000000..78e55ba --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Atlas.cs @@ -0,0 +1,372 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace Spine4_1_00 { + public class Atlas : IEnumerable { + readonly List pages = new List(); + List regions = new List(); + TextureLoader textureLoader; + + #region IEnumerable implementation + public IEnumerator GetEnumerator () { + return regions.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () { + return regions.GetEnumerator(); + } + #endregion + + public List Regions { get { return regions; } } + public List Pages { get { return pages; } } + +#if !(IS_UNITY) +#if WINDOWS_STOREAPP + private async Task ReadFile(string path, TextureLoader textureLoader) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false); + using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) { + try { + Atlas atlas = new Atlas(reader, Path.GetDirectoryName(path), textureLoader); + this.pages = atlas.pages; + this.regions = atlas.regions; + this.textureLoader = atlas.textureLoader; + } catch (Exception ex) { + throw new Exception("Error reading atlas file: " + path, ex); + } + } + } + + public Atlas(string path, TextureLoader textureLoader) { + this.ReadFile(path, textureLoader).Wait(); + } +#else + public Atlas (string path, TextureLoader textureLoader) { +#if WINDOWS_PHONE + Stream stream = Microsoft.Xna.Framework.TitleContainer.OpenStream(path); + using (StreamReader reader = new StreamReader(stream)) { +#else + using (StreamReader reader = new StreamReader(path)) { +#endif // WINDOWS_PHONE + try { + Atlas atlas = new Atlas(reader, Path.GetDirectoryName(path), textureLoader); + this.pages = atlas.pages; + this.regions = atlas.regions; + this.textureLoader = atlas.textureLoader; + } catch (Exception ex) { + throw new Exception("Error reading atlas file: " + path, ex); + } + } + } +#endif // WINDOWS_STOREAPP +#endif + + public Atlas (List pages, List regions) { + if (pages == null) throw new ArgumentNullException("pages", "pages cannot be null."); + if (regions == null) throw new ArgumentNullException("regions", "regions cannot be null."); + this.pages = pages; + this.regions = regions; + this.textureLoader = null; + } + + public Atlas (TextReader reader, string imagesDir, TextureLoader textureLoader) { + if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null."); + if (imagesDir == null) throw new ArgumentNullException("imagesDir", "imagesDir cannot be null."); + if (textureLoader == null) throw new ArgumentNullException("textureLoader", "textureLoader cannot be null."); + this.textureLoader = textureLoader; + + string[] entry = new string[5]; + AtlasPage page = null; + AtlasRegion region = null; + + var pageFields = new Dictionary(5); + pageFields.Add("size", () => { + page.width = int.Parse(entry[1], CultureInfo.InvariantCulture); + page.height = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + pageFields.Add("format", () => { + page.format = (Format)Enum.Parse(typeof(Format), entry[1], false); + }); + pageFields.Add("filter", () => { + page.minFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), entry[1], false); + page.magFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), entry[2], false); + }); + pageFields.Add("repeat", () => { + if (entry[1].IndexOf('x') != -1) page.uWrap = TextureWrap.Repeat; + if (entry[1].IndexOf('y') != -1) page.vWrap = TextureWrap.Repeat; + }); + pageFields.Add("pma", () => { + page.pma = entry[1] == "true"; + }); + + var regionFields = new Dictionary(8); + regionFields.Add("xy", () => { // Deprecated, use bounds. + region.x = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.y = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + regionFields.Add("size", () => { // Deprecated, use bounds. + region.width = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.height = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + regionFields.Add("bounds", () => { + region.x = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.y = int.Parse(entry[2], CultureInfo.InvariantCulture); + region.width = int.Parse(entry[3], CultureInfo.InvariantCulture); + region.height = int.Parse(entry[4], CultureInfo.InvariantCulture); + }); + regionFields.Add("offset", () => { // Deprecated, use offsets. + region.offsetX = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.offsetY = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + regionFields.Add("orig", () => { // Deprecated, use offsets. + region.originalWidth = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.originalHeight = int.Parse(entry[2], CultureInfo.InvariantCulture); + }); + regionFields.Add("offsets", () => { + region.offsetX = int.Parse(entry[1], CultureInfo.InvariantCulture); + region.offsetY = int.Parse(entry[2], CultureInfo.InvariantCulture); + region.originalWidth = int.Parse(entry[3], CultureInfo.InvariantCulture); + region.originalHeight = int.Parse(entry[4], CultureInfo.InvariantCulture); + }); + regionFields.Add("rotate", () => { + string value = entry[1]; + if (value == "true") + region.degrees = 90; + else if (value != "false") + region.degrees = int.Parse(value, CultureInfo.InvariantCulture); + }); + regionFields.Add("index", () => { + region.index = int.Parse(entry[1], CultureInfo.InvariantCulture); + }); + + string line = reader.ReadLine(); + // Ignore empty lines before first entry. + while (line != null && line.Trim().Length == 0) + line = reader.ReadLine(); + // Header entries. + while (true) { + if (line == null || line.Trim().Length == 0) break; + if (ReadEntry(entry, line) == 0) break; // Silently ignore all header fields. + line = reader.ReadLine(); + } + // Page and region entries. + List names = null; + List values = null; + while (true) { + if (line == null) break; + if (line.Trim().Length == 0) { + page = null; + line = reader.ReadLine(); + } else if (page == null) { + page = new AtlasPage(); + page.name = line.Trim(); + while (true) { + if (ReadEntry(entry, line = reader.ReadLine()) == 0) break; + Action field; + if (pageFields.TryGetValue(entry[0], out field)) field(); // Silently ignore unknown page fields. + } + textureLoader.Load(page, Path.Combine(imagesDir, page.name)); + pages.Add(page); + } else { + region = new AtlasRegion(); + region.page = page; + region.name = line; + while (true) { + int count = ReadEntry(entry, line = reader.ReadLine()); + if (count == 0) break; + Action field; + if (regionFields.TryGetValue(entry[0], out field)) + field(); + else { + if (names == null) { + names = new List(8); + values = new List(8); + } + names.Add(entry[0]); + int[] entryValues = new int[count]; + for (int i = 0; i < count; i++) + int.TryParse(entry[i + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out entryValues[i]); // Silently ignore non-integer values. + values.Add(entryValues); + } + } + if (region.originalWidth == 0 && region.originalHeight == 0) { + region.originalWidth = region.width; + region.originalHeight = region.height; + } + if (names != null && names.Count > 0) { + region.names = names.ToArray(); + region.values = values.ToArray(); + names.Clear(); + values.Clear(); + } + region.u = region.x / (float)page.width; + region.v = region.y / (float)page.height; + if (region.degrees == 90) { + region.u2 = (region.x + region.height) / (float)page.width; + region.v2 = (region.y + region.width) / (float)page.height; + + var tempSwap = region.packedWidth; + region.packedWidth = region.packedHeight; + region.packedHeight = tempSwap; + } else { + region.u2 = (region.x + region.width) / (float)page.width; + region.v2 = (region.y + region.height) / (float)page.height; + } + regions.Add(region); + } + } + } + + static private int ReadEntry (string[] entry, string line) { + if (line == null) return 0; + line = line.Trim(); + if (line.Length == 0) return 0; + int colon = line.IndexOf(':'); + if (colon == -1) return 0; + entry[0] = line.Substring(0, colon).Trim(); + for (int i = 1, lastMatch = colon + 1; ; i++) { + int comma = line.IndexOf(',', lastMatch); + if (comma == -1) { + entry[i] = line.Substring(lastMatch).Trim(); + return i; + } + entry[i] = line.Substring(lastMatch, comma - lastMatch).Trim(); + lastMatch = comma + 1; + if (i == 4) return 4; + } + } + + public void FlipV () { + for (int i = 0, n = regions.Count; i < n; i++) { + AtlasRegion region = regions[i]; + region.v = 1 - region.v; + region.v2 = 1 - region.v2; + } + } + + /// Returns the first region found with the specified name. This method uses string comparison to find the region, so the result + /// should be cached rather than calling this method multiple times. + /// The region, or null. + public AtlasRegion FindRegion (string name) { + for (int i = 0, n = regions.Count; i < n; i++) + if (regions[i].name == name) return regions[i]; + return null; + } + + public void Dispose () { + if (textureLoader == null) return; + for (int i = 0, n = pages.Count; i < n; i++) + textureLoader.Unload(pages[i].rendererObject); + } + } + + public enum Format { + Alpha, + Intensity, + LuminanceAlpha, + RGB565, + RGBA4444, + RGB888, + RGBA8888 + } + + public enum TextureFilter { + Nearest, + Linear, + MipMap, + MipMapNearestNearest, + MipMapLinearNearest, + MipMapNearestLinear, + MipMapLinearLinear + } + + public enum TextureWrap { + MirroredRepeat, + ClampToEdge, + Repeat + } + + public class AtlasPage { + public string name; + public int width, height; + public Format format = Format.RGBA8888; + public TextureFilter minFilter = TextureFilter.Nearest; + public TextureFilter magFilter = TextureFilter.Nearest; + public TextureWrap uWrap = TextureWrap.ClampToEdge; + public TextureWrap vWrap = TextureWrap.ClampToEdge; + public bool pma; + public object rendererObject; + + public AtlasPage Clone () { + return MemberwiseClone() as AtlasPage; + } + } + + public class AtlasRegion : TextureRegion { + public AtlasPage page; + public string name; + public int x, y; + public float offsetX, offsetY; + public int originalWidth, originalHeight; + public int packedWidth { get { return width; } set { width = value; } } + public int packedHeight { get { return height; } set { height = value; } } + public int degrees; + public bool rotate; + public int index; + public string[] names; + public int[][] values; + + override public int OriginalWidth { get { return originalWidth; } } + override public int OriginalHeight { get { return originalHeight; } } + + public AtlasRegion Clone () { + return MemberwiseClone() as AtlasRegion; + } + } + + public interface TextureLoader { + void Load (AtlasPage page, string path); + void Unload (Object texture); + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AtlasAttachmentLoader.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AtlasAttachmentLoader.cs new file mode 100644 index 0000000..875234a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AtlasAttachmentLoader.cs @@ -0,0 +1,109 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + + /// + /// An AttachmentLoader that configures attachments using texture regions from an Atlas. + /// See Loading Skeleton Data in the Spine Runtimes Guide. + /// + public class AtlasAttachmentLoader : AttachmentLoader { + private Atlas[] atlasArray; + + public AtlasAttachmentLoader (params Atlas[] atlasArray) { + if (atlasArray == null) throw new ArgumentNullException("atlas", "atlas array cannot be null."); + this.atlasArray = atlasArray; + } + + private void LoadSequence (string name, string basePath, Sequence sequence) { + TextureRegion[] regions = sequence.Regions; + for (int i = 0, n = regions.Length; i < n; i++) { + string path = sequence.GetPath(basePath, i); + regions[i] = FindRegion(path); + if (regions[i] == null) throw new ArgumentException(string.Format("Region not found in atlas: {0} (region attachment: {1})", path, name)); + } + } + + public RegionAttachment NewRegionAttachment (Skin skin, string name, string path, Sequence sequence) { + RegionAttachment attachment = new RegionAttachment(name); + if (sequence != null) + LoadSequence(name, path, sequence); + else { + AtlasRegion region = FindRegion(path); + if (region == null) + throw new ArgumentException(string.Format("Region not found in atlas: {0} (region attachment: {1})", path, name)); + attachment.Region = region; + } + return attachment; + } + + public MeshAttachment NewMeshAttachment (Skin skin, string name, string path, Sequence sequence) { + MeshAttachment attachment = new MeshAttachment(name); + if (sequence != null) + LoadSequence(name, path, sequence); + else { + AtlasRegion region = FindRegion(path); + if (region == null) + throw new ArgumentException(string.Format("Region not found in atlas: {0} (region attachment: {1})", path, name)); + attachment.Region = region; + } + return attachment; + } + + public BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, string name) { + return new BoundingBoxAttachment(name); + } + + public PathAttachment NewPathAttachment (Skin skin, string name) { + return new PathAttachment(name); + } + + public PointAttachment NewPointAttachment (Skin skin, string name) { + return new PointAttachment(name); + } + + public ClippingAttachment NewClippingAttachment (Skin skin, string name) { + return new ClippingAttachment(name); + } + + public AtlasRegion FindRegion (string name) { + AtlasRegion region; + + for (int i = 0; i < atlasArray.Length; i++) { + region = atlasArray[i].FindRegion(name); + if (region != null) + return region; + } + + return null; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/Attachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/Attachment.cs new file mode 100644 index 0000000..242d513 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/Attachment.cs @@ -0,0 +1,56 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + + /// The base class for all attachments. + abstract public class Attachment { + /// The attachment's name. + public string Name { get; } + + protected Attachment (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null"); + this.Name = name; + } + + /// Copy constructor. + protected Attachment (Attachment other) { + Name = other.Name; + } + + override public string ToString () { + return Name; + } + + ///Returns a copy of the attachment. + public abstract Attachment Copy (); + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AttachmentLoader.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AttachmentLoader.cs new file mode 100644 index 0000000..078ea8a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AttachmentLoader.cs @@ -0,0 +1,48 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_1_00 { + public interface AttachmentLoader { + /// May be null to not load any attachment. + RegionAttachment NewRegionAttachment (Skin skin, string name, string path, Sequence sequence); + + /// May be null to not load any attachment. + MeshAttachment NewMeshAttachment (Skin skin, string name, string path, Sequence sequence); + + /// May be null to not load any attachment. + BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, string name); + + /// May be null to not load any attachment + PathAttachment NewPathAttachment (Skin skin, string name); + + PointAttachment NewPointAttachment (Skin skin, string name); + + ClippingAttachment NewClippingAttachment (Skin skin, string name); + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AttachmentType.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AttachmentType.cs new file mode 100644 index 0000000..17beb29 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/AttachmentType.cs @@ -0,0 +1,34 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_1_00 { + public enum AttachmentType { + Region, Boundingbox, Mesh, Linkedmesh, Path, Point, Clipping, Sequence + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/BoundingBoxAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/BoundingBoxAttachment.cs new file mode 100644 index 0000000..8829d0c --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/BoundingBoxAttachment.cs @@ -0,0 +1,48 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// Attachment that has a polygon for bounds checking. + public class BoundingBoxAttachment : VertexAttachment { + public BoundingBoxAttachment (string name) + : base(name) { + } + + /// Copy constructor. + protected BoundingBoxAttachment (BoundingBoxAttachment other) + : base(other) { + } + + public override Attachment Copy () { + return new BoundingBoxAttachment(this); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/ClippingAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/ClippingAttachment.cs new file mode 100644 index 0000000..65b1668 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/ClippingAttachment.cs @@ -0,0 +1,51 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + public class ClippingAttachment : VertexAttachment { + internal SlotData endSlot; + + public SlotData EndSlot { get { return endSlot; } set { endSlot = value; } } + + public ClippingAttachment (string name) : base(name) { + } + + /// Copy constructor. + protected ClippingAttachment (ClippingAttachment other) + : base(other) { + endSlot = other.endSlot; + } + + public override Attachment Copy () { + return new ClippingAttachment(this); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/IHasTextureRegion.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/IHasTextureRegion.cs new file mode 100644 index 0000000..2b52372 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/IHasTextureRegion.cs @@ -0,0 +1,56 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Text; + +namespace Spine4_1_00 { + public interface IHasTextureRegion { + /// The name used to find the + string Path { get; set; } + /// + /// Sets the region used to draw the attachment. After setting the region or if the region's properties are changed, + /// must be called. + /// + TextureRegion Region { get; set; } + + /// + /// Updates any values the attachment calculates using the . Must be called after setting the + /// or if the region's properties are changed. + /// + void UpdateRegion (); + + float R { get; set; } + float G { get; set; } + float B { get; set; } + float A { get; set; } + + Sequence Sequence { get; set; } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/MeshAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/MeshAttachment.cs new file mode 100644 index 0000000..116b5f1 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/MeshAttachment.cs @@ -0,0 +1,220 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// Attachment that displays a texture region using a mesh. + public class MeshAttachment : VertexAttachment, IHasTextureRegion { + internal TextureRegion region; + internal string path; + internal float[] regionUVs, uvs; + internal int[] triangles; + internal float r = 1, g = 1, b = 1, a = 1; + internal int hullLength; + private MeshAttachment parentMesh; + private Sequence sequence; + + public TextureRegion Region { + get { return region; } + set { + if (value == null) throw new ArgumentNullException("region", "region cannot be null."); + region = value; + } + } + public int HullLength { get { return hullLength; } set { hullLength = value; } } + public float[] RegionUVs { get { return regionUVs; } set { regionUVs = value; } } + /// The UV pair for each vertex, normalized within the entire texture. + /// + public float[] UVs { get { return uvs; } set { uvs = value; } } + public int[] Triangles { get { return triangles; } set { triangles = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public string Path { get { return path; } set { path = value; } } + public Sequence Sequence { get { return sequence; } set { sequence = value; } } + + public MeshAttachment ParentMesh { + get { return parentMesh; } + set { + parentMesh = value; + if (value != null) { + bones = value.bones; + vertices = value.vertices; + worldVerticesLength = value.worldVerticesLength; + regionUVs = value.regionUVs; + triangles = value.triangles; + HullLength = value.HullLength; + Edges = value.Edges; + Width = value.Width; + Height = value.Height; + } + } + } + + // Nonessential. + public int[] Edges { get; set; } + public float Width { get; set; } + public float Height { get; set; } + + public MeshAttachment (string name) + : base(name) { + } + + /// Copy constructor. Use if the other mesh is a linked mesh. + protected MeshAttachment (MeshAttachment other) + : base(other) { + + if (parentMesh != null) throw new ArgumentException("Use newLinkedMesh to copy a linked mesh."); + + region = other.region; + path = other.path; + r = other.r; + g = other.g; + b = other.b; + a = other.a; + + regionUVs = new float[other.regionUVs.Length]; + Array.Copy(other.regionUVs, 0, regionUVs, 0, regionUVs.Length); + + uvs = new float[other.uvs.Length]; + Array.Copy(other.uvs, 0, uvs, 0, uvs.Length); + + triangles = new int[other.triangles.Length]; + Array.Copy(other.triangles, 0, triangles, 0, triangles.Length); + + hullLength = other.hullLength; + sequence = other.sequence == null ? null : new Sequence(other.sequence); + + // Nonessential. + if (other.Edges != null) { + Edges = new int[other.Edges.Length]; + Array.Copy(other.Edges, 0, Edges, 0, Edges.Length); + } + Width = other.Width; + Height = other.Height; + } + + + public void UpdateRegion () { + float[] regionUVs = this.regionUVs; + if (this.uvs == null || this.uvs.Length != regionUVs.Length) this.uvs = new float[regionUVs.Length]; + float[] uvs = this.uvs; + int n = uvs.Length; + float u, v, width, height; + + if (region is AtlasRegion) { + u = this.region.u; + v = this.region.v; + AtlasRegion region = (AtlasRegion)this.region; + // Note: difference from reference implementation. + // Covers rotation since region.width and height are already setup accordingly. + float textureWidth = this.region.width / (region.u2 - region.u); + float textureHeight = this.region.height / (region.v2 - region.v); + switch (region.degrees) { + case 90: + u -= (region.originalHeight - region.offsetY - region.packedWidth) / textureWidth; + v -= (region.originalWidth - region.offsetX - region.packedHeight) / textureHeight; + width = region.originalHeight / textureWidth; + height = region.originalWidth / textureHeight; + for (int i = 0; i < n; i += 2) { + uvs[i] = u + regionUVs[i + 1] * width; + uvs[i + 1] = v + (1 - regionUVs[i]) * height; + } + return; + case 180: + u -= (region.originalWidth - region.offsetX - region.packedWidth) / textureWidth; + v -= region.offsetY / textureHeight; + width = region.originalWidth / textureWidth; + height = region.originalHeight / textureHeight; + for (int i = 0; i < n; i += 2) { + uvs[i] = u + (1 - regionUVs[i]) * width; + uvs[i + 1] = v + (1 - regionUVs[i + 1]) * height; + } + return; + case 270: + u -= region.offsetY / textureWidth; + v -= region.offsetX / textureHeight; + width = region.originalHeight / textureWidth; + height = region.originalWidth / textureHeight; + for (int i = 0; i < n; i += 2) { + uvs[i] = u + (1 - regionUVs[i + 1]) * width; + uvs[i + 1] = v + regionUVs[i] * height; + } + return; + } + u -= region.offsetX / textureWidth; + v -= (region.originalHeight - region.offsetY - region.packedHeight) / textureHeight; + width = region.originalWidth / textureWidth; + height = region.originalHeight / textureHeight; + } else if (region == null) { + u = v = 0; + width = height = 1; + } else { + u = region.u; + v = region.v; + width = region.u2 - u; + height = region.v2 - v; + } + for (int i = 0; i < n; i += 2) { + uvs[i] = u + regionUVs[i] * width; + uvs[i + 1] = v + regionUVs[i + 1] * height; + } + } + + /// If the attachment has a , the region may be changed. + override public void ComputeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride = 2) { + if (sequence != null) sequence.Apply(slot, this); + base.ComputeWorldVertices(slot, start, count, worldVertices, offset, stride); + } + + ///Returns a new mesh with this mesh set as the . + public MeshAttachment NewLinkedMesh () { + MeshAttachment mesh = new MeshAttachment(Name); + + mesh.timelineAttachment = timelineAttachment; + mesh.region = region; + mesh.path = path; + mesh.r = r; + mesh.g = g; + mesh.b = b; + mesh.a = a; + mesh.ParentMesh = parentMesh != null ? parentMesh : this; + if (mesh.Region != null) mesh.UpdateRegion(); + return mesh; + } + + public override Attachment Copy () { + return parentMesh != null ? NewLinkedMesh() : new MeshAttachment(this); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/PathAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/PathAttachment.cs new file mode 100644 index 0000000..bf82146 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/PathAttachment.cs @@ -0,0 +1,65 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_1_00 { + public class PathAttachment : VertexAttachment { + internal float[] lengths; + internal bool closed, constantSpeed; + + /// The length in the setup pose from the start of the path to the end of each curve. + public float[] Lengths { get { return lengths; } set { lengths = value; } } + /// If true, the start and end knots are connected. + public bool Closed { get { return closed; } set { closed = value; } } + /// If true, additional calculations are performed to make computing positions along the path more accurate and movement along + /// the path have a constant speed. + public bool ConstantSpeed { get { return constantSpeed; } set { constantSpeed = value; } } + + public PathAttachment (String name) + : base(name) { + } + + /// Copy constructor. + protected PathAttachment (PathAttachment other) + : base(other) { + + lengths = new float[other.lengths.Length]; + Array.Copy(other.lengths, 0, lengths, 0, lengths.Length); + + closed = other.closed; + constantSpeed = other.constantSpeed; + } + + public override Attachment Copy () { + return new PathAttachment(this); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/PointAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/PointAttachment.cs new file mode 100644 index 0000000..319720b --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/PointAttachment.cs @@ -0,0 +1,71 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_1_00 { + /// + /// An attachment which is a single point and a rotation. This can be used to spawn projectiles, particles, etc. A bone can be + /// used in similar ways, but a PointAttachment is slightly less expensive to compute and can be hidden, shown, and placed in a + /// skin. + ///

+ /// See Point Attachments in the Spine User Guide. + ///

+ public class PointAttachment : Attachment { + internal float x, y, rotation; + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Rotation { get { return rotation; } set { rotation = value; } } + + public PointAttachment (string name) + : base(name) { + } + + /** Copy constructor. */ + protected PointAttachment (PointAttachment other) + : base(other) { + x = other.x; + y = other.y; + rotation = other.rotation; + } + + public void ComputeWorldPosition (Bone bone, out float ox, out float oy) { + bone.LocalToWorld(this.x, this.y, out ox, out oy); + } + + public float ComputeWorldRotation (Bone bone) { + float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation); + float ix = cos * bone.a + sin * bone.b; + float iy = cos * bone.c + sin * bone.d; + return MathUtils.Atan2(iy, ix) * MathUtils.RadDeg; + } + + public override Attachment Copy () { + return new PointAttachment(this); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/RegionAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/RegionAttachment.cs new file mode 100644 index 0000000..337beb7 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/RegionAttachment.cs @@ -0,0 +1,213 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// Attachment that displays a texture region. + public class RegionAttachment : Attachment, IHasTextureRegion { + public const int BLX = 0, BLY = 1; + public const int ULX = 2, ULY = 3; + public const int URX = 4, URY = 5; + public const int BRX = 6, BRY = 7; + + internal TextureRegion region; + internal float x, y, rotation, scaleX = 1, scaleY = 1, width, height; + internal float[] offset = new float[8], uvs = new float[8]; + internal float r = 1, g = 1, b = 1, a = 1; + internal Sequence sequence; + + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Rotation { get { return rotation; } set { rotation = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + public float Width { get { return width; } set { width = value; } } + public float Height { get { return height; } set { height = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public string Path { get; set; } + public TextureRegion Region { get { return region; } set { region = value; } } + + /// For each of the 4 vertices, a pair of x,y values that is the local position of the vertex. + /// + public float[] Offset { get { return offset; } } + public float[] UVs { get { return uvs; } } + public Sequence Sequence { get { return sequence; } set { sequence = value; } } + + public RegionAttachment (string name) + : base(name) { + } + + /// Copy constructor. + public RegionAttachment (RegionAttachment other) + : base(other) { + region = other.region; + Path = other.Path; + x = other.x; + y = other.y; + scaleX = other.scaleX; + scaleY = other.scaleY; + rotation = other.rotation; + width = other.width; + height = other.height; + Array.Copy(other.uvs, 0, uvs, 0, 8); + Array.Copy(other.offset, 0, offset, 0, 8); + r = other.r; + g = other.g; + b = other.b; + a = other.a; + sequence = other.sequence == null ? null : new Sequence(other.sequence); + } + + /// Calculates the and using the region and the attachment's transform. Must be called if the + /// region, the region's properties, or the transform are changed. + public void UpdateRegion () { + float width = Width; + float height = Height; + float localX2 = width / 2; + float localY2 = height / 2; + float localX = -localX2; + float localY = -localY2; + bool rotated = false; + if (region is AtlasRegion) { + AtlasRegion region = (AtlasRegion)this.region; + localX += region.offsetX / region.originalWidth * width; + localY += region.offsetY / region.originalHeight * height; + if (region.degrees == 90) { + rotated = true; + localX2 -= (region.originalWidth - region.offsetX - region.packedHeight) / region.originalWidth * width; + localY2 -= (region.originalHeight - region.offsetY - region.packedWidth) / region.originalHeight * height; + } else { + localX2 -= (region.originalWidth - region.offsetX - region.packedWidth) / region.originalWidth * width; + localY2 -= (region.originalHeight - region.offsetY - region.packedHeight) / region.originalHeight * height; + } + } + float scaleX = ScaleX; + float scaleY = ScaleY; + localX *= scaleX; + localY *= scaleY; + localX2 *= scaleX; + localY2 *= scaleY; + float rotation = Rotation; + float cos = MathUtils.CosDeg(this.rotation); + float sin = MathUtils.SinDeg(this.rotation); + float x = X; + float y = Y; + float localXCos = localX * cos + x; + float localXSin = localX * sin; + float localYCos = localY * cos + y; + float localYSin = localY * sin; + float localX2Cos = localX2 * cos + x; + float localX2Sin = localX2 * sin; + float localY2Cos = localY2 * cos + y; + float localY2Sin = localY2 * sin; + float[] offset = this.offset; + offset[BLX] = localXCos - localYSin; + offset[BLY] = localYCos + localXSin; + offset[ULX] = localXCos - localY2Sin; + offset[ULY] = localY2Cos + localXSin; + offset[URX] = localX2Cos - localY2Sin; + offset[URY] = localY2Cos + localX2Sin; + offset[BRX] = localX2Cos - localYSin; + offset[BRY] = localYCos + localX2Sin; + + float[] uvs = this.uvs; + if (rotated) { + uvs[URX] = region.u; + uvs[URY] = region.v2; + uvs[BRX] = region.u; + uvs[BRY] = region.v; + uvs[BLX] = region.u2; + uvs[BLY] = region.v; + uvs[ULX] = region.u2; + uvs[ULY] = region.v2; + } else { + uvs[ULX] = region.u; + uvs[ULY] = region.v2; + uvs[URX] = region.u; + uvs[URY] = region.v; + uvs[BRX] = region.u2; + uvs[BRY] = region.v; + uvs[BLX] = region.u2; + uvs[BLY] = region.v2; + } + } + + /// + /// Transforms the attachment's four vertices to world coordinates. If the attachment has a the region may + /// be changed. + /// The parent bone. + /// The output world vertices. Must have a length greater than or equal to offset + 8. + /// The worldVertices index to begin writing values. + /// The number of worldVertices entries between the value pairs written. + public void ComputeWorldVertices (Slot slot, float[] worldVertices, int offset, int stride = 2) { + if (sequence != null) sequence.Apply(slot, this); + + float[] vertexOffset = this.offset; + Bone bone = slot.Bone; + float bwx = bone.worldX, bwy = bone.worldY; + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float offsetX, offsetY; + + // Vertex order is different from RegionAttachment.java + offsetX = vertexOffset[BRX]; // 0 + offsetY = vertexOffset[BRY]; // 1 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // bl + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[BLX]; // 2 + offsetY = vertexOffset[BLY]; // 3 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // ul + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[ULX]; // 4 + offsetY = vertexOffset[ULY]; // 5 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // ur + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + offset += stride; + + offsetX = vertexOffset[URX]; // 6 + offsetY = vertexOffset[URY]; // 7 + worldVertices[offset] = offsetX * a + offsetY * b + bwx; // br + worldVertices[offset + 1] = offsetX * c + offsetY * d + bwy; + //offset += stride; + } + + public override Attachment Copy () { + return new RegionAttachment(this); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/Sequence.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/Sequence.cs new file mode 100644 index 0000000..7beb9ee --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/Sequence.cs @@ -0,0 +1,95 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Text; + +namespace Spine4_1_00 { + public class Sequence { + static int nextID = 0; + static readonly Object nextIdLock = new Object(); + + internal readonly int id; + internal readonly TextureRegion[] regions; + internal int start, digits, setupIndex; + + public int Start { get { return start; } set { start = value; } } + public int Digits { get { return digits; } set { digits = value; } } + /// The index of the region to show for the setup pose. + public int SetupIndex { get { return setupIndex; } set { setupIndex = value; } } + public TextureRegion[] Regions { get { return regions; } } + /// Returns a unique ID for this attachment. + public int Id { get { return id; } } + + public Sequence (int count) { + lock (Sequence.nextIdLock) { + id = Sequence.nextID++; + } + regions = new TextureRegion[count]; + } + + /// Copy constructor. + public Sequence (Sequence other) { + lock (Sequence.nextIdLock) { + id = Sequence.nextID++; + } + regions = new TextureRegion[other.regions.Length]; + Array.Copy(other.regions, 0, regions, 0, regions.Length); + + start = other.start; + digits = other.digits; + setupIndex = other.setupIndex; + } + + public void Apply (Slot slot, IHasTextureRegion attachment) { + int index = slot.SequenceIndex; + if (index == -1) index = setupIndex; + if (index >= regions.Length) index = regions.Length - 1; + TextureRegion region = regions[index]; + if (attachment.Region != region) { + attachment.Region = region; + attachment.UpdateRegion(); + } + } + + public string GetPath (string basePath, int index) { + StringBuilder buffer = new StringBuilder(basePath.Length + digits); + buffer.Append(basePath); + string frame = (start + index).ToString(); + for (int i = digits - frame.Length; i > 0; i--) + buffer.Append('0'); + buffer.Append(frame); + return buffer.ToString(); + } + } + + public enum SequenceMode { + Hold, Once, Loop, Pingpong, OnceReverse, LoopReverse, PingpongReverse + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/VertexAttachment.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/VertexAttachment.cs new file mode 100644 index 0000000..d108613 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Attachments/VertexAttachment.cs @@ -0,0 +1,158 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// >An attachment with vertices that are transformed by one or more bones and can be deformed by a slot's + /// . + public abstract class VertexAttachment : Attachment { + static int nextID = 0; + static readonly Object nextIdLock = new Object(); + + internal readonly int id; + internal VertexAttachment timelineAttachment; + internal int[] bones; + internal float[] vertices; + internal int worldVerticesLength; + + /// Gets a unique ID for this attachment. + public int Id { get { return id; } } + public int[] Bones { get { return bones; } set { bones = value; } } + public float[] Vertices { get { return vertices; } set { vertices = value; } } + public int WorldVerticesLength { get { return worldVerticesLength; } set { worldVerticesLength = value; } } + ///Timelines for the timeline attachment are also applied to this attachment. + /// May be null if no attachment-specific timelines should be applied. + public VertexAttachment TimelineAttachment { get { return timelineAttachment; } set { timelineAttachment = value; } } + + public VertexAttachment (string name) + : base(name) { + + lock (VertexAttachment.nextIdLock) { + id = VertexAttachment.nextID++; + } + timelineAttachment = this; + } + + /// Copy constructor. + public VertexAttachment (VertexAttachment other) + : base(other) { + + lock (VertexAttachment.nextIdLock) { + id = VertexAttachment.nextID++; + } + timelineAttachment = other.timelineAttachment; + if (other.bones != null) { + bones = new int[other.bones.Length]; + Array.Copy(other.bones, 0, bones, 0, bones.Length); + } else + bones = null; + + if (other.vertices != null) { + vertices = new float[other.vertices.Length]; + Array.Copy(other.vertices, 0, vertices, 0, vertices.Length); + } else + vertices = null; + + worldVerticesLength = other.worldVerticesLength; + } + + public void ComputeWorldVertices (Slot slot, float[] worldVertices) { + ComputeWorldVertices(slot, 0, worldVerticesLength, worldVertices, 0); + } + + /// + /// Transforms the attachment's local to world coordinates. If the slot's is + /// not empty, it is used to deform the vertices. + /// + /// See World transforms in the Spine + /// Runtimes Guide. + /// + /// The index of the first value to transform. Each vertex has 2 values, x and y. + /// The number of world vertex values to output. Must be less than or equal to - start. + /// The output world vertices. Must have a length greater than or equal to + . + /// The index to begin writing values. + /// The number of entries between the value pairs written. + public virtual void ComputeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride = 2) { + count = offset + (count >> 1) * stride; + ExposedList deformArray = slot.deform; + float[] vertices = this.vertices; + int[] bones = this.bones; + if (bones == null) { + if (deformArray.Count > 0) vertices = deformArray.Items; + Bone bone = slot.bone; + float x = bone.worldX, y = bone.worldY; + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + for (int vv = start, w = offset; w < count; vv += 2, w += stride) { + float vx = vertices[vv], vy = vertices[vv + 1]; + worldVertices[w] = vx * a + vy * b + x; + worldVertices[w + 1] = vx * c + vy * d + y; + } + return; + } + int v = 0, skip = 0; + for (int i = 0; i < start; i += 2) { + int n = bones[v]; + v += n + 1; + skip += n; + } + Bone[] skeletonBones = slot.bone.skeleton.bones.Items; + if (deformArray.Count == 0) { + for (int w = offset, b = skip * 3; w < count; w += stride) { + float wx = 0, wy = 0; + int n = bones[v++]; + n += v; + for (; v < n; v++, b += 3) { + Bone bone = skeletonBones[bones[v]]; + float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } else { + float[] deform = deformArray.Items; + for (int w = offset, b = skip * 3, f = skip << 1; w < count; w += stride) { + float wx = 0, wy = 0; + int n = bones[v++]; + n += v; + for (; v < n; v++, b += 3, f += 2) { + Bone bone = skeletonBones[bones[v]]; + float vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/BlendMode.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/BlendMode.cs new file mode 100644 index 0000000..751bdf3 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/BlendMode.cs @@ -0,0 +1,34 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_1_00 { + public enum BlendMode { + Normal, Additive, Multiply, Screen + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Bone.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Bone.cs new file mode 100644 index 0000000..802a62b --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Bone.cs @@ -0,0 +1,393 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// + /// Stores a bone's current pose. + /// + /// A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a + /// local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a + /// constraint or application code modifies the world transform after it was computed from the local transform. + /// + /// + public class Bone : IUpdatable { + static public bool yDown; + + internal BoneData data; + internal Skeleton skeleton; + internal Bone parent; + internal ExposedList children = new ExposedList(); + internal float x, y, rotation, scaleX, scaleY, shearX, shearY; + internal float ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY; + + internal float a, b, worldX; + internal float c, d, worldY; + + internal bool sorted, active; + + public BoneData Data { get { return data; } } + public Skeleton Skeleton { get { return skeleton; } } + public Bone Parent { get { return parent; } } + public ExposedList Children { get { return children; } } + /// Returns false when the bone has not been computed because is true and the + /// active skin does not contain this bone. + public bool Active { get { return active; } } + /// The local X translation. + public float X { get { return x; } set { x = value; } } + /// The local Y translation. + public float Y { get { return y; } set { y = value; } } + /// The local rotation. + public float Rotation { get { return rotation; } set { rotation = value; } } + + /// The local scaleX. + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + + /// The local scaleY. + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + + /// The local shearX. + public float ShearX { get { return shearX; } set { shearX = value; } } + + /// The local shearY. + public float ShearY { get { return shearY; } set { shearY = value; } } + + /// The rotation, as calculated by any constraints. + public float AppliedRotation { get { return arotation; } set { arotation = value; } } + + /// The applied local x translation. + public float AX { get { return ax; } set { ax = value; } } + + /// The applied local y translation. + public float AY { get { return ay; } set { ay = value; } } + + /// The applied local scaleX. + public float AScaleX { get { return ascaleX; } set { ascaleX = value; } } + + /// The applied local scaleY. + public float AScaleY { get { return ascaleY; } set { ascaleY = value; } } + + /// The applied local shearX. + public float AShearX { get { return ashearX; } set { ashearX = value; } } + + /// The applied local shearY. + public float AShearY { get { return ashearY; } set { ashearY = value; } } + + /// Part of the world transform matrix for the X axis. If changed, should be called. + public float A { get { return a; } set { a = value; } } + /// Part of the world transform matrix for the Y axis. If changed, should be called. + public float B { get { return b; } set { b = value; } } + /// Part of the world transform matrix for the X axis. If changed, should be called. + public float C { get { return c; } set { c = value; } } + /// Part of the world transform matrix for the Y axis. If changed, should be called. + public float D { get { return d; } set { d = value; } } + + /// The world X position. If changed, should be called. + public float WorldX { get { return worldX; } set { worldX = value; } } + /// The world Y position. If changed, should be called. + public float WorldY { get { return worldY; } set { worldY = value; } } + public float WorldRotationX { get { return MathUtils.Atan2(c, a) * MathUtils.RadDeg; } } + public float WorldRotationY { get { return MathUtils.Atan2(d, b) * MathUtils.RadDeg; } } + + /// Returns the magnitide (always positive) of the world scale X. + public float WorldScaleX { get { return (float)Math.Sqrt(a * a + c * c); } } + /// Returns the magnitide (always positive) of the world scale Y. + public float WorldScaleY { get { return (float)Math.Sqrt(b * b + d * d); } } + + public Bone (BoneData data, Skeleton skeleton, Bone parent) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + this.skeleton = skeleton; + this.parent = parent; + SetToSetupPose(); + } + + /// Copy constructor. Does not copy the bones. + /// May be null. + public Bone (Bone bone, Skeleton skeleton, Bone parent) { + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.skeleton = skeleton; + this.parent = parent; + data = bone.data; + x = bone.x; + y = bone.y; + rotation = bone.rotation; + scaleX = bone.scaleX; + scaleY = bone.scaleY; + shearX = bone.shearX; + shearY = bone.shearY; + } + + /// Computes the world transform using the parent bone and this bone's local applied transform. + public void Update () { + UpdateWorldTransform(ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY); + } + + /// Computes the world transform using the parent bone and this bone's local transform. + public void UpdateWorldTransform () { + UpdateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY); + } + + /// Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the + /// specified local transform. Child bones are not updated. + /// + /// See World transforms in the Spine + /// Runtimes Guide. + public void UpdateWorldTransform (float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY) { + ax = x; + ay = y; + arotation = rotation; + ascaleX = scaleX; + ascaleY = scaleY; + ashearX = shearX; + ashearY = shearY; + + Bone parent = this.parent; + if (parent == null) { // Root bone. + float rotationY = rotation + 90 + shearY, sx = skeleton.ScaleX, sy = skeleton.ScaleY; + a = MathUtils.CosDeg(rotation + shearX) * scaleX * sx; + b = MathUtils.CosDeg(rotationY) * scaleY * sx; + c = MathUtils.SinDeg(rotation + shearX) * scaleX * sy; + d = MathUtils.SinDeg(rotationY) * scaleY * sy; + worldX = x * sx + skeleton.x; + worldY = y * sy + skeleton.y; + return; + } + + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; + worldX = pa * x + pb * y + parent.worldX; + worldY = pc * x + pd * y + parent.worldY; + + switch (data.transformMode) { + case TransformMode.Normal: { + float rotationY = rotation + 90 + shearY; + float la = MathUtils.CosDeg(rotation + shearX) * scaleX; + float lb = MathUtils.CosDeg(rotationY) * scaleY; + float lc = MathUtils.SinDeg(rotation + shearX) * scaleX; + float ld = MathUtils.SinDeg(rotationY) * scaleY; + a = pa * la + pb * lc; + b = pa * lb + pb * ld; + c = pc * la + pd * lc; + d = pc * lb + pd * ld; + return; + } + case TransformMode.OnlyTranslation: { + float rotationY = rotation + 90 + shearY; + a = MathUtils.CosDeg(rotation + shearX) * scaleX; + b = MathUtils.CosDeg(rotationY) * scaleY; + c = MathUtils.SinDeg(rotation + shearX) * scaleX; + d = MathUtils.SinDeg(rotationY) * scaleY; + break; + } + case TransformMode.NoRotationOrReflection: { + float s = pa * pa + pc * pc, prx; + if (s > 0.0001f) { + s = Math.Abs(pa * pd - pb * pc) / s; + pa /= skeleton.ScaleX; + pc /= skeleton.ScaleY; + pb = pc * s; + pd = pa * s; + prx = MathUtils.Atan2(pc, pa) * MathUtils.RadDeg; + } else { + pa = 0; + pc = 0; + prx = 90 - MathUtils.Atan2(pd, pb) * MathUtils.RadDeg; + } + float rx = rotation + shearX - prx; + float ry = rotation + shearY - prx + 90; + float la = MathUtils.CosDeg(rx) * scaleX; + float lb = MathUtils.CosDeg(ry) * scaleY; + float lc = MathUtils.SinDeg(rx) * scaleX; + float ld = MathUtils.SinDeg(ry) * scaleY; + a = pa * la - pb * lc; + b = pa * lb - pb * ld; + c = pc * la + pd * lc; + d = pc * lb + pd * ld; + break; + } + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: { + float cos = MathUtils.CosDeg(rotation), sin = MathUtils.SinDeg(rotation); + float za = (pa * cos + pb * sin) / skeleton.ScaleX; + float zc = (pc * cos + pd * sin) / skeleton.ScaleY; + float s = (float)Math.Sqrt(za * za + zc * zc); + if (s > 0.00001f) s = 1 / s; + za *= s; + zc *= s; + s = (float)Math.Sqrt(za * za + zc * zc); + if (data.transformMode == TransformMode.NoScale + && (pa * pd - pb * pc < 0) != (skeleton.ScaleX < 0 != skeleton.ScaleY < 0)) s = -s; + + float r = MathUtils.PI / 2 + MathUtils.Atan2(zc, za); + float zb = MathUtils.Cos(r) * s; + float zd = MathUtils.Sin(r) * s; + float la = MathUtils.CosDeg(shearX) * scaleX; + float lb = MathUtils.CosDeg(90 + shearY) * scaleY; + float lc = MathUtils.SinDeg(shearX) * scaleX; + float ld = MathUtils.SinDeg(90 + shearY) * scaleY; + a = za * la + zb * lc; + b = za * lb + zb * ld; + c = zc * la + zd * lc; + d = zc * lb + zd * ld; + break; + } + } + + a *= skeleton.ScaleX; + b *= skeleton.ScaleX; + c *= skeleton.ScaleY; + d *= skeleton.ScaleY; + } + + public void SetToSetupPose () { + BoneData data = this.data; + x = data.x; + y = data.y; + rotation = data.rotation; + scaleX = data.scaleX; + scaleY = data.scaleY; + shearX = data.shearX; + shearY = data.shearY; + } + + /// + /// Computes the applied transform values from the world transform. + /// + /// If the world transform is modified (by a constraint, , etc) then this method should be called so + /// the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply another + /// constraint). + /// + /// Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after + /// calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. + /// + public void UpdateAppliedTransform () { + Bone parent = this.parent; + if (parent == null) { + ax = worldX - skeleton.x; + ay = worldY - skeleton.y; + arotation = MathUtils.Atan2(c, a) * MathUtils.RadDeg; + ascaleX = (float)Math.Sqrt(a * a + c * c); + ascaleY = (float)Math.Sqrt(b * b + d * d); + ashearX = 0; + ashearY = MathUtils.Atan2(a * b + c * d, a * d - b * c) * MathUtils.RadDeg; + return; + } + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; + float pid = 1 / (pa * pd - pb * pc); + float dx = worldX - parent.worldX, dy = worldY - parent.worldY; + ax = (dx * pd * pid - dy * pb * pid); + ay = (dy * pa * pid - dx * pc * pid); + float ia = pid * pd; + float id = pid * pa; + float ib = pid * pb; + float ic = pid * pc; + float ra = ia * a - ib * c; + float rb = ia * b - ib * d; + float rc = id * c - ic * a; + float rd = id * d - ic * b; + ashearX = 0; + ascaleX = (float)Math.Sqrt(ra * ra + rc * rc); + if (ascaleX > 0.0001f) { + float det = ra * rd - rb * rc; + ascaleY = det / ascaleX; + ashearY = MathUtils.Atan2(ra * rb + rc * rd, det) * MathUtils.RadDeg; + arotation = MathUtils.Atan2(rc, ra) * MathUtils.RadDeg; + } else { + ascaleX = 0; + ascaleY = (float)Math.Sqrt(rb * rb + rd * rd); + ashearY = 0; + arotation = 90 - MathUtils.Atan2(rd, rb) * MathUtils.RadDeg; + } + } + + public void WorldToLocal (float worldX, float worldY, out float localX, out float localY) { + float a = this.a, b = this.b, c = this.c, d = this.d; + float det = a * d - b * c; + float x = worldX - this.worldX, y = worldY - this.worldY; + localX = (x * d - y * b) / det; + localY = (y * a - x * c) / det; + } + + public void LocalToWorld (float localX, float localY, out float worldX, out float worldY) { + worldX = localX * a + localY * b + this.worldX; + worldY = localX * c + localY * d + this.worldY; + } + + public float WorldToLocalRotationX { + get { + Bone parent = this.parent; + if (parent == null) return arotation; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, a = this.a, c = this.c; + return MathUtils.Atan2(pa * c - pc * a, pd * a - pb * c) * MathUtils.RadDeg; + } + } + + public float WorldToLocalRotationY { + get { + Bone parent = this.parent; + if (parent == null) return arotation; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, b = this.b, d = this.d; + return MathUtils.Atan2(pa * d - pc * b, pd * b - pb * d) * MathUtils.RadDeg; + } + } + + public float WorldToLocalRotation (float worldRotation) { + float sin = MathUtils.SinDeg(worldRotation), cos = MathUtils.CosDeg(worldRotation); + return MathUtils.Atan2(a * sin - c * cos, d * cos - b * sin) * MathUtils.RadDeg + rotation - shearX; + } + + public float LocalToWorldRotation (float localRotation) { + localRotation -= rotation - shearX; + float sin = MathUtils.SinDeg(localRotation), cos = MathUtils.CosDeg(localRotation); + return MathUtils.Atan2(cos * c + sin * d, cos * a + sin * b) * MathUtils.RadDeg; + } + + /// + /// Rotates the world transform the specified amount. + /// + /// After changes are made to the world transform, should be called and will + /// need to be called on any child bones, recursively. + /// + public void RotateWorld (float degrees) { + float a = this.a, b = this.b, c = this.c, d = this.d; + float cos = MathUtils.CosDeg(degrees), sin = MathUtils.SinDeg(degrees); + this.a = cos * a - sin * c; + this.b = cos * b - sin * d; + this.c = sin * a + cos * c; + this.d = sin * b + cos * d; + } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/BoneData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/BoneData.cs new file mode 100644 index 0000000..afd2e09 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/BoneData.cs @@ -0,0 +1,105 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + public class BoneData { + internal int index; + internal string name; + internal BoneData parent; + internal float length; + internal float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY; + internal TransformMode transformMode = TransformMode.Normal; + internal bool skinRequired; + + /// The index of the bone in Skeleton.Bones + public int Index { get { return index; } } + + /// The name of the bone, which is unique across all bones in the skeleton. + public string Name { get { return name; } } + + /// May be null. + public BoneData Parent { get { return parent; } } + + public float Length { get { return length; } set { length = value; } } + + /// Local X translation. + public float X { get { return x; } set { x = value; } } + + /// Local Y translation. + public float Y { get { return y; } set { y = value; } } + + /// Local rotation. + public float Rotation { get { return rotation; } set { rotation = value; } } + + /// Local scaleX. + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + + /// Local scaleY. + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + + /// Local shearX. + public float ShearX { get { return shearX; } set { shearX = value; } } + + /// Local shearY. + public float ShearY { get { return shearY; } set { shearY = value; } } + + /// The transform mode for how parent world transforms affect this bone. + public TransformMode TransformMode { get { return transformMode; } set { transformMode = value; } } + + ///When true, only updates this bone if the contains this + /// bone. + /// + public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } } + + /// May be null. + public BoneData (int index, string name, BoneData parent) { + if (index < 0) throw new ArgumentException("index must be >= 0", "index"); + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.index = index; + this.name = name; + this.parent = parent; + } + + override public string ToString () { + return name; + } + } + + [Flags] + public enum TransformMode { + //0000 0 Flip Scale Rotation + Normal = 0, // 0000 + OnlyTranslation = 7, // 0111 + NoRotationOrReflection = 1, // 0001 + NoScale = 2, // 0010 + NoScaleOrReflection = 6, // 0110 + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/ConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/ConstraintData.cs new file mode 100644 index 0000000..cc33dbc --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/ConstraintData.cs @@ -0,0 +1,61 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_1_00 { + /// The base class for all constraint datas. + public abstract class ConstraintData { + internal readonly string name; + internal int order; + internal bool skinRequired; + + public ConstraintData (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + /// The constraint's name, which is unique across all constraints in the skeleton of the same type. + public string Name { get { return name; } } + + ///The ordinal of this constraint for the order a skeleton's constraints will be applied by + /// . + public int Order { get { return order; } set { order = value; } } + + ///When true, only updates this constraint if the contains + /// this constraint. + /// + public bool SkinRequired { get { return skinRequired; } set { skinRequired = value; } } + + override public string ToString () { + return name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Event.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Event.cs new file mode 100644 index 0000000..081735b --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Event.cs @@ -0,0 +1,64 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// Stores the current pose values for an Event. + public class Event { + internal readonly EventData data; + internal readonly float time; + internal int intValue; + internal float floatValue; + internal string stringValue; + internal float volume; + internal float balance; + + public EventData Data { get { return data; } } + /// The animation time this event was keyed. + public float Time { get { return time; } } + + public int Int { get { return intValue; } set { intValue = value; } } + public float Float { get { return floatValue; } set { floatValue = value; } } + public string String { get { return stringValue; } set { stringValue = value; } } + + public float Volume { get { return volume; } set { volume = value; } } + public float Balance { get { return balance; } set { balance = value; } } + + public Event (float time, EventData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.time = time; + this.data = data; + } + + override public string ToString () { + return this.data.Name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/EventData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/EventData.cs new file mode 100644 index 0000000..cad4b8c --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/EventData.cs @@ -0,0 +1,56 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// Stores the setup pose values for an Event. + public class EventData { + internal string name; + + /// The name of the event, which is unique across all events in the skeleton. + public string Name { get { return name; } } + public int Int { get; set; } + public float Float { get; set; } + public string @String { get; set; } + + public string AudioPath { get; set; } + public float Volume { get; set; } + public float Balance { get; set; } + + public EventData (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + override public string ToString () { + return Name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/ExposedList.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/ExposedList.cs new file mode 100644 index 0000000..c750dec --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/ExposedList.cs @@ -0,0 +1,637 @@ +// +// System.Collections.Generic.List +// +// Authors: +// Ben Maurer (bmaurer@ximian.com) +// Martin Baulig (martin@ximian.com) +// Carlos Alberto Cortez (calberto.cortez@gmail.com) +// David Waite (mass@akuma.org) +// +// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com) +// Copyright (C) 2005 David Waite +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Spine4_1_00 { + [DebuggerDisplay("Count={Count}")] + public class ExposedList : IEnumerable { + public T[] Items; + public int Count; + private const int DefaultCapacity = 4; + private static readonly T[] EmptyArray = new T[0]; + private int version; + + public ExposedList () { + Items = EmptyArray; + } + + public ExposedList (IEnumerable collection) { + CheckCollection(collection); + + // initialize to needed size (if determinable) + ICollection c = collection as ICollection; + if (c == null) { + Items = EmptyArray; + AddEnumerable(collection); + } else { + Items = new T[c.Count]; + AddCollection(c); + } + } + + public ExposedList (int capacity) { + if (capacity < 0) + throw new ArgumentOutOfRangeException("capacity"); + Items = new T[capacity]; + } + + internal ExposedList (T[] data, int size) { + Items = data; + Count = size; + } + + public void Add (T item) { + // If we check to see if we need to grow before trying to grow + // we can speed things up by 25% + if (Count == Items.Length) + GrowIfNeeded(1); + Items[Count++] = item; + version++; + } + + public void GrowIfNeeded (int addedCount) { + int minimumSize = Count + addedCount; + if (minimumSize > Items.Length) + Capacity = Math.Max(Math.Max(Capacity * 2, DefaultCapacity), minimumSize); + } + + public ExposedList Resize (int newSize) { + int itemsLength = Items.Length; + var oldItems = Items; + if (newSize > itemsLength) { + Array.Resize(ref Items, newSize); + } else if (newSize < itemsLength) { + // Allow nulling of T reference type to allow GC. + for (int i = newSize; i < itemsLength; i++) + oldItems[i] = default(T); + } + Count = newSize; + return this; + } + + public void EnsureCapacity (int min) { + if (Items.Length < min) { + int newCapacity = Items.Length == 0 ? DefaultCapacity : Items.Length * 2; + //if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; + if (newCapacity < min) newCapacity = min; + Capacity = newCapacity; + } + } + + private void CheckRange (int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + if ((uint)index + (uint)count > (uint)Count) + throw new ArgumentException("index and count exceed length of list"); + } + + private void AddCollection (ICollection collection) { + int collectionCount = collection.Count; + if (collectionCount == 0) + return; + + GrowIfNeeded(collectionCount); + collection.CopyTo(Items, Count); + Count += collectionCount; + } + + private void AddEnumerable (IEnumerable enumerable) { + foreach (T t in enumerable) { + Add(t); + } + } + + // Additional overload provided because ExposedList only implements IEnumerable, + // leading to sub-optimal behavior: It grows multiple times as it assumes not + // to know the final size ahead of insertion. + public void AddRange (ExposedList list) { + CheckCollection(list); + + int collectionCount = list.Count; + if (collectionCount == 0) + return; + + GrowIfNeeded(collectionCount); + list.CopyTo(Items, Count); + Count += collectionCount; + + version++; + } + + public void AddRange (IEnumerable collection) { + CheckCollection(collection); + + ICollection c = collection as ICollection; + if (c != null) + AddCollection(c); + else + AddEnumerable(collection); + version++; + } + + public int BinarySearch (T item) { + return Array.BinarySearch(Items, 0, Count, item); + } + + public int BinarySearch (T item, IComparer comparer) { + return Array.BinarySearch(Items, 0, Count, item, comparer); + } + + public int BinarySearch (int index, int count, T item, IComparer comparer) { + CheckRange(index, count); + return Array.BinarySearch(Items, index, count, item, comparer); + } + + public void Clear (bool clearArray = true) { + if (clearArray) + Array.Clear(Items, 0, Items.Length); + + Count = 0; + version++; + } + + public bool Contains (T item) { + return Array.IndexOf(Items, item, 0, Count) != -1; + } + + public ExposedList ConvertAll (Converter converter) { + if (converter == null) + throw new ArgumentNullException("converter"); + ExposedList u = new ExposedList(Count); + u.Count = Count; + T[] items = Items; + TOutput[] uItems = u.Items; + for (int i = 0; i < Count; i++) + uItems[i] = converter(items[i]); + return u; + } + + public void CopyTo (T[] array) { + Array.Copy(Items, 0, array, 0, Count); + } + + public void CopyTo (T[] array, int arrayIndex) { + Array.Copy(Items, 0, array, arrayIndex, Count); + } + + public void CopyTo (int index, T[] array, int arrayIndex, int count) { + CheckRange(index, count); + Array.Copy(Items, index, array, arrayIndex, count); + } + + public bool Exists (Predicate match) { + CheckMatch(match); + return GetIndex(0, Count, match) != -1; + } + + public T Find (Predicate match) { + CheckMatch(match); + int i = GetIndex(0, Count, match); + return (i != -1) ? Items[i] : default(T); + } + + private static void CheckMatch (Predicate match) { + if (match == null) + throw new ArgumentNullException("match"); + } + + public ExposedList FindAll (Predicate match) { + CheckMatch(match); + return FindAllList(match); + } + + private ExposedList FindAllList (Predicate match) { + ExposedList results = new ExposedList(); + for (int i = 0; i < Count; i++) + if (match(Items[i])) + results.Add(Items[i]); + + return results; + } + + public int FindIndex (Predicate match) { + CheckMatch(match); + return GetIndex(0, Count, match); + } + + public int FindIndex (int startIndex, Predicate match) { + CheckMatch(match); + CheckIndex(startIndex); + return GetIndex(startIndex, Count - startIndex, match); + } + + public int FindIndex (int startIndex, int count, Predicate match) { + CheckMatch(match); + CheckRange(startIndex, count); + return GetIndex(startIndex, count, match); + } + + private int GetIndex (int startIndex, int count, Predicate match) { + int end = startIndex + count; + for (int i = startIndex; i < end; i++) + if (match(Items[i])) + return i; + + return -1; + } + + public T FindLast (Predicate match) { + CheckMatch(match); + int i = GetLastIndex(0, Count, match); + return i == -1 ? default(T) : Items[i]; + } + + public int FindLastIndex (Predicate match) { + CheckMatch(match); + return GetLastIndex(0, Count, match); + } + + public int FindLastIndex (int startIndex, Predicate match) { + CheckMatch(match); + CheckIndex(startIndex); + return GetLastIndex(0, startIndex + 1, match); + } + + public int FindLastIndex (int startIndex, int count, Predicate match) { + CheckMatch(match); + int start = startIndex - count + 1; + CheckRange(start, count); + return GetLastIndex(start, count, match); + } + + private int GetLastIndex (int startIndex, int count, Predicate match) { + // unlike FindLastIndex, takes regular params for search range + for (int i = startIndex + count; i != startIndex;) + if (match(Items[--i])) + return i; + return -1; + } + + public void ForEach (Action action) { + if (action == null) + throw new ArgumentNullException("action"); + for (int i = 0; i < Count; i++) + action(Items[i]); + } + + public Enumerator GetEnumerator () { + return new Enumerator(this); + } + + public ExposedList GetRange (int index, int count) { + CheckRange(index, count); + T[] tmpArray = new T[count]; + Array.Copy(Items, index, tmpArray, 0, count); + return new ExposedList(tmpArray, count); + } + + public int IndexOf (T item) { + return Array.IndexOf(Items, item, 0, Count); + } + + public int IndexOf (T item, int index) { + CheckIndex(index); + return Array.IndexOf(Items, item, index, Count - index); + } + + public int IndexOf (T item, int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + if ((uint)index + (uint)count > (uint)Count) + throw new ArgumentOutOfRangeException("index and count exceed length of list"); + + return Array.IndexOf(Items, item, index, count); + } + + private void Shift (int start, int delta) { + if (delta < 0) + start -= delta; + + if (start < Count) + Array.Copy(Items, start, Items, start + delta, Count - start); + + Count += delta; + + if (delta < 0) + Array.Clear(Items, Count, -delta); + } + + private void CheckIndex (int index) { + if (index < 0 || (uint)index > (uint)Count) + throw new ArgumentOutOfRangeException("index"); + } + + public void Insert (int index, T item) { + CheckIndex(index); + if (Count == Items.Length) + GrowIfNeeded(1); + Shift(index, 1); + Items[index] = item; + version++; + } + + private void CheckCollection (IEnumerable collection) { + if (collection == null) + throw new ArgumentNullException("collection"); + } + + public void InsertRange (int index, IEnumerable collection) { + CheckCollection(collection); + CheckIndex(index); + if (collection == this) { + T[] buffer = new T[Count]; + CopyTo(buffer, 0); + GrowIfNeeded(Count); + Shift(index, buffer.Length); + Array.Copy(buffer, 0, Items, index, buffer.Length); + } else { + ICollection c = collection as ICollection; + if (c != null) + InsertCollection(index, c); + else + InsertEnumeration(index, collection); + } + version++; + } + + private void InsertCollection (int index, ICollection collection) { + int collectionCount = collection.Count; + GrowIfNeeded(collectionCount); + + Shift(index, collectionCount); + collection.CopyTo(Items, index); + } + + private void InsertEnumeration (int index, IEnumerable enumerable) { + foreach (T t in enumerable) + Insert(index++, t); + } + + public int LastIndexOf (T item) { + return Array.LastIndexOf(Items, item, Count - 1, Count); + } + + public int LastIndexOf (T item, int index) { + CheckIndex(index); + return Array.LastIndexOf(Items, item, index, index + 1); + } + + public int LastIndexOf (T item, int index, int count) { + if (index < 0) + throw new ArgumentOutOfRangeException("index", index, "index is negative"); + + if (count < 0) + throw new ArgumentOutOfRangeException("count", count, "count is negative"); + + if (index - count + 1 < 0) + throw new ArgumentOutOfRangeException("count", count, "count is too large"); + + return Array.LastIndexOf(Items, item, index, count); + } + + public bool Remove (T item) { + int loc = IndexOf(item); + if (loc != -1) + RemoveAt(loc); + + return loc != -1; + } + + public int RemoveAll (Predicate match) { + CheckMatch(match); + int i = 0; + int j = 0; + + // Find the first item to remove + for (i = 0; i < Count; i++) + if (match(Items[i])) + break; + + if (i == Count) + return 0; + + version++; + + // Remove any additional items + for (j = i + 1; j < Count; j++) { + if (!match(Items[j])) + Items[i++] = Items[j]; + } + if (j - i > 0) + Array.Clear(Items, i, j - i); + + Count = i; + return (j - i); + } + + public void RemoveAt (int index) { + if (index < 0 || (uint)index >= (uint)Count) + throw new ArgumentOutOfRangeException("index"); + Shift(index, -1); + Array.Clear(Items, Count, 1); + version++; + } + + // Spine Added Method + // Based on Stack.Pop(); https://referencesource.microsoft.com/#mscorlib/system/collections/stack.cs + /// Pops the last item of the list. If the list is empty, Pop throws an InvalidOperationException. + public T Pop () { + if (Count == 0) + throw new InvalidOperationException("List is empty. Nothing to pop."); + + int i = Count - 1; + T item = Items[i]; + Items[i] = default(T); + Count--; + version++; + return item; + } + + public void RemoveRange (int index, int count) { + CheckRange(index, count); + if (count > 0) { + Shift(index, -count); + Array.Clear(Items, Count, count); + version++; + } + } + + public void Reverse () { + Array.Reverse(Items, 0, Count); + version++; + } + + public void Reverse (int index, int count) { + CheckRange(index, count); + Array.Reverse(Items, index, count); + version++; + } + + public void Sort () { + Array.Sort(Items, 0, Count, Comparer.Default); + version++; + } + + public void Sort (IComparer comparer) { + Array.Sort(Items, 0, Count, comparer); + version++; + } + + public void Sort (Comparison comparison) { + Array.Sort(Items, comparison); + version++; + } + + public void Sort (int index, int count, IComparer comparer) { + CheckRange(index, count); + Array.Sort(Items, index, count, comparer); + version++; + } + + public T[] ToArray () { + T[] t = new T[Count]; + Array.Copy(Items, t, Count); + + return t; + } + + public void TrimExcess () { + Capacity = Count; + } + + public bool TrueForAll (Predicate match) { + CheckMatch(match); + + for (int i = 0; i < Count; i++) + if (!match(Items[i])) + return false; + + return true; + } + + public int Capacity { + get { + return Items.Length; + } + set { + if ((uint)value < (uint)Count) + throw new ArgumentOutOfRangeException(); + + Array.Resize(ref Items, value); + } + } + + #region Interface implementations. + + IEnumerator IEnumerable.GetEnumerator () { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator () { + return GetEnumerator(); + } + + #endregion + + public struct Enumerator : IEnumerator, IDisposable { + private ExposedList l; + private int next; + private int ver; + private T current; + + internal Enumerator (ExposedList l) + : this() { + this.l = l; + ver = l.version; + } + + public void Dispose () { + l = null; + } + + private void VerifyState () { + if (l == null) + throw new ObjectDisposedException(GetType().FullName); + if (ver != l.version) + throw new InvalidOperationException( + "Collection was modified; enumeration operation may not execute."); + } + + public bool MoveNext () { + VerifyState(); + + if (next < 0) + return false; + + if (next < l.Count) { + current = l.Items[next++]; + return true; + } + + next = -1; + return false; + } + + public T Current { + get { + return current; + } + } + + void IEnumerator.Reset () { + VerifyState(); + next = 0; + } + + object IEnumerator.Current { + get { + VerifyState(); + if (next <= 0) + throw new InvalidOperationException(); + return current; + } + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IUpdatable.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IUpdatable.cs new file mode 100644 index 0000000..5e3a80e --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IUpdatable.cs @@ -0,0 +1,42 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +namespace Spine4_1_00 { + + ///The interface for items updated by . + public interface IUpdatable { + void Update (); + + ///Returns false when this item has not been updated because a skin is required and the active + /// skin does not contain this item. + /// + /// + bool Active { get; } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IkConstraint.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IkConstraint.cs new file mode 100644 index 0000000..4fbb645 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IkConstraint.cs @@ -0,0 +1,368 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// + /// + /// Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of + /// the last bone is as close to the target bone as possible. + /// + /// See IK constraints in the Spine User Guide. + /// + public class IkConstraint : IUpdatable { + internal readonly IkConstraintData data; + internal readonly ExposedList bones = new ExposedList(); + internal Bone target; + internal int bendDirection; + internal bool compress, stretch; + internal float mix = 1, softness; + + internal bool active; + + public IkConstraint (IkConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + mix = data.mix; + softness = data.softness; + bendDirection = data.bendDirection; + compress = data.compress; + stretch = data.stretch; + + bones = new ExposedList(data.bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.bones.Items[boneData.index]); + target = skeleton.bones.Items[data.target.index]; + } + + /// Copy constructor. + public IkConstraint (IkConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.Bones.Count); + foreach (Bone bone in constraint.Bones) + bones.Add(skeleton.Bones.Items[bone.data.index]); + target = skeleton.Bones.Items[constraint.target.data.index]; + mix = constraint.mix; + softness = constraint.softness; + bendDirection = constraint.bendDirection; + compress = constraint.compress; + stretch = constraint.stretch; + } + + public void Update () { + if (mix == 0) return; + Bone target = this.target; + var bones = this.bones.Items; + switch (this.bones.Count) { + case 1: + Apply(bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix); + break; + case 2: + Apply(bones[0], bones[1], target.worldX, target.worldY, bendDirection, stretch, data.uniform, softness, mix); + break; + } + } + + /// The bones that will be modified by this IK constraint. + public ExposedList Bones { + get { return bones; } + } + + /// The bone that is the IK target. + public Bone Target { + get { return target; } + set { target = value; } + } + + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + /// + /// For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. + /// + public float Mix { + get { return mix; } + set { mix = value; } + } + + /// For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones + /// will not straighten completely until the target is this far out of range. + public float Softness { + get { return softness; } + set { softness = value; } + } + + /// For two bone IK, controls the bend direction of the IK bones, either 1 or -1. + public int BendDirection { + get { return bendDirection; } + set { bendDirection = value; } + } + + /// For one bone IK, when true and the target is too close, the bone is scaled to reach it. + public bool Compress { + get { return compress; } + set { compress = value; } + } + + /// When true and the target is out of range, the parent bone is scaled to reach it. + /// + /// For two bone IK: 1) the child bone's local Y translation is set to 0, + /// 2) stretch is not applied if is > 0, + /// and 3) if the parent bone has local nonuniform scale, stretch is not applied. + /// + public bool Stretch { + get { return stretch; } + set { stretch = value; } + } + + public bool Active { + get { return active; } + } + + /// The IK constraint's setup pose data. + public IkConstraintData Data { + get { return data; } + } + + override public string ToString () { + return data.name; + } + + /// Applies 1 bone IK. The target is specified in the world coordinate system. + static public void Apply (Bone bone, float targetX, float targetY, bool compress, bool stretch, bool uniform, + float alpha) { + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + Bone p = bone.parent; + + float pa = p.a, pb = p.b, pc = p.c, pd = p.d; + float rotationIK = -bone.ashearX - bone.arotation; + float tx = 0, ty = 0; + + switch (bone.data.transformMode) { + case TransformMode.OnlyTranslation: + tx = targetX - bone.worldX; + ty = targetY - bone.worldY; + break; + case TransformMode.NoRotationOrReflection: { + float s = Math.Abs(pa * pd - pb * pc) / (pa * pa + pc * pc); + float sa = pa / bone.skeleton.ScaleX; + float sc = pc / bone.skeleton.ScaleY; + pb = -sc * s * bone.skeleton.ScaleX; + pd = sa * s * bone.skeleton.ScaleY; + rotationIK += (float)Math.Atan2(sc, sa) * MathUtils.RadDeg; + goto default; // Fall through. + } + default: { + float x = targetX - p.worldX, y = targetY - p.worldY; + float d = pa * pd - pb * pc; + tx = (x * pd - y * pb) / d - bone.ax; + ty = (y * pa - x * pc) / d - bone.ay; + break; + } + } + + rotationIK += (float)Math.Atan2(ty, tx) * MathUtils.RadDeg; + if (bone.ascaleX < 0) rotationIK += 180; + if (rotationIK > 180) + rotationIK -= 360; + else if (rotationIK < -180) // + rotationIK += 360; + + float sx = bone.ascaleX, sy = bone.ascaleY; + if (compress || stretch) { + switch (bone.data.transformMode) { + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: + tx = targetX - bone.worldX; + ty = targetY - bone.worldY; + break; + } + float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty); + if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) { + float s = (dd / b - 1) * alpha + 1; + sx *= s; + if (uniform) sy *= s; + } + } + bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY); + } + + /// Applies 2 bone IK. The target is specified in the world coordinate system. + /// A direct descendant of the parent bone. + static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, bool uniform, + float softness, float alpha) { + if (parent == null) throw new ArgumentNullException("parent", "parent cannot be null."); + if (child == null) throw new ArgumentNullException("child", "child cannot be null."); + float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, sx = psx, sy = psy, csx = child.ascaleX; + int os1, os2, s2; + if (psx < 0) { + psx = -psx; + os1 = 180; + s2 = -1; + } else { + os1 = 0; + s2 = 1; + } + if (psy < 0) { + psy = -psy; + s2 = -s2; + } + if (csx < 0) { + csx = -csx; + os2 = 180; + } else + os2 = 0; + float cx = child.ax, cy, cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d; + bool u = Math.Abs(psx - psy) <= 0.0001f; + if (!u || stretch) { + cy = 0; + cwx = a * cx + parent.worldX; + cwy = c * cx + parent.worldY; + } else { + cy = child.ay; + cwx = a * cx + b * cy + parent.worldX; + cwy = c * cx + d * cy + parent.worldY; + } + Bone pp = parent.parent; + a = pp.a; + b = pp.b; + c = pp.c; + d = pp.d; + float id = 1 / (a * d - b * c), x = cwx - pp.worldX, y = cwy - pp.worldY; + float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py; + float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2; + if (l1 < 0.0001f) { + Apply(parent, targetX, targetY, false, stretch, false, alpha); + child.UpdateWorldTransform(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + return; + } + x = targetX - pp.worldX; + y = targetY - pp.worldY; + float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py; + float dd = tx * tx + ty * ty; + if (softness != 0) { + softness *= psx * (csx + 1) * 0.5f; + float td = (float)Math.Sqrt(dd), sd = td - l1 - l2 * psx + softness; + if (sd > 0) { + float p = Math.Min(1, sd / (softness * 2)) - 1; + p = (sd - softness * (1 - p * p)) / td; + tx -= p * tx; + ty -= p * ty; + dd = tx * tx + ty * ty; + } + } + if (u) { + l2 *= psx; + float cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2); + if (cos < -1) { + cos = -1; + a2 = MathUtils.PI * bendDir; + } else if (cos > 1) { + cos = 1; + a2 = 0; + if (stretch) { + a = ((float)Math.Sqrt(dd) / (l1 + l2) - 1) * alpha + 1; + sx *= a; + if (uniform) sy *= a; + } + } else + a2 = (float)Math.Acos(cos) * bendDir; + a = l1 + l2 * cos; + b = l2 * (float)Math.Sin(a2); + a1 = (float)Math.Atan2(ty * a - tx * b, tx * a + ty * b); + } else { + a = psx * l2; + b = psy * l2; + float aa = a * a, bb = b * b, ta = (float)Math.Atan2(ty, tx); + c = bb * l1 * l1 + aa * dd - aa * bb; + float c1 = -2 * bb * l1, c2 = bb - aa; + d = c1 * c1 - 4 * c2 * c; + if (d >= 0) { + float q = (float)Math.Sqrt(d); + if (c1 < 0) q = -q; + q = -(c1 + q) * 0.5f; + float r0 = q / c2, r1 = c / q; + float r = Math.Abs(r0) < Math.Abs(r1) ? r0 : r1; + if (r * r <= dd) { + y = (float)Math.Sqrt(dd - r * r) * bendDir; + a1 = ta - (float)Math.Atan2(y, r); + a2 = (float)Math.Atan2(y / psy, (r - l1) / psx); + goto break_outer; // break outer; + } + } + float minAngle = MathUtils.PI, minX = l1 - a, minDist = minX * minX, minY = 0; + float maxAngle = 0, maxX = l1 + a, maxDist = maxX * maxX, maxY = 0; + c = -a * l1 / (aa - bb); + if (c >= -1 && c <= 1) { + c = (float)Math.Acos(c); + x = a * (float)Math.Cos(c) + l1; + y = b * (float)Math.Sin(c); + d = x * x + y * y; + if (d < minDist) { + minAngle = c; + minDist = d; + minX = x; + minY = y; + } + if (d > maxDist) { + maxAngle = c; + maxDist = d; + maxX = x; + maxY = y; + } + } + if (dd <= (minDist + maxDist) * 0.5f) { + a1 = ta - (float)Math.Atan2(minY * bendDir, minX); + a2 = minAngle * bendDir; + } else { + a1 = ta - (float)Math.Atan2(maxY * bendDir, maxX); + a2 = maxAngle * bendDir; + } + } + break_outer: + float os = (float)Math.Atan2(cy, cx) * s2; + float rotation = parent.arotation; + a1 = (a1 - os) * MathUtils.RadDeg + os1 - rotation; + if (a1 > 180) + a1 -= 360; + else if (a1 < -180) + a1 += 360; + parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, sx, sy, 0, 0); + rotation = child.arotation; + a2 = ((a2 + os) * MathUtils.RadDeg - child.ashearX) * s2 + os2 - rotation; + if (a2 > 180) + a2 -= 360; + else if (a2 < -180) + a2 += 360; + child.UpdateWorldTransform(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IkConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IkConstraintData.cs new file mode 100644 index 0000000..1b26077 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/IkConstraintData.cs @@ -0,0 +1,103 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace Spine4_1_00 { + /// Stores the setup pose for an IkConstraint. + public class IkConstraintData : ConstraintData { + internal ExposedList bones = new ExposedList(); + internal BoneData target; + internal int bendDirection = 1; + internal bool compress, stretch, uniform; + internal float mix = 1, softness; + + public IkConstraintData (string name) : base(name) { + } + + /// The bones that are constrained by this IK Constraint. + public ExposedList Bones { + get { return bones; } + } + + /// The bone that is the IK target. + public BoneData Target { + get { return target; } + set { target = value; } + } + + /// + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + /// + /// For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. + /// + public float Mix { + get { return mix; } + set { mix = value; } + } + + /// For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones + /// will not straighten completely until the target is this far out of range. + public float Softness { + get { return softness; } + set { softness = value; } + } + + /// For two bone IK, controls the bend direction of the IK bones, either 1 or -1. + public int BendDirection { + get { return bendDirection; } + set { bendDirection = value; } + } + + /// For one bone IK, when true and the target is too close, the bone is scaled to reach it. + public bool Compress { + get { return compress; } + set { compress = value; } + } + + /// When true and the target is out of range, the parent bone is scaled to reach it. + /// + /// For two bone IK: 1) the child bone's local Y translation is set to 0, + /// 2) stretch is not applied if is > 0, + /// and 3) if the parent bone has local nonuniform scale, stretch is not applied. + public bool Stretch { + get { return stretch; } + set { stretch = value; } + } + + /// + /// When true and or is used, the bone is scaled on both the X and Y axes. + /// + public bool Uniform { + get { return uniform; } + set { uniform = value; } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Json.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Json.cs new file mode 100644 index 0000000..30786fc --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Json.cs @@ -0,0 +1,517 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + +namespace Spine4_1_00 { + public static class Json { + public static object Deserialize (TextReader text) { + var parser = new SharpJson.JsonDecoder(); + parser.parseNumbersAsFloat = true; + return parser.Decode(text.ReadToEnd()); + } + } +} + +/** + * Copyright (c) 2016 Adriano Tinoco d'Oliveira Rezende + * + * Based on the JSON parser by Patrick van Bergen + * http://techblog.procurios.nl/k/news/view/14605/14863/how-do-i-write-my-own-parser-(for-json).html + * + * Changes made: + * + * - Optimized parser speed (deserialize roughly near 3x faster than original) + * - Added support to handle lexer/parser error messages with line numbers + * - Added more fine grained control over type conversions during the parsing + * - Refactory API (Separate Lexer code from Parser code and the Encoder from Decoder) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +namespace Spine4_1_00 { + class Lexer { + public enum Token { + None, + Null, + True, + False, + Colon, + Comma, + String, + Number, + CurlyOpen, + CurlyClose, + SquaredOpen, + SquaredClose, + }; + + public bool hasError { + get { + return !success; + } + } + + public int lineNumber { + get; + private set; + } + + public bool parseNumbersAsFloat { + get; + set; + } + + char[] json; + int index = 0; + bool success = true; + char[] stringBuffer = new char[4096]; + + public Lexer (string text) { + Reset(); + + json = text.ToCharArray(); + parseNumbersAsFloat = false; + } + + public void Reset () { + index = 0; + lineNumber = 1; + success = true; + } + + public string ParseString () { + int idx = 0; + StringBuilder builder = null; + + SkipWhiteSpaces(); + + // " + char c = json[index++]; + + bool failed = false; + bool complete = false; + + while (!complete && !failed) { + if (index == json.Length) + break; + + c = json[index++]; + if (c == '"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.Length) + break; + + c = json[index++]; + + switch (c) { + case '"': + stringBuffer[idx++] = '"'; + break; + case '\\': + stringBuffer[idx++] = '\\'; + break; + case '/': + stringBuffer[idx++] = '/'; + break; + case 'b': + stringBuffer[idx++] = '\b'; + break; + case 'f': + stringBuffer[idx++] = '\f'; + break; + case 'n': + stringBuffer[idx++] = '\n'; + break; + case 'r': + stringBuffer[idx++] = '\r'; + break; + case 't': + stringBuffer[idx++] = '\t'; + break; + case 'u': + int remainingLength = json.Length - index; + if (remainingLength >= 4) { + var hex = new string(json, index, 4); + + // XXX: handle UTF + stringBuffer[idx++] = (char)Convert.ToInt32(hex, 16); + + // skip 4 chars + index += 4; + } else { + failed = true; + } + break; + } + } else { + stringBuffer[idx++] = c; + } + + if (idx >= stringBuffer.Length) { + if (builder == null) + builder = new StringBuilder(); + + builder.Append(stringBuffer, 0, idx); + idx = 0; + } + } + + if (!complete) { + success = false; + return null; + } + + if (builder != null) + return builder.ToString(); + else + return new string(stringBuffer, 0, idx); + } + + string GetNumberString () { + SkipWhiteSpaces(); + + int lastIndex = GetLastIndexOfNumber(index); + int charLength = (lastIndex - index) + 1; + + var result = new string(json, index, charLength); + + index = lastIndex + 1; + + return result; + } + + public float ParseFloatNumber () { + float number; + var str = GetNumberString(); + + if (!float.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out number)) + return 0; + + return number; + } + + public double ParseDoubleNumber () { + double number; + var str = GetNumberString(); + + if (!double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) + return 0; + + return number; + } + + int GetLastIndexOfNumber (int index) { + int lastIndex; + + for (lastIndex = index; lastIndex < json.Length; lastIndex++) { + char ch = json[lastIndex]; + + if ((ch < '0' || ch > '9') && ch != '+' && ch != '-' + && ch != '.' && ch != 'e' && ch != 'E') + break; + } + + return lastIndex - 1; + } + + void SkipWhiteSpaces () { + for (; index < json.Length; index++) { + char ch = json[index]; + + if (ch == '\n') + lineNumber++; + + if (!char.IsWhiteSpace(json[index])) + break; + } + } + + public Token LookAhead () { + SkipWhiteSpaces(); + + int savedIndex = index; + return NextToken(json, ref savedIndex); + } + + public Token NextToken () { + SkipWhiteSpaces(); + return NextToken(json, ref index); + } + + static Token NextToken (char[] json, ref int index) { + if (index == json.Length) + return Token.None; + + char c = json[index++]; + + switch (c) { + case '{': + return Token.CurlyOpen; + case '}': + return Token.CurlyClose; + case '[': + return Token.SquaredOpen; + case ']': + return Token.SquaredClose; + case ',': + return Token.Comma; + case '"': + return Token.String; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return Token.Number; + case ':': + return Token.Colon; + } + + index--; + + int remainingLength = json.Length - index; + + // false + if (remainingLength >= 5) { + if (json[index] == 'f' && + json[index + 1] == 'a' && + json[index + 2] == 'l' && + json[index + 3] == 's' && + json[index + 4] == 'e') { + index += 5; + return Token.False; + } + } + + // true + if (remainingLength >= 4) { + if (json[index] == 't' && + json[index + 1] == 'r' && + json[index + 2] == 'u' && + json[index + 3] == 'e') { + index += 4; + return Token.True; + } + } + + // null + if (remainingLength >= 4) { + if (json[index] == 'n' && + json[index + 1] == 'u' && + json[index + 2] == 'l' && + json[index + 3] == 'l') { + index += 4; + return Token.Null; + } + } + + return Token.None; + } + } + + public class JsonDecoder { + public string errorMessage { + get; + private set; + } + + public bool parseNumbersAsFloat { + get; + set; + } + + Lexer lexer; + + public JsonDecoder () { + errorMessage = null; + parseNumbersAsFloat = false; + } + + public object Decode (string text) { + errorMessage = null; + + lexer = new Lexer(text); + lexer.parseNumbersAsFloat = parseNumbersAsFloat; + + return ParseValue(); + } + + public static object DecodeText (string text) { + var builder = new JsonDecoder(); + return builder.Decode(text); + } + + IDictionary ParseObject () { + var table = new Dictionary(); + + // { + lexer.NextToken(); + + while (true) { + var token = lexer.LookAhead(); + + switch (token) { + case Lexer.Token.None: + TriggerError("Invalid token"); + return null; + case Lexer.Token.Comma: + lexer.NextToken(); + break; + case Lexer.Token.CurlyClose: + lexer.NextToken(); + return table; + default: + // name + string name = EvalLexer(lexer.ParseString()); + + if (errorMessage != null) + return null; + + // : + token = lexer.NextToken(); + + if (token != Lexer.Token.Colon) { + TriggerError("Invalid token; expected ':'"); + return null; + } + + // value + object value = ParseValue(); + + if (errorMessage != null) + return null; + + table[name] = value; + break; + } + } + + //return null; // Unreachable code + } + + IList ParseArray () { + var array = new List(); + + // [ + lexer.NextToken(); + + while (true) { + var token = lexer.LookAhead(); + + switch (token) { + case Lexer.Token.None: + TriggerError("Invalid token"); + return null; + case Lexer.Token.Comma: + lexer.NextToken(); + break; + case Lexer.Token.SquaredClose: + lexer.NextToken(); + return array; + default: + object value = ParseValue(); + + if (errorMessage != null) + return null; + + array.Add(value); + break; + } + } + + //return null; // Unreachable code + } + + object ParseValue () { + switch (lexer.LookAhead()) { + case Lexer.Token.String: + return EvalLexer(lexer.ParseString()); + case Lexer.Token.Number: + if (parseNumbersAsFloat) + return EvalLexer(lexer.ParseFloatNumber()); + else + return EvalLexer(lexer.ParseDoubleNumber()); + case Lexer.Token.CurlyOpen: + return ParseObject(); + case Lexer.Token.SquaredOpen: + return ParseArray(); + case Lexer.Token.True: + lexer.NextToken(); + return true; + case Lexer.Token.False: + lexer.NextToken(); + return false; + case Lexer.Token.Null: + lexer.NextToken(); + return null; + case Lexer.Token.None: + break; + } + + TriggerError("Unable to parse value"); + return null; + } + + void TriggerError (string message) { + errorMessage = string.Format("Error: '{0}' at line {1}", + message, lexer.lineNumber); + } + + T EvalLexer (T value) { + if (lexer.hasError) + TriggerError("Lexical error ocurred"); + + return value; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/MathUtils.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/MathUtils.cs new file mode 100644 index 0000000..8ff09bc --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/MathUtils.cs @@ -0,0 +1,173 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +//#define USE_FAST_SIN_COS_ATAN2_APPROXIMATIONS + +using System; + +namespace Spine4_1_00 { + public static class MathUtils { + public const float PI = 3.1415927f; + public const float PI2 = PI * 2; + public const float RadDeg = 180f / PI; + public const float DegRad = PI / 180; + + static Random random = new Random(); + +#if USE_FAST_SIN_COS_ATAN2_APPROXIMATIONS + const int SIN_BITS = 14; // 16KB. Adjust for accuracy. + const int SIN_MASK = ~(-1 << SIN_BITS); + const int SIN_COUNT = SIN_MASK + 1; + const float RadFull = PI * 2; + const float DegFull = 360; + const float RadToIndex = SIN_COUNT / RadFull; + const float DegToIndex = SIN_COUNT / DegFull; + static float[] sin = new float[SIN_COUNT]; + + static MathUtils () { + for (int i = 0; i < SIN_COUNT; i++) + sin[i] = (float)Math.Sin((i + 0.5f) / SIN_COUNT * RadFull); + for (int i = 0; i < 360; i += 90) + sin[(int)(i * DegToIndex) & SIN_MASK] = (float)Math.Sin(i * DegRad); + } + + /// Returns the sine of a given angle in radians from a lookup table. + static public float Sin (float radians) { + return sin[(int)(radians * RadToIndex) & SIN_MASK]; + } + + /// Returns the cosine of a given angle in radians from a lookup table. + static public float Cos (float radians) { + return sin[(int)((radians + PI / 2) * RadToIndex) & SIN_MASK]; + } + + /// Returns the sine of a given angle in degrees from a lookup table. + static public float SinDeg (float degrees) { + return sin[(int)(degrees * DegToIndex) & SIN_MASK]; + } + + /// Returns the cosine of a given angle in degrees from a lookup table. + static public float CosDeg (float degrees) { + return sin[(int)((degrees + 90) * DegToIndex) & SIN_MASK]; + } + + /// Returns atan2 in radians, faster but less accurate than Math.Atan2. Average error of 0.00231 radians (0.1323 + /// degrees), largest error of 0.00488 radians (0.2796 degrees). + static public float Atan2 (float y, float x) { + if (x == 0f) { + if (y > 0f) return PI / 2; + if (y == 0f) return 0f; + return -PI / 2; + } + float atan, z = y / x; + if (Math.Abs(z) < 1f) { + atan = z / (1f + 0.28f * z * z); + if (x < 0f) return atan + (y < 0f ? -PI : PI); + return atan; + } + atan = PI / 2 - z / (z * z + 0.28f); + return y < 0f ? atan - PI : atan; + } +#else + /// Returns the sine of a given angle in radians. + static public float Sin (float radians) { + return (float)Math.Sin(radians); + } + + /// Returns the cosine of a given angle in radians. + static public float Cos (float radians) { + return (float)Math.Cos(radians); + } + + /// Returns the sine of a given angle in degrees. + static public float SinDeg (float degrees) { + return (float)Math.Sin(degrees * DegRad); + } + + /// Returns the cosine of a given angle in degrees. + static public float CosDeg (float degrees) { + return (float)Math.Cos(degrees * DegRad); + } + + /// Returns the atan2 using Math.Atan2. + static public float Atan2 (float y, float x) { + return (float)Math.Atan2(y, x); + } +#endif + static public float Clamp (float value, float min, float max) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + static public float RandomTriangle (float min, float max) { + return RandomTriangle(min, max, (min + max) * 0.5f); + } + + static public float RandomTriangle (float min, float max, float mode) { + float u = (float)random.NextDouble(); + float d = max - min; + if (u <= (mode - min) / d) return min + (float)Math.Sqrt(u * d * (mode - min)); + return max - (float)Math.Sqrt((1 - u) * d * (max - mode)); + } + } + + public abstract class IInterpolation { + public static IInterpolation Pow2 = new Pow(2); + public static IInterpolation Pow2Out = new PowOut(2); + + protected abstract float Apply (float a); + + public float Apply (float start, float end, float a) { + return start + (end - start) * Apply(a); + } + } + + public class Pow : IInterpolation { + public float Power { get; set; } + + public Pow (float power) { + Power = power; + } + + protected override float Apply (float a) { + if (a <= 0.5f) return (float)Math.Pow(a * 2, Power) / 2; + return (float)Math.Pow((a - 1) * 2, Power) / (Power % 2 == 0 ? -2 : 2) + 1; + } + } + + public class PowOut : Pow { + public PowOut (float power) : base(power) { + } + + protected override float Apply (float a) { + return (float)Math.Pow(a - 1, Power) * (Power % 2 == 0 ? -1 : 1) + 1; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/PathConstraint.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/PathConstraint.cs new file mode 100644 index 0000000..cd1c56a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/PathConstraint.cs @@ -0,0 +1,514 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + + /// + /// + /// Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the + /// constrained bones so they follow a . + /// + /// See Path constraints in the Spine User Guide. + /// + public class PathConstraint : IUpdatable { + const int NONE = -1, BEFORE = -2, AFTER = -3; + const float Epsilon = 0.00001f; + + internal readonly PathConstraintData data; + internal readonly ExposedList bones; + internal Slot target; + internal float position, spacing, mixRotate, mixX, mixY; + + internal bool active; + + internal readonly ExposedList spaces = new ExposedList(), positions = new ExposedList(); + internal readonly ExposedList world = new ExposedList(), curves = new ExposedList(), lengths = new ExposedList(); + internal readonly float[] segments = new float[10]; + + public PathConstraint (PathConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + bones = new ExposedList(data.Bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.bones.Items[boneData.index]); + target = skeleton.slots.Items[data.target.index]; + position = data.position; + spacing = data.spacing; + mixRotate = data.mixRotate; + mixX = data.mixX; + mixY = data.mixY; + } + + /// Copy constructor. + public PathConstraint (PathConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.bones.Count); + foreach (Bone bone in constraint.bones) + bones.Add(skeleton.bones.Items[bone.data.index]); + target = skeleton.slots.Items[constraint.target.data.index]; + position = constraint.position; + spacing = constraint.spacing; + mixRotate = constraint.mixRotate; + mixX = constraint.mixX; + mixY = constraint.mixY; + } + + public static void ArraysFill (float[] a, int fromIndex, int toIndex, float val) { + for (int i = fromIndex; i < toIndex; i++) + a[i] = val; + } + + public void Update () { + PathAttachment attachment = target.Attachment as PathAttachment; + if (attachment == null) return; + + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY; + if (mixRotate == 0 && mixX == 0 && mixY == 0) return; + + PathConstraintData data = this.data; + bool tangents = data.rotateMode == RotateMode.Tangent, scale = data.rotateMode == RotateMode.ChainScale; + int boneCount = this.bones.Count, spacesCount = tangents ? boneCount : boneCount + 1; + Bone[] bonesItems = this.bones.Items; + float[] spaces = this.spaces.Resize(spacesCount).Items, lengths = scale ? this.lengths.Resize(boneCount).Items : null; + float spacing = this.spacing; + switch (data.spacingMode) { + case SpacingMode.Percent: + if (scale) { + for (int i = 0, n = spacesCount - 1; i < n; i++) { + Bone bone = bonesItems[i]; + float setupLength = bone.data.length; + if (setupLength < PathConstraint.Epsilon) + lengths[i] = 0; + else { + float x = setupLength * bone.a, y = setupLength * bone.c; + lengths[i] = (float)Math.Sqrt(x * x + y * y); + } + } + } + ArraysFill(spaces, 1, spacesCount, spacing); + break; + case SpacingMode.Proportional: { + float sum = 0; + for (int i = 0, n = spacesCount - 1; i < n;) { + Bone bone = bonesItems[i]; + float setupLength = bone.data.length; + if (setupLength < PathConstraint.Epsilon) { + if (scale) lengths[i] = 0; + spaces[++i] = spacing; + } else { + float x = setupLength * bone.a, y = setupLength * bone.c; + float length = (float)Math.Sqrt(x * x + y * y); + if (scale) lengths[i] = length; + spaces[++i] = length; + sum += length; + } + } + if (sum > 0) { + sum = spacesCount / sum * spacing; + for (int i = 1; i < spacesCount; i++) + spaces[i] *= sum; + } + break; + } + default: { + bool lengthSpacing = data.spacingMode == SpacingMode.Length; + for (int i = 0, n = spacesCount - 1; i < n;) { + Bone bone = bonesItems[i]; + float setupLength = bone.data.length; + if (setupLength < PathConstraint.Epsilon) { + if (scale) lengths[i] = 0; + spaces[++i] = spacing; + } else { + float x = setupLength * bone.a, y = setupLength * bone.c; + float length = (float)Math.Sqrt(x * x + y * y); + if (scale) lengths[i] = length; + spaces[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength; + } + } + break; + } + } + + float[] positions = ComputeWorldPositions(attachment, spacesCount, tangents); + float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation; + bool tip; + if (offsetRotation == 0) { + tip = data.rotateMode == RotateMode.Chain; + } else { + tip = false; + Bone p = target.bone; + offsetRotation *= p.a * p.d - p.b * p.c > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + } + for (int i = 0, p = 3; i < boneCount; i++, p += 3) { + Bone bone = bonesItems[i]; + bone.worldX += (boneX - bone.worldX) * mixX; + bone.worldY += (boneY - bone.worldY) * mixY; + float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY; + if (scale) { + float length = lengths[i]; + if (length >= PathConstraint.Epsilon) { + float s = ((float)Math.Sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1; + bone.a *= s; + bone.c *= s; + } + } + boneX = x; + boneY = y; + if (mixRotate > 0) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d, r, cos, sin; + if (tangents) + r = positions[p - 1]; + else if (spaces[i + 1] < PathConstraint.Epsilon) + r = positions[p + 2]; + else + r = MathUtils.Atan2(dy, dx); + r -= MathUtils.Atan2(c, a); + if (tip) { + cos = MathUtils.Cos(r); + sin = MathUtils.Sin(r); + float length = bone.data.length; + boneX += (length * (cos * a - sin * c) - dx) * mixRotate; + boneY += (length * (sin * a + cos * c) - dy) * mixRotate; + } else + r += offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r *= mixRotate; + cos = MathUtils.Cos(r); + sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + bone.UpdateAppliedTransform(); + } + } + + float[] ComputeWorldPositions (PathAttachment path, int spacesCount, bool tangents) { + Slot target = this.target; + float position = this.position; + float[] spaces = this.spaces.Items, output = this.positions.Resize(spacesCount * 3 + 2).Items, world; + bool closed = path.Closed; + int verticesLength = path.WorldVerticesLength, curveCount = verticesLength / 6, prevCurve = NONE; + + float pathLength, multiplier; + if (!path.ConstantSpeed) { + float[] lengths = path.Lengths; + curveCount -= closed ? 1 : 2; + pathLength = lengths[curveCount]; + + if (data.positionMode == PositionMode.Percent) position *= pathLength; + + switch (data.spacingMode) { + case SpacingMode.Percent: + multiplier = pathLength; + break; + case SpacingMode.Proportional: + multiplier = pathLength / spacesCount; + break; + default: + multiplier = 1; + break; + } + + world = this.world.Resize(8).Items; + for (int i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) { + float space = spaces[i] * multiplier; + position += space; + float p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + if (prevCurve != BEFORE) { + prevCurve = BEFORE; + path.ComputeWorldVertices(target, 2, 4, world, 0, 2); + } + AddBeforePosition(p, world, 0, output, o); + continue; + } else if (p > pathLength) { + if (prevCurve != AFTER) { + prevCurve = AFTER; + path.ComputeWorldVertices(target, verticesLength - 6, 4, world, 0, 2); + } + AddAfterPosition(p - pathLength, world, 0, output, o); + continue; + } + + // Determine curve containing position. + for (; ; curve++) { + float length = lengths[curve]; + if (p > length) continue; + if (curve == 0) + p /= length; + else { + float prev = lengths[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + if (curve != prevCurve) { + prevCurve = curve; + if (closed && curve == curveCount) { + path.ComputeWorldVertices(target, verticesLength - 4, 4, world, 0, 2); + path.ComputeWorldVertices(target, 0, 4, world, 4, 2); + } else + path.ComputeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2); + } + AddCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], output, o, + tangents || (i > 0 && space < PathConstraint.Epsilon)); + } + return output; + } + + // World vertices. + if (closed) { + verticesLength += 2; + world = this.world.Resize(verticesLength).Items; + path.ComputeWorldVertices(target, 2, verticesLength - 4, world, 0, 2); + path.ComputeWorldVertices(target, 0, 2, world, verticesLength - 4, 2); + world[verticesLength - 2] = world[0]; + world[verticesLength - 1] = world[1]; + } else { + curveCount--; + verticesLength -= 4; + world = this.world.Resize(verticesLength).Items; + path.ComputeWorldVertices(target, 2, verticesLength, world, 0, 2); + } + + // Curve lengths. + float[] curves = this.curves.Resize(curveCount).Items; + pathLength = 0; + float x1 = world[0], y1 = world[1], cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0; + float tmpx, tmpy, dddfx, dddfy, ddfx, ddfy, dfx, dfy; + for (int i = 0, w = 2; i < curveCount; i++, w += 6) { + cx1 = world[w]; + cy1 = world[w + 1]; + cx2 = world[w + 2]; + cy2 = world[w + 3]; + x2 = world[w + 4]; + y2 = world[w + 5]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.1875f; + tmpy = (y1 - cy1 * 2 + cy2) * 0.1875f; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375f; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375f; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.75f + tmpx + dddfx * 0.16666667f; + dfy = (cy1 - y1) * 0.75f + tmpy + dddfy * 0.16666667f; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + pathLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + curves[i] = pathLength; + x1 = x2; + y1 = y2; + } + + if (data.positionMode == PositionMode.Percent) position *= pathLength; + + switch (data.spacingMode) { + case SpacingMode.Percent: + multiplier = pathLength; + break; + case SpacingMode.Proportional: + multiplier = pathLength / spacesCount; + break; + default: + multiplier = 1; + break; + } + + float[] segments = this.segments; + float curveLength = 0; + for (int i = 0, o = 0, curve = 0, segment = 0; i < spacesCount; i++, o += 3) { + float space = spaces[i] * multiplier; + position += space; + float p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + AddBeforePosition(p, world, 0, output, o); + continue; + } else if (p > pathLength) { + AddAfterPosition(p - pathLength, world, verticesLength - 4, output, o); + continue; + } + + // Determine curve containing position. + for (; ; curve++) { + float length = curves[curve]; + if (p > length) continue; + if (curve == 0) + p /= length; + else { + float prev = curves[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + + // Curve segment lengths. + if (curve != prevCurve) { + prevCurve = curve; + int ii = curve * 6; + x1 = world[ii]; + y1 = world[ii + 1]; + cx1 = world[ii + 2]; + cy1 = world[ii + 3]; + cx2 = world[ii + 4]; + cy2 = world[ii + 5]; + x2 = world[ii + 6]; + y2 = world[ii + 7]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.03f; + tmpy = (y1 - cy1 * 2 + cy2) * 0.03f; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006f; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006f; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.3f + tmpx + dddfx * 0.16666667f; + dfy = (cy1 - y1) * 0.3f + tmpy + dddfy * 0.16666667f; + curveLength = (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[0] = curveLength; + for (ii = 1; ii < 8; ii++) { + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[ii] = curveLength; + } + dfx += ddfx; + dfy += ddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[8] = curveLength; + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + curveLength += (float)Math.Sqrt(dfx * dfx + dfy * dfy); + segments[9] = curveLength; + segment = 0; + } + + // Weight by segment length. + p *= curveLength; + for (; ; segment++) { + float length = segments[segment]; + if (p > length) continue; + if (segment == 0) + p /= length; + else { + float prev = segments[segment - 1]; + p = segment + (p - prev) / (length - prev); + } + break; + } + AddCurvePosition(p * 0.1f, x1, y1, cx1, cy1, cx2, cy2, x2, y2, output, o, tangents || (i > 0 && space < PathConstraint.Epsilon)); + } + return output; + } + + static void AddBeforePosition (float p, float[] temp, int i, float[] output, int o) { + float x1 = temp[i], y1 = temp[i + 1], dx = temp[i + 2] - x1, dy = temp[i + 3] - y1, r = MathUtils.Atan2(dy, dx); + output[o] = x1 + p * MathUtils.Cos(r); + output[o + 1] = y1 + p * MathUtils.Sin(r); + output[o + 2] = r; + } + + static void AddAfterPosition (float p, float[] temp, int i, float[] output, int o) { + float x1 = temp[i + 2], y1 = temp[i + 3], dx = x1 - temp[i], dy = y1 - temp[i + 1], r = MathUtils.Atan2(dy, dx); + output[o] = x1 + p * MathUtils.Cos(r); + output[o + 1] = y1 + p * MathUtils.Sin(r); + output[o + 2] = r; + } + + static void AddCurvePosition (float p, float x1, float y1, float cx1, float cy1, float cx2, float cy2, float x2, float y2, + float[] output, int o, bool tangents) { + if (p < PathConstraint.Epsilon || float.IsNaN(p)) { + output[o] = x1; + output[o + 1] = y1; + output[o + 2] = (float)Math.Atan2(cy1 - y1, cx1 - x1); + return; + } + float tt = p * p, ttt = tt * p, u = 1 - p, uu = u * u, uuu = uu * u; + float ut = u * p, ut3 = ut * 3, uut3 = u * ut3, utt3 = ut3 * p; + float x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt, y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt; + output[o] = x; + output[o + 1] = y; + if (tangents) { + if (p < 0.001f) + output[o + 2] = (float)Math.Atan2(cy1 - y1, cx1 - x1); + else + output[o + 2] = (float)Math.Atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)); + } + } + + /// The position along the path. + public float Position { get { return position; } set { position = value; } } + /// The spacing between bones. + public float Spacing { get { return spacing; } set { spacing = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. + public float MixRotate { get { return mixRotate; } set { mixRotate = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. + public float MixX { get { return mixX; } set { mixX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. + public float MixY { get { return mixY; } set { mixY = value; } } + /// The bones that will be modified by this path constraint. + public ExposedList Bones { get { return bones; } } + /// The slot whose path attachment will be used to constrained the bones. + public Slot Target { get { return target; } set { target = value; } } + public bool Active { get { return active; } } + /// The path constraint's setup pose data. + public PathConstraintData Data { get { return data; } } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/PathConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/PathConstraintData.cs new file mode 100644 index 0000000..a36c64a --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/PathConstraintData.cs @@ -0,0 +1,72 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + public class PathConstraintData : ConstraintData { + internal ExposedList bones = new ExposedList(); + internal SlotData target; + internal PositionMode positionMode; + internal SpacingMode spacingMode; + internal RotateMode rotateMode; + internal float offsetRotation; + internal float position, spacing, mixRotate, mixX, mixY; + + public PathConstraintData (string name) : base(name) { + } + + public ExposedList Bones { get { return bones; } } + public SlotData Target { get { return target; } set { target = value; } } + public PositionMode PositionMode { get { return positionMode; } set { positionMode = value; } } + public SpacingMode SpacingMode { get { return spacingMode; } set { spacingMode = value; } } + public RotateMode RotateMode { get { return rotateMode; } set { rotateMode = value; } } + public float OffsetRotation { get { return offsetRotation; } set { offsetRotation = value; } } + public float Position { get { return position; } set { position = value; } } + public float Spacing { get { return spacing; } set { spacing = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + public float RotateMix { get { return mixRotate; } set { mixRotate = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. + public float MixX { get { return mixX; } set { mixX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. + public float MixY { get { return mixY; } set { mixY = value; } } + } + + public enum PositionMode { + Fixed, Percent + } + + public enum SpacingMode { + Length, Fixed, Percent, Proportional + } + + public enum RotateMode { + Tangent, Chain, ChainScale + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Skeleton.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Skeleton.cs new file mode 100644 index 0000000..7af941b --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Skeleton.cs @@ -0,0 +1,724 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + public class Skeleton { + internal SkeletonData data; + internal ExposedList bones; + internal ExposedList slots; + internal ExposedList drawOrder; + internal ExposedList ikConstraints; + internal ExposedList transformConstraints; + internal ExposedList pathConstraints; + internal ExposedList springConstraints; + internal ExposedList updateCache = new ExposedList(); + internal Skin skin; + internal float r = 1, g = 1, b = 1, a = 1; + private float scaleX = 1, scaleY = 1; + internal float x, y; + + public SkeletonData Data { get { return data; } } + public ExposedList Bones { get { return bones; } } + public ExposedList UpdateCacheList { get { return updateCache; } } + public ExposedList Slots { get { return slots; } } + public ExposedList DrawOrder { get { return drawOrder; } } + public ExposedList IkConstraints { get { return ikConstraints; } } + public ExposedList PathConstraints { get { return pathConstraints; } } + public ExposedList SpringConstraints { get { return SpringConstraints; } } + public ExposedList TransformConstraints { get { return transformConstraints; } } + + public Skin Skin { + /// The skeleton's current skin. May be null. + get { return skin; } + /// Sets a skin, . + set { SetSkin(value); } + } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY * (Bone.yDown ? -1 : 1); } set { scaleY = value; } } + + [Obsolete("Use ScaleX instead. FlipX is when ScaleX is negative.")] + public bool FlipX { get { return scaleX < 0; } set { scaleX = value ? -1f : 1f; } } + + [Obsolete("Use ScaleY instead. FlipY is when ScaleY is negative.")] + public bool FlipY { get { return scaleY < 0; } set { scaleY = value ? -1f : 1f; } } + + /// Returns the root bone, or null if the skeleton has no bones. + public Bone RootBone { + get { return bones.Count == 0 ? null : bones.Items[0]; } + } + + public Skeleton (SkeletonData data) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = data; + + bones = new ExposedList(data.bones.Count); + Bone[] bonesItems = this.bones.Items; + foreach (BoneData boneData in data.bones) { + Bone bone; + if (boneData.parent == null) { + bone = new Bone(boneData, this, null); + } else { + Bone parent = bonesItems[boneData.parent.index]; + bone = new Bone(boneData, this, parent); + parent.children.Add(bone); + } + this.bones.Add(bone); + } + + slots = new ExposedList(data.slots.Count); + drawOrder = new ExposedList(data.slots.Count); + foreach (SlotData slotData in data.slots) { + Bone bone = bonesItems[slotData.boneData.index]; + Slot slot = new Slot(slotData, bone); + slots.Add(slot); + drawOrder.Add(slot); + } + + ikConstraints = new ExposedList(data.ikConstraints.Count); + foreach (IkConstraintData ikConstraintData in data.ikConstraints) + ikConstraints.Add(new IkConstraint(ikConstraintData, this)); + + transformConstraints = new ExposedList(data.transformConstraints.Count); + foreach (TransformConstraintData transformConstraintData in data.transformConstraints) + transformConstraints.Add(new TransformConstraint(transformConstraintData, this)); + + pathConstraints = new ExposedList(data.pathConstraints.Count); + foreach (PathConstraintData pathConstraintData in data.pathConstraints) + pathConstraints.Add(new PathConstraint(pathConstraintData, this)); + + springConstraints = new ExposedList(data.springConstraints.Count); + foreach (SpringConstraintData springConstraintData in data.springConstraints) + springConstraints.Add(new SpringConstraint(springConstraintData, this)); + + UpdateCache(); + } + + /// Copy constructor. + public Skeleton (Skeleton skeleton) { + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + data = skeleton.data; + + bones = new ExposedList(skeleton.bones.Count); + foreach (Bone bone in skeleton.bones) { + Bone newBone; + if (bone.parent == null) + newBone = new Bone(bone, this, null); + else { + Bone parent = bones.Items[bone.parent.data.index]; + newBone = new Bone(bone, this, parent); + parent.children.Add(newBone); + } + bones.Add(newBone); + } + + slots = new ExposedList(skeleton.slots.Count); + Bone[] bonesItems = bones.Items; + foreach (Slot slot in skeleton.slots) { + Bone bone = bonesItems[slot.bone.data.index]; + slots.Add(new Slot(slot, bone)); + } + + drawOrder = new ExposedList(slots.Count); + Slot[] slotsItems = slots.Items; + foreach (Slot slot in skeleton.drawOrder) + drawOrder.Add(slotsItems[slot.data.index]); + + ikConstraints = new ExposedList(skeleton.ikConstraints.Count); + foreach (IkConstraint ikConstraint in skeleton.ikConstraints) + ikConstraints.Add(new IkConstraint(ikConstraint, this)); + + transformConstraints = new ExposedList(skeleton.transformConstraints.Count); + foreach (TransformConstraint transformConstraint in skeleton.transformConstraints) + transformConstraints.Add(new TransformConstraint(transformConstraint, this)); + + pathConstraints = new ExposedList(skeleton.pathConstraints.Count); + foreach (PathConstraint pathConstraint in skeleton.pathConstraints) + pathConstraints.Add(new PathConstraint(pathConstraint, this)); + + springConstraints = new ExposedList(skeleton.springConstraints.Count); + foreach (SpringConstraint springConstraint in skeleton.springConstraints) + springConstraints.Add(new SpringConstraint(springConstraint, this)); + + skin = skeleton.skin; + r = skeleton.r; + g = skeleton.g; + b = skeleton.b; + a = skeleton.a; + scaleX = skeleton.scaleX; + scaleY = skeleton.scaleY; + + UpdateCache(); + } + + /// Caches information about bones and constraints. Must be called if the is modified or if bones, constraints, or + /// constraints, or weighted path attachments are added or removed. + public void UpdateCache () { + var updateCache = this.updateCache; + updateCache.Clear(); + + int boneCount = this.bones.Count; + Bone[] bones = this.bones.Items; + for (int i = 0; i < boneCount; i++) { + Bone bone = bones[i]; + bone.sorted = bone.data.skinRequired; + bone.active = !bone.sorted; + } + if (skin != null) { + BoneData[] skinBones = skin.bones.Items; + for (int i = 0, n = skin.bones.Count; i < n; i++) { + var bone = bones[skinBones[i].index]; + do { + bone.sorted = false; + bone.active = true; + bone = bone.parent; + } while (bone != null); + } + } + + int ikCount = this.ikConstraints.Count, transformCount = this.transformConstraints.Count, pathCount = this.pathConstraints.Count, + springCount = this.springConstraints.Count; + IkConstraint[] ikConstraints = this.ikConstraints.Items; + TransformConstraint[] transformConstraints = this.transformConstraints.Items; + PathConstraint[] pathConstraints = this.pathConstraints.Items; + SpringConstraint[] springConstraints = this.springConstraints.Items; + int constraintCount = ikCount + transformCount + pathCount + springCount; + for (int i = 0; i < constraintCount; i++) { + for (int ii = 0; ii < ikCount; ii++) { + IkConstraint constraint = ikConstraints[ii]; + if (constraint.data.order == i) { + SortIkConstraint(constraint); + goto continue_outer; + } + } + for (int ii = 0; ii < transformCount; ii++) { + TransformConstraint constraint = transformConstraints[ii]; + if (constraint.data.order == i) { + SortTransformConstraint(constraint); + goto continue_outer; + } + } + for (int ii = 0; ii < pathCount; ii++) { + PathConstraint constraint = pathConstraints[ii]; + if (constraint.data.order == i) { + SortPathConstraint(constraint); + goto continue_outer; + } + } + for (int ii = 0; ii < springCount; ii++) { + SpringConstraint constraint = springConstraints[ii]; + if (constraint.data.order == i) { + SortSpringConstraint(constraint); + goto continue_outer; + } + } + continue_outer: { } + } + + for (int i = 0; i < boneCount; i++) + SortBone(bones[i]); + } + + private void SortIkConstraint (IkConstraint constraint) { + constraint.active = constraint.target.active + && (!constraint.data.skinRequired || (skin != null && skin.constraints.Contains(constraint.data))); + if (!constraint.active) return; + + Bone target = constraint.target; + SortBone(target); + + var constrained = constraint.bones; + Bone parent = constrained.Items[0]; + SortBone(parent); + + if (constrained.Count == 1) { + updateCache.Add(constraint); + SortReset(parent.children); + } else { + Bone child = constrained.Items[constrained.Count - 1]; + SortBone(child); + + updateCache.Add(constraint); + + SortReset(parent.children); + child.sorted = true; + } + } + + private void SortTransformConstraint (TransformConstraint constraint) { + constraint.active = constraint.target.active + && (!constraint.data.skinRequired || (skin != null && skin.constraints.Contains(constraint.data))); + if (!constraint.active) return; + + SortBone(constraint.target); + + var constrained = constraint.bones.Items; + int boneCount = constraint.bones.Count; + if (constraint.data.local) { + for (int i = 0; i < boneCount; i++) { + Bone child = constrained[i]; + SortBone(child.parent); + SortBone(child); + } + } else { + for (int i = 0; i < boneCount; i++) + SortBone(constrained[i]); + } + + updateCache.Add(constraint); + + for (int i = 0; i < boneCount; i++) + SortReset(constrained[i].children); + for (int i = 0; i < boneCount; i++) + constrained[i].sorted = true; + } + + private void SortPathConstraint (PathConstraint constraint) { + constraint.active = constraint.target.bone.active + && (!constraint.data.skinRequired || (skin != null && skin.constraints.Contains(constraint.data))); + if (!constraint.active) return; + + Slot slot = constraint.target; + int slotIndex = slot.data.index; + Bone slotBone = slot.bone; + if (skin != null) SortPathConstraintAttachment(skin, slotIndex, slotBone); + if (data.defaultSkin != null && data.defaultSkin != skin) + SortPathConstraintAttachment(data.defaultSkin, slotIndex, slotBone); + + Attachment attachment = slot.attachment; + if (attachment is PathAttachment) SortPathConstraintAttachment(attachment, slotBone); + + var constrained = constraint.bones.Items; + int boneCount = constraint.bones.Count; + for (int i = 0; i < boneCount; i++) + SortBone(constrained[i]); + + updateCache.Add(constraint); + + for (int i = 0; i < boneCount; i++) + SortReset(constrained[i].children); + for (int i = 0; i < boneCount; i++) + constrained[i].sorted = true; + } + + private void SortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) { + foreach (var entry in skin.Attachments) + if (entry.SlotIndex == slotIndex) SortPathConstraintAttachment(entry.Attachment, slotBone); + } + + private void SortPathConstraintAttachment (Attachment attachment, Bone slotBone) { + if (!(attachment is PathAttachment)) return; + int[] pathBones = ((PathAttachment)attachment).bones; + if (pathBones == null) + SortBone(slotBone); + else { + var bones = this.bones.Items; + for (int i = 0, n = pathBones.Length; i < n;) { + int nn = pathBones[i++]; + nn += i; + while (i < nn) + SortBone(bones[pathBones[i++]]); + } + } + } + + private void SortSpringConstraint (SpringConstraint constraint) { + constraint.active = !constraint.data.skinRequired || (skin != null && skin.constraints.Contains(constraint.data)); + if (!constraint.active) return; + + Object[] constrained = constraint.bones.Items; + int boneCount = constraint.bones.Count; + for (int i = 0; i < boneCount; i++) + SortBone((Bone)constrained[i]); + + updateCache.Add(constraint); + + for (int i = 0; i < boneCount; i++) + SortReset(((Bone)constrained[i]).children); + for (int i = 0; i < boneCount; i++) + ((Bone)constrained[i]).sorted = true; + } + + private void SortBone (Bone bone) { + if (bone.sorted) return; + Bone parent = bone.parent; + if (parent != null) SortBone(parent); + bone.sorted = true; + updateCache.Add(bone); + } + + private static void SortReset (ExposedList bones) { + Bone[] bonesItems = bones.Items; + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bonesItems[i]; + if (!bone.active) continue; + if (bone.sorted) SortReset(bone.children); + bone.sorted = false; + } + } + + /// + /// Updates the world transform for each bone and applies all constraints. + /// + /// See World transforms in the Spine + /// Runtimes Guide. + /// + public void UpdateWorldTransform () { + Bone[] bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + bone.ax = bone.x; + bone.ay = bone.y; + bone.arotation = bone.rotation; + bone.ascaleX = bone.scaleX; + bone.ascaleY = bone.scaleY; + bone.ashearX = bone.shearX; + bone.ashearY = bone.shearY; + } + + var updateCache = this.updateCache.Items; + for (int i = 0, n = this.updateCache.Count; i < n; i++) + updateCache[i].Update(); + } + + /// + /// Temporarily sets the root bone as a child of the specified bone, then updates the world transform for each bone and applies + /// all constraints. + /// + public void UpdateWorldTransform (Bone parent) { + if (parent == null) throw new ArgumentNullException("parent", "parent cannot be null."); + + // Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection. + Bone rootBone = this.RootBone; + float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; + rootBone.worldX = pa * x + pb * y + parent.worldX; + rootBone.worldY = pc * x + pd * y + parent.worldY; + + float rotationY = rootBone.rotation + 90 + rootBone.shearY; + float la = MathUtils.CosDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX; + float lb = MathUtils.CosDeg(rotationY) * rootBone.scaleY; + float lc = MathUtils.SinDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX; + float ld = MathUtils.SinDeg(rotationY) * rootBone.scaleY; + rootBone.a = (pa * la + pb * lc) * scaleX; + rootBone.b = (pa * lb + pb * ld) * scaleX; + rootBone.c = (pc * la + pd * lc) * scaleY; + rootBone.d = (pc * lb + pd * ld) * scaleY; + + // Update everything except root bone. + var updateCache = this.updateCache.Items; + for (int i = 0, n = this.updateCache.Count; i < n; i++) { + var updatable = updateCache[i]; + if (updatable != rootBone) updatable.Update(); + } + } + + /// Sets the bones, constraints, and slots to their setup pose values. + public void SetToSetupPose () { + SetBonesToSetupPose(); + SetSlotsToSetupPose(); + } + + /// Sets the bones and constraints to their setup pose values. + public void SetBonesToSetupPose () { + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) + bones[i].SetToSetupPose(); + + IkConstraint[] ikConstraints = this.ikConstraints.Items; + for (int i = 0, n = this.ikConstraints.Count; i < n; i++) { + IkConstraint constraint = ikConstraints[i]; + IkConstraintData data = constraint.data; + constraint.mix = data.mix; + constraint.softness = data.softness; + constraint.bendDirection = data.bendDirection; + constraint.compress = data.compress; + constraint.stretch = data.stretch; + } + + TransformConstraint[] transformConstraints = this.transformConstraints.Items; + for (int i = 0, n = this.transformConstraints.Count; i < n; i++) { + TransformConstraint constraint = transformConstraints[i]; + TransformConstraintData data = constraint.data; + constraint.mixRotate = data.mixRotate; + constraint.mixX = data.mixX; + constraint.mixY = data.mixY; + constraint.mixScaleX = data.mixScaleX; + constraint.mixScaleY = data.mixScaleY; + constraint.mixShearY = data.mixShearY; + } + + PathConstraint[] pathConstraints = this.pathConstraints.Items; + for (int i = 0, n = this.pathConstraints.Count; i < n; i++) { + PathConstraint constraint = pathConstraints[i]; + PathConstraintData data = constraint.data; + constraint.position = data.position; + constraint.spacing = data.spacing; + constraint.mixRotate = data.mixRotate; + constraint.mixX = data.mixX; + constraint.mixY = data.mixY; + } + + SpringConstraint[] springConstraints = this.springConstraints.Items; + for (int i = 0, n = this.springConstraints.Count; i < n; i++) { + SpringConstraint constraint = springConstraints[i]; + SpringConstraintData data = constraint.data; + constraint.mix = data.mix; + constraint.friction = data.friction; + constraint.gravity = data.gravity; + constraint.wind = data.wind; + constraint.stiffness = data.stiffness; + constraint.damping = data.damping; + constraint.rope = data.rope; + constraint.stretch = data.stretch; + } + } + + public void SetSlotsToSetupPose () { + var slots = this.slots.Items; + int n = this.slots.Count; + Array.Copy(slots, 0, drawOrder.Items, 0, n); + for (int i = 0; i < n; i++) + slots[i].SetToSetupPose(); + } + + /// Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it + /// repeatedly. + /// May be null. + public Bone FindBone (string boneName) { + if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + if (bone.data.name == boneName) return bone; + } + return null; + } + + /// Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it + /// repeatedly. + /// May be null. + public Slot FindSlot (string slotName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + var slots = this.slots.Items; + for (int i = 0, n = this.slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.data.name == slotName) return slot; + } + return null; + } + + /// Sets a skin by name (). + public void SetSkin (string skinName) { + Skin foundSkin = data.FindSkin(skinName); + if (foundSkin == null) throw new ArgumentException("Skin not found: " + skinName, "skinName"); + SetSkin(foundSkin); + } + + /// + /// Sets the skin used to look up attachments before looking in the . If the + /// skin is changed, is called. + /// + /// Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. + /// If there was no old skin, each slot's setup mode attachment is attached from the new skin. + /// + /// After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling + /// . + /// Also, often is called before the next time the + /// skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. + /// + /// May be null. + public void SetSkin (Skin newSkin) { + if (newSkin == skin) return; + if (newSkin != null) { + if (skin != null) + newSkin.AttachAll(this, skin); + else { + Slot[] slots = this.slots.Items; + for (int i = 0, n = this.slots.Count; i < n; i++) { + Slot slot = slots[i]; + string name = slot.data.attachmentName; + if (name != null) { + Attachment attachment = newSkin.GetAttachment(i, name); + if (attachment != null) slot.Attachment = attachment; + } + } + } + } + skin = newSkin; + UpdateCache(); + } + + /// Finds an attachment by looking in the and using the slot name and attachment name. + /// May be null. + public Attachment GetAttachment (string slotName, string attachmentName) { + return GetAttachment(data.FindSlot(slotName).index, attachmentName); + } + + /// Finds an attachment by looking in the skin and skeletonData.defaultSkin using the slot index and attachment name.First the skin is checked and if the attachment was not found, the default skin is checked. + /// May be null. + public Attachment GetAttachment (int slotIndex, string attachmentName) { + if (attachmentName == null) throw new ArgumentNullException("attachmentName", "attachmentName cannot be null."); + if (skin != null) { + Attachment attachment = skin.GetAttachment(slotIndex, attachmentName); + if (attachment != null) return attachment; + } + return data.defaultSkin != null ? data.defaultSkin.GetAttachment(slotIndex, attachmentName) : null; + } + + /// A convenience method to set an attachment by finding the slot with FindSlot, finding the attachment with GetAttachment, then setting the slot's slot.Attachment. + /// May be null to clear the slot's attachment. + public void SetAttachment (string slotName, string attachmentName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + Slot[] slots = this.slots.Items; + for (int i = 0, n = this.slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.data.name == slotName) { + Attachment attachment = null; + if (attachmentName != null) { + attachment = GetAttachment(i, attachmentName); + if (attachment == null) throw new Exception("Attachment not found: " + attachmentName + ", for slot: " + slotName); + } + slot.Attachment = attachment; + return; + } + } + throw new Exception("Slot not found: " + slotName); + } + + /// Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method + /// than to call it repeatedly. + /// May be null. + public IkConstraint FindIkConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var ikConstraints = this.ikConstraints.Items; + for (int i = 0, n = this.ikConstraints.Count; i < n; i++) { + IkConstraint ikConstraint = ikConstraints[i]; + if (ikConstraint.data.name == constraintName) return ikConstraint; + } + return null; + } + + /// Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of + /// this method than to call it repeatedly. + /// May be null. + public TransformConstraint FindTransformConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var transformConstraints = this.transformConstraints.Items; + for (int i = 0, n = this.transformConstraints.Count; i < n; i++) { + TransformConstraint transformConstraint = transformConstraints[i]; + if (transformConstraint.data.Name == constraintName) return transformConstraint; + } + return null; + } + + /// Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method + /// than to call it repeatedly. + /// May be null. + public PathConstraint FindPathConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var pathConstraints = this.pathConstraints.Items; + for (int i = 0, n = this.pathConstraints.Count; i < n; i++) { + PathConstraint constraint = pathConstraints[i]; + if (constraint.data.Name.Equals(constraintName)) return constraint; + } + return null; + } + + /// Finds a spring constraint by comparing each spring constraint's name. It is more efficient to cache the results of this + /// method than to call it repeatedly. + /// May be null. + public SpringConstraint FindSpringConstraint (String constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + SpringConstraint[] springConstraints = this.springConstraints.Items; + for (int i = 0, n = this.springConstraints.Count; i < n; i++) { + SpringConstraint constraint = springConstraints[i]; + if (constraint.data.name.Equals(constraintName)) return constraint; + } + return null; + } + + /// Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose. + /// The horizontal distance between the skeleton origin and the left side of the AABB. + /// The vertical distance between the skeleton origin and the bottom side of the AABB. + /// The width of the AABB + /// The height of the AABB. + /// Reference to hold a float[]. May be a null reference. This method will assign it a new float[] with the appropriate size as needed. + public void GetBounds (out float x, out float y, out float width, out float height, ref float[] vertexBuffer) { + float[] temp = vertexBuffer; + temp = temp ?? new float[8]; + var drawOrder = this.drawOrder.Items; + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + for (int i = 0, n = this.drawOrder.Count; i < n; i++) { + Slot slot = drawOrder[i]; + if (!slot.bone.active) continue; + int verticesLength = 0; + float[] vertices = null; + Attachment attachment = slot.attachment; + RegionAttachment region = attachment as RegionAttachment; + if (region != null) { + verticesLength = 8; + vertices = temp; + if (vertices.Length < 8) vertices = temp = new float[8]; + region.ComputeWorldVertices(slot, temp, 0, 2); + } else { + var meshAttachment = attachment as MeshAttachment; + if (meshAttachment != null) { + MeshAttachment mesh = meshAttachment; + verticesLength = mesh.WorldVerticesLength; + vertices = temp; + if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength]; + mesh.ComputeWorldVertices(slot, 0, verticesLength, temp, 0, 2); + } + } + + if (vertices != null) { + for (int ii = 0; ii < verticesLength; ii += 2) { + float vx = vertices[ii], vy = vertices[ii + 1]; + minX = Math.Min(minX, vx); + minY = Math.Min(minY, vy); + maxX = Math.Max(maxX, vx); + maxY = Math.Max(maxY, vy); + } + } + } + x = minX; + y = minY; + width = maxX - minX; + height = maxY - minY; + vertexBuffer = temp; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonBinary.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonBinary.cs new file mode 100644 index 0000000..a2e9c68 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonBinary.cs @@ -0,0 +1,1261 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace Spine4_1_00 { + public class SkeletonBinary : SkeletonLoader { + public const int BONE_ROTATE = 0; + public const int BONE_TRANSLATE = 1; + public const int BONE_TRANSLATEX = 2; + public const int BONE_TRANSLATEY = 3; + public const int BONE_SCALE = 4; + public const int BONE_SCALEX = 5; + public const int BONE_SCALEY = 6; + public const int BONE_SHEAR = 7; + public const int BONE_SHEARX = 8; + public const int BONE_SHEARY = 9; + + public const int SLOT_ATTACHMENT = 0; + public const int SLOT_RGBA = 1; + public const int SLOT_RGB = 2; + public const int SLOT_RGBA2 = 3; + public const int SLOT_RGB2 = 4; + public const int SLOT_ALPHA = 5; + + public const int ATTACHMENT_DEFORM = 0; + public const int ATTACHMENT_SEQUENCE = 1; + + public const int PATH_POSITION = 0; + public const int PATH_SPACING = 1; + public const int PATH_MIX = 2; + + public const int CURVE_LINEAR = 0; + public const int CURVE_STEPPED = 1; + public const int CURVE_BEZIER = 2; + + public SkeletonBinary (AttachmentLoader attachmentLoader) + : base(attachmentLoader) { + } + + public SkeletonBinary (params Atlas[] atlasArray) + : base(atlasArray) { + } + +#if !ISUNITY && WINDOWS_STOREAPP + private async Task ReadFile(string path) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + using (var input = new BufferedStream(await folder.GetFileAsync(path).AsTask().ConfigureAwait(false))) { + SkeletonData skeletonData = ReadSkeletonData(input); + skeletonData.Name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + + public override SkeletonData ReadSkeletonData (string path) { + return this.ReadFile(path).Result; + } +#else + public override SkeletonData ReadSkeletonData (string path) { +#if WINDOWS_PHONE + using (var input = new BufferedStream(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) { +#else + using (var input = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { +#endif + SkeletonData skeletonData = ReadSkeletonData(input); + skeletonData.name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } +#endif // WINDOWS_STOREAPP + + public static readonly TransformMode[] TransformModeValues = { + TransformMode.Normal, + TransformMode.OnlyTranslation, + TransformMode.NoRotationOrReflection, + TransformMode.NoScale, + TransformMode.NoScaleOrReflection + }; + + /// Returns the version string of binary skeleton data. + public static string GetVersionString (Stream file) { + if (file == null) throw new ArgumentNullException("file"); + + SkeletonInput input = new SkeletonInput(file); + return input.GetVersionString(); + } + + public SkeletonData ReadSkeletonData (Stream file) { + if (file == null) throw new ArgumentNullException("file"); + float scale = this.scale; + + var skeletonData = new SkeletonData(); + SkeletonInput input = new SkeletonInput(file); + + long hash = input.ReadLong(); + skeletonData.hash = hash == 0 ? null : hash.ToString(); + skeletonData.version = input.ReadString(); + if (skeletonData.version.Length == 0) skeletonData.version = null; + // early return for old 3.8 format instead of reading past the end + if (skeletonData.version.Length > 13) return null; + skeletonData.x = input.ReadFloat(); + skeletonData.y = input.ReadFloat(); + skeletonData.width = input.ReadFloat(); + skeletonData.height = input.ReadFloat(); + + bool nonessential = input.ReadBoolean(); + + if (nonessential) { + skeletonData.fps = input.ReadFloat(); + + skeletonData.imagesPath = input.ReadString(); + if (string.IsNullOrEmpty(skeletonData.imagesPath)) skeletonData.imagesPath = null; + + skeletonData.audioPath = input.ReadString(); + if (string.IsNullOrEmpty(skeletonData.audioPath)) skeletonData.audioPath = null; + } + + int n; + Object[] o; + + // Strings. + o = input.strings = new String[n = input.ReadInt(true)]; + for (int i = 0; i < n; i++) + o[i] = input.ReadString(); + + // Bones. + var bones = skeletonData.bones.Resize(n = input.ReadInt(true)).Items; + for (int i = 0; i < n; i++) { + String name = input.ReadString(); + BoneData parent = i == 0 ? null : bones[input.ReadInt(true)]; + BoneData data = new BoneData(i, name, parent); + data.rotation = input.ReadFloat(); + data.x = input.ReadFloat() * scale; + data.y = input.ReadFloat() * scale; + data.scaleX = input.ReadFloat(); + data.scaleY = input.ReadFloat(); + data.shearX = input.ReadFloat(); + data.shearY = input.ReadFloat(); + data.Length = input.ReadFloat() * scale; + data.transformMode = TransformModeValues[input.ReadInt(true)]; + data.skinRequired = input.ReadBoolean(); + if (nonessential) input.ReadInt(); // Skip bone color. + bones[i] = data; + } + + // Slots. + var slots = skeletonData.slots.Resize(n = input.ReadInt(true)).Items; + for (int i = 0; i < n; i++) { + String slotName = input.ReadString(); + BoneData boneData = bones[input.ReadInt(true)]; + SlotData slotData = new SlotData(i, slotName, boneData); + int color = input.ReadInt(); + slotData.r = ((color & 0xff000000) >> 24) / 255f; + slotData.g = ((color & 0x00ff0000) >> 16) / 255f; + slotData.b = ((color & 0x0000ff00) >> 8) / 255f; + slotData.a = ((color & 0x000000ff)) / 255f; + + int darkColor = input.ReadInt(); // 0x00rrggbb + if (darkColor != -1) { + slotData.hasSecondColor = true; + slotData.r2 = ((darkColor & 0x00ff0000) >> 16) / 255f; + slotData.g2 = ((darkColor & 0x0000ff00) >> 8) / 255f; + slotData.b2 = ((darkColor & 0x000000ff)) / 255f; + } + + slotData.attachmentName = input.ReadStringRef(); + slotData.blendMode = (BlendMode)input.ReadInt(true); + slots[i] = slotData; + } + + // IK constraints. + o = skeletonData.ikConstraints.Resize(n = input.ReadInt(true)).Items; + for (int i = 0, nn; i < n; i++) { + IkConstraintData data = new IkConstraintData(input.ReadString()); + data.order = input.ReadInt(true); + data.skinRequired = input.ReadBoolean(); + var constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items; + for (int ii = 0; ii < nn; ii++) + constraintBones[ii] = bones[input.ReadInt(true)]; + data.target = bones[input.ReadInt(true)]; + data.mix = input.ReadFloat(); + data.softness = input.ReadFloat() * scale; + data.bendDirection = input.ReadSByte(); + data.compress = input.ReadBoolean(); + data.stretch = input.ReadBoolean(); + data.uniform = input.ReadBoolean(); + o[i] = data; + } + + // Transform constraints. + o = skeletonData.transformConstraints.Resize(n = input.ReadInt(true)).Items; + for (int i = 0, nn; i < n; i++) { + TransformConstraintData data = new TransformConstraintData(input.ReadString()); + data.order = input.ReadInt(true); + data.skinRequired = input.ReadBoolean(); + var constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items; + for (int ii = 0; ii < nn; ii++) + constraintBones[ii] = bones[input.ReadInt(true)]; + data.target = bones[input.ReadInt(true)]; + data.local = input.ReadBoolean(); + data.relative = input.ReadBoolean(); + data.offsetRotation = input.ReadFloat(); + data.offsetX = input.ReadFloat() * scale; + data.offsetY = input.ReadFloat() * scale; + data.offsetScaleX = input.ReadFloat(); + data.offsetScaleY = input.ReadFloat(); + data.offsetShearY = input.ReadFloat(); + data.mixRotate = input.ReadFloat(); + data.mixX = input.ReadFloat(); + data.mixY = input.ReadFloat(); + data.mixScaleX = input.ReadFloat(); + data.mixScaleY = input.ReadFloat(); + data.mixShearY = input.ReadFloat(); + o[i] = data; + } + + // Path constraints + o = skeletonData.pathConstraints.Resize(n = input.ReadInt(true)).Items; + for (int i = 0, nn; i < n; i++) { + PathConstraintData data = new PathConstraintData(input.ReadString()); + data.order = input.ReadInt(true); + data.skinRequired = input.ReadBoolean(); + Object[] constraintBones = data.bones.Resize(nn = input.ReadInt(true)).Items; + for (int ii = 0; ii < nn; ii++) + constraintBones[ii] = bones[input.ReadInt(true)]; + data.target = slots[input.ReadInt(true)]; + data.positionMode = (PositionMode)Enum.GetValues(typeof(PositionMode)).GetValue(input.ReadInt(true)); + data.spacingMode = (SpacingMode)Enum.GetValues(typeof(SpacingMode)).GetValue(input.ReadInt(true)); + data.rotateMode = (RotateMode)Enum.GetValues(typeof(RotateMode)).GetValue(input.ReadInt(true)); + data.offsetRotation = input.ReadFloat(); + data.position = input.ReadFloat(); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = input.ReadFloat(); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.mixRotate = input.ReadFloat(); + data.mixX = input.ReadFloat(); + data.mixY = input.ReadFloat(); + o[i] = data; + } + + // Default skin. + Skin defaultSkin = ReadSkin(input, skeletonData, true, nonessential); + if (defaultSkin != null) { + skeletonData.defaultSkin = defaultSkin; + skeletonData.skins.Add(defaultSkin); + } + + // Skins. + { + int i = skeletonData.skins.Count; + o = skeletonData.skins.Resize(n = i + input.ReadInt(true)).Items; + for (; i < n; i++) + o[i] = ReadSkin(input, skeletonData, false, nonessential); + } + + // Linked meshes. + n = linkedMeshes.Count; + for (int i = 0; i < n; i++) { + LinkedMesh linkedMesh = linkedMeshes[i]; + Skin skin = linkedMesh.skin == null ? skeletonData.DefaultSkin : skeletonData.FindSkin(linkedMesh.skin); + if (skin == null) throw new Exception("Skin not found: " + linkedMesh.skin); + Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Exception("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.TimelineAttachment = linkedMesh.inheritTimelines ? (VertexAttachment)parent : linkedMesh.mesh; + linkedMesh.mesh.ParentMesh = (MeshAttachment)parent; + if (linkedMesh.mesh.Sequence == null) linkedMesh.mesh.UpdateRegion(); + } + linkedMeshes.Clear(); + + // Events. + o = skeletonData.events.Resize(n = input.ReadInt(true)).Items; + for (int i = 0; i < n; i++) { + EventData data = new EventData(input.ReadStringRef()); + data.Int = input.ReadInt(false); + data.Float = input.ReadFloat(); + data.String = input.ReadString(); + data.AudioPath = input.ReadString(); + if (data.AudioPath != null) { + data.Volume = input.ReadFloat(); + data.Balance = input.ReadFloat(); + } + o[i] = data; + } + + // Animations. + o = skeletonData.animations.Resize(n = input.ReadInt(true)).Items; + for (int i = 0; i < n; i++) + o[i] = ReadAnimation(input.ReadString(), input, skeletonData); + + return skeletonData; + } + + /// May be null. + private Skin ReadSkin (SkeletonInput input, SkeletonData skeletonData, bool defaultSkin, bool nonessential) { + + Skin skin; + int slotCount; + + if (defaultSkin) { + slotCount = input.ReadInt(true); + if (slotCount == 0) return null; + skin = new Skin("default"); + } else { + skin = new Skin(input.ReadStringRef()); + Object[] bones = skin.bones.Resize(input.ReadInt(true)).Items; + var bonesItems = skeletonData.bones.Items; + for (int i = 0, n = skin.bones.Count; i < n; i++) + bones[i] = bonesItems[input.ReadInt(true)]; + + var ikConstraintsItems = skeletonData.ikConstraints.Items; + for (int i = 0, n = input.ReadInt(true); i < n; i++) + skin.constraints.Add(ikConstraintsItems[input.ReadInt(true)]); + var transformConstraintsItems = skeletonData.transformConstraints.Items; + for (int i = 0, n = input.ReadInt(true); i < n; i++) + skin.constraints.Add(transformConstraintsItems[input.ReadInt(true)]); + var pathConstraintsItems = skeletonData.pathConstraints.Items; + for (int i = 0, n = input.ReadInt(true); i < n; i++) + skin.constraints.Add(pathConstraintsItems[input.ReadInt(true)]); + skin.constraints.TrimExcess(); + + slotCount = input.ReadInt(true); + } + for (int i = 0; i < slotCount; i++) { + int slotIndex = input.ReadInt(true); + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + String name = input.ReadStringRef(); + Attachment attachment = ReadAttachment(input, skeletonData, skin, slotIndex, name, nonessential); + if (attachment != null) skin.SetAttachment(slotIndex, name, attachment); + } + } + return skin; + } + + private Attachment ReadAttachment (SkeletonInput input, SkeletonData skeletonData, Skin skin, int slotIndex, + String attachmentName, bool nonessential) { + float scale = this.scale; + + String name = input.ReadStringRef(); + if (name == null) name = attachmentName; + + switch ((AttachmentType)input.ReadByte()) { + case AttachmentType.Region: { + String path = input.ReadStringRef(); + float rotation = input.ReadFloat(); + float x = input.ReadFloat(); + float y = input.ReadFloat(); + float scaleX = input.ReadFloat(); + float scaleY = input.ReadFloat(); + float width = input.ReadFloat(); + float height = input.ReadFloat(); + int color = input.ReadInt(); + Sequence sequence = ReadSequence(input); + + if (path == null) path = name; + RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path, sequence); + if (region == null) return null; + region.Path = path; + region.x = x * scale; + region.y = y * scale; + region.scaleX = scaleX; + region.scaleY = scaleY; + region.rotation = rotation; + region.width = width * scale; + region.height = height * scale; + region.r = ((color & 0xff000000) >> 24) / 255f; + region.g = ((color & 0x00ff0000) >> 16) / 255f; + region.b = ((color & 0x0000ff00) >> 8) / 255f; + region.a = ((color & 0x000000ff)) / 255f; + region.sequence = sequence; + if (sequence == null) region.UpdateRegion(); + return region; + } + case AttachmentType.Boundingbox: { + int vertexCount = input.ReadInt(true); + Vertices vertices = ReadVertices(input, vertexCount); + if (nonessential) input.ReadInt(); //int color = nonessential ? input.ReadInt() : 0; // Avoid unused local warning. + + BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name); + if (box == null) return null; + box.worldVerticesLength = vertexCount << 1; + box.vertices = vertices.vertices; + box.bones = vertices.bones; + // skipped porting: if (nonessential) Color.rgba8888ToColor(box.getColor(), color); + return box; + } + case AttachmentType.Mesh: { + String path = input.ReadStringRef(); + int color = input.ReadInt(); + int vertexCount = input.ReadInt(true); + float[] uvs = ReadFloatArray(input, vertexCount << 1, 1); + int[] triangles = ReadShortArray(input); + Vertices vertices = ReadVertices(input, vertexCount); + int hullLength = input.ReadInt(true); + Sequence sequence = ReadSequence(input); + int[] edges = null; + float width = 0, height = 0; + if (nonessential) { + edges = ReadShortArray(input); + width = input.ReadFloat(); + height = input.ReadFloat(); + } + + if (path == null) path = name; + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path, sequence); + if (mesh == null) return null; + mesh.Path = path; + mesh.r = ((color & 0xff000000) >> 24) / 255f; + mesh.g = ((color & 0x00ff0000) >> 16) / 255f; + mesh.b = ((color & 0x0000ff00) >> 8) / 255f; + mesh.a = ((color & 0x000000ff)) / 255f; + mesh.bones = vertices.bones; + mesh.vertices = vertices.vertices; + mesh.WorldVerticesLength = vertexCount << 1; + mesh.triangles = triangles; + mesh.regionUVs = uvs; + if (sequence == null) mesh.UpdateRegion(); + mesh.HullLength = hullLength << 1; + mesh.Sequence = sequence; + if (nonessential) { + mesh.Edges = edges; + mesh.Width = width * scale; + mesh.Height = height * scale; + } + return mesh; + } + case AttachmentType.Linkedmesh: { + String path = input.ReadStringRef(); + int color = input.ReadInt(); + String skinName = input.ReadStringRef(); + String parent = input.ReadStringRef(); + bool inheritTimelines = input.ReadBoolean(); + Sequence sequence = ReadSequence(input); + float width = 0, height = 0; + if (nonessential) { + width = input.ReadFloat(); + height = input.ReadFloat(); + } + + if (path == null) path = name; + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path, sequence); + if (mesh == null) return null; + mesh.Path = path; + mesh.r = ((color & 0xff000000) >> 24) / 255f; + mesh.g = ((color & 0x00ff0000) >> 16) / 255f; + mesh.b = ((color & 0x0000ff00) >> 8) / 255f; + mesh.a = ((color & 0x000000ff)) / 255f; + mesh.Sequence = sequence; + if (nonessential) { + mesh.Width = width * scale; + mesh.Height = height * scale; + } + linkedMeshes.Add(new SkeletonJson.LinkedMesh(mesh, skinName, slotIndex, parent, inheritTimelines)); + return mesh; + } + case AttachmentType.Path: { + bool closed = input.ReadBoolean(); + bool constantSpeed = input.ReadBoolean(); + int vertexCount = input.ReadInt(true); + Vertices vertices = ReadVertices(input, vertexCount); + float[] lengths = new float[vertexCount / 3]; + for (int i = 0, n = lengths.Length; i < n; i++) + lengths[i] = input.ReadFloat() * scale; + if (nonessential) input.ReadInt(); //int color = nonessential ? input.ReadInt() : 0; + + PathAttachment path = attachmentLoader.NewPathAttachment(skin, name); + if (path == null) return null; + path.closed = closed; + path.constantSpeed = constantSpeed; + path.worldVerticesLength = vertexCount << 1; + path.vertices = vertices.vertices; + path.bones = vertices.bones; + path.lengths = lengths; + // skipped porting: if (nonessential) Color.rgba8888ToColor(path.getColor(), color); + return path; + } + case AttachmentType.Point: { + float rotation = input.ReadFloat(); + float x = input.ReadFloat(); + float y = input.ReadFloat(); + if (nonessential) input.ReadInt(); //int color = nonessential ? input.ReadInt() : 0; + + PointAttachment point = attachmentLoader.NewPointAttachment(skin, name); + if (point == null) return null; + point.x = x * scale; + point.y = y * scale; + point.rotation = rotation; + // skipped porting: if (nonessential) point.color = color; + return point; + } + case AttachmentType.Clipping: { + int endSlotIndex = input.ReadInt(true); + int vertexCount = input.ReadInt(true); + Vertices vertices = ReadVertices(input, vertexCount); + if (nonessential) input.ReadInt(); + + ClippingAttachment clip = attachmentLoader.NewClippingAttachment(skin, name); + if (clip == null) return null; + clip.EndSlot = skeletonData.slots.Items[endSlotIndex]; + clip.worldVerticesLength = vertexCount << 1; + clip.vertices = vertices.vertices; + clip.bones = vertices.bones; + // skipped porting: if (nonessential) Color.rgba8888ToColor(clip.getColor(), color); + return clip; + } + } + return null; + } + + private Sequence ReadSequence (SkeletonInput input) { + if (!input.ReadBoolean()) return null; + Sequence sequence = new Sequence(input.ReadInt(true)); + sequence.Start = input.ReadInt(true); + sequence.Digits = input.ReadInt(true); + sequence.SetupIndex = input.ReadInt(true); + return sequence; + } + + private Vertices ReadVertices (SkeletonInput input, int vertexCount) { + float scale = this.scale; + int verticesLength = vertexCount << 1; + Vertices vertices = new Vertices(); + if (!input.ReadBoolean()) { + vertices.vertices = ReadFloatArray(input, verticesLength, scale); + return vertices; + } + var weights = new ExposedList(verticesLength * 3 * 3); + var bonesArray = new ExposedList(verticesLength * 3); + for (int i = 0; i < vertexCount; i++) { + int boneCount = input.ReadInt(true); + bonesArray.Add(boneCount); + for (int ii = 0; ii < boneCount; ii++) { + bonesArray.Add(input.ReadInt(true)); + weights.Add(input.ReadFloat() * scale); + weights.Add(input.ReadFloat() * scale); + weights.Add(input.ReadFloat()); + } + } + + vertices.vertices = weights.ToArray(); + vertices.bones = bonesArray.ToArray(); + return vertices; + } + + private float[] ReadFloatArray (SkeletonInput input, int n, float scale) { + float[] array = new float[n]; + if (scale == 1) { + for (int i = 0; i < n; i++) + array[i] = input.ReadFloat(); + } else { + for (int i = 0; i < n; i++) + array[i] = input.ReadFloat() * scale; + } + return array; + } + + private int[] ReadShortArray (SkeletonInput input) { + int n = input.ReadInt(true); + int[] array = new int[n]; + for (int i = 0; i < n; i++) + array[i] = (input.ReadByte() << 8) | input.ReadByte(); + return array; + } + + /// SerializationException will be thrown when a Vertex attachment is not found. + /// Throws IOException when a read operation fails. + private Animation ReadAnimation (String name, SkeletonInput input, SkeletonData skeletonData) { + var timelines = new ExposedList(input.ReadInt(true)); + float scale = this.scale; + + // Slot timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int slotIndex = input.ReadInt(true); + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + int timelineType = input.ReadByte(), frameCount = input.ReadInt(true), frameLast = frameCount - 1; + switch (timelineType) { + case SLOT_ATTACHMENT: { + AttachmentTimeline timeline = new AttachmentTimeline(frameCount, slotIndex); + for (int frame = 0; frame < frameCount; frame++) + timeline.SetFrame(frame, input.ReadFloat(), input.ReadStringRef()); + timelines.Add(timeline); + break; + } + case SLOT_RGBA: { + RGBATimeline timeline = new RGBATimeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(); + float r = input.Read() / 255f, g = input.Read() / 255f; + float b = input.Read() / 255f, a = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, a); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float r2 = input.Read() / 255f, g2 = input.Read() / 255f; + float b2 = input.Read() / 255f, a2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1); + SetBezier(input, timeline, bezier++, frame, 3, time, time2, a, a2, 1); + break; + } + time = time2; + r = r2; + g = g2; + b = b2; + a = a2; + } + timelines.Add(timeline); + break; + } + case SLOT_RGB: { + RGBTimeline timeline = new RGBTimeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(); + float r = input.Read() / 255f, g = input.Read() / 255f, b = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float r2 = input.Read() / 255f, g2 = input.Read() / 255f, b2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1); + break; + } + time = time2; + r = r2; + g = g2; + b = b2; + } + timelines.Add(timeline); + break; + } + case SLOT_RGBA2: { + RGBA2Timeline timeline = new RGBA2Timeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(); + float r = input.Read() / 255f, g = input.Read() / 255f; + float b = input.Read() / 255f, a = input.Read() / 255f; + float r2 = input.Read() / 255f, g2 = input.Read() / 255f, b2 = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, a, r2, g2, b2); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float nr = input.Read() / 255f, ng = input.Read() / 255f; + float nb = input.Read() / 255f, na = input.Read() / 255f; + float nr2 = input.Read() / 255f, ng2 = input.Read() / 255f, nb2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1); + SetBezier(input, timeline, bezier++, frame, 3, time, time2, a, na, 1); + SetBezier(input, timeline, bezier++, frame, 4, time, time2, r2, nr2, 1); + SetBezier(input, timeline, bezier++, frame, 5, time, time2, g2, ng2, 1); + SetBezier(input, timeline, bezier++, frame, 6, time, time2, b2, nb2, 1); + break; + } + time = time2; + r = nr; + g = ng; + b = nb; + a = na; + r2 = nr2; + g2 = ng2; + b2 = nb2; + } + timelines.Add(timeline); + break; + } + case SLOT_RGB2: { + RGB2Timeline timeline = new RGB2Timeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(); + float r = input.Read() / 255f, g = input.Read() / 255f, b = input.Read() / 255f; + float r2 = input.Read() / 255f, g2 = input.Read() / 255f, b2 = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, r2, g2, b2); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float nr = input.Read() / 255f, ng = input.Read() / 255f, nb = input.Read() / 255f; + float nr2 = input.Read() / 255f, ng2 = input.Read() / 255f, nb2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1); + SetBezier(input, timeline, bezier++, frame, 3, time, time2, r2, nr2, 1); + SetBezier(input, timeline, bezier++, frame, 4, time, time2, g2, ng2, 1); + SetBezier(input, timeline, bezier++, frame, 5, time, time2, b2, nb2, 1); + break; + } + time = time2; + r = nr; + g = ng; + b = nb; + r2 = nr2; + g2 = ng2; + b2 = nb2; + } + timelines.Add(timeline); + break; + } + case SLOT_ALPHA: { + AlphaTimeline timeline = new AlphaTimeline(frameCount, input.ReadInt(true), slotIndex); + float time = input.ReadFloat(), a = input.Read() / 255f; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, a); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + float a2 = input.Read() / 255f; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, a, a2, 1); + break; + } + time = time2; + a = a2; + } + timelines.Add(timeline); + break; + } + } + } + } + + // Bone timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int boneIndex = input.ReadInt(true); + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + int type = input.ReadByte(), frameCount = input.ReadInt(true), bezierCount = input.ReadInt(true); + switch (type) { + case BONE_ROTATE: + timelines.Add(ReadTimeline(input, new RotateTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_TRANSLATE: + timelines.Add(ReadTimeline(input, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale)); + break; + case BONE_TRANSLATEX: + timelines.Add(ReadTimeline(input, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale)); + break; + case BONE_TRANSLATEY: + timelines.Add(ReadTimeline(input, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale)); + break; + case BONE_SCALE: + timelines.Add(ReadTimeline(input, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SCALEX: + timelines.Add(ReadTimeline(input, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SCALEY: + timelines.Add(ReadTimeline(input, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SHEAR: + timelines.Add(ReadTimeline(input, new ShearTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SHEARX: + timelines.Add(ReadTimeline(input, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + case BONE_SHEARY: + timelines.Add(ReadTimeline(input, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1)); + break; + } + } + } + + // IK constraint timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int index = input.ReadInt(true), frameCount = input.ReadInt(true), frameLast = frameCount - 1; + IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount, input.ReadInt(true), index); + float time = input.ReadFloat(), mix = input.ReadFloat(), softness = input.ReadFloat() * scale; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mix, softness, input.ReadSByte(), input.ReadBoolean(), input.ReadBoolean()); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), mix2 = input.ReadFloat(), softness2 = input.ReadFloat() * scale; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale); + break; + } + time = time2; + mix = mix2; + softness = softness2; + } + timelines.Add(timeline); + } + + // Transform constraint timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int index = input.ReadInt(true), frameCount = input.ReadInt(true), frameLast = frameCount - 1; + TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount, input.ReadInt(true), index); + float time = input.ReadFloat(), mixRotate = input.ReadFloat(), mixX = input.ReadFloat(), mixY = input.ReadFloat(), + mixScaleX = input.ReadFloat(), mixScaleY = input.ReadFloat(), mixShearY = input.ReadFloat(); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), mixRotate2 = input.ReadFloat(), mixX2 = input.ReadFloat(), mixY2 = input.ReadFloat(), + mixScaleX2 = input.ReadFloat(), mixScaleY2 = input.ReadFloat(), mixShearY2 = input.ReadFloat(); + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, mixRotate, mixRotate2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, mixX, mixX2, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, mixY, mixY2, 1); + SetBezier(input, timeline, bezier++, frame, 3, time, time2, mixScaleX, mixScaleX2, 1); + SetBezier(input, timeline, bezier++, frame, 4, time, time2, mixScaleY, mixScaleY2, 1); + SetBezier(input, timeline, bezier++, frame, 5, time, time2, mixShearY, mixShearY2, 1); + break; + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + mixScaleX = mixScaleX2; + mixScaleY = mixScaleY2; + mixShearY = mixShearY2; + } + timelines.Add(timeline); + } + + // Path constraint timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + int index = input.ReadInt(true); + PathConstraintData data = skeletonData.pathConstraints.Items[index]; + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + switch (input.ReadByte()) { + case PATH_POSITION: + timelines + .Add(ReadTimeline(input, new PathConstraintPositionTimeline(input.ReadInt(true), input.ReadInt(true), index), + data.positionMode == PositionMode.Fixed ? scale : 1)); + break; + case PATH_SPACING: + timelines + .Add(ReadTimeline(input, new PathConstraintSpacingTimeline(input.ReadInt(true), input.ReadInt(true), index), + data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1)); + break; + case PATH_MIX: + PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(input.ReadInt(true), input.ReadInt(true), + index); + float time = input.ReadFloat(), mixRotate = input.ReadFloat(), mixX = input.ReadFloat(), mixY = input.ReadFloat(); + for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) { + timeline.SetFrame(frame, time, mixRotate, mixX, mixY); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), mixRotate2 = input.ReadFloat(), mixX2 = input.ReadFloat(), + mixY2 = input.ReadFloat(); + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, mixRotate, mixRotate2, 1); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, mixX, mixX2, 1); + SetBezier(input, timeline, bezier++, frame, 2, time, time2, mixY, mixY2, 1); + break; + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + } + timelines.Add(timeline); + break; + } + } + } + + // Attachment timelines. + for (int i = 0, n = input.ReadInt(true); i < n; i++) { + Skin skin = skeletonData.skins.Items[input.ReadInt(true)]; + for (int ii = 0, nn = input.ReadInt(true); ii < nn; ii++) { + int slotIndex = input.ReadInt(true); + for (int iii = 0, nnn = input.ReadInt(true); iii < nnn; iii++) { + String attachmentName = input.ReadStringRef(); + Attachment attachment = skin.GetAttachment(slotIndex, attachmentName); + if (attachment == null) throw new SerializationException("Timeline attachment not found: " + attachmentName); + + int timelineType = input.ReadByte(), frameCount = input.ReadInt(true), frameLast = frameCount - 1; + switch (timelineType) { + case ATTACHMENT_DEFORM: { + VertexAttachment vertexAttachment = (VertexAttachment)attachment; + bool weighted = vertexAttachment.Bones != null; + float[] vertices = vertexAttachment.Vertices; + int deformLength = weighted ? (vertices.Length / 3) << 1 : vertices.Length; + + DeformTimeline timeline = new DeformTimeline(frameCount, input.ReadInt(true), slotIndex, vertexAttachment); + + float time = input.ReadFloat(); + for (int frame = 0, bezier = 0; ; frame++) { + float[] deform; + int end = input.ReadInt(true); + if (end == 0) + deform = weighted ? new float[deformLength] : vertices; + else { + deform = new float[deformLength]; + int start = input.ReadInt(true); + end += start; + if (scale == 1) { + for (int v = start; v < end; v++) + deform[v] = input.ReadFloat(); + } else { + for (int v = start; v < end; v++) + deform[v] = input.ReadFloat() * scale; + } + if (!weighted) { + for (int v = 0, vn = deform.Length; v < vn; v++) + deform[v] += vertices[v]; + } + } + timeline.SetFrame(frame, time, deform); + if (frame == frameLast) break; + float time2 = input.ReadFloat(); + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1); + break; + } + time = time2; + } + timelines.Add(timeline); + break; + } + case ATTACHMENT_SEQUENCE: { + SequenceTimeline timeline = new SequenceTimeline(frameCount, slotIndex, attachment); + for (int frame = 0; frame < frameCount; frame++) { + float time = input.ReadFloat(); + int modeAndIndex = input.ReadInt(); + timeline.SetFrame(frame, time, (SequenceMode)(modeAndIndex & 0xf), modeAndIndex >> 4, + input.ReadFloat()); + } + timelines.Add(timeline); + break; + } // end case + } // end switch + } + } + } + + // Draw order timeline. + int drawOrderCount = input.ReadInt(true); + if (drawOrderCount > 0) { + DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrderCount); + int slotCount = skeletonData.slots.Count; + for (int i = 0; i < drawOrderCount; i++) { + float time = input.ReadFloat(); + int offsetCount = input.ReadInt(true); + int[] drawOrder = new int[slotCount]; + for (int ii = slotCount - 1; ii >= 0; ii--) + drawOrder[ii] = -1; + int[] unchanged = new int[slotCount - offsetCount]; + int originalIndex = 0, unchangedIndex = 0; + for (int ii = 0; ii < offsetCount; ii++) { + int slotIndex = input.ReadInt(true); + // Collect unchanged items. + while (originalIndex != slotIndex) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + input.ReadInt(true)] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) + unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (int ii = slotCount - 1; ii >= 0; ii--) + if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex]; + timeline.SetFrame(i, time, drawOrder); + } + timelines.Add(timeline); + } + + // Event timeline. + int eventCount = input.ReadInt(true); + if (eventCount > 0) { + EventTimeline timeline = new EventTimeline(eventCount); + for (int i = 0; i < eventCount; i++) { + float time = input.ReadFloat(); + EventData eventData = skeletonData.events.Items[input.ReadInt(true)]; + Event e = new Event(time, eventData); + e.intValue = input.ReadInt(false); + e.floatValue = input.ReadFloat(); + e.stringValue = input.ReadBoolean() ? input.ReadString() : eventData.String; + if (e.Data.AudioPath != null) { + e.volume = input.ReadFloat(); + e.balance = input.ReadFloat(); + } + timeline.SetFrame(i, e); + } + timelines.Add(timeline); + } + + float duration = 0; + var items = timelines.Items; + for (int i = 0, n = timelines.Count; i < n; i++) + duration = Math.Max(duration, items[i].Duration); + return new Animation(name, timelines, duration); + } + + /// Throws IOException when a read operation fails. + private Timeline ReadTimeline (SkeletonInput input, CurveTimeline1 timeline, float scale) { + float time = input.ReadFloat(), value = input.ReadFloat() * scale; + for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) { + timeline.SetFrame(frame, time, value); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), value2 = input.ReadFloat() * scale; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, value, value2, scale); + break; + } + time = time2; + value = value2; + } + return timeline; + } + + /// Throws IOException when a read operation fails. + private Timeline ReadTimeline (SkeletonInput input, CurveTimeline2 timeline, float scale) { + float time = input.ReadFloat(), value1 = input.ReadFloat() * scale, value2 = input.ReadFloat() * scale; + for (int frame = 0, bezier = 0, frameLast = timeline.FrameCount - 1; ; frame++) { + timeline.SetFrame(frame, time, value1, value2); + if (frame == frameLast) break; + float time2 = input.ReadFloat(), nvalue1 = input.ReadFloat() * scale, nvalue2 = input.ReadFloat() * scale; + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frame); + break; + case CURVE_BEZIER: + SetBezier(input, timeline, bezier++, frame, 0, time, time2, value1, nvalue1, scale); + SetBezier(input, timeline, bezier++, frame, 1, time, time2, value2, nvalue2, scale); + break; + } + time = time2; + value1 = nvalue1; + value2 = nvalue2; + } + return timeline; + } + + /// Throws IOException when a read operation fails. + void SetBezier (SkeletonInput input, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2, + float value1, float value2, float scale) { + timeline.SetBezier(bezier, frame, value, time1, value1, input.ReadFloat(), input.ReadFloat() * scale, input.ReadFloat(), + input.ReadFloat() * scale, time2, value2); + } + + internal class Vertices { + public int[] bones; + public float[] vertices; + } + + internal class SkeletonInput { + private byte[] chars = new byte[32]; + private byte[] bytesBigEndian = new byte[8]; + internal string[] strings; + Stream input; + + public SkeletonInput (Stream input) { + this.input = input; + } + + public int Read () { + return input.ReadByte(); + } + + public byte ReadByte () { + return (byte)input.ReadByte(); + } + + public sbyte ReadSByte () { + int value = input.ReadByte(); + if (value == -1) throw new EndOfStreamException(); + return (sbyte)value; + } + + public bool ReadBoolean () { + return input.ReadByte() != 0; + } + + public float ReadFloat () { + input.Read(bytesBigEndian, 0, 4); + chars[3] = bytesBigEndian[0]; + chars[2] = bytesBigEndian[1]; + chars[1] = bytesBigEndian[2]; + chars[0] = bytesBigEndian[3]; + return BitConverter.ToSingle(chars, 0); + } + + public int ReadInt () { + input.Read(bytesBigEndian, 0, 4); + return (bytesBigEndian[0] << 24) + + (bytesBigEndian[1] << 16) + + (bytesBigEndian[2] << 8) + + bytesBigEndian[3]; + } + + public long ReadLong () { + input.Read(bytesBigEndian, 0, 8); + return ((long)(bytesBigEndian[0]) << 56) + + ((long)(bytesBigEndian[1]) << 48) + + ((long)(bytesBigEndian[2]) << 40) + + ((long)(bytesBigEndian[3]) << 32) + + ((long)(bytesBigEndian[4]) << 24) + + ((long)(bytesBigEndian[5]) << 16) + + ((long)(bytesBigEndian[6]) << 8) + + (long)(bytesBigEndian[7]); + } + + public int ReadInt (bool optimizePositive) { + int b = input.ReadByte(); + int result = b & 0x7F; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) result |= (input.ReadByte() & 0x7F) << 28; + } + } + } + return optimizePositive ? result : ((result >> 1) ^ -(result & 1)); + } + + public string ReadString () { + int byteCount = ReadInt(true); + switch (byteCount) { + case 0: + return null; + case 1: + return ""; + } + byteCount--; + byte[] buffer = this.chars; + if (buffer.Length < byteCount) buffer = new byte[byteCount]; + ReadFully(buffer, 0, byteCount); + return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount); + } + + ///May be null. + public String ReadStringRef () { + int index = ReadInt(true); + return index == 0 ? null : strings[index - 1]; + } + + public void ReadFully (byte[] buffer, int offset, int length) { + while (length > 0) { + int count = input.Read(buffer, offset, length); + if (count <= 0) throw new EndOfStreamException(); + offset += count; + length -= count; + } + } + + /// Returns the version string of binary skeleton data. + public string GetVersionString () { + try { + // try reading 4.0+ format + var initialPosition = input.Position; + ReadLong(); // long hash + + var stringPosition = input.Position; + int stringByteCount = ReadInt(true); + input.Position = stringPosition; + if (stringByteCount <= 13) { + string version = ReadString(); + if (char.IsDigit(version[0])) + return version; + } + // fallback to old version format + input.Position = initialPosition; + return GetVersionStringOld3X(); + } catch (Exception e) { + throw new ArgumentException("Stream does not contain valid binary Skeleton Data.\n" + e, "input"); + } + } + + /// Returns old 3.8 and earlier format version string of binary skeleton data. + public string GetVersionStringOld3X () { + // Hash. + int byteCount = ReadInt(true); + if (byteCount > 1) input.Position += byteCount - 1; + + // Version. + byteCount = ReadInt(true); + if (byteCount > 1 && byteCount <= 13) { + byteCount--; + var buffer = new byte[byteCount]; + ReadFully(buffer, 0, byteCount); + return System.Text.Encoding.UTF8.GetString(buffer, 0, byteCount); + } + throw new ArgumentException("Stream does not contain valid binary Skeleton Data."); + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonBounds.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonBounds.cs new file mode 100644 index 0000000..8f36486 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonBounds.cs @@ -0,0 +1,233 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + + /// + /// Collects each BoundingBoxAttachment that is visible and computes the world vertices for its polygon. + /// The polygon vertices are provided along with convenience methods for doing hit detection. + /// + public class SkeletonBounds { + private ExposedList polygonPool = new ExposedList(); + private float minX, minY, maxX, maxY; + + public ExposedList BoundingBoxes { get; private set; } + public ExposedList Polygons { get; private set; } + public float MinX { get { return minX; } set { minX = value; } } + public float MinY { get { return minY; } set { minY = value; } } + public float MaxX { get { return maxX; } set { maxX = value; } } + public float MaxY { get { return maxY; } set { maxY = value; } } + public float Width { get { return maxX - minX; } } + public float Height { get { return maxY - minY; } } + + public SkeletonBounds () { + BoundingBoxes = new ExposedList(); + Polygons = new ExposedList(); + } + + /// + /// Clears any previous polygons, finds all visible bounding box attachments, + /// and computes the world vertices for each bounding box's polygon. + /// The skeleton. + /// + /// If true, the axis aligned bounding box containing all the polygons is computed. + /// If false, the SkeletonBounds AABB methods will always return true. + /// + public void Update (Skeleton skeleton, bool updateAabb) { + ExposedList boundingBoxes = BoundingBoxes; + ExposedList polygons = Polygons; + Slot[] slots = skeleton.slots.Items; + int slotCount = skeleton.slots.Count; + + boundingBoxes.Clear(); + for (int i = 0, n = polygons.Count; i < n; i++) + polygonPool.Add(polygons.Items[i]); + polygons.Clear(); + + for (int i = 0; i < slotCount; i++) { + Slot slot = slots[i]; + if (!slot.bone.active) continue; + BoundingBoxAttachment boundingBox = slot.attachment as BoundingBoxAttachment; + if (boundingBox == null) continue; + boundingBoxes.Add(boundingBox); + + Polygon polygon = null; + int poolCount = polygonPool.Count; + if (poolCount > 0) { + polygon = polygonPool.Items[poolCount - 1]; + polygonPool.RemoveAt(poolCount - 1); + } else + polygon = new Polygon(); + polygons.Add(polygon); + + int count = boundingBox.worldVerticesLength; + polygon.Count = count; + if (polygon.Vertices.Length < count) polygon.Vertices = new float[count]; + boundingBox.ComputeWorldVertices(slot, polygon.Vertices); + } + + if (updateAabb) { + AabbCompute(); + } else { + minX = int.MinValue; + minY = int.MinValue; + maxX = int.MaxValue; + maxY = int.MaxValue; + } + } + + private void AabbCompute () { + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + Polygon[] polygons = Polygons.Items; + for (int i = 0, n = Polygons.Count; i < n; i++) { + Polygon polygon = polygons[i]; + float[] vertices = polygon.Vertices; + for (int ii = 0, nn = polygon.Count; ii < nn; ii += 2) { + float x = vertices[ii]; + float y = vertices[ii + 1]; + minX = Math.Min(minX, x); + minY = Math.Min(minY, y); + maxX = Math.Max(maxX, x); + maxY = Math.Max(maxY, y); + } + } + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + /// Returns true if the axis aligned bounding box contains the point. + public bool AabbContainsPoint (float x, float y) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + /// Returns true if the axis aligned bounding box intersects the line segment. + public bool AabbIntersectsSegment (float x1, float y1, float x2, float y2) { + float minX = this.minX; + float minY = this.minY; + float maxX = this.maxX; + float maxY = this.maxY; + if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) + return false; + float m = (y2 - y1) / (x2 - x1); + float y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + float x = (minY - y1) / m + x1; + if (x > minX && x < maxX) return true; + x = (maxY - y1) / m + x1; + if (x > minX && x < maxX) return true; + return false; + } + + /// Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. + public bool AabbIntersectsSkeleton (SkeletonBounds bounds) { + return minX < bounds.maxX && maxX > bounds.minX && minY < bounds.maxY && maxY > bounds.minY; + } + + /// Returns true if the polygon contains the point. + public bool ContainsPoint (Polygon polygon, float x, float y) { + float[] vertices = polygon.Vertices; + int nn = polygon.Count; + + int prevIndex = nn - 2; + bool inside = false; + for (int ii = 0; ii < nn; ii += 2) { + float vertexY = vertices[ii + 1]; + float prevY = vertices[prevIndex + 1]; + if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { + float vertexX = vertices[ii]; + if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; + } + prevIndex = ii; + } + return inside; + } + + /// Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more + /// efficient to only call this method if returns true. + public BoundingBoxAttachment ContainsPoint (float x, float y) { + Polygon[] polygons = Polygons.Items; + for (int i = 0, n = Polygons.Count; i < n; i++) + if (ContainsPoint(polygons[i], x, y)) return BoundingBoxes.Items[i]; + return null; + } + + /// Returns the first bounding box attachment that contains the line segment, or null. When doing many checks, it is usually + /// more efficient to only call this method if returns true. + public BoundingBoxAttachment IntersectsSegment (float x1, float y1, float x2, float y2) { + Polygon[] polygons = Polygons.Items; + for (int i = 0, n = Polygons.Count; i < n; i++) + if (IntersectsSegment(polygons[i], x1, y1, x2, y2)) return BoundingBoxes.Items[i]; + return null; + } + + /// Returns true if the polygon contains the line segment. + public bool IntersectsSegment (Polygon polygon, float x1, float y1, float x2, float y2) { + float[] vertices = polygon.Vertices; + int nn = polygon.Count; + + float width12 = x1 - x2, height12 = y1 - y2; + float det1 = x1 * y2 - y1 * x2; + float x3 = vertices[nn - 2], y3 = vertices[nn - 1]; + for (int ii = 0; ii < nn; ii += 2) { + float x4 = vertices[ii], y4 = vertices[ii + 1]; + float det2 = x3 * y4 - y3 * x4; + float width34 = x3 - x4, height34 = y3 - y4; + float det3 = width12 * height34 - height12 * width34; + float x = (det1 * width34 - width12 * det2) / det3; + if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { + float y = (det1 * height34 - height12 * det2) / det3; + if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; + } + x3 = x4; + y3 = y4; + } + return false; + } + + public Polygon GetPolygon (BoundingBoxAttachment attachment) { + int index = BoundingBoxes.IndexOf(attachment); + return index == -1 ? null : Polygons.Items[index]; + } + } + + public class Polygon { + public float[] Vertices { get; set; } + public int Count { get; set; } + + public Polygon () { + Vertices = new float[16]; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonClipping.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonClipping.cs new file mode 100644 index 0000000..df478e7 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonClipping.cs @@ -0,0 +1,292 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + public class SkeletonClipping { + internal readonly Triangulator triangulator = new Triangulator(); + internal readonly ExposedList clippingPolygon = new ExposedList(); + internal readonly ExposedList clipOutput = new ExposedList(128); + internal readonly ExposedList clippedVertices = new ExposedList(128); + internal readonly ExposedList clippedTriangles = new ExposedList(128); + internal readonly ExposedList clippedUVs = new ExposedList(128); + internal readonly ExposedList scratch = new ExposedList(); + + internal ClippingAttachment clipAttachment; + internal ExposedList> clippingPolygons; + + public ExposedList ClippedVertices { get { return clippedVertices; } } + public ExposedList ClippedTriangles { get { return clippedTriangles; } } + public ExposedList ClippedUVs { get { return clippedUVs; } } + + public bool IsClipping { get { return clipAttachment != null; } } + + public int ClipStart (Slot slot, ClippingAttachment clip) { + if (clipAttachment != null) return 0; + clipAttachment = clip; + + int n = clip.worldVerticesLength; + float[] vertices = clippingPolygon.Resize(n).Items; + clip.ComputeWorldVertices(slot, 0, n, vertices, 0, 2); + MakeClockwise(clippingPolygon); + clippingPolygons = triangulator.Decompose(clippingPolygon, triangulator.Triangulate(clippingPolygon)); + foreach (var polygon in clippingPolygons) { + MakeClockwise(polygon); + polygon.Add(polygon.Items[0]); + polygon.Add(polygon.Items[1]); + } + return clippingPolygons.Count; + } + + public void ClipEnd (Slot slot) { + if (clipAttachment != null && clipAttachment.endSlot == slot.data) ClipEnd(); + } + + public void ClipEnd () { + if (clipAttachment == null) return; + clipAttachment = null; + clippingPolygons = null; + clippedVertices.Clear(); + clippedTriangles.Clear(); + clippingPolygon.Clear(); + } + + public void ClipTriangles (float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs) { + ExposedList clipOutput = this.clipOutput, clippedVertices = this.clippedVertices; + var clippedTriangles = this.clippedTriangles; + var polygons = clippingPolygons.Items; + int polygonsCount = clippingPolygons.Count; + + int index = 0; + clippedVertices.Clear(); + clippedUVs.Clear(); + clippedTriangles.Clear(); + //outer: + for (int i = 0; i < trianglesLength; i += 3) { + int vertexOffset = triangles[i] << 1; + float x1 = vertices[vertexOffset], y1 = vertices[vertexOffset + 1]; + float u1 = uvs[vertexOffset], v1 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 1] << 1; + float x2 = vertices[vertexOffset], y2 = vertices[vertexOffset + 1]; + float u2 = uvs[vertexOffset], v2 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 2] << 1; + float x3 = vertices[vertexOffset], y3 = vertices[vertexOffset + 1]; + float u3 = uvs[vertexOffset], v3 = uvs[vertexOffset + 1]; + + for (int p = 0; p < polygonsCount; p++) { + int s = clippedVertices.Count; + if (Clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) { + int clipOutputLength = clipOutput.Count; + if (clipOutputLength == 0) continue; + float d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1; + float d = 1 / (d0 * d2 + d1 * (y1 - y3)); + + int clipOutputCount = clipOutputLength >> 1; + float[] clipOutputItems = clipOutput.Items; + float[] clippedVerticesItems = clippedVertices.Resize(s + clipOutputCount * 2).Items; + float[] clippedUVsItems = clippedUVs.Resize(s + clipOutputCount * 2).Items; + for (int ii = 0; ii < clipOutputLength; ii += 2) { + float x = clipOutputItems[ii], y = clipOutputItems[ii + 1]; + clippedVerticesItems[s] = x; + clippedVerticesItems[s + 1] = y; + float c0 = x - x3, c1 = y - y3; + float a = (d0 * c0 + d1 * c1) * d; + float b = (d4 * c0 + d2 * c1) * d; + float c = 1 - a - b; + clippedUVsItems[s] = u1 * a + u2 * b + u3 * c; + clippedUVsItems[s + 1] = v1 * a + v2 * b + v3 * c; + s += 2; + } + + s = clippedTriangles.Count; + int[] clippedTrianglesItems = clippedTriangles.Resize(s + 3 * (clipOutputCount - 2)).Items; + clipOutputCount--; + for (int ii = 1; ii < clipOutputCount; ii++) { + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + ii; + clippedTrianglesItems[s + 2] = index + ii + 1; + s += 3; + } + index += clipOutputCount + 1; + } else { + float[] clippedVerticesItems = clippedVertices.Resize(s + 3 * 2).Items; + float[] clippedUVsItems = clippedUVs.Resize(s + 3 * 2).Items; + clippedVerticesItems[s] = x1; + clippedVerticesItems[s + 1] = y1; + clippedVerticesItems[s + 2] = x2; + clippedVerticesItems[s + 3] = y2; + clippedVerticesItems[s + 4] = x3; + clippedVerticesItems[s + 5] = y3; + + clippedUVsItems[s] = u1; + clippedUVsItems[s + 1] = v1; + clippedUVsItems[s + 2] = u2; + clippedUVsItems[s + 3] = v2; + clippedUVsItems[s + 4] = u3; + clippedUVsItems[s + 5] = v3; + + s = clippedTriangles.Count; + int[] clippedTrianglesItems = clippedTriangles.Resize(s + 3).Items; + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + 1; + clippedTrianglesItems[s + 2] = index + 2; + index += 3; + break; //continue outer; + } + } + } + + } + + /** Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping + * area, false is returned. The clipping area must duplicate the first vertex at the end of the vertices list. */ + internal bool Clip (float x1, float y1, float x2, float y2, float x3, float y3, ExposedList clippingArea, ExposedList output) { + var originalOutput = output; + var clipped = false; + + // Avoid copy at the end. + ExposedList input = null; + if (clippingArea.Count % 4 >= 2) { + input = output; + output = scratch; + } else { + input = scratch; + } + + input.Clear(); + input.Add(x1); + input.Add(y1); + input.Add(x2); + input.Add(y2); + input.Add(x3); + input.Add(y3); + input.Add(x1); + input.Add(y1); + output.Clear(); + + float[] clippingVertices = clippingArea.Items; + int clippingVerticesLast = clippingArea.Count - 4; + for (int i = 0; ; i += 2) { + float edgeX = clippingVertices[i], edgeY = clippingVertices[i + 1]; + float edgeX2 = clippingVertices[i + 2], edgeY2 = clippingVertices[i + 3]; + float deltaX = edgeX - edgeX2, deltaY = edgeY - edgeY2; + + float[] inputVertices = input.Items; + int inputVerticesLength = input.Count - 2, outputStart = output.Count; + for (int ii = 0; ii < inputVerticesLength; ii += 2) { + float inputX = inputVertices[ii], inputY = inputVertices[ii + 1]; + float inputX2 = inputVertices[ii + 2], inputY2 = inputVertices[ii + 3]; + bool side2 = deltaX * (inputY2 - edgeY2) - deltaY * (inputX2 - edgeX2) > 0; + if (deltaX * (inputY - edgeY2) - deltaY * (inputX - edgeX2) > 0) { + if (side2) { // v1 inside, v2 inside + output.Add(inputX2); + output.Add(inputY2); + continue; + } + // v1 inside, v2 outside + float c0 = inputY2 - inputY, c2 = inputX2 - inputX; + float s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.Abs(s) > 0.000001f) { + float ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.Add(edgeX + (edgeX2 - edgeX) * ua); + output.Add(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.Add(edgeX); + output.Add(edgeY); + } + } else if (side2) { // v1 outside, v2 inside + float c0 = inputY2 - inputY, c2 = inputX2 - inputX; + float s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.Abs(s) > 0.000001f) { + float ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.Add(edgeX + (edgeX2 - edgeX) * ua); + output.Add(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.Add(edgeX); + output.Add(edgeY); + } + output.Add(inputX2); + output.Add(inputY2); + } + clipped = true; + } + + if (outputStart == output.Count) { // All edges outside. + originalOutput.Clear(); + return true; + } + + output.Add(output.Items[0]); + output.Add(output.Items[1]); + + if (i == clippingVerticesLast) break; + var temp = output; + output = input; + output.Clear(); + input = temp; + } + + if (originalOutput != output) { + originalOutput.Clear(); + for (int i = 0, n = output.Count - 2; i < n; i++) + originalOutput.Add(output.Items[i]); + } else + originalOutput.Resize(originalOutput.Count - 2); + + return clipped; + } + + public static void MakeClockwise (ExposedList polygon) { + float[] vertices = polygon.Items; + int verticeslength = polygon.Count; + + float area = vertices[verticeslength - 2] * vertices[1] - vertices[0] * vertices[verticeslength - 1], p1x, p1y, p2x, p2y; + for (int i = 0, n = verticeslength - 3; i < n; i += 2) { + p1x = vertices[i]; + p1y = vertices[i + 1]; + p2x = vertices[i + 2]; + p2y = vertices[i + 3]; + area += p1x * p2y - p2x * p1y; + } + if (area < 0) return; + + for (int i = 0, lastX = verticeslength - 2, n = verticeslength >> 1; i < n; i += 2) { + float x = vertices[i], y = vertices[i + 1]; + int other = lastX - i; + vertices[i] = vertices[other]; + vertices[i + 1] = vertices[other + 1]; + vertices[other] = x; + vertices[other + 1] = y; + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonData.cs new file mode 100644 index 0000000..d6776ad --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonData.cs @@ -0,0 +1,227 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + + /// Stores the setup pose and all of the stateless data for a skeleton. + public class SkeletonData { + internal string name; + internal ExposedList bones = new ExposedList(); // Ordered parents first + internal ExposedList slots = new ExposedList(); // Setup pose draw order. + internal ExposedList skins = new ExposedList(); + internal Skin defaultSkin; + internal ExposedList events = new ExposedList(); + internal ExposedList animations = new ExposedList(); + internal ExposedList ikConstraints = new ExposedList(); + internal ExposedList transformConstraints = new ExposedList(); + internal ExposedList pathConstraints = new ExposedList(); + internal ExposedList springConstraints = new ExposedList(); + internal float x, y, width, height; + internal string version, hash; + + // Nonessential. + internal float fps; + internal string imagesPath, audioPath; + + ///The skeleton's name, which by default is the name of the skeleton data file when possible, or null when a name hasn't been + ///set. + public string Name { get { return name; } set { name = value; } } + + /// The skeleton's bones, sorted parent first. The root bone is always the first bone. + public ExposedList Bones { get { return bones; } } + + public ExposedList Slots { get { return slots; } } + + /// All skins, including the default skin. + public ExposedList Skins { get { return skins; } set { skins = value; } } + + /// + /// The skeleton's default skin. + /// By default this skin contains all attachments that were not in a skin in Spine. + /// + /// May be null. + public Skin DefaultSkin { get { return defaultSkin; } set { defaultSkin = value; } } + + public ExposedList Events { get { return events; } set { events = value; } } + public ExposedList Animations { get { return animations; } set { animations = value; } } + public ExposedList IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } } + public ExposedList TransformConstraints { get { return transformConstraints; } set { transformConstraints = value; } } + public ExposedList PathConstraints { get { return pathConstraints; } set { pathConstraints = value; } } + + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Width { get { return width; } set { width = value; } } + public float Height { get { return height; } set { height = value; } } + /// The Spine version used to export this data, or null. + public string Version { get { return version; } set { version = value; } } + + ///The skeleton data hash. This value will change if any of the skeleton data has changed. + ///May be null. + public string Hash { get { return hash; } set { hash = value; } } + + public string ImagesPath { get { return imagesPath; } set { imagesPath = value; } } + + /// The path to the audio directory as defined in Spine. Available only when nonessential data was exported. + /// May be null. + public string AudioPath { get { return audioPath; } set { audioPath = value; } } + + /// The dopesheet FPS in Spine, or zero if nonessential data was not exported. + public float Fps { get { return fps; } set { fps = value; } } + + // --- Bones + + /// + /// Finds a bone by comparing each bone's name. + /// It is more efficient to cache the results of this method than to call it multiple times. + /// May be null. + public BoneData FindBone (string boneName) { + if (boneName == null) throw new ArgumentNullException("boneName", "boneName cannot be null."); + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + BoneData bone = bones[i]; + if (bone.name == boneName) return bone; + } + return null; + } + + // --- Slots + + /// May be null. + public SlotData FindSlot (string slotName) { + if (slotName == null) throw new ArgumentNullException("slotName", "slotName cannot be null."); + var slots = this.slots.Items; + for (int i = 0, n = this.slots.Count; i < n; i++) { + SlotData slot = slots[i]; + if (slot.name == slotName) return slot; + } + return null; + } + + // --- Skins + + /// May be null. + public Skin FindSkin (string skinName) { + if (skinName == null) throw new ArgumentNullException("skinName", "skinName cannot be null."); + foreach (Skin skin in skins) + if (skin.name == skinName) return skin; + return null; + } + + // --- Events + + /// May be null. + public EventData FindEvent (string eventDataName) { + if (eventDataName == null) throw new ArgumentNullException("eventDataName", "eventDataName cannot be null."); + foreach (EventData eventData in events) + if (eventData.name == eventDataName) return eventData; + return null; + } + + // --- Animations + + /// May be null. + public Animation FindAnimation (string animationName) { + if (animationName == null) throw new ArgumentNullException("animationName", "animationName cannot be null."); + var animations = this.animations.Items; + for (int i = 0, n = this.animations.Count; i < n; i++) { + Animation animation = animations[i]; + if (animation.name == animationName) return animation; + } + return null; + } + + // --- IK constraints + + /// May be null. + public IkConstraintData FindIkConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var ikConstraints = this.ikConstraints.Items; + for (int i = 0, n = this.ikConstraints.Count; i < n; i++) { + IkConstraintData ikConstraint = ikConstraints[i]; + if (ikConstraint.name == constraintName) return ikConstraint; + } + return null; + } + + // --- Transform constraints + + /// May be null. + public TransformConstraintData FindTransformConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var transformConstraints = this.transformConstraints.Items; + for (int i = 0, n = this.transformConstraints.Count; i < n; i++) { + TransformConstraintData transformConstraint = transformConstraints[i]; + if (transformConstraint.name == constraintName) return transformConstraint; + } + return null; + } + + // --- Path constraints + + /// + /// Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method + /// than to call it multiple times. + /// + /// May be null. + public PathConstraintData FindPathConstraint (string constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + var pathConstraints = this.pathConstraints.Items; + for (int i = 0, n = this.pathConstraints.Count; i < n; i++) { + PathConstraintData constraint = pathConstraints[i]; + if (constraint.name.Equals(constraintName)) return constraint; + } + return null; + } + + // --- Spring constraints + + /// + /// Finds a spring constraint by comparing each spring constraint's name. It is more efficient to cache the results of this + /// method than to call it multiple times. + /// + /// May be null. + public SpringConstraintData FindSpringConstraint (String constraintName) { + if (constraintName == null) throw new ArgumentNullException("constraintName", "constraintName cannot be null."); + Object[] springConstraints = this.springConstraints.Items; + for (int i = 0, n = this.springConstraints.Count; i < n; i++) { + SpringConstraintData constraint = (SpringConstraintData)springConstraints[i]; + if (constraint.name.Equals(constraintName)) return constraint; + } + return null; + } + + // --- + + override public string ToString () { + return name ?? base.ToString(); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonJson.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonJson.cs new file mode 100644 index 0000000..f29f489 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonJson.cs @@ -0,0 +1,1240 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.Collections.Generic; +using System.IO; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace Spine4_1_00 { + + /// + /// Loads skeleton data in the Spine JSON format. + /// + /// JSON is human readable but the binary format is much smaller on disk and faster to load. See . + /// + /// See Spine JSON format and + /// JSON and binary data in the Spine + /// Runtimes Guide. + /// + public class SkeletonJson : SkeletonLoader { + + public SkeletonJson (AttachmentLoader attachmentLoader) + : base(attachmentLoader) { + } + + public SkeletonJson (params Atlas[] atlasArray) + : base(atlasArray) { + } + +#if !IS_UNITY && WINDOWS_STOREAPP + private async Task ReadFile(string path) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false); + using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) { + SkeletonData skeletonData = ReadSkeletonData(reader); + skeletonData.Name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + + public override SkeletonData ReadSkeletonData (string path) { + return this.ReadFile(path).Result; + } +#else + public override SkeletonData ReadSkeletonData (string path) { +#if WINDOWS_PHONE + using (var reader = new StreamReader(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) { +#else + using (var reader = new StreamReader(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))) { +#endif + SkeletonData skeletonData = ReadSkeletonData(reader); + skeletonData.name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } +#endif + + public SkeletonData ReadSkeletonData (TextReader reader) { + if (reader == null) throw new ArgumentNullException("reader", "reader cannot be null."); + + float scale = this.scale; + var skeletonData = new SkeletonData(); + + var root = Json.Deserialize(reader) as Dictionary; + if (root == null) throw new Exception("Invalid JSON."); + + // Skeleton. + if (root.ContainsKey("skeleton")) { + var skeletonMap = (Dictionary)root["skeleton"]; + skeletonData.hash = (string)skeletonMap["hash"]; + skeletonData.version = (string)skeletonMap["spine"]; + skeletonData.x = GetFloat(skeletonMap, "x", 0); + skeletonData.y = GetFloat(skeletonMap, "y", 0); + skeletonData.width = GetFloat(skeletonMap, "width", 0); + skeletonData.height = GetFloat(skeletonMap, "height", 0); + skeletonData.fps = GetFloat(skeletonMap, "fps", 30); + skeletonData.imagesPath = GetString(skeletonMap, "images", null); + skeletonData.audioPath = GetString(skeletonMap, "audio", null); + } + + // Bones. + if (root.ContainsKey("bones")) { + foreach (Dictionary boneMap in (List)root["bones"]) { + BoneData parent = null; + if (boneMap.ContainsKey("parent")) { + parent = skeletonData.FindBone((string)boneMap["parent"]); + if (parent == null) + throw new Exception("Parent bone not found: " + boneMap["parent"]); + } + var data = new BoneData(skeletonData.Bones.Count, (string)boneMap["name"], parent); + data.length = GetFloat(boneMap, "length", 0) * scale; + data.x = GetFloat(boneMap, "x", 0) * scale; + data.y = GetFloat(boneMap, "y", 0) * scale; + data.rotation = GetFloat(boneMap, "rotation", 0); + data.scaleX = GetFloat(boneMap, "scaleX", 1); + data.scaleY = GetFloat(boneMap, "scaleY", 1); + data.shearX = GetFloat(boneMap, "shearX", 0); + data.shearY = GetFloat(boneMap, "shearY", 0); + + string tm = GetString(boneMap, "transform", TransformMode.Normal.ToString()); + data.transformMode = (TransformMode)Enum.Parse(typeof(TransformMode), tm, true); + data.skinRequired = GetBoolean(boneMap, "skin", false); + + skeletonData.bones.Add(data); + } + } + + // Slots. + if (root.ContainsKey("slots")) { + foreach (Dictionary slotMap in (List)root["slots"]) { + var slotName = (string)slotMap["name"]; + var boneName = (string)slotMap["bone"]; + BoneData boneData = skeletonData.FindBone(boneName); + if (boneData == null) throw new Exception("Slot bone not found: " + boneName); + var data = new SlotData(skeletonData.Slots.Count, slotName, boneData); + + if (slotMap.ContainsKey("color")) { + string color = (string)slotMap["color"]; + data.r = ToColor(color, 0); + data.g = ToColor(color, 1); + data.b = ToColor(color, 2); + data.a = ToColor(color, 3); + } + + if (slotMap.ContainsKey("dark")) { + var color2 = (string)slotMap["dark"]; + data.r2 = ToColor(color2, 0, 6); // expectedLength = 6. ie. "RRGGBB" + data.g2 = ToColor(color2, 1, 6); + data.b2 = ToColor(color2, 2, 6); + data.hasSecondColor = true; + } + + data.attachmentName = GetString(slotMap, "attachment", null); + if (slotMap.ContainsKey("blend")) + data.blendMode = (BlendMode)Enum.Parse(typeof(BlendMode), (string)slotMap["blend"], true); + else + data.blendMode = BlendMode.Normal; + skeletonData.slots.Add(data); + } + } + + // IK constraints. + if (root.ContainsKey("ik")) { + foreach (Dictionary constraintMap in (List)root["ik"]) { + IkConstraintData data = new IkConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + data.skinRequired = GetBoolean(constraintMap, "skin", false); + + if (constraintMap.ContainsKey("bones")) { + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("IK bone not found: " + boneName); + data.bones.Add(bone); + } + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindBone(targetName); + if (data.target == null) throw new Exception("IK target bone not found: " + targetName); + data.mix = GetFloat(constraintMap, "mix", 1); + data.softness = GetFloat(constraintMap, "softness", 0) * scale; + data.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1; + data.compress = GetBoolean(constraintMap, "compress", false); + data.stretch = GetBoolean(constraintMap, "stretch", false); + data.uniform = GetBoolean(constraintMap, "uniform", false); + + skeletonData.ikConstraints.Add(data); + } + } + + // Transform constraints. + if (root.ContainsKey("transform")) { + foreach (Dictionary constraintMap in (List)root["transform"]) { + TransformConstraintData data = new TransformConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + data.skinRequired = GetBoolean(constraintMap, "skin", false); + + if (constraintMap.ContainsKey("bones")) { + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("Transform constraint bone not found: " + boneName); + data.bones.Add(bone); + } + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindBone(targetName); + if (data.target == null) throw new Exception("Transform constraint target bone not found: " + targetName); + + data.local = GetBoolean(constraintMap, "local", false); + data.relative = GetBoolean(constraintMap, "relative", false); + + data.offsetRotation = GetFloat(constraintMap, "rotation", 0); + data.offsetX = GetFloat(constraintMap, "x", 0) * scale; + data.offsetY = GetFloat(constraintMap, "y", 0) * scale; + data.offsetScaleX = GetFloat(constraintMap, "scaleX", 0); + data.offsetScaleY = GetFloat(constraintMap, "scaleY", 0); + data.offsetShearY = GetFloat(constraintMap, "shearY", 0); + + data.mixRotate = GetFloat(constraintMap, "mixRotate", 1); + data.mixX = GetFloat(constraintMap, "mixX", 1); + data.mixY = GetFloat(constraintMap, "mixY", data.mixX); + data.mixScaleX = GetFloat(constraintMap, "mixScaleX", 1); + data.mixScaleY = GetFloat(constraintMap, "mixScaleY", data.mixScaleX); + data.mixShearY = GetFloat(constraintMap, "mixShearY", 1); + + skeletonData.transformConstraints.Add(data); + } + } + + // Path constraints. + if (root.ContainsKey("path")) { + foreach (Dictionary constraintMap in (List)root["path"]) { + PathConstraintData data = new PathConstraintData((string)constraintMap["name"]); + data.order = GetInt(constraintMap, "order", 0); + data.skinRequired = GetBoolean(constraintMap, "skin", false); + + if (constraintMap.ContainsKey("bones")) { + foreach (string boneName in (List)constraintMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("Path bone not found: " + boneName); + data.bones.Add(bone); + } + } + + string targetName = (string)constraintMap["target"]; + data.target = skeletonData.FindSlot(targetName); + if (data.target == null) throw new Exception("Path target slot not found: " + targetName); + + data.positionMode = (PositionMode)Enum.Parse(typeof(PositionMode), GetString(constraintMap, "positionMode", "percent"), true); + data.spacingMode = (SpacingMode)Enum.Parse(typeof(SpacingMode), GetString(constraintMap, "spacingMode", "length"), true); + data.rotateMode = (RotateMode)Enum.Parse(typeof(RotateMode), GetString(constraintMap, "rotateMode", "tangent"), true); + data.offsetRotation = GetFloat(constraintMap, "rotation", 0); + data.position = GetFloat(constraintMap, "position", 0); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = GetFloat(constraintMap, "spacing", 0); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.mixRotate = GetFloat(constraintMap, "mixRotate", 1); + data.mixX = GetFloat(constraintMap, "mixX", 1); + data.mixY = GetFloat(constraintMap, "mixY", data.mixX); + + skeletonData.pathConstraints.Add(data); + } + } + + // Skins. + if (root.ContainsKey("skins")) { + foreach (Dictionary skinMap in (List)root["skins"]) { + Skin skin = new Skin((string)skinMap["name"]); + if (skinMap.ContainsKey("bones")) { + foreach (string entryName in (List)skinMap["bones"]) { + BoneData bone = skeletonData.FindBone(entryName); + if (bone == null) throw new Exception("Skin bone not found: " + entryName); + skin.bones.Add(bone); + } + } + skin.bones.TrimExcess(); + if (skinMap.ContainsKey("ik")) { + foreach (string entryName in (List)skinMap["ik"]) { + IkConstraintData constraint = skeletonData.FindIkConstraint(entryName); + if (constraint == null) throw new Exception("Skin IK constraint not found: " + entryName); + skin.constraints.Add(constraint); + } + } + if (skinMap.ContainsKey("transform")) { + foreach (string entryName in (List)skinMap["transform"]) { + TransformConstraintData constraint = skeletonData.FindTransformConstraint(entryName); + if (constraint == null) throw new Exception("Skin transform constraint not found: " + entryName); + skin.constraints.Add(constraint); + } + } + if (skinMap.ContainsKey("path")) { + foreach (string entryName in (List)skinMap["path"]) { + PathConstraintData constraint = skeletonData.FindPathConstraint(entryName); + if (constraint == null) throw new Exception("Skin path constraint not found: " + entryName); + skin.constraints.Add(constraint); + } + } + skin.constraints.TrimExcess(); + if (skinMap.ContainsKey("attachments")) { + foreach (KeyValuePair slotEntry in (Dictionary)skinMap["attachments"]) { + int slotIndex = FindSlotIndex(skeletonData, slotEntry.Key); + foreach (KeyValuePair entry in ((Dictionary)slotEntry.Value)) { + try { + Attachment attachment = ReadAttachment((Dictionary)entry.Value, skin, slotIndex, entry.Key, skeletonData); + if (attachment != null) skin.SetAttachment(slotIndex, entry.Key, attachment); + } catch (Exception e) { + throw new Exception("Error reading attachment: " + entry.Key + ", skin: " + skin, e); + } + } + } + } + skeletonData.skins.Add(skin); + if (skin.name == "default") skeletonData.defaultSkin = skin; + } + } + + // Linked meshes. + for (int i = 0, n = linkedMeshes.Count; i < n; i++) { + LinkedMesh linkedMesh = linkedMeshes[i]; + Skin skin = linkedMesh.skin == null ? skeletonData.defaultSkin : skeletonData.FindSkin(linkedMesh.skin); + if (skin == null) throw new Exception("Slot not found: " + linkedMesh.skin); + Attachment parent = skin.GetAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Exception("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.TimelineAttachment = linkedMesh.inheritTimelines ? (VertexAttachment)parent : linkedMesh.mesh; + linkedMesh.mesh.ParentMesh = (MeshAttachment)parent; + if (linkedMesh.mesh.Region != null) linkedMesh.mesh.UpdateRegion(); + } + linkedMeshes.Clear(); + + // Events. + if (root.ContainsKey("events")) { + foreach (KeyValuePair entry in (Dictionary)root["events"]) { + var entryMap = (Dictionary)entry.Value; + var data = new EventData(entry.Key); + data.Int = GetInt(entryMap, "int", 0); + data.Float = GetFloat(entryMap, "float", 0); + data.String = GetString(entryMap, "string", string.Empty); + data.AudioPath = GetString(entryMap, "audio", null); + if (data.AudioPath != null) { + data.Volume = GetFloat(entryMap, "volume", 1); + data.Balance = GetFloat(entryMap, "balance", 0); + } + skeletonData.events.Add(data); + } + } + + // Animations. + if (root.ContainsKey("animations")) { + foreach (KeyValuePair entry in (Dictionary)root["animations"]) { + try { + ReadAnimation((Dictionary)entry.Value, entry.Key, skeletonData); + } catch (Exception e) { + throw new Exception("Error reading animation: " + entry.Key + "\n" + e.Message, e); + } + } + } + + skeletonData.bones.TrimExcess(); + skeletonData.slots.TrimExcess(); + skeletonData.skins.TrimExcess(); + skeletonData.events.TrimExcess(); + skeletonData.animations.TrimExcess(); + skeletonData.ikConstraints.TrimExcess(); + return skeletonData; + } + + private Attachment ReadAttachment (Dictionary map, Skin skin, int slotIndex, string name, SkeletonData skeletonData) { + float scale = this.scale; + name = GetString(map, "name", name); + + var typeName = GetString(map, "type", "region"); + var type = (AttachmentType)Enum.Parse(typeof(AttachmentType), typeName, true); + + switch (type) { + case AttachmentType.Region: { + string path = GetString(map, "path", name); + object sequenceJson; + map.TryGetValue("sequence", out sequenceJson); + Sequence sequence = ReadSequence(sequenceJson); + RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path, sequence); + if (region == null) return null; + region.Path = path; + region.x = GetFloat(map, "x", 0) * scale; + region.y = GetFloat(map, "y", 0) * scale; + region.scaleX = GetFloat(map, "scaleX", 1); + region.scaleY = GetFloat(map, "scaleY", 1); + region.rotation = GetFloat(map, "rotation", 0); + region.width = GetFloat(map, "width", 32) * scale; + region.height = GetFloat(map, "height", 32) * scale; + region.sequence = sequence; + + if (map.ContainsKey("color")) { + var color = (string)map["color"]; + region.r = ToColor(color, 0); + region.g = ToColor(color, 1); + region.b = ToColor(color, 2); + region.a = ToColor(color, 3); + } + + if (region.Region != null) region.UpdateRegion(); + return region; + } + case AttachmentType.Boundingbox: + BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name); + if (box == null) return null; + ReadVertices(map, box, GetInt(map, "vertexCount", 0) << 1); + return box; + case AttachmentType.Mesh: + case AttachmentType.Linkedmesh: { + string path = GetString(map, "path", name); + object sequenceJson; + map.TryGetValue("sequence", out sequenceJson); + Sequence sequence = ReadSequence(sequenceJson); + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path, sequence); + if (mesh == null) return null; + mesh.Path = path; + + if (map.ContainsKey("color")) { + var color = (string)map["color"]; + mesh.r = ToColor(color, 0); + mesh.g = ToColor(color, 1); + mesh.b = ToColor(color, 2); + mesh.a = ToColor(color, 3); + } + + mesh.Width = GetFloat(map, "width", 0) * scale; + mesh.Height = GetFloat(map, "height", 0) * scale; + mesh.Sequence = sequence; + + string parent = GetString(map, "parent", null); + if (parent != null) { + linkedMeshes.Add(new LinkedMesh(mesh, GetString(map, "skin", null), slotIndex, parent, GetBoolean(map, "timelines", true))); + return mesh; + } + + float[] uvs = GetFloatArray(map, "uvs", 1); + ReadVertices(map, mesh, uvs.Length); + mesh.triangles = GetIntArray(map, "triangles"); + mesh.regionUVs = uvs; + if (mesh.Region != null) mesh.UpdateRegion(); + + if (map.ContainsKey("hull")) mesh.HullLength = GetInt(map, "hull", 0) << 1; + if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges"); + return mesh; + } + case AttachmentType.Path: { + PathAttachment pathAttachment = attachmentLoader.NewPathAttachment(skin, name); + if (pathAttachment == null) return null; + pathAttachment.closed = GetBoolean(map, "closed", false); + pathAttachment.constantSpeed = GetBoolean(map, "constantSpeed", true); + + int vertexCount = GetInt(map, "vertexCount", 0); + ReadVertices(map, pathAttachment, vertexCount << 1); + + // potential BOZO see Java impl + pathAttachment.lengths = GetFloatArray(map, "lengths", scale); + return pathAttachment; + } + case AttachmentType.Point: { + PointAttachment point = attachmentLoader.NewPointAttachment(skin, name); + if (point == null) return null; + point.x = GetFloat(map, "x", 0) * scale; + point.y = GetFloat(map, "y", 0) * scale; + point.rotation = GetFloat(map, "rotation", 0); + + //string color = GetString(map, "color", null); + //if (color != null) point.color = color; + return point; + } + case AttachmentType.Clipping: { + ClippingAttachment clip = attachmentLoader.NewClippingAttachment(skin, name); + if (clip == null) return null; + + string end = GetString(map, "end", null); + if (end != null) { + SlotData slot = skeletonData.FindSlot(end); + if (slot == null) throw new Exception("Clipping end slot not found: " + end); + clip.EndSlot = slot; + } + + ReadVertices(map, clip, GetInt(map, "vertexCount", 0) << 1); + + //string color = GetString(map, "color", null); + // if (color != null) clip.color = color; + return clip; + } + } + return null; + } + + public static Sequence ReadSequence (object sequenceJson) { + var map = sequenceJson as Dictionary; + if (map == null) return null; + Sequence sequence = new Sequence(GetInt(map, "count")); + sequence.start = GetInt(map, "start", 1); + sequence.digits = GetInt(map, "digits", 0); + sequence.setupIndex = GetInt(map, "setup", 0); + return sequence; + } + + private void ReadVertices (Dictionary map, VertexAttachment attachment, int verticesLength) { + attachment.WorldVerticesLength = verticesLength; + float[] vertices = GetFloatArray(map, "vertices", 1); + float scale = Scale; + if (verticesLength == vertices.Length) { + if (scale != 1) { + for (int i = 0; i < vertices.Length; i++) { + vertices[i] *= scale; + } + } + attachment.vertices = vertices; + return; + } + ExposedList weights = new ExposedList(verticesLength * 3 * 3); + ExposedList bones = new ExposedList(verticesLength * 3); + for (int i = 0, n = vertices.Length; i < n;) { + int boneCount = (int)vertices[i++]; + bones.Add(boneCount); + for (int nn = i + (boneCount << 2); i < nn; i += 4) { + bones.Add((int)vertices[i]); + weights.Add(vertices[i + 1] * this.Scale); + weights.Add(vertices[i + 2] * this.Scale); + weights.Add(vertices[i + 3]); + } + } + attachment.bones = bones.ToArray(); + attachment.vertices = weights.ToArray(); + } + + private int FindSlotIndex (SkeletonData skeletonData, string slotName) { + SlotData[] slots = skeletonData.slots.Items; + for (int i = 0, n = skeletonData.slots.Count; i < n; i++) + if (slots[i].name == slotName) return i; + throw new Exception("Slot not found: " + slotName); + } + + private void ReadAnimation (Dictionary map, string name, SkeletonData skeletonData) { + var scale = this.scale; + var timelines = new ExposedList(); + + // Slot timelines. + if (map.ContainsKey("slots")) { + foreach (KeyValuePair entry in (Dictionary)map["slots"]) { + string slotName = entry.Key; + int slotIndex = FindSlotIndex(skeletonData, slotName); + var timelineMap = (Dictionary)entry.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + int frames = values.Count; + if (frames == 0) continue; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "attachment") { + var timeline = new AttachmentTimeline(frames, slotIndex); + int frame = 0; + foreach (Dictionary keyMap in values) { + timeline.SetFrame(frame++, GetFloat(keyMap, "time", 0), GetString(keyMap, "name", null)); + } + timelines.Add(timeline); + + } else if (timelineName == "rgba") { + var timeline = new RGBATimeline(frames, frames << 2, slotIndex); + + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + string color = (string)keyMap["color"]; + float r = ToColor(color, 0); + float g = ToColor(color, 1); + float b = ToColor(color, 2); + float a = ToColor(color, 3); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, a); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + + float time2 = GetFloat(nextMap, "time", 0); + color = (string)nextMap["color"]; + float nr = ToColor(color, 0); + float ng = ToColor(color, 1); + float nb = ToColor(color, 2); + float na = ToColor(color, 3); + + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1); + } + time = time2; + r = nr; + g = ng; + b = nb; + a = na; + keyMap = nextMap; + } + timelines.Add(timeline); + + } else if (timelineName == "rgb") { + var timeline = new RGBTimeline(frames, frames * 3, slotIndex); + + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + string color = (string)keyMap["color"]; + float r = ToColor(color, 0, 6); + float g = ToColor(color, 1, 6); + float b = ToColor(color, 2, 6); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + + float time2 = GetFloat(nextMap, "time", 0); + color = (string)nextMap["color"]; + float nr = ToColor(color, 0, 6); + float ng = ToColor(color, 1, 6); + float nb = ToColor(color, 2, 6); + + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1); + } + time = time2; + r = nr; + g = ng; + b = nb; + keyMap = nextMap; + } + timelines.Add(timeline); + + } else if (timelineName == "alpha") { + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + timelines.Add(ReadTimeline(ref keyMapEnumerator, new AlphaTimeline(frames, frames, slotIndex), 0, 1)); + + } else if (timelineName == "rgba2") { + var timeline = new RGBA2Timeline(frames, frames * 7, slotIndex); + + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + string color = (string)keyMap["light"]; + float r = ToColor(color, 0); + float g = ToColor(color, 1); + float b = ToColor(color, 2); + float a = ToColor(color, 3); + color = (string)keyMap["dark"]; + float r2 = ToColor(color, 0, 6); + float g2 = ToColor(color, 1, 6); + float b2 = ToColor(color, 2, 6); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, a, r2, g2, b2); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + + float time2 = GetFloat(nextMap, "time", 0); + color = (string)nextMap["light"]; + float nr = ToColor(color, 0); + float ng = ToColor(color, 1); + float nb = ToColor(color, 2); + float na = ToColor(color, 3); + color = (string)nextMap["dark"]; + float nr2 = ToColor(color, 0, 6); + float ng2 = ToColor(color, 1, 6); + float nb2 = ToColor(color, 2, 6); + + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 4, time, time2, r2, nr2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 5, time, time2, g2, ng2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 6, time, time2, b2, nb2, 1); + } + time = time2; + r = nr; + g = ng; + b = nb; + a = na; + r2 = nr2; + g2 = ng2; + b2 = nb2; + keyMap = nextMap; + } + timelines.Add(timeline); + + } else if (timelineName == "rgb2") { + var timeline = new RGB2Timeline(frames, frames * 6, slotIndex); + + var keyMapEnumerator = values.GetEnumerator(); + keyMapEnumerator.MoveNext(); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + string color = (string)keyMap["light"]; + float r = ToColor(color, 0, 6); + float g = ToColor(color, 1, 6); + float b = ToColor(color, 2, 6); + color = (string)keyMap["dark"]; + float r2 = ToColor(color, 0, 6); + float g2 = ToColor(color, 1, 6); + float b2 = ToColor(color, 2, 6); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, r, g, b, r2, g2, b2); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + + float time2 = GetFloat(nextMap, "time", 0); + color = (string)nextMap["light"]; + float nr = ToColor(color, 0, 6); + float ng = ToColor(color, 1, 6); + float nb = ToColor(color, 2, 6); + color = (string)nextMap["dark"]; + float nr2 = ToColor(color, 0, 6); + float ng2 = ToColor(color, 1, 6); + float nb2 = ToColor(color, 2, 6); + + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, r2, nr2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 4, time, time2, g2, ng2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 5, time, time2, b2, nb2, 1); + } + time = time2; + r = nr; + g = ng; + b = nb; + r2 = nr2; + g2 = ng2; + b2 = nb2; + keyMap = nextMap; + } + timelines.Add(timeline); + + } else + throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")"); + } + } + } + + // Bone timelines. + if (map.ContainsKey("bones")) { + foreach (KeyValuePair entry in (Dictionary)map["bones"]) { + string boneName = entry.Key; + int boneIndex = -1; + var bones = skeletonData.bones.Items; + for (int i = 0, n = skeletonData.bones.Count; i < n; i++) { + if (bones[i].name == boneName) { + boneIndex = i; + break; + } + } + if (boneIndex == -1) throw new Exception("Bone not found: " + boneName); + var timelineMap = (Dictionary)entry.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + int frames = values.Count; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "rotate") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new RotateTimeline(frames, frames, boneIndex), 0, 1)); + else if (timelineName == "translate") { + TranslateTimeline timeline = new TranslateTimeline(frames, frames << 1, boneIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 0, scale)); + } else if (timelineName == "translatex") { + timelines + .Add(ReadTimeline(ref keyMapEnumerator, new TranslateXTimeline(frames, frames, boneIndex), 0, scale)); + } else if (timelineName == "translatey") { + timelines + .Add(ReadTimeline(ref keyMapEnumerator, new TranslateYTimeline(frames, frames, boneIndex), 0, scale)); + } else if (timelineName == "scale") { + ScaleTimeline timeline = new ScaleTimeline(frames, frames << 1, boneIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 1, 1)); + } else if (timelineName == "scalex") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new ScaleXTimeline(frames, frames, boneIndex), 1, 1)); + else if (timelineName == "scaley") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new ScaleYTimeline(frames, frames, boneIndex), 1, 1)); + else if (timelineName == "shear") { + ShearTimeline timeline = new ShearTimeline(frames, frames << 1, boneIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, "x", "y", 0, 1)); + } else if (timelineName == "shearx") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new ShearXTimeline(frames, frames, boneIndex), 0, 1)); + else if (timelineName == "sheary") + timelines.Add(ReadTimeline(ref keyMapEnumerator, new ShearYTimeline(frames, frames, boneIndex), 0, 1)); + else + throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")"); + } + } + } + + // IK constraint timelines. + if (map.ContainsKey("ik")) { + foreach (KeyValuePair timelineMap in (Dictionary)map["ik"]) { + var values = (List)timelineMap.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + var keyMap = (Dictionary)keyMapEnumerator.Current; + IkConstraintData constraint = skeletonData.FindIkConstraint(timelineMap.Key); + IkConstraintTimeline timeline = new IkConstraintTimeline(values.Count, values.Count << 1, + skeletonData.IkConstraints.IndexOf(constraint)); + float time = GetFloat(keyMap, "time", 0); + float mix = GetFloat(keyMap, "mix", 1), softness = GetFloat(keyMap, "softness", 0) * scale; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mix, softness, GetBoolean(keyMap, "bendPositive", true) ? 1 : -1, + GetBoolean(keyMap, "compress", false), GetBoolean(keyMap, "stretch", false)); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float mix2 = GetFloat(nextMap, "mix", 1), softness2 = GetFloat(nextMap, "softness", 0) * scale; + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, mix, mix2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, softness, softness2, scale); + } + time = time2; + mix = mix2; + softness = softness2; + keyMap = nextMap; + } + timelines.Add(timeline); + } + } + + // Transform constraint timelines. + if (map.ContainsKey("transform")) { + foreach (KeyValuePair timelineMap in (Dictionary)map["transform"]) { + var values = (List)timelineMap.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + var keyMap = (Dictionary)keyMapEnumerator.Current; + TransformConstraintData constraint = skeletonData.FindTransformConstraint(timelineMap.Key); + TransformConstraintTimeline timeline = new TransformConstraintTimeline(values.Count, values.Count * 6, + skeletonData.TransformConstraints.IndexOf(constraint)); + float time = GetFloat(keyMap, "time", 0); + float mixRotate = GetFloat(keyMap, "mixRotate", 1), mixShearY = GetFloat(keyMap, "mixShearY", 1); + float mixX = GetFloat(keyMap, "mixX", 1), mixY = GetFloat(keyMap, "mixY", mixX); + float mixScaleX = GetFloat(keyMap, "mixScaleX", 1), mixScaleY = GetFloat(keyMap, "mixScaleY", mixScaleX); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float mixRotate2 = GetFloat(nextMap, "mixRotate", 1), mixShearY2 = GetFloat(nextMap, "mixShearY", 1); + float mixX2 = GetFloat(nextMap, "mixX", 1), mixY2 = GetFloat(nextMap, "mixY", mixX2); + float mixScaleX2 = GetFloat(nextMap, "mixScaleX", 1), mixScaleY2 = GetFloat(nextMap, "mixScaleY", mixScaleX2); + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 3, time, time2, mixScaleX, mixScaleX2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 4, time, time2, mixScaleY, mixScaleY2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 5, time, time2, mixShearY, mixShearY2, 1); + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + mixScaleX = mixScaleX2; + mixScaleY = mixScaleY2; + mixScaleX = mixScaleX2; + keyMap = nextMap; + } + timelines.Add(timeline); + } + } + + // Path constraint timelines. + if (map.ContainsKey("path")) { + foreach (KeyValuePair constraintMap in (Dictionary)map["path"]) { + PathConstraintData constraint = skeletonData.FindPathConstraint(constraintMap.Key); + if (constraint == null) throw new Exception("Path constraint not found: " + constraintMap.Key); + int constraintIndex = skeletonData.pathConstraints.IndexOf(constraint); + var timelineMap = (Dictionary)constraintMap.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + + int frames = values.Count; + var timelineName = (string)timelineEntry.Key; + if (timelineName == "position") { + CurveTimeline1 timeline = new PathConstraintPositionTimeline(frames, frames, constraintIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, 0, constraint.positionMode == PositionMode.Fixed ? scale : 1)); + } else if (timelineName == "spacing") { + CurveTimeline1 timeline = new PathConstraintSpacingTimeline(frames, frames, constraintIndex); + timelines.Add(ReadTimeline(ref keyMapEnumerator, timeline, 0, + constraint.spacingMode == SpacingMode.Length || constraint.spacingMode == SpacingMode.Fixed ? scale : 1)); + } else if (timelineName == "mix") { + PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(frames, frames * 3, constraintIndex); + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + float mixRotate = GetFloat(keyMap, "mixRotate", 1); + float mixX = GetFloat(keyMap, "mixX", 1), mixY = GetFloat(keyMap, "mixY", mixX); + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, mixRotate, mixX, mixY); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float mixRotate2 = GetFloat(nextMap, "mixRotate", 1); + float mixX2 = GetFloat(nextMap, "mixX", 1), mixY2 = GetFloat(nextMap, "mixY", mixX2); + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1); + bezier = ReadCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1); + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + keyMap = nextMap; + } + timelines.Add(timeline); + } + } + } + } + + // Attachment timelines. + if (map.ContainsKey("attachments")) { + foreach (KeyValuePair attachmentsMap in (Dictionary)map["attachments"]) { + Skin skin = skeletonData.FindSkin(attachmentsMap.Key); + foreach (KeyValuePair slotMap in (Dictionary)attachmentsMap.Value) { + SlotData slot = skeletonData.FindSlot(slotMap.Key); + if (slot == null) throw new Exception("Slot not found: " + slotMap.Key); + foreach (KeyValuePair attachmentMap in (Dictionary)slotMap.Value) { + Attachment attachment = skin.GetAttachment(slot.index, attachmentMap.Key); + if (attachment == null) throw new Exception("Timeline attachment not found: " + attachmentMap.Key); + foreach (KeyValuePair timelineMap in (Dictionary)attachmentMap.Value) { + var values = (List)timelineMap.Value; + var keyMapEnumerator = values.GetEnumerator(); + if (!keyMapEnumerator.MoveNext()) continue; + var keyMap = (Dictionary)keyMapEnumerator.Current; + int frames = values.Count; + string timelineName = timelineMap.Key; + if (timelineName == "deform") { + VertexAttachment vertexAttachment = (VertexAttachment)attachment; + bool weighted = vertexAttachment.bones != null; + float[] vertices = vertexAttachment.vertices; + int deformLength = weighted ? (vertices.Length / 3) << 1 : vertices.Length; + + DeformTimeline timeline = new DeformTimeline(frames, frames, slot.Index, vertexAttachment); + float time = GetFloat(keyMap, "time", 0); + for (int frame = 0, bezier = 0; ; frame++) { + float[] deform; + if (!keyMap.ContainsKey("vertices")) { + deform = weighted ? new float[deformLength] : vertices; + } else { + deform = new float[deformLength]; + int start = GetInt(keyMap, "offset", 0); + float[] verticesValue = GetFloatArray(keyMap, "vertices", 1); + Array.Copy(verticesValue, 0, deform, start, verticesValue.Length); + if (scale != 1) { + for (int i = start, n = i + verticesValue.Length; i < n; i++) + deform[i] *= scale; + } + + if (!weighted) { + for (int i = 0; i < deformLength; i++) + deform[i] += vertices[i]; + } + } + + timeline.SetFrame(frame, time, deform); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + break; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, 0, 1, 1); + } + time = time2; + keyMap = nextMap; + } + timelines.Add(timeline); + } else if (timelineName == "sequence") { + SequenceTimeline timeline = new SequenceTimeline(frames, slot.index, attachment); + float lastDelay = 0; + for (int frame = 0; keyMap != null; + keyMapEnumerator.MoveNext(), keyMap = (Dictionary)keyMapEnumerator.Current, frame++) { + + float delay = GetFloat(keyMap, "delay", lastDelay); + SequenceMode sequenceMode = (SequenceMode)Enum.Parse(typeof(SequenceMode), + GetString(keyMap, "mode", "hold"), true); + timeline.SetFrame(frame, GetFloat(keyMap, "time", 0), + sequenceMode, GetInt(keyMap, "index", 0), delay); + lastDelay = delay; + } + timelines.Add(timeline); + } + } + } + } + } + } + + // Draw order timeline. + if (map.ContainsKey("drawOrder")) { + var values = (List)map["drawOrder"]; + var timeline = new DrawOrderTimeline(values.Count); + int slotCount = skeletonData.slots.Count; + int frame = 0; + foreach (Dictionary drawOrderMap in values) { + int[] drawOrder = null; + if (drawOrderMap.ContainsKey("offsets")) { + drawOrder = new int[slotCount]; + for (int i = slotCount - 1; i >= 0; i--) + drawOrder[i] = -1; + var offsets = (List)drawOrderMap["offsets"]; + int[] unchanged = new int[slotCount - offsets.Count]; + int originalIndex = 0, unchangedIndex = 0; + foreach (Dictionary offsetMap in offsets) { + int slotIndex = FindSlotIndex(skeletonData, (string)offsetMap["slot"]); + // Collect unchanged items. + while (originalIndex != slotIndex) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + int index = originalIndex + (int)(float)offsetMap["offset"]; + drawOrder[index] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) + unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (int i = slotCount - 1; i >= 0; i--) + if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex]; + } + timeline.SetFrame(frame, GetFloat(drawOrderMap, "time", 0), drawOrder); + ++frame; + } + timelines.Add(timeline); + } + + // Event timeline. + if (map.ContainsKey("events")) { + var eventsMap = (List)map["events"]; + var timeline = new EventTimeline(eventsMap.Count); + int frame = 0; + foreach (Dictionary eventMap in eventsMap) { + EventData eventData = skeletonData.FindEvent((string)eventMap["name"]); + if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]); + var e = new Event(GetFloat(eventMap, "time", 0), eventData) { + intValue = GetInt(eventMap, "int", eventData.Int), + floatValue = GetFloat(eventMap, "float", eventData.Float), + stringValue = GetString(eventMap, "string", eventData.String) + }; + if (e.data.AudioPath != null) { + e.volume = GetFloat(eventMap, "volume", eventData.Volume); + e.balance = GetFloat(eventMap, "balance", eventData.Balance); + } + timeline.SetFrame(frame, e); + ++frame; + } + timelines.Add(timeline); + } + timelines.TrimExcess(); + float duration = 0; + var items = timelines.Items; + for (int i = 0, n = timelines.Count; i < n; i++) + duration = Math.Max(duration, items[i].Duration); + skeletonData.animations.Add(new Animation(name, timelines, duration)); + } + + static Timeline ReadTimeline (ref List.Enumerator keyMapEnumerator, CurveTimeline1 timeline, float defaultValue, float scale) { + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + float value = GetFloat(keyMap, "value", defaultValue) * scale; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, value); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + return timeline; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float value2 = GetFloat(nextMap, "value", defaultValue) * scale; + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, value, value2, scale); + } + time = time2; + value = value2; + keyMap = nextMap; + } + } + + static Timeline ReadTimeline (ref List.Enumerator keyMapEnumerator, CurveTimeline2 timeline, String name1, String name2, float defaultValue, + float scale) { + + var keyMap = (Dictionary)keyMapEnumerator.Current; + float time = GetFloat(keyMap, "time", 0); + float value1 = GetFloat(keyMap, name1, defaultValue) * scale, value2 = GetFloat(keyMap, name2, defaultValue) * scale; + for (int frame = 0, bezier = 0; ; frame++) { + timeline.SetFrame(frame, time, value1, value2); + if (!keyMapEnumerator.MoveNext()) { + timeline.Shrink(bezier); + return timeline; + } + var nextMap = (Dictionary)keyMapEnumerator.Current; + float time2 = GetFloat(nextMap, "time", 0); + float nvalue1 = GetFloat(nextMap, name1, defaultValue) * scale, nvalue2 = GetFloat(nextMap, name2, defaultValue) * scale; + if (keyMap.ContainsKey("curve")) { + object curve = keyMap["curve"]; + bezier = ReadCurve(curve, timeline, bezier, frame, 0, time, time2, value1, nvalue1, scale); + bezier = ReadCurve(curve, timeline, bezier, frame, 1, time, time2, value2, nvalue2, scale); + } + time = time2; + value1 = nvalue1; + value2 = nvalue2; + keyMap = nextMap; + } + } + + static int ReadCurve (object curve, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2, + float value1, float value2, float scale) { + + string curveString = curve as string; + if (curveString != null) { + if (curveString == "stepped") timeline.SetStepped(frame); + return bezier; + } + var curveValues = (List)curve; + int i = value << 2; + float cx1 = (float)curveValues[i]; + float cy1 = (float)curveValues[i + 1] * scale; + float cx2 = (float)curveValues[i + 2]; + float cy2 = (float)curveValues[i + 3] * scale; + SetBezier(timeline, frame, value, bezier, time1, value1, cx1, cy1, cx2, cy2, time2, value2); + return bezier + 1; + } + + static void SetBezier (CurveTimeline timeline, int frame, int value, int bezier, float time1, float value1, float cx1, float cy1, + float cx2, float cy2, float time2, float value2) { + timeline.SetBezier(bezier, frame, value, time1, value1, cx1, cy1, cx2, cy2, time2, value2); + } + + static float[] GetFloatArray (Dictionary map, string name, float scale) { + var list = (List)map[name]; + var values = new float[list.Count]; + if (scale == 1) { + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (float)list[i]; + } else { + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (float)list[i] * scale; + } + return values; + } + + static int[] GetIntArray (Dictionary map, string name) { + var list = (List)map[name]; + var values = new int[list.Count]; + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (int)(float)list[i]; + return values; + } + + static float GetFloat (Dictionary map, string name, float defaultValue) { + if (!map.ContainsKey(name)) return defaultValue; + return (float)map[name]; + } + + static int GetInt (Dictionary map, string name, int defaultValue) { + if (!map.ContainsKey(name)) return defaultValue; + return (int)(float)map[name]; + } + + static int GetInt (Dictionary map, string name) { + if (!map.ContainsKey(name)) throw new ArgumentException("Named value not found: " + name); + return (int)(float)map[name]; + } + + static bool GetBoolean (Dictionary map, string name, bool defaultValue) { + if (!map.ContainsKey(name)) return defaultValue; + return (bool)map[name]; + } + + static string GetString (Dictionary map, string name, string defaultValue) { + if (!map.ContainsKey(name)) return defaultValue; + return (string)map[name]; + } + + static float ToColor (string hexString, int colorIndex, int expectedLength = 8) { + if (hexString.Length != expectedLength) + throw new ArgumentException("Color hexidecimal length must be " + expectedLength + ", recieved: " + hexString, "hexString"); + return Convert.ToInt32(hexString.Substring(colorIndex * 2, 2), 16) / (float)255; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonLoader.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonLoader.cs new file mode 100644 index 0000000..d21be16 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SkeletonLoader.cs @@ -0,0 +1,92 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Spine4_1_00 { + + /// + /// Base class for loading skeleton data from a file. + /// + /// SeeJSON and binary data in the + /// Spine Runtimes Guide. + /// + public abstract class SkeletonLoader { + protected readonly AttachmentLoader attachmentLoader; + protected float scale = 1; + protected readonly List linkedMeshes = new List(); + + /// Creates a skeleton loader that loads attachments using an with the specified atlas. + /// + public SkeletonLoader (params Atlas[] atlasArray) { + attachmentLoader = new AtlasAttachmentLoader(atlasArray); + } + + /// Creates a skeleton loader that loads attachments using the specified attachment loader. + /// See Loading skeleton data in the + /// Spine Runtimes Guide. + public SkeletonLoader (AttachmentLoader attachmentLoader) { + if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader", "attachmentLoader cannot be null."); + this.attachmentLoader = attachmentLoader; + } + + /// Scales bone positions, image sizes, and translations as they are loaded. This allows different size images to be used at + /// runtime than were used in Spine. + /// + /// See Scaling in the Spine Runtimes Guide. + /// + public float Scale { + get { return scale; } + set { + if (scale == 0) throw new ArgumentNullException("scale", "scale cannot be 0."); + this.scale = value; + } + } + + public abstract SkeletonData ReadSkeletonData (string path); + + protected class LinkedMesh { + internal string parent, skin; + internal int slotIndex; + internal MeshAttachment mesh; + internal bool inheritTimelines; + + public LinkedMesh (MeshAttachment mesh, string skin, int slotIndex, string parent, bool inheritTimelines) { + this.mesh = mesh; + this.skin = skin; + this.slotIndex = slotIndex; + this.parent = parent; + this.inheritTimelines = inheritTimelines; + } + } + + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Skin.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Skin.cs new file mode 100644 index 0000000..98cf9d1 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Skin.cs @@ -0,0 +1,204 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Spine4_1_00 { + /// Stores attachments by slot index and attachment name. + /// See SkeletonData , Skeleton , and + /// Runtime skins in the Spine Runtimes Guide. + /// + public class Skin { + internal string name; + // Difference to reference implementation: using Dictionary instead of HashSet. + // Reason is that there is no efficient way to replace or access an already added element, losing any benefits. + private Dictionary attachments = new Dictionary(SkinKeyComparer.Instance); + internal readonly ExposedList bones = new ExposedList(); + internal readonly ExposedList constraints = new ExposedList(); + + public string Name { get { return name; } } + ///Returns all attachments contained in this skin. + public ICollection Attachments { get { return attachments.Values; } } + public ExposedList Bones { get { return bones; } } + public ExposedList Constraints { get { return constraints; } } + + public Skin (string name) { + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + this.name = name; + } + + /// Adds an attachment to the skin for the specified slot index and name. + /// If the name already exists for the slot, the previous value is replaced. + public void SetAttachment (int slotIndex, string name, Attachment attachment) { + if (attachment == null) throw new ArgumentNullException("attachment", "attachment cannot be null."); + attachments[new SkinKey(slotIndex, name)] = new SkinEntry(slotIndex, name, attachment); + } + + ///Adds all attachments, bones, and constraints from the specified skin to this skin. + public void AddSkin (Skin skin) { + foreach (BoneData data in skin.bones) + if (!bones.Contains(data)) bones.Add(data); + + foreach (ConstraintData data in skin.constraints) + if (!constraints.Contains(data)) constraints.Add(data); + + foreach (var item in skin.attachments) { + SkinEntry entry = item.Value; + SetAttachment(entry.slotIndex, entry.name, entry.attachment); + } + } + + ///Adds all attachments from the specified skin to this skin. Attachments are deep copied. + public void CopySkin (Skin skin) { + foreach (BoneData data in skin.bones) + if (!bones.Contains(data)) bones.Add(data); + + foreach (ConstraintData data in skin.constraints) + if (!constraints.Contains(data)) constraints.Add(data); + + foreach (var item in skin.attachments) { + SkinEntry entry = item.Value; + if (entry.attachment is MeshAttachment) { + SetAttachment(entry.slotIndex, entry.name, + entry.attachment != null ? ((MeshAttachment)entry.attachment).NewLinkedMesh() : null); + } else + SetAttachment(entry.slotIndex, entry.name, entry.attachment != null ? entry.attachment.Copy() : null); + } + } + + /// Returns the attachment for the specified slot index and name, or null. + /// May be null. + public Attachment GetAttachment (int slotIndex, string name) { + SkinEntry entry; + bool containsKey = attachments.TryGetValue(new SkinKey(slotIndex, name), out entry); + return containsKey ? entry.attachment : null; + } + + /// Removes the attachment in the skin for the specified slot index and name, if any. + public void RemoveAttachment (int slotIndex, string name) { + attachments.Remove(new SkinKey(slotIndex, name)); + } + + /// Returns all attachments in this skin for the specified slot index. + /// The target slotIndex. To find the slot index, use and . + public void GetAttachments (int slotIndex, List attachments) { + if (slotIndex < 0) throw new ArgumentException("slotIndex must be >= 0."); + if (attachments == null) throw new ArgumentNullException("attachments", "attachments cannot be null."); + foreach (var item in this.attachments) { + SkinEntry entry = item.Value; + if (entry.slotIndex == slotIndex) attachments.Add(entry); + } + } + + ///Clears all attachments, bones, and constraints. + public void Clear () { + attachments.Clear(); + bones.Clear(); + constraints.Clear(); + } + + override public string ToString () { + return name; + } + + /// Attach all attachments from this skin if the corresponding attachment from the old skin is currently attached. + internal void AttachAll (Skeleton skeleton, Skin oldSkin) { + Slot[] slots = skeleton.slots.Items; + foreach (var item in oldSkin.attachments) { + SkinEntry entry = item.Value; + int slotIndex = entry.slotIndex; + Slot slot = slots[slotIndex]; + if (slot.Attachment == entry.attachment) { + Attachment attachment = GetAttachment(slotIndex, entry.name); + if (attachment != null) slot.Attachment = attachment; + } + } + } + + /// Stores an entry in the skin consisting of the slot index, name, and attachment. + public struct SkinEntry { + internal readonly int slotIndex; + internal readonly string name; + internal readonly Attachment attachment; + + public SkinEntry (int slotIndex, string name, Attachment attachment) { + this.slotIndex = slotIndex; + this.name = name; + this.attachment = attachment; + } + + public int SlotIndex { + get { + return slotIndex; + } + } + + /// The name the attachment is associated with, equivalent to the skin placeholder name in the Spine editor. + public String Name { + get { + return name; + } + } + + public Attachment Attachment { + get { + return attachment; + } + } + } + + private struct SkinKey { + internal readonly int slotIndex; + internal readonly string name; + internal readonly int hashCode; + + public SkinKey (int slotIndex, string name) { + if (slotIndex < 0) throw new ArgumentException("slotIndex must be >= 0."); + if (name == null) throw new ArgumentNullException("name", "name cannot be null"); + this.slotIndex = slotIndex; + this.name = name; + this.hashCode = name.GetHashCode() + slotIndex * 37; + } + } + + class SkinKeyComparer : IEqualityComparer { + internal static readonly SkinKeyComparer Instance = new SkinKeyComparer(); + + bool IEqualityComparer.Equals (SkinKey e1, SkinKey e2) { + return e1.slotIndex == e2.slotIndex && string.Equals(e1.name, e2.name, StringComparison.Ordinal); + } + + int IEqualityComparer.GetHashCode (SkinKey e) { + return e.hashCode; + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Slot.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Slot.cs new file mode 100644 index 0000000..2e57206 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Slot.cs @@ -0,0 +1,199 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + + /// + /// Stores a slot's current pose. Slots organize attachments for purposes and provide a place to store + /// state for an attachment.State cannot be stored in an attachment itself because attachments are stateless and may be shared + /// across multiple skeletons. + /// + public class Slot { + internal SlotData data; + internal Bone bone; + internal float r, g, b, a; + internal float r2, g2, b2; + internal bool hasSecondColor; + internal Attachment attachment; + internal int sequenceIndex; + internal ExposedList deform = new ExposedList(); + internal int attachmentState; + + public Slot (SlotData data, Bone bone) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + this.data = data; + this.bone = bone; + + // darkColor = data.darkColor == null ? null : new Color(); + if (data.hasSecondColor) { + r2 = g2 = b2 = 0; + } + + SetToSetupPose(); + } + + /// Copy constructor. + public Slot (Slot slot, Bone bone) { + if (slot == null) throw new ArgumentNullException("slot", "slot cannot be null."); + if (bone == null) throw new ArgumentNullException("bone", "bone cannot be null."); + data = slot.data; + this.bone = bone; + r = slot.r; + g = slot.g; + b = slot.b; + a = slot.a; + + // darkColor = slot.darkColor == null ? null : new Color(slot.darkColor); + if (slot.hasSecondColor) { + r2 = slot.r2; + g2 = slot.g2; + b2 = slot.b2; + } else { + r2 = g2 = b2 = 0; + } + hasSecondColor = slot.hasSecondColor; + + attachment = slot.attachment; + sequenceIndex = slot.sequenceIndex; + deform.AddRange(slot.deform); + } + + /// The slot's setup pose data. + public SlotData Data { get { return data; } } + /// The bone this slot belongs to. + public Bone Bone { get { return bone; } } + /// The skeleton this slot belongs to. + public Skeleton Skeleton { get { return bone.skeleton; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float R { get { return r; } set { r = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float G { get { return g; } set { g = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float B { get { return b; } set { b = value; } } + /// The color used to tint the slot's attachment. If is set, this is used as the light color for two + /// color tinting. + public float A { get { return a; } set { a = value; } } + + public void ClampColor () { + r = MathUtils.Clamp(r, 0, 1); + g = MathUtils.Clamp(g, 0, 1); + b = MathUtils.Clamp(b, 0, 1); + a = MathUtils.Clamp(a, 0, 1); + } + + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float R2 { get { return r2; } set { r2 = value; } } + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float G2 { get { return g2; } set { g2 = value; } } + /// The dark color used to tint the slot's attachment for two color tinting, ignored if two color tinting is not used. + /// + public float B2 { get { return b2; } set { b2 = value; } } + /// Whether R2 G2 B2 are used to tint the slot's attachment for two color tinting. False if two color tinting is not used. + public bool HasSecondColor { get { return data.hasSecondColor; } set { data.hasSecondColor = value; } } + + public void ClampSecondColor () { + r2 = MathUtils.Clamp(r2, 0, 1); + g2 = MathUtils.Clamp(g2, 0, 1); + b2 = MathUtils.Clamp(b2, 0, 1); + } + + public Attachment Attachment { + /// The current attachment for the slot, or null if the slot has no attachment. + get { return attachment; } + /// + /// Sets the slot's attachment and, if the attachment changed, resets and clears the . + /// The deform is not cleared if the old attachment has the same as the + /// specified attachment. + /// May be null. + set { + if (attachment == value) return; + if (!(value is VertexAttachment) || !(this.attachment is VertexAttachment) + || ((VertexAttachment)value).TimelineAttachment != ((VertexAttachment)this.attachment).TimelineAttachment) { + deform.Clear(); + } + this.attachment = value; + sequenceIndex = -1; + } + } + + /// + /// The index of the texture region to display when the slot's attachment has a . -1 represents the + /// . + /// + public int SequenceIndex { get { return sequenceIndex; } set { sequenceIndex = value; } } + + /// Vertices to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a + /// weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions. + /// + /// See and . + public ExposedList Deform { + get { + return deform; + } + set { + if (deform == null) throw new ArgumentNullException("deform", "deform cannot be null."); + deform = value; + } + } + + /// Sets this slot to the setup pose. + public void SetToSetupPose () { + r = data.r; + g = data.g; + b = data.b; + a = data.a; + + // if (darkColor != null) darkColor.set(data.darkColor); + if (HasSecondColor) { + r2 = data.r2; + g2 = data.g2; + b2 = data.b2; + } + + if (data.attachmentName == null) + Attachment = null; + else { + attachment = null; + Attachment = bone.skeleton.GetAttachment(data.index, data.attachmentName); + } + } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SlotData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SlotData.cs new file mode 100644 index 0000000..d23a1d2 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SlotData.cs @@ -0,0 +1,77 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + public class SlotData { + internal int index; + internal string name; + internal BoneData boneData; + internal float r = 1, g = 1, b = 1, a = 1; + internal float r2 = 0, g2 = 0, b2 = 0; + internal bool hasSecondColor = false; + internal string attachmentName; + internal BlendMode blendMode; + + /// The index of the slot in . + public int Index { get { return index; } } + /// The name of the slot, which is unique across all slots in the skeleton. + public string Name { get { return name; } } + /// The bone this slot belongs to. + public BoneData BoneData { get { return boneData; } } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public float R2 { get { return r2; } set { r2 = value; } } + public float G2 { get { return g2; } set { g2 = value; } } + public float B2 { get { return b2; } set { b2 = value; } } + public bool HasSecondColor { get { return hasSecondColor; } set { hasSecondColor = value; } } + + /// The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. + public String AttachmentName { get { return attachmentName; } set { attachmentName = value; } } + /// The blend mode for drawing the slot's attachment. + public BlendMode BlendMode { get { return blendMode; } set { blendMode = value; } } + + public SlotData (int index, String name, BoneData boneData) { + if (index < 0) throw new ArgumentException("index must be >= 0.", "index"); + if (name == null) throw new ArgumentNullException("name", "name cannot be null."); + if (boneData == null) throw new ArgumentNullException("boneData", "boneData cannot be null."); + this.index = index; + this.name = name; + this.boneData = boneData; + } + + override public string ToString () { + return name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SpringConstraint.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SpringConstraint.cs new file mode 100644 index 0000000..bf2e5cf --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SpringConstraint.cs @@ -0,0 +1,105 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// + /// Stores the current pose for a spring constraint. A spring constraint applies physics to bones. + /// + /// See Spring constraints in the Spine User Guide. + /// + public class SpringConstraint : IUpdatable { + internal readonly SpringConstraintData data; + internal readonly ExposedList bones; + // BOZO! - stiffness -> strength. stiffness, damping, rope, stretch -> move to spring. + internal float mix, friction, gravity, wind, stiffness, damping; + internal bool rope, stretch; + + internal bool active; + + public SpringConstraint (SpringConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + mix = data.mix; + friction = data.friction; + gravity = data.gravity; + wind = data.wind; + stiffness = data.stiffness; + damping = data.damping; + rope = data.rope; + stretch = data.stretch; + + bones = new ExposedList(data.Bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.bones.Items[boneData.index]); + } + + /// Copy constructor. + public SpringConstraint (SpringConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint", "constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.bones.Count); + foreach (Bone bone in constraint.bones) + bones.Add(skeleton.bones.Items[bone.data.index]); + mix = constraint.mix; + friction = constraint.friction; + gravity = constraint.gravity; + wind = constraint.wind; + stiffness = constraint.stiffness; + damping = constraint.damping; + rope = constraint.rope; + stretch = constraint.stretch; + } + + /// Applies the constraint to the constrained bones. + public void Update () { + + } + + /// A percentage (0-1) that controls the mix between the constrained and unconstrained poses. + public float Mix { get { return mix; } set { mix = value; } } + public float Friction { get { return friction; } set { friction = value; } } + public float Gravity { get { return gravity; } set { gravity = value; } } + public float Wind { get { return wind; } set { wind = value; } } + public float Stiffness { get { return stiffness; } set { stiffness = value; } } + public float Damping { get { return damping; } set { damping = value; } } + public bool Rope { get { return rope; } set { rope = value; } } + public bool Stretch { get { return stretch; } set { stretch = value; } } + public bool Active { get { return active; } } + /// The spring constraint's setup pose data. + public SpringConstraintData Data { get { return data; } } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SpringConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SpringConstraintData.cs new file mode 100644 index 0000000..f818db6 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/SpringConstraintData.cs @@ -0,0 +1,59 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// + /// Stores the setup pose for a . + /// + /// See Spring constraints in the Spine User Guide. + /// + public class SpringConstraintData : ConstraintData { + internal ExposedList bones = new ExposedList(); + internal float mix, friction, gravity, wind, stiffness, damping; + internal bool rope, stretch; + + public SpringConstraintData (string name) : base(name) { + } + + /// The bones that are constrained by this spring constraint. + public ExposedList Bones { get { return bones; } } + + /// A percentage (0-1) that controls the mix between the constrained and unconstrained poses. + public float Mix { get { return mix; } set { mix = value; } } + public float Friction { get { return friction; } set { friction = value; } } + public float Gravity { get { return gravity; } set { gravity = value; } } + public float Wind { get { return wind; } set { wind = value; } } + public float Stiffness { get { return stiffness; } set { stiffness = value; } } + public float Damping { get { return damping; } set { damping = value; } } + public bool Rope { get { return rope; } set { rope = value; } } + public bool Stretch { get { return stretch; } set { stretch = value; } } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TextureRegion.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TextureRegion.cs new file mode 100644 index 0000000..5fcb65d --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TextureRegion.cs @@ -0,0 +1,49 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#if (UNITY_5 || UNITY_5_3_OR_NEWER || UNITY_WSA || UNITY_WP8 || UNITY_WP8_1) +#define IS_UNITY +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; + + +namespace Spine4_1_00 { + public class TextureRegion { + public int width, height; + public float u, v, u2, v2; + + virtual public int OriginalWidth { get { return width; } } + virtual public int OriginalHeight { get { return height; } } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TransformConstraint.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TransformConstraint.cs new file mode 100644 index 0000000..48d2be5 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TransformConstraint.cs @@ -0,0 +1,311 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + /// + /// + /// Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained + /// bones to match that of the target bone. + /// + /// See Transform constraints in the Spine User Guide. + /// + public class TransformConstraint : IUpdatable { + internal readonly TransformConstraintData data; + internal readonly ExposedList bones; + internal Bone target; + internal float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY; + + internal bool active; + + public TransformConstraint (TransformConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); + this.data = data; + mixRotate = data.mixRotate; + mixX = data.mixX; + mixY = data.mixY; + mixScaleX = data.mixScaleX; + mixScaleY = data.mixScaleY; + mixShearY = data.mixShearY; + bones = new ExposedList(); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.bones.Items[boneData.index]); + + target = skeleton.bones.Items[data.target.index]; + } + + /// Copy constructor. + public TransformConstraint (TransformConstraint constraint, Skeleton skeleton) { + if (constraint == null) throw new ArgumentNullException("constraint cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + data = constraint.data; + bones = new ExposedList(constraint.Bones.Count); + foreach (Bone bone in constraint.Bones) + bones.Add(skeleton.Bones.Items[bone.data.index]); + target = skeleton.Bones.Items[constraint.target.data.index]; + mixRotate = constraint.mixRotate; + mixX = constraint.mixX; + mixY = constraint.mixY; + mixScaleX = constraint.mixScaleX; + mixScaleY = constraint.mixScaleY; + mixShearY = constraint.mixShearY; + } + + public void Update () { + if (mixRotate == 0 && mixX == 0 && mixY == 0 && mixScaleX == 0 && mixScaleX == 0 && mixShearY == 0) return; + if (data.local) { + if (data.relative) + ApplyRelativeLocal(); + else + ApplyAbsoluteLocal(); + } else { + if (data.relative) + ApplyRelativeWorld(); + else + ApplyAbsoluteWorld(); + } + } + + void ApplyAbsoluteWorld () { + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, + mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; + bool translate = mixX != 0 || mixY != 0; + + Bone target = this.target; + float ta = target.a, tb = target.b, tc = target.c, td = target.d; + float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect; + + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + + if (mixRotate != 0) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float r = MathUtils.Atan2(tc, ta) - MathUtils.Atan2(c, a) + offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r *= mixRotate; + float cos = MathUtils.Cos(r), sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + + if (translate) { + float tx, ty; //Vector2 temp = this.temp; + target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY)); + bone.worldX += (tx - bone.worldX) * mixX; + bone.worldY += (ty - bone.worldY) * mixY; + } + + if (mixScaleX != 0) { + float s = (float)Math.Sqrt(bone.a * bone.a + bone.c * bone.c); + if (s != 0) s = (s + ((float)Math.Sqrt(ta * ta + tc * tc) - s + data.offsetScaleX) * mixScaleX) / s; + bone.a *= s; + bone.c *= s; + } + if (mixScaleY != 0) { + float s = (float)Math.Sqrt(bone.b * bone.b + bone.d * bone.d); + if (s != 0) s = (s + ((float)Math.Sqrt(tb * tb + td * td) - s + data.offsetScaleY) * mixScaleY) / s; + bone.b *= s; + bone.d *= s; + } + + if (mixShearY > 0) { + float b = bone.b, d = bone.d; + float by = MathUtils.Atan2(d, b); + float r = MathUtils.Atan2(td, tb) - MathUtils.Atan2(tc, ta) - (by - MathUtils.Atan2(bone.c, bone.a)); + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r = by + (r + offsetShearY) * mixShearY; + float s = (float)Math.Sqrt(b * b + d * d); + bone.b = MathUtils.Cos(r) * s; + bone.d = MathUtils.Sin(r) * s; + } + + bone.UpdateAppliedTransform(); + } + } + + void ApplyRelativeWorld () { + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, + mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; + bool translate = mixX != 0 || mixY != 0; + + Bone target = this.target; + float ta = target.a, tb = target.b, tc = target.c, td = target.d; + float degRadReflect = ta * td - tb * tc > 0 ? MathUtils.DegRad : -MathUtils.DegRad; + float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect; + + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + + if (mixRotate != 0) { + float a = bone.a, b = bone.b, c = bone.c, d = bone.d; + float r = MathUtils.Atan2(tc, ta) + offsetRotation; + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + r *= mixRotate; + float cos = MathUtils.Cos(r), sin = MathUtils.Sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + + if (translate) { + float tx, ty; //Vector2 temp = this.temp; + target.LocalToWorld(data.offsetX, data.offsetY, out tx, out ty); //target.localToWorld(temp.set(data.offsetX, data.offsetY)); + bone.worldX += tx * mixX; + bone.worldY += ty * mixY; + } + + if (mixScaleX != 0) { + float s = ((float)Math.Sqrt(ta * ta + tc * tc) - 1 + data.offsetScaleX) * mixScaleX + 1; + bone.a *= s; + bone.c *= s; + } + if (mixScaleY != 0) { + float s = ((float)Math.Sqrt(tb * tb + td * td) - 1 + data.offsetScaleY) * mixScaleY + 1; + bone.b *= s; + bone.d *= s; + } + + if (mixShearY > 0) { + float r = MathUtils.Atan2(td, tb) - MathUtils.Atan2(tc, ta); + if (r > MathUtils.PI) + r -= MathUtils.PI2; + else if (r < -MathUtils.PI) // + r += MathUtils.PI2; + float b = bone.b, d = bone.d; + r = MathUtils.Atan2(d, b) + (r - MathUtils.PI / 2 + offsetShearY) * mixShearY; + float s = (float)Math.Sqrt(b * b + d * d); + bone.b = MathUtils.Cos(r) * s; + bone.d = MathUtils.Sin(r) * s; + } + + bone.UpdateAppliedTransform(); + } + } + + void ApplyAbsoluteLocal () { + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, + mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; + + Bone target = this.target; + + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + + float rotation = bone.arotation; + if (mixRotate != 0) { + float r = target.arotation - rotation + data.offsetRotation; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + rotation += r * mixRotate; + } + + float x = bone.ax, y = bone.ay; + x += (target.ax - x + data.offsetX) * mixX; + y += (target.ay - y + data.offsetY) * mixY; + + float scaleX = bone.ascaleX, scaleY = bone.ascaleY; + if (mixScaleX != 0 && scaleX != 0) + scaleX = (scaleX + (target.ascaleX - scaleX + data.offsetScaleX) * mixScaleX) / scaleX; + if (mixScaleY != 0 && scaleY != 0) + scaleY = (scaleY + (target.ascaleY - scaleY + data.offsetScaleY) * mixScaleY) / scaleY; + + float shearY = bone.ashearY; + if (mixShearY != 0) { + float r = target.ashearY - shearY + data.offsetShearY; + r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360; + shearY += r * mixShearY; + } + + bone.UpdateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } + + void ApplyRelativeLocal () { + float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, + mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; + + Bone target = this.target; + + var bones = this.bones.Items; + for (int i = 0, n = this.bones.Count; i < n; i++) { + Bone bone = bones[i]; + + float rotation = bone.arotation + (target.arotation + data.offsetRotation) * mixRotate; + float x = bone.ax + (target.ax + data.offsetX) * mixX; + float y = bone.ay + (target.ay + data.offsetY) * mixY; + float scaleX = bone.ascaleX * (((target.ascaleX - 1 + data.offsetScaleX) * mixScaleX) + 1); + float scaleY = bone.ascaleY * (((target.ascaleY - 1 + data.offsetScaleY) * mixScaleY) + 1); + float shearY = bone.ashearY + (target.ashearY + data.offsetShearY) * mixShearY; + + bone.UpdateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } + + /// The bones that will be modified by this transform constraint. + public ExposedList Bones { get { return bones; } } + /// The target bone whose world transform will be copied to the constrained bones. + public Bone Target { get { return target; } set { target = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + public float MixRotate { get { return mixRotate; } set { mixRotate = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. + public float MixX { get { return mixX; } set { mixX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. + public float MixY { get { return mixY; } set { mixY = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. + public float MixScaleX { get { return mixScaleX; } set { mixScaleX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. + public float MixScaleY { get { return mixScaleY; } set { mixScaleY = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. + public float MixShearY { get { return mixShearY; } set { mixShearY = value; } } + public bool Active { get { return active; } } + /// The transform constraint's setup pose data. + public TransformConstraintData Data { get { return data; } } + + override public string ToString () { + return data.name; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TransformConstraintData.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TransformConstraintData.cs new file mode 100644 index 0000000..7f697d5 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/TransformConstraintData.cs @@ -0,0 +1,68 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + public class TransformConstraintData : ConstraintData { + internal ExposedList bones = new ExposedList(); + internal BoneData target; + internal float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY; + internal float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY; + internal bool relative, local; + + public ExposedList Bones { get { return bones; } } + public BoneData Target { get { return target; } set { target = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + public float MixRotate { get { return mixRotate; } set { mixRotate = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. + public float MixX { get { return mixX; } set { mixX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. + public float MixY { get { return mixY; } set { mixY = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. + public float MixScaleX { get { return mixScaleX; } set { mixScaleX = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. + public float MixScaleY { get { return mixScaleY; } set { mixScaleY = value; } } + /// A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. + public float MixShearY { get { return mixShearY; } set { mixShearY = value; } } + + public float OffsetRotation { get { return offsetRotation; } set { offsetRotation = value; } } + public float OffsetX { get { return offsetX; } set { offsetX = value; } } + public float OffsetY { get { return offsetY; } set { offsetY = value; } } + public float OffsetScaleX { get { return offsetScaleX; } set { offsetScaleX = value; } } + public float OffsetScaleY { get { return offsetScaleY; } set { offsetScaleY = value; } } + public float OffsetShearY { get { return offsetShearY; } set { offsetShearY = value; } } + + public bool Relative { get { return relative; } set { relative = value; } } + public bool Local { get { return local; } set { local = value; } } + + public TransformConstraintData (string name) : base(name) { + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Triangulator.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Triangulator.cs new file mode 100644 index 0000000..3917bd5 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/Triangulator.cs @@ -0,0 +1,275 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; + +namespace Spine4_1_00 { + public class Triangulator { + private readonly ExposedList> convexPolygons = new ExposedList>(); + private readonly ExposedList> convexPolygonsIndices = new ExposedList>(); + + private readonly ExposedList indicesArray = new ExposedList(); + private readonly ExposedList isConcaveArray = new ExposedList(); + private readonly ExposedList triangles = new ExposedList(); + + private readonly Pool> polygonPool = new Pool>(); + private readonly Pool> polygonIndicesPool = new Pool>(); + + public ExposedList Triangulate (ExposedList verticesArray) { + var vertices = verticesArray.Items; + int vertexCount = verticesArray.Count >> 1; + + var indicesArray = this.indicesArray; + indicesArray.Clear(); + int[] indices = indicesArray.Resize(vertexCount).Items; + for (int i = 0; i < vertexCount; i++) + indices[i] = i; + + var isConcaveArray = this.isConcaveArray; + bool[] isConcave = isConcaveArray.Resize(vertexCount).Items; + for (int i = 0, n = vertexCount; i < n; ++i) + isConcave[i] = IsConcave(i, vertexCount, vertices, indices); + + var triangles = this.triangles; + triangles.Clear(); + triangles.EnsureCapacity(Math.Max(0, vertexCount - 2) << 2); + + while (vertexCount > 3) { + // Find ear tip. + int previous = vertexCount - 1, i = 0, next = 1; + + // outer: + while (true) { + if (!isConcave[i]) { + int p1 = indices[previous] << 1, p2 = indices[i] << 1, p3 = indices[next] << 1; + float p1x = vertices[p1], p1y = vertices[p1 + 1]; + float p2x = vertices[p2], p2y = vertices[p2 + 1]; + float p3x = vertices[p3], p3y = vertices[p3 + 1]; + for (int ii = (next + 1) % vertexCount; ii != previous; ii = (ii + 1) % vertexCount) { + if (!isConcave[ii]) continue; + int v = indices[ii] << 1; + float vx = vertices[v], vy = vertices[v + 1]; + if (PositiveArea(p3x, p3y, p1x, p1y, vx, vy)) { + if (PositiveArea(p1x, p1y, p2x, p2y, vx, vy)) { + if (PositiveArea(p2x, p2y, p3x, p3y, vx, vy)) goto break_outer; // break outer; + } + } + } + break; + } + break_outer: + + if (next == 0) { + do { + if (!isConcave[i]) break; + i--; + } while (i > 0); + break; + } + + previous = i; + i = next; + next = (next + 1) % vertexCount; + } + + // Cut ear tip. + triangles.Add(indices[(vertexCount + i - 1) % vertexCount]); + triangles.Add(indices[i]); + triangles.Add(indices[(i + 1) % vertexCount]); + indicesArray.RemoveAt(i); + isConcaveArray.RemoveAt(i); + vertexCount--; + + int previousIndex = (vertexCount + i - 1) % vertexCount; + int nextIndex = i == vertexCount ? 0 : i; + isConcave[previousIndex] = IsConcave(previousIndex, vertexCount, vertices, indices); + isConcave[nextIndex] = IsConcave(nextIndex, vertexCount, vertices, indices); + } + + if (vertexCount == 3) { + triangles.Add(indices[2]); + triangles.Add(indices[0]); + triangles.Add(indices[1]); + } + + return triangles; + } + + public ExposedList> Decompose (ExposedList verticesArray, ExposedList triangles) { + var vertices = verticesArray.Items; + var convexPolygons = this.convexPolygons; + for (int i = 0, n = convexPolygons.Count; i < n; i++) + polygonPool.Free(convexPolygons.Items[i]); + convexPolygons.Clear(); + + var convexPolygonsIndices = this.convexPolygonsIndices; + for (int i = 0, n = convexPolygonsIndices.Count; i < n; i++) + polygonIndicesPool.Free(convexPolygonsIndices.Items[i]); + convexPolygonsIndices.Clear(); + + var polygonIndices = polygonIndicesPool.Obtain(); + polygonIndices.Clear(); + + var polygon = polygonPool.Obtain(); + polygon.Clear(); + + // Merge subsequent triangles if they form a triangle fan. + int fanBaseIndex = -1, lastWinding = 0; + int[] trianglesItems = triangles.Items; + for (int i = 0, n = triangles.Count; i < n; i += 3) { + int t1 = trianglesItems[i] << 1, t2 = trianglesItems[i + 1] << 1, t3 = trianglesItems[i + 2] << 1; + float x1 = vertices[t1], y1 = vertices[t1 + 1]; + float x2 = vertices[t2], y2 = vertices[t2 + 1]; + float x3 = vertices[t3], y3 = vertices[t3 + 1]; + + // If the base of the last triangle is the same as this triangle, check if they form a convex polygon (triangle fan). + var merged = false; + if (fanBaseIndex == t1) { + int o = polygon.Count - 4; + float[] p = polygon.Items; + int winding1 = Winding(p[o], p[o + 1], p[o + 2], p[o + 3], x3, y3); + int winding2 = Winding(x3, y3, p[0], p[1], p[2], p[3]); + if (winding1 == lastWinding && winding2 == lastWinding) { + polygon.Add(x3); + polygon.Add(y3); + polygonIndices.Add(t3); + merged = true; + } + } + + // Otherwise make this triangle the new base. + if (!merged) { + if (polygon.Count > 0) { + convexPolygons.Add(polygon); + convexPolygonsIndices.Add(polygonIndices); + } else { + polygonPool.Free(polygon); + polygonIndicesPool.Free(polygonIndices); + } + polygon = polygonPool.Obtain(); + polygon.Clear(); + polygon.Add(x1); + polygon.Add(y1); + polygon.Add(x2); + polygon.Add(y2); + polygon.Add(x3); + polygon.Add(y3); + polygonIndices = polygonIndicesPool.Obtain(); + polygonIndices.Clear(); + polygonIndices.Add(t1); + polygonIndices.Add(t2); + polygonIndices.Add(t3); + lastWinding = Winding(x1, y1, x2, y2, x3, y3); + fanBaseIndex = t1; + } + } + + if (polygon.Count > 0) { + convexPolygons.Add(polygon); + convexPolygonsIndices.Add(polygonIndices); + } + + // Go through the list of polygons and try to merge the remaining triangles with the found triangle fans. + for (int i = 0, n = convexPolygons.Count; i < n; i++) { + polygonIndices = convexPolygonsIndices.Items[i]; + if (polygonIndices.Count == 0) continue; + int firstIndex = polygonIndices.Items[0]; + int lastIndex = polygonIndices.Items[polygonIndices.Count - 1]; + + polygon = convexPolygons.Items[i]; + int o = polygon.Count - 4; + float[] p = polygon.Items; + float prevPrevX = p[o], prevPrevY = p[o + 1]; + float prevX = p[o + 2], prevY = p[o + 3]; + float firstX = p[0], firstY = p[1]; + float secondX = p[2], secondY = p[3]; + int winding = Winding(prevPrevX, prevPrevY, prevX, prevY, firstX, firstY); + + for (int ii = 0; ii < n; ii++) { + if (ii == i) continue; + var otherIndices = convexPolygonsIndices.Items[ii]; + if (otherIndices.Count != 3) continue; + int otherFirstIndex = otherIndices.Items[0]; + int otherSecondIndex = otherIndices.Items[1]; + int otherLastIndex = otherIndices.Items[2]; + + var otherPoly = convexPolygons.Items[ii]; + float x3 = otherPoly.Items[otherPoly.Count - 2], y3 = otherPoly.Items[otherPoly.Count - 1]; + + if (otherFirstIndex != firstIndex || otherSecondIndex != lastIndex) continue; + int winding1 = Winding(prevPrevX, prevPrevY, prevX, prevY, x3, y3); + int winding2 = Winding(x3, y3, firstX, firstY, secondX, secondY); + if (winding1 == winding && winding2 == winding) { + otherPoly.Clear(); + otherIndices.Clear(); + polygon.Add(x3); + polygon.Add(y3); + polygonIndices.Add(otherLastIndex); + prevPrevX = prevX; + prevPrevY = prevY; + prevX = x3; + prevY = y3; + ii = 0; + } + } + } + + // Remove empty polygons that resulted from the merge step above. + for (int i = convexPolygons.Count - 1; i >= 0; i--) { + polygon = convexPolygons.Items[i]; + if (polygon.Count == 0) { + convexPolygons.RemoveAt(i); + polygonPool.Free(polygon); + polygonIndices = convexPolygonsIndices.Items[i]; + convexPolygonsIndices.RemoveAt(i); + polygonIndicesPool.Free(polygonIndices); + } + } + + return convexPolygons; + } + + static private bool IsConcave (int index, int vertexCount, float[] vertices, int[] indices) { + int previous = indices[(vertexCount + index - 1) % vertexCount] << 1; + int current = indices[index] << 1; + int next = indices[(index + 1) % vertexCount] << 1; + return !PositiveArea(vertices[previous], vertices[previous + 1], vertices[current], vertices[current + 1], vertices[next], + vertices[next + 1]); + } + + static private bool PositiveArea (float p1x, float p1y, float p2x, float p2y, float p3x, float p3y) { + return p1x * (p3y - p2y) + p2x * (p1y - p3y) + p3x * (p2y - p1y) >= 0; + } + + static private int Winding (float p1x, float p1y, float p2x, float p2y, float p3x, float p3y) { + float px = p2x - p1x, py = p2y - p1y; + return p3x * py - p3y * px + px * p1y - p1x * py >= 0 ? 1 : -1; + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/MeshBatcher.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/MeshBatcher.cs new file mode 100644 index 0000000..9a6c7f3 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/MeshBatcher.cs @@ -0,0 +1,195 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; + +namespace Spine4_1_00 { + public struct VertexPositionColorTextureColor : IVertexType { + public Vector3 Position; + public Color Color; + public Vector2 TextureCoordinate; + public Color Color2; + + public readonly static VertexDeclaration VertexDeclaration = new VertexDeclaration + ( + new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), + new VertexElement(12, VertexElementFormat.Color, VertexElementUsage.Color, 0), + new VertexElement(16, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + new VertexElement(24, VertexElementFormat.Color, VertexElementUsage.Color, 1) + ); + + VertexDeclaration IVertexType.VertexDeclaration { + get { return VertexDeclaration; } + } + } + + // #region License + // /* + // Microsoft Public License (Ms-PL) + // MonoGame - Copyright � 2009 The MonoGame Team + // + // All rights reserved. + // + // This license governs use of the accompanying software. If you use the software, you accept this license. If you do not + // accept the license, do not use the software. + // + // 1. Definitions + // The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under + // U.S. copyright law. + // + // A "contribution" is the original software, or any additions or changes to the software. + // A "contributor" is any person that distributes its contribution under this license. + // "Licensed patents" are a contributor's patent claims that read directly on its contribution. + // + // 2. Grant of Rights + // (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, + // each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + // (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, + // each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + // + // 3. Conditions and Limitations + // (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + // (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, + // your patent license from such contributor to the software ends automatically. + // (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution + // notices that are present in the software. + // (D) If you distribute any portion of the software in source code form, you may do so only under this license by including + // a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object + // code form, you may only do so under a license that complies with this license. + // (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees + // or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent + // permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular + // purpose and non-infringement. + // */ + // #endregion License + // + + /// Draws batched meshes. + public class MeshBatcher { + private readonly List items; + private readonly Queue freeItems; + private VertexPositionColorTextureColor[] vertexArray = { }; + private short[] triangles = { }; + + public MeshBatcher () { + items = new List(256); + freeItems = new Queue(256); + EnsureCapacity(256, 512); + } + + /// Returns a pooled MeshItem. + public MeshItem NextItem (int vertexCount, int triangleCount) { + MeshItem item = freeItems.Count > 0 ? freeItems.Dequeue() : new MeshItem(); + if (item.vertices.Length < vertexCount) item.vertices = new VertexPositionColorTextureColor[vertexCount]; + if (item.triangles.Length < triangleCount) item.triangles = new int[triangleCount]; + item.vertexCount = vertexCount; + item.triangleCount = triangleCount; + items.Add(item); + return item; + } + + private void EnsureCapacity (int vertexCount, int triangleCount) { + if (vertexArray.Length < vertexCount) vertexArray = new VertexPositionColorTextureColor[vertexCount]; + if (triangles.Length < triangleCount) triangles = new short[triangleCount]; + } + + public void Draw (GraphicsDevice device) { + if (items.Count == 0) return; + + int itemCount = items.Count; + int vertexCount = 0, triangleCount = 0; + for (int i = 0; i < itemCount; i++) { + MeshItem item = items[i]; + vertexCount += item.vertexCount; + triangleCount += item.triangleCount; + } + EnsureCapacity(vertexCount, triangleCount); + + vertexCount = 0; + triangleCount = 0; + Texture2D lastTexture = null; + for (int i = 0; i < itemCount; i++) { + MeshItem item = items[i]; + int itemVertexCount = item.vertexCount; + + if (item.texture != lastTexture || vertexCount + itemVertexCount > short.MaxValue) { + FlushVertexArray(device, vertexCount, triangleCount); + vertexCount = 0; + triangleCount = 0; + lastTexture = item.texture; + device.Textures[0] = lastTexture; + if (item.textureLayers != null) { + for (int layer = 1; layer < item.textureLayers.Length; ++layer) + device.Textures[layer] = item.textureLayers[layer]; + } + } + + int[] itemTriangles = item.triangles; + int itemTriangleCount = item.triangleCount; + for (int ii = 0, t = triangleCount; ii < itemTriangleCount; ii++, t++) + triangles[t] = (short)(itemTriangles[ii] + vertexCount); + triangleCount += itemTriangleCount; + + Array.Copy(item.vertices, 0, vertexArray, vertexCount, itemVertexCount); + vertexCount += itemVertexCount; + } + FlushVertexArray(device, vertexCount, triangleCount); + } + + public void AfterLastDrawPass () { + int itemCount = items.Count; + for (int i = 0; i < itemCount; i++) { + var item = items[i]; + item.texture = null; + freeItems.Enqueue(item); + } + items.Clear(); + } + + private void FlushVertexArray (GraphicsDevice device, int vertexCount, int triangleCount) { + if (vertexCount == 0) return; + device.DrawUserIndexedPrimitives( + PrimitiveType.TriangleList, + vertexArray, 0, vertexCount, + triangles, 0, triangleCount / 3, + VertexPositionColorTextureColor.VertexDeclaration); + } + } + + public class MeshItem { + public Texture2D texture = null; + public Texture2D[] textureLayers = null; + public int vertexCount, triangleCount; + public VertexPositionColorTextureColor[] vertices = { }; + public int[] triangles = { }; + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/ShapeRenderer.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/ShapeRenderer.cs new file mode 100644 index 0000000..c0aa52c --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/ShapeRenderer.cs @@ -0,0 +1,165 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Spine4_1_00 { + /// + /// Batch drawing of lines and shapes that can be derived from lines. + /// + /// Call drawing methods in between Begin()/End() + /// + public class ShapeRenderer { + GraphicsDevice device; + List vertices = new List(); + Color color = Color.White; + BasicEffect effect; + public BasicEffect Effect { get { return effect; } set { effect = value; } } + + public ShapeRenderer (GraphicsDevice device) { + this.device = device; + this.effect = new BasicEffect(device); + effect.World = Matrix.Identity; + effect.View = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 1.0f), Vector3.Zero, Vector3.Up); + effect.TextureEnabled = false; + effect.VertexColorEnabled = true; + } + + public void SetColor (Color color) { + this.color = color; + } + + public void Begin () { + device.RasterizerState = new RasterizerState(); + device.BlendState = BlendState.AlphaBlend; + } + + public void Line (float x1, float y1, float x2, float y2) { + vertices.Add(new VertexPositionColor(new Vector3(x1, y1, 0), color)); + vertices.Add(new VertexPositionColor(new Vector3(x2, y2, 0), color)); + } + + /** Calls {@link #circle(float, float, float, int)} by estimating the number of segments needed for a smooth circle. */ + public void Circle (float x, float y, float radius) { + Circle(x, y, radius, Math.Max(1, (int)(6 * (float)Math.Pow(radius, 1.0f / 3.0f)))); + } + + /** Draws a circle using {@link ShapeType#Line} or {@link ShapeType#Filled}. */ + public void Circle (float x, float y, float radius, int segments) { + if (segments <= 0) throw new ArgumentException("segments must be > 0."); + float angle = 2 * MathUtils.PI / segments; + float cos = MathUtils.Cos(angle); + float sin = MathUtils.Sin(angle); + float cx = radius, cy = 0; + float temp = 0; + + for (int i = 0; i < segments; i++) { + vertices.Add(new VertexPositionColor(new Vector3(x + cx, y + cy, 0), color)); + temp = cx; + cx = cos * cx - sin * cy; + cy = sin * temp + cos * cy; + vertices.Add(new VertexPositionColor(new Vector3(x + cx, y + cy, 0), color)); + } + vertices.Add(new VertexPositionColor(new Vector3(x + cx, y + cy, 0), color)); + + temp = cx; + cx = radius; + cy = 0; + vertices.Add(new VertexPositionColor(new Vector3(x + cx, y + cy, 0), color)); + } + + public void Triangle (float x1, float y1, float x2, float y2, float x3, float y3) { + vertices.Add(new VertexPositionColor(new Vector3(x1, y1, 0), color)); + vertices.Add(new VertexPositionColor(new Vector3(x2, y2, 0), color)); + + vertices.Add(new VertexPositionColor(new Vector3(x2, y2, 0), color)); + vertices.Add(new VertexPositionColor(new Vector3(x3, y3, 0), color)); + + vertices.Add(new VertexPositionColor(new Vector3(x3, y3, 0), color)); + vertices.Add(new VertexPositionColor(new Vector3(x1, y1, 0), color)); + } + + public void X (float x, float y, float len) { + Line(x + len, y + len, x - len, y - len); + Line(x - len, y + len, x + len, y - len); + } + + public void Polygon (float[] polygonVertices, int offset, int count) { + if (count < 3) throw new ArgumentException("Polygon must contain at least 3 vertices"); + + offset <<= 1; + + var firstX = polygonVertices[offset]; + var firstY = polygonVertices[offset + 1]; + var last = offset + count; + + for (int i = offset, n = offset + count; i < n; i += 2) { + var x1 = polygonVertices[i]; + var y1 = polygonVertices[i + 1]; + + var x2 = 0f; + var y2 = 0f; + + if (i + 2 >= last) { + x2 = firstX; + y2 = firstY; + } else { + x2 = polygonVertices[i + 2]; + y2 = polygonVertices[i + 3]; + } + + Line(x1, y1, x2, y2); + } + } + + public void Rect (float x, float y, float width, float height) { + Line(x, y, x + width, y); + Line(x + width, y, x + width, y + height); + Line(x + width, y + height, x, y + height); + Line(x, y + height, x, y); + } + + public void End () { + if (vertices.Count == 0) return; + var verticesArray = vertices.ToArray(); + + foreach (EffectPass pass in effect.CurrentTechnique.Passes) { + pass.Apply(); + device.DrawUserPrimitives(PrimitiveType.LineList, verticesArray, 0, verticesArray.Length / 2); + } + + vertices.Clear(); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/SkeletonRenderer.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/SkeletonRenderer.cs new file mode 100644 index 0000000..789a677 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/SkeletonRenderer.cs @@ -0,0 +1,276 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; + +namespace Spine4_1_00 { + /// Draws region and mesh attachments. + public class SkeletonRenderer { + private const int TL = 0; + private const int TR = 1; + private const int BL = 2; + private const int BR = 3; + + SkeletonClipping clipper = new SkeletonClipping(); + GraphicsDevice device; + MeshBatcher batcher; + public MeshBatcher Batcher { get { return batcher; } } + RasterizerState rasterizerState; + float[] vertices = new float[8]; + int[] quadTriangles = { 0, 1, 2, 2, 3, 0 }; + BlendState defaultBlendState; + + Effect effect; + public Effect Effect { get { return effect; } set { effect = value; } } + public IVertexEffect VertexEffect { get; set; } + + private bool premultipliedAlpha; + public bool PremultipliedAlpha { get { return premultipliedAlpha; } set { premultipliedAlpha = value; } } + + /// Attachments are rendered back to front in the x/y plane by the SkeletonRenderer. + /// Each attachment is offset by a customizable z-spacing value on the z-axis to avoid z-fighting + /// in shaders with ZWrite enabled. Typical values lie in the range [-0.1, 0]. + private float zSpacing = 0.0f; + public float ZSpacing { get { return zSpacing; } set { zSpacing = value; } } + + /// A Z position offset added at each vertex. + private float z = 0.0f; + public float Z { get { return z; } set { z = value; } } + + public SkeletonRenderer (GraphicsDevice device) { + this.device = device; + + batcher = new MeshBatcher(); + + var basicEffect = new BasicEffect(device); + basicEffect.World = Matrix.Identity; + basicEffect.View = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 1.0f), Vector3.Zero, Vector3.Up); + basicEffect.TextureEnabled = true; + basicEffect.VertexColorEnabled = true; + effect = basicEffect; + + rasterizerState = new RasterizerState(); + rasterizerState.CullMode = CullMode.None; + + Bone.yDown = true; + } + + public void Begin () { + defaultBlendState = premultipliedAlpha ? BlendState.AlphaBlend : BlendState.NonPremultiplied; + + device.RasterizerState = rasterizerState; + device.BlendState = defaultBlendState; + } + + public void End () { + foreach (EffectPass pass in effect.CurrentTechnique.Passes) { + pass.Apply(); + batcher.Draw(device); + } + batcher.AfterLastDrawPass(); + } + + public void Draw (Skeleton skeleton) { + var drawOrder = skeleton.DrawOrder; + var drawOrderItems = skeleton.DrawOrder.Items; + float skeletonR = skeleton.R, skeletonG = skeleton.G, skeletonB = skeleton.B, skeletonA = skeleton.A; + Color color = new Color(); + + if (VertexEffect != null) VertexEffect.Begin(skeleton); + + for (int i = 0, n = drawOrder.Count; i < n; i++) { + Slot slot = drawOrderItems[i]; + Attachment attachment = slot.Attachment; + float attachmentZOffset = z + zSpacing * i; + + float attachmentColorR, attachmentColorG, attachmentColorB, attachmentColorA; + object textureObject = null; + int verticesCount = 0; + float[] vertices = this.vertices; + int indicesCount = 0; + int[] indices = null; + float[] uvs = null; + + if (attachment is RegionAttachment) { + RegionAttachment regionAttachment = (RegionAttachment)attachment; + attachmentColorR = regionAttachment.R; attachmentColorG = regionAttachment.G; attachmentColorB = regionAttachment.B; attachmentColorA = regionAttachment.A; + AtlasRegion region = (AtlasRegion)regionAttachment.Region; + textureObject = region.page.rendererObject; + verticesCount = 4; + regionAttachment.ComputeWorldVertices(slot, vertices, 0, 2); + indicesCount = 6; + indices = quadTriangles; + uvs = regionAttachment.UVs; + } else if (attachment is MeshAttachment) { + MeshAttachment mesh = (MeshAttachment)attachment; + attachmentColorR = mesh.R; attachmentColorG = mesh.G; attachmentColorB = mesh.B; attachmentColorA = mesh.A; + AtlasRegion region = (AtlasRegion)mesh.Region; + textureObject = region.page.rendererObject; + int vertexCount = mesh.WorldVerticesLength; + if (vertices.Length < vertexCount) vertices = new float[vertexCount]; + verticesCount = vertexCount >> 1; + mesh.ComputeWorldVertices(slot, vertices); + indicesCount = mesh.Triangles.Length; + indices = mesh.Triangles; + uvs = mesh.UVs; + } else if (attachment is ClippingAttachment) { + ClippingAttachment clip = (ClippingAttachment)attachment; + clipper.ClipStart(slot, clip); + continue; + } else { + continue; + } + + // set blend state + BlendState blendState = new BlendState(); + Blend blendSrc; + Blend blendDst; + if (premultipliedAlpha) + { + blendState.AlphaBlendFunction = BlendState.AlphaBlend.AlphaBlendFunction; + blendState.BlendFactor = BlendState.AlphaBlend.BlendFactor; + blendState.ColorBlendFunction = BlendState.AlphaBlend.ColorBlendFunction; + blendState.ColorWriteChannels = BlendState.AlphaBlend.ColorWriteChannels; + blendState.ColorWriteChannels1 = BlendState.AlphaBlend.ColorWriteChannels1; + blendState.ColorWriteChannels2 = BlendState.AlphaBlend.ColorWriteChannels2; + blendState.ColorWriteChannels3 = BlendState.AlphaBlend.ColorWriteChannels3; + blendState.MultiSampleMask = BlendState.AlphaBlend.MultiSampleMask; + } + else + { + blendState.AlphaBlendFunction = BlendState.NonPremultiplied.AlphaBlendFunction; + blendState.BlendFactor = BlendState.NonPremultiplied.BlendFactor; + blendState.ColorBlendFunction = BlendState.NonPremultiplied.ColorBlendFunction; + blendState.ColorWriteChannels = BlendState.NonPremultiplied.ColorWriteChannels; + blendState.ColorWriteChannels1 = BlendState.NonPremultiplied.ColorWriteChannels1; + blendState.ColorWriteChannels2 = BlendState.NonPremultiplied.ColorWriteChannels2; + blendState.ColorWriteChannels3 = BlendState.NonPremultiplied.ColorWriteChannels3; + blendState.MultiSampleMask = BlendState.NonPremultiplied.MultiSampleMask; + } + switch (slot.Data.BlendMode) + { + case BlendMode.Additive: + blendState = BlendState.Additive; + break; + case BlendMode.Multiply: + blendSrc = BlendXna.GetXNABlend(BlendXna.GL_DST_COLOR); + blendDst = BlendXna.GetXNABlend(BlendXna.GL_ONE_MINUS_SRC_ALPHA); + blendState.ColorSourceBlend = blendSrc; + blendState.AlphaSourceBlend = blendSrc; + blendState.ColorDestinationBlend = blendDst; + blendState.AlphaDestinationBlend = blendDst; + break; + case BlendMode.Screen: + blendSrc = BlendXna.GetXNABlend(premultipliedAlpha ? BlendXna.GL_ONE : BlendXna.GL_SRC_ALPHA); + blendDst = BlendXna.GetXNABlend(BlendXna.GL_ONE_MINUS_SRC_COLOR); + blendState.ColorSourceBlend = blendSrc; + blendState.AlphaSourceBlend = blendSrc; + blendState.ColorDestinationBlend = blendDst; + blendState.AlphaDestinationBlend = blendDst; + break; + default: + blendState = defaultBlendState; + break; + } + + if (device.BlendState != blendState) { + End(); + device.BlendState = blendState; + } + + // calculate color + float a = skeletonA * slot.A * attachmentColorA; + if (premultipliedAlpha) { + color = new Color( + skeletonR * slot.R * attachmentColorR * a, + skeletonG * slot.G * attachmentColorG * a, + skeletonB * slot.B * attachmentColorB * a, a); + } else { + color = new Color( + skeletonR * slot.R * attachmentColorR, + skeletonG * slot.G * attachmentColorG, + skeletonB * slot.B * attachmentColorB, a); + } + + Color darkColor = new Color(); + if (slot.HasSecondColor) { + if (premultipliedAlpha) { + darkColor = new Color(slot.R2 * a, slot.G2 * a, slot.B2 * a); + } else { + darkColor = new Color(slot.R2 * a, slot.G2 * a, slot.B2 * a); + } + } + darkColor.A = premultipliedAlpha ? (byte)255 : (byte)0; + + // clip + if (clipper.IsClipping) { + clipper.ClipTriangles(vertices, verticesCount << 1, indices, indicesCount, uvs); + vertices = clipper.ClippedVertices.Items; + verticesCount = clipper.ClippedVertices.Count >> 1; + indices = clipper.ClippedTriangles.Items; + indicesCount = clipper.ClippedTriangles.Count; + uvs = clipper.ClippedUVs.Items; + } + + if (verticesCount == 0 || indicesCount == 0) + continue; + + // submit to batch + MeshItem item = batcher.NextItem(verticesCount, indicesCount); + if (textureObject is Texture2D) + item.texture = (Texture2D)textureObject; + else { + item.textureLayers = (Texture2D[])textureObject; + item.texture = item.textureLayers[0]; + } + for (int ii = 0, nn = indicesCount; ii < nn; ii++) { + item.triangles[ii] = indices[ii]; + } + VertexPositionColorTextureColor[] itemVertices = item.vertices; + for (int ii = 0, v = 0, nn = verticesCount << 1; v < nn; ii++, v += 2) { + itemVertices[ii].Color = color; + itemVertices[ii].Color2 = darkColor; + itemVertices[ii].Position.X = vertices[v]; + itemVertices[ii].Position.Y = vertices[v + 1]; + itemVertices[ii].Position.Z = attachmentZOffset; + itemVertices[ii].TextureCoordinate.X = uvs[v]; + itemVertices[ii].TextureCoordinate.Y = uvs[v + 1]; + if (VertexEffect != null) VertexEffect.Transform(ref itemVertices[ii]); + } + + clipper.ClipEnd(slot); + } + clipper.ClipEnd(); + if (VertexEffect != null) VertexEffect.End(); + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/VertexEffect.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/VertexEffect.cs new file mode 100644 index 0000000..edc7f58 --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/VertexEffect.cs @@ -0,0 +1,97 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Spine4_1_00 { + public interface IVertexEffect { + void Begin (Skeleton skeleton); + void Transform (ref VertexPositionColorTextureColor vertex); + void End (); + } + + public class JitterEffect : IVertexEffect { + public float JitterX { get; set; } + public float JitterY { get; set; } + + public JitterEffect (float jitterX, float jitterY) { + JitterX = jitterX; + JitterY = jitterY; + } + + public void Begin (Skeleton skeleton) { + } + + public void End () { + } + + public void Transform (ref VertexPositionColorTextureColor vertex) { + vertex.Position.X += MathUtils.RandomTriangle(-JitterX, JitterY); + vertex.Position.Y += MathUtils.RandomTriangle(-JitterX, JitterY); + } + } + + public class SwirlEffect : IVertexEffect { + private float worldX, worldY, angle; + + public float Radius { get; set; } + public float Angle { get { return angle; } set { angle = value * MathUtils.DegRad; } } + public float CenterX { get; set; } + public float CenterY { get; set; } + public IInterpolation Interpolation { get; set; } + + public SwirlEffect (float radius) { + Radius = radius; + Interpolation = IInterpolation.Pow2; + } + + public void Begin (Skeleton skeleton) { + worldX = skeleton.X + CenterX; + worldY = skeleton.Y + CenterY; + } + + public void End () { + } + + public void Transform (ref VertexPositionColorTextureColor vertex) { + float x = vertex.Position.X - worldX; + float y = vertex.Position.Y - worldY; + float dist = (float)Math.Sqrt(x * x + y * y); + if (dist < Radius) { + float theta = Interpolation.Apply(0, angle, (Radius - dist) / Radius); + float cos = MathUtils.Cos(theta), sin = MathUtils.Sin(theta); + vertex.Position.X = cos * x - sin * y + worldX; + vertex.Position.Y = sin * x + cos * y + worldY; + } + } + } +} diff --git a/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/XnaTextureLoader.cs b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/XnaTextureLoader.cs new file mode 100644 index 0000000..a06878e --- /dev/null +++ b/SpineViewerWPF/SpineLibrary/spine-runtimes-4.1.00/XnaLoader/XnaTextureLoader.cs @@ -0,0 +1,110 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated September 24, 2021. Replaces all prior versions. + * + * Copyright (c) 2013-2021, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.IO; + +namespace Spine4_1_00 { + public class XnaTextureLoader : TextureLoader { + GraphicsDevice device; + string[] textureLayerSuffixes = null; + + /// + /// Constructor. + /// + /// The graphics device to be used. + /// If true multiple textures layers + /// (e.g. a diffuse/albedo texture and a normal map) are loaded instead of a single texture. + /// Names are constructed based on suffixes added according to the textureSuffixes parameter. + /// If loadMultipleTextureLayers is true, the strings of this array + /// define the path name suffix of each layer to be loaded. Array size must be equal to the number of layers to be loaded. + /// The first array entry is the suffix to be replaced (e.g. "_albedo", or "" for a first layer without a suffix), + /// subsequent array entries contain the suffix to replace the first entry with (e.g. "_normals"). + /// + /// An example would be: + /// new string[] { "", "_normals" } for loading a base diffuse texture named "skeletonname.png" and + /// a normalmap named "skeletonname_normals.png". + public XnaTextureLoader (GraphicsDevice device, bool loadMultipleTextureLayers = false, string[] textureSuffixes = null) { + this.device = device; + if (loadMultipleTextureLayers) + this.textureLayerSuffixes = textureSuffixes; + } + + public void Load (AtlasPage page, String path) { + Texture2D texture = Util.LoadTexture(device, path); + + if (page.width == 0 || page.height == 0) + { + page.width = texture.Width; + page.height = texture.Height; + } + if (textureLayerSuffixes == null) { + page.rendererObject = texture; + } else { + Texture2D[] textureLayersArray = new Texture2D[textureLayerSuffixes.Length]; + textureLayersArray[0] = texture; + for (int layer = 1; layer < textureLayersArray.Length; ++layer) { + string layerPath = GetLayerName(path, textureLayerSuffixes[0], textureLayerSuffixes[layer]); + textureLayersArray[layer] = Util.LoadTexture(device, layerPath); + } + page.rendererObject = textureLayersArray; + } + } + + public void Unload(Object texture) + { + string type = texture.GetType().FullName; + if (type == "Microsoft.Xna.Framework.Graphics.Texture2D[]") + { + foreach (Texture2D texture2d in (Texture2D[])texture) + { + texture2d.Dispose(); + } + + } + else + { + ((Texture2D)texture).Dispose(); + } + + } + + private string GetLayerName (string firstLayerPath, string firstLayerSuffix, string replacementSuffix) { + + int suffixLocation = firstLayerPath.LastIndexOf(firstLayerSuffix + "."); + if (suffixLocation == -1) { + throw new Exception(string.Concat("Error composing texture layer name: first texture layer name '", firstLayerPath, + "' does not contain suffix to be replaced: '", firstLayerSuffix, "'")); + } + return firstLayerPath.Remove(suffixLocation, firstLayerSuffix.Length).Insert(suffixLocation, replacementSuffix); + } + } +} diff --git a/SpineViewerWPF/SpineViewerWPF.csproj b/SpineViewerWPF/SpineViewerWPF.csproj index 3edfb07..ad3317b 100644 --- a/SpineViewerWPF/SpineViewerWPF.csproj +++ b/SpineViewerWPF/SpineViewerWPF.csproj @@ -126,6 +126,8 @@ + + @@ -627,6 +629,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UCPlayer.xaml diff --git a/SpineViewerWPF/Views/UCPlayer.xaml.cs b/SpineViewerWPF/Views/UCPlayer.xaml.cs index 3817017..be2cba4 100644 --- a/SpineViewerWPF/Views/UCPlayer.xaml.cs +++ b/SpineViewerWPF/Views/UCPlayer.xaml.cs @@ -1,188 +1,205 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; - -namespace SpineViewerWPF.Views -{ - /// - /// Player.xaml 的互動邏輯 - /// - public partial class UCPlayer : UserControl - { - public IPlayer player; - - - public UCPlayer() - { - InitializeComponent(); - App.appXC = new WpfXnaControl.XnaControl(); - - switch (App.globalValues.SelectSpineVersion) - { - case "2.1.08": - player = new Player_2_1_08(); - break; - case "2.1.25": - player = new Player_2_1_25(); - break; - case "3.1.07": - player = new Player_3_1_07(); - break; - case "3.2.xx": - player = new Player_3_2_xx(); - break; - case "3.4.02": - player = new Player_3_4_02(); - break; - case "3.5.51": - player = new Player_3_5_51(); - break; - case "3.6.32": - player = new Player_3_6_32(); - break; - case "3.6.39": - player = new Player_3_6_39(); - break; - case "3.6.53": - player = new Player_3_6_53(); - break; - case "3.7.94": - player = new Player_3_7_94(); - break; - case "3.8.95": - player = new Player_3_8_95(); +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace SpineViewerWPF.Views +{ + /// + /// Player.xaml 的互動邏輯 + /// + public partial class UCPlayer : UserControl + { + public IPlayer player; + + + public UCPlayer() + { + InitializeComponent(); + + if(App.appXC == null) + { + App.appXC = new WpfXnaControl.XnaControl(); + } + + + + if(player != null) + { + player.Dispose(); + } + + switch (App.globalValues.SelectSpineVersion) + { + case "2.1.08": + player = new Player_2_1_08(); break; - case "4.0.31": - player = new Player_4_0_31(); - break; - } - - App.appXC.Initialize += player.Initialize; - App.appXC.Update += player.Update; - App.appXC.LoadContent += player.LoadContent; - App.appXC.Draw += player.Draw; - App.appXC.Width = App.globalValues.FrameWidth; - App.appXC.Height = App.globalValues.FrameHeight; - - var transformGroup = (TransformGroup)Frame.RenderTransform; - var tt = (TranslateTransform)transformGroup.Children.Where(x => x.GetType() == typeof(TranslateTransform)).FirstOrDefault(); - tt.X = (float)((App.mainWidth ) / 2 - (App.canvasWidth / 2) -10); - tt.Y = (float)((App.mainHeight ) / 2 - (App.canvasHeight / 2)-40); - - Frame.Children.Add(App.appXC); - - } - - - private void Frame_MouseDown(object sender, MouseButtonEventArgs e) - { - App.isPress = true; - App.mouseLocation = Mouse.GetPosition(this.Frame); - } - - private void Frame_MouseMove(object sender, MouseEventArgs e) - { - if (App.isPress) - { - System.Windows.Point position = Mouse.GetPosition(this.Frame); - if (App.globalValues.UseBG && App.globalValues.ControlBG) - { - Common.SetBGXY(position.X, position.Y, App.mouseLocation.X, App.mouseLocation.Y); - } - else if (Keyboard.IsKeyDown(Key.LeftAlt)) - { - Common.SetXY(position.X, position.Y, App.mouseLocation.X, App.mouseLocation.Y); - } - else if (Keyboard.IsKeyDown(Key.LeftCtrl)) - { - - var transformGroup = (TransformGroup)Frame.RenderTransform; - var tt = (TranslateTransform)transformGroup.Children.Where(x => x.GetType() == typeof(TranslateTransform)).FirstOrDefault(); - tt.X = (float)(position.X + tt.X - App.mouseLocation.X); - tt.Y = (float)(position.Y + tt.Y - App.mouseLocation.Y); - } - App.mouseLocation = Mouse.GetPosition(this.Frame); - } - } - - private void Frame_MouseUp(object sender, MouseButtonEventArgs e) - { - App.isPress = false; - } - - private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) - { - player.SizeChange(); - } - - private void Frame_MouseLeave(object sender, MouseEventArgs e) - { - App.isPress = false; - } - - public void ChangeSet() - { - player.ChangeSet(); - } - - public void Reload() - { - var transformGroupL = (TransformGroup)Frame.LayoutTransform; - var st = (ScaleTransform)transformGroupL.Children.Where(x => x.GetType() == typeof(ScaleTransform)).FirstOrDefault(); - st.ScaleX = 1; - st.ScaleY = 1; - var transformGroupR = (TransformGroup)Frame.RenderTransform; - var tt = (TranslateTransform)transformGroupR.Children.Where(x => x.GetType() == typeof(TranslateTransform)).FirstOrDefault(); - tt.X = (float)((App.mainWidth) / 2 - (App.canvasWidth / 2) - 10); - tt.Y = (float)((App.mainHeight) / 2 - (App.canvasHeight / 2) - 40); - Frame.Children.Remove(App.appXC); - App.appXC.Initialize -= player.Initialize; - App.appXC.Update -= player.Update; - App.appXC.LoadContent -= player.LoadContent; - App.appXC.Draw -= player.Draw; - - - App.appXC.Initialize += player.Initialize; - App.appXC.Update += player.Update; - App.appXC.LoadContent += player.LoadContent; - App.appXC.Draw += player.Draw; - App.appXC.Width = App.globalValues.FrameWidth; - App.appXC.Height = App.globalValues.FrameHeight; - - Frame.Children.Add(App.appXC); - player.ChangeSet(); - MainWindow.SetCBAnimeName(); - } - - private void ScrollViewer_MouseWheel(object sender, MouseWheelEventArgs e) - { - if (Keyboard.IsKeyDown(Key.LeftAlt)) - { - Player.Frame_MouseWheel(e); - } - else if (Keyboard.IsKeyDown(Key.LeftCtrl)) - { - double zoom = e.Delta > 0 ? .02 : -.02; - var transformGroup = (TransformGroup)Frame.LayoutTransform; - var st = (ScaleTransform)transformGroup.Children.Where(x => x.GetType() == typeof(ScaleTransform)).FirstOrDefault(); - App.globalValues.ViewScale += zoom; - st.ScaleX = App.globalValues.ViewScale; - st.ScaleY = App.globalValues.ViewScale; - Frame.Width = Frame.ActualWidth; - Frame.Height = Frame.ActualHeight; - - } - } - } -} + case "2.1.25": + player = new Player_2_1_25(); + break; + case "3.1.07": + player = new Player_3_1_07(); + break; + case "3.2.xx": + player = new Player_3_2_xx(); + break; + case "3.4.02": + player = new Player_3_4_02(); + break; + case "3.5.51": + player = new Player_3_5_51(); + break; + case "3.6.32": + player = new Player_3_6_32(); + break; + case "3.6.39": + player = new Player_3_6_39(); + break; + case "3.6.53": + player = new Player_3_6_53(); + break; + case "3.7.94": + player = new Player_3_7_94(); + break; + case "3.8.95": + player = new Player_3_8_95(); + break; + case "4.0.31": + player = new Player_4_0_31(); + break; + case "4.0.64": + player = new Player_4_0_64(); + break; + case "4.1.00": + player = new Player_4_1_00(); + break; + } + + App.appXC.Initialize += player.Initialize; + App.appXC.Update += player.Update; + App.appXC.LoadContent += player.LoadContent; + App.appXC.Draw += player.Draw; + App.appXC.Width = App.globalValues.FrameWidth; + App.appXC.Height = App.globalValues.FrameHeight; + + var transformGroup = (TransformGroup)Frame.RenderTransform; + var tt = (TranslateTransform)transformGroup.Children.Where(x => x.GetType() == typeof(TranslateTransform)).FirstOrDefault(); + tt.X = (float)((App.mainWidth ) / 2 - (App.canvasWidth / 2) -10); + tt.Y = (float)((App.mainHeight ) / 2 - (App.canvasHeight / 2)-40); + + Frame.Children.Add(App.appXC); + + } + + + private void Frame_MouseDown(object sender, MouseButtonEventArgs e) + { + App.isPress = true; + App.mouseLocation = Mouse.GetPosition(this.Frame); + } + + private void Frame_MouseMove(object sender, MouseEventArgs e) + { + if (App.isPress) + { + System.Windows.Point position = Mouse.GetPosition(this.Frame); + if (App.globalValues.UseBG && App.globalValues.ControlBG) + { + Common.SetBGXY(position.X, position.Y, App.mouseLocation.X, App.mouseLocation.Y); + } + else if (Keyboard.IsKeyDown(Key.LeftAlt)) + { + Common.SetXY(position.X, position.Y, App.mouseLocation.X, App.mouseLocation.Y); + } + else if (Keyboard.IsKeyDown(Key.LeftCtrl)) + { + + var transformGroup = (TransformGroup)Frame.RenderTransform; + var tt = (TranslateTransform)transformGroup.Children.Where(x => x.GetType() == typeof(TranslateTransform)).FirstOrDefault(); + tt.X = (float)(position.X + tt.X - App.mouseLocation.X); + tt.Y = (float)(position.Y + tt.Y - App.mouseLocation.Y); + } + App.mouseLocation = Mouse.GetPosition(this.Frame); + } + } + + private void Frame_MouseUp(object sender, MouseButtonEventArgs e) + { + App.isPress = false; + } + + private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) + { + player.SizeChange(); + } + + private void Frame_MouseLeave(object sender, MouseEventArgs e) + { + App.isPress = false; + } + + public void ChangeSet() + { + player.ChangeSet(); + } + + public void Reload() + { + var transformGroupL = (TransformGroup)Frame.LayoutTransform; + var st = (ScaleTransform)transformGroupL.Children.Where(x => x.GetType() == typeof(ScaleTransform)).FirstOrDefault(); + st.ScaleX = 1; + st.ScaleY = 1; + var transformGroupR = (TransformGroup)Frame.RenderTransform; + var tt = (TranslateTransform)transformGroupR.Children.Where(x => x.GetType() == typeof(TranslateTransform)).FirstOrDefault(); + tt.X = (float)((App.mainWidth) / 2 - (App.canvasWidth / 2) - 10); + tt.Y = (float)((App.mainHeight) / 2 - (App.canvasHeight / 2) - 40); + Frame.Children.Remove(App.appXC); + App.appXC.Initialize -= player.Initialize; + App.appXC.Update -= player.Update; + App.appXC.LoadContent -= player.LoadContent; + App.appXC.Draw -= player.Draw; + + + App.appXC.Initialize += player.Initialize; + App.appXC.Update += player.Update; + App.appXC.LoadContent += player.LoadContent; + App.appXC.Draw += player.Draw; + App.appXC.Width = App.globalValues.FrameWidth; + App.appXC.Height = App.globalValues.FrameHeight; + + Frame.Children.Add(App.appXC); + player.ChangeSet(); + MainWindow.SetCBAnimeName(); + } + + private void ScrollViewer_MouseWheel(object sender, MouseWheelEventArgs e) + { + if (Keyboard.IsKeyDown(Key.LeftAlt)) + { + Player.Frame_MouseWheel(e); + } + else if (Keyboard.IsKeyDown(Key.LeftCtrl)) + { + double zoom = e.Delta > 0 ? .02 : -.02; + var transformGroup = (TransformGroup)Frame.LayoutTransform; + var st = (ScaleTransform)transformGroup.Children.Where(x => x.GetType() == typeof(ScaleTransform)).FirstOrDefault(); + App.globalValues.ViewScale += zoom; + st.ScaleX = App.globalValues.ViewScale; + st.ScaleY = App.globalValues.ViewScale; + Frame.Width = Frame.ActualWidth; + Frame.Height = Frame.ActualHeight; + + } + } + } +} diff --git a/SpineViewerWPF/Windows/Open.xaml b/SpineViewerWPF/Windows/Open.xaml index 261bada..29ea1cd 100644 --- a/SpineViewerWPF/Windows/Open.xaml +++ b/SpineViewerWPF/Windows/Open.xaml @@ -26,8 +26,8 @@ - - + + @@ -43,6 +43,8 @@ + + diff --git a/SpineViewerWPF/Windows/Open.xaml.cs b/SpineViewerWPF/Windows/Open.xaml.cs index 68d62bd..8e1ba0a 100644 --- a/SpineViewerWPF/Windows/Open.xaml.cs +++ b/SpineViewerWPF/Windows/Open.xaml.cs @@ -152,5 +152,55 @@ private void btn_Open_Click(object sender, RoutedEventArgs e) this.Close(); } + + private void TextBox_PreviewDragOver(object sender, DragEventArgs e) + { + e.Effects = DragDropEffects.Copy; + e.Handled = true; + } + + private void TextBox_PreviewDrop(object sender, DragEventArgs e) + { + object text = e.Data.GetData(DataFormats.FileDrop); + TextBox tb = sender as TextBox; + if (tb != null) + { + if(tb.Name == "tb_Atlas_File") + { + if(((string[])text)[0].IndexOf(".atlas") != -1) + { + tb_Atlas_File.Text = ((string[])text)[0]; + App.globalValues.SelectAtlasFile = tb_Atlas_File.Text; + if (!Common.CheckSpineFile(App.globalValues.SelectAtlasFile)) + { + MessageBox.Show("Can not found Spine Json or Binary file!"); + + bool isSelectSp = SelectFile("Spine Json File (*.json)|*.json|Spine Binary File (*.skel)|*.skel", tb_JS_file); + if (isSelectSp) + { + App.globalValues.SelectSpineFile = tb_JS_file.Text; + } + } + else + { + tb_JS_file.Text = App.globalValues.SelectSpineFile; + } + } + } + else if (tb.Name == "tb_JS_file") + { + if (((string[])text)[0].IndexOf(".json") > 0 || ((string[])text)[0].IndexOf(".skel") > 0) + { + App.globalValues.SelectSpineFile = ((string[])text)[0]; + tb_JS_file.Text = App.globalValues.SelectSpineFile; + } + } + + + + } + } + + } }