diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..0656c35
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+[*.cs]
+
+dotnet_diagnostic.CS8600.severity = suggestion
+dotnet_diagnostic.CS8601.severity = suggestion
+dotnet_diagnostic.CS8602.severity = suggestion
+dotnet_diagnostic.CS8603.severity = suggestion
+dotnet_diagnostic.CS8604.severity = suggestion
+dotnet_diagnostic.CS8605.severity = suggestion
+dotnet_diagnostic.CS8618.severity = suggestion
+dotnet_diagnostic.CS8625.severity = suggestion
+dotnet_diagnostic.CS8714.severity = suggestion
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8a30d25
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,398 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..89905f7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+# SpineViewer
+
+[中文](README.md) | [English](README.en.md)
+
+---
+
+## 简介
+
+## 安装
+
+---
+
+*如果你觉得这个项目不错请给个 :star:, 并分享给更多人知道! :)*
diff --git a/SpineViewer.sln b/SpineViewer.sln
new file mode 100644
index 0000000..477fce0
--- /dev/null
+++ b/SpineViewer.sln
@@ -0,0 +1,64 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35208.52
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpineViewer", "SpineViewer\SpineViewer.csproj", "{ECD11621-9F72-4BCB-92A4-7D8A426F84FA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SpineRuntimes", "SpineRuntimes", "{EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpineRuntime36", "SpineRuntimes\SpineRuntime36\SpineRuntime36.csproj", "{CA964DA9-3649-44BC-84F7-B1108A652905}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpineRuntime38", "SpineRuntimes\SpineRuntime38\SpineRuntime38.csproj", "{ECF7297E-031B-4E37-8033-7C2345DB8766}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{65DD4332-305A-4AAA-AD92-A7D293296BC9}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {ECD11621-9F72-4BCB-92A4-7D8A426F84FA}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {ECD11621-9F72-4BCB-92A4-7D8A426F84FA}.Debug|Any CPU.Build.0 = Debug|x64
+ {ECD11621-9F72-4BCB-92A4-7D8A426F84FA}.Debug|x64.ActiveCfg = Debug|x64
+ {ECD11621-9F72-4BCB-92A4-7D8A426F84FA}.Debug|x64.Build.0 = Debug|x64
+ {ECD11621-9F72-4BCB-92A4-7D8A426F84FA}.Release|Any CPU.ActiveCfg = Release|x64
+ {ECD11621-9F72-4BCB-92A4-7D8A426F84FA}.Release|Any CPU.Build.0 = Release|x64
+ {ECD11621-9F72-4BCB-92A4-7D8A426F84FA}.Release|x64.ActiveCfg = Release|x64
+ {ECD11621-9F72-4BCB-92A4-7D8A426F84FA}.Release|x64.Build.0 = Release|x64
+ {CA964DA9-3649-44BC-84F7-B1108A652905}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {CA964DA9-3649-44BC-84F7-B1108A652905}.Debug|Any CPU.Build.0 = Debug|x64
+ {CA964DA9-3649-44BC-84F7-B1108A652905}.Debug|x64.ActiveCfg = Debug|x64
+ {CA964DA9-3649-44BC-84F7-B1108A652905}.Debug|x64.Build.0 = Debug|x64
+ {CA964DA9-3649-44BC-84F7-B1108A652905}.Release|Any CPU.ActiveCfg = Release|x64
+ {CA964DA9-3649-44BC-84F7-B1108A652905}.Release|Any CPU.Build.0 = Release|x64
+ {CA964DA9-3649-44BC-84F7-B1108A652905}.Release|x64.ActiveCfg = Release|x64
+ {CA964DA9-3649-44BC-84F7-B1108A652905}.Release|x64.Build.0 = Release|x64
+ {ECF7297E-031B-4E37-8033-7C2345DB8766}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {ECF7297E-031B-4E37-8033-7C2345DB8766}.Debug|Any CPU.Build.0 = Debug|x64
+ {ECF7297E-031B-4E37-8033-7C2345DB8766}.Debug|x64.ActiveCfg = Debug|x64
+ {ECF7297E-031B-4E37-8033-7C2345DB8766}.Debug|x64.Build.0 = Debug|x64
+ {ECF7297E-031B-4E37-8033-7C2345DB8766}.Release|Any CPU.ActiveCfg = Release|x64
+ {ECF7297E-031B-4E37-8033-7C2345DB8766}.Release|Any CPU.Build.0 = Release|x64
+ {ECF7297E-031B-4E37-8033-7C2345DB8766}.Release|x64.ActiveCfg = Release|x64
+ {ECF7297E-031B-4E37-8033-7C2345DB8766}.Release|x64.Build.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {CA964DA9-3649-44BC-84F7-B1108A652905} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8}
+ {ECF7297E-031B-4E37-8033-7C2345DB8766} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {91F0EFD1-4B07-4C3C-82D8-90432349D3A5}
+ EndGlobalSection
+EndGlobal
diff --git a/SpineViewer/Properties/Resources.Designer.cs b/SpineViewer/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..5bccaeb
--- /dev/null
+++ b/SpineViewer/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// 此代码由工具生成。
+// 运行时版本:4.0.30319.42000
+//
+// 对此文件的更改可能会导致不正确的行为,并且如果
+// 重新生成代码,这些更改将会丢失。
+//
+//------------------------------------------------------------------------------
+
+namespace SpineViewer.Properties {
+ using System;
+
+
+ ///
+ /// 一个强类型的资源类,用于查找本地化的字符串等。
+ ///
+ // 此类是由 StronglyTypedResourceBuilder
+ // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
+ // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
+ // (以 /str 作为命令选项),或重新生成 VS 项目。
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// 返回此类使用的缓存的 ResourceManager 实例。
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SpineViewer.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// 重写当前线程的 CurrentUICulture 属性,对
+ /// 使用此强类型资源类的所有资源查找执行重写。
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/SpineViewer/Properties/Resources.resx b/SpineViewer/Properties/Resources.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/SpineViewer/Properties/Resources.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj
new file mode 100644
index 0000000..28fede2
--- /dev/null
+++ b/SpineViewer/SpineViewer.csproj
@@ -0,0 +1,42 @@
+
+
+
+ WinExe
+ net8.0-windows
+ enable
+ true
+ enable
+ x64
+ 0.1.0
+ $(SolutionDir)out
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
\ No newline at end of file
diff --git a/SpineViewer/src/MainForm.Designer.cs b/SpineViewer/src/MainForm.Designer.cs
new file mode 100644
index 0000000..e27dd65
--- /dev/null
+++ b/SpineViewer/src/MainForm.Designer.cs
@@ -0,0 +1,572 @@
+namespace SpineViewer
+{
+ partial class MainForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ ListViewItem listViewItem1 = new ListViewItem(new string[] { "A Loooooooooog Name1", "A Loooooooooog Version" }, -1);
+ menuStrip = new MenuStrip();
+ toolStripMenuItem_File = new ToolStripMenuItem();
+ toolStripMenuItem_Open = new ToolStripMenuItem();
+ toolStripMenuItem_BatchOpen = new ToolStripMenuItem();
+ toolStripSeparator1 = new ToolStripSeparator();
+ toolStripMenuItem_Export = new ToolStripMenuItem();
+ toolStripSeparator2 = new ToolStripSeparator();
+ toolStripMenuItem_Exit = new ToolStripMenuItem();
+ toolStripMenuItem_Help = new ToolStripMenuItem();
+ toolStripMenuItem_About = new ToolStripMenuItem();
+ rtbLog = new RichTextBox();
+ splitContainer_MainForm = new SplitContainer();
+ splitContainer_Functional = new SplitContainer();
+ splitContainer_Information = new SplitContainer();
+ groupBox_SkelList = new GroupBox();
+ tableLayoutPanel = new TableLayoutPanel();
+ flowLayoutPanel_Buttons = new FlowLayoutPanel();
+ button_Add = new Button();
+ button_Insert = new Button();
+ button_Remove = new Button();
+ button_MoveUp = new Button();
+ button_MoveDown = new Button();
+ listView_SkelList = new ListView();
+ columnHeader_Name = new ColumnHeader();
+ columnHeader_Version = new ColumnHeader();
+ splitContainer_Config = new SplitContainer();
+ groupBox_SkelConfig = new GroupBox();
+ propertyGrid_Skel = new PropertyGrid();
+ groupBox_PreviewConfig = new GroupBox();
+ groupBox_Preview = new GroupBox();
+ panel_PreviewContainer = new Panel();
+ panel_Preview = new Panel();
+ panel_MainForm = new Panel();
+ openFileDialog_Skel = new OpenFileDialog();
+ openFileDialog_Atlas = new OpenFileDialog();
+ toolTip1 = new ToolTip(components);
+ menuStrip.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer_MainForm).BeginInit();
+ splitContainer_MainForm.Panel1.SuspendLayout();
+ splitContainer_MainForm.Panel2.SuspendLayout();
+ splitContainer_MainForm.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer_Functional).BeginInit();
+ splitContainer_Functional.Panel1.SuspendLayout();
+ splitContainer_Functional.Panel2.SuspendLayout();
+ splitContainer_Functional.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer_Information).BeginInit();
+ splitContainer_Information.Panel1.SuspendLayout();
+ splitContainer_Information.Panel2.SuspendLayout();
+ splitContainer_Information.SuspendLayout();
+ groupBox_SkelList.SuspendLayout();
+ tableLayoutPanel.SuspendLayout();
+ flowLayoutPanel_Buttons.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)splitContainer_Config).BeginInit();
+ splitContainer_Config.Panel1.SuspendLayout();
+ splitContainer_Config.Panel2.SuspendLayout();
+ splitContainer_Config.SuspendLayout();
+ groupBox_SkelConfig.SuspendLayout();
+ groupBox_Preview.SuspendLayout();
+ panel_PreviewContainer.SuspendLayout();
+ panel_MainForm.SuspendLayout();
+ SuspendLayout();
+ //
+ // menuStrip
+ //
+ menuStrip.BackColor = SystemColors.Control;
+ menuStrip.ImageScalingSize = new Size(24, 24);
+ menuStrip.Items.AddRange(new ToolStripItem[] { toolStripMenuItem_File, toolStripMenuItem_Help });
+ menuStrip.Location = new Point(0, 0);
+ menuStrip.Name = "menuStrip";
+ menuStrip.Size = new Size(1519, 32);
+ menuStrip.TabIndex = 0;
+ menuStrip.Text = "菜单";
+ //
+ // toolStripMenuItem_File
+ //
+ toolStripMenuItem_File.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_Open, toolStripMenuItem_BatchOpen, toolStripSeparator1, toolStripMenuItem_Export, toolStripSeparator2, toolStripMenuItem_Exit });
+ toolStripMenuItem_File.Name = "toolStripMenuItem_File";
+ toolStripMenuItem_File.Size = new Size(84, 28);
+ toolStripMenuItem_File.Text = "文件(&F)";
+ //
+ // toolStripMenuItem_Open
+ //
+ toolStripMenuItem_Open.Name = "toolStripMenuItem_Open";
+ toolStripMenuItem_Open.ShortcutKeys = Keys.Control | Keys.O;
+ toolStripMenuItem_Open.Size = new Size(254, 34);
+ toolStripMenuItem_Open.Text = "打开(&O)...";
+ toolStripMenuItem_Open.Click += toolStripMenuItem_Open_Click;
+ //
+ // toolStripMenuItem_BatchOpen
+ //
+ toolStripMenuItem_BatchOpen.Name = "toolStripMenuItem_BatchOpen";
+ toolStripMenuItem_BatchOpen.Size = new Size(254, 34);
+ toolStripMenuItem_BatchOpen.Text = "批量打开(&B)...";
+ toolStripMenuItem_BatchOpen.Click += toolStripMenuItem_BatchOpen_Click;
+ //
+ // toolStripSeparator1
+ //
+ toolStripSeparator1.Name = "toolStripSeparator1";
+ toolStripSeparator1.Size = new Size(251, 6);
+ //
+ // toolStripMenuItem_Export
+ //
+ toolStripMenuItem_Export.Name = "toolStripMenuItem_Export";
+ toolStripMenuItem_Export.ShortcutKeys = Keys.Control | Keys.S;
+ toolStripMenuItem_Export.Size = new Size(254, 34);
+ toolStripMenuItem_Export.Text = "导出(&E)...";
+ toolStripMenuItem_Export.Click += toolStripMenuItem_Export_Click;
+ //
+ // toolStripSeparator2
+ //
+ toolStripSeparator2.Name = "toolStripSeparator2";
+ toolStripSeparator2.Size = new Size(251, 6);
+ //
+ // toolStripMenuItem_Exit
+ //
+ toolStripMenuItem_Exit.Name = "toolStripMenuItem_Exit";
+ toolStripMenuItem_Exit.ShortcutKeys = Keys.Alt | Keys.F4;
+ toolStripMenuItem_Exit.Size = new Size(254, 34);
+ toolStripMenuItem_Exit.Text = "退出(&X)";
+ toolStripMenuItem_Exit.Click += toolStripMenuItem_Exit_Click;
+ //
+ // toolStripMenuItem_Help
+ //
+ toolStripMenuItem_Help.DropDownItems.AddRange(new ToolStripItem[] { toolStripMenuItem_About });
+ toolStripMenuItem_Help.Name = "toolStripMenuItem_Help";
+ toolStripMenuItem_Help.Size = new Size(88, 28);
+ toolStripMenuItem_Help.Text = "帮助(&H)";
+ //
+ // toolStripMenuItem_About
+ //
+ toolStripMenuItem_About.Name = "toolStripMenuItem_About";
+ toolStripMenuItem_About.Size = new Size(171, 34);
+ toolStripMenuItem_About.Text = "关于(&A)";
+ //
+ // rtbLog
+ //
+ rtbLog.BackColor = SystemColors.Window;
+ rtbLog.BorderStyle = BorderStyle.None;
+ rtbLog.Dock = DockStyle.Fill;
+ rtbLog.Font = new Font("Consolas", 9F);
+ rtbLog.Location = new Point(0, 0);
+ rtbLog.Margin = new Padding(3, 2, 3, 2);
+ rtbLog.Name = "rtbLog";
+ rtbLog.ReadOnly = true;
+ rtbLog.Size = new Size(1499, 102);
+ rtbLog.TabIndex = 0;
+ rtbLog.Text = "";
+ rtbLog.WordWrap = false;
+ //
+ // splitContainer_MainForm
+ //
+ splitContainer_MainForm.Cursor = Cursors.SizeNS;
+ splitContainer_MainForm.Dock = DockStyle.Fill;
+ splitContainer_MainForm.Location = new Point(10, 5);
+ splitContainer_MainForm.Name = "splitContainer_MainForm";
+ splitContainer_MainForm.Orientation = Orientation.Horizontal;
+ //
+ // splitContainer_MainForm.Panel1
+ //
+ splitContainer_MainForm.Panel1.Controls.Add(splitContainer_Functional);
+ splitContainer_MainForm.Panel1.Cursor = Cursors.Default;
+ //
+ // splitContainer_MainForm.Panel2
+ //
+ splitContainer_MainForm.Panel2.Controls.Add(rtbLog);
+ splitContainer_MainForm.Panel2.Cursor = Cursors.Default;
+ splitContainer_MainForm.Size = new Size(1499, 838);
+ splitContainer_MainForm.SplitterDistance = 732;
+ splitContainer_MainForm.TabIndex = 3;
+ splitContainer_MainForm.TabStop = false;
+ splitContainer_MainForm.SplitterMoved += splitContainer_SplitterMoved;
+ splitContainer_MainForm.MouseUp += splitContainer_MouseUp;
+ //
+ // splitContainer_Functional
+ //
+ splitContainer_Functional.Cursor = Cursors.SizeWE;
+ splitContainer_Functional.Dock = DockStyle.Fill;
+ splitContainer_Functional.Location = new Point(0, 0);
+ splitContainer_Functional.Name = "splitContainer_Functional";
+ //
+ // splitContainer_Functional.Panel1
+ //
+ splitContainer_Functional.Panel1.Controls.Add(splitContainer_Information);
+ splitContainer_Functional.Panel1.Cursor = Cursors.Default;
+ //
+ // splitContainer_Functional.Panel2
+ //
+ splitContainer_Functional.Panel2.Controls.Add(groupBox_Preview);
+ splitContainer_Functional.Panel2.Cursor = Cursors.Default;
+ splitContainer_Functional.Size = new Size(1499, 732);
+ splitContainer_Functional.SplitterDistance = 698;
+ splitContainer_Functional.TabIndex = 2;
+ splitContainer_Functional.TabStop = false;
+ splitContainer_Functional.SplitterMoved += splitContainer_SplitterMoved;
+ splitContainer_Functional.MouseUp += splitContainer_MouseUp;
+ //
+ // splitContainer_Information
+ //
+ splitContainer_Information.Cursor = Cursors.SizeWE;
+ splitContainer_Information.Dock = DockStyle.Fill;
+ splitContainer_Information.Location = new Point(0, 0);
+ splitContainer_Information.Name = "splitContainer_Information";
+ //
+ // splitContainer_Information.Panel1
+ //
+ splitContainer_Information.Panel1.Controls.Add(groupBox_SkelList);
+ splitContainer_Information.Panel1.Cursor = Cursors.Default;
+ //
+ // splitContainer_Information.Panel2
+ //
+ splitContainer_Information.Panel2.Controls.Add(splitContainer_Config);
+ splitContainer_Information.Panel2.Cursor = Cursors.Default;
+ splitContainer_Information.Size = new Size(698, 732);
+ splitContainer_Information.SplitterDistance = 336;
+ splitContainer_Information.TabIndex = 1;
+ splitContainer_Information.TabStop = false;
+ splitContainer_Information.SplitterMoved += splitContainer_SplitterMoved;
+ splitContainer_Information.MouseUp += splitContainer_MouseUp;
+ //
+ // groupBox_SkelList
+ //
+ groupBox_SkelList.Controls.Add(tableLayoutPanel);
+ groupBox_SkelList.Dock = DockStyle.Fill;
+ groupBox_SkelList.Location = new Point(0, 0);
+ groupBox_SkelList.Name = "groupBox_SkelList";
+ groupBox_SkelList.Size = new Size(336, 732);
+ groupBox_SkelList.TabIndex = 0;
+ groupBox_SkelList.TabStop = false;
+ groupBox_SkelList.Text = "模型列表";
+ //
+ // tableLayoutPanel
+ //
+ tableLayoutPanel.ColumnCount = 1;
+ tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
+ tableLayoutPanel.Controls.Add(flowLayoutPanel_Buttons, 0, 0);
+ tableLayoutPanel.Controls.Add(listView_SkelList, 0, 1);
+ tableLayoutPanel.Dock = DockStyle.Fill;
+ tableLayoutPanel.Location = new Point(3, 26);
+ tableLayoutPanel.Name = "tableLayoutPanel";
+ tableLayoutPanel.RowCount = 2;
+ tableLayoutPanel.RowStyles.Add(new RowStyle());
+ tableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
+ tableLayoutPanel.Size = new Size(330, 703);
+ tableLayoutPanel.TabIndex = 1;
+ //
+ // flowLayoutPanel_Buttons
+ //
+ flowLayoutPanel_Buttons.AutoSize = true;
+ flowLayoutPanel_Buttons.Controls.Add(button_Add);
+ flowLayoutPanel_Buttons.Controls.Add(button_Insert);
+ flowLayoutPanel_Buttons.Controls.Add(button_Remove);
+ flowLayoutPanel_Buttons.Controls.Add(button_MoveUp);
+ flowLayoutPanel_Buttons.Controls.Add(button_MoveDown);
+ flowLayoutPanel_Buttons.Dock = DockStyle.Fill;
+ flowLayoutPanel_Buttons.Location = new Point(3, 3);
+ flowLayoutPanel_Buttons.Name = "flowLayoutPanel_Buttons";
+ flowLayoutPanel_Buttons.Size = new Size(324, 40);
+ flowLayoutPanel_Buttons.TabIndex = 4;
+ //
+ // button_Add
+ //
+ button_Add.Anchor = AnchorStyles.None;
+ button_Add.AutoSize = true;
+ button_Add.Location = new Point(3, 3);
+ button_Add.Name = "button_Add";
+ button_Add.Size = new Size(56, 34);
+ button_Add.TabIndex = 0;
+ button_Add.Text = "添加";
+ button_Add.UseVisualStyleBackColor = true;
+ button_Add.Click += toolStripMenuItem_Open_Click;
+ //
+ // button_Insert
+ //
+ button_Insert.Anchor = AnchorStyles.None;
+ button_Insert.AutoSize = true;
+ button_Insert.Enabled = false;
+ button_Insert.Location = new Point(65, 3);
+ button_Insert.Name = "button_Insert";
+ button_Insert.Size = new Size(56, 34);
+ button_Insert.TabIndex = 4;
+ button_Insert.Text = "插入";
+ button_Insert.UseVisualStyleBackColor = true;
+ button_Insert.Click += button_Insert_Click;
+ //
+ // button_Remove
+ //
+ button_Remove.Anchor = AnchorStyles.None;
+ button_Remove.AutoSize = true;
+ button_Remove.Enabled = false;
+ button_Remove.Location = new Point(127, 3);
+ button_Remove.Name = "button_Remove";
+ button_Remove.Size = new Size(56, 34);
+ button_Remove.TabIndex = 1;
+ button_Remove.Text = "移除";
+ button_Remove.UseVisualStyleBackColor = true;
+ button_Remove.Click += button_Remove_Click;
+ //
+ // button_MoveUp
+ //
+ button_MoveUp.Anchor = AnchorStyles.None;
+ button_MoveUp.AutoSize = true;
+ button_MoveUp.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ button_MoveUp.Enabled = false;
+ button_MoveUp.Location = new Point(189, 3);
+ button_MoveUp.Name = "button_MoveUp";
+ button_MoveUp.Size = new Size(56, 34);
+ button_MoveUp.TabIndex = 2;
+ button_MoveUp.Text = "上移";
+ button_MoveUp.UseVisualStyleBackColor = true;
+ button_MoveUp.Click += button_MoveUp_Click;
+ //
+ // button_MoveDown
+ //
+ button_MoveDown.Anchor = AnchorStyles.None;
+ button_MoveDown.AutoSize = true;
+ button_MoveDown.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ button_MoveDown.Enabled = false;
+ button_MoveDown.Location = new Point(251, 3);
+ button_MoveDown.Name = "button_MoveDown";
+ button_MoveDown.Size = new Size(56, 34);
+ button_MoveDown.TabIndex = 3;
+ button_MoveDown.Text = "下移";
+ button_MoveDown.UseVisualStyleBackColor = true;
+ button_MoveDown.Click += button_MoveDown_Click;
+ //
+ // listView_SkelList
+ //
+ listView_SkelList.Columns.AddRange(new ColumnHeader[] { columnHeader_Name, columnHeader_Version });
+ listView_SkelList.Dock = DockStyle.Fill;
+ listView_SkelList.FullRowSelect = true;
+ listView_SkelList.GridLines = true;
+ listView_SkelList.Items.AddRange(new ListViewItem[] { listViewItem1 });
+ listView_SkelList.Location = new Point(3, 49);
+ listView_SkelList.Name = "listView_SkelList";
+ listView_SkelList.ShowItemToolTips = true;
+ listView_SkelList.Size = new Size(324, 651);
+ listView_SkelList.TabIndex = 1;
+ listView_SkelList.UseCompatibleStateImageBehavior = false;
+ listView_SkelList.View = View.Details;
+ listView_SkelList.SelectedIndexChanged += listView_SkelList_SelectedIndexChanged;
+ //
+ // columnHeader_Name
+ //
+ columnHeader_Name.Text = "名称";
+ columnHeader_Name.Width = 150;
+ //
+ // columnHeader_Version
+ //
+ columnHeader_Version.Text = "版本";
+ columnHeader_Version.Width = 150;
+ //
+ // splitContainer_Config
+ //
+ splitContainer_Config.Cursor = Cursors.SizeNS;
+ splitContainer_Config.Dock = DockStyle.Fill;
+ splitContainer_Config.Location = new Point(0, 0);
+ splitContainer_Config.Name = "splitContainer_Config";
+ splitContainer_Config.Orientation = Orientation.Horizontal;
+ //
+ // splitContainer_Config.Panel1
+ //
+ splitContainer_Config.Panel1.Controls.Add(groupBox_SkelConfig);
+ splitContainer_Config.Panel1.Cursor = Cursors.Default;
+ //
+ // splitContainer_Config.Panel2
+ //
+ splitContainer_Config.Panel2.Controls.Add(groupBox_PreviewConfig);
+ splitContainer_Config.Panel2.Cursor = Cursors.Default;
+ splitContainer_Config.Size = new Size(358, 732);
+ splitContainer_Config.SplitterDistance = 493;
+ splitContainer_Config.TabIndex = 0;
+ splitContainer_Config.TabStop = false;
+ splitContainer_Config.SplitterMoved += splitContainer_SplitterMoved;
+ splitContainer_Config.MouseUp += splitContainer_MouseUp;
+ //
+ // groupBox_SkelConfig
+ //
+ groupBox_SkelConfig.Controls.Add(propertyGrid_Skel);
+ groupBox_SkelConfig.Dock = DockStyle.Fill;
+ groupBox_SkelConfig.Location = new Point(0, 0);
+ groupBox_SkelConfig.Name = "groupBox_SkelConfig";
+ groupBox_SkelConfig.Size = new Size(358, 493);
+ groupBox_SkelConfig.TabIndex = 0;
+ groupBox_SkelConfig.TabStop = false;
+ groupBox_SkelConfig.Text = "模型参数";
+ //
+ // propertyGrid_Skel
+ //
+ propertyGrid_Skel.Dock = DockStyle.Fill;
+ propertyGrid_Skel.HelpVisible = false;
+ propertyGrid_Skel.Location = new Point(3, 26);
+ propertyGrid_Skel.Name = "propertyGrid_Skel";
+ propertyGrid_Skel.Size = new Size(352, 464);
+ propertyGrid_Skel.TabIndex = 0;
+ propertyGrid_Skel.ToolbarVisible = false;
+ //
+ // groupBox_PreviewConfig
+ //
+ groupBox_PreviewConfig.Dock = DockStyle.Fill;
+ groupBox_PreviewConfig.Location = new Point(0, 0);
+ groupBox_PreviewConfig.Name = "groupBox_PreviewConfig";
+ groupBox_PreviewConfig.Size = new Size(358, 235);
+ groupBox_PreviewConfig.TabIndex = 1;
+ groupBox_PreviewConfig.TabStop = false;
+ groupBox_PreviewConfig.Text = "画面参数";
+ //
+ // groupBox_Preview
+ //
+ groupBox_Preview.Controls.Add(panel_PreviewContainer);
+ groupBox_Preview.Dock = DockStyle.Fill;
+ groupBox_Preview.Location = new Point(0, 0);
+ groupBox_Preview.Name = "groupBox_Preview";
+ groupBox_Preview.Size = new Size(797, 732);
+ groupBox_Preview.TabIndex = 1;
+ groupBox_Preview.TabStop = false;
+ groupBox_Preview.Text = "预览画面";
+ //
+ // panel_PreviewContainer
+ //
+ panel_PreviewContainer.Controls.Add(panel_Preview);
+ panel_PreviewContainer.Dock = DockStyle.Fill;
+ panel_PreviewContainer.Location = new Point(3, 26);
+ panel_PreviewContainer.Margin = new Padding(0);
+ panel_PreviewContainer.Name = "panel_PreviewContainer";
+ panel_PreviewContainer.Size = new Size(791, 703);
+ panel_PreviewContainer.TabIndex = 1;
+ //
+ // panel_Preview
+ //
+ panel_Preview.BackColor = SystemColors.ControlDark;
+ panel_Preview.Location = new Point(107, 95);
+ panel_Preview.Name = "panel_Preview";
+ panel_Preview.Size = new Size(256, 256);
+ panel_Preview.TabIndex = 0;
+ //
+ // panel_MainForm
+ //
+ panel_MainForm.Controls.Add(splitContainer_MainForm);
+ panel_MainForm.Dock = DockStyle.Fill;
+ panel_MainForm.Location = new Point(0, 32);
+ panel_MainForm.Name = "panel_MainForm";
+ panel_MainForm.Padding = new Padding(10, 5, 10, 10);
+ panel_MainForm.Size = new Size(1519, 853);
+ panel_MainForm.TabIndex = 4;
+ //
+ // openFileDialog_Skel
+ //
+ openFileDialog_Skel.AddExtension = false;
+ openFileDialog_Skel.AddToRecent = false;
+ openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json";
+ //
+ // openFileDialog_Atlas
+ //
+ openFileDialog_Atlas.AddExtension = false;
+ openFileDialog_Atlas.AddToRecent = false;
+ openFileDialog_Atlas.Filter = "atlas 文件 (*.atlas)|*.atlas";
+ //
+ // MainForm
+ //
+ AutoScaleDimensions = new SizeF(11F, 24F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(1519, 885);
+ Controls.Add(panel_MainForm);
+ Controls.Add(menuStrip);
+ MainMenuStrip = menuStrip;
+ Margin = new Padding(3, 2, 3, 2);
+ Name = "MainForm";
+ StartPosition = FormStartPosition.CenterScreen;
+ Text = "SpineViewer";
+ menuStrip.ResumeLayout(false);
+ menuStrip.PerformLayout();
+ splitContainer_MainForm.Panel1.ResumeLayout(false);
+ splitContainer_MainForm.Panel2.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)splitContainer_MainForm).EndInit();
+ splitContainer_MainForm.ResumeLayout(false);
+ splitContainer_Functional.Panel1.ResumeLayout(false);
+ splitContainer_Functional.Panel2.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)splitContainer_Functional).EndInit();
+ splitContainer_Functional.ResumeLayout(false);
+ splitContainer_Information.Panel1.ResumeLayout(false);
+ splitContainer_Information.Panel2.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)splitContainer_Information).EndInit();
+ splitContainer_Information.ResumeLayout(false);
+ groupBox_SkelList.ResumeLayout(false);
+ tableLayoutPanel.ResumeLayout(false);
+ tableLayoutPanel.PerformLayout();
+ flowLayoutPanel_Buttons.ResumeLayout(false);
+ flowLayoutPanel_Buttons.PerformLayout();
+ splitContainer_Config.Panel1.ResumeLayout(false);
+ splitContainer_Config.Panel2.ResumeLayout(false);
+ ((System.ComponentModel.ISupportInitialize)splitContainer_Config).EndInit();
+ splitContainer_Config.ResumeLayout(false);
+ groupBox_SkelConfig.ResumeLayout(false);
+ groupBox_Preview.ResumeLayout(false);
+ panel_PreviewContainer.ResumeLayout(false);
+ panel_MainForm.ResumeLayout(false);
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private MenuStrip menuStrip;
+ private ToolStripMenuItem toolStripMenuItem_File;
+ private ToolStripMenuItem toolStripMenuItem_Open;
+ private ToolStripMenuItem toolStripMenuItem_Exit;
+ private ToolStripSeparator toolStripSeparator1;
+ private ToolStripMenuItem toolStripMenuItem_Export;
+ private ToolStripSeparator toolStripSeparator2;
+ private RichTextBox rtbLog;
+ private SplitContainer splitContainer_MainForm;
+ private SplitContainer splitContainer_Functional;
+ private SplitContainer splitContainer_Information;
+ private GroupBox groupBox_SkelList;
+ private GroupBox groupBox_SkelConfig;
+ private SplitContainer splitContainer_Config;
+ private GroupBox groupBox_PreviewConfig;
+ private Panel panel_MainForm;
+ private ToolStripMenuItem toolStripMenuItem_Help;
+ private ToolStripMenuItem toolStripMenuItem_About;
+ private ToolStripMenuItem toolStripMenuItem_BatchOpen;
+ private TableLayoutPanel tableLayoutPanel;
+ private FlowLayoutPanel flowLayoutPanel_Buttons;
+ private Button button_Add;
+ private Button button_Insert;
+ private Button button_Remove;
+ private Button button_MoveUp;
+ private Button button_MoveDown;
+ private ListView listView_SkelList;
+ private ColumnHeader columnHeader_Name;
+ private ColumnHeader columnHeader_Version;
+ private GroupBox groupBox_Preview;
+ private Panel panel_Preview;
+ private Panel panel_PreviewContainer;
+ private OpenFileDialog openFileDialog_Skel;
+ private OpenFileDialog openFileDialog_Atlas;
+ private ToolTip toolTip1;
+ private PropertyGrid propertyGrid_Skel;
+ }
+}
diff --git a/SpineViewer/src/MainForm.cs b/SpineViewer/src/MainForm.cs
new file mode 100644
index 0000000..e62f18b
--- /dev/null
+++ b/SpineViewer/src/MainForm.cs
@@ -0,0 +1,179 @@
+using NLog;
+using SpineViewer.Spine;
+using System.ComponentModel;
+using System.Diagnostics;
+
+namespace SpineViewer
+{
+ public partial class MainForm : Form
+ {
+ Manager spineManger = null;
+
+ public MainForm()
+ {
+ InitializeComponent();
+ InitializeLogConfiguration();
+
+ spineManger = new(listView_SkelList);
+ }
+
+ ///
+ /// ʼ־
+ ///
+ private void InitializeLogConfiguration()
+ {
+ // ־
+ var rtbTarget = new NLog.Windows.Forms.RichTextBoxTarget
+ {
+ Name = "rtbTarget",
+ TargetForm = this,
+ TargetRichTextBox = rtbLog,
+ AutoScroll = true,
+ MaxLines = 3000,
+ SupportLinks = true,
+ Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}"
+ };
+
+ rtbTarget.WordColoringRules.Add(new("[D]", "Gray", "Empty", FontStyle.Bold));
+ rtbTarget.WordColoringRules.Add(new("[I]", "Gray", "Empty", FontStyle.Bold));
+ rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty", FontStyle.Bold));
+ rtbTarget.WordColoringRules.Add(new("[E]", "Red", "Empty", FontStyle.Bold));
+ rtbTarget.WordColoringRules.Add(new("[F]", "Red", "Empty", FontStyle.Bold));
+
+ LogManager.Configuration.AddTarget(rtbTarget);
+ LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, rtbTarget);
+ LogManager.ReconfigExistingLoggers();
+ }
+
+ #region ˵
+
+ private void toolStripMenuItem_Open_Click(object sender, EventArgs e)
+ {
+ var dialog = new SkelSelectDialog();
+ if (dialog.ShowDialog() == DialogResult.OK)
+ {
+ try
+ {
+ spineManger.Add(Spine.Spine.New(dialog.Version, dialog.SkelPath, dialog.AtlasPath));
+ }
+ catch (Exception ex)
+ {
+ Program.Logger.Error(ex.ToString());
+ Program.Logger.Error($"Failed to load {dialog.SkelPath} {dialog.AtlasPath}");
+ MessageBox.Show(ex.ToString(), "ʧ", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+ }
+
+ private void toolStripMenuItem_BatchOpen_Click(object sender, EventArgs e)
+ {
+ }
+
+ private void toolStripMenuItem_Export_Click(object sender, EventArgs e)
+ {
+ var a = new SizeF(10, 100);
+ spineManger.Spines[0].Position = spineManger.Spines[0].Position + a;
+ }
+
+ private void toolStripMenuItem_Exit_Click(object sender, EventArgs e)
+ {
+ Close();
+ }
+
+ #endregion
+
+ #region ģб
+
+ // button_Add_Click => toolStripMenuItem_Open_Click
+
+ private void button_Insert_Click(object sender, EventArgs e)
+ {
+ if (listView_SkelList.SelectedIndices.Count <= 0)
+ return;
+
+ var index = listView_SkelList.SelectedIndices[0];
+ var dialog = new SkelSelectDialog();
+ dialog.ShowDialog();
+ try
+ {
+ spineManger.Insert(index, Spine.Spine.New(dialog.Version, dialog.SkelPath, dialog.AtlasPath));
+ }
+ catch (Exception ex)
+ {
+ Program.Logger.Error(ex.ToString());
+ Program.Logger.Error($"Failed to load {dialog.SkelPath} {dialog.AtlasPath}");
+ }
+ }
+
+ private void button_Remove_Click(object sender, EventArgs e)
+ {
+ if (listView_SkelList.SelectedIndices.Count <= 0)
+ return;
+ spineManger.Remove(listView_SkelList.SelectedIndices.Cast());
+ }
+
+ private void button_MoveUp_Click(object sender, EventArgs e)
+ {
+ if (listView_SkelList.SelectedIndices.Count <= 0)
+ return;
+ spineManger.MoveUp(listView_SkelList.SelectedIndices[0]);
+ }
+
+ private void button_MoveDown_Click(object sender, EventArgs e)
+ {
+ if (listView_SkelList.SelectedIndices.Count <= 0)
+ return;
+ spineManger.MoveDown(listView_SkelList.SelectedIndices[0]);
+ }
+
+ private void listView_SkelList_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ if (listView_SkelList.SelectedIndices.Count <= 0)
+ {
+ button_Insert.Enabled = false;
+ button_Remove.Enabled = false;
+ button_MoveUp.Enabled = false;
+ button_MoveDown.Enabled = false;
+ propertyGrid_Skel.SelectedObject = null;
+ }
+ else if (listView_SkelList.SelectedIndices.Count <= 1)
+ {
+ button_Insert.Enabled = true;
+ button_Remove.Enabled = true;
+ button_MoveUp.Enabled = true;
+ button_MoveDown.Enabled = true;
+ propertyGrid_Skel.SelectedObject = spineManger.Spines[listView_SkelList.SelectedIndices[0]];
+ }
+ else
+ {
+ button_Insert.Enabled = false;
+ button_Remove.Enabled = true;
+ button_MoveUp.Enabled = false;
+ button_MoveDown.Enabled = false;
+ propertyGrid_Skel.SelectedObject = null;
+ }
+ }
+
+ #endregion
+
+ #region
+ #endregion
+
+ #region Ԥ
+ #endregion
+
+ #region
+
+ private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e)
+ {
+ ActiveControl = null;
+ }
+
+ private void splitContainer_MouseUp(object sender, MouseEventArgs e)
+ {
+ ActiveControl = null;
+ }
+
+ #endregion
+ }
+}
diff --git a/SpineViewer/src/MainForm.resx b/SpineViewer/src/MainForm.resx
new file mode 100644
index 0000000..a73caa7
--- /dev/null
+++ b/SpineViewer/src/MainForm.resx
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
+ 156, 25
+
+
+ 404, 23
+
+
+ 651, 23
+
+
+ 89
+
+
\ No newline at end of file
diff --git a/SpineViewer/src/Program.cs b/SpineViewer/src/Program.cs
new file mode 100644
index 0000000..ef00088
--- /dev/null
+++ b/SpineViewer/src/Program.cs
@@ -0,0 +1,49 @@
+using NLog;
+
+namespace SpineViewer
+{
+ internal static class Program
+ {
+ public static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+
+ ///
+ /// The main entry point for the application.
+ ///
+ [STAThread]
+ static void Main()
+ {
+ InitializeLogConfiguration();
+ Logger.Info("Program Started.");
+
+ // To customize application configuration such as set high DPI settings or default font,
+ // see https://aka.ms/applicationconfiguration.
+ ApplicationConfiguration.Initialize();
+ Application.Run(new MainForm());
+ }
+
+ ///
+ /// ʼ־
+ ///
+ private static void InitializeLogConfiguration()
+ {
+ var config = new NLog.Config.LoggingConfiguration();
+
+ // ļ־
+ var fileTarget = new NLog.Targets.FileTarget("fileTarget")
+ {
+ Encoding = System.Text.Encoding.UTF8,
+ FileName = "${basedir}/logs/app.log",
+ ArchiveFileName = "${basedir}/logs/app.{#}.log",
+ ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling,
+ ArchiveAboveSize = 1048576,
+ MaxArchiveFiles = 5,
+ Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}"
+ };
+
+ config.AddTarget(fileTarget);
+ config.AddRule(LogLevel.Debug, LogLevel.Fatal, fileTarget);
+ LogManager.Configuration = config;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/SpineViewer/src/SkelBatchSelectForm.Designer.cs b/SpineViewer/src/SkelBatchSelectForm.Designer.cs
new file mode 100644
index 0000000..0224f9e
--- /dev/null
+++ b/SpineViewer/src/SkelBatchSelectForm.Designer.cs
@@ -0,0 +1,39 @@
+namespace SpineViewer.src
+{
+ partial class SkelBatchSelectForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.components = new System.ComponentModel.Container();
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(800, 450);
+ this.Text = "SkelBatchSelectForm";
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/SpineViewer/src/SkelBatchSelectForm.cs b/SpineViewer/src/SkelBatchSelectForm.cs
new file mode 100644
index 0000000..f2f0762
--- /dev/null
+++ b/SpineViewer/src/SkelBatchSelectForm.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace SpineViewer.src
+{
+ public partial class SkelBatchSelectForm: Form
+ {
+ public SkelBatchSelectForm()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/SpineViewer/src/SkelBatchSelectForm.resx b/SpineViewer/src/SkelBatchSelectForm.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/SpineViewer/src/SkelBatchSelectForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/SpineViewer/src/SkelSelectDialog.Designer.cs b/SpineViewer/src/SkelSelectDialog.Designer.cs
new file mode 100644
index 0000000..97c3805
--- /dev/null
+++ b/SpineViewer/src/SkelSelectDialog.Designer.cs
@@ -0,0 +1,285 @@
+namespace SpineViewer
+{
+ partial class SkelSelectDialog
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ panel1 = new Panel();
+ tableLayoutPanel1 = new TableLayoutPanel();
+ label4 = new Label();
+ label1 = new Label();
+ label2 = new Label();
+ label3 = new Label();
+ textBox_SkelPath = new TextBox();
+ button_SelectSkel = new Button();
+ button_SelectAtlas = new Button();
+ comboBox_Version = new ComboBox();
+ textBox_AtlasPath = new TextBox();
+ tableLayoutPanel2 = new TableLayoutPanel();
+ button_Ok = new Button();
+ button_Cancel = new Button();
+ openFileDialog_Skel = new OpenFileDialog();
+ openFileDialog_Atlas = new OpenFileDialog();
+ panel1.SuspendLayout();
+ tableLayoutPanel1.SuspendLayout();
+ tableLayoutPanel2.SuspendLayout();
+ SuspendLayout();
+ //
+ // panel1
+ //
+ panel1.Controls.Add(tableLayoutPanel1);
+ panel1.Dock = DockStyle.Fill;
+ panel1.Location = new Point(0, 0);
+ panel1.Name = "panel1";
+ panel1.Padding = new Padding(50, 15, 50, 10);
+ panel1.Size = new Size(907, 286);
+ panel1.TabIndex = 0;
+ //
+ // tableLayoutPanel1
+ //
+ tableLayoutPanel1.AutoSize = true;
+ tableLayoutPanel1.ColumnCount = 4;
+ tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
+ tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
+ tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
+ tableLayoutPanel1.ColumnStyles.Add(new ColumnStyle());
+ tableLayoutPanel1.Controls.Add(label4, 0, 0);
+ tableLayoutPanel1.Controls.Add(label1, 0, 1);
+ tableLayoutPanel1.Controls.Add(label2, 0, 2);
+ tableLayoutPanel1.Controls.Add(label3, 0, 3);
+ tableLayoutPanel1.Controls.Add(textBox_SkelPath, 1, 1);
+ tableLayoutPanel1.Controls.Add(button_SelectSkel, 3, 1);
+ tableLayoutPanel1.Controls.Add(button_SelectAtlas, 3, 2);
+ tableLayoutPanel1.Controls.Add(comboBox_Version, 1, 3);
+ tableLayoutPanel1.Controls.Add(textBox_AtlasPath, 1, 2);
+ tableLayoutPanel1.Controls.Add(tableLayoutPanel2, 0, 4);
+ tableLayoutPanel1.Dock = DockStyle.Fill;
+ tableLayoutPanel1.Location = new Point(50, 15);
+ tableLayoutPanel1.Name = "tableLayoutPanel1";
+ tableLayoutPanel1.RowCount = 5;
+ tableLayoutPanel1.RowStyles.Add(new RowStyle());
+ tableLayoutPanel1.RowStyles.Add(new RowStyle());
+ tableLayoutPanel1.RowStyles.Add(new RowStyle());
+ tableLayoutPanel1.RowStyles.Add(new RowStyle());
+ tableLayoutPanel1.RowStyles.Add(new RowStyle());
+ tableLayoutPanel1.Size = new Size(807, 261);
+ tableLayoutPanel1.TabIndex = 0;
+ //
+ // label4
+ //
+ label4.AutoSize = true;
+ tableLayoutPanel1.SetColumnSpan(label4, 4);
+ label4.Dock = DockStyle.Fill;
+ label4.Location = new Point(15, 15);
+ label4.Margin = new Padding(15);
+ label4.Name = "label4";
+ label4.Size = new Size(777, 24);
+ label4.TabIndex = 11;
+ label4.Text = "说明:如果没有选择atlas,则会自动读取与skel同目录下同名的atlas文件";
+ label4.TextAlign = ContentAlignment.MiddleCenter;
+ //
+ // label1
+ //
+ label1.Anchor = AnchorStyles.Right;
+ label1.AutoSize = true;
+ label1.Location = new Point(10, 62);
+ label1.Name = "label1";
+ label1.Size = new Size(119, 24);
+ label1.TabIndex = 0;
+ label1.Text = "skel文件路径:";
+ //
+ // label2
+ //
+ label2.Anchor = AnchorStyles.Right;
+ label2.AutoSize = true;
+ label2.Location = new Point(3, 102);
+ label2.Name = "label2";
+ label2.Size = new Size(126, 24);
+ label2.TabIndex = 1;
+ label2.Text = "atlas文件路径:";
+ //
+ // label3
+ //
+ label3.Anchor = AnchorStyles.Right;
+ label3.AutoSize = true;
+ label3.Location = new Point(79, 141);
+ label3.Name = "label3";
+ label3.Size = new Size(50, 24);
+ label3.TabIndex = 2;
+ label3.Text = "版本:";
+ //
+ // textBox_SkelPath
+ //
+ tableLayoutPanel1.SetColumnSpan(textBox_SkelPath, 2);
+ textBox_SkelPath.Dock = DockStyle.Fill;
+ textBox_SkelPath.Location = new Point(135, 57);
+ textBox_SkelPath.Name = "textBox_SkelPath";
+ textBox_SkelPath.Size = new Size(630, 30);
+ textBox_SkelPath.TabIndex = 3;
+ //
+ // button_SelectSkel
+ //
+ button_SelectSkel.AutoSize = true;
+ button_SelectSkel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ button_SelectSkel.Location = new Point(771, 57);
+ button_SelectSkel.Name = "button_SelectSkel";
+ button_SelectSkel.Size = new Size(32, 34);
+ button_SelectSkel.TabIndex = 5;
+ button_SelectSkel.Text = "...";
+ button_SelectSkel.UseVisualStyleBackColor = true;
+ button_SelectSkel.Click += button_SelectSkel_Click;
+ //
+ // button_SelectAtlas
+ //
+ button_SelectAtlas.AutoSize = true;
+ button_SelectAtlas.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ button_SelectAtlas.Location = new Point(771, 97);
+ button_SelectAtlas.Name = "button_SelectAtlas";
+ button_SelectAtlas.Size = new Size(32, 34);
+ button_SelectAtlas.TabIndex = 6;
+ button_SelectAtlas.Text = "...";
+ button_SelectAtlas.UseVisualStyleBackColor = true;
+ button_SelectAtlas.Click += button_SelectAtlas_Click;
+ //
+ // comboBox_Version
+ //
+ comboBox_Version.Anchor = AnchorStyles.Left;
+ comboBox_Version.DropDownStyle = ComboBoxStyle.DropDownList;
+ comboBox_Version.FormattingEnabled = true;
+ comboBox_Version.Location = new Point(135, 137);
+ comboBox_Version.Name = "comboBox_Version";
+ comboBox_Version.Size = new Size(182, 32);
+ comboBox_Version.Sorted = true;
+ comboBox_Version.TabIndex = 9;
+ //
+ // textBox_AtlasPath
+ //
+ tableLayoutPanel1.SetColumnSpan(textBox_AtlasPath, 2);
+ textBox_AtlasPath.Dock = DockStyle.Fill;
+ textBox_AtlasPath.Location = new Point(135, 97);
+ textBox_AtlasPath.Name = "textBox_AtlasPath";
+ textBox_AtlasPath.Size = new Size(630, 30);
+ textBox_AtlasPath.TabIndex = 4;
+ //
+ // tableLayoutPanel2
+ //
+ tableLayoutPanel2.AutoSize = true;
+ tableLayoutPanel2.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ tableLayoutPanel2.ColumnCount = 2;
+ tableLayoutPanel1.SetColumnSpan(tableLayoutPanel2, 4);
+ tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
+ tableLayoutPanel2.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
+ tableLayoutPanel2.Controls.Add(button_Ok, 0, 0);
+ tableLayoutPanel2.Controls.Add(button_Cancel, 1, 0);
+ tableLayoutPanel2.Dock = DockStyle.Bottom;
+ tableLayoutPanel2.Location = new Point(3, 218);
+ tableLayoutPanel2.Name = "tableLayoutPanel2";
+ tableLayoutPanel2.RowCount = 1;
+ tableLayoutPanel2.RowStyles.Add(new RowStyle());
+ tableLayoutPanel2.Size = new Size(801, 40);
+ tableLayoutPanel2.TabIndex = 10;
+ //
+ // button_Ok
+ //
+ button_Ok.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
+ button_Ok.Location = new Point(258, 3);
+ button_Ok.Margin = new Padding(3, 3, 30, 3);
+ button_Ok.Name = "button_Ok";
+ button_Ok.Size = new Size(112, 34);
+ button_Ok.TabIndex = 7;
+ button_Ok.Text = "确认";
+ button_Ok.UseVisualStyleBackColor = true;
+ button_Ok.Click += button_Ok_Click;
+ //
+ // button_Cancel
+ //
+ button_Cancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Left;
+ button_Cancel.Location = new Point(430, 3);
+ button_Cancel.Margin = new Padding(30, 3, 3, 3);
+ button_Cancel.Name = "button_Cancel";
+ button_Cancel.Size = new Size(112, 34);
+ button_Cancel.TabIndex = 8;
+ button_Cancel.Text = "取消";
+ button_Cancel.UseVisualStyleBackColor = true;
+ button_Cancel.Click += button_Cancel_Click;
+ //
+ // openFileDialog_Skel
+ //
+ openFileDialog_Skel.AddExtension = false;
+ openFileDialog_Skel.AddToRecent = false;
+ openFileDialog_Skel.Filter = "skel 文件 (*.skel; *.json)|*.skel;*.json";
+ //
+ // openFileDialog_Atlas
+ //
+ openFileDialog_Atlas.AddExtension = false;
+ openFileDialog_Atlas.AddToRecent = false;
+ openFileDialog_Atlas.Filter = "atlas 文件 (*.atlas)|*.atlas";
+ //
+ // SkelSelectDialog
+ //
+ AcceptButton = button_Ok;
+ AutoScaleDimensions = new SizeF(11F, 24F);
+ AutoScaleMode = AutoScaleMode.Font;
+ CancelButton = button_Cancel;
+ ClientSize = new Size(907, 286);
+ Controls.Add(panel1);
+ FormBorderStyle = FormBorderStyle.FixedDialog;
+ MaximizeBox = false;
+ MinimizeBox = false;
+ Name = "SkelSelectDialog";
+ ShowIcon = false;
+ ShowInTaskbar = false;
+ StartPosition = FormStartPosition.CenterScreen;
+ Text = "打开骨骼";
+ panel1.ResumeLayout(false);
+ panel1.PerformLayout();
+ tableLayoutPanel1.ResumeLayout(false);
+ tableLayoutPanel1.PerformLayout();
+ tableLayoutPanel2.ResumeLayout(false);
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ private Panel panel1;
+ private TableLayoutPanel tableLayoutPanel1;
+ private Label label1;
+ private Label label2;
+ private Label label3;
+ private TextBox textBox_SkelPath;
+ private Button button_SelectSkel;
+ private Button button_SelectAtlas;
+ private Button button_Ok;
+ private Button button_Cancel;
+ private ComboBox comboBox_Version;
+ private TextBox textBox_AtlasPath;
+ private TableLayoutPanel tableLayoutPanel2;
+ private OpenFileDialog openFileDialog_Skel;
+ private OpenFileDialog openFileDialog_Atlas;
+ private Label label4;
+ }
+}
\ No newline at end of file
diff --git a/SpineViewer/src/SkelSelectDialog.cs b/SpineViewer/src/SkelSelectDialog.cs
new file mode 100644
index 0000000..211f029
--- /dev/null
+++ b/SpineViewer/src/SkelSelectDialog.cs
@@ -0,0 +1,80 @@
+using SpineViewer.Spine;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace SpineViewer
+{
+ public partial class SkelSelectDialog : Form
+ {
+ public string SkelPath { get; private set; }
+ public string? AtlasPath { get; private set; }
+ public Spine.Version Version { get; private set; }
+
+ public SkelSelectDialog()
+ {
+ InitializeComponent();
+ comboBox_Version.DataSource = VersionHelper.Versions.ToList();
+ comboBox_Version.DisplayMember = "Value";
+ comboBox_Version.ValueMember = "Key";
+ comboBox_Version.SelectedValue = Spine.Version.V38;
+ }
+
+ private void button_SelectSkel_Click(object sender, EventArgs e)
+ {
+ openFileDialog_Skel.InitialDirectory = Path.GetDirectoryName(textBox_SkelPath.Text);
+ if (openFileDialog_Skel.ShowDialog() == DialogResult.OK)
+ {
+ textBox_SkelPath.Text = Path.GetFullPath(openFileDialog_Skel.FileName);
+ }
+ }
+
+ private void button_SelectAtlas_Click(object sender, EventArgs e)
+ {
+ openFileDialog_Atlas.InitialDirectory = Path.GetDirectoryName(textBox_AtlasPath.Text);
+ if (openFileDialog_Atlas.ShowDialog() == DialogResult.OK)
+ {
+ textBox_AtlasPath.Text = Path.GetFullPath(openFileDialog_Atlas.FileName);
+ }
+ }
+
+ private void button_Ok_Click(object sender, EventArgs e)
+ {
+ var skelPath = textBox_SkelPath.Text;
+ var atlasPath = textBox_AtlasPath.Text;
+
+ if (!File.Exists(skelPath))
+ {
+ MessageBox.Show($"{skelPath}", "skel文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ if (string.IsNullOrEmpty(atlasPath))
+ {
+ atlasPath = null;
+ }
+ else if (!File.Exists(atlasPath))
+ {
+ MessageBox.Show($"{atlasPath}", "atlas文件不存在", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ SkelPath = skelPath;
+ AtlasPath = atlasPath;
+ Version = (Spine.Version)comboBox_Version.SelectedValue;
+
+ DialogResult = DialogResult.OK;
+ }
+
+ private void button_Cancel_Click(object sender, EventArgs e)
+ {
+ DialogResult = DialogResult.Cancel;
+ }
+ }
+}
diff --git a/SpineViewer/src/SkelSelectDialog.resx b/SpineViewer/src/SkelSelectDialog.resx
new file mode 100644
index 0000000..b6e9490
--- /dev/null
+++ b/SpineViewer/src/SkelSelectDialog.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 156, 25
+
+
+ 404, 23
+
+
\ No newline at end of file
diff --git a/SpineViewer/src/Spine/BlendMode.cs b/SpineViewer/src/Spine/BlendMode.cs
new file mode 100644
index 0000000..75ffdcf
--- /dev/null
+++ b/SpineViewer/src/Spine/BlendMode.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.Spine
+{
+ ///
+ /// SFML 混合模式
+ ///
+ public static class BlendMode
+ {
+ ///
+ /// Alpha Blend
+ ///
+ /// res.c = src.c * src.a + dst.c * (1 - src.a)
+ /// res.a = src.a * 1 + dst.a * (1 - src.a)
+ ///
+ ///
+ public static SFML.Graphics.BlendMode Normal = SFML.Graphics.BlendMode.Alpha;
+
+ ///
+ /// Additive Blend
+ ///
+ /// res.c = src.c * src.a + dst.c * 1
+ /// res.a = src.a * 1 + dst.a * 1
+ ///
+ ///
+ public static SFML.Graphics.BlendMode Additive = SFML.Graphics.BlendMode.Add;
+
+ ///
+ /// Multiply Blend (PremultipliedAlpha Only)
+ ///
+ /// res.c = src.c * dst.c + dst.c * (1 - src.a)
+ /// res.a = src.a * 1 + dst.a * (1 - src.a)
+ ///
+ ///
+ public static SFML.Graphics.BlendMode Multiply = new(
+ SFML.Graphics.BlendMode.Factor.DstColor,
+ SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
+ SFML.Graphics.BlendMode.Equation.Add,
+ SFML.Graphics.BlendMode.Factor.One,
+ SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
+ SFML.Graphics.BlendMode.Equation.Add
+ );
+
+ ///
+ /// Screen Blend (PremultipliedAlpha Only)
+ ///
+ /// res.c = src.c * 1 + dst.c * (1 - src.c) = 1 - [(1 - src.c)(1 - dst.c)]
+ /// res.a = src.a * 1 + dst.a * (1 - src.a)
+ ///
+ ///
+ public static SFML.Graphics.BlendMode Screen = new(
+ SFML.Graphics.BlendMode.Factor.One,
+ SFML.Graphics.BlendMode.Factor.OneMinusSrcColor,
+ SFML.Graphics.BlendMode.Equation.Add,
+ SFML.Graphics.BlendMode.Factor.One,
+ SFML.Graphics.BlendMode.Factor.OneMinusSrcAlpha,
+ SFML.Graphics.BlendMode.Equation.Add
+ );
+ }
+}
diff --git a/SpineViewer/src/Spine/Implementations/Spine36.cs b/SpineViewer/src/Spine/Implementations/Spine36.cs
new file mode 100644
index 0000000..09bb936
--- /dev/null
+++ b/SpineViewer/src/Spine/Implementations/Spine36.cs
@@ -0,0 +1,323 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using SpineRuntime36;
+
+namespace SpineViewer.Spine.Implementations
+{
+ [SpineImplementation(Version.V36)]
+ internal class Spine36 : Spine
+ {
+ private class TextureLoader : SpineRuntime36.TextureLoader
+ {
+ public void Load(AtlasPage page, string path)
+ {
+ var texture = new SFML.Graphics.Texture(path);
+ if (page.magFilter == TextureFilter.Linear)
+ texture.Smooth = true;
+ if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
+ texture.Repeated = true;
+
+ page.rendererObject = texture;
+ page.width = (int)texture.Size.X;
+ page.height = (int)texture.Size.Y;
+ }
+
+ public void Unload(object texture)
+ {
+ ((SFML.Graphics.Texture)texture).Dispose();
+ }
+ }
+ private static TextureLoader textureLoader = new();
+
+ private Atlas atlas;
+ private SkeletonBinary? skeletonBinary;
+ private SkeletonJson? skeletonJson;
+ private SkeletonData skeletonData;
+ private AnimationStateData animationStateData;
+
+ private Skeleton skeleton;
+ private AnimationState animationState;
+
+ private SkeletonClipping clipping = new();
+
+ public Spine36(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
+ {
+ atlas = new Atlas(AtlasPath, textureLoader);
+ if (Path.GetExtension(SkelPath) == ".skel")
+ {
+ skeletonJson = null;
+ skeletonBinary = new SkeletonBinary(atlas);
+ skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
+ }
+ else if (Path.GetExtension(SkelPath) == ".json")
+ {
+ skeletonBinary = null;
+ skeletonJson = new SkeletonJson(atlas);
+ skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
+ }
+ else
+ {
+ throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
+ }
+ animationStateData = new AnimationStateData(skeletonData);
+ skeleton = new Skeleton(skeletonData);
+ animationState = new AnimationState(animationStateData);
+
+ foreach (var anime in skeletonData.Animations)
+ animationNames.Add(anime.Name);
+ CurrentAnimation = DefaultAnimationName;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ atlas.Dispose();
+ }
+
+ public override float Scale
+ {
+ get
+ {
+ if (skeletonBinary is not null)
+ return skeletonBinary.Scale;
+ else if (skeletonJson is not null)
+ return skeletonJson.Scale;
+ else
+ return 1f;
+ }
+ set
+ {
+ // 保存状态
+ var position = Position;
+ var flipX = FlipX;
+ var flipY = FlipY;
+ var savedTrack0 = animationState.GetCurrent(0);
+
+ var val = Math.Max(value, SCALE_MIN);
+ if (skeletonBinary is not null)
+ {
+ skeletonBinary.Scale = val;
+ skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
+ }
+ else if (skeletonJson is not null)
+ {
+ skeletonJson.Scale = val;
+ skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
+ }
+
+ // reload skel-dependent data
+ animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
+ skeleton = new Skeleton(skeletonData);
+ animationState = new AnimationState(animationStateData);
+
+ // 恢复状态
+ Position = position;
+ FlipX = flipX;
+ FlipY = flipY;
+
+ // 恢复原本 Track0 上所有动画
+ if (savedTrack0 is not null)
+ {
+ var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
+ entry.TrackTime = savedTrack0.TrackTime;
+ var savedEntry = savedTrack0.Next;
+ while (savedEntry is not null)
+ {
+ entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
+ entry.TrackTime = savedEntry.TrackTime;
+ savedEntry = savedEntry.Next;
+ }
+ }
+ }
+ }
+
+ public override PointF Position
+ {
+ get => new(skeleton.X, skeleton.Y);
+ set
+ {
+ skeleton.X = value.X;
+ skeleton.Y = value.Y;
+ }
+ }
+
+ public override bool FlipX
+ {
+ get => skeleton.FlipX;
+ set => skeleton.FlipX = value;
+ }
+
+ public override bool FlipY
+ {
+ get => skeleton.FlipY;
+ set => skeleton.FlipY = value;
+ }
+
+ public override string CurrentAnimation
+ {
+ get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
+ set { if (animationNames.Contains(value)) animationState.SetAnimation(0, value, true); }
+ }
+
+ public override RectangleF Bounds
+ {
+ get
+ {
+ float[] _ = [];
+ skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
+ return new RectangleF(x, y, w, h);
+ }
+ }
+
+ public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
+
+ public override void Update(float delta)
+ {
+ skeleton.Update(delta);
+ animationState.Update(delta);
+ animationState.Apply(skeleton);
+ skeleton.UpdateWorldTransform();
+ }
+
+ private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime36.BlendMode spineBlendMode)
+ {
+ return spineBlendMode switch
+ {
+ SpineRuntime36.BlendMode.Normal => BlendMode.Normal,
+ SpineRuntime36.BlendMode.Additive => BlendMode.Additive,
+ SpineRuntime36.BlendMode.Multiply => BlendMode.Multiply,
+ SpineRuntime36.BlendMode.Screen => BlendMode.Screen,
+ _ => throw new NotImplementedException($"{spineBlendMode}"),
+ };
+ }
+
+ public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
+ {
+ vertexArray.Clear();
+ states.Texture = null;
+
+ // 要用 DrawOrder 而不是 Slots
+ foreach (var slot in skeleton.DrawOrder)
+ {
+ var attachment = slot.Attachment;
+
+ SFML.Graphics.Texture texture;
+
+ float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
+ int worldVerticesCount; // 等于顶点数组的长度除以 2
+ int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
+ int worldTriangleIndicesLength; // 三角形索引数组长度
+ float[] uvs; // 纹理坐标
+ float tintR = skeleton.R * slot.R;
+ float tintG = skeleton.G * slot.G;
+ float tintB = skeleton.B * slot.B;
+ float tintA = skeleton.A * slot.A;
+
+ if (attachment is RegionAttachment regionAttachment)
+ {
+ texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
+
+ regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
+ worldVerticesCount = 4;
+ worldTriangleIndices = [0, 1, 2, 2, 3, 0];
+ worldTriangleIndicesLength = 6;
+ uvs = regionAttachment.UVs;
+ tintR *= regionAttachment.R;
+ tintG *= regionAttachment.G;
+ tintB *= regionAttachment.B;
+ tintA *= regionAttachment.A;
+ }
+ else if (attachment is MeshAttachment meshAttachment)
+ {
+ texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
+
+ if (meshAttachment.WorldVerticesLength > worldVertices.Length)
+ worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
+ meshAttachment.ComputeWorldVertices(slot, worldVertices);
+ worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
+ worldTriangleIndices = meshAttachment.Triangles;
+ worldTriangleIndicesLength = meshAttachment.Triangles.Length;
+ uvs = meshAttachment.UVs;
+ tintR *= meshAttachment.R;
+ tintG *= meshAttachment.G;
+ tintB *= meshAttachment.B;
+ tintA *= meshAttachment.A;
+ }
+ else if (attachment is ClippingAttachment clippingAttachment)
+ {
+ clipping.ClipStart(slot, clippingAttachment);
+ continue;
+ }
+ else
+ {
+ clipping.ClipEnd(slot);
+ continue;
+ }
+
+ SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
+
+ states.Texture ??= texture;
+ if (states.BlendMode != blendMode || states.Texture != texture)
+ {
+ if (vertexArray.VertexCount > 0)
+ {
+ if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
+ states.Shader = FragmentShader;
+ else
+ states.Shader = null;
+ target.Draw(vertexArray, states);
+ vertexArray.Clear();
+ }
+ states.BlendMode = blendMode;
+ states.Texture = texture;
+ }
+
+ if (clipping.IsClipping)
+ {
+ // 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
+ clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
+ worldVertices = clipping.ClippedVertices.Items;
+ worldVerticesCount = clipping.ClippedVertices.Count / 2;
+ worldTriangleIndices = clipping.ClippedTriangles.Items;
+ worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
+ uvs = clipping.ClippedUVs.Items;
+ }
+
+ var textureSizeX = texture.Size.X;
+ var textureSizeY = texture.Size.Y;
+
+ SFML.Graphics.Vertex vertex = new();
+ vertex.Color.R = (byte)(tintR * 255);
+ vertex.Color.G = (byte)(tintG * 255);
+ vertex.Color.B = (byte)(tintB * 255);
+ vertex.Color.A = (byte)(tintA * 255);
+
+ // 必须用 worldTriangleIndicesLength 不能直接 foreach
+ for (int i = 0; i < worldTriangleIndicesLength; i++)
+ {
+ var index = worldTriangleIndices[i] * 2;
+ vertex.Position.X = worldVertices[index];
+ vertex.Position.Y = worldVertices[index + 1];
+ vertex.TexCoords.X = uvs[index] * textureSizeX;
+ vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
+ vertexArray.Append(vertex);
+ }
+
+ clipping.ClipEnd(slot);
+ }
+
+ if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
+ states.Shader = FragmentShader;
+ else
+ states.Shader = null;
+ target.Draw(vertexArray, states);
+ clipping.ClipEnd();
+ }
+ }
+}
diff --git a/SpineViewer/src/Spine/Implementations/Spine38.cs b/SpineViewer/src/Spine/Implementations/Spine38.cs
new file mode 100644
index 0000000..1b02291
--- /dev/null
+++ b/SpineViewer/src/Spine/Implementations/Spine38.cs
@@ -0,0 +1,335 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using SpineRuntime38;
+using SpineViewer.Spine;
+
+namespace SpineViewer.Spine.Implementations
+{
+ [SpineImplementation(Version.V38)]
+ internal class Spine38 : Spine
+ {
+ private class TextureLoader : SpineRuntime38.TextureLoader
+ {
+ public void Load(AtlasPage page, string path)
+ {
+ var texture = new SFML.Graphics.Texture(path);
+ if (page.magFilter == TextureFilter.Linear)
+ texture.Smooth = true;
+ if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat)
+ texture.Repeated = true;
+
+ page.rendererObject = texture;
+ page.width = (int)texture.Size.X;
+ page.height = (int)texture.Size.Y;
+ }
+
+ public void Unload(object texture)
+ {
+ ((SFML.Graphics.Texture)texture).Dispose();
+ }
+ }
+
+ private static TextureLoader textureLoader = new();
+
+ private Atlas atlas;
+ private SkeletonBinary? skeletonBinary;
+ private SkeletonJson? skeletonJson;
+ private SkeletonData skeletonData;
+ private AnimationStateData animationStateData;
+
+ private Skeleton skeleton;
+ private AnimationState animationState;
+
+ private SkeletonClipping clipping = new();
+
+ public Spine38(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath)
+ {
+ atlas = new Atlas(AtlasPath, textureLoader);
+ if (Path.GetExtension(SkelPath) == ".skel")
+ {
+ skeletonJson = null;
+ skeletonBinary = new SkeletonBinary(atlas);
+ skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
+ }
+ else if (Path.GetExtension(SkelPath) == ".json")
+ {
+ skeletonBinary = null;
+ skeletonJson = new SkeletonJson(atlas);
+ skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
+ }
+ else
+ {
+ throw new ArgumentException($"Unknown skeleton file format {SkelPath}");
+ }
+ animationStateData = new AnimationStateData(skeletonData);
+
+ skeleton = new Skeleton(skeletonData);
+ animationState = new AnimationState(animationStateData);
+
+ foreach (var anime in skeletonData.Animations)
+ animationNames.Add(anime.Name);
+
+ CurrentAnimation = DefaultAnimationName;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ atlas.Dispose();
+ }
+
+ public override float Scale
+ {
+ get
+ {
+ if (skeletonBinary is not null)
+ return skeletonBinary.Scale;
+ else if (skeletonJson is not null)
+ return skeletonJson.Scale;
+ else
+ return 1f;
+ }
+ set
+ {
+ // 保存状态
+ var position = Position;
+ var flipX = FlipX;
+ var flipY = FlipY;
+ var savedTrack0 = animationState.GetCurrent(0);
+
+ var val = Math.Max(value, SCALE_MIN);
+ if (skeletonBinary is not null)
+ {
+ skeletonBinary.Scale = val;
+ skeletonData = skeletonBinary.ReadSkeletonData(SkelPath);
+ }
+ else if (skeletonJson is not null)
+ {
+ skeletonJson.Scale = val;
+ skeletonData = skeletonJson.ReadSkeletonData(SkelPath);
+ }
+
+ // reload skel-dependent data
+ animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix };
+ skeleton = new Skeleton(skeletonData);
+ animationState = new AnimationState(animationStateData);
+
+ // 恢复状态
+ Position = position;
+ FlipX = flipX;
+ FlipY = flipY;
+
+ // 恢复原本 Track0 上所有动画
+ if (savedTrack0 is not null)
+ {
+ var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true);
+ entry.TrackTime = savedTrack0.TrackTime;
+ var savedEntry = savedTrack0.Next;
+ while (savedEntry is not null)
+ {
+ entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0);
+ entry.TrackTime = savedEntry.TrackTime;
+ savedEntry = savedEntry.Next;
+ }
+ }
+ }
+ }
+
+ public override PointF Position
+ {
+ get => new(skeleton.X, skeleton.Y);
+ set
+ {
+ skeleton.X = value.X;
+ skeleton.Y = value.Y;
+ }
+ }
+
+ public override bool FlipX
+ {
+ get => skeleton.ScaleX < 0;
+ set
+ {
+ if (skeleton.ScaleX > 0 && value || skeleton.ScaleX < 0 && !value)
+ skeleton.ScaleX *= -1;
+ }
+ }
+
+ public override bool FlipY
+ {
+ get => skeleton.ScaleY < 0;
+ set
+ {
+ if (skeleton.ScaleY > 0 && value || skeleton.ScaleY < 0 && !value)
+ skeleton.ScaleY *= -1;
+ }
+ }
+
+ public override string CurrentAnimation
+ {
+ get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName;
+ set { if (animationNames.Contains(value)) animationState.SetAnimation(0, value, true); }
+ }
+
+ public override RectangleF Bounds
+ {
+ get
+ {
+ float[] _ = [];
+ skeleton.GetBounds(out var x, out var y, out var w, out var h, ref _);
+ return new RectangleF(x, y, w, h);
+ }
+ }
+
+ public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; }
+
+ public override void Update(float delta)
+ {
+ skeleton.Update(delta);
+ animationState.Update(delta);
+ animationState.Apply(skeleton);
+ skeleton.UpdateWorldTransform();
+ }
+
+ private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime38.BlendMode spineBlendMode)
+ {
+ return spineBlendMode switch
+ {
+ SpineRuntime38.BlendMode.Normal => BlendMode.Normal,
+ SpineRuntime38.BlendMode.Additive => BlendMode.Additive,
+ SpineRuntime38.BlendMode.Multiply => BlendMode.Multiply,
+ SpineRuntime38.BlendMode.Screen => BlendMode.Screen,
+ _ => throw new NotImplementedException($"{spineBlendMode}"),
+ };
+ }
+
+ public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states)
+ {
+ vertexArray.Clear();
+ states.Texture = null;
+
+ // 要用 DrawOrder 而不是 Slots
+ foreach (var slot in skeleton.DrawOrder)
+ {
+ var attachment = slot.Attachment;
+
+ SFML.Graphics.Texture texture;
+
+ float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值
+ int worldVerticesCount; // 等于顶点数组的长度除以 2
+ int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1
+ int worldTriangleIndicesLength; // 三角形索引数组长度
+ float[] uvs; // 纹理坐标
+ float tintR = skeleton.R * slot.R;
+ float tintG = skeleton.G * slot.G;
+ float tintB = skeleton.B * slot.B;
+ float tintA = skeleton.A * slot.A;
+
+ if (attachment is RegionAttachment regionAttachment)
+ {
+ texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject;
+
+ regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices, 0);
+ worldVerticesCount = 4;
+ worldTriangleIndices = [0, 1, 2, 2, 3, 0];
+ worldTriangleIndicesLength = 6;
+ uvs = regionAttachment.UVs;
+ tintR *= regionAttachment.R;
+ tintG *= regionAttachment.G;
+ tintB *= regionAttachment.B;
+ tintA *= regionAttachment.A;
+ }
+ else if (attachment is MeshAttachment meshAttachment)
+ {
+ texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject;
+
+ if (meshAttachment.WorldVerticesLength > worldVertices.Length)
+ worldVertices = worldVerticesBuffer = new float[meshAttachment.WorldVerticesLength * 2];
+ meshAttachment.ComputeWorldVertices(slot, worldVertices);
+ worldVerticesCount = meshAttachment.WorldVerticesLength / 2;
+ worldTriangleIndices = meshAttachment.Triangles;
+ worldTriangleIndicesLength = meshAttachment.Triangles.Length;
+ uvs = meshAttachment.UVs;
+ tintR *= meshAttachment.R;
+ tintG *= meshAttachment.G;
+ tintB *= meshAttachment.B;
+ tintA *= meshAttachment.A;
+ }
+ else if (attachment is ClippingAttachment clippingAttachment)
+ {
+ clipping.ClipStart(slot, clippingAttachment);
+ continue;
+ }
+ else
+ {
+ clipping.ClipEnd(slot);
+ continue;
+ }
+
+ SFML.Graphics.BlendMode blendMode = GetSFMLBlendMode(slot.Data.BlendMode);
+
+ states.Texture ??= texture;
+ if (states.BlendMode != blendMode || states.Texture != texture)
+ {
+ if (vertexArray.VertexCount > 0)
+ {
+ // XXX: 实测不用设置 sampler2D 的值也正确
+ if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
+ states.Shader = FragmentShader;
+ else
+ states.Shader = null;
+ target.Draw(vertexArray, states);
+ vertexArray.Clear();
+ }
+ states.BlendMode = blendMode;
+ states.Texture = texture;
+ }
+
+ if (clipping.IsClipping)
+ {
+ // 这里必须单独记录 Count, 和 Items 的 Length 是不一致的
+ clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs);
+ worldVertices = clipping.ClippedVertices.Items;
+ worldVerticesCount = clipping.ClippedVertices.Count / 2;
+ worldTriangleIndices = clipping.ClippedTriangles.Items;
+ worldTriangleIndicesLength = clipping.ClippedTriangles.Count;
+ uvs = clipping.ClippedUVs.Items;
+ }
+
+ var textureSizeX = texture.Size.X;
+ var textureSizeY = texture.Size.Y;
+
+ SFML.Graphics.Vertex vertex = new();
+ vertex.Color.R = (byte)(tintR * 255);
+ vertex.Color.G = (byte)(tintG * 255);
+ vertex.Color.B = (byte)(tintB * 255);
+ vertex.Color.A = (byte)(tintA * 255);
+
+ // 必须用 worldTriangleIndicesLength 不能直接 foreach
+ for (int i = 0; i < worldTriangleIndicesLength; i++)
+ {
+ var index = worldTriangleIndices[i] * 2;
+ vertex.Position.X = worldVertices[index];
+ vertex.Position.Y = worldVertices[index + 1];
+ vertex.TexCoords.X = uvs[index] * textureSizeX;
+ vertex.TexCoords.Y = uvs[index + 1] * textureSizeY;
+ vertexArray.Append(vertex);
+ }
+
+ clipping.ClipEnd(slot);
+ }
+
+ if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive))
+ states.Shader = FragmentShader;
+ else
+ states.Shader = null;
+ target.Draw(vertexArray, states);
+ clipping.ClipEnd();
+ }
+ }
+}
diff --git a/SpineViewer/src/Spine/Manager.cs b/SpineViewer/src/Spine/Manager.cs
new file mode 100644
index 0000000..916d672
--- /dev/null
+++ b/SpineViewer/src/Spine/Manager.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.Spine
+{
+ ///
+ /// 骨骼列表管理器
+ ///
+ class Manager
+ {
+ ///
+ /// 骨骼列表
+ ///
+ public ReadOnlyCollection Spines { get => spines.AsReadOnly(); }
+ private readonly List spines = [];
+
+ private readonly ListView listView;
+
+ ///
+ /// 骨骼管理器
+ ///
+ /// 用于显示骨骼信息的列表
+ public Manager(ListView listView)
+ {
+ listView.BeginUpdate();
+ listView.Columns.Clear();
+ listView.Items.Clear();
+ listView.Columns.AddRange([
+ new() { Text = "名称", Width = 150},
+ new() { Text = "版本", Width = 150 }
+ ]);
+ listView.EndUpdate();
+ this.listView = listView;
+ }
+
+ ///
+ /// 在末尾添加一个
+ ///
+ public void Add(Spine spine)
+ {
+ spines.Add(spine);
+ listView.Items.Add(new ListViewItem([
+ Path.GetFileNameWithoutExtension(spine.SkelPath),
+ spine.Version.String()
+ ], -1)
+ { ToolTipText = spine.SkelPath });
+ }
+
+ ///
+ /// 在指定下标之前添加一个
+ ///
+ public void Insert(int index, Spine spine)
+ {
+ spines.Insert(index, spine);
+ listView.Items.Insert(index, new ListViewItem([
+ Path.GetFileNameWithoutExtension(spine.SkelPath),
+ spine.Version.String()
+ ], -1)
+ { ToolTipText = spine.SkelPath });
+ }
+
+ ///
+ /// 批量移除
+ ///
+ public void Remove(IEnumerable indices)
+ {
+ foreach (var i in indices.OrderByDescending(x => x))
+ {
+ spines.RemoveAt(i);
+ listView.Items.RemoveAt(i);
+ }
+ }
+
+ ///
+ /// 指定下标元素前移一位
+ ///
+ public void MoveUp(int index)
+ {
+ if (index > 0)
+ {
+ (spines[index - 1], spines[index]) = (spines[index], spines[index - 1]);
+ var item = listView.Items[index];
+ listView.Items.RemoveAt(index);
+ listView.Items.Insert(index - 1, item);
+ }
+ }
+
+ ///
+ /// 指定下标元素后移一位
+ ///
+ public void MoveDown(int index)
+ {
+ if (index < spines.Count - 1)
+ {
+ (spines[index], spines[index + 1]) = (spines[index + 1], spines[index]);
+ var item = listView.Items[index + 1];
+ listView.Items.RemoveAt(index + 1);
+ listView.Items.Insert(index, item);
+ }
+ }
+
+ ///
+ /// 全部移除
+ ///
+ public void Clear()
+ {
+ spines.Clear();
+ listView.Clear();
+ }
+ }
+}
diff --git a/SpineViewer/src/Spine/Previewer.cs b/SpineViewer/src/Spine/Previewer.cs
new file mode 100644
index 0000000..eebca5f
--- /dev/null
+++ b/SpineViewer/src/Spine/Previewer.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.Spine
+{
+ ///
+ /// 骨骼预览器
+ ///
+ class Previewer
+ {
+ private readonly Panel panel;
+
+ public Previewer(Panel panel)
+ {
+ this.panel = panel;
+ }
+ }
+}
diff --git a/SpineViewer/src/Spine/Spine.cs b/SpineViewer/src/Spine/Spine.cs
new file mode 100644
index 0000000..a9b2d38
--- /dev/null
+++ b/SpineViewer/src/Spine/Spine.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Text.RegularExpressions;
+using System.Numerics;
+using System.Collections;
+using System.Collections.ObjectModel;
+using SFML.System;
+using SFML.Window;
+using System.ComponentModel;
+using System.Reflection;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace SpineViewer.Spine
+{
+
+
+ ///
+ /// Spine 实现类标记
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ public class SpineImplementationAttribute : Attribute
+ {
+ public Version Version { get; }
+
+ public SpineImplementationAttribute(Version version)
+ {
+ Version = version;
+ }
+ }
+
+ ///
+ /// Spine 基类, 使用静态方法 New 来创建具体版本对象
+ ///
+ public abstract class Spine : SFML.Graphics.Drawable, IDisposable
+ {
+ ///
+ /// 实现类缓存
+ ///
+ private static readonly Dictionary ImplementationTypes = [];
+
+ ///
+ /// 用于解决 PMA 和渐变动画问题的片段着色器
+ ///
+ private const string FRAGMENT_SHADER = (
+ "uniform sampler2D t;" +
+ "void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
+ "if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
+ "gl_FragColor = gl_Color * p; }"
+ );
+
+ ///
+ /// 用于解决 PMA 和渐变动画问题的片段着色器
+ ///
+ protected static readonly SFML.Graphics.Shader? FragmentShader = null;
+
+ ///
+ /// 静态构造函数
+ ///
+ static Spine()
+ {
+ // 遍历并缓存标记了 SpineImplementationAttribute 的类型
+ var impTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => typeof(Spine).IsAssignableFrom(t) && !t.IsAbstract);
+ foreach (var type in impTypes)
+ {
+ var attr = type.GetCustomAttribute();
+ if (attr is not null)
+ {
+ ImplementationTypes[attr.Version] = type;
+ }
+ }
+ Program.Logger.Debug($"Find Spine implementations: [{string.Join(", ", ImplementationTypes.Keys)}]");
+
+ // 加载 FragmentShader
+ try
+ {
+ FragmentShader = SFML.Graphics.Shader.FromString(null, null, FRAGMENT_SHADER);
+ }
+ catch (Exception ex)
+ {
+ Program.Logger.Error(ex.ToString());
+ Program.Logger.Error("Failed to load fragment shader");
+ FragmentShader = null;
+ }
+ }
+
+ ///
+ /// 创建特定版本的 Spine
+ ///
+ public static Spine New(Version version, string skelPath, string? atlasPath = null)
+ {
+ if (!ImplementationTypes.TryGetValue(version, out var spineType))
+ {
+ throw new NotImplementedException($"Not implemented version: {version}");
+ }
+ return (Spine)Activator.CreateInstance(spineType, skelPath, atlasPath);
+ }
+
+ ///
+ /// 构造函数
+ ///
+ public Spine(string skelPath, string? atlasPath = null)
+ {
+ // 获取子类类型
+ var type = GetType();
+ var attr = type.GetCustomAttribute();
+ if (attr is null)
+ {
+ throw new InvalidOperationException($"Class {type.Name} has no SpineImplementationAttribute.");
+ }
+
+ atlasPath ??= Path.ChangeExtension(skelPath, ".atlas");
+
+ // 设置 Version
+ Version = attr.Version;
+ SkelPath = Path.GetFullPath(skelPath);
+ AtlasPath = Path.GetFullPath(atlasPath);
+ Name = Path.GetFileNameWithoutExtension(skelPath);
+ }
+
+ ~Spine() { Dispose(false); }
+ public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
+ protected virtual void Dispose(bool disposing) { }
+
+ ///
+ /// 缩放最小值
+ ///
+ [Browsable(false)]
+ public const float SCALE_MIN = 0.001f;
+
+ ///
+ /// 获取所属版本
+ ///
+ [TypeConverter(typeof(VersionTypeConverter))]
+ [Browsable(true), Category("基本信息"), DisplayName("运行时版本")]
+ public Version Version { get; }
+
+ ///
+ /// skel 文件完整路径
+ ///
+ [Browsable(true), Category("基本信息"), DisplayName("skel文件路径")]
+ public string SkelPath { get; }
+
+ ///
+ /// atlas 文件完整路径
+ ///
+ [Browsable(true), Category("基本信息"), DisplayName("atlas文件路径")]
+ public string AtlasPath { get; }
+
+ [Browsable(true), Category("基本信息"), DisplayName("名称")]
+ public string Name { get; }
+
+ ///
+ /// 缩放比例
+ ///
+ [Browsable(true), Category("空间变换"), DisplayName("缩放比例")]
+ public abstract float Scale { get; set; }
+
+ ///
+ /// 位置
+ ///
+ [TypeConverter(typeof(PointFTypeConverter))]
+ [Browsable(true), Category("空间变换"), DisplayName("位置")]
+ public abstract PointF Position { get; set; }
+
+ ///
+ /// 水平翻转
+ ///
+ [Browsable(true), Category("空间变换"), DisplayName("水平翻转")]
+ public abstract bool FlipX { get; set; }
+
+ ///
+ /// 垂直翻转
+ ///
+ [Browsable(true), Category("空间变换"), DisplayName("垂直翻转")]
+ public abstract bool FlipY { get; set; }
+
+ ///
+ /// 是否使用预乘Alpha
+ ///
+ [Browsable(true), Category("其他"), DisplayName("预乘Alpha通道")]
+ public bool UsePremultipliedAlpha { get; set; }
+
+ ///
+ /// 包含的所有动画名称
+ ///
+ [Browsable(false)]
+ public ReadOnlyCollection AnimationNames { get => animationNames.AsReadOnly(); }
+ protected List animationNames = [];
+
+ ///
+ /// 默认动画名称
+ ///
+ [Browsable(false)]
+ public string DefaultAnimationName { get => animationNames.Last(); }
+
+ ///
+ /// 当前动画名称
+ ///
+ [TypeConverter(typeof(AnimationTypeConverter))]
+ [Browsable(true), Category("其他"), DisplayName("当前播放动画"), PropertyTab()]
+ public abstract string CurrentAnimation { get; set; }
+
+ ///
+ /// 骨骼包围盒
+ ///
+ [Browsable(false)]
+ public abstract RectangleF Bounds { get; }
+
+ ///
+ /// 获取动画时长, 如果动画不存在则返回 0
+ ///
+ public abstract float GetAnimationDuration(string name);
+
+ ///
+ /// 更新内部状态
+ ///
+ /// 时间间隔
+ public abstract void Update(float delta);
+
+ ///
+ /// 顶点坐标缓冲区
+ ///
+ protected float[] worldVerticesBuffer = new float[1024];
+
+ ///
+ /// 顶点缓冲区
+ ///
+ protected SFML.Graphics.VertexArray vertexArray = new(SFML.Graphics.PrimitiveType.Triangles);
+
+ ///
+ /// SFML.Graphics.Drawable 接口实现
+ ///
+ public abstract void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states);
+ }
+}
diff --git a/SpineViewer/src/Spine/TypeConverter.cs b/SpineViewer/src/Spine/TypeConverter.cs
new file mode 100644
index 0000000..b1b7480
--- /dev/null
+++ b/SpineViewer/src/Spine/TypeConverter.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.Spine
+{
+ public class VersionTypeConverter : EnumConverter
+ {
+ public VersionTypeConverter() : base(typeof(Version)) { }
+
+ public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType)
+ {
+ if (destinationType == typeof(string) && value is Version version)
+ {
+ // 调用自定义的 String() 方法
+ return version.String();
+ }
+
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+ }
+
+ public class PointFTypeConverter : ExpandableObjectConverter
+ {
+ public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
+ {
+ return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
+ }
+
+ public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
+ {
+ if (destinationType == typeof(string) && value is PointF point)
+ {
+ return $"{point.X}, {point.Y}";
+ }
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+
+ public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
+ {
+ return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
+ }
+
+ public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
+ {
+ if (value is string str)
+ {
+ var parts = str.Split(',');
+ if (parts.Length == 2 &&
+ float.TryParse(parts[0], out var x) &&
+ float.TryParse(parts[1], out var y))
+ {
+ return new PointF(x, y);
+ }
+ }
+ return base.ConvertFrom(context, culture, value);
+ }
+
+ public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
+ {
+ return TypeDescriptor.GetProperties(typeof(PointF), attributes);
+ }
+
+ public override bool GetPropertiesSupported(ITypeDescriptorContext? context) => true;
+ }
+
+ public class AnimationTypeConverter : StringConverter
+ {
+ public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
+ {
+ // 支持标准值列表
+ return true;
+ }
+
+ public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
+ {
+ // 排他模式,只有下拉列表中的值可选
+ return true;
+ }
+
+ public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
+ {
+ if (context?.Instance is Spine obj)
+ {
+ // 返回 AnimationNames 作为下拉选项
+ return new StandardValuesCollection(obj.AnimationNames);
+ }
+
+ return base.GetStandardValues(context);
+ }
+ }
+}
diff --git a/SpineViewer/src/Spine/Version.cs b/SpineViewer/src/Spine/Version.cs
new file mode 100644
index 0000000..989e106
--- /dev/null
+++ b/SpineViewer/src/Spine/Version.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SpineViewer.Spine
+{
+ public static class VersionHelper
+ {
+ ///
+ /// 描述缓存
+ ///
+ public static readonly Dictionary Versions = [];
+
+ static VersionHelper()
+ {
+ // 初始化缓存
+ foreach (var value in Enum.GetValues(typeof(Version)))
+ {
+ var field = typeof(Version).GetField(value.ToString());
+ var attribute = field?.GetCustomAttribute();
+ Versions[(Version)value] = attribute?.Description ?? value.ToString();
+ }
+ }
+
+ ///
+ /// 版本号字符串
+ ///
+ public static string String(this Version version)
+ {
+ return Versions.TryGetValue(version, out var description) ? description : version.ToString();
+ }
+ }
+
+ ///
+ /// 支持的 Spine 版本
+ ///
+ public enum Version
+ {
+ [Description("v3.6.x")] V36 = 0x0306,
+ [Description("v3.7.x")] V37 = 0x0307,
+ [Description("v3.8.x")] V38 = 0x0308,
+ [Description("v3.9.x")] V39 = 0x0309,
+ [Description("v4.0.x")] V40 = 0x0400,
+ [Description("v4.1.x")] V41 = 0x0401,
+ [Description("v4.2.x")] V42 = 0x0402
+ }
+}