Merge pull request #53 from ww-rm/dev/wpf

Dev/wpf
This commit is contained in:
ww-rm
2025-06-18 01:39:18 +08:00
committed by GitHub
64 changed files with 2171 additions and 760 deletions

View File

@@ -1,45 +1,79 @@
name: Build & Release name: Build & Release
on: on:
push: pull_request:
tags: branches:
- 'v*.*.*' - main
types:
- closed
jobs: jobs:
build-release: build-release:
if: ${{ github.event.pull_request.merged == true }}
runs-on: windows-latest runs-on: windows-latest
env: env:
PROJECT_NAME: SpineViewer PROJECT_NAME: SpineViewer
VERSION: ${{ github.ref_name }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
fetch-tags: true
- name: Setup .NET SDK - name: Setup .NET SDK
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '8.0.x' dotnet-version: "8.0.x"
- name: Extract version from csproj
shell: pwsh
run: |
[xml]$proj = Get-Content "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj"
$VERSION_NUM = $proj.Project.PropertyGroup.Version
$VERSION_TAG = "v$VERSION_NUM"
"VERSION=$VERSION_TAG" >> $env:GITHUB_ENV
- name: Check Version Tag
shell: pwsh
run: |
if (-not $env:VERSION) {
Write-Error "Version tag not found in csproj file."
exit 1
}
Write-Host "Version tag found: $env:VERSION"
- name: Tag merge commit
shell: pwsh
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag $env:VERSION
git push --tags
- name: Publish FrameworkDependent version - name: Publish FrameworkDependent version
shell: pwsh
run: | run: |
dotnet publish ${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj -c Release -r win-x64 --sc false -o publish/${{ env.PROJECT_NAME }}-${{ env.VERSION }} dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION"
- name: Publish SelfContained version - name: Publish SelfContained version
shell: pwsh
run: | run: |
dotnet publish ${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj -c Release -r win-x64 --sc true -o publish/${{ env.PROJECT_NAME }}-${{ env.VERSION }}-SelfContained dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
- name: Create release directory - name: Create release directory
run: mkdir release shell: pwsh
run: |
New-Item -ItemType Directory -Path release -Force | Out-Null
- name: Compress FrameworkDependent version - name: Compress FrameworkDependent version
shell: pwsh shell: pwsh
run: | run: |
Compress-Archive -Path "publish/${env:PROJECT_NAME}-${env:VERSION}" -DestinationPath "release/${env:PROJECT_NAME}-${env:VERSION}.zip" -Force Compress-Archive -Path "publish\$env:PROJECT_NAME-$env:VERSION\*" -DestinationPath "release\$env:PROJECT_NAME-$env:VERSION.zip" -Force
- name: Compress SelfContained version - name: Compress SelfContained version
shell: pwsh shell: pwsh
run: | run: |
Compress-Archive -Path "publish/${env:PROJECT_NAME}-${env:VERSION}-SelfContained" -DestinationPath "release/${env:PROJECT_NAME}-${env:VERSION}-SelfContained.zip" -Force Compress-Archive -Path "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained\*" -DestinationPath "release\$env:PROJECT_NAME-$env:VERSION-SelfContained.zip" -Force
- name: Create GitHub Release - name: Create GitHub Release
id: create_release id: create_release

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath> <BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.0</Version> <Version>0.15.1</Version>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,111 +1,133 @@
# [SpineViewer](https://github.com/ww-rm/SpineViewer) # [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) [![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) [![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) [![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) [中文](README.md) | [English](README.en.md)
A *WYSIWYG* Spine file viewer & exporter. A simple and user-friendly Spine file viewer and exporter with multi-language support (Chinese/English/Japanese).
![previewer](img/preview.webp) ![previewer](img/preview.webp)
## Features ## Features
- Supports multiple Spine file versions * Supports multiple versions of Spine files.
- Drag & drop or copy/paste to open files in batch * Batch open files via drag-and-drop or copy-paste.
- List-based skeleton view with render layer management * Batch preview functionality.
- Multi-select list to batch-adjust skeleton parameters * List-based multi-skeleton viewing and render order management.
- Multi-track animation support * Batch adjustment of skeleton parameters using multi-selection.
- Skin / custom slot attachment configuration * Multi-track animation settings.
- Debug rendering mode * Skin and custom slot attachment settings.
- Fullscreen preview * Debug rendering support.
- Export to single-frame image, animated GIF/WebP/AVIF, video formats * Fullscreen preview mode.
- Batch export at multiple resolutions * Export to single frame/image sequence/animated GIF/video formats.
- Custom FFmpeg export parameters * Automatic resolution batch export.
- …and more * FFmpeg custom export support.
* Program parameter saving.
* ...
### Spine Version Support ### Supported Spine Versions
| Version | View & Export | Format Conversion | Version Conversion | | Version | View & Export |
| :------: | :-----------: | :---------------: | :----------------: | | :-----: | :------------------: |
| `2.1.x` | :white_check_mark: | | | | `2.1.x` | :white\_check\_mark: |
| `3.6.x` | :white_check_mark: | | | | `3.6.x` | :white\_check\_mark: |
| `3.7.x` | :white_check_mark: | | | | `3.7.x` | :white\_check\_mark: |
| `3.8.x` | :white_check_mark: | :white_check_mark: | | | `3.8.x` | :white\_check\_mark: |
| `4.0.x` | :white_check_mark: | | | | `4.0.x` | :white\_check\_mark: |
| `4.1.x` | :white_check_mark: | | | | `4.1.x` | :white\_check\_mark: |
| `4.2.x` | :white_check_mark: | :white_check_mark: | | | `4.2.x` | :white\_check\_mark: |
| `4.3.x` | | | | | `4.3.x` | |
More versions coming soon 🚀🚀🚀 More versions under development \:rocket: \:rocket: \:rocket:
### Supported Export Formats ### Supported Export Formats
| Export Format | Use Case | | Format | Use Case |
| --------------------- | ----------------------------------------------------------------------------------------- | | -------------- | ----------------------------------------------------------------------------- |
| Single Frame | Generate highresolution still images; pick any frame manually. | | Single Frame | Generate high-resolution images of models; manually adjust the desired frame. |
| Frame Sequence (PNG) | Lossless PNG sequences with alpha channel preserved. | | Frame Sequence | Supports PNG format with transparency and lossless compression. |
| GIF / WebP / AVIF | Perfect for quick animated previews. | | GIF/Video | Export preview animations or common video formats. |
| MP4 | The most widely compatible video format. | | Custom Export | Supports arbitrary FFmpeg parameters for custom, complex export needs. |
| 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 ## Installation
1. Go to the [Releases](https://github.com/ww-rm/SpineViewer/releases) page and download the ZIP. Download the compressed package from the [Release](https://github.com/ww-rm/SpineViewer/releases) page.
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. The software requires the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) to run.
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) Alternatively, download the package with the `SelfContained` suffix for standalone execution.
- Direct download: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z)
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).
## Usage ## Usage
### Importing Skeletons ### How to Change the Display Language
You can import Spine skeletons in three ways: In the menu, go to "File" -> "Preferences..." -> "Language," select your desired language, and confirm the change.
- Drag & drop or paste skeleton files or folders onto the model list. ### Basic Overview
- Use **File > Open** to batchopen multiple skeleton files.
- Use **File > Open Single Model** to open one at a time.
### Adjusting Content The program is organized into a left-right layout:
- Rightclick menu and keyboard shortcuts are available in the model list. You can multiselect to adjust parameters in batch. * **Left Panel:** Functionality panel.
- In the preview pane, you can also use mouse controls: * **Right Panel:** Preview display.
- **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.
Below the preview, playback controls let you scrub through the timeline like a basic player. The left panel includes three sub-panels:
### Exporting Content * **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.
Exports follow the “what you see is what you get” principle—your realtime preview is exactly what gets exported. Hover your mouse over buttons, labels, or input fields to see help text for most UI elements.
Key export options: ### Skeleton Import
- **Render Selected Only**: includes only the selected models in both preview and export. Drag-and-drop or paste skeleton files/directories into the Model panel.
- **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.
## More Alternatively, use the right-click menu in the Browse panel to import selected items.
Detailed usage and advanced tips are in the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). ### Content Adjustment
Encounter a bug or have a feature request? Open an [Issue](https://github.com/ww-rm/SpineViewer/issues).
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).
## Acknowledgements ## Acknowledgements
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes) * [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
- [SFML.Net](https://github.com/SFML/SFML.Net) * [SFML.Net](https://github.com/SFML/SFML.Net)
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore) * [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
* [HandyControl](https://github.com/HandyOrg/HandyControl)
* [NLog](https://github.com/NLog/NLog)
* [SkiaSharp](https://github.com/mono/SkiaSharp)
--- ---
If you find this project useful, please give it a and share it with others! *If you find this project helpful, please give it a \:star: and share it with others! :)*
[![Stargazers over time](https://starchart.cc/ww-rm/SpineViewer.svg?variant=adaptive)](https://starchart.cc/ww-rm/SpineViewer) [![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) [中文](README.md) | [English](README.en.md)
*所见即所得* 的 Spine 文件查看&导出程序. 一个简单好用的 Spine 文件查看&导出程序, 支持中/英/日多语言界面.
![previewer](img/preview.webp) ![previewer](img/preview.webp)
@@ -14,6 +14,7 @@
- 支持多版本 spine 文件 - 支持多版本 spine 文件
- 支持拖拽/复制粘贴批量打开文件 - 支持拖拽/复制粘贴批量打开文件
- 支持批量预览
- 支持列表式多骨骼查看和渲染层级管理 - 支持列表式多骨骼查看和渲染层级管理
- 支持列表多选批量设置骨骼参数 - 支持列表多选批量设置骨骼参数
- 支持多轨道动画设置 - 支持多轨道动画设置
@@ -23,20 +24,21 @@
- 支持单帧/动图/视频文件导出 - 支持单帧/动图/视频文件导出
- 支持自动分辨率批量导出 - 支持自动分辨率批量导出
- 支持 FFmpeg 自定义导出 - 支持 FFmpeg 自定义导出
- 支持程序参数保存
- ... - ...
### Spine 版本支持 ### Spine 版本支持
| 版本 | 查看&导出 | 格式转换 | 版本转换 | | 版本 | 查看&导出 |
| :---: | :---: | :---: | :---: | | :---: | :---: |
| `2.1.x` | :white_check_mark: | | | | `2.1.x` | :white_check_mark: |
| `3.6.x` | :white_check_mark: | | | | `3.6.x` | :white_check_mark: |
| `3.7.x` | :white_check_mark: | | | | `3.7.x` | :white_check_mark: |
| `3.8.x` | :white_check_mark: | :white_check_mark: | | | `3.8.x` | :white_check_mark: |
| `4.0.x` | :white_check_mark: | | | | `4.0.x` | :white_check_mark: |
| `4.1.x` | :white_check_mark: | | | | `4.1.x` | :white_check_mark: |
| `4.2.x` | :white_check_mark: | :white_check_mark: | | | `4.2.x` | :white_check_mark: |
| `4.3.x` | | | | | `4.3.x` | |
更多版本正在施工 :rocket: :rocket: :rocket: 更多版本正在施工 :rocket: :rocket: :rocket:
@@ -46,10 +48,7 @@
| --- | --- | | --- | --- |
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. | | 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. | | 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
| GIF/WebP/AVIF | 适合生成预览动图. | | 动图/视频 | 可以生成预览动图或者常见格式视频. |
| MP4 | 最常见的视频格式, 兼容性最好. |
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
| MKV/MOV | 适合折腾. |
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. | | 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
## 安装 ## 安装
@@ -60,21 +59,35 @@
也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行. 也可以下载带有 `SelfContained` 后缀的压缩包, 可以独立运行.
导出 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). 导出 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).
## 使用方法 ## 使用方法
### 如何修改显示语言
窗口菜单的 "文件" -> "首选项..." -> "语言", 选择你需要的语言并确认修改.
### 基本介绍
程序大致是左右布局, 左侧是功能面板, 右侧是画面.
左侧有三个子面板, 分别是:
- **浏览**. 该面板用于预览指定文件夹的内容, 并没有真正导入文件到程序. 在该面板可以为模型生成 webp 格式的预览图, 或者导入选中的模型.
- **模型**. 该面板记录导入并进行渲染的模型列表, 可以在这个面板设置与模型渲染相关的参数和渲染顺序, 以及一些与模型有关的功能.
- **画面**. 该面板用于设置右侧预览画面的参数.
绝大部分按钮或者标签或者输入框都可以通过鼠标指针悬停来获取帮助文本.
### 骨骼导入 ### 骨骼导入
有 3 种方式导入骨骼文件: 可以直接拖放/粘贴需要导入骨骼文件/目录到模型面板.
- 拖放/粘贴需要导入的骨骼文件/目录到模型列表 或者在浏览面板内右键菜单导入选中项.
- 从文件菜单里批量打开骨骼文件
- 从文件菜单选择单个模型打开
### 内容调整 ### 内容调整
模型列表支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整. 模型面板支持右键菜单以及部分快捷键, 并且可以多选进行模型参数的批量调整.
预览画面除了使用面板进行参数设置外, 支持部分鼠标动作: 预览画面除了使用面板进行参数设置外, 支持部分鼠标动作:
@@ -89,9 +102,10 @@
导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面. 导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面.
在模型面板里, 右键菜单可以对选中项进行导出操作.
导出有以下几个关键参数: 导出有以下几个关键参数:
- 仅渲染选中. 这个参数不仅影响预览模式, 也影响导出, 如果仅渲染选中, 那么在导出时只有被选中的模型会被考虑, 忽略其他模型.
- 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹. - 输出文件夹. 这个参数某些时候可选, 当不提供时, 则将输出产物输出到每个模型各自的模型文件夹, 否则输出产物全部输出到提供的输出文件夹.
- 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份. - 导出单个. 默认是每个模型独立导出, 即对模型列表进行批量操作, 如果选择仅导出单个, 那么被导出的所有模型将在同一个画面上被渲染, 输出产物只有一份.
- 自动分辨率. 该模式会忽略预览画面的分辨率和视区参数, 导出产物的分辨率与被导出内容的实际大小一致, 如果是动图或者视频则会与完整显示动画的必需大小一致. - 自动分辨率. 该模式会忽略预览画面的分辨率和视区参数, 导出产物的分辨率与被导出内容的实际大小一致, 如果是动图或者视频则会与完整显示动画的必需大小一致.
@@ -105,6 +119,9 @@
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes) - [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
- [SFML.Net](https://github.com/SFML/SFML.Net) - [SFML.Net](https://github.com/SFML/SFML.Net)
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore) - [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

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath> <BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.0</Version> <Version>0.15.1</Version>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>

View File

@@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V21
private readonly ImmutableArray<IAnimation> _animations; private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName; private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData21(string skelPath, string atlasPath) : base(skelPath, atlasPath) public SpineObjectData21(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{ {
// 加载 atlas // 加载 atlas
try { _atlas = new Atlas(atlasPath, _textureLoader); } try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try try
{ {
if (Utf8Validator.IsUtf8(skelPath)) if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
else }
catch
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
} }
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex) catch (Exception ex)
{ {
_atlas.Dispose(); _atlas.Dispose();

View File

@@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V36
private readonly ImmutableArray<IAnimation> _animations; private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName; private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData36(string skelPath, string atlasPath) : base(skelPath, atlasPath) public SpineObjectData36(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{ {
// 加载 atlas // 加载 atlas
try { _atlas = new Atlas(atlasPath, _textureLoader); } try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try try
{ {
if (Utf8Validator.IsUtf8(skelPath)) if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
else }
catch
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
} }
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex) catch (Exception ex)
{ {
_atlas.Dispose(); _atlas.Dispose();

View File

@@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V37
private readonly ImmutableArray<IAnimation> _animations; private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName; private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData37(string skelPath, string atlasPath) : base(skelPath, atlasPath) public SpineObjectData37(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{ {
// 加载 atlas // 加载 atlas
try { _atlas = new Atlas(atlasPath, _textureLoader); } try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try try
{ {
if (Utf8Validator.IsUtf8(skelPath)) if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
else }
catch
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
} }
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex) catch (Exception ex)
{ {
_atlas.Dispose(); _atlas.Dispose();

View File

@@ -27,19 +27,38 @@ namespace Spine.Implementations.SpineWrappers.V38
private readonly ImmutableArray<IAnimation> _animations; private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName; private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData38(string skelPath, string atlasPath) : base(skelPath, atlasPath) public SpineObjectData38(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{ {
// 加载 atlas // 加载 atlas
try { _atlas = new Atlas(atlasPath, _textureLoader); } try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
try try
{ {
if (Utf8Validator.IsUtf8(skelPath)) if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
else }
catch
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
} }
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex) catch (Exception ex)
{ {
_atlas.Dispose(); _atlas.Dispose();

View File

@@ -26,20 +26,39 @@ namespace Spine.Implementations.SpineWrappers.V40
private readonly ImmutableArray<IAnimation> _animations; private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName; private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData40(string skelPath, string atlasPath) : base(skelPath, atlasPath) public SpineObjectData40(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{ {
// 加载 atlas // 加载 atlas
try { _atlas = new Atlas(atlasPath, _textureLoader); } try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
// 加载 skel // 加载 skel
try try
{ {
if (Utf8Validator.IsUtf8(skelPath)) if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
else }
catch
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
} }
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex) catch (Exception ex)
{ {
_atlas.Dispose(); _atlas.Dispose();

View File

@@ -26,20 +26,39 @@ namespace Spine.Implementations.SpineWrappers.V41
private readonly ImmutableArray<IAnimation> _animations; private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName; private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData41(string skelPath, string atlasPath) : base(skelPath, atlasPath) public SpineObjectData41(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{ {
// 加载 atlas // 加载 atlas
try { _atlas = new Atlas(atlasPath, _textureLoader); } try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
// 加载 skel // 加载 skel
try try
{ {
if (Utf8Validator.IsUtf8(skelPath)) if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
else }
catch
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
} }
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex) catch (Exception ex)
{ {
_atlas.Dispose(); _atlas.Dispose();

View File

@@ -26,20 +26,39 @@ namespace Spine.Implementations.SpineWrappers.V42
private readonly ImmutableArray<IAnimation> _animations; private readonly ImmutableArray<IAnimation> _animations;
private readonly FrozenDictionary<string, IAnimation> _animationsByName; private readonly FrozenDictionary<string, IAnimation> _animationsByName;
public SpineObjectData42(string skelPath, string atlasPath) : base(skelPath, atlasPath) public SpineObjectData42(string skelPath, string atlasPath, Spine.SpineWrappers.TextureLoader textureLoader)
: base(skelPath, atlasPath, textureLoader)
{ {
// 加载 atlas // 加载 atlas
try { _atlas = new Atlas(atlasPath, _textureLoader); } try { _atlas = new Atlas(atlasPath, textureLoader); }
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); } catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
// 加载 skel // 加载 skel
try try
{ {
if (Utf8Validator.IsUtf8(skelPath)) if (Utf8Validator.IsUtf8(skelPath))
{
try
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
else }
catch
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath); _skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
} }
}
else
{
try
{
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
}
catch
{
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
}
}
}
catch (Exception ex) catch (Exception ex)
{ {
_atlas.Dispose(); _atlas.Dispose();

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath> <BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.0</Version> <Version>0.15.1</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -21,7 +21,6 @@ namespace Spine
{ {
[".skel"] = ".atlas", [".skel"] = ".atlas",
[".json"] = ".atlas", [".json"] = ".atlas",
[".skel.bytes"] = ".atlas.bytes",
}.ToFrozenDictionary(); }.ToFrozenDictionary();
/// <summary> /// <summary>
@@ -45,10 +44,12 @@ namespace Spine
/// <param name="skelPath">skel 文件路径</param> /// <param name="skelPath">skel 文件路径</param>
/// <param name="atlasPath">atlas 文件路径, 为空时会根据 <paramref name="skelPath"/> 进行自动检测</param> /// <param name="atlasPath">atlas 文件路径, 为空时会根据 <paramref name="skelPath"/> 进行自动检测</param>
/// <param name="version">要使用的运行时版本, 为空时会自动检测</param> /// <param name="version">要使用的运行时版本, 为空时会自动检测</param>
public SpineObject(string skelPath, string? atlasPath = null, SpineVersion? version = null) public SpineObject(string skelPath, string? atlasPath = null, SpineVersion? version = null, TextureLoader? textureLoader = null)
{ {
if (string.IsNullOrWhiteSpace(skelPath)) throw new ArgumentException(skelPath, nameof(skelPath)); if (string.IsNullOrWhiteSpace(skelPath)) throw new ArgumentException(skelPath, nameof(skelPath));
if (!File.Exists(skelPath)) throw new FileNotFoundException($"{nameof(skelPath)} not found", skelPath); if (!File.Exists(skelPath)) throw new FileNotFoundException($"{nameof(skelPath)} not found", skelPath);
textureLoader ??= TextureLoader.DefaultLoader;
SkelPath = Path.GetFullPath(skelPath); SkelPath = Path.GetFullPath(skelPath);
AssetsDir = Directory.GetParent(skelPath).FullName; AssetsDir = Directory.GetParent(skelPath).FullName;
Name = Path.GetFileNameWithoutExtension(skelPath); Name = Path.GetFileNameWithoutExtension(skelPath);
@@ -91,7 +92,7 @@ namespace Spine
{ {
try try
{ {
_data = SpineObjectData.New(v, skelPath, atlasPath); _data = SpineObjectData.New(v, skelPath, atlasPath, textureLoader);
Version = v; Version = v;
break; break;
} }
@@ -109,7 +110,7 @@ namespace Spine
{ {
// 根据版本实例化对象 // 根据版本实例化对象
Version = version; Version = version;
_data = SpineObjectData.New(Version, skelPath, atlasPath); _data = SpineObjectData.New(Version, skelPath, atlasPath, textureLoader);
} }
// 创建状态实例 // 创建状态实例
@@ -177,7 +178,6 @@ namespace Spine
// 拷贝调试属性 // 拷贝调试属性
EnableDebug = other.EnableDebug; EnableDebug = other.EnableDebug;
DebugTexture = other.DebugTexture; DebugTexture = other.DebugTexture;
DebugNonTexture = other.DebugNonTexture;
DebugBounds = other.DebugBounds; DebugBounds = other.DebugBounds;
DebugBones = other.DebugBones; DebugBones = other.DebugBones;
DebugRegions = other.DebugRegions; DebugRegions = other.DebugRegions;
@@ -236,7 +236,7 @@ namespace Spine
/// <summary> /// <summary>
/// 是否使用预乘 Alpha /// 是否使用预乘 Alpha
/// </summary> /// </summary>
public bool UsePma { get; set; } = false; public bool UsePma { get; set; }
/// <summary> /// <summary>
/// 物理约束更新方式 /// 物理约束更新方式
@@ -246,62 +246,57 @@ namespace Spine
/// <summary> /// <summary>
/// 启用渲染调试, 将会使所有 <c>DebugXXX</c> 属性生效 /// 启用渲染调试, 将会使所有 <c>DebugXXX</c> 属性生效
/// </summary> /// </summary>
public bool EnableDebug { get; set; } = false; public bool EnableDebug { get; set; }
/// <summary> /// <summary>
/// 显示纹理 /// 显示纹理
/// </summary> /// </summary>
public bool DebugTexture { get; set; } = true; public bool DebugTexture { get; set; } = true;
/// <summary>
/// 是否显示非纹理内容, 一个总开关
/// </summary>
public bool DebugNonTexture { get; set; } = true;
/// <summary> /// <summary>
/// 显示包围盒 /// 显示包围盒
/// </summary> /// </summary>
public bool DebugBounds { get; set; } = true; public bool DebugBounds { get; set; }
/// <summary> /// <summary>
/// 显示骨骼 /// 显示骨骼
/// </summary> /// </summary>
public bool DebugBones { get; set; } = false; public bool DebugBones { get; set; }
/// <summary> /// <summary>
/// 显示区域附件边框 /// 显示区域附件边框
/// </summary> /// </summary>
public bool DebugRegions { get; set; } = false; public bool DebugRegions { get; set; }
/// <summary> /// <summary>
/// 显示网格附件边框线 /// 显示网格附件边框线
/// </summary> /// </summary>
public bool DebugMeshHulls { get; set; } = false; public bool DebugMeshHulls { get; set; }
/// <summary> /// <summary>
/// 显示网格附件网格线 /// 显示网格附件网格线
/// </summary> /// </summary>
public bool DebugMeshes { get; set; } = false; public bool DebugMeshes { get; set; }
/// <summary> /// <summary>
/// 显示碰撞盒附件边框线 /// 显示碰撞盒附件边框线
/// </summary> /// </summary>
public bool DebugBoundingBoxes { get; set; } = false; public bool DebugBoundingBoxes { get; set; }
/// <summary> /// <summary>
/// 显示路径附件网格线 /// 显示路径附件网格线
/// </summary> /// </summary>
public bool DebugPaths { get; set; } = false; public bool DebugPaths { get; set; }
/// <summary> /// <summary>
/// 显示点附件 /// 显示点附件
/// </summary> /// </summary>
public bool DebugPoints { get; set; } = false; public bool DebugPoints { get; set; }
/// <summary> /// <summary>
/// 显示剪裁附件网格线 /// 显示剪裁附件网格线
/// </summary> /// </summary>
public bool DebugClippings { get; set; } = false; public bool DebugClippings { get; set; }
/// <summary> /// <summary>
/// 获取某个插槽上的附件名, 插槽不存在或者无附件均返回 null /// 获取某个插槽上的附件名, 插槽不存在或者无附件均返回 null
@@ -864,7 +859,7 @@ namespace Spine
else else
{ {
if (DebugTexture) DrawTexture(target, states); if (DebugTexture) DrawTexture(target, states);
if (DebugNonTexture) DrawNonTexture(target); DrawNonTexture(target);
} }
} }

View File

@@ -20,18 +20,13 @@ namespace Spine.SpineWrappers
/// <summary> /// <summary>
/// 构建版本对象 /// 构建版本对象
/// </summary> /// </summary>
public static SpineObjectData New(SpineVersion version, string skelPath, string atlasPath) => CreateInstance(version.Tag, skelPath, atlasPath); public static SpineObjectData New(SpineVersion version, string skelPath, string atlasPath, TextureLoader textureLoader)
=> CreateInstance(version.Tag, skelPath, atlasPath, textureLoader);
/// <summary>
/// 纹理加载器, 可以设置一些预置参数
/// </summary>
public static TextureLoader TextureLoader => _textureLoader;
protected static readonly TextureLoader _textureLoader = new();
/// <summary> /// <summary>
/// 构造函数, 继承的子类应当实现一个相同签名的构造函数 /// 构造函数, 继承的子类应当实现一个相同签名的构造函数
/// </summary> /// </summary>
public SpineObjectData(string skelPath, string atlasPath) { } public SpineObjectData(string skelPath, string atlasPath, TextureLoader textureLoader) { }
public abstract string SkeletonVersion { get; } public abstract string SkeletonVersion { get; }

View File

@@ -19,23 +19,62 @@ namespace Spine.SpineWrappers
SpineRuntime42.TextureLoader SpineRuntime42.TextureLoader
{ {
/// <summary> /// <summary>
/// 强制启用 Smooth /// 默认的全局纹理加载器
/// </summary> /// </summary>
public bool ForceSmooth { get; set; } = false; public static TextureLoader DefaultLoader { get; } = new();
/// <summary> /// <summary>
/// 强制启用 Repeated /// 在读取纹理时强制进行通道预乘操作
/// </summary> /// </summary>
public bool ForceRepeated { get; set; } = false; public bool ForcePremul { get; set; }
/// <summary>
/// 强制使用 Nearest
/// </summary>
public bool ForceNearest { get; set; }
/// <summary> /// <summary>
/// 强制启用 Mipmap /// 强制启用 Mipmap
/// </summary> /// </summary>
public bool ForceMipmap { get; set; } = false; public bool ForceMipmap { get; set; }
public void Load(SpineRuntime21.AtlasPage page, string path) private SFML.Graphics.Texture ReadTexture(string path)
{ {
var texture = new SFML.Graphics.Texture(path); if (ForcePremul)
{
using var image = new SFML.Graphics.Image(path);
var width = image.Size.X;
var height = image.Size.Y;
var pixels = image.Pixels;
var size = width * height * 4;
for (int i = 0; i < size; i += 4)
{
byte a = pixels[i + 3];
if (a == 0)
{
pixels[i + 0] = 0;
pixels[i + 1] = 0;
pixels[i + 2] = 0;
}
else if (a != 255)
{
float f = a / 255f;
pixels[i + 0] = (byte)(pixels[i + 0] * f);
pixels[i + 1] = (byte)(pixels[i + 1] * f);
pixels[i + 2] = (byte)(pixels[i + 2] * f);
}
}
var tex = new SFML.Graphics.Texture(width, height);
tex.Update(pixels);
return tex;
}
return new(path);
}
public virtual void Load(SpineRuntime21.AtlasPage page, string path)
{
var texture = ReadTexture(path);
if (page.magFilter == SpineRuntime21.TextureFilter.Linear) if (page.magFilter == SpineRuntime21.TextureFilter.Linear)
{ {
texture.Smooth = true; texture.Smooth = true;
@@ -61,16 +100,16 @@ namespace Spine.SpineWrappers
break; break;
} }
if (ForceSmooth) texture.Smooth = true; if (ForceNearest) texture.Smooth = false;
if (ForceRepeated) texture.Repeated = true;
if (ForceMipmap) texture.GenerateMipmap(); if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture; page.rendererObject = texture;
} }
public void Load(SpineRuntime36.AtlasPage page, string path) public virtual void Load(SpineRuntime36.AtlasPage page, string path)
{ {
var texture = new SFML.Graphics.Texture(path); var texture = ReadTexture(path);
if (page.magFilter == SpineRuntime36.TextureFilter.Linear) if (page.magFilter == SpineRuntime36.TextureFilter.Linear)
{ {
texture.Smooth = true; texture.Smooth = true;
@@ -96,16 +135,16 @@ namespace Spine.SpineWrappers
break; break;
} }
if (ForceSmooth) texture.Smooth = true; if (ForceNearest) texture.Smooth = false;
if (ForceRepeated) texture.Repeated = true;
if (ForceMipmap) texture.GenerateMipmap(); if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture; page.rendererObject = texture;
} }
public void Load(SpineRuntime37.AtlasPage page, string path) public virtual void Load(SpineRuntime37.AtlasPage page, string path)
{ {
var texture = new SFML.Graphics.Texture(path); var texture = ReadTexture(path);
if (page.magFilter == SpineRuntime37.TextureFilter.Linear) if (page.magFilter == SpineRuntime37.TextureFilter.Linear)
{ {
texture.Smooth = true; texture.Smooth = true;
@@ -131,16 +170,16 @@ namespace Spine.SpineWrappers
break; break;
} }
if (ForceSmooth) texture.Smooth = true; if (ForceNearest) texture.Smooth = false;
if (ForceRepeated) texture.Repeated = true;
if (ForceMipmap) texture.GenerateMipmap(); if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture; page.rendererObject = texture;
} }
public void Load(SpineRuntime38.AtlasPage page, string path) public virtual void Load(SpineRuntime38.AtlasPage page, string path)
{ {
var texture = new SFML.Graphics.Texture(path); var texture = ReadTexture(path);
if (page.magFilter == SpineRuntime38.TextureFilter.Linear) if (page.magFilter == SpineRuntime38.TextureFilter.Linear)
{ {
texture.Smooth = true; texture.Smooth = true;
@@ -166,16 +205,20 @@ namespace Spine.SpineWrappers
break; break;
} }
if (ForceSmooth) texture.Smooth = true; if (ForceNearest) texture.Smooth = false;
if (ForceRepeated) texture.Repeated = true;
if (ForceMipmap) texture.GenerateMipmap(); if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture; page.rendererObject = texture;
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
// page.width = (int)texture.Size.X;
// page.height = (int)texture.Size.Y;
} }
public void Load(SpineRuntime40.AtlasPage page, string path) public virtual void Load(SpineRuntime40.AtlasPage page, string path)
{ {
var texture = new SFML.Graphics.Texture(path); var texture = ReadTexture(path);
if (page.magFilter == SpineRuntime40.TextureFilter.Linear) if (page.magFilter == SpineRuntime40.TextureFilter.Linear)
{ {
texture.Smooth = true; texture.Smooth = true;
@@ -201,16 +244,16 @@ namespace Spine.SpineWrappers
break; break;
} }
if (ForceSmooth) texture.Smooth = true; if (ForceNearest) texture.Smooth = false;
if (ForceRepeated) texture.Repeated = true;
if (ForceMipmap) texture.GenerateMipmap(); if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture; page.rendererObject = texture;
} }
public void Load(SpineRuntime41.AtlasPage page, string path) public virtual void Load(SpineRuntime41.AtlasPage page, string path)
{ {
var texture = new SFML.Graphics.Texture(path); var texture = ReadTexture(path);
if (page.magFilter == SpineRuntime41.TextureFilter.Linear) if (page.magFilter == SpineRuntime41.TextureFilter.Linear)
{ {
texture.Smooth = true; texture.Smooth = true;
@@ -236,16 +279,16 @@ namespace Spine.SpineWrappers
break; break;
} }
if (ForceSmooth) texture.Smooth = true; if (ForceNearest) texture.Smooth = false;
if (ForceRepeated) texture.Repeated = true;
if (ForceMipmap) texture.GenerateMipmap(); if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture; page.rendererObject = texture;
} }
public void Load(SpineRuntime42.AtlasPage page, string path) public virtual void Load(SpineRuntime42.AtlasPage page, string path)
{ {
var texture = new SFML.Graphics.Texture(path); var texture = ReadTexture(path);
if (page.magFilter == SpineRuntime42.TextureFilter.Linear) if (page.magFilter == SpineRuntime42.TextureFilter.Linear)
{ {
texture.Smooth = true; texture.Smooth = true;
@@ -271,18 +314,13 @@ namespace Spine.SpineWrappers
break; break;
} }
if (ForceSmooth) texture.Smooth = true; if (ForceNearest) texture.Smooth = false;
if (ForceRepeated) texture.Repeated = true;
if (ForceMipmap) texture.GenerateMipmap(); if (ForceMipmap) texture.GenerateMipmap();
page.rendererObject = texture; page.rendererObject = texture;
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
// page.width = (int)texture.Size.X;
// page.height = (int)texture.Size.Y;
} }
public void Unload(object texture) public virtual void Unload(object texture)
{ {
((SFML.Graphics.Texture)texture).Dispose(); ((SFML.Graphics.Texture)texture).Dispose();
} }

View File

@@ -11,7 +11,7 @@
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/> <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/> <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
<ResourceDictionary Source="/Resources/Geometries.xaml"/> <ResourceDictionary Source="/Resources/Geometries.xaml"/>
<ResourceDictionary Source="/Resources/Strings/zh-cn.xaml"/> <ResourceDictionary Source="/Resources/Strings/zh.xaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource ToggleButtonSwitch}"> <Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource ToggleButtonSwitch}">

View File

@@ -1,5 +1,6 @@
using NLog; using NLog;
using SpineViewer.Views; using SpineViewer.Views;
using System.Collections.Frozen;
using System.Configuration; using System.Configuration;
using System.Data; using System.Data;
using System.Diagnostics; using System.Diagnostics;
@@ -7,13 +8,13 @@ using System.Globalization;
using System.Reflection; using System.Reflection;
using System.Windows; using System.Windows;
namespace SpineViewer; namespace SpineViewer
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{ {
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion; public static string Version => Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
private static readonly Logger _logger; private static readonly Logger _logger;
@@ -66,18 +67,9 @@ public partial class App : Application
var uiCulture = CultureInfo.CurrentUICulture.Name.ToLowerInvariant(); var uiCulture = CultureInfo.CurrentUICulture.Name.ToLowerInvariant();
_logger.Info("Current UI Culture: {0}", uiCulture); _logger.Info("Current UI Culture: {0}", uiCulture);
if (uiCulture.StartsWith("zh")) if (uiCulture.StartsWith("zh")) { } // 默认就是中文, 无需操作
{ else if (uiCulture.StartsWith("ja")) Language = AppLanguage.JA;
; // 默认就是中文, 无需操作 else Language = AppLanguage.EN;
}
else if (uiCulture.StartsWith("ja"))
{
dict.Source = new("Resources/Strings/ja-jp.xaml", UriKind.Relative);
}
else
{
dict.Source = new("Resources/Strings/en-us.xaml", UriKind.Relative);
}
Resources.MergedDictionaries.Add(dict); Resources.MergedDictionaries.Add(dict);
} }
@@ -88,5 +80,36 @@ public partial class App : Application
_logger.Error("Dispatcher unhandled exception: {0}", e.Exception.Message); _logger.Error("Dispatcher unhandled exception: {0}", e.Exception.Message);
e.Handled = true; e.Handled = true;
} }
}
/// <summary>
/// 程序语言
/// </summary>
public AppLanguage Language
{
get => _language;
set
{
var uri = $"Resources/Strings/{value.ToString().ToLower()}.xaml";
try
{
Resources.MergedDictionaries.Add(new() { Source = new(uri, UriKind.Relative) });
_language = value;
}
catch (Exception ex)
{
_logger.Error("Failed to switch language to {0}, {1}", value, ex.Message);
_logger.Trace(ex.ToString());
}
}
}
private AppLanguage _language = AppLanguage.ZH;
}
public enum AppLanguage
{
ZH,
EN,
JA
}
}

View File

@@ -49,7 +49,7 @@ namespace SpineViewer.Extensions
public static Rect GetCurrentBounds(this SpineObject self) public static Rect GetCurrentBounds(this SpineObject self)
{ {
self.Skeleton.GetBounds(out var x, out var y, out var w, out var h); self.Skeleton.GetBounds(out var x, out var y, out var w, out var h);
return new(x, y, w, h); return new(x, y, Math.Max(w, 1e-6f), Math.Max(h, 1e-6f));
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,84 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Spine.SpineWrappers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows.Media;
namespace SpineViewer.Models
{
/// <summary>
/// 首选项参数模型, 用于对话框修改以及本地保存
/// </summary>
public partial class PreferenceModel : ObservableObject
{
#region
[ObservableProperty]
private bool _forcePremul;
[ObservableProperty]
private bool _forceNearest;
[ObservableProperty]
private bool _forceMipmap;
#endregion
#region
[ObservableProperty]
private bool _isShown = true;
[ObservableProperty]
private bool _usePma;
[ObservableProperty]
private bool _debugTexture = true;
[ObservableProperty]
private bool _debugBounds;
[ObservableProperty]
private bool _debugBones;
[ObservableProperty]
private bool _debugRegions;
[ObservableProperty]
private bool _debugMeshHulls;
[ObservableProperty]
private bool _debugMeshes;
[ObservableProperty]
private bool _debugBoundingBoxes;
[ObservableProperty]
private bool _debugPaths;
[ObservableProperty]
private bool _debugPoints;
[ObservableProperty]
private bool _debugClippings;
#endregion
#region
[ObservableProperty]
private bool _renderSelectedOnly;
[ObservableProperty]
private AppLanguage _appLanguage;
#endregion
}
}

View File

@@ -13,38 +13,6 @@ namespace SpineViewer.Models
{ {
public class SpineObjectConfigModel public class SpineObjectConfigModel
{ {
/// <summary>
/// 保存 Json 文件的格式参数
/// </summary>
private static readonly JsonSerializerOptions _jsonOptions = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
/// <summary>
/// 从文件反序列对象, 可能抛出异常
/// </summary>
public static SpineObjectConfigModel Deserialize(string path)
{
if (!File.Exists(path)) throw new FileNotFoundException("Config file not found", path);
var json = File.ReadAllText(path, Encoding.UTF8);
var model = JsonSerializer.Deserialize<SpineObjectConfigModel>(json, _jsonOptions);
return model ?? throw new JsonException($"null data in file '{path}'");
}
/// <summary>
/// 保存预设至文件, 概率抛出异常
/// </summary>
public void Serialize(string path)
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
var json = JsonSerializer.Serialize(this, _jsonOptions);
File.WriteAllText(path, json, Encoding.UTF8);
}
public bool UsePma { get; set; } public bool UsePma { get; set; }
public string Physics { get; set; } = ISkeleton.Physics.Update.ToString(); public string Physics { get; set; } = ISkeleton.Physics.Update.ToString();
@@ -67,7 +35,7 @@ namespace SpineViewer.Models
public bool DebugTexture { get; set; } = true; public bool DebugTexture { get; set; } = true;
public bool DebugBounds { get; set; } = true; public bool DebugBounds { get; set; }
public bool DebugBones { get; set; } public bool DebugBones { get; set; }

View File

@@ -25,6 +25,12 @@ namespace SpineViewer.Models
/// </summary> /// </summary>
public class SpineObjectModel : ObservableObject, SFML.Graphics.Drawable, IDisposable public class SpineObjectModel : ObservableObject, SFML.Graphics.Drawable, IDisposable
{ {
/// <summary>
/// 一些加载默认选项
/// </summary>
public static SpineObjectLoadOptions LoadOptions => _loadOptions;
private static readonly SpineObjectLoadOptions _loadOptions = new();
/// <summary> /// <summary>
/// 日志器 /// 日志器
/// </summary> /// </summary>
@@ -40,9 +46,21 @@ namespace SpineViewer.Models
/// <summary> /// <summary>
/// 构造函数, 可能会抛出异常 /// 构造函数, 可能会抛出异常
/// </summary> /// </summary>
public SpineObjectModel(string skelPath, string? atlasPath = null, SpineVersion? version = null) public SpineObjectModel(string skelPath, string? atlasPath = null)
{ {
_spineObject = new(skelPath, atlasPath, version) { DebugNonTexture = false }; _spineObject = new(skelPath, atlasPath)
{
UsePma = _loadOptions.UsePma,
DebugTexture = _loadOptions.DebugTexture,
DebugBounds = _loadOptions.DebugBounds,
DebugRegions = _loadOptions.DebugRegions,
DebugMeshHulls = _loadOptions.DebugMeshHulls,
DebugMeshes = _loadOptions.DebugMeshes,
DebugBoundingBoxes = _loadOptions.DebugBoundingBoxes,
DebugPaths = _loadOptions.DebugPaths,
DebugPoints = _loadOptions.DebugPoints,
DebugClippings = _loadOptions.DebugClippings
};
_skins = _spineObject.Data.Skins.Select(v => v.Name).ToImmutableArray(); _skins = _spineObject.Data.Skins.Select(v => v.Name).ToImmutableArray();
_slotAttachments = _spineObject.Data.SlotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.Keys); _slotAttachments = _spineObject.Data.SlotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.Keys);
_animations = _spineObject.Data.Animations.Select(v => v.Name).ToImmutableArray(); _animations = _spineObject.Data.Animations.Select(v => v.Name).ToImmutableArray();
@@ -52,6 +70,19 @@ namespace SpineViewer.Models
_spineObject.AnimationState.SetAnimation(0, _spineObject.Data.Animations[0], true); _spineObject.AnimationState.SetAnimation(0, _spineObject.Data.Animations[0], true);
} }
/// <summary>
/// 从工作区配置进行构造
/// </summary>
public SpineObjectModel(SpineObjectWorkspaceConfigModel cfg)
{
_spineObject = new(cfg.SkelPath, cfg.AtlasPath);
_skins = _spineObject.Data.Skins.Select(v => v.Name).ToImmutableArray();
_slotAttachments = _spineObject.Data.SlotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.Keys);
_animations = _spineObject.Data.Animations.Select(v => v.Name).ToImmutableArray();
ObjectConfig = cfg.ObjectConfig;
_isShown = cfg.IsShown;
}
public event EventHandler<SkinStatusChangedEventArgs>? SkinStatusChanged; public event EventHandler<SkinStatusChangedEventArgs>? SkinStatusChanged;
public event EventHandler<SlotAttachmentChangedEventArgs>? SlotAttachmentChanged; public event EventHandler<SlotAttachmentChangedEventArgs>? SlotAttachmentChanged;
@@ -73,7 +104,7 @@ namespace SpineViewer.Models
public bool IsSelected public bool IsSelected
{ {
get { lock (_lock) return _isSelected; } get { lock (_lock) return _isSelected; }
set { lock (_lock) if (SetProperty(ref _isSelected, value)) _spineObject.DebugNonTexture = _isSelected; } set { lock (_lock) SetProperty(ref _isSelected, value); }
} }
private bool _isSelected = false; private bool _isSelected = false;
@@ -82,18 +113,18 @@ namespace SpineViewer.Models
get { lock (_lock) return _isShown; } get { lock (_lock) return _isShown; }
set { lock (_lock) SetProperty(ref _isShown, value); } set { lock (_lock) SetProperty(ref _isShown, value); }
} }
private bool _isShown = true; private bool _isShown = _loadOptions.IsShown;
public bool UsePma public bool UsePma
{ {
get { lock (_lock) return _spineObject.UsePma; } get { lock (_lock) return _spineObject.UsePma; }
set { lock (_lock) SetProperty(_spineObject.UsePma, value, _spineObject, (m, v) => m.UsePma = v); } set { lock (_lock) SetProperty(_spineObject.UsePma, value, v => _spineObject.UsePma = v); }
} }
public ISkeleton.Physics Physics public ISkeleton.Physics Physics
{ {
get { lock (_lock) return _spineObject.Physics; } get { lock (_lock) return _spineObject.Physics; }
set { lock (_lock) SetProperty(_spineObject.Physics, value, _spineObject, (m, v) => m.Physics = v); } set { lock (_lock) SetProperty(_spineObject.Physics, value, v => _spineObject.Physics = v); }
} }
/// <summary> /// <summary>
@@ -128,7 +159,7 @@ namespace SpineViewer.Models
public bool FlipX public bool FlipX
{ {
get { lock (_lock) return _spineObject.Skeleton.ScaleX < 0; } get { lock (_lock) return _spineObject.Skeleton.ScaleX < 0; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.ScaleX < 0, value, _spineObject, (m, v) => m.Skeleton.ScaleX *= -1); } set { lock (_lock) SetProperty(_spineObject.Skeleton.ScaleX < 0, value, v => _spineObject.Skeleton.ScaleX *= -1); }
} }
/// <summary> /// <summary>
@@ -137,19 +168,19 @@ namespace SpineViewer.Models
public bool FlipY public bool FlipY
{ {
get { lock (_lock) return _spineObject.Skeleton.ScaleY < 0; } get { lock (_lock) return _spineObject.Skeleton.ScaleY < 0; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.ScaleY < 0, value, _spineObject, (m, v) => m.Skeleton.ScaleY *= -1); } set { lock (_lock) SetProperty(_spineObject.Skeleton.ScaleY < 0, value, v => _spineObject.Skeleton.ScaleY *= -1); }
} }
public float X public float X
{ {
get { lock (_lock) return _spineObject.Skeleton.X; } get { lock (_lock) return _spineObject.Skeleton.X; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.X, value, _spineObject, (m, v) => m.Skeleton.X = v); } set { lock (_lock) SetProperty(_spineObject.Skeleton.X, value, v => _spineObject.Skeleton.X = v); }
} }
public float Y public float Y
{ {
get { lock (_lock) return _spineObject.Skeleton.Y; } get { lock (_lock) return _spineObject.Skeleton.Y; }
set { lock (_lock) SetProperty(_spineObject.Skeleton.Y, value, _spineObject, (m, v) => m.Skeleton.Y = v); } set { lock (_lock) SetProperty(_spineObject.Skeleton.Y, value, v => _spineObject.Skeleton.Y = v); }
} }
public ImmutableArray<string> Skins => _skins; public ImmutableArray<string> Skins => _skins;
@@ -242,67 +273,67 @@ namespace SpineViewer.Models
public bool EnableDebug public bool EnableDebug
{ {
get { lock (_lock) return _spineObject.EnableDebug; } get { lock (_lock) return _spineObject.EnableDebug; }
set { lock (_lock) SetProperty(_spineObject.EnableDebug, value, _spineObject, (m, v) => m.EnableDebug = v); } set { lock (_lock) SetProperty(_spineObject.EnableDebug, value, v => _spineObject.EnableDebug = v); }
} }
public bool DebugTexture public bool DebugTexture
{ {
get { lock (_lock) return _spineObject.DebugTexture; } get { lock (_lock) return _spineObject.DebugTexture; }
set { lock (_lock) SetProperty(_spineObject.DebugTexture, value, _spineObject, (m, v) => m.DebugTexture = v); } set { lock (_lock) SetProperty(_spineObject.DebugTexture, value, v => _spineObject.DebugTexture = v); }
} }
public bool DebugBounds public bool DebugBounds
{ {
get { lock (_lock) return _spineObject.DebugBounds; } get { lock (_lock) return _spineObject.DebugBounds; }
set { lock (_lock) SetProperty(_spineObject.DebugBounds, value, _spineObject, (m, v) => m.DebugBounds = v); } set { lock (_lock) SetProperty(_spineObject.DebugBounds, value, v => _spineObject.DebugBounds = v); }
} }
public bool DebugBones public bool DebugBones
{ {
get { lock (_lock) return _spineObject.DebugBones; } get { lock (_lock) return _spineObject.DebugBones; }
set { lock (_lock) SetProperty(_spineObject.DebugBones, value, _spineObject, (m, v) => m.DebugBones = v); } set { lock (_lock) SetProperty(_spineObject.DebugBones, value, v => _spineObject.DebugBones = v); }
} }
public bool DebugRegions public bool DebugRegions
{ {
get { lock (_lock) return _spineObject.DebugRegions; } get { lock (_lock) return _spineObject.DebugRegions; }
set { lock (_lock) SetProperty(_spineObject.DebugRegions, value, _spineObject, (m, v) => m.DebugRegions = v); } set { lock (_lock) SetProperty(_spineObject.DebugRegions, value, v => _spineObject.DebugRegions = v); }
} }
public bool DebugMeshHulls public bool DebugMeshHulls
{ {
get { lock (_lock) return _spineObject.DebugMeshHulls; } get { lock (_lock) return _spineObject.DebugMeshHulls; }
set { lock (_lock) SetProperty(_spineObject.DebugMeshHulls, value, _spineObject, (m, v) => m.DebugMeshHulls = v); } set { lock (_lock) SetProperty(_spineObject.DebugMeshHulls, value, v => _spineObject.DebugMeshHulls = v); }
} }
public bool DebugMeshes public bool DebugMeshes
{ {
get { lock (_lock) return _spineObject.DebugMeshes; } get { lock (_lock) return _spineObject.DebugMeshes; }
set { lock (_lock) SetProperty(_spineObject.DebugMeshes, value, _spineObject, (m, v) => m.DebugMeshes = v); } set { lock (_lock) SetProperty(_spineObject.DebugMeshes, value, v => _spineObject.DebugMeshes = v); }
} }
public bool DebugBoundingBoxes public bool DebugBoundingBoxes
{ {
get { lock (_lock) return _spineObject.DebugBoundingBoxes; } get { lock (_lock) return _spineObject.DebugBoundingBoxes; }
set { lock (_lock) SetProperty(_spineObject.DebugBoundingBoxes, value, _spineObject, (m, v) => m.DebugBoundingBoxes = v); } set { lock (_lock) SetProperty(_spineObject.DebugBoundingBoxes, value, v => _spineObject.DebugBoundingBoxes = v); }
} }
public bool DebugPaths public bool DebugPaths
{ {
get { lock (_lock) return _spineObject.DebugPaths; } get { lock (_lock) return _spineObject.DebugPaths; }
set { lock (_lock) SetProperty(_spineObject.DebugPaths, value, _spineObject, (m, v) => m.DebugPaths = v); } set { lock (_lock) SetProperty(_spineObject.DebugPaths, value, v => _spineObject.DebugPaths = v); }
} }
public bool DebugPoints public bool DebugPoints
{ {
get { lock (_lock) return _spineObject.DebugPoints; } get { lock (_lock) return _spineObject.DebugPoints; }
set { lock (_lock) SetProperty(_spineObject.DebugPoints, value, _spineObject, (m, v) => m.DebugPoints = v); } set { lock (_lock) SetProperty(_spineObject.DebugPoints, value, v => _spineObject.DebugPoints = v); }
} }
public bool DebugClippings public bool DebugClippings
{ {
get { lock (_lock) return _spineObject.DebugClippings; } get { lock (_lock) return _spineObject.DebugClippings; }
set { lock (_lock) SetProperty(_spineObject.DebugClippings, value, _spineObject, (m, v) => m.DebugClippings = v); } set { lock (_lock) SetProperty(_spineObject.DebugClippings, value, v => _spineObject.DebugClippings = v); }
} }
public void Update(float delta) public void Update(float delta)
@@ -326,10 +357,9 @@ namespace SpineViewer.Models
lock (_lock) return _spineObject.GetCurrentBounds(); lock (_lock) return _spineObject.GetCurrentBounds();
} }
/// <summary> public SpineObjectConfigModel ObjectConfig
/// 导出参数对象 {
/// </summary> get
public SpineObjectConfigModel Dump()
{ {
lock (_lock) lock (_lock)
{ {
@@ -340,6 +370,7 @@ namespace SpineViewer.Models
FlipY = _spineObject.Skeleton.ScaleY < 0, FlipY = _spineObject.Skeleton.ScaleY < 0,
X = _spineObject.Skeleton.X, X = _spineObject.Skeleton.X,
Y = _spineObject.Skeleton.Y, Y = _spineObject.Skeleton.Y,
UsePma = _spineObject.UsePma, UsePma = _spineObject.UsePma,
Physics = _spineObject.Physics.ToString(), Physics = _spineObject.Physics.ToString(),
@@ -365,39 +396,36 @@ namespace SpineViewer.Models
return config; return config;
} }
} }
set
/// <summary>
/// 从参数对象加载参数值
/// </summary>
public void Load(SpineObjectConfigModel config)
{ {
lock (_lock) lock (_lock)
{ {
_spineObject.Skeleton.ScaleX = config.Scale; _spineObject.Skeleton.ScaleX = value.Scale;
_spineObject.Skeleton.ScaleY = config.Scale; _spineObject.Skeleton.ScaleY = value.Scale;
OnPropertyChanged(nameof(Scale)); OnPropertyChanged(nameof(Scale));
SetProperty(_spineObject.Skeleton.ScaleX < 0, config.FlipX, _spineObject, (m, v) => m.Skeleton.ScaleX *= -1, nameof(FlipX)); SetProperty(_spineObject.Skeleton.ScaleX < 0, value.FlipX, v => _spineObject.Skeleton.ScaleX *= -1, nameof(FlipX));
SetProperty(_spineObject.Skeleton.ScaleY < 0, config.FlipY, _spineObject, (m, v) => m.Skeleton.ScaleY *= -1, nameof(FlipY)); SetProperty(_spineObject.Skeleton.ScaleY < 0, value.FlipY, v => _spineObject.Skeleton.ScaleY *= -1, nameof(FlipY));
SetProperty(_spineObject.Skeleton.X, config.X, _spineObject, (m, v) => m.Skeleton.X = v, nameof(X)); SetProperty(_spineObject.Skeleton.X, value.X, v => _spineObject.Skeleton.X = v, nameof(X));
SetProperty(_spineObject.Skeleton.Y, config.Y, _spineObject, (m, v) => m.Skeleton.Y = v, nameof(Y)); SetProperty(_spineObject.Skeleton.Y, value.Y, v => _spineObject.Skeleton.Y = v, nameof(Y));
SetProperty(_spineObject.UsePma, config.UsePma, _spineObject, (m, v) => m.UsePma = v, nameof(UsePma)); SetProperty(_spineObject.UsePma, value.UsePma, v => _spineObject.UsePma = v, nameof(UsePma));
SetProperty(_spineObject.Physics, Enum.Parse<ISkeleton.Physics>(config.Physics ?? "Update", true), _spineObject, (m, v) => m.Physics = v, nameof(Physics)); SetProperty(_spineObject.Physics, Enum.Parse<ISkeleton.Physics>(value.Physics ?? "Update", true), v => _spineObject.Physics = v, nameof(Physics));
foreach (var name in _spineObject.Data.Skins.Select(v => v.Name).Except(config.LoadedSkins)) foreach (var name in _spineObject.Data.Skins.Select(v => v.Name).Except(value.LoadedSkins))
if (_spineObject.SetSkinStatus(name, false)) if (_spineObject.SetSkinStatus(name, false))
SkinStatusChanged?.Invoke(this, new(name, false)); SkinStatusChanged?.Invoke(this, new(name, false));
foreach (var name in config.LoadedSkins) foreach (var name in value.LoadedSkins)
if (_spineObject.SetSkinStatus(name, true)) if (_spineObject.SetSkinStatus(name, true))
SkinStatusChanged?.Invoke(this, new(name, true)); SkinStatusChanged?.Invoke(this, new(name, true));
foreach (var (slotName, attachmentName) in config.SlotAttachment) foreach (var (slotName, attachmentName) in value.SlotAttachment)
if (_spineObject.SetAttachment(slotName, attachmentName)) if (_spineObject.SetAttachment(slotName, attachmentName))
SlotAttachmentChanged?.Invoke(this, new(slotName, attachmentName)); SlotAttachmentChanged?.Invoke(this, new(slotName, attachmentName));
// XXX: 处理空动画 // XXX: 处理空动画
_spineObject.AnimationState.ClearTracks(); _spineObject.AnimationState.ClearTracks();
int trackIndex = 0; int trackIndex = 0;
foreach (var name in config.Animations) foreach (var name in value.Animations)
{ {
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
_spineObject.AnimationState.SetAnimation(trackIndex, name, true); _spineObject.AnimationState.SetAnimation(trackIndex, name, true);
@@ -405,16 +433,31 @@ namespace SpineViewer.Models
trackIndex++; trackIndex++;
} }
SetProperty(_spineObject.DebugTexture, config.DebugTexture, _spineObject, (m, v) => m.DebugTexture = v, nameof(DebugTexture)); SetProperty(_spineObject.DebugTexture, value.DebugTexture, v => _spineObject.DebugTexture = v, nameof(DebugTexture));
SetProperty(_spineObject.DebugBounds, config.DebugBounds, _spineObject, (m, v) => m.DebugBounds = v, nameof(DebugBounds)); SetProperty(_spineObject.DebugBounds, value.DebugBounds, v => _spineObject.DebugBounds = v, nameof(DebugBounds));
SetProperty(_spineObject.DebugBones, config.DebugBones, _spineObject, (m, v) => m.DebugBones = v, nameof(DebugBones)); SetProperty(_spineObject.DebugBones, value.DebugBones, v => _spineObject.DebugBones = v, nameof(DebugBones));
SetProperty(_spineObject.DebugRegions, config.DebugRegions, _spineObject, (m, v) => m.DebugRegions = v, nameof(DebugRegions)); SetProperty(_spineObject.DebugRegions, value.DebugRegions, v => _spineObject.DebugRegions = v, nameof(DebugRegions));
SetProperty(_spineObject.DebugMeshHulls, config.DebugMeshHulls, _spineObject, (m, v) => m.DebugMeshHulls = v, nameof(DebugMeshHulls)); SetProperty(_spineObject.DebugMeshHulls, value.DebugMeshHulls, v => _spineObject.DebugMeshHulls = v, nameof(DebugMeshHulls));
SetProperty(_spineObject.DebugMeshes, config.DebugMeshes, _spineObject, (m, v) => m.DebugMeshes = v, nameof(DebugMeshes)); SetProperty(_spineObject.DebugMeshes, value.DebugMeshes, v => _spineObject.DebugMeshes = v, nameof(DebugMeshes));
SetProperty(_spineObject.DebugBoundingBoxes, config.DebugBoundingBoxes, _spineObject, (m, v) => m.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes)); SetProperty(_spineObject.DebugBoundingBoxes, value.DebugBoundingBoxes, v => _spineObject.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes));
SetProperty(_spineObject.DebugPaths, config.DebugPaths, _spineObject, (m, v) => m.DebugPaths = v, nameof(DebugPaths)); SetProperty(_spineObject.DebugPaths, value.DebugPaths, v => _spineObject.DebugPaths = v, nameof(DebugPaths));
SetProperty(_spineObject.DebugPoints, config.DebugPoints, _spineObject, (m, v) => m.DebugPoints = v, nameof(DebugPoints)); SetProperty(_spineObject.DebugPoints, value.DebugPoints, v => _spineObject.DebugPoints = v, nameof(DebugPoints));
SetProperty(_spineObject.DebugClippings, config.DebugClippings, _spineObject, (m, v) => m.DebugClippings = v, nameof(DebugClippings)); SetProperty(_spineObject.DebugClippings, value.DebugClippings, v => _spineObject.DebugClippings = v, nameof(DebugClippings));
}
}
}
public SpineObjectWorkspaceConfigModel WorkspaceConfig
{
get
{
return new()
{
SkelPath = SkelPath,
AtlasPath = AtlasPath,
IsShown = IsShown,
ObjectConfig = ObjectConfig
};
} }
} }
@@ -475,4 +518,20 @@ namespace SpineViewer.Models
public int TrackIndex { get; } = trackIndex; public int TrackIndex { get; } = trackIndex;
public string? AnimationName { get; } = animationName; public string? AnimationName { get; } = animationName;
} }
public class SpineObjectLoadOptions
{
public bool IsShown { get; set; } = true;
public bool UsePma { get; set; }
public bool DebugTexture { get; set; } = true;
public bool DebugBounds { get; set; }
public bool DebugBones { get; set; }
public bool DebugRegions { get; set; }
public bool DebugMeshHulls { get; set; }
public bool DebugMeshes { get; set; }
public bool DebugBoundingBoxes { get; set; }
public bool DebugPaths { get; set; }
public bool DebugPoints { get; set; }
public bool DebugClippings { get; set; }
}
} }

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows.Media;
namespace SpineViewer.Models
{
public class WorkspaceModel
{
public RendererWorkspaceConfigModel RendererConfig { get; set; } = new();
public List<SpineObjectWorkspaceConfigModel> LoadedSpineObjects { get; set; } = [];
}
public class RendererWorkspaceConfigModel
{
public uint ResolutionX { get; set; } = 100;
public uint ResolutionY { get; set; } = 100;
public float CenterX { get; set; }
public float CenterY { get; set; }
public float Zoom { get; set; } = 1f;
public float Rotation { get; set; }
public bool FlipX { get; set; }
public bool FlipY { get; set; } = true;
public uint MaxFps { get; set; } = 30;
public bool ShowAxis { get; set; } = true;
public Color BackgroundColor { get; set; }
// TODO: 背景图片
//public string? BackgroundImagePath { get; set; }
//public ? BackgroundImageDisplayMode { get; set; }
}
public class SpineObjectWorkspaceConfigModel
{
public string SkelPath { get; set; } = "";
public string AtlasPath { get; set; } = "";
public bool IsShown { get; set; } = true;
public SpineObjectConfigModel ObjectConfig { get; set; } = new();
}
}

View File

@@ -19,6 +19,7 @@ namespace SpineViewer.Resources
public static string Str_GeneratePreviewsTitle => Get<string>("Str_GeneratePreviewsTitle"); public static string Str_GeneratePreviewsTitle => Get<string>("Str_GeneratePreviewsTitle");
public static string Str_DeletePreviewsTitle => Get<string>("Str_DeletePreviewsTitle"); public static string Str_DeletePreviewsTitle => Get<string>("Str_DeletePreviewsTitle");
public static string Str_AddSpineObjectsTitle => Get<string>("Str_AddSpineObjectsTitle"); public static string Str_AddSpineObjectsTitle => Get<string>("Str_AddSpineObjectsTitle");
public static string Str_ReloadSpineObjectsTitle => Get<string>("Str_ReloadSpineObjectsTitle");
public static string Str_CustomFFmpegExporterTitle => Get<string>("Str_CustomFFmpegExporterTitle"); public static string Str_CustomFFmpegExporterTitle => Get<string>("Str_CustomFFmpegExporterTitle");
public static string Str_InfoPopup => Get<string>("Str_InfoPopup"); public static string Str_InfoPopup => Get<string>("Str_InfoPopup");
@@ -39,7 +40,6 @@ namespace SpineViewer.Resources
public static string Str_OutputDirNotFound => Get<string>("Str_OutputDirNotFound"); public static string Str_OutputDirNotFound => Get<string>("Str_OutputDirNotFound");
public static string Str_OutputDirRequired => Get<string>("Str_OutputDirRequired"); public static string Str_OutputDirRequired => Get<string>("Str_OutputDirRequired");
public static string Str_InvalidMaxResolution => Get<string>("Str_InvalidMaxResolution"); public static string Str_InvalidMaxResolution => Get<string>("Str_InvalidMaxResolution");
public static string Str_InvalidDuration => Get<string>("Str_InvalidDuration");
public static string Str_FFmpegFormatRequired => Get<string>("Str_FFmpegFormatRequired"); public static string Str_FFmpegFormatRequired => Get<string>("Str_FFmpegFormatRequired");
public static string Str_Copied => Get<string>("Str_Copied"); public static string Str_Copied => Get<string>("Str_Copied");

View File

@@ -11,7 +11,10 @@
<s:String x:Key="Str_Experiment">Experimental Features</s:String> <s:String x:Key="Str_Experiment">Experimental Features</s:String>
<s:String x:Key="Str_Open">Open...</s:String> <s:String x:Key="Str_Open">Open...</s:String>
<s:String x:Key="Str_Preference">Preferences...</s:String> <s:String x:Key="Str_OpenWorkspace">Open Workspace...</s:String>
<s:String x:Key="Str_SaveWorkspace">Save Workspace...</s:String>
<s:String x:Key="Str_Preference">Preferences</s:String>
<s:String x:Key="Str_PreferenceWithDots">Preferences...</s:String>
<s:String x:Key="Str_Exit">Exit</s:String> <s:String x:Key="Str_Exit">Exit</s:String>
<!-- 标签页 --> <!-- 标签页 -->
@@ -35,9 +38,10 @@
<s:String x:Key="Str_ListViewStatusBar">{0} items, {1} selected</s:String> <s:String x:Key="Str_ListViewStatusBar">{0} items, {1} selected</s:String>
<s:String x:Key="Str_AddSpineObject">Add...</s:String> <s:String x:Key="Str_AddSpineObject">Add...</s:String>
<s:String x:Key="Str_RemoveSpineObject">Remove</s:String> <s:String x:Key="Str_RemoveSpineObject">Remove</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">Add from Clipboard</s:String>
<s:String x:Key="Str_Reload">Reload</s:String>
<s:String x:Key="Str_MoveUpSpineObject">Move Up</s:String> <s:String x:Key="Str_MoveUpSpineObject">Move Up</s:String>
<s:String x:Key="Str_MoveDownSpineObject">Move Down</s:String> <s:String x:Key="Str_MoveDownSpineObject">Move Down</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">Add from Clipboard</s:String>
<s:String x:Key="Str_CopySpineObjectConfig">Copy Config</s:String> <s:String x:Key="Str_CopySpineObjectConfig">Copy Config</s:String>
<s:String x:Key="Str_ApplySpineObjectConfig">Apply Config</s:String> <s:String x:Key="Str_ApplySpineObjectConfig">Apply Config</s:String>
<s:String x:Key="Str_SaveSpineObjectConfigToFile">Save Config to File...</s:String> <s:String x:Key="Str_SaveSpineObjectConfigToFile">Save Config to File...</s:String>
@@ -119,6 +123,7 @@
<s:String x:Key="Str_GeneratePreviewsTitle">Generate Previews</s:String> <s:String x:Key="Str_GeneratePreviewsTitle">Generate Previews</s:String>
<s:String x:Key="Str_DeletePreviewsTitle">Delete Previews</s:String> <s:String x:Key="Str_DeletePreviewsTitle">Delete Previews</s:String>
<s:String x:Key="Str_AddSpineObjectsTitle">Batch Add Spine Files</s:String> <s:String x:Key="Str_AddSpineObjectsTitle">Batch Add Spine Files</s:String>
<s:String x:Key="Str_ReloadSpineObjectsTitle">Reload Spine Files</s:String>
<s:String x:Key="Str_InfoPopup">Information</s:String> <s:String x:Key="Str_InfoPopup">Information</s:String>
<s:String x:Key="Str_WarnPopup">Warning</s:String> <s:String x:Key="Str_WarnPopup">Warning</s:String>
@@ -139,7 +144,6 @@
<s:String x:Key="Str_OutputDirNotFound">Output Directory Not Found</s:String> <s:String x:Key="Str_OutputDirNotFound">Output Directory Not Found</s:String>
<s:String x:Key="Str_OutputDirRequired">Output folder required for single export</s:String> <s:String x:Key="Str_OutputDirRequired">Output folder required for single export</s:String>
<s:String x:Key="Str_InvalidMaxResolution">Valid max resolution required when using auto resolution</s:String> <s:String x:Key="Str_InvalidMaxResolution">Valid max resolution required when using auto resolution</s:String>
<s:String x:Key="Str_InvalidDuration">Export duration cannot be negative for single export</s:String>
<s:String x:Key="Str_FFmpegFormatRequired">FFmpeg export format is required</s:String> <s:String x:Key="Str_FFmpegFormatRequired">FFmpeg export format is required</s:String>
<s:String x:Key="Str_ResolutionTooltip">Screen resolution; adjust related parameters in the render settings panel</s:String> <s:String x:Key="Str_ResolutionTooltip">Screen resolution; adjust related parameters in the render settings panel</s:String>
@@ -159,7 +163,7 @@
<s:String x:Key="Str_ImageQualityTooltip">Range 0100; only effective for certain formats</s:String> <s:String x:Key="Str_ImageQualityTooltip">Range 0100; only effective for certain formats</s:String>
<s:String x:Key="Str_Duration">Duration</s:String> <s:String x:Key="Str_Duration">Duration</s:String>
<s:String x:Key="Str_ExportDurationTooltip">Export duration; if smaller than 0, each model uses its maximum animation length when exporting individually</s:String> <s:String x:Key="Str_ExportDurationTooltip">Export duration; if less than 0, the maximum duration of all animations in all models will be used during export.</s:String>
<s:String x:Key="Str_Fps">FPS</s:String> <s:String x:Key="Str_Fps">FPS</s:String>
<s:String x:Key="Str_KeepLastFrame">Keep Last Frame</s:String> <s:String x:Key="Str_KeepLastFrame">Keep Last Frame</s:String>
@@ -194,4 +198,17 @@
<s:String x:Key="Str_ProgremVersion">Program Version</s:String> <s:String x:Key="Str_ProgremVersion">Program Version</s:String>
<s:String x:Key="Str_ProjectUrl">Project URL</s:String> <s:String x:Key="Str_ProjectUrl">Project URL</s:String>
<!-- 首选项对话框 -->
<s:String x:Key="Str_TextureLoadPreference">Texture Loading Options</s:String>
<s:String x:Key="Str_ForcePremul">Force Premultiplied Channels</s:String>
<s:String x:Key="Str_ForcePremulTooltip">When enabled, this applies premultiplied operations to pixels during texture loading, helping to resolve black edge issues at some connections.</s:String>
<s:String x:Key="Str_ForceNearest">Force Nearest Interpolation</s:String>
<s:String x:Key="Str_ForceMipmap">Force Mipmap</s:String>
<s:String x:Key="Str_ForceMipmapTooltip">When enabled, this helps reduce aliasing when textures are scaled down, at the cost of slightly higher video memory usage.</s:String>
<s:String x:Key="Str_SpineLoadPreference">Model Loading Options</s:String>
<s:String x:Key="Str_AppPreference">Application Options</s:String>
<s:String x:Key="Str_Language">Language</s:String>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -11,7 +11,10 @@
<s:String x:Key="Str_Experiment">実験機能</s:String> <s:String x:Key="Str_Experiment">実験機能</s:String>
<s:String x:Key="Str_Open">開く...</s:String> <s:String x:Key="Str_Open">開く...</s:String>
<s:String x:Key="Str_Preference">設定...</s:String> <s:String x:Key="Str_OpenWorkspace">ワークスペースを開く...</s:String>
<s:String x:Key="Str_SaveWorkspace">ワークスペースを保存...</s:String>
<s:String x:Key="Str_Preference">設定</s:String>
<s:String x:Key="Str_PreferenceWithDots">設定...</s:String>
<s:String x:Key="Str_Exit">終了</s:String> <s:String x:Key="Str_Exit">終了</s:String>
<!-- 标签页 --> <!-- 标签页 -->
@@ -35,9 +38,10 @@
<s:String x:Key="Str_ListViewStatusBar">全{0}件、選択中{1}件</s:String> <s:String x:Key="Str_ListViewStatusBar">全{0}件、選択中{1}件</s:String>
<s:String x:Key="Str_AddSpineObject">追加...</s:String> <s:String x:Key="Str_AddSpineObject">追加...</s:String>
<s:String x:Key="Str_RemoveSpineObject">削除</s:String> <s:String x:Key="Str_RemoveSpineObject">削除</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">クリップボードから追加</s:String>
<s:String x:Key="Str_Reload">再読み込み</s:String>
<s:String x:Key="Str_MoveUpSpineObject">上へ移動</s:String> <s:String x:Key="Str_MoveUpSpineObject">上へ移動</s:String>
<s:String x:Key="Str_MoveDownSpineObject">下へ移動</s:String> <s:String x:Key="Str_MoveDownSpineObject">下へ移動</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">クリップボードから追加</s:String>
<s:String x:Key="Str_CopySpineObjectConfig">パラメーターをコピー</s:String> <s:String x:Key="Str_CopySpineObjectConfig">パラメーターをコピー</s:String>
<s:String x:Key="Str_ApplySpineObjectConfig">パラメーターを適用</s:String> <s:String x:Key="Str_ApplySpineObjectConfig">パラメーターを適用</s:String>
<s:String x:Key="Str_SaveSpineObjectConfigToFile">パラメータファイルを保存...</s:String> <s:String x:Key="Str_SaveSpineObjectConfigToFile">パラメータファイルを保存...</s:String>
@@ -119,6 +123,7 @@
<s:String x:Key="Str_GeneratePreviewsTitle">プレビューを生成</s:String> <s:String x:Key="Str_GeneratePreviewsTitle">プレビューを生成</s:String>
<s:String x:Key="Str_DeletePreviewsTitle">プレビューを削除</s:String> <s:String x:Key="Str_DeletePreviewsTitle">プレビューを削除</s:String>
<s:String x:Key="Str_AddSpineObjectsTitle">一括でスケルトンファイルを追加</s:String> <s:String x:Key="Str_AddSpineObjectsTitle">一括でスケルトンファイルを追加</s:String>
<s:String x:Key="Str_ReloadSpineObjectsTitle">スケルトンファイルの再読み込み</s:String>
<s:String x:Key="Str_InfoPopup">情報</s:String> <s:String x:Key="Str_InfoPopup">情報</s:String>
<s:String x:Key="Str_WarnPopup">警告</s:String> <s:String x:Key="Str_WarnPopup">警告</s:String>
@@ -139,7 +144,6 @@
<s:String x:Key="Str_OutputDirNotFound">出力フォルダーが存在しません</s:String> <s:String x:Key="Str_OutputDirNotFound">出力フォルダーが存在しません</s:String>
<s:String x:Key="Str_OutputDirRequired">単一エクスポート時は出力フォルダーを指定する必要があります</s:String> <s:String x:Key="Str_OutputDirRequired">単一エクスポート時は出力フォルダーを指定する必要があります</s:String>
<s:String x:Key="Str_InvalidMaxResolution">自動解像度使用時は有効な最大解像度を指定する必要があります</s:String> <s:String x:Key="Str_InvalidMaxResolution">自動解像度使用時は有効な最大解像度を指定する必要があります</s:String>
<s:String x:Key="Str_InvalidDuration">単一エクスポート時、持続時間は0以上である必要があります</s:String>
<s:String x:Key="Str_FFmpegFormatRequired">FFmpegエクスポートフォーマットを指定する必要があります</s:String> <s:String x:Key="Str_FFmpegFormatRequired">FFmpegエクスポートフォーマットを指定する必要があります</s:String>
<s:String x:Key="Str_ResolutionTooltip">画面解像度。関連パラメーターは画面パネルで調整してください</s:String> <s:String x:Key="Str_ResolutionTooltip">画面解像度。関連パラメーターは画面パネルで調整してください</s:String>
@@ -159,7 +163,7 @@
<s:String x:Key="Str_ImageQualityTooltip">値の範囲は0-100。一部の画像フォーマットでのみ有効です</s:String> <s:String x:Key="Str_ImageQualityTooltip">値の範囲は0-100。一部の画像フォーマットでのみ有効です</s:String>
<s:String x:Key="Str_Duration">時間</s:String> <s:String x:Key="Str_Duration">時間</s:String>
<s:String x:Key="Str_ExportDurationTooltip">エクスポート時間。0未満の場合、個別エクスポート時には各モデルのすべてのトラックアニメーションの最大時間が使用されます</s:String> <s:String x:Key="Str_ExportDurationTooltip">エクスポート時間。0 未満の場合、エクスポート時にすべてのモデルのすべてのアニメーションの最大時間が使用されます</s:String>
<s:String x:Key="Str_Fps">FPS</s:String> <s:String x:Key="Str_Fps">FPS</s:String>
<s:String x:Key="Str_KeepLastFrame">最後のフレームを保持</s:String> <s:String x:Key="Str_KeepLastFrame">最後のフレームを保持</s:String>
@@ -194,5 +198,18 @@
<s:String x:Key="Str_ProgremVersion">プログラムバージョン</s:String> <s:String x:Key="Str_ProgremVersion">プログラムバージョン</s:String>
<s:String x:Key="Str_ProjectUrl">プロジェクトURL</s:String> <s:String x:Key="Str_ProjectUrl">プロジェクトURL</s:String>
<!-- 首选项对话框 -->
<s:String x:Key="Str_TextureLoadPreference">テクスチャ読み込みオプション</s:String>
<s:String x:Key="Str_ForcePremul">強制プリマルチチャンネル</s:String>
<s:String x:Key="Str_ForcePremulTooltip">有効にすると、テクスチャ読み込み時にピクセルにプリマルチ処理を適用し、一部の接続部分で発生する黒い縁の問題を解決します。</s:String>
<s:String x:Key="Str_ForceNearest">Nearest補間を強制使用</s:String>
<s:String x:Key="Str_ForceMipmap">Mipmapを強制使用</s:String>
<s:String x:Key="Str_ForceMipmapTooltip">有効にすると、テクスチャ縮小時のジャギーを軽減しますが、ビデオメモリの使用量が若干増加します。</s:String>
<s:String x:Key="Str_SpineLoadPreference">モデル読み込みオプション</s:String>
<s:String x:Key="Str_AppPreference">アプリケーションプション</s:String>
<s:String x:Key="Str_Language">言語</s:String>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -11,7 +11,10 @@
<s:String x:Key="Str_Experiment">实验性功能</s:String> <s:String x:Key="Str_Experiment">实验性功能</s:String>
<s:String x:Key="Str_Open">打开...</s:String> <s:String x:Key="Str_Open">打开...</s:String>
<s:String x:Key="Str_Preference">首选项...</s:String> <s:String x:Key="Str_OpenWorkspace">打开工作区...</s:String>
<s:String x:Key="Str_SaveWorkspace">保存工作区...</s:String>
<s:String x:Key="Str_Preference">首选项</s:String>
<s:String x:Key="Str_PreferenceWithDots">首选项...</s:String>
<s:String x:Key="Str_Exit">退出</s:String> <s:String x:Key="Str_Exit">退出</s:String>
<!-- 标签页 --> <!-- 标签页 -->
@@ -35,9 +38,10 @@
<s:String x:Key="Str_ListViewStatusBar">共 {0} 项,已选择 {1} 项</s:String> <s:String x:Key="Str_ListViewStatusBar">共 {0} 项,已选择 {1} 项</s:String>
<s:String x:Key="Str_AddSpineObject">添加...</s:String> <s:String x:Key="Str_AddSpineObject">添加...</s:String>
<s:String x:Key="Str_RemoveSpineObject">移除</s:String> <s:String x:Key="Str_RemoveSpineObject">移除</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">从剪贴板添加</s:String>
<s:String x:Key="Str_Reload">重新加载</s:String>
<s:String x:Key="Str_MoveUpSpineObject">上移</s:String> <s:String x:Key="Str_MoveUpSpineObject">上移</s:String>
<s:String x:Key="Str_MoveDownSpineObject">下移</s:String> <s:String x:Key="Str_MoveDownSpineObject">下移</s:String>
<s:String x:Key="Str_AddSpineObjectFromClipboard">从剪贴板添加</s:String>
<s:String x:Key="Str_CopySpineObjectConfig">复制参数</s:String> <s:String x:Key="Str_CopySpineObjectConfig">复制参数</s:String>
<s:String x:Key="Str_ApplySpineObjectConfig">应用参数</s:String> <s:String x:Key="Str_ApplySpineObjectConfig">应用参数</s:String>
<s:String x:Key="Str_SaveSpineObjectConfigToFile">保存参数文件...</s:String> <s:String x:Key="Str_SaveSpineObjectConfigToFile">保存参数文件...</s:String>
@@ -119,6 +123,7 @@
<s:String x:Key="Str_GeneratePreviewsTitle">生成预览图</s:String> <s:String x:Key="Str_GeneratePreviewsTitle">生成预览图</s:String>
<s:String x:Key="Str_DeletePreviewsTitle">删除预览图</s:String> <s:String x:Key="Str_DeletePreviewsTitle">删除预览图</s:String>
<s:String x:Key="Str_AddSpineObjectsTitle">批量添加骨骼文件</s:String> <s:String x:Key="Str_AddSpineObjectsTitle">批量添加骨骼文件</s:String>
<s:String x:Key="Str_ReloadSpineObjectsTitle">重新加载骨骼文件</s:String>
<s:String x:Key="Str_InfoPopup">提示信息</s:String> <s:String x:Key="Str_InfoPopup">提示信息</s:String>
<s:String x:Key="Str_WarnPopup">警告信息</s:String> <s:String x:Key="Str_WarnPopup">警告信息</s:String>
@@ -139,7 +144,6 @@
<s:String x:Key="Str_OutputDirNotFound">输出文件夹不存在</s:String> <s:String x:Key="Str_OutputDirNotFound">输出文件夹不存在</s:String>
<s:String x:Key="Str_OutputDirRequired">导出单个时必须提供输出文件夹</s:String> <s:String x:Key="Str_OutputDirRequired">导出单个时必须提供输出文件夹</s:String>
<s:String x:Key="Str_InvalidMaxResolution">使用自动分辨率时需要提供有效的最大分辨率</s:String> <s:String x:Key="Str_InvalidMaxResolution">使用自动分辨率时需要提供有效的最大分辨率</s:String>
<s:String x:Key="Str_InvalidDuration">导出单个时导出时长不能为负数</s:String>
<s:String x:Key="Str_FFmpegFormatRequired">必须指定 FFmpeg 导出格式</s:String> <s:String x:Key="Str_FFmpegFormatRequired">必须指定 FFmpeg 导出格式</s:String>
<s:String x:Key="Str_ResolutionTooltip">画面分辨率,相关参数请在画面参数面板进行调整</s:String> <s:String x:Key="Str_ResolutionTooltip">画面分辨率,相关参数请在画面参数面板进行调整</s:String>
@@ -159,7 +163,7 @@
<s:String x:Key="Str_ImageQualityTooltip">取值范围 0-100仅对部分图像格式生效</s:String> <s:String x:Key="Str_ImageQualityTooltip">取值范围 0-100仅对部分图像格式生效</s:String>
<s:String x:Key="Str_Duration">时长</s:String> <s:String x:Key="Str_Duration">时长</s:String>
<s:String x:Key="Str_ExportDurationTooltip">导出时长,如果小于 0则在逐个导出时每个模型使用各自的所有轨道动画时长最大值</s:String> <s:String x:Key="Str_ExportDurationTooltip">导出时长,如果小于 0则在导出时使用所有模型所有动画的最大时长</s:String>
<s:String x:Key="Str_Fps">帧率</s:String> <s:String x:Key="Str_Fps">帧率</s:String>
<s:String x:Key="Str_KeepLastFrame">保留最后一帧</s:String> <s:String x:Key="Str_KeepLastFrame">保留最后一帧</s:String>
@@ -194,4 +198,17 @@
<s:String x:Key="Str_ProgremVersion">程序版本</s:String> <s:String x:Key="Str_ProgremVersion">程序版本</s:String>
<s:String x:Key="Str_ProjectUrl">项目地址</s:String> <s:String x:Key="Str_ProjectUrl">项目地址</s:String>
<!-- 首选项对话框 -->
<s:String x:Key="Str_TextureLoadPreference">纹理加载选项</s:String>
<s:String x:Key="Str_ForcePremul">强制预乘通道</s:String>
<s:String x:Key="Str_ForcePremulTooltip">开启后,会在加载纹理时对像素进行预乘操作,有助于解决某些情况下的连接处黑边问题</s:String>
<s:String x:Key="Str_ForceNearest">强制使用 Nearest 插值</s:String>
<s:String x:Key="Str_ForceMipmap">强制使用 Mipmap</s:String>
<s:String x:Key="Str_ForceMipmapTooltip">开启后有助于改善纹理缩小时的锯齿现象,但是会略微增加显存占用</s:String>
<s:String x:Key="Str_SpineLoadPreference">模型加载选项</s:String>
<s:String x:Key="Str_AppPreference">应用程序选项</s:String>
<s:String x:Key="Str_Language">语言</s:String>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -1,18 +0,0 @@
using SpineViewer.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Services
{
public static class AboutDialogService
{
public static bool ShowAboutDialog()
{
var dialog = new AboutDialog() { Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
}
}

View File

@@ -1,18 +0,0 @@
using SpineViewer.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Services
{
public static class DiagnosticsDialogService
{
public static bool ShowDiagnosticsDialog()
{
var dialog = new DiagnosticsDialog() { Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
}
}

View File

@@ -0,0 +1,115 @@
using Microsoft.Win32;
using SpineViewer.Models;
using SpineViewer.ViewModels.Exporters;
using SpineViewer.Views;
using SpineViewer.Views.ExporterDialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Services
{
/// <summary>
/// 用于弹出各种对话框的服务
/// </summary>
public static class DialogService
{
public static bool ShowDiagnosticsDialog()
{
var dialog = new DiagnosticsDialog() { Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
public static bool ShowAboutDialog()
{
var dialog = new AboutDialog() { Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
public static bool ShowFrameExporterDialog(FrameExporterViewModel vm)
{
var dialog = new FrameExporterDialog() { DataContext = vm, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
public static bool ShowFrameSequenceExporterDialog(FrameSequenceExporterViewModel vm)
{
var dialog = new FrameSequenceExporterDialog() { DataContext = vm, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
public static bool ShowFFmpegVideoExporterDialog(FFmpegVideoExporterViewModel vm)
{
var dialog = new FFmpegVideoExporterDialog() { DataContext = vm, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
public static bool ShowCustomFFmpegExporterDialog(CustomFFmpegExporterViewModel vm)
{
var dialog = new CustomFFmpegExporterDialog() { DataContext = vm, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
/// <summary>
/// 将给定的首选项参数在对话框上进行显示, 返回值表示是否确认修改
/// </summary>
public static bool ShowPreferenceDialog(PreferenceModel m)
{
var dialog = new PreferenceDialog() { DataContext = m, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
/// <summary>
/// 获取用户选择的文件夹
/// </summary>
/// <param name="folderName"></param>
/// <returns>是否确认了选择</returns>
public static bool ShowOpenFolderDialog(out string? folderName)
{
var dialog = new OpenFolderDialog() { Multiselect = false };
if (dialog.ShowDialog() is true)
{
folderName = dialog.FolderName;
return true;
}
folderName = null;
return false;
}
public static bool ShowOpenJsonDialog(out string? fileName, string initialDirectory = "")
{
var dialog = new OpenFileDialog()
{
InitialDirectory = initialDirectory,
Filter = "Json|*.jcfg;*.json|All|*.*"
};
if (dialog.ShowDialog() is true)
{
fileName = dialog.FileName;
return true;
}
fileName = null;
return false;
}
public static bool ShowSaveJsonDialog(ref string? fileName, string initialDirectory = "")
{
var dialog = new SaveFileDialog()
{
FileName = fileName,
InitialDirectory = initialDirectory,
DefaultExt = ".jcfg",
Filter = "Json|*.jcfg;*.json|All|*.*",
};
if (dialog.ShowDialog() is true)
{
fileName = dialog.FileName;
return true;
}
fileName = null;
return false;
}
}
}

View File

@@ -1,38 +0,0 @@
using SpineViewer.ViewModels.Exporters;
using SpineViewer.Views;
using SpineViewer.Views.ExporterDialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Services
{
public static class ExporterDialogService
{
public static bool ShowFrameExporterDialog(FrameExporterViewModel vm)
{
var dialog = new FrameExporterDialog() { DataContext = vm, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
public static bool ShowFrameSequenceExporterDialog(FrameSequenceExporterViewModel vm)
{
var dialog = new FrameSequenceExporterDialog() { DataContext = vm, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
public static bool ShowFFmpegVideoExporterDialog(FFmpegVideoExporterViewModel vm)
{
var dialog = new FFmpegVideoExporterDialog() { DataContext = vm, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
public static bool ShowCustomFFmpegExporterDialog(CustomFFmpegExporterViewModel vm)
{
var dialog = new CustomFFmpegExporterDialog() { DataContext = vm, Owner = App.Current.MainWindow };
return dialog.ShowDialog() ?? false;
}
}
}

View File

@@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Services
{
public static class OpenFolderService
{
/// <summary>
/// 获取用户选择的文件夹
/// </summary>
/// <param name="selectedPath"></param>
/// <returns>是否确认了选择</returns>
public static bool OpenFolder(out string? selectedPath)
{
// XXX: 此处使用了 System.Windows.Forms 的文件夹浏览对话框
using var folderDialog = new System.Windows.Forms.FolderBrowserDialog();
if (folderDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
selectedPath = folderDialog.SelectedPath;
return true;
}
selectedPath = null;
return false;
}
}
}

View File

@@ -1,25 +0,0 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.Services
{
public static class SaveService
{
/// <summary>
/// 获取用户选择的文件夹
/// </summary>
/// <param name="selectedPath"></param>
/// <returns>是否确认了选择</returns>
public static bool SaveFile(out string? selectedPath)
{
var dialog = new SaveFileDialog() { };
selectedPath = null;
// TODO
return false;
}
}
}

View File

@@ -7,22 +7,17 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath> <BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.15.0</Version> <Version>0.15.1</Version>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<NoWarn>$(NoWarn);NETSDK1206</NoWarn> <NoWarn>$(NoWarn);NETSDK1206</NoWarn>
<ApplicationIcon>appicon.ico</ApplicationIcon> <ApplicationIcon>appicon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Using Remove="System.Windows.Forms" />
<Using Remove="System.Drawing" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="appicon.ico" /> <Content Include="appicon.ico" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,120 @@
using FFMpegCore;
using Microsoft.Win32;
using NLog;
using SpineViewer.Models;
using SpineViewer.Services;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows.Media;
namespace SpineViewer.Utils
{
public static class JsonHelper
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
static JsonHelper()
{
_jsonOptions.Converters.Add(new ColorJsonConverter());
}
/// <summary>
/// 保存 Json 文件的格式参数
/// </summary>
public static JsonSerializerOptions JsonOptions => _jsonOptions;
private static readonly JsonSerializerOptions _jsonOptions = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
/// <summary>
/// 从文件反序列对象, 不会抛出异常
/// </summary>
public static bool Deserialize<T>(string path, out T obj)
{
if (!File.Exists(path))
{
_logger.Error("Json file {0} not found", path);
MessagePopupService.Error($"Json file {path} not found");
}
else
{
try
{
var json = File.ReadAllText(path, Encoding.UTF8);
var model = JsonSerializer.Deserialize<T>(json, _jsonOptions);
if (model is T m)
{
obj = m;
return true;
}
_logger.Error("Null data in file {0}", path);
MessagePopupService.Error($"Null data in file {path}");
}
catch (Exception ex)
{
_logger.Error("Failed to read json file {0}, {1}", path, ex.Message);
_logger.Trace(ex.ToString());
MessagePopupService.Error($"Failed to read json file {path}, {ex.ToString()}");
}
}
obj = default;
return false;
}
/// <summary>
/// 保存至文件, 不会抛出异常
/// </summary>
public static bool Serialize<T>(T obj, string path)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
var json = JsonSerializer.Serialize(obj, _jsonOptions);
File.WriteAllText(path, json, Encoding.UTF8);
}
catch (Exception ex)
{
_logger.Error("Failed to save json file {0}, {1}", path, ex.Message);
_logger.Trace(ex.ToString());
MessagePopupService.Error($"Failed to save json file {path}, {ex.ToString()}");
return false;
}
return true;
}
}
public class ColorJsonConverter : JsonConverter<Color>
{
public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 解析 JSON 对象
var jsonObject = JsonDocument.ParseValue(ref reader).RootElement;
var r = jsonObject.GetProperty("R").GetByte();
var g = jsonObject.GetProperty("G").GetByte();
var b = jsonObject.GetProperty("B").GetByte();
var a = jsonObject.GetProperty("A").GetByte();
return Color.FromArgb(a, r, g, b);
}
public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteNumber("R", value.R);
writer.WriteNumber("G", value.G);
writer.WriteNumber("B", value.B);
writer.WriteNumber("A", value.A);
writer.WriteEndObject();
}
}
}

View File

@@ -6,7 +6,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Data; using System.Windows.Data;
namespace SpineViewer.Extensions namespace SpineViewer.Utils
{ {
public class ObservableCollectionWithLock<T> : ObservableCollection<T> public class ObservableCollectionWithLock<T> : ObservableCollection<T>
{ {

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Data; using System.Windows.Data;
namespace SpineViewer.Extensions namespace SpineViewer.Utils
{ {
public class StringFormatMultiValueConverter : IMultiValueConverter public class StringFormatMultiValueConverter : IMultiValueConverter
{ {
@@ -16,7 +16,7 @@ namespace SpineViewer.Extensions
if (values == null || values.Length <= 0) if (values == null || values.Length <= 0)
return DependencyProperty.UnsetValue; return DependencyProperty.UnsetValue;
if (App.Current.TryFindResource(parameter) is string format) if (Application.Current.TryFindResource(parameter) is string format)
{ {
return string.Format(culture, format, values); return string.Format(culture, format, values);
} }

View File

@@ -12,6 +12,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
namespace SpineViewer.ViewModels namespace SpineViewer.ViewModels
{ {
public class DiagnosticsDialogViewModel : ObservableObject public class DiagnosticsDialogViewModel : ObservableObject
@@ -31,7 +32,20 @@ namespace SpineViewer.ViewModels
} }
} }
public string Memory => $"{new Microsoft.VisualBasic.Devices.ComputerInfo().TotalPhysicalMemory / 1024f / 1024f / 1024f:F1} GB"; public string Memory
{
get
{
var searcher = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
foreach (ManagementObject obj in searcher.Get())
{
ulong bytes = (ulong)obj["TotalPhysicalMemory"];
float gb = bytes / 1024f / 1024f / 1024f;
return $"{gb:F1} GB";
}
return "Unknown";
}
}
public string WindowsVersion public string WindowsVersion
{ {

View File

@@ -5,7 +5,9 @@ using SFMLRenderer;
using Spine; using Spine;
using Spine.Exporters; using Spine.Exporters;
using SpineViewer.Extensions; using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources; using SpineViewer.Resources;
using SpineViewer.ViewModels.MainWindow;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@@ -100,18 +102,19 @@ namespace SpineViewer.ViewModels.Exporters
/// </summary> /// </summary>
protected void SetAutoResolutionStatic(BaseExporter exporter, params SpineObject[] spines) protected void SetAutoResolutionStatic(BaseExporter exporter, params SpineObject[] spines)
{ {
var bounds = spines[0].GetAnimationBounds(); var bounds = spines[0].GetCurrentBounds();
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetAnimationBounds()); foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetCurrentBounds());
SetAutoResolution(exporter, bounds); SetAutoResolution(exporter, bounds);
} }
/// <summary> /// <summary>
/// 使用提供的模型设置导出器的自动分辨率和视区参数, 动画画面 /// 使用提供的模型设置导出器的自动分辨率和视区参数, 动画画面
/// </summary> /// </summary>
protected void SetAutoResolutionAnimated(BaseExporter exporter, params SpineObject[] spines) protected void SetAutoResolutionAnimated(VideoExporter exporter, params SpineObject[] spines)
{ {
var bounds = spines[0].GetAnimationBounds(); var fps = exporter.Fps;
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetAnimationBounds()); var bounds = spines[0].GetAnimationBounds(fps);
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetAnimationBounds(fps));
SetAutoResolution(exporter, bounds); SetAutoResolution(exporter, bounds);
} }
@@ -132,9 +135,21 @@ namespace SpineViewer.ViewModels.Exporters
return null; return null;
} }
public RelayCommand<IList?> Cmd_Export => _cmd_Export ??= new(Export_Execute, args => args is not null && args.Count > 0); public RelayCommand<IList?> Cmd_Export => _cmd_Export ??= new(Export_Execute, Export_CanExecute);
private RelayCommand<IList?>? _cmd_Export; private RelayCommand<IList?>? _cmd_Export;
protected abstract void Export_Execute(IList? args); private void Export_Execute(IList? args)
{
if (!Export_CanExecute(args)) return;
Export(args.Cast<SpineObjectModel>().ToArray());
// XXX: 导出途中应该停掉渲染好一些, 让性能专注在导出上
}
private bool Export_CanExecute(IList? args)
{
return args is not null && args.Count > 0;
}
protected abstract void Export(SpineObjectModel[] models);
} }
} }

View File

@@ -12,6 +12,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.IO; using System.IO;
using SpineViewer.ViewModels.MainWindow;
namespace SpineViewer.ViewModels.Exporters namespace SpineViewer.ViewModels.Exporters
{ {
@@ -46,11 +47,10 @@ namespace SpineViewer.ViewModels.Exporters
return null; return null;
} }
protected override void Export_Execute(IList? args) protected override void Export(SpineObjectModel[] models)
{ {
if (args is null || args.Count <= 0) return; if (!DialogService.ShowCustomFFmpegExporterDialog(this)) return;
if (!ExporterDialogService.ShowCustomFFmpegExporterDialog(this)) return; SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_CustomFFmpegExporterTitle); ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_CustomFFmpegExporterTitle);
foreach (var sp in spines) sp.Dispose(); foreach (var sp in spines) sp.Dispose();
} }
@@ -63,7 +63,6 @@ namespace SpineViewer.ViewModels.Exporters
using var exporter = new CustomFFmpegExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2) using var exporter = new CustomFFmpegExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2)
{ {
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A), BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
Duration = _duration,
Fps = _fps, Fps = _fps,
KeepLast = _keepLast, KeepLast = _keepLast,
Format = _format, Format = _format,
@@ -90,6 +89,7 @@ namespace SpineViewer.ViewModels.Exporters
var output = Path.Combine(_outputDir!, filename); var output = Path.Combine(_outputDir!, filename);
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines); if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
exporter.ProgressReporter = (total, done, text) => exporter.ProgressReporter = (total, done, text) =>
{ {

View File

@@ -12,12 +12,13 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.IO; using System.IO;
using System.Collections.Immutable; using System.Collections.Immutable;
using SpineViewer.ViewModels.MainWindow;
namespace SpineViewer.ViewModels.Exporters namespace SpineViewer.ViewModels.Exporters
{ {
public class FFmpegVideoExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain) public class FFmpegVideoExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{ {
public ImmutableArray<FFmpegVideoExporter.VideoFormat> VideoFormats { get; } = Enum.GetValues<FFmpegVideoExporter.VideoFormat>().ToImmutableArray(); public ImmutableArray<FFmpegVideoExporter.VideoFormat> VideoFormatOptions { get; } = Enum.GetValues<FFmpegVideoExporter.VideoFormat>().ToImmutableArray();
public FFmpegVideoExporter.VideoFormat Format { get => _format; set => SetProperty(ref _format, value); } public FFmpegVideoExporter.VideoFormat Format { get => _format; set => SetProperty(ref _format, value); }
protected FFmpegVideoExporter.VideoFormat _format = FFmpegVideoExporter.VideoFormat.Mp4; protected FFmpegVideoExporter.VideoFormat _format = FFmpegVideoExporter.VideoFormat.Mp4;
@@ -33,11 +34,10 @@ namespace SpineViewer.ViewModels.Exporters
private string FormatSuffix => $".{_format.ToString().ToLower()}"; private string FormatSuffix => $".{_format.ToString().ToLower()}";
protected override void Export_Execute(IList? args) protected override void Export(SpineObjectModel[] models)
{ {
if (args is null || args.Count <= 0) return; if (!DialogService.ShowFFmpegVideoExporterDialog(this)) return;
if (!ExporterDialogService.ShowFFmpegVideoExporterDialog(this)) return; SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FFmpegVideoExporterTitle); ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FFmpegVideoExporterTitle);
foreach (var sp in spines) sp.Dispose(); foreach (var sp in spines) sp.Dispose();
} }
@@ -50,7 +50,6 @@ namespace SpineViewer.ViewModels.Exporters
using var exporter = new FFmpegVideoExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2) using var exporter = new FFmpegVideoExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2)
{ {
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A), BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
Duration = _duration,
Fps = _fps, Fps = _fps,
KeepLast = _keepLast, KeepLast = _keepLast,
Format = _format, Format = _format,
@@ -75,6 +74,7 @@ namespace SpineViewer.ViewModels.Exporters
var output = Path.Combine(_outputDir!, filename); var output = Path.Combine(_outputDir!, filename);
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines); if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
exporter.ProgressReporter = (total, done, text) => exporter.ProgressReporter = (total, done, text) =>
{ {

View File

@@ -6,6 +6,7 @@ using SpineViewer.Extensions;
using SpineViewer.Models; using SpineViewer.Models;
using SpineViewer.Resources; using SpineViewer.Resources;
using SpineViewer.Services; using SpineViewer.Services;
using SpineViewer.ViewModels.MainWindow;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@@ -19,7 +20,7 @@ namespace SpineViewer.ViewModels.Exporters
{ {
public class FrameExporterViewModel(MainWindowViewModel vmMain) : BaseExporterViewModel(vmMain) public class FrameExporterViewModel(MainWindowViewModel vmMain) : BaseExporterViewModel(vmMain)
{ {
public ImmutableArray<SKEncodedImageFormat> FrameFormats { get; } = Enum.GetValues<SKEncodedImageFormat>().ToImmutableArray(); public ImmutableArray<SKEncodedImageFormat> FrameFormatOptions { get; } = Enum.GetValues<SKEncodedImageFormat>().ToImmutableArray();
public SKEncodedImageFormat Format { get => _format; set => SetProperty(ref _format, value); } public SKEncodedImageFormat Format { get => _format; set => SetProperty(ref _format, value); }
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png; protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;
@@ -37,11 +38,10 @@ namespace SpineViewer.ViewModels.Exporters
} }
} }
protected override void Export_Execute(IList? args) protected override void Export(SpineObjectModel[] models)
{ {
if (args is null || args.Count <= 0) return; if (!DialogService.ShowFrameExporterDialog(this)) return;
if (!ExporterDialogService.ShowFrameExporterDialog(this)) return; SpineObject[] spines = models.Select(m => m.GetSpineObject(true)).ToArray();
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject(true)).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameExporterTitle); ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameExporterTitle);
foreach (var sp in spines) sp.Dispose(); foreach (var sp in spines) sp.Dispose();
} }

View File

@@ -4,6 +4,7 @@ using SpineViewer.Extensions;
using SpineViewer.Models; using SpineViewer.Models;
using SpineViewer.Resources; using SpineViewer.Resources;
using SpineViewer.Services; using SpineViewer.Services;
using SpineViewer.ViewModels.MainWindow;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@@ -16,11 +17,10 @@ namespace SpineViewer.ViewModels.Exporters
{ {
public class FrameSequenceExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain) public class FrameSequenceExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{ {
protected override void Export_Execute(IList? args) protected override void Export(SpineObjectModel[] models)
{ {
if (args is null || args.Count <= 0) return; if (!DialogService.ShowFrameSequenceExporterDialog(this)) return;
if (!ExporterDialogService.ShowFrameSequenceExporterDialog(this)) return; SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameSequenceExporterTitle); ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameSequenceExporterTitle);
foreach (var sp in spines) sp.Dispose(); foreach (var sp in spines) sp.Dispose();
} }
@@ -33,7 +33,6 @@ namespace SpineViewer.ViewModels.Exporters
using var exporter = new FrameSequenceExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2) using var exporter = new FrameSequenceExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2)
{ {
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A), BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
Duration = _duration,
Fps = _fps, Fps = _fps,
KeepLast = _keepLast KeepLast = _keepLast
}; };
@@ -54,6 +53,7 @@ namespace SpineViewer.ViewModels.Exporters
var output = Path.Combine(_outputDir!, folderName); var output = Path.Combine(_outputDir!, folderName);
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines); if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
exporter.ProgressReporter = (total, done, text) => exporter.ProgressReporter = (total, done, text) =>
{ {

View File

@@ -1,4 +1,5 @@
using SpineViewer.Resources; using SpineViewer.Resources;
using SpineViewer.ViewModels.MainWindow;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -17,14 +18,5 @@ namespace SpineViewer.ViewModels.Exporters
public bool KeepLast { get => _keepLast; set => SetProperty(ref _keepLast, value); } public bool KeepLast { get => _keepLast; set => SetProperty(ref _keepLast, value); }
protected bool _keepLast = true; protected bool _keepLast = true;
public override string? Validate()
{
if (base.Validate() is string err)
return err;
if (_exportSingle && _duration <= 0)
return AppResource.Str_InvalidDuration;
return null;
}
} }
} }

View File

@@ -8,7 +8,6 @@ using SpineViewer.Extensions;
using SpineViewer.Models; using SpineViewer.Models;
using SpineViewer.Resources; using SpineViewer.Resources;
using SpineViewer.Services; using SpineViewer.Services;
using SpineViewer.ViewModels;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@@ -22,7 +21,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Shell; using System.Windows.Shell;
namespace SpineViewer.ViewModels namespace SpineViewer.ViewModels.MainWindow
{ {
public class ExplorerListViewModel : ObservableObject public class ExplorerListViewModel : ObservableObject
{ {
@@ -95,7 +94,7 @@ namespace SpineViewer.ViewModels
/// </summary> /// </summary>
public RelayCommand Cmd_ChangeCurrentDirectory => _cmd_ChangeCurrentDirectory ??= new(() => public RelayCommand Cmd_ChangeCurrentDirectory => _cmd_ChangeCurrentDirectory ??= new(() =>
{ {
if (OpenFolderService.OpenFolder(out var selectedPath)) if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{ {
_currentDirectory = selectedPath; _currentDirectory = selectedPath;
RefreshItems(); RefreshItems();

View File

@@ -1,25 +1,13 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using HandyControl.Controls;
using NLog; using NLog;
using SFMLRenderer; using SFMLRenderer;
using Spine;
using Spine.Exporters;
using SpineViewer.Extensions;
using SpineViewer.Models; using SpineViewer.Models;
using SpineViewer.Services; using SpineViewer.Services;
using SpineViewer.ViewModels.Exporters; using SpineViewer.Utils;
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Shell; using System.Windows.Shell;
namespace SpineViewer.ViewModels namespace SpineViewer.ViewModels.MainWindow
{ {
/// <summary> /// <summary>
/// MainWindow 上下文对象 /// MainWindow 上下文对象
@@ -34,9 +22,10 @@ namespace SpineViewer.ViewModels
_explorerListViewModel = new(this); _explorerListViewModel = new(this);
_spineObjectListViewModel = new(this); _spineObjectListViewModel = new(this);
_sfmlRendererViewModel = new(this); _sfmlRendererViewModel = new(this);
_preferenceViewModel = new(this);
} }
public string Title => $"SpineViewer - {App.Version}"; public string Title => $"SpineViewer - v{App.Version}";
/// <summary> /// <summary>
/// SFML 渲染对象 /// SFML 渲染对象
@@ -56,6 +45,9 @@ namespace SpineViewer.ViewModels
public ObservableCollectionWithLock<SpineObjectModel> SpineObjects => _spineObjectModels; public ObservableCollectionWithLock<SpineObjectModel> SpineObjects => _spineObjectModels;
private readonly ObservableCollectionWithLock<SpineObjectModel> _spineObjectModels = []; private readonly ObservableCollectionWithLock<SpineObjectModel> _spineObjectModels = [];
public PreferenceViewModel PreferenceViewModel => _preferenceViewModel;
private readonly PreferenceViewModel _preferenceViewModel;
/// <summary> /// <summary>
/// 浏览页列表 ViewModel /// 浏览页列表 ViewModel
/// </summary> /// </summary>
@@ -80,18 +72,63 @@ namespace SpineViewer.ViewModels
public SFMLRendererViewModel SFMLRendererViewModel => _sfmlRendererViewModel; public SFMLRendererViewModel SFMLRendererViewModel => _sfmlRendererViewModel;
private readonly SFMLRendererViewModel _sfmlRendererViewModel; private readonly SFMLRendererViewModel _sfmlRendererViewModel;
/// <summary>
/// 打开工作区
/// </summary>
public RelayCommand Cmd_OpenWorkspace => _cmd_OpenWorkspace ??= new(OpenWorkspace_Execute);
private RelayCommand? _cmd_OpenWorkspace;
private void OpenWorkspace_Execute()
{
if (!DialogService.ShowOpenJsonDialog(out var fileName)) return;
if (JsonHelper.Deserialize<WorkspaceModel>(fileName, out var obj))
{
Workspace = obj;
}
}
/// <summary>
/// 保存工作区
/// </summary>
public RelayCommand Cmd_SaveWorkspace => _cmd_SaveWorkspace ??= new(SaveWorkspace_Execute);
private RelayCommand? _cmd_SaveWorkspace;
private void SaveWorkspace_Execute()
{
string fileName = "workspace.jcfg";
if (!DialogService.ShowSaveJsonDialog(ref fileName)) return;
JsonHelper.Serialize(Workspace, fileName);
}
/// <summary> /// <summary>
/// 显示诊断信息对话框 /// 显示诊断信息对话框
/// </summary> /// </summary>
public RelayCommand Cmd_ShowDiagnosticsDialog => _cmd_ShowDiagnosticsDialog ??= new(() => { DiagnosticsDialogService.ShowDiagnosticsDialog(); }); public RelayCommand Cmd_ShowDiagnosticsDialog => _cmd_ShowDiagnosticsDialog ??= new(() => { DialogService.ShowDiagnosticsDialog(); });
private RelayCommand? _cmd_ShowDiagnosticsDialog; private RelayCommand? _cmd_ShowDiagnosticsDialog;
/// <summary> /// <summary>
/// 显示关于对话框 /// 显示关于对话框
/// </summary> /// </summary>
public RelayCommand Cmd_ShowAboutDialog => _cmd_ShowAboutDialog ??= new(() => { AboutDialogService.ShowAboutDialog(); }); public RelayCommand Cmd_ShowAboutDialog => _cmd_ShowAboutDialog ??= new(() => { DialogService.ShowAboutDialog(); });
private RelayCommand? _cmd_ShowAboutDialog; private RelayCommand? _cmd_ShowAboutDialog;
public WorkspaceModel Workspace
{
get
{
return new()
{
RendererConfig = _sfmlRendererViewModel.WorkspaceConfig,
LoadedSpineObjects = _spineObjectListViewModel.LoadedSpineObjects
};
}
set
{
_sfmlRendererViewModel.WorkspaceConfig = value.RendererConfig;
_spineObjectListViewModel.LoadedSpineObjects = value.LoadedSpineObjects;
}
}
/// <summary> /// <summary>
/// 调试命令 /// 调试命令
/// </summary> /// </summary>
@@ -101,31 +138,8 @@ namespace SpineViewer.ViewModels
private void Debug_Execute() private void Debug_Execute()
{ {
#if DEBUG #if DEBUG
var path = @"C:\Users\ljh\Desktop\a.mp4";
using var exporter = new FFmpegVideoExporter(_sfmlRenderer.Resolution); MessagePopupService.Quest("测试一下");
using var view = _sfmlRenderer.GetView();
exporter.Center = view.Center;
exporter.Size = view.Size;
exporter.Rotation = view.Rotation;
exporter.Viewport = view.Viewport;
SpineObject[] spines;
lock (_spineObjectModels.Lock)
{
spines = _spineObjectModels.Select(it => it.GetSpineObject(true)).ToArray();
}
exporter.Fps = 60;
exporter.Format = FFmpegVideoExporter.VideoFormat.Webm;
exporter.Duration = 3;
exporter.BackgroundColor = new(0, 0, 0, 0);
ProgressService.RunAsync((pr, ct) =>
{
exporter.ProgressReporter = (total, done, text) =>
{ pr.Total = total; pr.Done = done; pr.ProgressText = text; };
exporter.Export(path, ct, spines);
}, "测试一下");
#endif #endif
} }
} }

View File

@@ -0,0 +1,241 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NLog;
using Spine.SpineWrappers;
using SpineViewer.Models;
using SpineViewer.Services;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
namespace SpineViewer.ViewModels.MainWindow
{
public class PreferenceViewModel : ObservableObject
{
/// <summary>
/// 文件保存路径
/// </summary>
public static readonly string PreferenceFilePath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "preference.json");
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly MainWindowViewModel _vmMain;
public PreferenceViewModel(MainWindowViewModel vmMain)
{
_vmMain = vmMain;
}
/// <summary>
/// 显示首选项对话框
/// </summary>
public RelayCommand Cmd_ShowPreferenceDialog => _cmd_ShowPreferenceDialog ??= new(ShowPreferenceDialog_Execute);
private RelayCommand? _cmd_ShowPreferenceDialog;
private void ShowPreferenceDialog_Execute()
{
var m = Preference;
if (!DialogService.ShowPreferenceDialog(m))
return;
Preference = m;
SavePreference(m);
}
private static void SavePreference(PreferenceModel m)
{
JsonHelper.Serialize(m, PreferenceFilePath);
}
/// <summary>
/// 保存首选项, 保存失败会有日志提示
/// </summary>
public void SavePreference() => SavePreference(Preference);
/// <summary>
/// 加载首选项, 加载失败会有日志提示
/// </summary>
public void LoadPreference()
{
if (JsonHelper.Deserialize<PreferenceModel>(PreferenceFilePath, out var obj))
Preference = obj;
}
/// <summary>
/// 获取参数副本或者进行设置
/// </summary>
private PreferenceModel Preference
{
get
{
return new()
{
ForcePremul = ForcePremul,
ForceNearest = ForceNearest,
ForceMipmap = ForceMipmap,
IsShown = IsShown,
UsePma = UsePma,
DebugTexture = DebugTexture,
DebugBounds = DebugBounds,
DebugBones = DebugBones,
DebugRegions = DebugRegions,
DebugMeshHulls = DebugMeshHulls,
DebugMeshes = DebugMeshes,
DebugBoundingBoxes = DebugBoundingBoxes,
DebugPaths = DebugPaths,
DebugPoints = DebugPoints,
DebugClippings = DebugClippings,
RenderSelectedOnly = RenderSelectedOnly,
AppLanguage = AppLanguage,
};
}
set
{
ForcePremul = value.ForcePremul;
ForceNearest = value.ForceNearest;
ForceMipmap = value.ForceMipmap;
IsShown = value.IsShown;
UsePma = value.UsePma;
DebugTexture = value.DebugTexture;
DebugBounds = value.DebugBounds;
DebugBones = value.DebugBones;
DebugRegions = value.DebugRegions;
DebugMeshHulls = value.DebugMeshHulls;
DebugMeshes = value.DebugMeshes;
DebugBoundingBoxes = value.DebugBoundingBoxes;
DebugPaths = value.DebugPaths;
DebugPoints = value.DebugPoints;
DebugClippings = value.DebugClippings;
RenderSelectedOnly = value.RenderSelectedOnly;
AppLanguage = value.AppLanguage;
}
}
#region
public bool ForcePremul
{
get => TextureLoader.DefaultLoader.ForcePremul;
set => SetProperty(TextureLoader.DefaultLoader.ForcePremul, value, v => TextureLoader.DefaultLoader.ForcePremul = v);
}
public bool ForceNearest
{
get => TextureLoader.DefaultLoader.ForceNearest;
set => SetProperty(TextureLoader.DefaultLoader.ForceNearest, value, v => TextureLoader.DefaultLoader.ForceNearest = v);
}
public bool ForceMipmap
{
get => TextureLoader.DefaultLoader.ForceMipmap;
set => SetProperty(TextureLoader.DefaultLoader.ForceMipmap, value, v => TextureLoader.DefaultLoader.ForceMipmap = v);
}
#endregion
#region
public bool IsShown
{
get => SpineObjectModel.LoadOptions.IsShown;
set => SetProperty(SpineObjectModel.LoadOptions.IsShown, value, v => SpineObjectModel.LoadOptions.IsShown = v);
}
public bool UsePma
{
get => SpineObjectModel.LoadOptions.UsePma;
set => SetProperty(SpineObjectModel.LoadOptions.UsePma, value, v => SpineObjectModel.LoadOptions.UsePma = v);
}
public bool DebugTexture
{
get => SpineObjectModel.LoadOptions.DebugTexture;
set => SetProperty(SpineObjectModel.LoadOptions.DebugTexture, value, v => SpineObjectModel.LoadOptions.DebugTexture = v);
}
public bool DebugBounds
{
get => SpineObjectModel.LoadOptions.DebugBounds;
set => SetProperty(SpineObjectModel.LoadOptions.DebugBounds, value, v => SpineObjectModel.LoadOptions.DebugBounds = v);
}
public bool DebugBones
{
get => SpineObjectModel.LoadOptions.DebugBones;
set => SetProperty(SpineObjectModel.LoadOptions.DebugBones, value, v => SpineObjectModel.LoadOptions.DebugBones = v);
}
public bool DebugRegions
{
get => SpineObjectModel.LoadOptions.DebugRegions;
set => SetProperty(SpineObjectModel.LoadOptions.DebugRegions, value, v => SpineObjectModel.LoadOptions.DebugRegions = v);
}
public bool DebugMeshHulls
{
get => SpineObjectModel.LoadOptions.DebugMeshHulls;
set => SetProperty(SpineObjectModel.LoadOptions.DebugMeshHulls, value, v => SpineObjectModel.LoadOptions.DebugMeshHulls = v);
}
public bool DebugMeshes
{
get => SpineObjectModel.LoadOptions.DebugMeshes;
set => SetProperty(SpineObjectModel.LoadOptions.DebugMeshes, value, v => SpineObjectModel.LoadOptions.DebugMeshes = v);
}
public bool DebugBoundingBoxes
{
get => SpineObjectModel.LoadOptions.DebugBoundingBoxes;
set => SetProperty(SpineObjectModel.LoadOptions.DebugBoundingBoxes, value, v => SpineObjectModel.LoadOptions.DebugBoundingBoxes = v);
}
public bool DebugPaths
{
get => SpineObjectModel.LoadOptions.DebugPaths;
set => SetProperty(SpineObjectModel.LoadOptions.DebugPaths, value, v => SpineObjectModel.LoadOptions.DebugPaths = v);
}
public bool DebugPoints
{
get => SpineObjectModel.LoadOptions.DebugPoints;
set => SetProperty(SpineObjectModel.LoadOptions.DebugPoints, value, v => SpineObjectModel.LoadOptions.DebugPoints = v);
}
public bool DebugClippings
{
get => SpineObjectModel.LoadOptions.DebugClippings;
set => SetProperty(SpineObjectModel.LoadOptions.DebugClippings, value, v => SpineObjectModel.LoadOptions.DebugClippings = v);
}
#endregion
#region
public static ImmutableArray<AppLanguage> AppLanguageOptions { get; } = Enum.GetValues<AppLanguage>().ToImmutableArray();
public bool RenderSelectedOnly
{
get => _vmMain.SFMLRendererViewModel.RenderSelectedOnly;
set => SetProperty(_vmMain.SFMLRendererViewModel.RenderSelectedOnly, value, v => _vmMain.SFMLRendererViewModel.RenderSelectedOnly = v);
}
public AppLanguage AppLanguage
{
get => ((App)App.Current).Language;
set => SetProperty(((App)App.Current).Language, value, v => ((App)App.Current).Language = v);
}
#endregion
}
}

View File

@@ -7,6 +7,7 @@ using SpineViewer.Extensions;
using SpineViewer.Models; using SpineViewer.Models;
using SpineViewer.Resources; using SpineViewer.Resources;
using SpineViewer.Services; using SpineViewer.Services;
using SpineViewer.Utils;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
@@ -18,7 +19,7 @@ using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
namespace SpineViewer.ViewModels namespace SpineViewer.ViewModels.MainWindow
{ {
public class SFMLRendererViewModel : ObservableObject public class SFMLRendererViewModel : ObservableObject
{ {
@@ -31,6 +32,16 @@ namespace SpineViewer.ViewModels
private readonly ObservableCollectionWithLock<SpineObjectModel> _models; private readonly ObservableCollectionWithLock<SpineObjectModel> _models;
private readonly ISFMLRenderer _renderer; private readonly ISFMLRenderer _renderer;
/// <summary>
/// 被选中对象的背景颜色
/// </summary>
private static readonly SFML.Graphics.Color _selectedBackgroundColor = new(255, 255, 255, 50);
/// <summary>
/// 被选中对象背景顶点缓冲区
/// </summary>
private readonly SFML.Graphics.VertexArray _selectedBackgroundVertices = new(SFML.Graphics.PrimitiveType.Quads, 4); // XXX: 暂时未使用 Dispose 释放
/// <summary> /// <summary>
/// 预览画面坐标轴颜色 /// 预览画面坐标轴颜色
/// </summary> /// </summary>
@@ -78,55 +89,55 @@ namespace SpineViewer.ViewModels
public uint ResolutionX public uint ResolutionX
{ {
get => _renderer.Resolution.X; get => _renderer.Resolution.X;
set => SetProperty(_renderer.Resolution.X, value, _renderer, (r, v) => r.Resolution = new(v, r.Resolution.Y)); set => SetProperty(_renderer.Resolution.X, value, v => _renderer.Resolution = new(v, _renderer.Resolution.Y));
} }
public uint ResolutionY public uint ResolutionY
{ {
get => _renderer.Resolution.Y; get => _renderer.Resolution.Y;
set => SetProperty(_renderer.Resolution.Y, value, _renderer, (r, v) => r.Resolution = new(r.Resolution.X, v)); set => SetProperty(_renderer.Resolution.Y, value, v => _renderer.Resolution = new(_renderer.Resolution.X, v));
} }
public float CenterX public float CenterX
{ {
get => _renderer.Center.X; get => _renderer.Center.X;
set => SetProperty(_renderer.Center.X, value, _renderer, (r, v) => r.Center = new(v, r.Center.Y)); set => SetProperty(_renderer.Center.X, value, v => _renderer.Center = new(v, _renderer.Center.Y));
} }
public float CenterY public float CenterY
{ {
get => _renderer.Center.Y; get => _renderer.Center.Y;
set => SetProperty(_renderer.Center.Y, value, _renderer, (r, v) => r.Center = new(r.Center.X, v)); set => SetProperty(_renderer.Center.Y, value, v => _renderer.Center = new(_renderer.Center.X, v));
} }
public float Zoom public float Zoom
{ {
get => _renderer.Zoom; get => _renderer.Zoom;
set => SetProperty(_renderer.Zoom, value, _renderer, (r, v) => r.Zoom = value); set => SetProperty(_renderer.Zoom, value, v => _renderer.Zoom = value);
} }
public float Rotation public float Rotation
{ {
get => _renderer.Rotation; get => _renderer.Rotation;
set => SetProperty(_renderer.Rotation, value, _renderer, (r, v) => r.Rotation = value); set => SetProperty(_renderer.Rotation, value, v => _renderer.Rotation = value);
} }
public bool FlipX public bool FlipX
{ {
get => _renderer.FlipX; get => _renderer.FlipX;
set => SetProperty(_renderer.FlipX, value, _renderer, (r, v) => r.FlipX = value); set => SetProperty(_renderer.FlipX, value, v => _renderer.FlipX = value);
} }
public bool FlipY public bool FlipY
{ {
get => _renderer.FlipY; get => _renderer.FlipY;
set => SetProperty(_renderer.FlipY, value, _renderer, (r, v) => r.FlipY = value); set => SetProperty(_renderer.FlipY, value, v => _renderer.FlipY = value);
} }
public uint MaxFps public uint MaxFps
{ {
get => _renderer.MaxFps; get => _renderer.MaxFps;
set => SetProperty(_renderer.MaxFps, value, _renderer, (r, v) => r.MaxFps = value); set => SetProperty(_renderer.MaxFps, value, v => _renderer.MaxFps = value);
} }
public bool ShowAxis public bool ShowAxis
@@ -139,7 +150,7 @@ namespace SpineViewer.ViewModels
public Color BackgroundColor public Color BackgroundColor
{ {
get => Color.FromRgb(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B); get => Color.FromRgb(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B);
set => SetProperty(BackgroundColor, value, this, (m, v) => m._backgroundColor = new(value.R, value.G, value.B)); set => SetProperty(BackgroundColor, value, v => _backgroundColor = new(value.R, value.G, value.B));
} }
private SFML.Graphics.Color _backgroundColor = new(105, 105, 105); private SFML.Graphics.Color _backgroundColor = new(105, 105, 105);
@@ -381,6 +392,18 @@ namespace SpineViewer.ViewModels
sp.Update(0); // 避免物理效果出现问题 sp.Update(0); // 避免物理效果出现问题
sp.Update(delta); sp.Update(delta);
// 为选中对象绘制一个半透明背景
if (sp.IsSelected)
{
var rc = sp.GetCurrentBounds().ToFloatRect();
_selectedBackgroundVertices[0] = new(new(rc.Left, rc.Top), _selectedBackgroundColor);
_selectedBackgroundVertices[1] = new(new(rc.Left + rc.Width, rc.Top), _selectedBackgroundColor);
_selectedBackgroundVertices[2] = new(new(rc.Left + rc.Width, rc.Top + rc.Height), _selectedBackgroundColor);
_selectedBackgroundVertices[3] = new(new(rc.Left, rc.Top + rc.Height), _selectedBackgroundColor);
_renderer.Draw(_selectedBackgroundVertices);
}
// 仅在预览画面临时启用调试模式
sp.EnableDebug = true; sp.EnableDebug = true;
_renderer.Draw(sp); _renderer.Draw(sp);
sp.EnableDebug = false; sp.EnableDebug = false;
@@ -401,5 +424,41 @@ namespace SpineViewer.ViewModels
_renderer.SetActive(false); _renderer.SetActive(false);
} }
} }
public RendererWorkspaceConfigModel WorkspaceConfig
{
// TODO: 背景图片
get
{
return new()
{
ResolutionX = ResolutionX,
ResolutionY = ResolutionY,
CenterX = CenterX,
CenterY = CenterY,
Zoom = Zoom,
Rotation = Rotation,
FlipX = FlipX,
FlipY = FlipY,
MaxFps = MaxFps,
ShowAxis = ShowAxis,
BackgroundColor = BackgroundColor,
};
}
set
{
ResolutionX = value.ResolutionX;
ResolutionY = value.ResolutionY;
CenterX = value.CenterX;
CenterY = value.CenterY;
Zoom = value.Zoom;
Rotation = value.Rotation;
FlipX = value.FlipX;
FlipY = value.FlipY;
MaxFps = value.MaxFps;
ShowAxis = value.ShowAxis;
BackgroundColor = value.BackgroundColor;
}
}
} }
} }

View File

@@ -6,6 +6,7 @@ using SpineViewer.Extensions;
using SpineViewer.Models; using SpineViewer.Models;
using SpineViewer.Resources; using SpineViewer.Resources;
using SpineViewer.Services; using SpineViewer.Services;
using SpineViewer.Utils;
using SpineViewer.ViewModels.Exporters; using SpineViewer.ViewModels.Exporters;
using System; using System;
using System.Collections; using System.Collections;
@@ -17,7 +18,7 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Shell; using System.Windows.Shell;
namespace SpineViewer.ViewModels namespace SpineViewer.ViewModels.MainWindow
{ {
public class SpineObjectListViewModel : ObservableObject public class SpineObjectListViewModel : ObservableObject
{ {
@@ -100,7 +101,7 @@ namespace SpineViewer.ViewModels
private void AddSpineObject_Execute() private void AddSpineObject_Execute()
{ {
throw new NotImplementedException(); MessagePopupService.Info("Not Implemented, try next version :)");
} }
/// <summary> /// <summary>
@@ -111,7 +112,7 @@ namespace SpineViewer.ViewModels
private void RemoveSpineObject_Execute(IList? args) private void RemoveSpineObject_Execute(IList? args)
{ {
if (args is null) return; if (!RemoveSpineObject_CanExecute(args)) return;
if (args.Count > 1) if (args.Count > 1)
{ {
@@ -121,7 +122,7 @@ namespace SpineViewer.ViewModels
lock (_spineObjectModels.Lock) lock (_spineObjectModels.Lock)
{ {
// XXX: 这里必须要浅拷贝一次, 不能直接对会被修改的绑定数据 args 进行 foreach 遍历 // NOTE: 这里必须要浅拷贝一次, 不能直接对会被修改的绑定数据 args 进行 foreach 遍历
foreach (var sp in args.Cast<SpineObjectModel>().ToArray()) foreach (var sp in args.Cast<SpineObjectModel>().ToArray())
{ {
_spineObjectModels.Remove(sp); _spineObjectModels.Remove(sp);
@@ -137,6 +138,121 @@ namespace SpineViewer.ViewModels
return true; return true;
} }
/// <summary>
/// 从剪贴板文件列表添加模型
/// </summary>
public RelayCommand Cmd_AddSpineObjectFromClipboard => _cmd_AddSpineObjectFromClipboard ??= new(AddSpineObjectFromClipboard_Execute);
private RelayCommand? _cmd_AddSpineObjectFromClipboard;
private void AddSpineObjectFromClipboard_Execute()
{
if (!Clipboard.ContainsFileDropList()) return;
AddSpineObjectFromFileList(Clipboard.GetFileDropList().Cast<string>().ToArray());
}
/// <summary>
/// 重新加载模型
/// </summary>
public RelayCommand<IList?> Cmd_ReloadSpineObject => _cmd_ReloadSpineObject ??= new(ReloadSpineObject_Execute, ReloadSpineObject_CanExecute);
private RelayCommand<IList?>? _cmd_ReloadSpineObject;
private void ReloadSpineObject_Execute(IList? args)
{
if (!ReloadSpineObject_CanExecute(args)) return;
if (args.Count <= 1)
{
lock (_spineObjectModels.Lock)
{
var sp = (SpineObjectModel)args[0];
var idx = _spineObjectModels.IndexOf(sp);
if (idx < 0) return;
try
{
var spNew = new SpineObjectModel(sp.SkelPath, sp.AtlasPath);
spNew.ObjectConfig = sp.ObjectConfig;
_spineObjectModels[idx] = spNew;
sp.Dispose();
}
catch (Exception ex)
{
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
_logger.Trace(ex.ToString());
}
}
}
else
{
ProgressService.RunAsync((pr, ct) => ReloadSpineObjectsTask(
args.Cast<SpineObjectModel>().ToArray(), pr, ct),
AppResource.Str_ReloadSpineObjectsTitle
);
}
}
private bool ReloadSpineObject_CanExecute(IList? args)
{
if (args is null) return false;
if (args.Count <= 0) return false;
return true;
}
private void ReloadSpineObjectsTask(SpineObjectModel[] spines, IProgressReporter reporter, CancellationToken ct)
{
int totalCount = spines.Length;
int success = 0;
int error = 0;
_vmMain.ProgressState = TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
reporter.Total = totalCount;
reporter.Done = 0;
reporter.ProgressText = $"[0/{totalCount}]";
for (int i = 0; i < totalCount; i++)
{
if (ct.IsCancellationRequested) break;
var sp = spines[i];
reporter.ProgressText = $"[{i}/{totalCount}] {sp.Name}";
lock (_spineObjectModels.Lock)
{
var idx = _spineObjectModels.IndexOf(sp);
if (idx >= 0)
{
try
{
var spNew = new SpineObjectModel(sp.SkelPath, sp.AtlasPath);
spNew.ObjectConfig = sp.ObjectConfig;
_spineObjectModels[idx] = spNew;
sp.Dispose();
success++;
}
catch (Exception ex)
{
error++;
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
_logger.Trace(ex.ToString());
}
}
}
reporter.Done = i + 1;
reporter.ProgressText = $"[{i + 1}/{totalCount}] {sp.Name}";
_vmMain.ProgressValue = (i + 1f) / totalCount;
}
_vmMain.ProgressState = TaskbarItemProgressState.None;
if (error > 0)
_logger.Warn("Batch reload {0} successfully, {1} failed", success, error);
else
_logger.Info("{0} skel reloaded successfully", success);
_logger.LogCurrentProcessMemoryUsage();
}
/// <summary> /// <summary>
/// 模型上移一位 /// 模型上移一位
/// </summary> /// </summary>
@@ -145,8 +261,7 @@ namespace SpineViewer.ViewModels
private void MoveUpSpineObject_Execute(IList? args) private void MoveUpSpineObject_Execute(IList? args)
{ {
if (args is null) return; if (!MoveUpSpineObject_CanExecute(args)) return;
if (args.Count != 1) return;
var sp = (SpineObjectModel)args[0]; var sp = (SpineObjectModel)args[0];
lock (_spineObjectModels.Lock) lock (_spineObjectModels.Lock)
{ {
@@ -171,8 +286,7 @@ namespace SpineViewer.ViewModels
private void MoveDownSpineObject_Execute(IList? args) private void MoveDownSpineObject_Execute(IList? args)
{ {
if (args is null) return; if (!MoveDownSpineObject_CanExecute(args)) return;
if (args.Count != 1) return;
var sp = (SpineObjectModel)args[0]; var sp = (SpineObjectModel)args[0];
lock (_spineObjectModels.Lock) lock (_spineObjectModels.Lock)
{ {
@@ -189,18 +303,6 @@ namespace SpineViewer.ViewModels
return true; return true;
} }
/// <summary>
/// 从剪贴板文件列表添加模型
/// </summary>
public RelayCommand Cmd_AddSpineObjectFromClipboard => _cmd_AddSpineObjectFromClipboard ??= new(AddSpineObjectFromClipboard_Execute);
private RelayCommand? _cmd_AddSpineObjectFromClipboard;
private void AddSpineObjectFromClipboard_Execute()
{
if (!Clipboard.ContainsFileDropList()) return;
AddSpineObjectFromFileList(Clipboard.GetFileDropList().Cast<string>().ToArray());
}
/// <summary> /// <summary>
/// 复制模型参数 /// 复制模型参数
/// </summary> /// </summary>
@@ -209,10 +311,9 @@ namespace SpineViewer.ViewModels
private void CopySpineObjectConfig_Execute(IList? args) private void CopySpineObjectConfig_Execute(IList? args)
{ {
if (args is null) return; if (!CopySpineObjectConfig_CanExecute(args)) return;
if (args.Count != 1) return;
var sp = (SpineObjectModel)args[0]; var sp = (SpineObjectModel)args[0];
_copiedSpineObjectConfigModel = sp.Dump(); _copiedSpineObjectConfigModel = sp.ObjectConfig;
_logger.Info("Copy config from model: {0}", sp.Name); _logger.Info("Copy config from model: {0}", sp.Name);
} }
@@ -231,12 +332,10 @@ namespace SpineViewer.ViewModels
private void ApplySpineObjectConfig_Execute(IList? args) private void ApplySpineObjectConfig_Execute(IList? args)
{ {
if (_copiedSpineObjectConfigModel is null) return; if (!ApplySpineObjectConfig_CanExecute(args)) return;
if (args is null) return;
if (args.Count <= 0) return;
foreach (SpineObjectModel sp in args) foreach (SpineObjectModel sp in args)
{ {
sp.Load(_copiedSpineObjectConfigModel); sp.ObjectConfig = _copiedSpineObjectConfigModel;
_logger.Info("Apply config to model: {0}", sp.Name); _logger.Info("Apply config to model: {0}", sp.Name);
} }
} }
@@ -249,6 +348,51 @@ namespace SpineViewer.ViewModels
return true; return true;
} }
public RelayCommand<IList?> Cmd_ApplySpineObjectConfigFromFile => _cmd_ApplySpineObjectConfigFromFile ??= new(ApplySpineObjectConfigFromFile_Execute, ApplySpineObjectConfigFromFile_CanExecute);
private RelayCommand<IList?>? _cmd_ApplySpineObjectConfigFromFile;
private void ApplySpineObjectConfigFromFile_Execute(IList? args)
{
if (!ApplySpineObjectConfigFromFile_CanExecute(args)) return;
if (!DialogService.ShowOpenJsonDialog(out var fileName)) return;
if (JsonHelper.Deserialize<SpineObjectConfigModel>(fileName, out var config))
{
foreach (SpineObjectModel sp in args)
{
sp.ObjectConfig = config;
_logger.Info("Apply config to model: {0}", sp.Name);
}
}
}
private bool ApplySpineObjectConfigFromFile_CanExecute(IList? args)
{
if (args is null) return false;
if (args.Count <= 0) return false;
return true;
}
public RelayCommand<IList?> Cmd_SaveSpineObjectConfigToFile => _cmd_SaveSpineObjectConfigToFile ??= new(SaveSpineObjectConfigToFile_Execute, SaveSpineObjectConfigToFile_CanExecute);
private RelayCommand<IList?>? _cmd_SaveSpineObjectConfigToFile;
private void SaveSpineObjectConfigToFile_Execute(IList? args)
{
if (!SaveSpineObjectConfigToFile_CanExecute(args)) return;
var sp = (SpineObjectModel)args[0];
var config = sp.ObjectConfig;
string fileName = $"{Path.ChangeExtension(Path.GetFileName(sp.SkelPath), ".jcfg")}";
if (!DialogService.ShowSaveJsonDialog(ref fileName, sp.AssetsDir)) return;
JsonHelper.Serialize(sp.ObjectConfig, fileName);
}
private bool SaveSpineObjectConfigToFile_CanExecute(IList? args)
{
if (args is null) return false;
if (args.Count != 1) return false;
return true;
}
/// <summary> /// <summary>
/// 从路径列表添加对象 /// 从路径列表添加对象
/// </summary> /// </summary>
@@ -289,17 +433,7 @@ namespace SpineViewer.ViewModels
} }
else if (validPaths.Count > 0) else if (validPaths.Count > 0)
{ {
var skelPath = validPaths[0]; AddSpineObject(validPaths[0]);
try
{
var sp = new SpineObjectModel(skelPath);
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
}
_logger.LogCurrentProcessMemoryUsage(); _logger.LogCurrentProcessMemoryUsage();
} }
} }
@@ -326,18 +460,10 @@ namespace SpineViewer.ViewModels
var skelPath = paths[i]; var skelPath = paths[i];
reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}"; reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}";
try if (AddSpineObject(skelPath))
{
var sp = new SpineObjectModel(skelPath);
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
success++; success++;
} else
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
error++; error++;
}
reporter.Done = i + 1; reporter.Done = i + 1;
reporter.ProgressText = $"[{i + 1}/{totalCount}] {skelPath}"; reporter.ProgressText = $"[{i + 1}/{totalCount}] {skelPath}";
@@ -352,5 +478,132 @@ namespace SpineViewer.ViewModels
_logger.LogCurrentProcessMemoryUsage(); _logger.LogCurrentProcessMemoryUsage();
} }
/// <summary>
/// 安全地在末尾添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
private bool AddSpineObject(string skelPath, string? atlasPath = null)
{
try
{
var sp = new SpineObjectModel(skelPath, atlasPath);
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
return true;
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
}
return false;
}
private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
{
try
{
var sp = new SpineObjectModel(cfg);
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
return true;
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load: {0}, {1}", cfg.SkelPath, ex.Message);
}
return false;
}
public List<SpineObjectWorkspaceConfigModel> LoadedSpineObjects
{
get
{
List<SpineObjectWorkspaceConfigModel> loadedSpineObjects = [];
lock (_spineObjectModels.Lock)
{
foreach (var sp in _spineObjectModels)
{
loadedSpineObjects.Add(sp.WorkspaceConfig);
}
}
return loadedSpineObjects;
}
set
{
AddSpineObjectFromWorkspaceList(value);
}
}
private void AddSpineObjectFromWorkspaceList(List<SpineObjectWorkspaceConfigModel> models)
{
lock (_spineObjectModels.Lock)
{
var spines = _spineObjectModels.ToArray();
_spineObjectModels.Clear();
foreach (var sp in spines)
{
sp.Dispose();
}
}
if (models.Count > 1)
{
ProgressService.RunAsync((pr, ct) => AddSpineObjectFromWorkspaceListTask(
models, pr, ct),
AppResource.Str_AddSpineObjectsTitle
);
}
else if (models.Count > 0)
{
AddSpineObject(models[0]);
_logger.LogCurrentProcessMemoryUsage();
}
}
private void AddSpineObjectFromWorkspaceListTask(List<SpineObjectWorkspaceConfigModel> models, IProgressReporter reporter, CancellationToken ct)
{
int totalCount = models.Count;
int success = 0;
int error = 0;
_vmMain.ProgressState = TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
reporter.Total = totalCount;
reporter.Done = 0;
reporter.ProgressText = $"[0/{totalCount}]";
for (int i = 0; i < totalCount; i++)
{
if (ct.IsCancellationRequested) break;
var cfg = models[i];
reporter.ProgressText = $"[{i}/{totalCount}] {cfg}";
if (AddSpineObject(cfg))
success++;
else
error++;
reporter.Done = i + 1;
reporter.ProgressText = $"[{i + 1}/{totalCount}] {cfg}";
_vmMain.ProgressValue = (i + 1f) / totalCount;
}
_vmMain.ProgressState = TaskbarItemProgressState.None;
if (error > 0)
_logger.Warn("Batch load {0} successfully, {1} failed", success, error);
else
_logger.Info("{0} skel loaded successfully", success);
_logger.LogCurrentProcessMemoryUsage();
// 从工作区加载需要同步一次时间轴
lock (_spineObjectModels.Lock)
{
foreach (var sp in _spineObjectModels)
sp.ResetAnimationsTime();
}
}
} }
} }

View File

@@ -9,7 +9,7 @@ using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Windows; using System.Windows;
namespace SpineViewer.ViewModels namespace SpineViewer.ViewModels.MainWindow
{ {
public class SpineObjectTabViewModel : ObservableObject public class SpineObjectTabViewModel : ObservableObject
{ {
@@ -49,15 +49,15 @@ namespace SpineViewer.ViewModels
IEnumerable<string> commonSkinNames = _selectedObjects[0].Skins; IEnumerable<string> commonSkinNames = _selectedObjects[0].Skins;
foreach (var obj in _selectedObjects.Skip(1)) commonSkinNames = commonSkinNames.Intersect(obj.Skins); foreach (var obj in _selectedObjects.Skip(1)) commonSkinNames = commonSkinNames.Intersect(obj.Skins);
foreach (var name in commonSkinNames) _skins.Add(new(name, _selectedObjects)); foreach (var name in commonSkinNames.Order()) _skins.Add(new(name, _selectedObjects));
IEnumerable<string> commonSlotNames = _selectedObjects[0].SlotAttachments.Keys; IEnumerable<string> commonSlotNames = _selectedObjects[0].SlotAttachments.Keys;
foreach (var obj in _selectedObjects.Skip(1)) commonSlotNames = commonSlotNames.Intersect(obj.SlotAttachments.Keys); foreach (var obj in _selectedObjects.Skip(1)) commonSlotNames = commonSlotNames.Intersect(obj.SlotAttachments.Keys);
foreach (var name in commonSlotNames) _slots.Add(new(name, _selectedObjects)); foreach (var name in commonSlotNames.Order()) _slots.Add(new(name, _selectedObjects));
IEnumerable<int> commonTrackIndices = _selectedObjects[0].GetTrackIndices(); IEnumerable<int> commonTrackIndices = _selectedObjects[0].GetTrackIndices();
foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices()); foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects)); foreach (var idx in commonTrackIndices.Order()) _animationTracks.Add(new(idx, _selectedObjects));
} }
OnPropertyChanged(); OnPropertyChanged();
@@ -611,7 +611,7 @@ namespace SpineViewer.ViewModels
// 但是目前无法识别是否增加了轨道, 因此总是重建列表 // 但是目前无法识别是否增加了轨道, 因此总是重建列表
// 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行 // 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行
App.Current.Dispatcher.BeginInvoke( Application.Current.Dispatcher.BeginInvoke(
() => () =>
{ {
_animationTracks.Clear(); _animationTracks.Clear();
@@ -786,7 +786,7 @@ namespace SpineViewer.ViewModels
{ {
get get
{ {
/// XXX: 空轨道和多选不相同都会返回 null // XXX: 空轨道和多选不相同都会返回 null
if (_spines.Length <= 0) return null; if (_spines.Length <= 0) return null;
var val = _spines[0].GetAnimation(_trackIndex); var val = _spines[0].GetAnimation(_trackIndex);
if (_spines.Skip(1).Any(it => it.GetAnimation(_trackIndex) != val)) return null; if (_spines.Skip(1).Any(it => it.GetAnimation(_trackIndex) != val)) return null;

View File

@@ -63,6 +63,7 @@ namespace SpineViewer.ViewModels
private void Cancel_Execute() private void Cancel_Execute()
{ {
if (!Cancel_CanExecute()) return;
if (!MessagePopupService.Quest(AppResource.Str_CancelQuest)) return; if (!MessagePopupService.Quest(AppResource.Str_CancelQuest)) return;
_cts.Cancel(); _cts.Cancel();
Cmd_Cancel.NotifyCanExecuteChanged(); Cmd_Cancel.NotifyCanExecuteChanged();

View File

@@ -44,7 +44,7 @@ namespace SpineViewer.Views.ExporterDialogs
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e) private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
{ {
if (OpenFolderService.OpenFolder(out var selectedPath)) if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{ {
var vm = (CustomFFmpegExporterViewModel)DataContext; var vm = (CustomFFmpegExporterViewModel)DataContext;
vm.OutputDir = selectedPath; vm.OutputDir = selectedPath;

View File

@@ -128,7 +128,7 @@
<!-- 视频格式 --> <!-- 视频格式 -->
<Label Grid.Row="13" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/> <Label Grid.Row="13" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/>
<ComboBox Grid.Row="13" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding VideoFormats}"/> <ComboBox Grid.Row="13" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding VideoFormatOptions}"/>
<!-- 动图是否循环 --> <!-- 动图是否循环 -->
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/> <Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>

View File

@@ -44,7 +44,7 @@ namespace SpineViewer.Views.ExporterDialogs
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e) private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
{ {
if (OpenFolderService.OpenFolder(out var selectedPath)) if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{ {
var vm = (FFmpegVideoExporterViewModel)DataContext; var vm = (FFmpegVideoExporterViewModel)DataContext;
vm.OutputDir = selectedPath; vm.OutputDir = selectedPath;

View File

@@ -110,7 +110,7 @@
<!-- 图像格式 --> <!-- 图像格式 -->
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_ImageFormat}"/> <Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_ImageFormat}"/>
<ComboBox Grid.Row="9" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding FrameFormats}"/> <ComboBox Grid.Row="9" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding FrameFormatOptions}"/>
<!-- 图像质量 --> <!-- 图像质量 -->
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_ImageQuality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/> <Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_ImageQuality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>

View File

@@ -44,7 +44,7 @@ namespace SpineViewer.Views.ExporterDialogs
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e) private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
{ {
if (OpenFolderService.OpenFolder(out var selectedPath)) if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{ {
var vm = (FrameExporterViewModel)DataContext; var vm = (FrameExporterViewModel)DataContext;
vm.OutputDir = selectedPath; vm.OutputDir = selectedPath;

View File

@@ -44,7 +44,7 @@ namespace SpineViewer.Views.ExporterDialogs
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e) private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
{ {
if (OpenFolderService.OpenFolder(out var selectedPath)) if (DialogService.ShowOpenFolderDialog(out var selectedPath))
{ {
var vm = (FrameSequenceExporterViewModel)DataContext; var vm = (FrameSequenceExporterViewModel)DataContext;
vm.OutputDir = selectedPath; vm.OutputDir = selectedPath;

View File

@@ -5,8 +5,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol" xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:vm="clr-namespace:SpineViewer.ViewModels" xmlns:vm="clr-namespace:SpineViewer.ViewModels.MainWindow"
xmlns:ext="clr-namespace:SpineViewer.Extensions" xmlns:utils="clr-namespace:SpineViewer.Utils"
xmlns:SFMLRenderer="clr-namespace:SFMLRenderer;assembly=SFMLRenderer" xmlns:SFMLRenderer="clr-namespace:SFMLRenderer;assembly=SFMLRenderer"
mc:Ignorable="d" mc:Ignorable="d"
Title="{Binding Title}" Title="{Binding Title}"
@@ -35,7 +35,7 @@
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<ext:StringFormatMultiValueConverter x:Key="StrFmtCvter"/> <utils:StringFormatMultiValueConverter x:Key="StrFmtCvter"/>
</Window.Resources> </Window.Resources>
<Window.TaskbarItemInfo> <Window.TaskbarItemInfo>
@@ -56,9 +56,10 @@
<!-- 菜单 --> <!-- 菜单 -->
<Menu x:Name="_mainMenu"> <Menu x:Name="_mainMenu">
<MenuItem Header="{DynamicResource Str_File}"> <MenuItem Header="{DynamicResource Str_File}">
<MenuItem Header="{DynamicResource Str_Open}" InputGestureText="Ctrl+O"/> <MenuItem Header="{DynamicResource Str_OpenWorkspace}" Command="{Binding Cmd_OpenWorkspace}"/>
<MenuItem Header="{DynamicResource Str_SaveWorkspace}" Command="{Binding Cmd_SaveWorkspace}"/>
<Separator/> <Separator/>
<MenuItem Header="{DynamicResource Str_Preference}"/> <MenuItem Header="{DynamicResource Str_PreferenceWithDots}" Command="{Binding PreferenceViewModel.Cmd_ShowPreferenceDialog}"/>
<!--<MenuItem Header="{DynamicResource Str_Exit}" InputGestureText="Alt+F4"/>--> <!--<MenuItem Header="{DynamicResource Str_Exit}" InputGestureText="Alt+F4"/>-->
</MenuItem> </MenuItem>
<!--<MenuItem Header="{DynamicResource Str_Tool}"/>--> <!--<MenuItem Header="{DynamicResource Str_Tool}"/>-->
@@ -244,11 +245,11 @@
</i:Interaction.Triggers> </i:Interaction.Triggers>
<ListView.InputBindings> <ListView.InputBindings>
<KeyBinding Gesture="Ctrl+O" Command="{Binding Cmd_AddSpineObject}"/>
<KeyBinding Gesture="Delete" Command="{Binding Cmd_RemoveSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/> <KeyBinding Gesture="Delete" Command="{Binding Cmd_RemoveSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
<KeyBinding Gesture="Ctrl+V" Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
<KeyBinding Gesture="Ctrl+R" Command="{Binding Cmd_ReloadSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
<KeyBinding Gesture="Alt+W" Command="{Binding Cmd_MoveUpSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/> <KeyBinding Gesture="Alt+W" Command="{Binding Cmd_MoveUpSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
<KeyBinding Gesture="Alt+S" Command="{Binding Cmd_MoveDownSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/> <KeyBinding Gesture="Alt+S" Command="{Binding Cmd_MoveDownSpineObject}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
<KeyBinding Gesture="Ctrl+V" Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding Cmd_CopySpineObjectConfig}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/> <KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding Cmd_CopySpineObjectConfig}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
<KeyBinding Gesture="Ctrl+Shift+V" Command="{Binding Cmd_ApplySpineObjectConfig}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/> <KeyBinding Gesture="Ctrl+Shift+V" Command="{Binding Cmd_ApplySpineObjectConfig}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
</ListView.InputBindings> </ListView.InputBindings>
@@ -256,12 +257,18 @@
<ListView.ContextMenu> <ListView.ContextMenu>
<ContextMenu> <ContextMenu>
<MenuItem Header="{DynamicResource Str_AddSpineObject}" <MenuItem Header="{DynamicResource Str_AddSpineObject}"
InputGestureText="Ctrl+O"
Command="{Binding Cmd_AddSpineObject}"/> Command="{Binding Cmd_AddSpineObject}"/>
<MenuItem Header="{DynamicResource Str_RemoveSpineObject}" <MenuItem Header="{DynamicResource Str_RemoveSpineObject}"
InputGestureText="Delete" InputGestureText="Delete"
Command="{Binding Cmd_RemoveSpineObject}" Command="{Binding Cmd_RemoveSpineObject}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/> CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_AddSpineObjectFromClipboard}"
InputGestureText="Ctrl+V"
Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
<MenuItem Header="{DynamicResource Str_Reload}"
InputGestureText="Ctrl+R"
Command="{Binding Cmd_ReloadSpineObject}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/> <Separator/>
<MenuItem Header="{DynamicResource Str_MoveUpSpineObject}" <MenuItem Header="{DynamicResource Str_MoveUpSpineObject}"
InputGestureText="Alt+W" InputGestureText="Alt+W"
@@ -272,10 +279,6 @@
Command="{Binding Cmd_MoveDownSpineObject}" Command="{Binding Cmd_MoveDownSpineObject}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/> CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/> <Separator/>
<MenuItem Header="{DynamicResource Str_AddSpineObjectFromClipboard}"
InputGestureText="Ctrl+V"
Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
<Separator/>
<MenuItem Header="{DynamicResource Str_CopySpineObjectConfig}" <MenuItem Header="{DynamicResource Str_CopySpineObjectConfig}"
InputGestureText="Ctrl+Shift+C" InputGestureText="Ctrl+Shift+C"
Command="{Binding Cmd_CopySpineObjectConfig}" Command="{Binding Cmd_CopySpineObjectConfig}"
@@ -284,9 +287,12 @@
InputGestureText="Ctrl+Shift+V" InputGestureText="Ctrl+Shift+V"
Command="{Binding Cmd_ApplySpineObjectConfig}" Command="{Binding Cmd_ApplySpineObjectConfig}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/> CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/> <MenuItem Header="{DynamicResource Str_ApplySpineObjectConfigFromFile}"
<MenuItem Header="{DynamicResource Str_ApplySpineObjectConfigFromFile}"/> Command="{Binding Cmd_ApplySpineObjectConfigFromFile}"
<MenuItem Header="{DynamicResource Str_SaveSpineObjectConfigToFile}"/> CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_SaveSpineObjectConfigToFile}"
Command="{Binding Cmd_SaveSpineObjectConfigToFile}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/> <Separator/>
<MenuItem Header="{DynamicResource Str_Export}"> <MenuItem Header="{DynamicResource Str_Export}">
<MenuItem Header="{DynamicResource Str_ExportFrame}" <MenuItem Header="{DynamicResource Str_ExportFrame}"
@@ -687,7 +693,6 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- 水平分辨率 --> <!-- 水平分辨率 -->
@@ -726,17 +731,13 @@
<Label Grid.Row="8" Grid.Column="0" Content="{DynamicResource Str_MaxFps}"/> <Label Grid.Row="8" Grid.Column="0" Content="{DynamicResource Str_MaxFps}"/>
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding MaxFps}"/> <TextBox Grid.Row="8" Grid.Column="1" Text="{Binding MaxFps}"/>
<!-- 仅渲染选中 -->
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_RenderSelectedOnly}"/>
<ToggleButton Grid.Row="9" Grid.Column="1" IsChecked="{Binding RenderSelectedOnly}"/>
<!-- 显示坐标轴 --> <!-- 显示坐标轴 -->
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_ShowAxis}"/> <Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_ShowAxis}"/>
<ToggleButton Grid.Row="10" Grid.Column="1" IsChecked="{Binding ShowAxis}"/> <ToggleButton Grid.Row="9" Grid.Column="1" IsChecked="{Binding ShowAxis}"/>
<!-- 背景颜色 --> <!-- 背景颜色 -->
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/> <Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/> <TextBox Grid.Row="10" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
<!-- 背景图案 --> <!-- 背景图案 -->
<!-- 背景图案模式 --> <!-- 背景图案模式 -->

View File

@@ -5,7 +5,7 @@ using NLog.Targets;
using Spine; using Spine;
using SpineViewer.Natives; using SpineViewer.Natives;
using SpineViewer.Resources; using SpineViewer.Resources;
using SpineViewer.ViewModels; using SpineViewer.ViewModels.MainWindow;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
@@ -61,6 +61,9 @@ public partial class MainWindow : Window
vm.FlipY = true; vm.FlipY = true;
vm.MaxFps = 30; vm.MaxFps = 30;
vm.StartRender(); vm.StartRender();
// 加载首选项
_vm.PreferenceViewModel.LoadPreference();
} }
private void MainWindow_Closed(object? sender, EventArgs e) private void MainWindow_Closed(object? sender, EventArgs e)

View File

@@ -1,12 +0,0 @@
<Window x:Class="SpineViewer.Views.MessageBoxWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SpineViewer.Views"
mc:Ignorable="d"
Title="MessageBoxWindow" Height="450" Width="800">
<Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,162 @@
<Window x:Class="SpineViewer.Views.PreferenceDialog"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SpineViewer.Views"
xmlns:models="clr-namespace:SpineViewer.Models"
xmlns:vm="clr-namespace:SpineViewer.ViewModels.MainWindow"
d:DataContext="{d:DesignInstance Type=models:PreferenceModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_Preference}"
Height="650"
Width="600"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner">
<DockPanel>
<Border DockPanel.Dock="Bottom">
<WrapPanel HorizontalAlignment="Center">
<WrapPanel.Resources>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource ButtonDefault}">
<Setter Property="Margin" Value="5"/>
<Setter Property="Width" Value="100"/>
</Style>
</WrapPanel.Resources>
<Button Content="{DynamicResource Str_OK}" Click="ButtonOK_Click"/>
<Button Content="{DynamicResource Str_Cancel}" Click="ButtonCancel_Click"/>
</WrapPanel>
</Border>
<Border>
<Border.Resources>
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource TextBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
<Setter Property="BorderBrush" Value="LightGray"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
<Setter Property="Margin" Value="0 5 0 10"/>
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Grid.IsSharedSizeScope="True">
<GroupBox Header="{DynamicResource Str_TextureLoadPreference}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" IsChecked="{Binding ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ForceNearest}"/>
<ToggleButton Grid.Row="1" Grid.Column="1" IsChecked="{Binding ForceNearest}"/>
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
</Grid>
</GroupBox>
<GroupBox Header="{DynamicResource Str_SpineLoadPreference}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_IsShown}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" IsChecked="{Binding IsShown}"/>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_UsePma}"/>
<ToggleButton Grid.Row="1" Grid.Column="1" IsChecked="{Binding UsePma}"/>
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_DebugTexture}"/>
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding DebugTexture}"/>
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_DebugBounds}"/>
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding DebugBounds}"/>
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_DebugBones}"/>
<ToggleButton Grid.Row="4" Grid.Column="1" IsChecked="{Binding DebugBones}"/>
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_DebugRegions}"/>
<ToggleButton Grid.Row="5" Grid.Column="1" IsChecked="{Binding DebugRegions}"/>
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_DebugMeshHulls}"/>
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding DebugMeshHulls}"/>
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_DebugMeshes}"/>
<ToggleButton Grid.Row="7" Grid.Column="1" IsChecked="{Binding DebugMeshes}"/>
<Label Grid.Row="8" Grid.Column="0" Content="{DynamicResource Str_DebugClippings}"/>
<ToggleButton Grid.Row="8" Grid.Column="1" IsChecked="{Binding DebugClippings}"/>
<!--<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_DebugBoundingBoxes}"/>
<ToggleButton Grid.Row="9" Grid.Column="1" IsChecked="{Binding DebugBoundingBoxes}"/>
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_DebugPaths}"/>
<ToggleButton Grid.Row="10" Grid.Column="1" IsChecked="{Binding DebugPaths}"/>
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_DebugPoints}"/>
<ToggleButton Grid.Row="11" Grid.Column="1" IsChecked="{Binding DebugPoints}"/>-->
</Grid>
</GroupBox>
<GroupBox Header="{DynamicResource Str_AppPreference}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_RenderSelectedOnly}"/>
<ToggleButton Grid.Row="0" Grid.Column="1" IsChecked="{Binding RenderSelectedOnly}"/>
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Language}"/>
<ComboBox Grid.Row="1" Grid.Column="1"
SelectedItem="{Binding AppLanguage}"
ItemsSource="{x:Static vm:PreferenceViewModel.AppLanguageOptions}"/>
</Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
</Border>
</DockPanel>
</Window>

View File

@@ -1,4 +1,6 @@
using System; using SpineViewer.Services;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -15,13 +17,23 @@ using System.Windows.Shapes;
namespace SpineViewer.Views namespace SpineViewer.Views
{ {
/// <summary> /// <summary>
/// MessageBoxWindow.xaml 的交互逻辑 /// PreferenceDialog.xaml 的交互逻辑
/// </summary> /// </summary>
public partial class MessageBoxWindow : Window public partial class PreferenceDialog : Window
{ {
public MessageBoxWindow() public PreferenceDialog()
{ {
InitializeComponent(); InitializeComponent();
} }
private void ButtonOK_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
private void ButtonCancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
} }
} }

79
SpineViewer/app.manifest Normal file
View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC 清单选项
如果想要更改 Windows 用户帐户控制级别,请使用
以下节点之一替换 requestedExecutionLevel 节点。
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
元素。
-->
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
Windows 版本的列表。取消评论适当的元素,
Windows 将自动选择最兼容的环境。 -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->
</application>
</compatibility>
<!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI无需
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
<!--
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
-->
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 177 KiB