Compare commits

..

28 Commits

Author SHA1 Message Date
ww-rm
b34639c383 Merge pull request #48 from ww-rm/dev/wf
Release v0.12.16
2025-06-11 13:43:20 +08:00
ww-rm
c0e2bb81e5 更新至v0.12.16 2025-06-11 13:41:25 +08:00
ww-rm
55ebcc1857 合并导出时也允许自动时长 2025-06-11 13:38:55 +08:00
ww-rm
e3a201af89 Merge pull request #42 from ww-rm/dev/wf
修复工作流错误
2025-05-28 18:23:30 +08:00
ww-rm
292ede8461 修复工作流错误 2025-05-28 18:22:33 +08:00
ww-rm
112a9a1bf2 Merge pull request #41 from ww-rm/dev/wf
修复工作流错误
2025-05-28 17:29:22 +08:00
ww-rm
63ed8d7ca4 修复工作流错误 2025-05-28 17:27:55 +08:00
ww-rm
d1e33c25bc Merge pull request #40 from ww-rm/dev/wf
修复工作流错误
2025-05-28 17:15:34 +08:00
ww-rm
ce7c6f3802 修复工作流错误 2025-05-28 17:14:08 +08:00
ww-rm
0a30af0ad2 Merge pull request #39 from ww-rm/dev/wf
修复工作流错误
2025-05-28 17:11:05 +08:00
ww-rm
0b478cab18 修复工作流错误 2025-05-28 17:09:58 +08:00
ww-rm
b14849a0b1 Merge pull request #38 from ww-rm/dev/wf
Dev/wf
2025-05-28 17:00:46 +08:00
ww-rm
b7f5f24e6f Merge branch 'dev/wf' of github.com:ww-rm/SpineViewer into dev/wf 2025-05-28 16:59:51 +08:00
ww-rm
150331d2e4 修复工作流错误 2025-05-28 16:59:38 +08:00
ww-rm
dcec8797b0 Merge pull request #37 from ww-rm/dev/wf
Dev/wf
2025-05-28 16:46:12 +08:00
ww-rm
4d1aec9ed8 Merge branch 'release/wf' into dev/wf 2025-05-28 16:45:42 +08:00
ww-rm
0cb325820b 修复工作流版本提取错误 2025-05-28 16:38:00 +08:00
ww-rm
2a862b28be 更改工作流自动获取版本号 2025-05-28 16:29:29 +08:00
ww-rm
c2935f49e9 更新至v0.12.15 2025-05-28 16:29:29 +08:00
ww-rm
22043f8f38 update changelog 2025-05-28 16:29:29 +08:00
ww-rm
3020a818f0 修复附件类型枚举量的字符串大小写问题 2025-05-28 16:29:29 +08:00
ww-rm
30177e8d7f 更改工作流自动获取版本号 2025-05-28 16:28:12 +08:00
ww-rm
3ad49838be 更新至v0.12.15 2025-05-28 16:05:55 +08:00
ww-rm
3e480abd44 update changelog 2025-05-28 16:05:42 +08:00
ww-rm
49f6b28aef 修复附件类型枚举量的字符串大小写问题 2025-05-28 16:05:09 +08:00
ww-rm
b81d13b582 更新至v0.12.14 2025-05-28 09:13:03 +08:00
ww-rm
04eb3cb640 update changelog 2025-05-28 09:12:37 +08:00
ww-rm
0ac75a088a 修复curve读取错误 2025-05-28 09:11:25 +08:00
458 changed files with 76952 additions and 44225 deletions

View File

@@ -1,18 +0,0 @@
---
name: 问题报告/Bug report
about: 报告可能的程序错误/Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## 问题描述/Describe the bug
清晰完整的描述问题是什么以及如何发生的。/A clear and concise description of what the bug is.
## 复现方式(可选)/To Reproduce (Optional)
## 截图(可选)/Screenshots (Optional)**
如果有必要,提供报错时的有关截图。/If applicable, add screenshots to help explain your problem.
## 附件(可选)/Attachments (Optional)
请将会**出现问题的文件**以及**日志文件**打包成一个 ZIP 后作为附件贴在 issue 内。/Please compress the problematic files and the log files into a single ZIP archive and attach it to this issue.

View File

@@ -3,7 +3,7 @@ name: Build & Release
on:
pull_request:
branches:
- main
- release/wf
types:
- closed
@@ -13,7 +13,6 @@ jobs:
runs-on: windows-latest
env:
PROJECT_NAME: SpineViewer
PROJ_CLI_NAME: SpineViewerCLI
steps:
- name: Checkout code
@@ -31,7 +30,7 @@ jobs:
run: |
[xml]$proj = Get-Content "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj"
$VERSION_NUM = $proj.Project.PropertyGroup.Version
$VERSION_TAG = "v$VERSION_NUM".Trim()
$VERSION_TAG = "v$VERSION_NUM"
"VERSION=$VERSION_TAG" >> $env:GITHUB_ENV
- name: Check Version Tag
@@ -55,13 +54,11 @@ jobs:
shell: pwsh
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:PROJ_CLI_NAME\$env:PROJ_CLI_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION"
- name: Publish SelfContained version
shell: pwsh
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:PROJ_CLI_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
shell: pwsh

View File

@@ -1,148 +1,12 @@
# CHANGELOG
## v0.15.19
## v0.12.15
- 模型重载后选中最后一个重载模型
- 修复 3.4 版本可能的奇数顶点数组导致的越界崩溃问题
- 移除参数自动记录中的背景图片路径
- 增加测试性桌面投影功能
- 修复附件类型枚举量字符串大小写问题
## v0.15.18
## v0.12.14
- 完善窗口日志颜色标记
- 修复预览图背景颜色为透明
- 修复面板高度首次还原错误
- 增加托盘图标
- 增加可选预览背景画面和填充模式
- 增强支持的纹理格式(例如 webp
## v0.15.17
- 修改图标配色
## v0.15.16
- 修改模型添加顺序, 每次向顶层添加
- 添加模型后自动选中最近添加的模型S
- 点击预览画面或者选中项发生变化时转移焦点至列表
- 增加移除全部菜单项
- 增加单例模式和命令行文件参数
- 增加文件关联设置
## v0.15.15
- 增加报错信息
- 导入后自动选中最后一项
## v0.15.14
- 将预览画面的首选项移动至上一次状态参数中
- 增加预览画面像素的自动保存和恢复
- 增加日志启动时的版本号输出
## v0.15.13
- 增加程序布局自动存储和还原
- 增加部分预览画面首选项
## v0.15.12
- 增加单个模型和单个轨道的时间因子
- 增加单个轨道的 Alpha 混合参数
- 调整轨道清除命令至右键菜单
- 设置默认标签页为模型
- 完善导入时的报错信息
## v0.15.11
- 修复自定义导出中参数构造错误
- 增加 mov 格式及参数说明
## v0.15.10
- 增加插槽可见性参数, 允许任何情况下对插槽启用和禁用对插槽的渲染
## v0.15.9
- 添加 V34 和 V35 版本支持
## v0.15.8
- 修复渲染纹理过程中可能的 null 错误
## v0.15.7
- 合并社区 CLI 功能项目
## v0.15.6
- 修复导出单个的时长错误
- 修改默认导出背景色为不透明黑色
## v0.15.5
- 修复自定义导出时的画面错误
- 设置 mp4 像素格式为 yuv420p 避免 windows 默认播放器无法打开
- 增加预览画面和导出时的速度参数设置
- 修复一些提示文本错误
- 导出时自动将分辨率向下调整为 2 的倍数, 避免 yuv420p 格式出错
## v0.15.4
- 修复导出时可能的卡死问题
- 增加 webp 格式无损压缩参数
## v0.15.3
- 增加 skel.bytes 后缀识别
## v0.15.2
- 修复首选项文件读取为空时的提示信息
- 工作区参数增加浏览路径
## v0.15.1
- 新版本正式发布
## v0.15.0
### 项目分支变更
自 v0.15.0 开始, 该项目将全面更换至 WPF 框架, Winforms 版本将不再进行功能更新, 只进行 bug 修复.
整个项目将具有下列分支:
- `dev/wf`: Winforms 版本开发分支, 继承 v0.15.0 之前的内容.
- `dev/wpf`: WPF 版本开发分支, v0.15.0 之后的内容.
- `release/wf`: `dev/wf` 的发布分支, 用于保留旧版发布功能.
- `main`: 最新的稳定发布分支, 也就是现在的 WPF 版本发布分支.
所有的本地开发和 pr 操作均在 `dev` 子分支下进行, 确认无误后再合并到对应的发布分支进行发布.
### 项目结构变更
粗略的将一些功能模块划分为独立的库项目:
- `SpineViewer`: 项目主体, UI 和程序逻辑
- `Spine`: 对不同版本 Spine 运行时的封装库, 提供所有必需操作的统一接口
- `SFMLRenderer`: 一个 WPF 控件, 支持渲染 SFML 内容
- `SpineRuntimes/*`: 官方不同版本的运行时库, 部分版本在官方基础上有修改和扩展
- `NLog.Windows.Wpf`: NLog 在 WPF 上的扩展库 (尚未完工)
每个项目的具体内容见各自的 README 文档.
### 功能变更
目前 v0.15.0 仅为 pre-release, 功能尚未完全迁移, 有以下功能变化和预期计划:
- 完善了全屏查看功能. 快捷键 F11 可快速切换全屏/窗口模式, 并且支持全屏模式下, 鼠标移动至边缘唤出操作面板.
- 增加了浏览面板. 支持打开文件夹进行浏览, 可以对指定文件夹下所有模型生成预览图进行查看.
- 支持复制指定模型的参数, 并且可以一键应用到多个模型上, 无法应用的项会忽略.
- 导出功能进行了精简. 分为 4 种类型的导出, 且减少了参数项, 仅保留常用参数.
- 导出方式变化. 导出方式变为直接对选中项然后右键菜单进行导出, 不再受 "显示" 和 "仅渲染选中" 参数影响.
- 版本转换功能将暂时不在新版本中提供, 旧版本中已有的功能仍然可用.
- 未来将增加动态桌面功能.
- 修复 v38 文件读取的小 bug
## v0.12.13

View File

@@ -1,41 +0,0 @@
# 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.

View File

@@ -1,18 +0,0 @@
<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.15.18</Version>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NLog" Version="5.4.0" />
</ItemGroup>
</Project>

View File

@@ -1,53 +0,0 @@
using NLog;
using NLog.Conditions;
using NLog.Config;
using NLog.Layouts;
using System.Windows;
namespace NLog.Windows.Wpf
{
[NLogConfigurationItem]
public class RichTextBoxRowColoringRule
{
public static RichTextBoxRowColoringRule Default { get; private set; }
[RequiredParameter]
public ConditionExpression Condition { get; set; }
public Layout FontColor { get; set; }
public Layout BackgroundColor { get; set; }
public FontStyle FontStyle { get; set; }
public FontWeight FontWeight { get; set; }
static RichTextBoxRowColoringRule()
{
RichTextBoxRowColoringRule.Default = new RichTextBoxRowColoringRule();
}
public RichTextBoxRowColoringRule() : this(null, "Empty", "Empty", FontStyles.Normal, FontWeights.Normal) { }
public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor)
{
this.Condition = (ConditionExpression)condition;
this.FontColor = Layout.FromString(fontColor);
this.BackgroundColor = Layout.FromString(backColor);
this.FontStyle = FontStyles.Normal;
this.FontWeight = FontWeights.Normal;
}
public RichTextBoxRowColoringRule(string condition, string fontColor, string backColor, FontStyle fontStyle, FontWeight fontWeight)
{
this.Condition = (ConditionExpression)condition;
this.FontColor = Layout.FromString(fontColor);
this.BackgroundColor = Layout.FromString(backColor);
this.FontStyle = fontStyle;
this.FontWeight = fontWeight;
}
public bool CheckCondition(LogEventInfo logEvent)
{
return true.Equals(this.Condition.Evaluate(logEvent));
}
}
}

View File

@@ -1,281 +0,0 @@
using NLog;
using NLog.Common;
using NLog.Config;
using NLog.Targets;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
namespace NLog.Windows.Wpf
{
[Target("RichTextBox")]
public sealed class RichTextBoxTarget : TargetWithLayout
{
public static ReadOnlyCollection<RichTextBoxRowColoringRule> DefaultRowColoringRules { get; } = CreateDefaultColoringRules();
private static ReadOnlyCollection<RichTextBoxRowColoringRule> CreateDefaultColoringRules()
{
return new List<RichTextBoxRowColoringRule>()
{
new RichTextBoxRowColoringRule("level == LogLevel.Fatal", "White", "Red", FontStyles.Normal, FontWeights.Bold),
new RichTextBoxRowColoringRule("level == LogLevel.Error", "Red", "Empty", FontStyles.Italic, FontWeights.Bold),
new RichTextBoxRowColoringRule("level == LogLevel.Warn", "Orange", "Empty"),
new RichTextBoxRowColoringRule("level == LogLevel.Info", "Black", "Empty"),
new RichTextBoxRowColoringRule("level == LogLevel.Debug", "Gray", "Empty"),
new RichTextBoxRowColoringRule("level == LogLevel.Trace", "DarkGray", "Empty", FontStyles.Italic, FontWeights.Normal),
}.AsReadOnly();
}
public RichTextBoxTarget() { }
public string ControlName { get; set; }
public string WindowName { get; set; }
public bool UseDefaultRowColoringRules { get; set; }
public bool AutoScroll { get; set; }
public int MaxLines { get; set; }
[ArrayParameter(typeof(RichTextBoxRowColoringRule), "row-coloring")]
public IList<RichTextBoxRowColoringRule> RowColoringRules { get; } = new List<RichTextBoxRowColoringRule>();
[ArrayParameter(typeof(RichTextBoxWordColoringRule), "word-coloring")]
public IList<RichTextBoxWordColoringRule> WordColoringRules { get; } = new List<RichTextBoxWordColoringRule>();
[NLogConfigurationIgnoreProperty]
public Window TargetWindow { get; set; }
[NLogConfigurationIgnoreProperty]
public RichTextBox TargetRichTextBox { get; set; }
protected override void InitializeTarget()
{
base.InitializeTarget();
if (TargetRichTextBox != null)
return;
if (WindowName == null)
{
HandleError("WindowName should be specified for {0}.{1}", GetType().Name, Name);
return;
}
if (string.IsNullOrEmpty(ControlName))
{
HandleError("Rich text box control name must be specified for {0}.{1}", GetType().Name, Name);
return;
}
var targetWindow = Application.Current.Windows.OfType<Window>().FirstOrDefault(w => w.Name == WindowName);
if (targetWindow == null)
{
InternalLogger.Info("{0}: WindowName '{1}' not found", this, WindowName);
return;
}
var targetControl = targetWindow.FindName(ControlName) as RichTextBox;
if (targetControl == null)
{
InternalLogger.Info("{0}: WIndowName '{1}' does not contain ControlName '{2}'", this, WindowName, ControlName);
return;
}
AttachToControl(targetWindow, targetControl);
}
private static void HandleError(string message, params object[] args)
{
if (LogManager.ThrowExceptions)
{
throw new NLogConfigurationException(string.Format(message, args));
}
InternalLogger.Error(message, args);
}
private void AttachToControl(Window window, RichTextBox textboxControl)
{
InternalLogger.Info("{0}: Attaching target to textbox {1}.{2}", this, window.Name, textboxControl.Name);
DetachFromControl();
TargetWindow = window;
TargetRichTextBox = textboxControl;
}
private void DetachFromControl()
{
TargetWindow = null;
TargetRichTextBox = null;
}
protected override void CloseTarget()
{
DetachFromControl();
}
protected override void Write(LogEventInfo logEvent)
{
RichTextBox textbox = TargetRichTextBox;
if (textbox == null || textbox.Dispatcher.HasShutdownStarted || textbox.Dispatcher.HasShutdownFinished)
{
//no last logged textbox
InternalLogger.Trace("{0}: Attached Textbox is {1}, skipping logging", this, textbox == null ? "null" : "disposed");
return;
}
string logMessage = RenderLogEvent(Layout, logEvent);
RichTextBoxRowColoringRule matchingRule = FindMatchingRule(logEvent);
_ = DoSendMessageToTextbox(logMessage, matchingRule, logEvent);
}
private bool DoSendMessageToTextbox(string logMessage, RichTextBoxRowColoringRule rule, LogEventInfo logEvent)
{
RichTextBox textbox = TargetRichTextBox;
try
{
if (textbox != null && !textbox.Dispatcher.HasShutdownStarted && !textbox.Dispatcher.HasShutdownFinished)
{
if (!textbox.Dispatcher.CheckAccess())
{
textbox.Dispatcher.BeginInvoke(() => SendTheMessageToRichTextBox(textbox, logMessage, rule, logEvent));
}
else
{
SendTheMessageToRichTextBox(textbox, logMessage, rule, logEvent);
}
return true;
}
}
catch (Exception ex)
{
InternalLogger.Warn(ex, "{0}: Failed to append RichTextBox", this);
if (LogManager.ThrowExceptions)
{
throw;
}
}
return false;
}
private RichTextBoxRowColoringRule FindMatchingRule(LogEventInfo logEvent)
{
//custom rules first
if (RowColoringRules.Count > 0)
{
foreach (RichTextBoxRowColoringRule coloringRule in RowColoringRules)
{
if (coloringRule.CheckCondition(logEvent))
{
return coloringRule;
}
}
}
if (UseDefaultRowColoringRules && DefaultRowColoringRules != null)
{
foreach (RichTextBoxRowColoringRule coloringRule in DefaultRowColoringRules)
{
if (coloringRule.CheckCondition(logEvent))
{
return coloringRule;
}
}
}
return RichTextBoxRowColoringRule.Default;
}
private void SendTheMessageToRichTextBox(RichTextBox textBox, string logMessage, RichTextBoxRowColoringRule rule, LogEventInfo logEvent)
{
if (textBox == null) return;
var document = textBox.Document;
// 插入文本(带换行)
var tr = new TextRange(document.ContentEnd, document.ContentEnd)
{
Text = logMessage + Environment.NewLine
};
// 设置行级样式
var fgColor = rule.FontColor?.Render(logEvent);
var bgColor = rule.BackgroundColor?.Render(logEvent);
tr.ApplyPropertyValue(TextElement.ForegroundProperty,
string.IsNullOrEmpty(fgColor) || fgColor == "Empty"
? textBox.Foreground
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(fgColor)));
tr.ApplyPropertyValue(TextElement.BackgroundProperty,
string.IsNullOrEmpty(bgColor) || bgColor == "Empty"
? Brushes.Transparent
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(bgColor)));
tr.ApplyPropertyValue(TextElement.FontStyleProperty, rule.FontStyle);
tr.ApplyPropertyValue(TextElement.FontWeightProperty, rule.FontWeight);
// Word coloring在刚插入的范围内做匹配
if (WordColoringRules.Count > 0)
{
foreach (var wordRule in WordColoringRules)
{
var pattern = wordRule.Regex?.Render(logEvent) ?? string.Empty;
var text = wordRule.Text?.Render(logEvent) ?? string.Empty;
var wholeWords = wordRule.WholeWords.RenderValue(logEvent);
var ignoreCase = wordRule.IgnoreCase.RenderValue(logEvent);
var regex = wordRule.ResolveRegEx(pattern, text, wholeWords, ignoreCase);
var matches = regex.Matches(tr.Text);
foreach (Match match in matches)
{
// 匹配到的部分范围
var start = tr.Start.GetPositionAtOffset(match.Index, LogicalDirection.Forward);
var endPos = tr.Start.GetPositionAtOffset(match.Index + match.Length, LogicalDirection.Backward);
if (start == null || endPos == null) continue;
var wordRange = new TextRange(start, endPos);
var wordFg = wordRule.FontColor?.Render(logEvent);
var wordBg = wordRule.BackgroundColor?.Render(logEvent);
wordRange.ApplyPropertyValue(TextElement.ForegroundProperty,
string.IsNullOrEmpty(wordFg) || wordFg == "Empty"
? tr.GetPropertyValue(TextElement.ForegroundProperty)
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(wordFg)));
wordRange.ApplyPropertyValue(TextElement.BackgroundProperty,
string.IsNullOrEmpty(wordBg) || wordBg == "Empty"
? tr.GetPropertyValue(TextElement.BackgroundProperty)
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(wordBg)));
wordRange.ApplyPropertyValue(TextElement.FontStyleProperty, wordRule.FontStyle);
wordRange.ApplyPropertyValue(TextElement.FontWeightProperty, wordRule.FontWeight);
}
}
}
// 限制最大行数
if (MaxLines > 0)
{
while (document.Blocks.Count > MaxLines)
{
document.Blocks.Remove(document.Blocks.FirstBlock);
}
}
// 自动滚动到最后
if (AutoScroll)
{
textBox.ScrollToEnd();
}
}
}
}

