From dca04050b67c616d80010eb2ced5edf6f5a9eaba Mon Sep 17 00:00:00 2001 From: ww-rm Date: Wed, 26 Feb 2025 14:26:34 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 11 + .gitignore | 398 ++++++++++++ README.md | 13 + SpineViewer.sln | 64 ++ SpineViewer/Properties/Resources.Designer.cs | 63 ++ SpineViewer/Properties/Resources.resx | 120 ++++ SpineViewer/SpineViewer.csproj | 42 ++ SpineViewer/src/MainForm.Designer.cs | 572 ++++++++++++++++++ SpineViewer/src/MainForm.cs | 179 ++++++ SpineViewer/src/MainForm.resx | 135 +++++ SpineViewer/src/Program.cs | 49 ++ .../src/SkelBatchSelectForm.Designer.cs | 39 ++ SpineViewer/src/SkelBatchSelectForm.cs | 20 + SpineViewer/src/SkelBatchSelectForm.resx | 120 ++++ SpineViewer/src/SkelSelectDialog.Designer.cs | 285 +++++++++ SpineViewer/src/SkelSelectDialog.cs | 80 +++ SpineViewer/src/SkelSelectDialog.resx | 126 ++++ SpineViewer/src/Spine/BlendMode.cs | 64 ++ .../src/Spine/Implementations/Spine36.cs | 323 ++++++++++ .../src/Spine/Implementations/Spine38.cs | 335 ++++++++++ SpineViewer/src/Spine/Manager.cs | 115 ++++ SpineViewer/src/Spine/Previewer.cs | 21 + SpineViewer/src/Spine/Spine.cs | 240 ++++++++ SpineViewer/src/Spine/TypeConverter.cs | 97 +++ SpineViewer/src/Spine/Version.cs | 51 ++ 25 files changed, 3562 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 SpineViewer.sln create mode 100644 SpineViewer/Properties/Resources.Designer.cs create mode 100644 SpineViewer/Properties/Resources.resx create mode 100644 SpineViewer/SpineViewer.csproj create mode 100644 SpineViewer/src/MainForm.Designer.cs create mode 100644 SpineViewer/src/MainForm.cs create mode 100644 SpineViewer/src/MainForm.resx create mode 100644 SpineViewer/src/Program.cs create mode 100644 SpineViewer/src/SkelBatchSelectForm.Designer.cs create mode 100644 SpineViewer/src/SkelBatchSelectForm.cs create mode 100644 SpineViewer/src/SkelBatchSelectForm.resx create mode 100644 SpineViewer/src/SkelSelectDialog.Designer.cs create mode 100644 SpineViewer/src/SkelSelectDialog.cs create mode 100644 SpineViewer/src/SkelSelectDialog.resx create mode 100644 SpineViewer/src/Spine/BlendMode.cs create mode 100644 SpineViewer/src/Spine/Implementations/Spine36.cs create mode 100644 SpineViewer/src/Spine/Implementations/Spine38.cs create mode 100644 SpineViewer/src/Spine/Manager.cs create mode 100644 SpineViewer/src/Spine/Previewer.cs create mode 100644 SpineViewer/src/Spine/Spine.cs create mode 100644 SpineViewer/src/Spine/TypeConverter.cs create mode 100644 SpineViewer/src/Spine/Version.cs 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 + } +}