3
.github/workflows/dotnet-desktop.yml
vendored
3
.github/workflows/dotnet-desktop.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
|||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
env:
|
env:
|
||||||
PROJECT_NAME: SpineViewer
|
PROJECT_NAME: SpineViewer
|
||||||
|
PROJ_CLI_NAME: SpineViewerCLI
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -54,11 +55,13 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION"
|
dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION"
|
||||||
|
dotnet publish "$env:PROJECT_NAME\$env:PROJ_CLI_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION"
|
||||||
|
|
||||||
- name: Publish SelfContained version
|
- name: Publish SelfContained version
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
|
dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
|
||||||
|
dotnet publish "$env:PROJECT_NAME\$env:PROJ_CLI_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
|
||||||
|
|
||||||
- name: Create release directory
|
- name: Create release directory
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|||||||
41
CONTRIBUTING.md
Normal file
41
CONTRIBUTING.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# CONTRIBUTING
|
||||||
|
|
||||||
|
## 仓库分支
|
||||||
|
|
||||||
|
仓库目前包含 4 个分支:
|
||||||
|
|
||||||
|
- `main`: 默认分支, 也是项目最新版的发布用分支
|
||||||
|
- `dev/wpf`: WPF 版本开发分支
|
||||||
|
- `release/wf`: Winforms 旧版本发布分支 (已弃用, 仅进行 bug 修复)
|
||||||
|
- `dev/wf`: Winforms 旧版本开发分支 (已弃用, 仅进行 bug 修复)
|
||||||
|
|
||||||
|
仓库的每个发布分支都有对应的开发分支 `dev/*`, **在进行贡献和推送时请在开发分支上进行**, 待开发分支上审核完毕进行必要的确认 (例如版本号的更新) 后, 再从开发分支向对应的发布分支发起 pr, 合并后将会通过 Actions 进行自动生成和发布.
|
||||||
|
|
||||||
|
## 仓库结构
|
||||||
|
|
||||||
|
仓库目前包含两个可执行文件项目, 分别是:
|
||||||
|
|
||||||
|
- `SpineViewer.csproj`
|
||||||
|
- `SpineViewerCLI.csproj`
|
||||||
|
|
||||||
|
前者为仓库主要项目, 提供一个预览操作 Spine 模型文件的 UI 界面, 后者基于社区贡献进行开发, 提供一些便捷的 CLI 功能, 从而可以对模型文件进行一些批量操作.
|
||||||
|
|
||||||
|
除此之外其余项目均为一些基础功能库, 为以上两个项目提供必要的功能支持. 原则上 UI 项目和 CLI 项目二者独立互不引用, 仅引用相同的基础功能库, 以保证整个仓库的层次结构清晰便于维护.
|
||||||
|
|
||||||
|
## 如何贡献
|
||||||
|
|
||||||
|
对于一些小改动, 例如:
|
||||||
|
|
||||||
|
- 某些文件内的 bug 修复 (例如一些逻辑上的错误)
|
||||||
|
- 已有功能的扩展性增强 (例如在已有代码逻辑结构上扩充某些功能字段)
|
||||||
|
- 其他可能的对**已有功能**的修复改进
|
||||||
|
|
||||||
|
可以直接 fork 修改后向开发分支发起 pr, 经 review 无问题后可直接合并.
|
||||||
|
|
||||||
|
对于较大的改动, 例如:
|
||||||
|
|
||||||
|
- 新增某些代码文件 (例如需要添加一些全新的类)
|
||||||
|
- 添加一些全新的逻辑或者功能代码 (例如在自行车上加装发动机)
|
||||||
|
- 其他可能影响项目代码逻辑结构的改动
|
||||||
|
|
||||||
|
这些改动请先提 Issue, 进行必要性讨论, 以及确认新功能的引入方式, 请不要直接将这些可能的破坏性改动发起 pr.
|
||||||
@@ -28,6 +28,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt
|
|||||||
.editorconfig = .editorconfig
|
.editorconfig = .editorconfig
|
||||||
.gitignore = .gitignore
|
.gitignore = .gitignore
|
||||||
CHANGELOG.md = CHANGELOG.md
|
CHANGELOG.md = CHANGELOG.md
|
||||||
|
CONTRIBUTING.md = CONTRIBUTING.md
|
||||||
README.en.md = README.en.md
|
README.en.md = README.en.md
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
@@ -36,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SFMLRenderer", "SFMLRendere
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLog.Windows.Wpf", "NLog.Windows.Wpf\NLog.Windows.Wpf.csproj", "{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLog.Windows.Wpf", "NLog.Windows.Wpf\NLog.Windows.Wpf.csproj", "{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineViewerCLI", "SpineViewerCLI\SpineViewerCLI.csproj", "{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|x64 = Debug|x64
|
Debug|x64 = Debug|x64
|
||||||
@@ -86,6 +89,10 @@ Global
|
|||||||
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Debug|x64.Build.0 = Debug|x64
|
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Debug|x64.Build.0 = Debug|x64
|
||||||
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Release|x64.ActiveCfg = Release|x64
|
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Release|x64.ActiveCfg = Release|x64
|
||||||
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Release|x64.Build.0 = Release|x64
|
{8EAB9780-9DBA-A755-6C73-0CE5AC5CE557}.Release|x64.Build.0 = Release|x64
|
||||||
|
{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{6BC146FC-F81E-65DA-EDDF-5734DBCCB628}.Release|x64.Build.0 = Release|x64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
<Version>0.15.6</Version>
|
<Version>0.15.7</Version>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
var output = Path.Combine(_outputDir!, filename);
|
var output = Path.Combine(_outputDir!, filename);
|
||||||
|
|
||||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||||
exporter.Duration = _duration >= 0 ? _duration : spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
|
||||||
|
// 如果时长是一个负数值则使用所有动画时长的最大值
|
||||||
|
exporter.Duration = _duration < 0 ? spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max() : _duration;
|
||||||
|
|
||||||
exporter.ProgressReporter = (total, done, text) =>
|
exporter.ProgressReporter = (total, done, text) =>
|
||||||
{
|
{
|
||||||
@@ -119,12 +121,7 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
{
|
{
|
||||||
// 统计总帧数
|
// 统计总帧数
|
||||||
int totalFrameCount = 0;
|
int totalFrameCount = 0;
|
||||||
if (_duration > 0)
|
if (_duration < 0)
|
||||||
{
|
|
||||||
exporter.Duration = _duration;
|
|
||||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
foreach (var sp in spines)
|
foreach (var sp in spines)
|
||||||
{
|
{
|
||||||
@@ -132,6 +129,11 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
totalFrameCount += exporter.GetFrameCount();
|
totalFrameCount += exporter.GetFrameCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
exporter.Duration = _duration;
|
||||||
|
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||||
|
}
|
||||||
|
|
||||||
pr.Total = totalFrameCount;
|
pr.Total = totalFrameCount;
|
||||||
pr.Done = 0;
|
pr.Done = 0;
|
||||||
@@ -154,7 +156,9 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
||||||
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
|
||||||
|
// 如果时长是负数则需要每次都设置成动画的时长值, 否则前面统计帧数时已经设置过时长值
|
||||||
|
if (_duration < 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||||
|
|
||||||
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
|
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
|
||||||
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
|
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
var output = Path.Combine(_outputDir!, filename);
|
var output = Path.Combine(_outputDir!, filename);
|
||||||
|
|
||||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||||
exporter.Duration = _duration >= 0 ? _duration : spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
|
||||||
|
// 如果时长是一个负数值则使用所有动画时长的最大值
|
||||||
|
exporter.Duration = _duration < 0 ? spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max() : _duration;
|
||||||
|
|
||||||
exporter.ProgressReporter = (total, done, text) =>
|
exporter.ProgressReporter = (total, done, text) =>
|
||||||
{
|
{
|
||||||
@@ -110,12 +112,7 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
{
|
{
|
||||||
// 统计总帧数
|
// 统计总帧数
|
||||||
int totalFrameCount = 0;
|
int totalFrameCount = 0;
|
||||||
if (_duration > 0)
|
if (_duration < 0)
|
||||||
{
|
|
||||||
exporter.Duration = _duration;
|
|
||||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
foreach (var sp in spines)
|
foreach (var sp in spines)
|
||||||
{
|
{
|
||||||
@@ -123,6 +120,11 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
totalFrameCount += exporter.GetFrameCount();
|
totalFrameCount += exporter.GetFrameCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
exporter.Duration = _duration;
|
||||||
|
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||||
|
}
|
||||||
|
|
||||||
pr.Total = totalFrameCount;
|
pr.Total = totalFrameCount;
|
||||||
pr.Done = 0;
|
pr.Done = 0;
|
||||||
@@ -145,7 +147,9 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
||||||
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
|
||||||
|
// 如果时长是负数则需要每次都设置成动画的时长值, 否则前面统计帧数时已经设置过时长值
|
||||||
|
if (_duration < 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||||
|
|
||||||
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
|
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
|
||||||
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
|
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
var output = Path.Combine(_outputDir!, folderName);
|
var output = Path.Combine(_outputDir!, folderName);
|
||||||
|
|
||||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||||
exporter.Duration = _duration >= 0 ? _duration : spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
|
||||||
|
// 如果时长是一个负数值则使用所有动画时长的最大值
|
||||||
|
exporter.Duration = _duration < 0 ? spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max() : _duration;
|
||||||
|
|
||||||
exporter.ProgressReporter = (total, done, text) =>
|
exporter.ProgressReporter = (total, done, text) =>
|
||||||
{
|
{
|
||||||
@@ -83,12 +85,7 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
{
|
{
|
||||||
// 统计总帧数
|
// 统计总帧数
|
||||||
int totalFrameCount = 0;
|
int totalFrameCount = 0;
|
||||||
if (_duration > 0)
|
if (_duration < 0)
|
||||||
{
|
|
||||||
exporter.Duration = _duration;
|
|
||||||
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
foreach (var sp in spines)
|
foreach (var sp in spines)
|
||||||
{
|
{
|
||||||
@@ -96,6 +93,11 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
totalFrameCount += exporter.GetFrameCount();
|
totalFrameCount += exporter.GetFrameCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
exporter.Duration = _duration;
|
||||||
|
totalFrameCount = exporter.GetFrameCount() * spines.Length;
|
||||||
|
}
|
||||||
|
|
||||||
pr.Total = totalFrameCount;
|
pr.Total = totalFrameCount;
|
||||||
pr.Done = 0;
|
pr.Done = 0;
|
||||||
@@ -118,7 +120,9 @@ namespace SpineViewer.ViewModels.Exporters
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
|
||||||
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
|
||||||
|
// 如果时长是负数则需要每次都设置成动画的时长值, 否则前面统计帧数时已经设置过时长值
|
||||||
|
if (_duration < 0) exporter.Duration = sp.GetAnimationMaxDuration();
|
||||||
|
|
||||||
var folderName = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}";
|
var folderName = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}";
|
||||||
var output = Path.Combine(_outputDir ?? sp.AssetsDir, folderName);
|
var output = Path.Combine(_outputDir ?? sp.AssetsDir, folderName);
|
||||||
|
|||||||
240
SpineViewerCLI/SpineViewerCLI.cs
Normal file
240
SpineViewerCLI/SpineViewerCLI.cs
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using Spine;
|
||||||
|
using Spine.Exporters;
|
||||||
|
|
||||||
|
namespace SpineViewerCLI
|
||||||
|
{
|
||||||
|
public class CLI
|
||||||
|
{
|
||||||
|
const string USAGE = @"
|
||||||
|
usage: SpineViewerCLI.exe [--skel PATH] [--atlas PATH] [--output PATH] [--animation STR] [--pma] [--fps INT] [--loop] [--crf INT] [--width INT] [--height INT] [--centerx INT] [--centery INT] [--zoom FLOAT] [--speed FLOAT] [--color HEX] [--quiet]
|
||||||
|
|
||||||
|
options:
|
||||||
|
--skel PATH Path to the .skel file
|
||||||
|
--atlas PATH Path to the .atlas file, default searches in the skel file directory
|
||||||
|
--output PATH Output file path
|
||||||
|
--animation STR Animation name
|
||||||
|
--pma Use premultiplied alpha, default false
|
||||||
|
--fps INT Frames per second, default 24
|
||||||
|
--loop Whether to loop the animation, default false
|
||||||
|
--crf INT Constant Rate Factor i.e. video quality, from 0 (lossless) to 51 (worst), default 23
|
||||||
|
--width INT Output width, default 512
|
||||||
|
--height INT Output height, default 512
|
||||||
|
--centerx INT Center X offset, default automatically finds bounds
|
||||||
|
--centery INT Center Y offset, default automatically finds bounds
|
||||||
|
--zoom FLOAT Zoom level, default 1.0
|
||||||
|
--speed FLOAT Speed of animation, default 1.0
|
||||||
|
--color HEX Background color as a hex RGBA color, default 000000ff (opaque black)
|
||||||
|
--quiet Removes console progress log, default false
|
||||||
|
";
|
||||||
|
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
string? skelPath = null;
|
||||||
|
string? atlasPath = null;
|
||||||
|
string? output = null;
|
||||||
|
string? animation = null;
|
||||||
|
bool pma = false;
|
||||||
|
uint fps = 24;
|
||||||
|
bool loop = false;
|
||||||
|
int crf = 23;
|
||||||
|
uint? width = null;
|
||||||
|
uint? height = null;
|
||||||
|
int? centerx = null;
|
||||||
|
int? centery = null;
|
||||||
|
float zoom = 1;
|
||||||
|
float speed = 1;
|
||||||
|
Color backgroundColor = Color.Black;
|
||||||
|
bool quiet = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
switch (args[i])
|
||||||
|
{
|
||||||
|
case "--help":
|
||||||
|
Console.Write(USAGE);
|
||||||
|
Environment.Exit(0);
|
||||||
|
break;
|
||||||
|
case "--skel":
|
||||||
|
skelPath = args[++i];
|
||||||
|
break;
|
||||||
|
case "--atlas":
|
||||||
|
atlasPath = args[++i];
|
||||||
|
break;
|
||||||
|
case "--output":
|
||||||
|
output = args[++i];
|
||||||
|
break;
|
||||||
|
case "--animation":
|
||||||
|
animation = args[++i];
|
||||||
|
break;
|
||||||
|
case "--pma":
|
||||||
|
pma = true;
|
||||||
|
break;
|
||||||
|
case "--fps":
|
||||||
|
fps = uint.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--loop":
|
||||||
|
loop = true;
|
||||||
|
break;
|
||||||
|
case "--crf":
|
||||||
|
crf = int.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--width":
|
||||||
|
width = uint.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--height":
|
||||||
|
height = uint.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--centerx":
|
||||||
|
centerx = int.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--centery":
|
||||||
|
centery = int.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--zoom":
|
||||||
|
zoom = float.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--speed":
|
||||||
|
speed = float.Parse(args[++i]);
|
||||||
|
break;
|
||||||
|
case "--color":
|
||||||
|
backgroundColor = new Color(uint.Parse(args[++i], NumberStyles.HexNumber));
|
||||||
|
break;
|
||||||
|
case "--quiet":
|
||||||
|
quiet = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Console.Error.WriteLine($"Unknown argument: {args[i]}");
|
||||||
|
Environment.Exit(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(skelPath))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Missing --skel");
|
||||||
|
Environment.Exit(2);
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(output))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Missing --output");
|
||||||
|
Environment.Exit(2);
|
||||||
|
}
|
||||||
|
if (!Enum.TryParse<FFmpegVideoExporter.VideoFormat>(Path.GetExtension(output).TrimStart('.'), true, out var videoFormat))
|
||||||
|
{
|
||||||
|
var validExtensions = string.Join(", ", Enum.GetNames(typeof(FFmpegVideoExporter.VideoFormat)));
|
||||||
|
Console.Error.WriteLine($"Invalid output extension. Supported formats are: {validExtensions}");
|
||||||
|
Environment.Exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sp = new SpineObject(skelPath, atlasPath);
|
||||||
|
sp.UsePma = pma;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(animation))
|
||||||
|
{
|
||||||
|
var availableAnimations = string.Join(", ", sp.Data.Animations);
|
||||||
|
Console.Error.WriteLine($"Missing --animation. Available animations for {sp.Name}: {availableAnimations}");
|
||||||
|
Environment.Exit(2);
|
||||||
|
}
|
||||||
|
var trackEntry = sp.AnimationState.SetAnimation(0, animation, loop);
|
||||||
|
sp.Update(0);
|
||||||
|
|
||||||
|
FFmpegVideoExporter exporter;
|
||||||
|
if (width is uint w && height is uint h && centerx is int cx && centery is int cy)
|
||||||
|
{
|
||||||
|
exporter = new FFmpegVideoExporter(w, h)
|
||||||
|
{
|
||||||
|
Center = (cx, cy),
|
||||||
|
Size = (w / zoom, -h / zoom),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var bounds = GetFloatRectCanvasBounds(GetSpineObjectAnimationBounds(sp, fps), new(width ?? 512, height ?? 512));
|
||||||
|
exporter = new FFmpegVideoExporter(width ?? (uint)Math.Ceiling(bounds.Width), height ?? (uint)Math.Ceiling(bounds.Height))
|
||||||
|
{
|
||||||
|
Center = bounds.Position + bounds.Size / 2,
|
||||||
|
Size = (bounds.Width, -bounds.Height),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
exporter.Duration = trackEntry.Animation.Duration;
|
||||||
|
exporter.Fps = fps;
|
||||||
|
exporter.Format = videoFormat;
|
||||||
|
exporter.Loop = loop;
|
||||||
|
exporter.Crf = crf;
|
||||||
|
exporter.Speed = speed;
|
||||||
|
exporter.BackgroundColor = backgroundColor;
|
||||||
|
|
||||||
|
if (!quiet)
|
||||||
|
exporter.ProgressReporter = (total, done, text) => Console.Write($"\r{text}");
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
exporter.Export(output, cts.Token, sp);
|
||||||
|
|
||||||
|
if (!quiet)
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SpineObject CopySpineObject(SpineObject sp)
|
||||||
|
{
|
||||||
|
var spineObject = new SpineObject(sp, true);
|
||||||
|
foreach (var tr in sp.AnimationState.IterTracks().Where(t => t is not null))
|
||||||
|
{
|
||||||
|
var t = spineObject.AnimationState.SetAnimation(tr!.TrackIndex, tr.Animation, tr.Loop);
|
||||||
|
}
|
||||||
|
spineObject.Update(0);
|
||||||
|
return spineObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FloatRect GetSpineObjectBounds(SpineObject sp)
|
||||||
|
{
|
||||||
|
sp.Skeleton.GetBounds(out var x, out var y, out var w, out var h);
|
||||||
|
return new(x, y, Math.Max(w, 1e-6f), Math.Max(h, 1e-6f));
|
||||||
|
}
|
||||||
|
static FloatRect FloatRectUnion(FloatRect a, FloatRect b)
|
||||||
|
{
|
||||||
|
float left = Math.Min(a.Left, b.Left);
|
||||||
|
float top = Math.Min(a.Top, b.Top);
|
||||||
|
float right = Math.Max(a.Left + a.Width, b.Left + b.Width);
|
||||||
|
float bottom = Math.Max(a.Top + a.Height, b.Top + b.Height);
|
||||||
|
return new FloatRect(left, top, right - left, bottom - top);
|
||||||
|
}
|
||||||
|
static FloatRect GetSpineObjectAnimationBounds(SpineObject sp, float fps = 10)
|
||||||
|
{
|
||||||
|
sp = CopySpineObject(sp);
|
||||||
|
var bounds = GetSpineObjectBounds(sp);
|
||||||
|
var maxDuration = sp.AnimationState.IterTracks().Select(t => t?.Animation.Duration ?? 0).DefaultIfEmpty(0).Max();
|
||||||
|
sp.Update(0);
|
||||||
|
for (float tick = 0, delta = 1 / fps; tick < maxDuration; tick += delta)
|
||||||
|
{
|
||||||
|
bounds = FloatRectUnion(bounds, GetSpineObjectBounds(sp));
|
||||||
|
sp.Update(delta);
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
static FloatRect GetFloatRectCanvasBounds(FloatRect rect, Vector2u resolution)
|
||||||
|
{
|
||||||
|
float sizeW = rect.Width;
|
||||||
|
float sizeH = rect.Height;
|
||||||
|
float innerW = resolution.X;
|
||||||
|
float innerH = resolution.Y;
|
||||||
|
var scale = Math.Max(Math.Abs(sizeW / innerW), Math.Abs(sizeH / innerH));
|
||||||
|
var scaleW = scale * Math.Sign(sizeW);
|
||||||
|
var scaleH = scale * Math.Sign(sizeH);
|
||||||
|
|
||||||
|
innerW *= scaleW;
|
||||||
|
innerH *= scaleH;
|
||||||
|
|
||||||
|
var x = rect.Left - (innerW - sizeW) / 2;
|
||||||
|
var y = rect.Top - (innerH - sizeH) / 2;
|
||||||
|
var w = resolution.X * scaleW;
|
||||||
|
var h = resolution.Y * scaleH;
|
||||||
|
return new(x, y, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SpineViewerCLI/SpineViewerCLI.csproj
Normal file
23
SpineViewerCLI/SpineViewerCLI.csproj
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
|
<Version>0.0.1</Version>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<NoWarn>$(NoWarn);NETSDK1206</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SFMLRenderer\SFMLRenderer.csproj" />
|
||||||
|
<ProjectReference Include="..\Spine\Spine.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user