View File

@@ -1,59 +0,0 @@
using NLog.Config;
using NLog.Layouts;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Windows;
namespace NLog.Windows.Wpf
{
[NLogConfigurationItem]
public class RichTextBoxWordColoringRule
{
public Layout Regex { get; set; }
public Layout Text { get; set; }
public Layout<bool> WholeWords { get; set; }
public Layout<bool> IgnoreCase { get; set; }
public Layout FontColor { get; set; }
public Layout BackgroundColor { get; set; }
public FontStyle FontStyle { get; set; }
public FontWeight FontWeight { get; set; }
internal Regex ResolveRegEx(string pattern, string text, bool wholeWords, bool ignoreCase)
{
if (string.IsNullOrEmpty(pattern) && text != null)
{
pattern = System.Text.RegularExpressions.Regex.Escape(text);
if (wholeWords)
pattern = "\b" + pattern + "\b";
}
RegexOptions options = RegexOptions.None;
if (ignoreCase)
options |= RegexOptions.IgnoreCase;
return new Regex(pattern, options); // RegEx-Cache
}
public RichTextBoxWordColoringRule() : this(null, "Empty", "Empty", FontStyles.Normal, FontWeights.Normal) { }
public RichTextBoxWordColoringRule(string text, string fontColor, string backgroundColor)
{
this.Text = text;
this.FontColor = Layout.FromString(fontColor);
this.BackgroundColor = Layout.FromString(backgroundColor);
this.FontStyle = FontStyles.Normal;
this.FontWeight = FontWeights.Normal;
}
public RichTextBoxWordColoringRule(string text, string textColor, string backgroundColor, FontStyle fontStyle, FontWeight fontWeight)
{
this.Text = text;
this.FontColor = Layout.FromString(textColor);
this.BackgroundColor = Layout.FromString(backgroundColor);
this.FontStyle = fontStyle;
this.FontWeight = fontWeight;
}
}
}

View File

@@ -1,140 +1,111 @@
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
[![Build and Release](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml/badge.svg)](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
[![GitHub Release](https://img.shields.io/github/v/release/ww-rm/SpineViewer?logo=github\&logoColor=959da5\&label=Release\&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases)
[![Downloads](https://img.shields.io/github/downloads/ww-rm/SpineViewer/total?logo=github\&logoColor=959da5\&label=Downloads\&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases)
[![Build and Release](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml/badge.svg)](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
[![GitHub Release](https://img.shields.io/github/v/release/ww-rm/SpineViewer?logo=github&logoColor=959da5&label=Release&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases)
[![Downloads](https://img.shields.io/github/downloads/ww-rm/SpineViewer/total?logo=github&logoColor=959da5&label=Downloads&labelColor=3f4850)](https://github.com/ww-rm/SpineViewer/releases)
[中文](README.md) | [English](README.en.md)
A simple and user-friendly Spine file viewer and exporter with multi-language support (Chinese/English/Japanese).
A *WYSIWYG* Spine file viewer & exporter.
![previewer](img/preview.webp)
## Features
* Supports multiple versions of Spine files.
* Batch open files via drag-and-drop or copy-paste.
* Batch preview functionality.
* List-based multi-skeleton viewing and render order management.
* Batch adjustment of skeleton parameters using multi-selection.
* Multi-track animation settings.
* Skin and custom slot attachment settings.
* Custom slot visibility settings.
* Debug rendering support.
* View/model/track time scale adjustment.
* Track alpha blending parameter settings.
* Fullscreen preview mode.
* Export to single frame/image sequence/animated GIF/video formats.
* Automatic resolution batch export.
* FFmpeg custom export support.
* Program parameter saving.
* File name extension association.
* Supports texture image formats other than PNG.
* ...
- Supports multiple Spine file versions
- Drag & drop or copy/paste to open files in batch
- List-based skeleton view with render layer management
- Multi-select list to batch-adjust skeleton parameters
- Multi-track animation support
- Skin / custom slot attachment configuration
- Debug rendering mode
- Fullscreen preview
- Export to single-frame image, animated GIF/WebP/AVIF, video formats
- Batch export at multiple resolutions
- Custom FFmpeg export parameters
- …and more
### Supported Spine Versions
### Spine Version Support
| Version | View & Export |
| :-----: | :------------------: |
| `2.1.x` | :white\_check\_mark: |
| `3.4.x` | :white\_check\_mark: |
| `3.5.x` | :white\_check\_mark: |
| `3.6.x` | :white\_check\_mark: |
| `3.7.x` | :white\_check\_mark: |
| `3.8.x` | :white\_check\_mark: |
| `4.0.x` | :white\_check\_mark: |
| `4.1.x` | :white\_check\_mark: |
| `4.2.x` | :white\_check\_mark: |
| `4.3.x` | |
| Version | View & Export | Format Conversion | Version Conversion |
| :------: | :-----------: | :---------------: | :----------------: |
| `2.1.x` | :white_check_mark: | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.0.x` | :white_check_mark: | | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | :white_check_mark: | |
| `4.3.x` | | | |
More versions under development \:rocket: \:rocket: \:rocket:
More versions coming soon 🚀🚀🚀
### Supported Export Formats
| Format | Use Case |
| -------------- | ----------------------------------------------------------------------------- |
| Single Frame | Generate high-resolution images of models; manually adjust the desired frame. |
| Frame Sequence | Supports PNG format with transparency and lossless compression. |
| GIF/Video | Export preview animations or common video formats. |
| Custom Export | Supports arbitrary FFmpeg parameters for custom, complex export needs. |
| Export Format | Use Case |
| --------------------- | ----------------------------------------------------------------------------------------- |
| Single Frame | Generate highresolution still images; pick any frame manually. |
| Frame Sequence (PNG) | Lossless PNG sequences with alpha channel preserved. |
| GIF / WebP / AVIF | Perfect for quick animated previews. |
| MP4 | The most widely compatible video format. |
| WebM | Browserfriendly streaming with optional transparency. |
| MKV / MOV | For those who like to tinker. |
| Custom FFmpeg Command | Use any FFmpeg arguments for complex, tailored export workflows. |
## Installation
Download the compressed package from the [Release](https://github.com/ww-rm/SpineViewer/releases) page.
The software requires the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) to run.
Alternatively, download the package with the `SelfContained` suffix for standalone execution.
For exporting GIF/MP4 and other animation/video formats, FFmpeg must be installed and added to the system environment variables. Visit the [FFmpeg Windows download page](https://ffmpeg.org/download.html#build-windows) or download the latest version directly: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
1. Go to the [Releases](https://github.com/ww-rm/SpineViewer/releases) page and download the ZIP.
2. Make sure you have the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) installed.
3. Alternatively, download the `SelfContained` ZIP, which runs standalone without any .NET prerequisites.
4. To export GIF or other video formats, install the `ffmpeg` CLI and add it to your PATH.
- Windows builds: see the [FFmpeg download page](https://ffmpeg.org/download.html#build-windows)
- Direct download: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z)
## Usage
### How to Change the Display Language
### Importing Skeletons
In the menu, go to "File" -> "Preferences..." -> "Language," select your desired language, and confirm the change.
You can import Spine skeletons in three ways:
### Basic Overview
- Drag & drop or paste skeleton files or folders onto the model list.
- Use **File > Open** to batchopen multiple skeleton files.
- Use **File > Open Single Model** to open one at a time.
The program is organized into a left-right layout:
### Adjusting Content
* **Left Panel:** Functionality panel.
* **Right Panel:** Preview display.
- Rightclick menu and keyboard shortcuts are available in the model list. You can multiselect to adjust parameters in batch.
- In the preview pane, you can also use mouse controls:
- **Leftclick & drag** to move a model; hold **Ctrl** to multiselect (synced with the list).
- **Rightclick & drag** to pan the entire scene.
- **Mouse wheel** to zoom; hold **Ctrl** to zoom all selected models proportionally.
- **“Render Selected Only”** mode shows only the selected models in preview; use the list to change selection.
The left panel includes three sub-panels:
Below the preview, playback controls let you scrub through the timeline like a basic player.
* **Browse:** Preview the content of a specified folder without importing files into the program. This panel allows generating `.webp` previews for models or importing selected models.
* **Model:** Lists imported models for rendering. Parameters and rendering order can be adjusted here, along with other model-related functionalities.
* **Display:** Adjust parameters for the right-side preview display.
### Exporting Content
Hover your mouse over buttons, labels, or input fields to see help text for most UI elements.
Exports follow the “what you see is what you get” principle—your realtime preview is exactly what gets exported.
### Skeleton Import
Key export options:
Drag-and-drop or paste skeleton files/directories into the Model panel.
- **Render Selected Only**: includes only the selected models in both preview and export.
- **Output Folder**: if unspecified, exports go into each models source folder; otherwise, everything exports to the chosen folder.
- **Export Single**: by default, each model is exported separately; enable this to render all selected models together into a single output.
- **Auto Resolution**: ignores preview resolution and viewport size—exports at the contents actual bounds; for animations, matches the full animation area.
Alternatively, use the right-click menu in the Browse panel to import selected items.
## More
### Content Adjustment
The Model panel supports right-click menus, some shortcuts, and batch adjustments of model parameters through multi-selection.
For preview display adjustments:
* **Left-click:** Select and drag models. Hold `Ctrl` for multi-selection, synchronized with the left-side list.
* **Right-click:** Drag the entire display.
* **Scroll wheel:** Zoom in/out. Hold `Ctrl` to scale selected models.
* **Render selected-only mode:** In this mode, the preview only shows selected models, and selection status can only be changed via the left-side list.
The buttons below the preview display allow time adjustments, serving as a simple playback control.
### Content Export
Export follows the **WYSIWYG (What You See Is What You Get)** principle, meaning the preview display reflects the exported output.
Use the right-click menu in the Model panel to export selected items.
Key export parameters include:
* **Output folder:** Optional. When not specified, output is saved to the respective model folder; otherwise, all output is saved to the provided folder.
* **Export single:** By default, each model is exported independently. Selecting "Export single" renders all selected models in a single frame, producing a unified output.
* **Auto resolution:** Ignores the preview resolution and viewport parameters, exporting output at the actual size of the content. For animations/videos, the output matches the size required for full visibility.
### More Information
For detailed usage and documentation, see the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). For usage questions or bug reports, submit an [Issue](https://github.com/ww-rm/SpineViewer/issues).
Detailed usage and advanced tips are in the [Wiki](https://github.com/ww-rm/SpineViewer/wiki).
Encounter a bug or have a feature request? Open an [Issue](https://github.com/ww-rm/SpineViewer/issues).
## Acknowledgements
* [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
* [SFML.Net](https://github.com/SFML/SFML.Net)
* [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
* [HandyControl](https://github.com/HandyOrg/HandyControl)
* [NLog](https://github.com/NLog/NLog)
* [SkiaSharp](https://github.com/mono/SkiaSharp)
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
- [SFML.Net](https://github.com/SFML/SFML.Net)
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
---
*If you find this project helpful, please give it a \:star: and share it with others! :)*
If you find this project useful, please give it a and share it with others!
[![Stargazers over time](https://starchart.cc/ww-rm/SpineViewer.svg?variant=adaptive)](https://starchart.cc/ww-rm/SpineViewer)

View File

@@ -6,7 +6,7 @@
[中文](README.md) | [English](README.en.md)
一个简单好用的 Spine 文件查看&导出程序, 支持中/英/日多语言界面.
*所见即所得* 的 Spine 文件查看&导出程序.
![previewer](img/preview.webp)
@@ -14,38 +14,29 @@
- 支持多版本 spine 文件
- 支持拖拽/复制粘贴批量打开文件
- 支持批量预览
- 支持列表式多骨骼查看和渲染层级管理
- 支持列表多选批量设置骨骼参数
- 支持多轨道动画设置
- 支持皮肤/自定义插槽附件设置
- 支持自定义插槽可见性
- 支持调试渲染
- 支持画面/模型/轨道时间倍速设置
- 支持设置轨道 Alpha 混合参数
- 支持全屏预览
- 支持单帧/动图/视频文件导出
- 支持自动分辨率批量导出
- 支持 FFmpeg 自定义导出
- 支持程序参数保存
- 支持文件后缀关联
- 支持非 png 格式的纹理图片格式
- ......
- ...
### Spine 版本支持
| 版本 | 查看&导出 |
| :---: | :---: |
| `2.1.x` | :white_check_mark: |
| `3.4.x` | :white_check_mark: |
| `3.5.x` | :white_check_mark: |
| `3.6.x` | :white_check_mark: |
| `3.7.x` | :white_check_mark: |
| `3.8.x` | :white_check_mark: |
| `4.0.x` | :white_check_mark: |
| `4.1.x` | :white_check_mark: |
| `4.2.x` | :white_check_mark: |
| `4.3.x` | |
| 版本 | 查看&导出 | 格式转换 | 版本转换 |
| :---: | :---: | :---: | :---: |
| `2.1.x` | :white_check_mark: | | |
| `3.6.x` | :white_check_mark: | | |
| `3.7.x` | :white_check_mark: | | |
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
| `4.0.x` | :white_check_mark: | | |
| `4.1.x` | :white_check_mark: | | |
| `4.2.x` | :white_check_mark: | :white_check_mark: | |
| `4.3.x` | | | |
更多版本正在施工 :rocket: :rocket: :rocket:
@@ -55,7 +46,10 @@
| --- | --- |
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
| 动图/视频 | 可以生成预览动图或者常见格式视频. |
| GIF/WebP/AVIF | 适合生成预览动图. |
| MP4 | 最常见的视频格式, 兼容性最好. |
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
| MKV/MOV | 适合折腾. |
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
## 安装
@@ -66,35 +60,21 @@
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
导出 GIF/MP4 等动图/视频格式需要在本地安装 ffmpeg 命令行, 并且添加至环境变量, [点击前往 FFmpeg-Windows 下载页面](https://ffmpeg.org/download.html#build-windows), 也可以点这个下载最新版本 [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
导出 GIF视频格式需要在本地安装 ffmpeg 命令行, 并且添加至环境变量, [点击前往 FFmpeg-Windows 下载页面](https://ffmpeg.org/download.html#build-windows), 也可以点这个下载最新版本 [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
## 使用方法
### 如何修改显示语言
窗口菜单的 "文件" -> "首选项..." -> "语言", 选择你需要的语言并确认修改.
### 基本介绍
程序大致是左右布局, 左侧是功能面板, 右侧是画面.
左侧有三个子面板, 分别是:
- **浏览**. 该面板用于预览指定文件夹的内容, 并没有真正导入文件到程序. 在该面板可以为模型生成 webp 格式的预览图, 或者导入选中的模型.
- **模型**. 该面板记录导入并进行渲染的模型列表, 可以在这个面板设置与模型渲染相关的参数和渲染顺序, 以及一些与模型有关的功能.
- **画面**. 该面板用于设置右侧预览画面的参数.
绝大部分按钮或者标签或者输入框都可以通过鼠标指针悬停来获取帮助文本.
### 骨骼导入
可以直接拖放/粘贴需要导入骨骼文件/目录到模型面板.
有 3 种方式导入骨骼文件:
或者在浏览面板内右键菜单导入选中项.
- 拖放/粘贴需要导入的骨骼文件/目录到模型列表
- 从文件菜单里批量打开骨骼文件
- 从文件菜单选择单个模型打开
### 内容调整
模型面板支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整.
模型列表支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整.
预览画面除了使用面板进行参数设置外, 支持部分鼠标动作:
@@ -109,10 +89,9 @@
导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面.
在模型面板里, 右键菜单可以对选中项进行导出操作.
导出有以下几个关键参数:
- 仅渲染选中. 这个参数不仅影响预览模式, 也影响导出, 如果仅渲染选中, 那么在导出时只有被选中的模型会被考虑, 忽略其他模型.
- 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹.
- 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份.
- 自动分辨率. 该模式会忽略预览画面的分辨率和视区参数, 导出产物的分辨率与被导出内容的实际大小一致, 如果是动图或者视频则会与完整显示动画的必需大小一致.
@@ -126,9 +105,6 @@
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
- [SFML.Net](https://github.com/SFML/SFML.Net)
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
- [HandyControl](https://github.com/HandyOrg/HandyControl)
- [NLog](https://github.com/NLog/NLog)
- [SkiaSharp](https://github.com/mono/SkiaSharp)
---

View File

@@ -1,138 +0,0 @@
using SFML.Graphics;
using SFML.System;
using SFML.Window;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SFMLRenderer
{
/// <summary>
/// 定义了 SFML 渲染器的基本功能和事件, 基本上是对 <see cref="RenderWindow"/> 的抽象
/// <para>实现示例可以见 <see cref="SFMLRenderPanel"/></para>
/// </summary>
public interface ISFMLRenderer
{
/// <summary>
/// 发生在资源首次创建完成后, 该事件发生之后渲染器才是可用的, 操作才会生效
/// </summary>
public event EventHandler? RendererCreated;
/// <summary>
/// 发生在资源即将不可用之前, 该事件发生之后对渲染器的操作将被忽略
/// </summary>
public event EventHandler? RendererDisposing;
public event EventHandler<MouseMoveEventArgs>? CanvasMouseMove;
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonPressed;
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonReleased;
public event EventHandler<MouseWheelScrollEventArgs>? CanvasMouseWheelScrolled;
/// <summary>
/// 分辨率, 影响画面的相对比例
/// </summary>
public Vector2u Resolution { get; set; }
/// <summary>
/// 快捷设置视区中心点
/// </summary>
public Vector2f Center { get; set; }
/// <summary>
/// 快捷设置视区缩放
/// </summary>
public float Zoom { get; set; }
/// <summary>
/// 快捷设置视区旋转
/// </summary>
public float Rotation { get; set; }
/// <summary>
/// 快捷设置视区水平翻转
/// </summary>
public bool FlipX { get; set; }
/// <summary>
/// 快捷设置视区垂直翻转
/// </summary>
public bool FlipY { get; set; }
/// <summary>
/// 最大帧率, 影响 Draw 的最大调用频率, <see cref="RenderWindow.SetFramerateLimit(uint)"/>
/// </summary>
public uint MaxFps { get; set; }
/// <summary>
/// 垂直同步, <see cref="RenderWindow.SetVerticalSyncEnabled(bool)"/>
/// </summary>
public bool VerticalSync { get; set; }
/// <summary>
/// <inheritdoc cref="RenderWindow.SetActive(bool)"/>
/// </summary>
public bool SetActive(bool active);
/// <summary>
/// <inheritdoc cref="RenderWindow.GetView"/>
/// </summary>
public View GetView();
/// <summary>
/// <inheritdoc cref="RenderWindow.SetView(View)"/>
/// </summary>
public void SetView(View view);
/// <summary>
/// <inheritdoc cref="RenderWindow.MapPixelToCoords(Vector2i)"/>
/// </summary>
public Vector2f MapPixelToCoords(Vector2i point);
/// <summary>
/// <inheritdoc cref="RenderWindow.MapCoordsToPixel(Vector2f)"/>
/// </summary>
public Vector2i MapCoordsToPixel(Vector2f point);
/// <summary>
/// <inheritdoc cref="RenderWindow.Clear()"/>
/// </summary>
public void Clear();
/// <summary>
/// <inheritdoc cref="RenderWindow.Clear(Color)"/>
/// </summary>
public void Clear(Color color);
/// <summary>
/// <inheritdoc cref="RenderWindow.Draw(Drawable)"/>
/// </summary>
public void Draw(Drawable drawable);
/// <summary>
/// <inheritdoc cref="RenderWindow.Draw(Drawable, RenderStates)"/>
/// </summary>
public void Draw(Drawable drawable, RenderStates states);
/// <summary>
/// <inheritdoc cref="RenderWindow.Draw(Vertex[], PrimitiveType)"/>
/// </summary>
public void Draw(Vertex[] vertices, PrimitiveType type);
/// <summary>
/// <inheritdoc cref="RenderWindow.Draw(Vertex[], PrimitiveType, RenderStates)"/>
/// </summary>
public void Draw(Vertex[] vertices, PrimitiveType type, RenderStates states);
/// <summary>
/// <inheritdoc cref="RenderWindow.Draw(Vertex[], uint, uint, PrimitiveType)"/>
/// </summary>
public void Draw(Vertex[] vertices, uint start, uint count, PrimitiveType type);
/// <summary>
/// <inheritdoc cref="RenderWindow.Display"/>
/// </summary>
public void Display();
}
}

View File

@@ -1,21 +0,0 @@
# SFMLRenderer
这个库封装了一个用于 WPF 的 SFML 渲染控件.
```mermaid
classDiagram
namespace SFMLRenderer {
class ISFMLRenderer {
<<Interface>>
}
class SFMLHwndHost
class SFMLRenderPanel
}
ISFMLRenderer <|.. SFMLRenderPanel
SFMLHwndHost <.. SFMLRenderPanel
```

View File

@@ -1,73 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
namespace SFMLRenderer
{
/// <summary>
/// 原生窗口控件, 不应直接使用该类, 而是使用 <see cref="SFMLRenderPanel"/> 或者二次封装
/// </summary>
public class SFMLHwndHost : HwndHost
{
private HwndSource? _hwndSource;
private SFML.Graphics.RenderWindow? _renderWindow;
/// <summary>
/// 内部的 SFML 窗口对象
/// </summary>
public SFML.Graphics.RenderWindow? RenderWindow => _renderWindow;
/// <summary>
/// 窗口建立事件
/// </summary>
public event EventHandler? RenderWindowBuilded;
/// <summary>
/// 窗口销毁事件
/// </summary>
public event EventHandler? RenderWindowDestroying;
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
var ps = new HwndSourceParameters(GetType().Name, (int)Width, (int)Height)
{
ParentWindow = hwndParent.Handle,
WindowStyle = 0x40000000 | 0x10000000, // WS_CHILD | WS_VISIBLE
HwndSourceHook = HwndMessageHook
};
_hwndSource = new HwndSource(ps);
_renderWindow = new(_hwndSource.Handle);
_renderWindow.SetActive(false);
RenderWindowBuilded?.Invoke(this, EventArgs.Empty);
return new HandleRef(this, _hwndSource.Handle);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
RenderWindowDestroying?.Invoke(this, EventArgs.Empty);
_renderWindow?.Close();
var rw = _renderWindow;
_renderWindow = null;
rw?.Dispose();
var hs = _hwndSource;
_hwndSource = null;
hs?.Dispose();
}
private IntPtr HwndMessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
_renderWindow?.DispatchEvents();
return IntPtr.Zero;
}
}
}

View File

@@ -1,14 +0,0 @@
<UserControl x:Class="SFMLRenderer.SFMLRenderPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SFMLRenderer"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<local:SFMLHwndHost x:Name="_hwndHost"
Width="100"
Height="100"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</UserControl>

View File

@@ -1,253 +0,0 @@
using SFML.Graphics;
using SFML.System;
using SFML.Window;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SFMLRenderer
{
/// <summary>
/// SFMLRenderPanel.xaml 的交互逻辑
/// </summary>
public partial class SFMLRenderPanel : System.Windows.Controls.UserControl, ISFMLRenderer
{
private RenderWindow? RenderWindow => _hwndHost.RenderWindow;
public SFMLRenderPanel()
{
InitializeComponent();
}
public event EventHandler? RendererCreated
{
add => _hwndHost.RenderWindowBuilded += value;
remove => _hwndHost.RenderWindowBuilded -= value;
}
public event EventHandler? RendererDisposing
{
add => _hwndHost.RenderWindowDestroying += value;
remove => _hwndHost.RenderWindowDestroying -= value;
}
public event EventHandler<MouseMoveEventArgs>? CanvasMouseMove
{
add { if (RenderWindow is RenderWindow w) w.MouseMoved += value; }
remove { if (RenderWindow is RenderWindow w) w.MouseMoved -= value; }
}
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonPressed
{
add { if (RenderWindow is RenderWindow w) w.MouseButtonPressed += value; }
remove { if (RenderWindow is RenderWindow w) w.MouseButtonPressed -= value; }
}
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonReleased
{
add { if (RenderWindow is RenderWindow w) w.MouseButtonReleased += value; }
remove { if (RenderWindow is RenderWindow w) w.MouseButtonReleased -= value; }
}
public event EventHandler<MouseWheelScrollEventArgs>? CanvasMouseWheelScrolled
{
add { if (RenderWindow is RenderWindow w) w.MouseWheelScrolled += value; }
remove { if (RenderWindow is RenderWindow w) w.MouseWheelScrolled -= value; }
}
public Vector2u Resolution
{
get => _resolution;
set
{
if (RenderWindow is null) return;
if (value == _resolution) return;
if (value.X <= 0 || value.Y <= 0) return;
var zoom = Zoom;
float parentW = (float)ActualWidth;
float parentH = (float)ActualHeight;
float renderW = value.X;
float renderH = value.Y;
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
renderW *= scale;
renderH *= scale;
_hwndHost.Width = renderW;
_hwndHost.Height = renderH;
_resolution = value;
// 设置完 resolution 后还原缩放比例
Zoom = zoom;
}
}
private Vector2u _resolution = new(100, 100);
public Vector2f Center
{
get
{
if (RenderWindow is null) return default;
using var view = RenderWindow.GetView();
return view.Center;
}
set
{
if (RenderWindow is null) return;
using var view = RenderWindow.GetView();
view.Center = value;
RenderWindow.SetView(view);
}
}
public float Zoom
{
get
{
if (RenderWindow is null) return 1;
using var view = RenderWindow.GetView();
return Math.Abs(_resolution.X / view.Size.X); // XXX: 仅使用宽度进行缩放计算
}
set
{
value = Math.Abs(value);
if (RenderWindow is null || value <= 0) return;
using var view = RenderWindow.GetView();
var signX = Math.Sign(view.Size.X);
var signY = Math.Sign(view.Size.Y);
view.Size = new(_resolution.X / value * signX, _resolution.Y / value * signY);
RenderWindow.SetView(view);
}
}
public float Rotation
{
get
{
if (RenderWindow is null) return default;
using var view = RenderWindow.GetView();
return view.Rotation;
}
set
{
if (RenderWindow is null) return;
using var view = RenderWindow.GetView();
view.Rotation = value;
RenderWindow.SetView(view);
}
}
public bool FlipX
{
get
{
if (RenderWindow is null) return false;
using var view = RenderWindow.GetView();
return view.Size.X < 0;
}
set
{
if (RenderWindow is null) return;
using var view = RenderWindow.GetView();
var size = view.Size;
if (size.X > 0 && value || size.X < 0 && !value)
size.X *= -1;
view.Size = size;
RenderWindow.SetView(view);
}
}
public bool FlipY
{
get
{
if (RenderWindow is null) return false;
using var view = RenderWindow.GetView();
return view.Size.Y < 0;
}
set
{
if (RenderWindow is null) return;
using var view = RenderWindow.GetView();
var size = view.Size;
if (size.Y > 0 && value || size.Y < 0 && !value)
size.Y *= -1;
view.Size = size;
RenderWindow.SetView(view);
}
}
public uint MaxFps
{
get => _maxFps;
set
{
if (RenderWindow is null) return;
RenderWindow.SetFramerateLimit(value);
_maxFps = value;
}
}
private uint _maxFps = 0;
public bool VerticalSync
{
get => _verticalSync;
set
{
if (RenderWindow is null) return;
RenderWindow.SetVerticalSyncEnabled(value);
_verticalSync = value;
}
}
private bool _verticalSync = false;
public void Clear() => RenderWindow?.Clear();
public void Clear(Color color) => RenderWindow?.Clear(color);
public void Display() => RenderWindow?.Display();
public void Draw(Drawable drawable) => RenderWindow?.Draw(drawable);
public void Draw(Drawable drawable, RenderStates states) => RenderWindow?.Draw(drawable, states);
public void Draw(Vertex[] vertices, PrimitiveType type) => RenderWindow?.Draw(vertices, type);
public void Draw(Vertex[] vertices, PrimitiveType type, RenderStates states) => RenderWindow?.Draw(vertices, type, states);
public void Draw(Vertex[] vertices, uint start, uint count, PrimitiveType type) => RenderWindow?.Draw(vertices, start, count, type);
public View GetView() => RenderWindow?.GetView() ?? new();
public Vector2i MapCoordsToPixel(Vector2f point) => RenderWindow?.MapCoordsToPixel(point) ?? default;
public Vector2f MapPixelToCoords(Vector2i point) => RenderWindow?.MapPixelToCoords(point) ?? default;
public bool SetActive(bool active) => RenderWindow?.SetActive(active) ?? false;
public void SetView(View view) => RenderWindow?.SetView(view);
protected override void OnRenderSizeChanged(System.Windows.SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
if (RenderWindow is null) return;
float parentW = (float)sizeInfo.NewSize.Width;
float parentH = (float)sizeInfo.NewSize.Height;
float renderW = (float)_hwndHost.ActualWidth;
float renderH = (float)_hwndHost.ActualHeight;
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
renderW *= scale;
renderH *= scale;
_hwndHost.Width = renderW;
_hwndHost.Height = renderH;
}
}
}

View File

@@ -1,171 +0,0 @@
using SFML.Graphics;
using SFML.System;
using SFML.Window;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace SFMLRenderer
{
public class SFMLRenderWindow : RenderWindow, ISFMLRenderer
{
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(10) };
public SFMLRenderWindow(VideoMode mode, string title, Styles style) : base(mode, title, style)
{
SetActive(false);
_timer.Tick += (s, e) => DispatchEvents();
_timer.Start();
RendererCreated?.Invoke(this, EventArgs.Empty);
}
public event EventHandler? RendererCreated;
public event EventHandler? RendererDisposing
{
add => throw new NotImplementedException();
remove => throw new NotImplementedException();
}
public event EventHandler<MouseMoveEventArgs>? CanvasMouseMove
{
add { MouseMoved += value; }
remove { MouseMoved -= value; }
}
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonPressed
{
add { MouseButtonPressed += value; }
remove { MouseButtonPressed -= value; }
}
public event EventHandler<MouseButtonEventArgs>? CanvasMouseButtonReleased
{
add { MouseButtonReleased += value; }
remove { MouseButtonReleased -= value; }
}
public event EventHandler<MouseWheelScrollEventArgs>? CanvasMouseWheelScrolled
{
add { MouseWheelScrolled += value; }
remove { MouseWheelScrolled -= value; }
}
public Vector2u Resolution
{
get => Size;
set => Size = value;
}
public Vector2f Center
{
get
{
using var view = GetView();
return view.Center;
}
set
{
using var view = GetView();
view.Center = value;
SetView(view);
}
}
public float Zoom
{
get
{
using var view = GetView();
return Math.Abs(Size.X / view.Size.X); // XXX: 仅使用宽度进行缩放计算
}
set
{
value = Math.Abs(value);
if (value <= 0) return;
using var view = GetView();
var signX = Math.Sign(view.Size.X);
var signY = Math.Sign(view.Size.Y);
var resolution = Size;
view.Size = new(resolution.X / value * signX, resolution.Y / value * signY);
SetView(view);
}
}
public float Rotation
{
get
{
using var view = GetView();
return view.Rotation;
}
set
{
using var view = GetView();
view.Rotation = value;
SetView(view);
}
}
public bool FlipX
{
get
{
using var view = GetView();
return view.Size.X < 0;
}
set
{
using var view = GetView();
var size = view.Size;
if (size.X > 0 && value || size.X < 0 && !value)
size.X *= -1;
view.Size = size;
SetView(view);
}
}
public bool FlipY
{
get
{
using var view = GetView();
return view.Size.Y < 0;
}
set
{
using var view = GetView();
var size = view.Size;
if (size.Y > 0 && value || size.Y < 0 && !value)
size.Y *= -1;
view.Size = size;
SetView(view);
}
}
public uint MaxFps
{
get => _maxFps;
set
{
SetFramerateLimit(value);
_maxFps = value;
}
}
private uint _maxFps = 0;
public bool VerticalSync
{
get => _verticalSync;
set
{
SetVerticalSyncEnabled(value);
_verticalSync = value;
}
}
private bool _verticalSync = false;
}
}

View File

@@ -1,22 +0,0 @@
<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.15.19</Version>
<UseWPF>true</UseWPF>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);NETSDK1206</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SFML.Net" Version="2.6.1" />
</ItemGroup>
</Project>

View File

@@ -1,207 +0,0 @@
using NLog;
using SFML.Graphics;
using SFML.System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Exporters
{
/// <summary>
/// 导出类基类, 提供基本的帧渲染功能
/// </summary>
public abstract class BaseExporter : IDisposable
{
/// <summary>
/// 日志器
/// </summary>
protected static readonly Logger _logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// 用于渲染的画布
/// </summary>
protected RenderTexture _renderTexture;
/// <summary>
/// 初始化导出器
/// </summary>
/// <param name="width">画布宽像素值</param>
/// <param name="height">画布高像素值</param>
public BaseExporter(uint width , uint height)
{
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
width = width >> 1 << 1;
height = height >> 1 << 1;
if (width <= 0 || height <= 0)
throw new ArgumentException($"Invalid resolution: {width}, {height}");
_renderTexture = new(width, height);
_renderTexture.SetActive(false);
}
/// <summary>
/// 初始化导出器
/// </summary>
public BaseExporter(Vector2u resolution)
{
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
resolution.X = resolution.X >> 1 << 1;
resolution.Y = resolution.Y >> 1 << 1;
if (resolution.X <= 0 || resolution.Y <= 0)
throw new ArgumentException($"Invalid resolution: {resolution}");
_renderTexture = new(resolution.X, resolution.Y);
_renderTexture.SetActive(false);
}
/// <summary>
/// 可选的进度回调函数
/// <list type="number">
/// <item><c>total</c>: 任务总量</item>
/// <item><c>done</c>: 已完成量</item>
/// <item><c>progressText</c>: 需要设置的进度提示文本</item>
/// </list>
/// </summary>
public Action<float, float, string>? ProgressReporter { get => _progressReporter; set => _progressReporter = value; }
protected Action<float, float, string>? _progressReporter;
/// <summary>
/// 背景颜色
/// </summary>
public Color BackgroundColor
{
get => _backgroundColor;
set
{
_backgroundColor = value;
var bcPma = value;
var a = bcPma.A / 255f;
bcPma.R = (byte)(bcPma.R * a);
bcPma.G = (byte)(bcPma.G * a);
bcPma.B = (byte)(bcPma.B * a);
_backgroundColorPma = bcPma;
}
}
protected Color _backgroundColor = Color.Black;
/// <summary>
/// 预乘后的背景颜色
/// </summary>
protected Color _backgroundColorPma = Color.Black;
/// <summary>
/// 画面分辨率
/// <inheritdoc cref="RenderTexture.Size"/>
/// </summary>
public Vector2u Resolution
{
get => _renderTexture.Size;
set
{
// XXX: 强制变成 2 的倍数, 防止像是 yuv420p 这种像素格式报错
value.X = value.X >> 1 << 1;
value.Y = value.Y >> 1 << 1;
if (value.X <= 0 || value.Y <= 0)
{
_logger.Warn("Omit invalid exporter resolution: {0}", value);
return;
}
if (_renderTexture.Size != value)
{
using var old = _renderTexture;
using var view = old.GetView();
var renderTexture = new RenderTexture(value.X, value.Y);
renderTexture.SetActive(false);
renderTexture.SetView(view);
_renderTexture = renderTexture;
}
}
}
/// <summary>
/// <inheritdoc cref="View.Viewport"/>
/// </summary>
public FloatRect Viewport
{
get { using var view = _renderTexture.GetView(); return view.Viewport; }
set { using var view = _renderTexture.GetView(); view.Viewport = value; _renderTexture.SetView(view); }
}
/// <summary>
/// <inheritdoc cref="View.Center"/>
/// </summary>
public Vector2f Center
{
get { using var view = _renderTexture.GetView(); return view.Center; }
set { using var view = _renderTexture.GetView(); view.Center = value; _renderTexture.SetView(view); }
}
/// <summary>
/// <inheritdoc cref="View.Size"/>
/// </summary>
public Vector2f Size
{
get { using var view = _renderTexture.GetView(); return view.Size; }
set { using var view = _renderTexture.GetView(); view.Size = value; _renderTexture.SetView(view); }
}
/// <summary>
/// <inheritdoc cref="View.Rotation"/>
/// </summary>
public float Rotation
{
get { using var view = _renderTexture.GetView(); return view.Rotation; }
set { using var view = _renderTexture.GetView(); view.Rotation = value; _renderTexture.SetView(view); }
}
/// <summary>
/// 获取的一帧, 结果是预乘的
/// </summary>
protected virtual SFMLImageVideoFrame GetFrame(SpineObject[] spines)
{
_renderTexture.SetActive(true);
_renderTexture.Clear(_backgroundColorPma);
foreach (var sp in spines.Reverse()) _renderTexture.Draw(sp);
_renderTexture.Display();
_renderTexture.SetActive(false);
return new(_renderTexture.Texture.CopyToImage());
}
/// <summary>
/// 导出给定的模型, 从前往后对应从上往下的渲染顺序
/// </summary>
/// <param name="output">输出路径, 一般而言都是文件路径, 少数情况指定的是文件夹</param>
/// <param name="spines">要导出的模型, 从前往后对应从上往下的渲染顺序</param>
public abstract void Export(string output, params SpineObject[] spines);
#region IDisposable
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
_renderTexture.Dispose();
}
_disposed = true;
}
~BaseExporter()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
if (_disposed)
{
GC.SuppressFinalize(this);
}
}
#endregion
}
}

View File

@@ -1,100 +0,0 @@
using FFMpegCore;
using FFMpegCore.Pipes;
using SFML.Graphics;
using SFML.System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Exporters
{
/// <summary>
/// 自定义参数的 FFmpeg 导出类
/// </summary>
public class CustomFFmpegExporter : VideoExporter
{
public CustomFFmpegExporter(uint width = 100, uint height = 100) : base(width, height) { }
public CustomFFmpegExporter(Vector2u resolution) : base(resolution) { }
/// <summary>
/// <c>-f</c>
/// </summary>
public string? Format { get => _format; set => _format = value; }
private string? _format;
/// <summary>
/// <c>-c:v</c>
/// </summary>
public string? Codec { get => _codec; set => _codec = value; }
private string? _codec;
/// <summary>
/// <c>-pix_fmt</c>
/// </summary>
public string? PixelFormat { get => _pixelFormat; set => _pixelFormat = value; }
private string? _pixelFormat;
/// <summary>
/// <c>-b:v</c>
/// </summary>
public string? Bitrate { get => _bitrate; set => _bitrate = value; }
private string? _bitrate;
/// <summary>
/// <c>-vf</c>
/// </summary>
public string? Filter { get => _filter; set => _filter = value; }
private string? _filter;
/// <summary>
/// 其他自定义参数
/// </summary>
public string? CustomArgs { get => _customArgs; set => _customArgs = value; }
private string? _customArgs;
private void SetOutputOptions(FFMpegArgumentOptions options)
{
if (!string.IsNullOrEmpty(_format)) options.ForceFormat(_format);
if (!string.IsNullOrEmpty(_codec)) options.WithVideoCodec(_codec);
if (!string.IsNullOrEmpty(_pixelFormat)) options.ForcePixelFormat(_pixelFormat);
if (!string.IsNullOrEmpty(_bitrate)) options.WithCustomArgument($"-b:v {_bitrate}");
if (!string.IsNullOrEmpty(_filter)) options.WithCustomArgument($"-vf \"unpremultiply=inplace=1, {_filter}\"");
else options.WithCustomArgument("-vf unpremultiply=inplace=1");
if (!string.IsNullOrEmpty(_customArgs)) options.WithCustomArgument($"{_customArgs}");
}
/// <summary>
/// 获取的一帧, 结果是预乘的
/// </summary>
protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
{
// BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死
// 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失
using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y);
using var view = _renderTexture.GetView();
tex.SetView(view);
tex.Clear(_backgroundColorPma);
foreach (var sp in spines.Reverse()) tex.Draw(sp);
tex.Display();
return new(tex.Texture.CopyToImage());
}
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
{
var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
try
{
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, true, SetOutputOptions);
_logger.Info("FFmpeg arguments: {0}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
}
}
}
}

View File

@@ -1,171 +0,0 @@
using FFMpegCore;
using FFMpegCore.Enums;
using FFMpegCore.Pipes;
using NLog;
using SFML.Graphics;
using SFML.System;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Exporters
{
/// <summary>
/// 基于 FFmpeg 命令行的导出类, 可以导出动图及视频格式
/// </summary>
public class FFmpegVideoExporter : VideoExporter
{
public FFmpegVideoExporter(uint width = 100, uint height = 100) : base(width, height) { }
public FFmpegVideoExporter(Vector2u resolution) : base(resolution) { }
/// <summary>
/// FFmpeg 导出格式
/// </summary>
public enum VideoFormat
{
Gif,
Webp,
Mp4,
Webm,
Mkv,
Mov,
}
/// <summary>
/// 视频格式
/// </summary>
public VideoFormat Format { get => _format; set => _format = value; }
private VideoFormat _format = VideoFormat.Mp4;
/// <summary>
/// 动图是否循环 [Gif/Webp]
/// </summary>
public bool Loop { get => _loop; set => _loop = value; }
private bool _loop = true;
/// <summary>
/// 质量 [Webp]
/// </summary>
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
private int _quality = 75;
/// <summary>
/// 无损压缩 [Webp]
/// </summary>
public bool Lossless { get => _lossless; set => _lossless = value; }
private bool _lossless = false;
/// <summary>
/// CRF [Mp4/Webm/Mkv]
/// </summary>
public int Crf { get => _crf; set => _crf = Math.Clamp(value, 0, 63); }
private int _crf = 23;
/// <summary>
/// prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道 [Mov]
/// </summary>
public int Profile { get => _profile; set => _profile = Math.Clamp(value, -1, 5); }
private int _profile = 5;
/// <summary>
/// 获取的一帧, 结果是预乘的
/// </summary>
protected override SFMLImageVideoFrame GetFrame(SpineObject[] spines)
{
// BUG: 也许和 SFML 多线程或者 FFmpeg 调用有关, 当渲染线程也在运行的时候此处并行渲染会导致和 SFML 有关的内容都卡死
// 不知道为什么用 FFmpeg 必须临时创建 RenderTexture, 否则无法正常渲染, 会导致画面帧丢失
using var tex = new RenderTexture(_renderTexture.Size.X, _renderTexture.Size.Y);
using var view = _renderTexture.GetView();
tex.SetView(view);
tex.Clear(_backgroundColorPma);
foreach (var sp in spines.Reverse()) tex.Draw(sp);
tex.Display();
return new(tex.Texture.CopyToImage());
}
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
{
var videoFramesSource = new RawVideoPipeSource(GetFrames(spines, output, ct)) { FrameRate = _fps };
Action<FFMpegArgumentOptions> setOutputOptions = _format switch
{
VideoFormat.Gif => SetGifOptions,
VideoFormat.Webp => SetWebpOptions,
VideoFormat.Mp4 => SetMp4Options,
VideoFormat.Webm => SetWebmOptions,
VideoFormat.Mkv => SetMkvOptions,
VideoFormat.Mov => SetMovOptions,
_ => throw new NotImplementedException(),
};
try
{
var ffmpegArgs = FFMpegArguments.FromPipeInput(videoFramesSource).OutputToFile(output, true, setOutputOptions);
_logger.Info("FFmpeg arguments: {0}", ffmpegArgs.Arguments);
ffmpegArgs.ProcessSynchronously();
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
}
}
private void SetGifOptions(FFMpegArgumentOptions options)
{
// Gif 固定使用 256 调色板和 128 透明度阈值
var v = "split [s0][s1]";
var s0 = "[s0] palettegen=max_colors=256 [p]";
var s1 = "[s1][p] paletteuse=alpha_threshold=128";
var customArgs = $"-vf \"unpremultiply=inplace=1, {v};{s0};{s1}\" -loop {(_loop ? 0 : -1)}";
options.ForceFormat("gif")
.WithCustomArgument(customArgs);
}
private void SetWebpOptions(FFMpegArgumentOptions options)
{
var customArgs = $"-vf unpremultiply=inplace=1 -quality {_quality} -loop {(_loop ? 0 : 1)} -lossless {(_lossless ? 1 : 0)}";
options.ForceFormat("webp").WithVideoCodec("libwebp_anim").ForcePixelFormat("yuva420p")
.WithCustomArgument(customArgs);
}
private void SetMp4Options(FFMpegArgumentOptions options)
{
// XXX: windows 默认播放器在播放 MP4 格式时对于 libx264 编码器只支持 yuv420p 的像素格式
// 但是如果是 libx265 则没有该限制
var customArgs = "-vf unpremultiply=inplace=1";
options.ForceFormat("mp4").WithVideoCodec("libx264").ForcePixelFormat("yuv420p")
.WithFastStart()
.WithConstantRateFactor(_crf)
.WithCustomArgument(customArgs);
}
private void SetWebmOptions(FFMpegArgumentOptions options)
{
var customArgs = "-vf unpremultiply=inplace=1";
options.ForceFormat("webm").WithVideoCodec("libvpx-vp9").ForcePixelFormat("yuva420p")
.WithConstantRateFactor(_crf)
.WithCustomArgument(customArgs);
}
private void SetMkvOptions(FFMpegArgumentOptions options)
{
var customArgs = "-vf unpremultiply=inplace=1";
options.ForceFormat("matroska").WithVideoCodec("libx265").ForcePixelFormat("yuv444p")
.WithConstantRateFactor(_crf)
.WithCustomArgument(customArgs);
}
private void SetMovOptions(FFMpegArgumentOptions options)
{
var customArgs = "-vf unpremultiply=inplace=1";
options.ForceFormat("mov").WithVideoCodec("prores_ks").ForcePixelFormat("yuva444p10le")
.WithFastStart()
.WithCustomArgument($"-profile {_profile}")
.WithCustomArgument(customArgs);
}
}
}

View File

@@ -1,37 +0,0 @@
using SFML.System;
using SkiaSharp;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Exporters
{
/// <summary>
/// 单帧画面导出类
/// </summary>
public class FrameExporter : BaseExporter
{
public FrameExporter(uint width = 100, uint height = 100) : base(width, height) { }
public FrameExporter(Vector2u resolution) : base(resolution) { }
public SKEncodedImageFormat Format { get => _format; set => _format = value; }
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
protected int _quality = 80;
public override void Export(string output, params SpineObject[] spines)
{
using var frame = GetFrame(spines);
var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
using var data = skImage.Encode(_format, _quality);
using var stream = File.OpenWrite(output);
data.SaveTo(stream);
}
}
}

View File

@@ -1,61 +0,0 @@
using NLog;
using SFML.System;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Exporters
{
/// <summary>
/// 帧序列导出器, 导出 png 帧序列
/// </summary>
public class FrameSequenceExporter : VideoExporter
{
public FrameSequenceExporter(uint width = 100, uint height = 100) : base(width, height) { }
public FrameSequenceExporter(Vector2u resolution) : base(resolution) { }
public override void Export(string output, CancellationToken ct, params SpineObject[] spines)
{
Directory.CreateDirectory(output);
int frameCount = GetFrameCount();
int frameIdx = 0;
_progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
foreach (var frame in GetFrames(spines))
{
if (ct.IsCancellationRequested)
{
_logger.Info("Export cancelled");
frame.Dispose();
break;
}
var savePath = Path.Combine(output, $"frame_{_fps}_{frameIdx:d6}.png");
var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {savePath}");
try
{
using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
using var data = skImage.Encode(SKEncodedImageFormat.Png, 100);
using var stream = File.OpenWrite(savePath);
data.SaveTo(stream);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to save frame {0}, {1}", savePath, ex.Message);
}
finally
{
frame.Dispose();
}
frameIdx++;
}
}
}
}

View File

@@ -1,52 +0,0 @@
using FFMpegCore.Pipes;
using SFML.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Spine.Exporters
{
/// <summary>
/// <see cref="SFML.Graphics.Image"/> 帧对象包装类, 将接管给定对象生命周期
/// </summary>
public class SFMLImageVideoFrame(Image image) : IVideoFrame, IDisposable
{
private readonly Image _image = image;
/// <summary>
/// 接管的 <see cref="SFML.Graphics.Image"/> 内部对象
/// </summary>
public Image Image => _image;
public int Width => (int)_image.Size.X;
public int Height => (int)_image.Size.Y;
public string Format => "rgba";
public void Serialize(Stream pipe) => pipe.Write(_image.Pixels);
public async Task SerializeAsync(Stream pipe, CancellationToken token) => await pipe.WriteAsync(_image.Pixels, token);
#region IDisposable
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_image.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@@ -1,157 +0,0 @@
using NLog;
using SFML.System;
using SkiaSharp;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Exporters
{
/// <summary>
/// 多帧画面导出基类, 可以获取连续的帧序列
/// </summary>
public abstract class VideoExporter : BaseExporter
{
public VideoExporter(uint width, uint height) : base(width, height) { }
public VideoExporter(Vector2u resolution) : base(resolution) { }
/// <summary>
/// 导出时长
/// </summary>
public float Duration
{
get => _duration;
set
{
if (value < 0)
{
_logger.Warn("Omit invalid duration: {0}", value);
return;
}
_duration = value;
}
}
protected float _duration = 0;
/// <summary>
/// 帧率
/// </summary>
public float Fps
{
get => _fps;
set
{
if (value <= 0)
{
_logger.Warn("Omit invalid fps: {0}", value);
return;
}
_fps = value;
}
}
protected float _fps = 24;
public float Speed
{
get => _speed;
set
{
if (_speed <= 0)
{
_logger.Warn("Omit invalid speed: {0}", value);
return;
}
_speed = value;
}
}
protected float _speed = 1f;
/// <summary>
/// 是否保留最后一帧
/// </summary>
public bool KeepLast { get => _keepLast; set => _keepLast = value; }
protected bool _keepLast = true;
/// <summary>
/// 获取总帧数
/// </summary>
public int GetFrameCount()
{
var delta = 1f / _fps;
var total = (int)(_duration * _fps); // 完整帧的数量
var deltaFinal = _duration - delta * total; // 最后一帧时长
var final = _keepLast && deltaFinal > 1e-3 ? 1 : 0;
var frameCount = 1 + total + final; // 所有帧的数量 = 起始帧 + 完整帧 + 最后一帧
return frameCount;
}
/// <summary>
/// 生成帧序列
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines)
{
float delta = 1f / _fps;
int total = (int)(_duration * _fps); // 完整帧的数量
bool hasFinal = _keepLast && (_duration - delta * total) > 1e-3;
// 导出首帧
var firstFrame = GetFrame(spines);
yield return firstFrame;
// 导出完整帧
for (int i = 0; i < total; i++)
{
foreach (var spine in spines) spine.Update(delta * _speed);
yield return GetFrame(spines);
}
// 导出最后一帧
if (hasFinal)
{
// XXX: 此处还是按照完整的一帧时长进行更新, 也许可以只更新准确的最后一帧时长
foreach (var spine in spines) spine.Update(delta * _speed);
yield return GetFrame(spines);
}
}
/// <summary>
/// 生成帧序列, 支持中途取消和进度输出
/// </summary>
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines, string output, CancellationToken ct)
{
int frameCount = GetFrameCount();
int frameIdx = 0;
_progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
foreach (var frame in GetFrames(spines))
{
if (ct.IsCancellationRequested)
{
_logger.Info("Export cancelled");
frame.Dispose();
break;
}
_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {output}");
yield return frame;
frameIdx++;
}
}
public sealed override void Export(string output, params SpineObject[] spines) => Export(output, default, spines);
/// <summary>
/// 导出给定的模型, 从前往后对应从上往下的渲染顺序
/// </summary>
/// <param name="output">输出路径, 一般而言都是文件路径, 少数情况指定的是文件夹</param>
/// <param name="ct">取消令牌</param>
/// <param name="spines">要导出的模型, 从前往后对应从上往下的渲染顺序</param>
public abstract void Export(string output, CancellationToken ct, params SpineObject[] spines);
}
}

View File

@@ -1,23 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime21;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V21
{
internal sealed class Animation21(Animation innerObject) : IAnimation
{
private readonly Animation _o = innerObject;
public Animation InnerObject => _o;
public string Name => _o.Name;
public float Duration => _o.Duration;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,179 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21
{
internal sealed class AnimationState21(AnimationState innerObject, SpineObjectData21 data) : IAnimationState
{
private readonly AnimationState _o = innerObject;
private readonly SpineObjectData21 _data = data;
private readonly Dictionary<TrackEntry, TrackEntry21> _trackEntryPool = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public AnimationState InnerObject => _o;
#pragma warning disable CS0067
// NOTE: 2.1 没有这两个事件
public event IAnimationState.TrackEntryDelegate? Interrupt;
public event IAnimationState.TrackEntryDelegate? Dispose;
#pragma warning restore CS0067
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public void Update(float delta) => _o.Update(delta);
public void Apply(ISkeleton skeleton)
{
if (skeleton is Skeleton21 skel)
{
_o.Apply(skel.InnerObject);
return;
}
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
}
/// <summary>
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
/// </summary>
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
{
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
return tr;
}
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
public void ClearTrack(int index) => _o.ClearTrack(index);
public void ClearTracks() => _o.ClearTracks();
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
{
if (animation is Animation21 anime)
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
{
if (animation is Animation21 anime)
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21.Attachments
{
internal abstract class Attachment21(Attachment innerObject) : IAttachment
{
private readonly Attachment _o = innerObject;
public virtual Attachment InnerObject => _o;
public string Name => _o.Name;
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21.Attachments
{
internal sealed class BoundingBoxAttachment21(BoundingBoxAttachment innerObject) :
Attachment21(innerObject),
IBoundingBoxAttachment
{
private readonly BoundingBoxAttachment _o = innerObject;
public override BoundingBoxAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot21 st)
{
var length = _o.Vertices.Length;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21.Attachments
{
internal sealed class MeshAttachment21(MeshAttachment innerObject) :
Attachment21(innerObject),
IMeshAttachment
{
private readonly MeshAttachment _o = innerObject;
public override MeshAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot21 st)
{
var length = _o.Vertices.Length;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
public int[] Triangles => _o.Triangles;
public int HullLength => _o.HullLength;
}
}

View File

@@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21.Attachments
{
internal sealed class RegionAttachment21(RegionAttachment innerObject) :
Attachment21(innerObject),
IRegionAttachment
{
private readonly RegionAttachment _o = innerObject;
public override RegionAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot21 st)
{
if (worldVertices.Length < 8) worldVertices = new float[8];
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices);
return 8;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21.Attachments
{
internal sealed class SkinnedMeshAttachment21(SkinnedMeshAttachment innerObject) :
Attachment21(innerObject),
ISkinnedMeshAttachment
{
private readonly SkinnedMeshAttachment _o = innerObject;
public override SkinnedMeshAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot21 st)
{
var length = _o.UVs.Length;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot21)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
public int[] Triangles => _o.Triangles;
public int HullLength => _o.HullLength;
}
}

View File

@@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21
{
internal sealed class Bone21(Bone innerObject, Bone21? parent = null) : IBone
{
private readonly Bone _o = innerObject;
private readonly Bone21? _parent = parent;
public Bone InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public IBone? Parent => _parent;
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
public float Length => _o.Data.Length;
public float WorldX => _o.WorldX;
public float WorldY => _o.WorldY;
public float A => _o.M00;
public float B => _o.M01;
public float C => _o.M10;
public float D => _o.M11;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Frozen;
using System.Collections.Immutable;
using Spine.SpineWrappers;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21
{
internal sealed class Skeleton21 : ISkeleton
{
private readonly Skeleton _o;
private readonly SpineObjectData21 _data;
private readonly ImmutableArray<IBone> _bones;
private readonly FrozenDictionary<string, IBone> _bonesByName;
private readonly ImmutableArray<ISlot> _slots;
private readonly FrozenDictionary<string, ISlot> _slotsByName;
private Skin21? _skin;
public Skeleton21(Skeleton innerObject, SpineObjectData21 data)
{
_o = innerObject;
_data = data;
List<Bone21> bones = [];
Dictionary<string, IBone> bonesByName = [];
foreach (var b in _o.Bones)
{
var bone = new Bone21(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
bones.Add(bone);
bonesByName[bone.Name] = bone;
}
_bones = bones.Cast<IBone>().ToImmutableArray();
_bonesByName = bonesByName.ToFrozenDictionary();
List<Slot21> slots = [];
Dictionary<string, ISlot> slotsByName = [];
foreach (var s in _o.Slots)
{
var slot = new Slot21(s, _data, bones[s.Bone.Data.Index]);
slots.Add(slot);
slotsByName[slot.Name] = slot;
}
_slots = slots.Cast<ISlot>().ToImmutableArray();
_slotsByName = slotsByName.ToFrozenDictionary();
}
public Skeleton InnerObject => _o;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public float X { get => _o.X; set => _o.X = value; }
public float Y { get => _o.Y; set => _o.Y = value; }
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
public ImmutableArray<IBone> Bones => _bones;
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
public ImmutableArray<ISlot> Slots => _slots;
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
public ISkin? Skin
{
get => _skin;
set
{
if (value is null)
{
_o.Skin = null;
_skin = null;
return;
}
if (value is Skin21 sk)
{
_o.Skin = sk.InnerObject;
_skin = sk;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
public void UpdateCache() => _o.UpdateCache();
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
public void SetToSetupPose() => _o.SetToSetupPose();
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
public void Update(float delta) => _o.Update(delta);
public void GetBounds(out float x, out float y, out float w, out float h)
{
_o.GetBounds(out x, out y, out w, out h);
}
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,42 +0,0 @@
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using Spine.Utils;
using SpineRuntime21;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V21
{
internal sealed class SkeletonClipping21 : ISkeletonClipping
{
public bool IsClipping => false;
public float[] ClippedVertices { get; private set; } = [];
public int ClippedVerticesLength { get; private set; } = 0;
public int[] ClippedTriangles { get; private set; } = [];
public int ClippedTrianglesLength { get; private set; } = 0;
public float[] ClippedUVs { get; private set; } = [];
public void ClipEnd(ISlot slot) { }
public void ClipEnd() { }
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment) { }
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
{
ClippedVertices = vertices.ToArray();
ClippedVerticesLength = verticesLength;
ClippedTriangles = triangles.ToArray();
ClippedTrianglesLength = trianglesLength;
ClippedUVs = uvs.ToArray();
}
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21
{
internal sealed class Skin21 : ISkin
{
private readonly Skin _o;
/// <summary>
/// 使用指定名字创建空皮肤
/// </summary>
public Skin21(string name) => _o = new(name);
/// <summary>
/// 包装已有皮肤对象
/// </summary>
public Skin21(Skin innerObject) => _o = innerObject;
public Skin InnerObject => _o;
public string Name => _o.Name;
public void AddSkin(ISkin skin)
{
if (skin is Skin21 sk)
{
// NOTE: 3.7 及以下不支持 AddSkin
foreach (var (k, v) in sk._o.Attachments)
_o.AddAttachment(k.Key, k.Value, v);
return;
}
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
}
public void Clear() => _o.Attachments.Clear();
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,73 +0,0 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using SpineRuntime21;
namespace Spine.Implementations.SpineWrappers.V21
{
internal sealed class Slot21 : ISlot
{
private readonly Slot _o;
private readonly SpineObjectData21 _data;
private readonly Bone21 _bone;
private readonly SFML.Graphics.BlendMode _blendMode;
public Slot21(Slot innerObject, SpineObjectData21 data, Bone21 bone)
{
_o = innerObject;
_data = data;
_bone = bone;
_blendMode = _o.Data.AdditiveBlending ? SFMLBlendMode.AdditivePma : SFMLBlendMode.NormalPma; // NOTE: 2.1 没有完整的 BlendMode
}
public Slot InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public SFML.Graphics.BlendMode Blend => _blendMode;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public IBone Bone => _bone;
public Spine.SpineWrappers.Attachments.IAttachment? Attachment
{
get
{
if (_o.Attachment is Attachment att)
{
return _data.SlotAttachments[Name][att.Name];
}
return null;
}
set
{
if (value is null)
{
_o.Attachment = null;
return;
}
if (value is Attachments.Attachment21 att)
{
_o.Attachment = att.InnerObject;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public bool Disabled { get; set; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,146 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime21;
using Spine.Implementations.SpineWrappers.V21.Attachments;
namespace Spine.Implementations.SpineWrappers.V21
{
[SpineImplementation(2, 1)]
internal sealed class SpineObjectData21 : SpineObjectData
{
private readonly Atlas _atlas;
private readonly SkeletonData _skeletonData;
private readonly AnimationStateData _animationStateData;
private readonly ImmutableArray<ISkin> _skins;
private readonly FrozenDictionary<string, ISkin> _skinsByName;
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData21(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex)
{
_atlas.Dispose();
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据
_animationStateData = new AnimationStateData(_skeletonData);
// 整理皮肤和附件
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
List<ISkin> skins = [];
Dictionary<string, ISkin> skinsByName = [];
foreach (var s in _skeletonData.Skins)
{
var skin = new Skin21(s);
skins.Add(skin);
skinsByName[s.Name] = skin;
foreach (var (k, att) in s.Attachments)
{
var slotName = _skeletonData.Slots[k.Key].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = [];
attachments[att.Name] = att switch
{
RegionAttachment regionAtt => new RegionAttachment21(regionAtt),
MeshAttachment meshAtt => new MeshAttachment21(meshAtt),
SkinnedMeshAttachment skMeshAtt => new SkinnedMeshAttachment21(skMeshAtt),
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment21(bbAtt),
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
};
}
}
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
_skins = skins.ToImmutableArray();
_skinsByName = skinsByName.ToFrozenDictionary();
// 整理所有动画数据
List<IAnimation> animations = [];
Dictionary<string, IAnimation> animationsByName = [];
foreach (var a in _skeletonData.Animations)
{
var anime = new Animation21(a);
animations.Add(anime);
animationsByName[anime.Name] = anime;
}
_animations = animations.ToImmutableArray();
_animationsByName = animationsByName.ToFrozenDictionary();
}
public override string SkeletonVersion => _skeletonData.Version;
public override ImmutableArray<ISkin> Skins => _skins;
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
public override ImmutableArray<IAnimation> Animations => _animations;
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
protected override void DisposeAtlas() => _atlas.Dispose();
public override ISkeleton CreateSkeleton() => new Skeleton21(new(_skeletonData), this);
public override IAnimationState CreateAnimationState() => new AnimationState21(new(_animationStateData), this);
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping21();
public override ISkin CreateSkin(string name) => new Skin21(name);
}
}

View File

@@ -1,135 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime21;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V21
{
internal sealed class TrackEntry21(TrackEntry innerObject, AnimationState21 animationState, SpineObjectData21 data): ITrackEntry
{
private readonly TrackEntry _o = innerObject;
private readonly AnimationState21 _animationState = animationState;
private readonly SpineObjectData21 _data = data;
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public TrackEntry InnerObject => _o;
#pragma warning disable CS0067
// 2.1 没有这两个事件
public event IAnimationState.TrackEntryDelegate? Interrupt;
public event IAnimationState.TrackEntryDelegate? Dispose;
#pragma warning restore CS0067
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public int TrackIndex { get => _o.TrackIndex; }
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
public float TrackTime { get => _o.Time; set => _o.Time = value; }
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public float Alpha { get => _o.Mix; set => _o.Mix = value; }
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,23 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime34;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V34
{
internal sealed class Animation34(Animation innerObject) : IAnimation
{
private readonly Animation _o = innerObject;
public Animation InnerObject => _o;
public string Name => _o.Name;
public float Duration => _o.Duration;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,180 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34
{
internal sealed class AnimationState34(AnimationState innerObject, SpineObjectData34 data) : IAnimationState
{
private readonly AnimationState _o = innerObject;
private readonly SpineObjectData34 _data = data;
private readonly Dictionary<TrackEntry, TrackEntry34> _trackEntryPool = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public AnimationState InnerObject => _o;
#pragma warning disable CS0067
// NOTE: 3.4 以下没有这两个事件
public event IAnimationState.TrackEntryDelegate? Interrupt;
public event IAnimationState.TrackEntryDelegate? Dispose;
#pragma warning restore CS0067
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public void Update(float delta) => _o.Update(delta);
public void Apply(ISkeleton skeleton)
{
if (skeleton is Skeleton34 skel)
{
_o.Apply(skel.InnerObject);
return;
}
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
}
/// <summary>
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
/// </summary>
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
{
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
return tr;
}
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
public void ClearTrack(int index) => _o.ClearTrack(index);
public void ClearTracks() => _o.ClearTracks();
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
{
if (animation is Animation34 anime)
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
{
if (animation is Animation34 anime)
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34.Attachments
{
internal abstract class Attachment34(Attachment innerObject) : IAttachment
{
private readonly Attachment _o = innerObject;
public virtual Attachment InnerObject => _o;
public string Name => _o.Name;
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34.Attachments
{
internal sealed class BoundingBoxAttachment34(BoundingBoxAttachment innerObject) :
Attachment34(innerObject),
IBoundingBoxAttachment
{
private readonly BoundingBoxAttachment _o = innerObject;
public override BoundingBoxAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot34 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34.Attachments
{
internal sealed class MeshAttachment34(MeshAttachment innerObject) :
Attachment34(innerObject),
IMeshAttachment
{
private readonly MeshAttachment _o = innerObject;
public override MeshAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot34 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
public int[] Triangles => _o.Triangles;
public int HullLength => _o.HullLength;
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34.Attachments
{
internal sealed class PathAttachment34(PathAttachment innerObject) :
Attachment34(innerObject),
IPathAttachment
{
private readonly PathAttachment _o = innerObject;
public override PathAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot34 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34.Attachments
{
internal sealed class RegionAttachment34(RegionAttachment innerObject) :
Attachment34(innerObject),
IRegionAttachment
{
private readonly RegionAttachment _o = innerObject;
public override RegionAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot34 st)
{
if (worldVertices.Length < 8) worldVertices = new float[8];
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices);
return 8;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot34)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
}
}

View File

@@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34
{
internal sealed class Bone34(Bone innerObject, Bone34? parent = null) : IBone
{
private readonly Bone _o = innerObject;
private readonly Bone34? _parent = parent;
public Bone InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public IBone? Parent => _parent;
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
public float Length => _o.Data.Length;
public float WorldX => _o.WorldX;
public float WorldY => _o.WorldY;
public float A => _o.A;
public float B => _o.B;
public float C => _o.C;
public float D => _o.D;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Frozen;
using System.Collections.Immutable;
using Spine.SpineWrappers;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34
{
internal sealed class Skeleton34 : ISkeleton
{
private readonly Skeleton _o;
private readonly SpineObjectData34 _data;
private readonly ImmutableArray<IBone> _bones;
private readonly FrozenDictionary<string, IBone> _bonesByName;
private readonly ImmutableArray<ISlot> _slots;
private readonly FrozenDictionary<string, ISlot> _slotsByName;
private Skin34? _skin;
public Skeleton34(Skeleton innerObject, SpineObjectData34 data)
{
_o = innerObject;
_data = data;
List<Bone34> bones = [];
Dictionary<string, IBone> bonesByName = [];
foreach (var b in _o.Bones)
{
var bone = new Bone34(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
bones.Add(bone);
bonesByName[bone.Name] = bone;
}
_bones = bones.Cast<IBone>().ToImmutableArray();
_bonesByName = bonesByName.ToFrozenDictionary();
List<Slot34> slots = [];
Dictionary<string, ISlot> slotsByName = [];
foreach (var s in _o.Slots)
{
var slot = new Slot34(s, _data, bones[s.Bone.Data.Index]);
slots.Add(slot);
slotsByName[slot.Name] = slot;
}
_slots = slots.Cast<ISlot>().ToImmutableArray();
_slotsByName = slotsByName.ToFrozenDictionary();
}
public Skeleton InnerObject => _o;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public float X { get => _o.X; set => _o.X = value; }
public float Y { get => _o.Y; set => _o.Y = value; }
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
public ImmutableArray<IBone> Bones => _bones;
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
public ImmutableArray<ISlot> Slots => _slots;
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
public ISkin? Skin
{
get => _skin;
set
{
if (value is null)
{
_o.Skin = null;
_skin = null;
return;
}
if (value is Skin34 sk)
{
_o.Skin = sk.InnerObject;
_skin = sk;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
public void UpdateCache() => _o.UpdateCache();
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
public void SetToSetupPose() => _o.SetToSetupPose();
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
public void Update(float delta) => _o.Update(delta);
public void GetBounds(out float x, out float y, out float w, out float h)
{
float[] _ = [];
_o.GetBounds(out x, out y, out w, out h);
}
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,42 +0,0 @@
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using Spine.Utils;
using SpineRuntime34;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V34
{
internal sealed class SkeletonClipping34 : ISkeletonClipping
{
public bool IsClipping => false;
public float[] ClippedVertices { get; private set; } = [];
public int ClippedVerticesLength { get; private set; } = 0;
public int[] ClippedTriangles { get; private set; } = [];
public int ClippedTrianglesLength { get; private set; } = 0;
public float[] ClippedUVs { get; private set; } = [];
public void ClipEnd(ISlot slot) { }
public void ClipEnd() { }
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment) { }
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
{
ClippedVertices = vertices.ToArray();
ClippedVerticesLength = verticesLength;
ClippedTriangles = triangles.ToArray();
ClippedTrianglesLength = trianglesLength;
ClippedUVs = uvs.ToArray();
}
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34
{
internal sealed class Skin34 : ISkin
{
private readonly Skin _o;
/// <summary>
/// 使用指定名字创建空皮肤
/// </summary>
public Skin34(string name) => _o = new(name);
/// <summary>
/// 包装已有皮肤对象
/// </summary>
public Skin34(Skin innerObject) => _o = innerObject;
public Skin InnerObject => _o;
public string Name => _o.Name;
public void AddSkin(ISkin skin)
{
if (skin is Skin34 sk)
{
// NOTE: 3.7 及以下不支持 AddSkin
foreach (var (k, v) in sk._o.Attachments)
_o.AddAttachment(k.slotIndex, k.name, v);
return;
}
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
}
public void Clear() => _o.Attachments.Clear();
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using SpineRuntime34;
namespace Spine.Implementations.SpineWrappers.V34
{
internal sealed class Slot34 : ISlot
{
private readonly Slot _o;
private readonly SpineObjectData34 _data;
private readonly Bone34 _bone;
private readonly SFML.Graphics.BlendMode _blendMode;
public Slot34(Slot innerObject, SpineObjectData34 data, Bone34 bone)
{
_o = innerObject;
_data = data;
_bone = bone;
_blendMode = _o.Data.BlendMode switch
{
BlendMode.normal => SFMLBlendMode.NormalPma,
BlendMode.additive => SFMLBlendMode.AdditivePma,
BlendMode.multiply => SFMLBlendMode.MultiplyPma,
BlendMode.screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
};
}
public Slot InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public SFML.Graphics.BlendMode Blend => _blendMode;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public IBone Bone => _bone;
public Spine.SpineWrappers.Attachments.IAttachment? Attachment
{
get
{
if (_o.Attachment is Attachment att)
{
return _data.SlotAttachments[Name][att.Name];
}
return null;
}
set
{
if (value is null)
{
_o.Attachment = null;
return;
}
if (value is Attachments.Attachment34 att)
{
_o.Attachment = att.InnerObject;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public bool Disabled { get; set; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,146 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime34;
using Spine.Implementations.SpineWrappers.V34.Attachments;
namespace Spine.Implementations.SpineWrappers.V34
{
[SpineImplementation(3, 4)]
internal sealed class SpineObjectData34 : SpineObjectData
{
private readonly Atlas _atlas;
private readonly SkeletonData _skeletonData;
private readonly AnimationStateData _animationStateData;
private readonly ImmutableArray<ISkin> _skins;
private readonly FrozenDictionary<string, ISkin> _skinsByName;
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData34(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex)
{
_atlas.Dispose();
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据
_animationStateData = new AnimationStateData(_skeletonData);
// 整理皮肤和附件
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
List<ISkin> skins = [];
Dictionary<string, ISkin> skinsByName = [];
foreach (var s in _skeletonData.Skins)
{
var skin = new Skin34(s);
skins.Add(skin);
skinsByName[s.Name] = skin;
foreach (var (k, att) in s.Attachments)
{
var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = [];
attachments[att.Name] = att switch
{
RegionAttachment regionAtt => new RegionAttachment34(regionAtt),
MeshAttachment meshAtt => new MeshAttachment34(meshAtt),
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment34(bbAtt),
PathAttachment pathAtt => new PathAttachment34(pathAtt),
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
};
}
}
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
_skins = skins.ToImmutableArray();
_skinsByName = skinsByName.ToFrozenDictionary();
// 整理所有动画数据
List<IAnimation> animations = [];
Dictionary<string, IAnimation> animationsByName = [];
foreach (var a in _skeletonData.Animations)
{
var anime = new Animation34(a);
animations.Add(anime);
animationsByName[anime.Name] = anime;
}
_animations = animations.ToImmutableArray();
_animationsByName = animationsByName.ToFrozenDictionary();
}
public override string SkeletonVersion => _skeletonData.Version;
public override ImmutableArray<ISkin> Skins => _skins;
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
public override ImmutableArray<IAnimation> Animations => _animations;
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
protected override void DisposeAtlas() => _atlas.Dispose();
public override ISkeleton CreateSkeleton() => new Skeleton34(new(_skeletonData), this);
public override IAnimationState CreateAnimationState() => new AnimationState34(new(_animationStateData), this);
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping34();
public override ISkin CreateSkin(string name) => new Skin34(name);
}
}

View File

@@ -1,135 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime34;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V34
{
internal sealed class TrackEntry34(TrackEntry innerObject, AnimationState34 animationState, SpineObjectData34 data): ITrackEntry
{
private readonly TrackEntry _o = innerObject;
private readonly AnimationState34 _animationState = animationState;
private readonly SpineObjectData34 _data = data;
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public TrackEntry InnerObject => _o;
#pragma warning disable CS0067
// 3.4 及以下没有这两个事件
public event IAnimationState.TrackEntryDelegate? Interrupt;
public event IAnimationState.TrackEntryDelegate? Dispose;
#pragma warning restore CS0067
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public int TrackIndex { get => _o.TrackIndex; }
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
public float TrackTime { get => _o.Time; set => _o.Time = value; }
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public float Alpha { get => _o.Mix; set => _o.Mix = value; }
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,23 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime35;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V35
{
internal sealed class Animation35(Animation innerObject) : IAnimation
{
private readonly Animation _o = innerObject;
public Animation InnerObject => _o;
public string Name => _o.Name;
public float Duration => _o.Duration;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,229 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35
{
internal sealed class AnimationState35(AnimationState innerObject, SpineObjectData35 data) : IAnimationState
{
private readonly AnimationState _o = innerObject;
private readonly SpineObjectData35 _data = data;
private readonly Dictionary<TrackEntry, TrackEntry35> _trackEntryPool = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public AnimationState InnerObject => _o;
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Interrupt
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Interrupt += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Interrupt -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Dispose
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Dispose += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Dispose -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public void Update(float delta) => _o.Update(delta);
public void Apply(ISkeleton skeleton)
{
if (skeleton is Skeleton35 skel)
{
_o.Apply(skel.InnerObject);
return;
}
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
}
/// <summary>
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
/// </summary>
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
{
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
return tr;
}
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
public void ClearTrack(int index) => _o.ClearTrack(index);
public void ClearTracks() => _o.ClearTracks();
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
{
if (animation is Animation35 anime)
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
{
if (animation is Animation35 anime)
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35.Attachments
{
internal abstract class Attachment35(Attachment innerObject) : IAttachment
{
private readonly Attachment _o = innerObject;
public virtual Attachment InnerObject => _o;
public string Name => _o.Name;
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35.Attachments
{
internal sealed class BoundingBoxAttachment35(BoundingBoxAttachment innerObject) :
Attachment35(innerObject),
IBoundingBoxAttachment
{
private readonly BoundingBoxAttachment _o = innerObject;
public override BoundingBoxAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot35 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35.Attachments
{
internal sealed class ClippingAttachment35(ClippingAttachment innerObject) :
Attachment35(innerObject),
IClippingAttachment
{
private readonly ClippingAttachment _o = innerObject;
public override ClippingAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot35 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35.Attachments
{
internal sealed class MeshAttachment35(MeshAttachment innerObject) :
Attachment35(innerObject),
IMeshAttachment
{
private readonly MeshAttachment _o = innerObject;
public override MeshAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot35 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
public int[] Triangles => _o.Triangles;
public int HullLength => _o.HullLength;
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35.Attachments
{
internal sealed class PathAttachment35(PathAttachment innerObject) :
Attachment35(innerObject),
IPathAttachment
{
private readonly PathAttachment _o = innerObject;
public override PathAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot35 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35.Attachments
{
internal sealed class PointAttachment35(PointAttachment innerObject) :
Attachment35(innerObject),
IPointAttachment
{
private readonly PointAttachment _o = innerObject;
public override PointAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot35 st)
{
if (worldVertices.Length < 2) worldVertices = new float[2];
_o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
return 2;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35.Attachments
{
internal sealed class RegionAttachment35(RegionAttachment innerObject) :
Attachment35(innerObject),
IRegionAttachment
{
private readonly RegionAttachment _o = innerObject;
public override RegionAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot35 st)
{
if (worldVertices.Length < 8) worldVertices = new float[8];
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
return 8;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot35)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
}
}

View File

@@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35
{
internal sealed class Bone35(Bone innerObject, Bone35? parent = null) : IBone
{
private readonly Bone _o = innerObject;
private readonly Bone35? _parent = parent;
public Bone InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public IBone? Parent => _parent;
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
public float Length => _o.Data.Length;
public float WorldX => _o.WorldX;
public float WorldY => _o.WorldY;
public float A => _o.A;
public float B => _o.B;
public float C => _o.C;
public float D => _o.D;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Frozen;
using System.Collections.Immutable;
using Spine.SpineWrappers;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35
{
internal sealed class Skeleton35 : ISkeleton
{
private readonly Skeleton _o;
private readonly SpineObjectData35 _data;
private readonly ImmutableArray<IBone> _bones;
private readonly FrozenDictionary<string, IBone> _bonesByName;
private readonly ImmutableArray<ISlot> _slots;
private readonly FrozenDictionary<string, ISlot> _slotsByName;
private Skin35? _skin;
public Skeleton35(Skeleton innerObject, SpineObjectData35 data)
{
_o = innerObject;
_data = data;
List<Bone35> bones = [];
Dictionary<string, IBone> bonesByName = [];
foreach (var b in _o.Bones)
{
var bone = new Bone35(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
bones.Add(bone);
bonesByName[bone.Name] = bone;
}
_bones = bones.Cast<IBone>().ToImmutableArray();
_bonesByName = bonesByName.ToFrozenDictionary();
List<Slot35> slots = [];
Dictionary<string, ISlot> slotsByName = [];
foreach (var s in _o.Slots)
{
var slot = new Slot35(s, _data, bones[s.Bone.Data.Index]);
slots.Add(slot);
slotsByName[slot.Name] = slot;
}
_slots = slots.Cast<ISlot>().ToImmutableArray();
_slotsByName = slotsByName.ToFrozenDictionary();
}
public Skeleton InnerObject => _o;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public float X { get => _o.X; set => _o.X = value; }
public float Y { get => _o.Y; set => _o.Y = value; }
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
public ImmutableArray<IBone> Bones => _bones;
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
public ImmutableArray<ISlot> Slots => _slots;
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
public ISkin? Skin
{
get => _skin;
set
{
if (value is null)
{
_o.Skin = null;
_skin = null;
return;
}
if (value is Skin35 sk)
{
_o.Skin = sk.InnerObject;
_skin = sk;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
public void UpdateCache() => _o.UpdateCache();
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
public void SetToSetupPose() => _o.SetToSetupPose();
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
public void Update(float delta) => _o.Update(delta);
public void GetBounds(out float x, out float y, out float w, out float h)
{
float[] _ = [];
_o.GetBounds(out x, out y, out w, out h, ref _);
}
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,56 +0,0 @@
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using Spine.Utils;
using SpineRuntime35;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V35
{
internal sealed class SkeletonClipping35 : ISkeletonClipping
{
private readonly SkeletonClipping _o = new();
public bool IsClipping => _o.IsClipping();
public float[] ClippedVertices => _o.ClippedVertices.Items;
public int ClippedVerticesLength => _o.ClippedVertices.Count;
public int[] ClippedTriangles => _o.ClippedTriangles.Items;
public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
public float[] ClippedUVs => _o.ClippedUVs.Items;
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
=> _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
{
if (slot is Slot35 st && clippingAttachment is Attachments.ClippingAttachment35 att)
{
_o.ClipStart(st.InnerObject, att.InnerObject);
return;
}
throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
}
public void ClipEnd(ISlot slot)
{
if (slot is Slot35 st)
{
_o.ClipEnd(st.InnerObject);
return;
}
throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
}
public void ClipEnd() => _o.ClipEnd();
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35
{
internal sealed class Skin35 : ISkin
{
private readonly Skin _o;
/// <summary>
/// 使用指定名字创建空皮肤
/// </summary>
public Skin35(string name) => _o = new(name);
/// <summary>
/// 包装已有皮肤对象
/// </summary>
public Skin35(Skin innerObject) => _o = innerObject;
public Skin InnerObject => _o;
public string Name => _o.Name;
public void AddSkin(ISkin skin)
{
if (skin is Skin35 sk)
{
// NOTE: 3.7 及以下不支持 AddSkin
foreach (var (k, v) in sk._o.Attachments)
_o.AddAttachment(k.slotIndex, k.name, v);
return;
}
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
}
public void Clear() => _o.Attachments.Clear();
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using SpineRuntime35;
namespace Spine.Implementations.SpineWrappers.V35
{
internal sealed class Slot35 : ISlot
{
private readonly Slot _o;
private readonly SpineObjectData35 _data;
private readonly Bone35 _bone;
private readonly SFML.Graphics.BlendMode _blendMode;
public Slot35(Slot innerObject, SpineObjectData35 data, Bone35 bone)
{
_o = innerObject;
_data = data;
_bone = bone;
_blendMode = _o.Data.BlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
};
}
public Slot InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public SFML.Graphics.BlendMode Blend => _blendMode;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public IBone Bone => _bone;
public Spine.SpineWrappers.Attachments.IAttachment? Attachment
{
get
{
if (_o.Attachment is Attachment att)
{
return _data.SlotAttachments[Name][att.Name];
}
return null;
}
set
{
if (value is null)
{
_o.Attachment = null;
return;
}
if (value is Attachments.Attachment35 att)
{
_o.Attachment = att.InnerObject;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public bool Disabled { get; set; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime35;
using Spine.Implementations.SpineWrappers.V35.Attachments;
namespace Spine.Implementations.SpineWrappers.V35
{
[SpineImplementation(3, 5)]
internal sealed class SpineObjectData35 : SpineObjectData
{
private readonly Atlas _atlas;
private readonly SkeletonData _skeletonData;
private readonly AnimationStateData _animationStateData;
private readonly ImmutableArray<ISkin> _skins;
private readonly FrozenDictionary<string, ISkin> _skinsByName;
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData35(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex)
{
_atlas.Dispose();
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据
_animationStateData = new AnimationStateData(_skeletonData);
// 整理皮肤和附件
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
List<ISkin> skins = [];
Dictionary<string, ISkin> skinsByName = [];
foreach (var s in _skeletonData.Skins)
{
var skin = new Skin35(s);
skins.Add(skin);
skinsByName[s.Name] = skin;
foreach (var (k, att) in s.Attachments)
{
var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = [];
attachments[att.Name] = att switch
{
RegionAttachment regionAtt => new RegionAttachment35(regionAtt),
MeshAttachment meshAtt => new MeshAttachment35(meshAtt),
ClippingAttachment clipAtt => new ClippingAttachment35(clipAtt),
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment35(bbAtt),
PathAttachment pathAtt => new PathAttachment35(pathAtt),
PointAttachment ptAtt => new PointAttachment35(ptAtt),
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
};
}
}
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
_skins = skins.ToImmutableArray();
_skinsByName = skinsByName.ToFrozenDictionary();
// 整理所有动画数据
List<IAnimation> animations = [];
Dictionary<string, IAnimation> animationsByName = [];
foreach (var a in _skeletonData.Animations)
{
var anime = new Animation35(a);
animations.Add(anime);
animationsByName[anime.Name] = anime;
}
_animations = animations.ToImmutableArray();
_animationsByName = animationsByName.ToFrozenDictionary();
}
public override string SkeletonVersion => _skeletonData.Version;
public override ImmutableArray<ISkin> Skins => _skins;
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
public override ImmutableArray<IAnimation> Animations => _animations;
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
protected override void DisposeAtlas() => _atlas.Dispose();
public override ISkeleton CreateSkeleton() => new Skeleton35(new(_skeletonData), this);
public override IAnimationState CreateAnimationState() => new AnimationState35(new(_animationStateData), this);
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping35();
public override ISkin CreateSkin(string name) => new Skin35(name);
}
}

View File

@@ -1,185 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime35;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V35
{
internal sealed class TrackEntry35(TrackEntry innerObject, AnimationState35 animationState, SpineObjectData35 data): ITrackEntry
{
private readonly TrackEntry _o = innerObject;
private readonly AnimationState35 _animationState = animationState;
private readonly SpineObjectData35 _data = data;
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public TrackEntry InnerObject => _o;
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Interrupt
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Interrupt += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Interrupt -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Dispose
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Dispose += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Dispose -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public int TrackIndex { get => _o.TrackIndex; }
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,23 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime36;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V36
{
internal sealed class Animation36(Animation innerObject) : IAnimation
{
private readonly Animation _o = innerObject;
public Animation InnerObject => _o;
public string Name => _o.Name;
public float Duration => _o.Duration;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,229 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36
{
internal sealed class AnimationState36(AnimationState innerObject, SpineObjectData36 data) : IAnimationState
{
private readonly AnimationState _o = innerObject;
private readonly SpineObjectData36 _data = data;
private readonly Dictionary<TrackEntry, TrackEntry36> _trackEntryPool = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public AnimationState InnerObject => _o;
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Interrupt
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Interrupt += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Interrupt -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Dispose
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Dispose += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Dispose -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public void Update(float delta) => _o.Update(delta);
public void Apply(ISkeleton skeleton)
{
if (skeleton is Skeleton36 skel)
{
_o.Apply(skel.InnerObject);
return;
}
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
}
/// <summary>
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
/// </summary>
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
{
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
return tr;
}
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
public void ClearTrack(int index) => _o.ClearTrack(index);
public void ClearTracks() => _o.ClearTracks();
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
{
if (animation is Animation36 anime)
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
{
if (animation is Animation36 anime)
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36.Attachments
{
internal abstract class Attachment36(Attachment innerObject) : IAttachment
{
private readonly Attachment _o = innerObject;
public virtual Attachment InnerObject => _o;
public string Name => _o.Name;
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36.Attachments
{
internal sealed class BoundingBoxAttachment36(BoundingBoxAttachment innerObject) :
Attachment36(innerObject),
IBoundingBoxAttachment
{
private readonly BoundingBoxAttachment _o = innerObject;
public override BoundingBoxAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot36 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36.Attachments
{
internal sealed class ClippingAttachment36(ClippingAttachment innerObject) :
Attachment36(innerObject),
IClippingAttachment
{
private readonly ClippingAttachment _o = innerObject;
public override ClippingAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot36 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36.Attachments
{
internal sealed class MeshAttachment36(MeshAttachment innerObject) :
Attachment36(innerObject),
IMeshAttachment
{
private readonly MeshAttachment _o = innerObject;
public override MeshAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot36 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
public int[] Triangles => _o.Triangles;
public int HullLength => _o.HullLength;
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36.Attachments
{
internal sealed class PathAttachment36(PathAttachment innerObject) :
Attachment36(innerObject),
IPathAttachment
{
private readonly PathAttachment _o = innerObject;
public override PathAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot36 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36.Attachments
{
internal sealed class PointAttachment36(PointAttachment innerObject) :
Attachment36(innerObject),
IPointAttachment
{
private readonly PointAttachment _o = innerObject;
public override PointAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot36 st)
{
if (worldVertices.Length < 2) worldVertices = new float[2];
_o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
return 2;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36.Attachments
{
internal sealed class RegionAttachment36(RegionAttachment innerObject) :
Attachment36(innerObject),
IRegionAttachment
{
private readonly RegionAttachment _o = innerObject;
public override RegionAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot36 st)
{
if (worldVertices.Length < 8) worldVertices = new float[8];
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
return 8;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot36)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
}
}

View File

@@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36
{
internal sealed class Bone36(Bone innerObject, Bone36? parent = null) : IBone
{
private readonly Bone _o = innerObject;
private readonly Bone36? _parent = parent;
public Bone InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public IBone? Parent => _parent;
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
public float Length => _o.Data.Length;
public float WorldX => _o.WorldX;
public float WorldY => _o.WorldY;
public float A => _o.A;
public float B => _o.B;
public float C => _o.C;
public float D => _o.D;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Frozen;
using System.Collections.Immutable;
using Spine.SpineWrappers;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36
{
internal sealed class Skeleton36 : ISkeleton
{
private readonly Skeleton _o;
private readonly SpineObjectData36 _data;
private readonly ImmutableArray<IBone> _bones;
private readonly FrozenDictionary<string, IBone> _bonesByName;
private readonly ImmutableArray<ISlot> _slots;
private readonly FrozenDictionary<string, ISlot> _slotsByName;
private Skin36? _skin;
public Skeleton36(Skeleton innerObject, SpineObjectData36 data)
{
_o = innerObject;
_data = data;
List<Bone36> bones = [];
Dictionary<string, IBone> bonesByName = [];
foreach (var b in _o.Bones)
{
var bone = new Bone36(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
bones.Add(bone);
bonesByName[bone.Name] = bone;
}
_bones = bones.Cast<IBone>().ToImmutableArray();
_bonesByName = bonesByName.ToFrozenDictionary();
List<Slot36> slots = [];
Dictionary<string, ISlot> slotsByName = [];
foreach (var s in _o.Slots)
{
var slot = new Slot36(s, _data, bones[s.Bone.Data.Index]);
slots.Add(slot);
slotsByName[slot.Name] = slot;
}
_slots = slots.Cast<ISlot>().ToImmutableArray();
_slotsByName = slotsByName.ToFrozenDictionary();
}
public Skeleton InnerObject => _o;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public float X { get => _o.X; set => _o.X = value; }
public float Y { get => _o.Y; set => _o.Y = value; }
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
public ImmutableArray<IBone> Bones => _bones;
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
public ImmutableArray<ISlot> Slots => _slots;
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
public ISkin? Skin
{
get => _skin;
set
{
if (value is null)
{
_o.Skin = null;
_skin = null;
return;
}
if (value is Skin36 sk)
{
_o.Skin = sk.InnerObject;
_skin = sk;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
public void UpdateCache() => _o.UpdateCache();
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
public void SetToSetupPose() => _o.SetToSetupPose();
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
public void Update(float delta) => _o.Update(delta);
public void GetBounds(out float x, out float y, out float w, out float h)
{
float[] _ = [];
_o.GetBounds(out x, out y, out w, out h, ref _);
}
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,56 +0,0 @@
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using Spine.Utils;
using SpineRuntime36;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V36
{
internal sealed class SkeletonClipping36 : ISkeletonClipping
{
private readonly SkeletonClipping _o = new();
public bool IsClipping => _o.IsClipping;
public float[] ClippedVertices => _o.ClippedVertices.Items;
public int ClippedVerticesLength => _o.ClippedVertices.Count;
public int[] ClippedTriangles => _o.ClippedTriangles.Items;
public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
public float[] ClippedUVs => _o.ClippedUVs.Items;
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
=> _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
{
if (slot is Slot36 st && clippingAttachment is Attachments.ClippingAttachment36 att)
{
_o.ClipStart(st.InnerObject, att.InnerObject);
return;
}
throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
}
public void ClipEnd(ISlot slot)
{
if (slot is Slot36 st)
{
_o.ClipEnd(st.InnerObject);
return;
}
throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
}
public void ClipEnd() => _o.ClipEnd();
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36
{
internal sealed class Skin36 : ISkin
{
private readonly Skin _o;
/// <summary>
/// 使用指定名字创建空皮肤
/// </summary>
public Skin36(string name) => _o = new(name);
/// <summary>
/// 包装已有皮肤对象
/// </summary>
public Skin36(Skin innerObject) => _o = innerObject;
public Skin InnerObject => _o;
public string Name => _o.Name;
public void AddSkin(ISkin skin)
{
if (skin is Skin36 sk)
{
// NOTE: 3.7 及以下不支持 AddSkin
foreach (var (k, v) in sk._o.Attachments)
_o.AddAttachment(k.slotIndex, k.name, v);
return;
}
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
}
public void Clear() => _o.Attachments.Clear();
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using SpineRuntime36;
namespace Spine.Implementations.SpineWrappers.V36
{
internal sealed class Slot36 : ISlot
{
private readonly Slot _o;
private readonly SpineObjectData36 _data;
private readonly Bone36 _bone;
private readonly SFML.Graphics.BlendMode _blendMode;
public Slot36(Slot innerObject, SpineObjectData36 data, Bone36 bone)
{
_o = innerObject;
_data = data;
_bone = bone;
_blendMode = _o.Data.BlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
};
}
public Slot InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public SFML.Graphics.BlendMode Blend => _blendMode;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public IBone Bone => _bone;
public Spine.SpineWrappers.Attachments.IAttachment? Attachment
{
get
{
if (_o.Attachment is Attachment att)
{
return _data.SlotAttachments[Name][att.Name];
}
return null;
}
set
{
if (value is null)
{
_o.Attachment = null;
return;
}
if (value is Attachments.Attachment36 att)
{
_o.Attachment = att.InnerObject;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public bool Disabled { get; set; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime36;
using Spine.Implementations.SpineWrappers.V36.Attachments;
namespace Spine.Implementations.SpineWrappers.V36
{
[SpineImplementation(3, 6)]
internal sealed class SpineObjectData36 : SpineObjectData
{
private readonly Atlas _atlas;
private readonly SkeletonData _skeletonData;
private readonly AnimationStateData _animationStateData;
private readonly ImmutableArray<ISkin> _skins;
private readonly FrozenDictionary<string, ISkin> _skinsByName;
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData36(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex)
{
_atlas.Dispose();
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据
_animationStateData = new AnimationStateData(_skeletonData);
// 整理皮肤和附件
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
List<ISkin> skins = [];
Dictionary<string, ISkin> skinsByName = [];
foreach (var s in _skeletonData.Skins)
{
var skin = new Skin36(s);
skins.Add(skin);
skinsByName[s.Name] = skin;
foreach (var (k, att) in s.Attachments)
{
var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = [];
attachments[att.Name] = att switch
{
RegionAttachment regionAtt => new RegionAttachment36(regionAtt),
MeshAttachment meshAtt => new MeshAttachment36(meshAtt),
ClippingAttachment clipAtt => new ClippingAttachment36(clipAtt),
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment36(bbAtt),
PathAttachment pathAtt => new PathAttachment36(pathAtt),
PointAttachment ptAtt => new PointAttachment36(ptAtt),
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
};
}
}
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
_skins = skins.ToImmutableArray();
_skinsByName = skinsByName.ToFrozenDictionary();
// 整理所有动画数据
List<IAnimation> animations = [];
Dictionary<string, IAnimation> animationsByName = [];
foreach (var a in _skeletonData.Animations)
{
var anime = new Animation36(a);
animations.Add(anime);
animationsByName[anime.Name] = anime;
}
_animations = animations.ToImmutableArray();
_animationsByName = animationsByName.ToFrozenDictionary();
}
public override string SkeletonVersion => _skeletonData.Version;
public override ImmutableArray<ISkin> Skins => _skins;
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
public override ImmutableArray<IAnimation> Animations => _animations;
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
protected override void DisposeAtlas() => _atlas.Dispose();
public override ISkeleton CreateSkeleton() => new Skeleton36(new(_skeletonData), this);
public override IAnimationState CreateAnimationState() => new AnimationState36(new(_animationStateData), this);
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping36();
public override ISkin CreateSkin(string name) => new Skin36(name);
}
}

View File

@@ -1,185 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime36;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V36
{
internal sealed class TrackEntry36(TrackEntry innerObject, AnimationState36 animationState, SpineObjectData36 data): ITrackEntry
{
private readonly TrackEntry _o = innerObject;
private readonly AnimationState36 _animationState = animationState;
private readonly SpineObjectData36 _data = data;
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public TrackEntry InnerObject => _o;
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Interrupt
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Interrupt += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Interrupt -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Dispose
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Dispose += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Dispose -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public int TrackIndex { get => _o.TrackIndex; }
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,23 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime37;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V37
{
internal sealed class Animation37(Animation innerObject) : IAnimation
{
private readonly Animation _o = innerObject;
public Animation InnerObject => _o;
public string Name => _o.Name;
public float Duration => _o.Duration;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,229 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37
{
internal sealed class AnimationState37(AnimationState innerObject, SpineObjectData37 data) : IAnimationState
{
private readonly AnimationState _o = innerObject;
private readonly SpineObjectData37 _data = data;
private readonly Dictionary<TrackEntry, TrackEntry37> _trackEntryPool = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public AnimationState InnerObject => _o;
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Interrupt
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Interrupt += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Interrupt -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Dispose
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Dispose += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Dispose -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public void Update(float delta) => _o.Update(delta);
public void Apply(ISkeleton skeleton)
{
if (skeleton is Skeleton37 skel)
{
_o.Apply(skel.InnerObject);
return;
}
throw new ArgumentException($"Received {skeleton.GetType().Name}", nameof(skeleton));
}
/// <summary>
/// 获取 <see cref="ITrackEntry"/> 对象, 不存在则创建
/// </summary>
public ITrackEntry GetTrackEntry(TrackEntry trackEntry)
{
if (!_trackEntryPool.TryGetValue(trackEntry, out var tr))
_trackEntryPool[trackEntry] = tr = new(trackEntry, this, _data);
return tr;
}
public IEnumerable<ITrackEntry?> IterTracks() => _o.Tracks.Select(t => t is null ? null : GetTrackEntry(t));
public ITrackEntry? GetCurrent(int index) { var t = _o.GetCurrent(index); return t is null ? null : GetTrackEntry(t); }
public void ClearTrack(int index) => _o.ClearTrack(index);
public void ClearTracks() => _o.ClearTracks();
public ITrackEntry SetAnimation(int trackIndex, string animationName, bool loop)
=> GetTrackEntry(_o.SetAnimation(trackIndex, animationName, loop));
public ITrackEntry SetAnimation(int trackIndex, IAnimation animation, bool loop)
{
if (animation is Animation37 anime)
return GetTrackEntry(_o.SetAnimation(trackIndex, anime.InnerObject, loop));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) => GetTrackEntry(_o.SetEmptyAnimation(trackIndex, mixDuration));
public void SetEmptyAnimations(float mixDuration) => _o.SetEmptyAnimations(mixDuration);
public ITrackEntry AddAnimation(int trackIndex, string animationName, bool loop, float delay)
=> GetTrackEntry(_o.AddAnimation(trackIndex, animationName, loop, delay));
public ITrackEntry AddAnimation(int trackIndex, IAnimation animation, bool loop, float delay)
{
if (animation is Animation37 anime)
return GetTrackEntry(_o.AddAnimation(trackIndex, anime.InnerObject, loop, delay));
throw new ArgumentException($"Received {animation.GetType().Name}", nameof(animation));
}
public ITrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay)
=> GetTrackEntry(_o.AddEmptyAnimation(trackIndex, mixDuration, delay));
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37.Attachments
{
internal abstract class Attachment37(Attachment innerObject) : IAttachment
{
private readonly Attachment _o = innerObject;
public virtual Attachment InnerObject => _o;
public string Name => _o.Name;
public abstract int ComputeWorldVertices(ISlot slot, ref float[] worldVertices);
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37.Attachments
{
internal sealed class BoundingBoxAttachment37(BoundingBoxAttachment innerObject) :
Attachment37(innerObject),
IBoundingBoxAttachment
{
private readonly BoundingBoxAttachment _o = innerObject;
public override BoundingBoxAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot37 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37.Attachments
{
internal sealed class ClippingAttachment37(ClippingAttachment innerObject) :
Attachment37(innerObject),
IClippingAttachment
{
private readonly ClippingAttachment _o = innerObject;
public override ClippingAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot37 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37.Attachments
{
internal sealed class MeshAttachment37(MeshAttachment innerObject) :
Attachment37(innerObject),
IMeshAttachment
{
private readonly MeshAttachment _o = innerObject;
public override MeshAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot37 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
public int[] Triangles => _o.Triangles;
public int HullLength => _o.HullLength;
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37.Attachments
{
internal sealed class PathAttachment37(PathAttachment innerObject) :
Attachment37(innerObject),
IPathAttachment
{
private readonly PathAttachment _o = innerObject;
public override PathAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot37 st)
{
var length = _o.WorldVerticesLength;
if (worldVertices.Length < length) worldVertices = new float[length];
_o.ComputeWorldVertices(st.InnerObject, worldVertices);
return length;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37.Attachments
{
internal sealed class PointAttachment37(PointAttachment innerObject) :
Attachment37(innerObject),
IPointAttachment
{
private readonly PointAttachment _o = innerObject;
public override PointAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot37 st)
{
if (worldVertices.Length < 2) worldVertices = new float[2];
_o.ComputeWorldPosition(st.InnerObject.Bone, out worldVertices[0], out worldVertices[1]);
return 2;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
}
}
}

View File

@@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers.Attachments;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37.Attachments
{
internal sealed class RegionAttachment37(RegionAttachment innerObject) :
Attachment37(innerObject),
IRegionAttachment
{
private readonly RegionAttachment _o = innerObject;
public override RegionAttachment InnerObject => _o;
public override int ComputeWorldVertices(Spine.SpineWrappers.ISlot slot, ref float[] worldVertices)
{
if (slot is Slot37 st)
{
if (worldVertices.Length < 8) worldVertices = new float[8];
_o.ComputeWorldVertices(st.InnerObject.Bone, worldVertices, 0);
return 8;
}
throw new ArgumentException($"Invalid slot type. Expected {nameof(Slot37)}, but received {slot.GetType().Name}", nameof(slot));
}
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public SFML.Graphics.Texture RendererObject => (SFML.Graphics.Texture)((AtlasRegion)_o.RendererObject).page.rendererObject;
public float[] UVs => _o.UVs;
}
}

View File

@@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37
{
internal sealed class Bone37(Bone innerObject, Bone37? parent = null) : IBone
{
private readonly Bone _o = innerObject;
private readonly Bone37? _parent = parent;
public Bone InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public IBone? Parent => _parent;
public bool Active => true; // NOTE: 3.7 及以下没有 Active 属性, 此处总是返回 true
public float Length => _o.Data.Length;
public float WorldX => _o.WorldX;
public float WorldY => _o.WorldY;
public float A => _o.A;
public float B => _o.B;
public float C => _o.C;
public float D => _o.D;
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Frozen;
using System.Collections.Immutable;
using Spine.SpineWrappers;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37
{
internal sealed class Skeleton37 : ISkeleton
{
private readonly Skeleton _o;
private readonly SpineObjectData37 _data;
private readonly ImmutableArray<IBone> _bones;
private readonly FrozenDictionary<string, IBone> _bonesByName;
private readonly ImmutableArray<ISlot> _slots;
private readonly FrozenDictionary<string, ISlot> _slotsByName;
private Skin37? _skin;
public Skeleton37(Skeleton innerObject, SpineObjectData37 data)
{
_o = innerObject;
_data = data;
List<Bone37> bones = [];
Dictionary<string, IBone> bonesByName = [];
foreach (var b in _o.Bones)
{
var bone = new Bone37(b, b.Parent is null ? null : bones[b.Parent.Data.Index]);
bones.Add(bone);
bonesByName[bone.Name] = bone;
}
_bones = bones.Cast<IBone>().ToImmutableArray();
_bonesByName = bonesByName.ToFrozenDictionary();
List<Slot37> slots = [];
Dictionary<string, ISlot> slotsByName = [];
foreach (var s in _o.Slots)
{
var slot = new Slot37(s, _data, bones[s.Bone.Data.Index]);
slots.Add(slot);
slotsByName[slot.Name] = slot;
}
_slots = slots.Cast<ISlot>().ToImmutableArray();
_slotsByName = slotsByName.ToFrozenDictionary();
}
public Skeleton InnerObject => _o;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public float X { get => _o.X; set => _o.X = value; }
public float Y { get => _o.Y; set => _o.Y = value; }
public float ScaleX { get => _o.ScaleX; set => _o.ScaleX = value; }
public float ScaleY { get => _o.ScaleY; set => _o.ScaleY = value; }
public ImmutableArray<IBone> Bones => _bones;
public FrozenDictionary<string, IBone> BonesByName => _bonesByName;
public ImmutableArray<ISlot> Slots => _slots;
public FrozenDictionary<string, ISlot> SlotsByName => _slotsByName;
public ISkin? Skin
{
get => _skin;
set
{
if (value is null)
{
_o.Skin = null;
_skin = null;
return;
}
if (value is Skin37 sk)
{
_o.Skin = sk.InnerObject;
_skin = sk;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public IEnumerable<ISlot> IterDrawOrder() => _o.DrawOrder.Select(s => _slots[s.Data.Index]);
public void UpdateCache() => _o.UpdateCache();
public void UpdateWorldTransform(ISkeleton.Physics physics) => _o.UpdateWorldTransform();
public void SetToSetupPose() => _o.SetToSetupPose();
public void SetBonesToSetupPose() => _o.SetBonesToSetupPose();
public void SetSlotsToSetupPose() => _o.SetSlotsToSetupPose();
public void Update(float delta) => _o.Update(delta);
public void GetBounds(out float x, out float y, out float w, out float h)
{
float[] _ = [];
_o.GetBounds(out x, out y, out w, out h, ref _);
}
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,56 +0,0 @@
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using Spine.Utils;
using SpineRuntime37;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V37
{
internal sealed class SkeletonClipping37 : ISkeletonClipping
{
private readonly SkeletonClipping _o = new();
public bool IsClipping => _o.IsClipping;
public float[] ClippedVertices => _o.ClippedVertices.Items;
public int ClippedVerticesLength => _o.ClippedVertices.Count;
public int[] ClippedTriangles => _o.ClippedTriangles.Items;
public int ClippedTrianglesLength => _o.ClippedTriangles.Count;
public float[] ClippedUVs => _o.ClippedUVs.Items;
public void ClipTriangles(float[] vertices, int verticesLength, int[] triangles, int trianglesLength, float[] uvs)
=> _o.ClipTriangles(vertices, verticesLength, triangles, trianglesLength, uvs);
public void ClipStart(ISlot slot, IClippingAttachment clippingAttachment)
{
if (slot is Slot37 st && clippingAttachment is Attachments.ClippingAttachment37 att)
{
_o.ClipStart(st.InnerObject, att.InnerObject);
return;
}
throw new ArgumentException($"Received {slot.GetType().Name} {clippingAttachment.GetType().Name}");
}
public void ClipEnd(ISlot slot)
{
if (slot is Slot37 st)
{
_o.ClipEnd(st.InnerObject);
return;
}
throw new ArgumentException($"Received {slot.GetType().Name}", nameof(slot));
}
public void ClipEnd() => _o.ClipEnd();
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.SpineWrappers;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37
{
internal sealed class Skin37 : ISkin
{
private readonly Skin _o;
/// <summary>
/// 使用指定名字创建空皮肤
/// </summary>
public Skin37(string name) => _o = new(name);
/// <summary>
/// 包装已有皮肤对象
/// </summary>
public Skin37(Skin innerObject) => _o = innerObject;
public Skin InnerObject => _o;
public string Name => _o.Name;
public void AddSkin(ISkin skin)
{
if (skin is Skin37 sk)
{
// NOTE: 3.7 及以下不支持 AddSkin
foreach (var (k, v) in sk._o.Attachments)
_o.AddAttachment(k.slotIndex, k.name, v);
return;
}
throw new ArgumentException($"Received {skin.GetType().Name}", nameof(skin));
}
public void Clear() => _o.Attachments.Clear();
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,80 +0,0 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using SpineRuntime37;
namespace Spine.Implementations.SpineWrappers.V37
{
internal sealed class Slot37 : ISlot
{
private readonly Slot _o;
private readonly SpineObjectData37 _data;
private readonly Bone37 _bone;
private readonly SFML.Graphics.BlendMode _blendMode;
public Slot37(Slot innerObject, SpineObjectData37 data, Bone37 bone)
{
_o = innerObject;
_data = data;
_bone = bone;
_blendMode = _o.Data.BlendMode switch
{
BlendMode.Normal => SFMLBlendMode.NormalPma,
BlendMode.Additive => SFMLBlendMode.AdditivePma,
BlendMode.Multiply => SFMLBlendMode.MultiplyPma,
BlendMode.Screen => SFMLBlendMode.ScreenPma,
_ => throw new NotImplementedException($"{_o.Data.BlendMode}"),
};
}
public Slot InnerObject => _o;
public string Name => _o.Data.Name;
public int Index => _o.Data.Index;
public SFML.Graphics.BlendMode Blend => _blendMode;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }
public float A { get => _o.A; set => _o.A = value; }
public IBone Bone => _bone;
public Spine.SpineWrappers.Attachments.IAttachment? Attachment
{
get
{
if (_o.Attachment is Attachment att)
{
return _data.SlotAttachments[Name][att.Name];
}
return null;
}
set
{
if (value is null)
{
_o.Attachment = null;
return;
}
if (value is Attachments.Attachment37 att)
{
_o.Attachment = att.InnerObject;
return;
}
throw new ArgumentException($"Received {value.GetType().Name}", nameof(value));
}
}
public bool Disabled { get; set; }
public override string ToString() => _o.ToString();
}
}

View File

@@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spine.Utils;
using Spine.SpineWrappers;
using Spine.SpineWrappers.Attachments;
using SpineRuntime37;
using Spine.Implementations.SpineWrappers.V37.Attachments;
namespace Spine.Implementations.SpineWrappers.V37
{
[SpineImplementation(3, 7)]
internal sealed class SpineObjectData37 : SpineObjectData
{
private readonly Atlas _atlas;
private readonly SkeletonData _skeletonData;
private readonly AnimationStateData _animationStateData;
private readonly ImmutableArray<ISkin> _skins;
private readonly FrozenDictionary<string, ISkin> _skinsByName;
private readonly FrozenDictionary<string, FrozenDictionary<string, IAttachment>> _slotAttachments;
private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData37(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{
// 加载 atlas
try
{
_atlas = new Atlas(atlasPath, textureLoader);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
}
try
{
if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex)
{
_atlas.Dispose();
_logger.Trace(ex.ToString());
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
}
// 加载动画数据
_animationStateData = new AnimationStateData(_skeletonData);
// 整理皮肤和附件
Dictionary<string, Dictionary<string, IAttachment>> slotAttachments = [];
List<ISkin> skins = [];
Dictionary<string, ISkin> skinsByName = [];
foreach (var s in _skeletonData.Skins)
{
var skin = new Skin37(s);
skins.Add(skin);
skinsByName[s.Name] = skin;
foreach (var (k, att) in s.Attachments)
{
var slotName = _skeletonData.Slots.Items[k.slotIndex].Name;
if (!slotAttachments.TryGetValue(slotName, out var attachments))
slotAttachments[slotName] = attachments = [];
attachments[att.Name] = att switch
{
RegionAttachment regionAtt => new RegionAttachment37(regionAtt),
MeshAttachment meshAtt => new MeshAttachment37(meshAtt),
ClippingAttachment clipAtt => new ClippingAttachment37(clipAtt),
BoundingBoxAttachment bbAtt => new BoundingBoxAttachment37(bbAtt),
PathAttachment pathAtt => new PathAttachment37(pathAtt),
PointAttachment ptAtt => new PointAttachment37(ptAtt),
_ => throw new InvalidOperationException($"Unrecognized attachment type {att.GetType().FullName}")
};
}
}
_slotAttachments = slotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.ToFrozenDictionary());
_skins = skins.ToImmutableArray();
_skinsByName = skinsByName.ToFrozenDictionary();
// 整理所有动画数据
List<IAnimation> animations = [];
Dictionary<string, IAnimation> animationsByName = [];
foreach (var a in _skeletonData.Animations)
{
var anime = new Animation37(a);
animations.Add(anime);
animationsByName[anime.Name] = anime;
}
_animations = animations.ToImmutableArray();
_animationsByName = animationsByName.ToFrozenDictionary();
}
public override string SkeletonVersion => _skeletonData.Version;
public override ImmutableArray<ISkin> Skins => _skins;
public override FrozenDictionary<string, ISkin> SkinsByName => _skinsByName;
public override FrozenDictionary<string, FrozenDictionary<string, IAttachment>> SlotAttachments => _slotAttachments;
public override float DefaultMix { get => _animationStateData.DefaultMix; set => _animationStateData.DefaultMix = value; }
public override ImmutableArray<IAnimation> Animations => _animations;
public override FrozenDictionary<string, IAnimation> AnimationsByName => _animationsByName;
protected override void DisposeAtlas() => _atlas.Dispose();
public override ISkeleton CreateSkeleton() => new Skeleton37(new(_skeletonData), this);
public override IAnimationState CreateAnimationState() => new AnimationState37(new(_animationStateData), this);
public override ISkeletonClipping CreateSkeletonClipping() => new SkeletonClipping37();
public override ISkin CreateSkin(string name) => new Skin37(name);
}
}

View File

@@ -1,185 +0,0 @@
using Spine.SpineWrappers;
using SpineRuntime37;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spine.Implementations.SpineWrappers.V37
{
internal sealed class TrackEntry37(TrackEntry innerObject, AnimationState37 animationState, SpineObjectData37 data): ITrackEntry
{
private readonly TrackEntry _o = innerObject;
private readonly AnimationState37 _animationState = animationState;
private readonly SpineObjectData37 _data = data;
private readonly Dictionary<IAnimationState.TrackEntryDelegate, AnimationState.TrackEntryDelegate> _eventMapping = [];
private readonly Dictionary<IAnimationState.TrackEntryDelegate, int> _eventCount = [];
public TrackEntry InnerObject => _o;
public event IAnimationState.TrackEntryDelegate? Start
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Start += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Start -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Interrupt
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Interrupt += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Interrupt -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? End
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.End += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.End -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Complete
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Complete += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Complete -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public event IAnimationState.TrackEntryDelegate? Dispose
{
add
{
if (value is null) return;
if (!_eventMapping.TryGetValue(value, out var f))
{
_eventMapping[value] = f = (TrackEntry t) => value(_animationState.GetTrackEntry(t));
_eventCount[value] = 0;
}
_o.Dispose += f;
_eventCount[value]++;
}
remove
{
if (value is null) return;
if (_eventMapping.TryGetValue(value, out var f))
{
_o.Dispose -= f;
_eventCount[value]--;
if (_eventCount[value] <= 0)
{
_eventMapping.Remove(value);
_eventCount.Remove(value);
}
}
}
}
public int TrackIndex { get => _o.TrackIndex; }
public IAnimation Animation { get => _data.AnimationsByName[_o.Animation.Name]; }
public ITrackEntry? Next { get { var t = _o.Next; return t is null ? null : _animationState.GetTrackEntry(t); } }
public bool Loop { get => _o.Loop; set => _o.Loop = value; }
public float TrackTime { get => _o.TrackTime; set => _o.TrackTime = value; }
public float TimeScale { get => _o.TimeScale; set => _o.TimeScale = value; }
public float Alpha { get => _o.Alpha; set => _o.Alpha = value; }
public float MixDuration { get => _o.MixDuration; set => _o.MixDuration = value; }
public override string ToString() => _o.ToString();
}
}

Some files were not shown because too many files have changed in this diff Show More