54
.github/workflows/dotnet-desktop.yml
vendored
54
.github/workflows/dotnet-desktop.yml
vendored
@@ -1,45 +1,79 @@
|
||||
name: Build & Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
if: ${{ github.event.pull_request.merged == true }}
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
PROJECT_NAME: SpineViewer
|
||||
VERSION: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v3
|
||||
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
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet publish ${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj -c Release -r win-x64 --sc false -o publish/${{ env.PROJECT_NAME }}-${{ env.VERSION }}
|
||||
dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc false -o "publish\$env:PROJECT_NAME-$env:VERSION"
|
||||
|
||||
- name: Publish SelfContained version
|
||||
shell: pwsh
|
||||
run: |
|
||||
dotnet publish ${{ env.PROJECT_NAME }}/${{ env.PROJECT_NAME }}.csproj -c Release -r win-x64 --sc true -o publish/${{ env.PROJECT_NAME }}-${{ env.VERSION }}-SelfContained
|
||||
dotnet publish "$env: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
|
||||
run: mkdir release
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Path release -Force | Out-Null
|
||||
|
||||
- name: Compress FrameworkDependent version
|
||||
shell: pwsh
|
||||
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
|
||||
shell: pwsh
|
||||
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
|
||||
id: create_release
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.15.0</Version>
|
||||
<Version>0.15.1</Version>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
160
README.en.md
160
README.en.md
@@ -1,111 +1,133 @@
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||
[中文](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).
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Supports multiple Spine file versions
|
||||
- Drag & drop or copy/paste to open files in batch
|
||||
- List-based skeleton view with render layer management
|
||||
- Multi-select list to batch-adjust skeleton parameters
|
||||
- Multi-track animation support
|
||||
- Skin / custom slot attachment configuration
|
||||
- Debug rendering mode
|
||||
- Fullscreen preview
|
||||
- Export to single-frame image, animated GIF/WebP/AVIF, video formats
|
||||
- Batch export at multiple resolutions
|
||||
- Custom FFmpeg export parameters
|
||||
- …and more
|
||||
* Supports multiple versions of Spine files.
|
||||
* Batch open files via drag-and-drop or copy-paste.
|
||||
* Batch preview functionality.
|
||||
* List-based multi-skeleton viewing and render order management.
|
||||
* Batch adjustment of skeleton parameters using multi-selection.
|
||||
* Multi-track animation settings.
|
||||
* Skin and custom slot attachment settings.
|
||||
* Debug rendering support.
|
||||
* Fullscreen preview mode.
|
||||
* Export to single frame/image sequence/animated GIF/video formats.
|
||||
* Automatic resolution batch export.
|
||||
* FFmpeg custom export support.
|
||||
* Program parameter saving.
|
||||
* ...
|
||||
|
||||
### Spine Version Support
|
||||
### Supported Spine Versions
|
||||
|
||||
| Version | View & Export | Format Conversion | Version Conversion |
|
||||
| :------: | :-----------: | :---------------: | :----------------: |
|
||||
| `2.1.x` | :white_check_mark: | | |
|
||||
| `3.6.x` | :white_check_mark: | | |
|
||||
| `3.7.x` | :white_check_mark: | | |
|
||||
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
|
||||
| `4.0.x` | :white_check_mark: | | |
|
||||
| `4.1.x` | :white_check_mark: | | |
|
||||
| `4.2.x` | :white_check_mark: | :white_check_mark: | |
|
||||
| `4.3.x` | | | |
|
||||
| Version | View & Export |
|
||||
| :-----: | :------------------: |
|
||||
| `2.1.x` | :white\_check\_mark: |
|
||||
| `3.6.x` | :white\_check\_mark: |
|
||||
| `3.7.x` | :white\_check\_mark: |
|
||||
| `3.8.x` | :white\_check\_mark: |
|
||||
| `4.0.x` | :white\_check\_mark: |
|
||||
| `4.1.x` | :white\_check\_mark: |
|
||||
| `4.2.x` | :white\_check\_mark: |
|
||||
| `4.3.x` | |
|
||||
|
||||
More versions coming soon 🚀🚀🚀
|
||||
More versions under development \:rocket: \:rocket: \:rocket:
|
||||
|
||||
### Supported Export Formats
|
||||
|
||||
| Export Format | Use Case |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Single Frame | Generate high‑resolution still images; pick any frame manually. |
|
||||
| Frame Sequence (PNG) | Lossless PNG sequences with alpha channel preserved. |
|
||||
| GIF / WebP / AVIF | Perfect for quick animated previews. |
|
||||
| MP4 | The most widely compatible video format. |
|
||||
| WebM | Browser‑friendly streaming with optional transparency. |
|
||||
| MKV / MOV | For those who like to tinker. |
|
||||
| Custom FFmpeg Command | Use any FFmpeg arguments for complex, tailored export workflows. |
|
||||
| Format | Use Case |
|
||||
| -------------- | ----------------------------------------------------------------------------- |
|
||||
| Single Frame | Generate high-resolution images of models; manually adjust the desired frame. |
|
||||
| Frame Sequence | Supports PNG format with transparency and lossless compression. |
|
||||
| GIF/Video | Export preview animations or common video formats. |
|
||||
| Custom Export | Supports arbitrary FFmpeg parameters for custom, complex export needs. |
|
||||
|
||||
## Installation
|
||||
|
||||
1. Go to the [Releases](https://github.com/ww-rm/SpineViewer/releases) page and download the ZIP.
|
||||
2. Make sure you have the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) installed.
|
||||
3. Alternatively, download the `SelfContained` ZIP, which runs standalone without any .NET prerequisites.
|
||||
4. To export GIF or other video formats, install the `ffmpeg` CLI and add it to your PATH.
|
||||
- Windows builds: see the [FFmpeg download page](https://ffmpeg.org/download.html#build-windows)
|
||||
- Direct download: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z)
|
||||
Download the compressed package from the [Release](https://github.com/ww-rm/SpineViewer/releases) page.
|
||||
|
||||
The software requires the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) to run.
|
||||
|
||||
Alternatively, download the package with the `SelfContained` suffix for standalone execution.
|
||||
|
||||
For exporting GIF/MP4 and other animation/video formats, FFmpeg must be installed and added to the system environment variables. Visit the [FFmpeg Windows download page](https://ffmpeg.org/download.html#build-windows) or download the latest version directly: [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
|
||||
|
||||
## 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.
|
||||
- Use **File > Open** to batch‑open multiple skeleton files.
|
||||
- Use **File > Open Single Model** to open one at a time.
|
||||
### Basic Overview
|
||||
|
||||
### Adjusting Content
|
||||
The program is organized into a left-right layout:
|
||||
|
||||
- Right‑click menu and keyboard shortcuts are available in the model list. You can multi‑select to adjust parameters in batch.
|
||||
- In the preview pane, you can also use mouse controls:
|
||||
- **Left‑click & drag** to move a model; hold **Ctrl** to multi‑select (synced with the list).
|
||||
- **Right‑click & 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.
|
||||
* **Left Panel:** Functionality panel.
|
||||
* **Right Panel:** Preview display.
|
||||
|
||||
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 real‑time 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.
|
||||
- **Output Folder**: if unspecified, exports go into each model’s 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 content’s actual bounds; for animations, matches the full animation area.
|
||||
Drag-and-drop or paste skeleton files/directories into the Model panel.
|
||||
|
||||
## 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).
|
||||
Encounter a bug or have a feature request? Open an [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||
### Content Adjustment
|
||||
|
||||
The Model panel supports right-click menus, some shortcuts, and batch adjustments of model parameters through multi-selection.
|
||||
|
||||
For preview display adjustments:
|
||||
|
||||
* **Left-click:** Select and drag models. Hold `Ctrl` for multi-selection, synchronized with the left-side list.
|
||||
* **Right-click:** Drag the entire display.
|
||||
* **Scroll wheel:** Zoom in/out. Hold `Ctrl` to scale selected models.
|
||||
* **Render selected-only mode:** In this mode, the preview only shows selected models, and selection status can only be changed via the left-side list.
|
||||
|
||||
The buttons below the preview display allow time adjustments, serving as a simple playback control.
|
||||
|
||||
### Content Export
|
||||
|
||||
Export follows the **WYSIWYG (What You See Is What You Get)** principle, meaning the preview display reflects the exported output.
|
||||
|
||||
Use the right-click menu in the Model panel to export selected items.
|
||||
|
||||
Key export parameters include:
|
||||
|
||||
* **Output folder:** Optional. When not specified, output is saved to the respective model folder; otherwise, all output is saved to the provided folder.
|
||||
* **Export single:** By default, each model is exported independently. Selecting "Export single" renders all selected models in a single frame, producing a unified output.
|
||||
* **Auto resolution:** Ignores the preview resolution and viewport parameters, exporting output at the actual size of the content. For animations/videos, the output matches the size required for full visibility.
|
||||
|
||||
### More Information
|
||||
|
||||
For detailed usage and documentation, see the [Wiki](https://github.com/ww-rm/SpineViewer/wiki). For usage questions or bug reports, submit an [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
|
||||
- [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||
* [spine-runtimes](https://github.com/EsotericSoftware/spine-runtimes)
|
||||
* [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||
* [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||
* [HandyControl](https://github.com/HandyOrg/HandyControl)
|
||||
* [NLog](https://github.com/NLog/NLog)
|
||||
* [SkiaSharp](https://github.com/mono/SkiaSharp)
|
||||
|
||||
---
|
||||
|
||||
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! :)*
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
|
||||
61
README.md
61
README.md
@@ -6,7 +6,7 @@
|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
*所见即所得* 的 Spine 文件查看&导出程序.
|
||||
一个简单好用的 Spine 文件查看&导出程序, 支持中/英/日多语言界面.
|
||||
|
||||

|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
- 支持多版本 spine 文件
|
||||
- 支持拖拽/复制粘贴批量打开文件
|
||||
- 支持批量预览
|
||||
- 支持列表式多骨骼查看和渲染层级管理
|
||||
- 支持列表多选批量设置骨骼参数
|
||||
- 支持多轨道动画设置
|
||||
@@ -23,20 +24,21 @@
|
||||
- 支持单帧/动图/视频文件导出
|
||||
- 支持自动分辨率批量导出
|
||||
- 支持 FFmpeg 自定义导出
|
||||
- 支持程序参数保存
|
||||
- ...
|
||||
|
||||
### Spine 版本支持
|
||||
|
||||
| 版本 | 查看&导出 | 格式转换 | 版本转换 |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| `2.1.x` | :white_check_mark: | | |
|
||||
| `3.6.x` | :white_check_mark: | | |
|
||||
| `3.7.x` | :white_check_mark: | | |
|
||||
| `3.8.x` | :white_check_mark: | :white_check_mark: | |
|
||||
| `4.0.x` | :white_check_mark: | | |
|
||||
| `4.1.x` | :white_check_mark: | | |
|
||||
| `4.2.x` | :white_check_mark: | :white_check_mark: | |
|
||||
| `4.3.x` | | | |
|
||||
| 版本 | 查看&导出 |
|
||||
| :---: | :---: |
|
||||
| `2.1.x` | :white_check_mark: |
|
||||
| `3.6.x` | :white_check_mark: |
|
||||
| `3.7.x` | :white_check_mark: |
|
||||
| `3.8.x` | :white_check_mark: |
|
||||
| `4.0.x` | :white_check_mark: |
|
||||
| `4.1.x` | :white_check_mark: |
|
||||
| `4.2.x` | :white_check_mark: |
|
||||
| `4.3.x` | |
|
||||
|
||||
更多版本正在施工 :rocket: :rocket: :rocket:
|
||||
|
||||
@@ -46,10 +48,7 @@
|
||||
| --- | --- |
|
||||
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
|
||||
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
|
||||
| GIF/WebP/AVIF | 适合生成预览动图. |
|
||||
| MP4 | 最常见的视频格式, 兼容性最好. |
|
||||
| WebM | 适合浏览器在线播放格式, 支持透明背景. |
|
||||
| MKV/MOV | 适合折腾. |
|
||||
| 动图/视频 | 可以生成预览动图或者常见格式视频. |
|
||||
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
|
||||
|
||||
## 安装
|
||||
@@ -60,21 +59,35 @@
|
||||
|
||||
也可以下载带有 `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)
|
||||
- [SFML.Net](https://github.com/SFML/SFML.Net)
|
||||
- [FFMpegCore](https://github.com/rosenbjerg/FFMpegCore)
|
||||
- [HandyControl](https://github.com/HandyOrg/HandyControl)
|
||||
- [NLog](https://github.com/NLog/NLog)
|
||||
- [SkiaSharp](https://github.com/mono/SkiaSharp)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.15.0</Version>
|
||||
<Version>0.15.1</Version>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V21
|
||||
private readonly ImmutableArray<IAnimation> _animations;
|
||||
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
|
||||
try { _atlas = new Atlas(atlasPath, _textureLoader); }
|
||||
try { _atlas = new Atlas(atlasPath, textureLoader); }
|
||||
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
|
||||
|
||||
try
|
||||
{
|
||||
if (Utf8Validator.IsUtf8(skelPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
else
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
|
||||
@@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V36
|
||||
private readonly ImmutableArray<IAnimation> _animations;
|
||||
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
|
||||
try { _atlas = new Atlas(atlasPath, _textureLoader); }
|
||||
try { _atlas = new Atlas(atlasPath, textureLoader); }
|
||||
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
|
||||
|
||||
try
|
||||
{
|
||||
if (Utf8Validator.IsUtf8(skelPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
else
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
|
||||
@@ -26,19 +26,38 @@ namespace Spine.Implementations.SpineWrappers.V37
|
||||
private readonly ImmutableArray<IAnimation> _animations;
|
||||
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
|
||||
try { _atlas = new Atlas(atlasPath, _textureLoader); }
|
||||
try { _atlas = new Atlas(atlasPath, textureLoader); }
|
||||
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
|
||||
|
||||
try
|
||||
{
|
||||
if (Utf8Validator.IsUtf8(skelPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
else
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
|
||||
@@ -27,19 +27,38 @@ namespace Spine.Implementations.SpineWrappers.V38
|
||||
private readonly ImmutableArray<IAnimation> _animations;
|
||||
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
|
||||
try { _atlas = new Atlas(atlasPath, _textureLoader); }
|
||||
try { _atlas = new Atlas(atlasPath, textureLoader); }
|
||||
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
|
||||
|
||||
try
|
||||
{
|
||||
if (Utf8Validator.IsUtf8(skelPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
else
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
|
||||
@@ -26,20 +26,39 @@ namespace Spine.Implementations.SpineWrappers.V40
|
||||
private readonly ImmutableArray<IAnimation> _animations;
|
||||
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
|
||||
try { _atlas = new Atlas(atlasPath, _textureLoader); }
|
||||
try { _atlas = new Atlas(atlasPath, textureLoader); }
|
||||
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
|
||||
|
||||
// 加载 skel
|
||||
try
|
||||
{
|
||||
if (Utf8Validator.IsUtf8(skelPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
else
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
|
||||
@@ -26,20 +26,39 @@ namespace Spine.Implementations.SpineWrappers.V41
|
||||
private readonly ImmutableArray<IAnimation> _animations;
|
||||
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
|
||||
try { _atlas = new Atlas(atlasPath, _textureLoader); }
|
||||
try { _atlas = new Atlas(atlasPath, textureLoader); }
|
||||
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
|
||||
|
||||
// 加载 skel
|
||||
try
|
||||
{
|
||||
if (Utf8Validator.IsUtf8(skelPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
else
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
|
||||
@@ -26,20 +26,39 @@ namespace Spine.Implementations.SpineWrappers.V42
|
||||
private readonly ImmutableArray<IAnimation> _animations;
|
||||
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
|
||||
try { _atlas = new Atlas(atlasPath, _textureLoader); }
|
||||
try { _atlas = new Atlas(atlasPath, textureLoader); }
|
||||
catch (Exception ex) { throw new InvalidDataException($"Failed to load atlas '{atlasPath}'", ex); }
|
||||
|
||||
// 加载 skel
|
||||
try
|
||||
{
|
||||
if (Utf8Validator.IsUtf8(skelPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
else
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.15.0</Version>
|
||||
<Version>0.15.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace Spine
|
||||
{
|
||||
[".skel"] = ".atlas",
|
||||
[".json"] = ".atlas",
|
||||
[".skel.bytes"] = ".atlas.bytes",
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
/// <summary>
|
||||
@@ -45,10 +44,12 @@ namespace Spine
|
||||
/// <param name="skelPath">skel 文件路径</param>
|
||||
/// <param name="atlasPath">atlas 文件路径, 为空时会根据 <paramref name="skelPath"/> 进行自动检测</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 (!File.Exists(skelPath)) throw new FileNotFoundException($"{nameof(skelPath)} not found", skelPath);
|
||||
textureLoader ??= TextureLoader.DefaultLoader;
|
||||
|
||||
SkelPath = Path.GetFullPath(skelPath);
|
||||
AssetsDir = Directory.GetParent(skelPath).FullName;
|
||||
Name = Path.GetFileNameWithoutExtension(skelPath);
|
||||
@@ -91,7 +92,7 @@ namespace Spine
|
||||
{
|
||||
try
|
||||
{
|
||||
_data = SpineObjectData.New(v, skelPath, atlasPath);
|
||||
_data = SpineObjectData.New(v, skelPath, atlasPath, textureLoader);
|
||||
Version = v;
|
||||
break;
|
||||
}
|
||||
@@ -109,7 +110,7 @@ namespace Spine
|
||||
{
|
||||
// 根据版本实例化对象
|
||||
Version = version;
|
||||
_data = SpineObjectData.New(Version, skelPath, atlasPath);
|
||||
_data = SpineObjectData.New(Version, skelPath, atlasPath, textureLoader);
|
||||
}
|
||||
|
||||
// 创建状态实例
|
||||
@@ -177,7 +178,6 @@ namespace Spine
|
||||
// 拷贝调试属性
|
||||
EnableDebug = other.EnableDebug;
|
||||
DebugTexture = other.DebugTexture;
|
||||
DebugNonTexture = other.DebugNonTexture;
|
||||
DebugBounds = other.DebugBounds;
|
||||
DebugBones = other.DebugBones;
|
||||
DebugRegions = other.DebugRegions;
|
||||
@@ -236,7 +236,7 @@ namespace Spine
|
||||
/// <summary>
|
||||
/// 是否使用预乘 Alpha
|
||||
/// </summary>
|
||||
public bool UsePma { get; set; } = false;
|
||||
public bool UsePma { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 物理约束更新方式
|
||||
@@ -246,62 +246,57 @@ namespace Spine
|
||||
/// <summary>
|
||||
/// 启用渲染调试, 将会使所有 <c>DebugXXX</c> 属性生效
|
||||
/// </summary>
|
||||
public bool EnableDebug { get; set; } = false;
|
||||
public bool EnableDebug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示纹理
|
||||
/// </summary>
|
||||
public bool DebugTexture { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示非纹理内容, 一个总开关
|
||||
/// </summary>
|
||||
public bool DebugNonTexture { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 显示包围盒
|
||||
/// </summary>
|
||||
public bool DebugBounds { get; set; } = true;
|
||||
public bool DebugBounds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示骨骼
|
||||
/// </summary>
|
||||
public bool DebugBones { get; set; } = false;
|
||||
public bool DebugBones { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示区域附件边框
|
||||
/// </summary>
|
||||
public bool DebugRegions { get; set; } = false;
|
||||
public bool DebugRegions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示网格附件边框线
|
||||
/// </summary>
|
||||
public bool DebugMeshHulls { get; set; } = false;
|
||||
public bool DebugMeshHulls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示网格附件网格线
|
||||
/// </summary>
|
||||
public bool DebugMeshes { get; set; } = false;
|
||||
public bool DebugMeshes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示碰撞盒附件边框线
|
||||
/// </summary>
|
||||
public bool DebugBoundingBoxes { get; set; } = false;
|
||||
public bool DebugBoundingBoxes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示路径附件网格线
|
||||
/// </summary>
|
||||
public bool DebugPaths { get; set; } = false;
|
||||
public bool DebugPaths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示点附件
|
||||
/// </summary>
|
||||
public bool DebugPoints { get; set; } = false;
|
||||
public bool DebugPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示剪裁附件网格线
|
||||
/// </summary>
|
||||
public bool DebugClippings { get; set; } = false;
|
||||
public bool DebugClippings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个插槽上的附件名, 插槽不存在或者无附件均返回 null
|
||||
@@ -864,7 +859,7 @@ namespace Spine
|
||||
else
|
||||
{
|
||||
if (DebugTexture) DrawTexture(target, states);
|
||||
if (DebugNonTexture) DrawNonTexture(target);
|
||||
DrawNonTexture(target);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,18 +20,13 @@ namespace Spine.SpineWrappers
|
||||
/// <summary>
|
||||
/// 构建版本对象
|
||||
/// </summary>
|
||||
public static SpineObjectData New(SpineVersion version, string skelPath, string atlasPath) => CreateInstance(version.Tag, skelPath, atlasPath);
|
||||
|
||||
/// <summary>
|
||||
/// 纹理加载器, 可以设置一些预置参数
|
||||
/// </summary>
|
||||
public static TextureLoader TextureLoader => _textureLoader;
|
||||
protected static readonly TextureLoader _textureLoader = new();
|
||||
public static SpineObjectData New(SpineVersion version, string skelPath, string atlasPath, TextureLoader textureLoader)
|
||||
=> CreateInstance(version.Tag, skelPath, atlasPath, textureLoader);
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数, 继承的子类应当实现一个相同签名的构造函数
|
||||
/// </summary>
|
||||
public SpineObjectData(string skelPath, string atlasPath) { }
|
||||
public SpineObjectData(string skelPath, string atlasPath, TextureLoader textureLoader) { }
|
||||
|
||||
public abstract string SkeletonVersion { get; }
|
||||
|
||||
|
||||
@@ -19,23 +19,62 @@ namespace Spine.SpineWrappers
|
||||
SpineRuntime42.TextureLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 强制启用 Smooth
|
||||
/// 默认的全局纹理加载器
|
||||
/// </summary>
|
||||
public bool ForceSmooth { get; set; } = false;
|
||||
public static TextureLoader DefaultLoader { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 强制启用 Repeated
|
||||
/// 在读取纹理时强制进行通道预乘操作
|
||||
/// </summary>
|
||||
public bool ForceRepeated { get; set; } = false;
|
||||
public bool ForcePremul { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 强制使用 Nearest
|
||||
/// </summary>
|
||||
public bool ForceNearest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 强制启用 Mipmap
|
||||
/// </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)
|
||||
{
|
||||
texture.Smooth = true;
|
||||
@@ -61,16 +100,16 @@ namespace Spine.SpineWrappers
|
||||
break;
|
||||
}
|
||||
|
||||
if (ForceSmooth) texture.Smooth = true;
|
||||
if (ForceRepeated) texture.Repeated = true;
|
||||
if (ForceNearest) texture.Smooth = false;
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
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)
|
||||
{
|
||||
texture.Smooth = true;
|
||||
@@ -96,16 +135,16 @@ namespace Spine.SpineWrappers
|
||||
break;
|
||||
}
|
||||
|
||||
if (ForceSmooth) texture.Smooth = true;
|
||||
if (ForceRepeated) texture.Repeated = true;
|
||||
if (ForceNearest) texture.Smooth = false;
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
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)
|
||||
{
|
||||
texture.Smooth = true;
|
||||
@@ -131,16 +170,16 @@ namespace Spine.SpineWrappers
|
||||
break;
|
||||
}
|
||||
|
||||
if (ForceSmooth) texture.Smooth = true;
|
||||
if (ForceRepeated) texture.Repeated = true;
|
||||
if (ForceNearest) texture.Smooth = false;
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
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)
|
||||
{
|
||||
texture.Smooth = true;
|
||||
@@ -166,16 +205,20 @@ namespace Spine.SpineWrappers
|
||||
break;
|
||||
}
|
||||
|
||||
if (ForceSmooth) texture.Smooth = true;
|
||||
if (ForceRepeated) texture.Repeated = true;
|
||||
if (ForceNearest) texture.Smooth = false;
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
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)
|
||||
{
|
||||
texture.Smooth = true;
|
||||
@@ -201,16 +244,16 @@ namespace Spine.SpineWrappers
|
||||
break;
|
||||
}
|
||||
|
||||
if (ForceSmooth) texture.Smooth = true;
|
||||
if (ForceRepeated) texture.Repeated = true;
|
||||
if (ForceNearest) texture.Smooth = false;
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
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)
|
||||
{
|
||||
texture.Smooth = true;
|
||||
@@ -236,16 +279,16 @@ namespace Spine.SpineWrappers
|
||||
break;
|
||||
}
|
||||
|
||||
if (ForceSmooth) texture.Smooth = true;
|
||||
if (ForceRepeated) texture.Repeated = true;
|
||||
if (ForceNearest) texture.Smooth = false;
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
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)
|
||||
{
|
||||
texture.Smooth = true;
|
||||
@@ -271,18 +314,13 @@ namespace Spine.SpineWrappers
|
||||
break;
|
||||
}
|
||||
|
||||
if (ForceSmooth) texture.Smooth = true;
|
||||
if (ForceRepeated) texture.Repeated = true;
|
||||
if (ForceNearest) texture.Smooth = false;
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Geometries.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Strings/zh-cn.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Strings/zh.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource ToggleButtonSwitch}">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using NLog;
|
||||
using SpineViewer.Views;
|
||||
using System.Collections.Frozen;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
@@ -7,8 +8,8 @@ using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewer;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
@@ -66,18 +67,9 @@ public partial class App : Application
|
||||
var uiCulture = CultureInfo.CurrentUICulture.Name.ToLowerInvariant();
|
||||
_logger.Info("Current UI Culture: {0}", uiCulture);
|
||||
|
||||
if (uiCulture.StartsWith("zh"))
|
||||
{
|
||||
; // 默认就是中文, 无需操作
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (uiCulture.StartsWith("zh")) { } // 默认就是中文, 无需操作
|
||||
else if (uiCulture.StartsWith("ja")) Language = AppLanguage.JA;
|
||||
else Language = AppLanguage.EN;
|
||||
|
||||
Resources.MergedDictionaries.Add(dict);
|
||||
}
|
||||
@@ -88,5 +80,36 @@ public partial class App : Application
|
||||
_logger.Error("Dispatcher unhandled exception: {0}", e.Exception.Message);
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ namespace SpineViewer.Extensions
|
||||
public static Rect GetCurrentBounds(this SpineObject self)
|
||||
{
|
||||
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>
|
||||
|
||||
84
SpineViewer/Models/PreferenceModel.cs
Normal file
84
SpineViewer/Models/PreferenceModel.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -13,38 +13,6 @@ namespace SpineViewer.Models
|
||||
{
|
||||
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 string Physics { get; set; } = ISkeleton.Physics.Update.ToString();
|
||||
@@ -67,7 +35,7 @@ namespace SpineViewer.Models
|
||||
|
||||
public bool DebugTexture { get; set; } = true;
|
||||
|
||||
public bool DebugBounds { get; set; } = true;
|
||||
public bool DebugBounds { get; set; }
|
||||
|
||||
public bool DebugBones { get; set; }
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ namespace SpineViewer.Models
|
||||
/// </summary>
|
||||
public class SpineObjectModel : ObservableObject, SFML.Graphics.Drawable, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 一些加载默认选项
|
||||
/// </summary>
|
||||
public static SpineObjectLoadOptions LoadOptions => _loadOptions;
|
||||
private static readonly SpineObjectLoadOptions _loadOptions = new();
|
||||
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
@@ -40,9 +46,21 @@ namespace SpineViewer.Models
|
||||
/// <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();
|
||||
_slotAttachments = _spineObject.Data.SlotAttachments.ToFrozenDictionary(it => it.Key, it => it.Value.Keys);
|
||||
_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);
|
||||
}
|
||||
|
||||
/// <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<SlotAttachmentChangedEventArgs>? SlotAttachmentChanged;
|
||||
@@ -73,7 +104,7 @@ namespace SpineViewer.Models
|
||||
public bool 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;
|
||||
|
||||
@@ -82,18 +113,18 @@ namespace SpineViewer.Models
|
||||
get { lock (_lock) return _isShown; }
|
||||
set { lock (_lock) SetProperty(ref _isShown, value); }
|
||||
}
|
||||
private bool _isShown = true;
|
||||
private bool _isShown = _loadOptions.IsShown;
|
||||
|
||||
public bool 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
|
||||
{
|
||||
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>
|
||||
@@ -128,7 +159,7 @@ namespace SpineViewer.Models
|
||||
public bool FlipX
|
||||
{
|
||||
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>
|
||||
@@ -137,19 +168,19 @@ namespace SpineViewer.Models
|
||||
public bool FlipY
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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;
|
||||
@@ -242,67 +273,67 @@ namespace SpineViewer.Models
|
||||
public bool 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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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)
|
||||
@@ -326,10 +357,9 @@ namespace SpineViewer.Models
|
||||
lock (_lock) return _spineObject.GetCurrentBounds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出参数对象
|
||||
/// </summary>
|
||||
public SpineObjectConfigModel Dump()
|
||||
public SpineObjectConfigModel ObjectConfig
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
@@ -340,6 +370,7 @@ namespace SpineViewer.Models
|
||||
FlipY = _spineObject.Skeleton.ScaleY < 0,
|
||||
X = _spineObject.Skeleton.X,
|
||||
Y = _spineObject.Skeleton.Y,
|
||||
|
||||
UsePma = _spineObject.UsePma,
|
||||
Physics = _spineObject.Physics.ToString(),
|
||||
|
||||
@@ -365,39 +396,36 @@ namespace SpineViewer.Models
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从参数对象加载参数值
|
||||
/// </summary>
|
||||
public void Load(SpineObjectConfigModel config)
|
||||
set
|
||||
{
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_spineObject.Skeleton.ScaleX = config.Scale;
|
||||
_spineObject.Skeleton.ScaleY = config.Scale;
|
||||
_spineObject.Skeleton.ScaleX = value.Scale;
|
||||
_spineObject.Skeleton.ScaleY = value.Scale;
|
||||
OnPropertyChanged(nameof(Scale));
|
||||
SetProperty(_spineObject.Skeleton.ScaleX < 0, config.FlipX, _spineObject, (m, v) => m.Skeleton.ScaleX *= -1, nameof(FlipX));
|
||||
SetProperty(_spineObject.Skeleton.ScaleY < 0, config.FlipY, _spineObject, (m, v) => m.Skeleton.ScaleY *= -1, nameof(FlipY));
|
||||
SetProperty(_spineObject.Skeleton.X, config.X, _spineObject, (m, v) => m.Skeleton.X = v, nameof(X));
|
||||
SetProperty(_spineObject.Skeleton.Y, config.Y, _spineObject, (m, v) => m.Skeleton.Y = v, nameof(Y));
|
||||
SetProperty(_spineObject.UsePma, config.UsePma, _spineObject, (m, v) => m.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.Skeleton.ScaleX < 0, value.FlipX, v => _spineObject.Skeleton.ScaleX *= -1, nameof(FlipX));
|
||||
SetProperty(_spineObject.Skeleton.ScaleY < 0, value.FlipY, v => _spineObject.Skeleton.ScaleY *= -1, nameof(FlipY));
|
||||
SetProperty(_spineObject.Skeleton.X, value.X, v => _spineObject.Skeleton.X = v, nameof(X));
|
||||
SetProperty(_spineObject.Skeleton.Y, value.Y, v => _spineObject.Skeleton.Y = v, nameof(Y));
|
||||
SetProperty(_spineObject.UsePma, value.UsePma, v => _spineObject.UsePma = v, nameof(UsePma));
|
||||
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))
|
||||
SkinStatusChanged?.Invoke(this, new(name, false));
|
||||
foreach (var name in config.LoadedSkins)
|
||||
foreach (var name in value.LoadedSkins)
|
||||
if (_spineObject.SetSkinStatus(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))
|
||||
SlotAttachmentChanged?.Invoke(this, new(slotName, attachmentName));
|
||||
|
||||
// XXX: 处理空动画
|
||||
_spineObject.AnimationState.ClearTracks();
|
||||
int trackIndex = 0;
|
||||
foreach (var name in config.Animations)
|
||||
foreach (var name in value.Animations)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
_spineObject.AnimationState.SetAnimation(trackIndex, name, true);
|
||||
@@ -405,16 +433,31 @@ namespace SpineViewer.Models
|
||||
trackIndex++;
|
||||
}
|
||||
|
||||
SetProperty(_spineObject.DebugTexture, config.DebugTexture, _spineObject, (m, v) => m.DebugTexture = v, nameof(DebugTexture));
|
||||
SetProperty(_spineObject.DebugBounds, config.DebugBounds, _spineObject, (m, v) => m.DebugBounds = v, nameof(DebugBounds));
|
||||
SetProperty(_spineObject.DebugBones, config.DebugBones, _spineObject, (m, v) => m.DebugBones = v, nameof(DebugBones));
|
||||
SetProperty(_spineObject.DebugRegions, config.DebugRegions, _spineObject, (m, v) => m.DebugRegions = v, nameof(DebugRegions));
|
||||
SetProperty(_spineObject.DebugMeshHulls, config.DebugMeshHulls, _spineObject, (m, v) => m.DebugMeshHulls = v, nameof(DebugMeshHulls));
|
||||
SetProperty(_spineObject.DebugMeshes, config.DebugMeshes, _spineObject, (m, v) => m.DebugMeshes = v, nameof(DebugMeshes));
|
||||
SetProperty(_spineObject.DebugBoundingBoxes, config.DebugBoundingBoxes, _spineObject, (m, v) => m.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes));
|
||||
SetProperty(_spineObject.DebugPaths, config.DebugPaths, _spineObject, (m, v) => m.DebugPaths = v, nameof(DebugPaths));
|
||||
SetProperty(_spineObject.DebugPoints, config.DebugPoints, _spineObject, (m, v) => m.DebugPoints = v, nameof(DebugPoints));
|
||||
SetProperty(_spineObject.DebugClippings, config.DebugClippings, _spineObject, (m, v) => m.DebugClippings = v, nameof(DebugClippings));
|
||||
SetProperty(_spineObject.DebugTexture, value.DebugTexture, v => _spineObject.DebugTexture = v, nameof(DebugTexture));
|
||||
SetProperty(_spineObject.DebugBounds, value.DebugBounds, v => _spineObject.DebugBounds = v, nameof(DebugBounds));
|
||||
SetProperty(_spineObject.DebugBones, value.DebugBones, v => _spineObject.DebugBones = v, nameof(DebugBones));
|
||||
SetProperty(_spineObject.DebugRegions, value.DebugRegions, v => _spineObject.DebugRegions = v, nameof(DebugRegions));
|
||||
SetProperty(_spineObject.DebugMeshHulls, value.DebugMeshHulls, v => _spineObject.DebugMeshHulls = v, nameof(DebugMeshHulls));
|
||||
SetProperty(_spineObject.DebugMeshes, value.DebugMeshes, v => _spineObject.DebugMeshes = v, nameof(DebugMeshes));
|
||||
SetProperty(_spineObject.DebugBoundingBoxes, value.DebugBoundingBoxes, v => _spineObject.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes));
|
||||
SetProperty(_spineObject.DebugPaths, value.DebugPaths, v => _spineObject.DebugPaths = v, nameof(DebugPaths));
|
||||
SetProperty(_spineObject.DebugPoints, value.DebugPoints, v => _spineObject.DebugPoints = v, nameof(DebugPoints));
|
||||
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 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; }
|
||||
}
|
||||
}
|
||||
|
||||
57
SpineViewer/Models/WorkspaceModel.cs
Normal file
57
SpineViewer/Models/WorkspaceModel.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@ namespace SpineViewer.Resources
|
||||
public static string Str_GeneratePreviewsTitle => Get<string>("Str_GeneratePreviewsTitle");
|
||||
public static string Str_DeletePreviewsTitle => Get<string>("Str_DeletePreviewsTitle");
|
||||
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_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_OutputDirRequired => Get<string>("Str_OutputDirRequired");
|
||||
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_Copied => Get<string>("Str_Copied");
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
<s:String x:Key="Str_Experiment">Experimental Features</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>
|
||||
|
||||
<!-- 标签页 -->
|
||||
@@ -35,9 +38,10 @@
|
||||
<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_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_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_ApplySpineObjectConfig">Apply Config</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_DeletePreviewsTitle">Delete Previews</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_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_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_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_ResolutionTooltip">Screen resolution; adjust related parameters in the render settings panel</s:String>
|
||||
@@ -159,7 +163,7 @@
|
||||
<s:String x:Key="Str_ImageQualityTooltip">Range 0–100; only effective for certain formats</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_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_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>
|
||||
@@ -11,7 +11,10 @@
|
||||
<s:String x:Key="Str_Experiment">実験機能</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>
|
||||
|
||||
<!-- 标签页 -->
|
||||
@@ -35,9 +38,10 @@
|
||||
<s:String x:Key="Str_ListViewStatusBar">全{0}件、選択中{1}件</s:String>
|
||||
<s:String x:Key="Str_AddSpineObject">追加...</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_MoveDownSpineObject">下へ移動</s:String>
|
||||
<s:String x:Key="Str_AddSpineObjectFromClipboard">クリップボードから追加</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectConfig">パラメーターをコピー</s:String>
|
||||
<s:String x:Key="Str_ApplySpineObjectConfig">パラメーターを適用</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_DeletePreviewsTitle">プレビューを削除</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_WarnPopup">警告</s:String>
|
||||
@@ -139,7 +144,6 @@
|
||||
<s:String x:Key="Str_OutputDirNotFound">出力フォルダーが存在しません</s:String>
|
||||
<s:String x:Key="Str_OutputDirRequired">単一エクスポート時は出力フォルダーを指定する必要があります</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_ResolutionTooltip">画面解像度。関連パラメーターは画面パネルで調整してください</s:String>
|
||||
@@ -159,7 +163,7 @@
|
||||
<s:String x:Key="Str_ImageQualityTooltip">値の範囲は0-100。一部の画像フォーマットでのみ有効です</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_KeepLastFrame">最後のフレームを保持</s:String>
|
||||
@@ -194,5 +198,18 @@
|
||||
<s:String x:Key="Str_ProgremVersion">プログラムバージョン</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>
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
<s:String x:Key="Str_Experiment">实验性功能</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>
|
||||
|
||||
<!-- 标签页 -->
|
||||
@@ -35,9 +38,10 @@
|
||||
<s:String x:Key="Str_ListViewStatusBar">共 {0} 项,已选择 {1} 项</s:String>
|
||||
<s:String x:Key="Str_AddSpineObject">添加...</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_MoveDownSpineObject">下移</s:String>
|
||||
<s:String x:Key="Str_AddSpineObjectFromClipboard">从剪贴板添加</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectConfig">复制参数</s:String>
|
||||
<s:String x:Key="Str_ApplySpineObjectConfig">应用参数</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_DeletePreviewsTitle">删除预览图</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_WarnPopup">警告信息</s:String>
|
||||
@@ -139,7 +144,6 @@
|
||||
<s:String x:Key="Str_OutputDirNotFound">输出文件夹不存在</s:String>
|
||||
<s:String x:Key="Str_OutputDirRequired">导出单个时必须提供输出文件夹</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_ResolutionTooltip">画面分辨率,相关参数请在画面参数面板进行调整</s:String>
|
||||
@@ -159,7 +163,7 @@
|
||||
<s:String x:Key="Str_ImageQualityTooltip">取值范围 0-100,仅对部分图像格式生效</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_KeepLastFrame">保留最后一帧</s:String>
|
||||
@@ -194,4 +198,17 @@
|
||||
<s:String x:Key="Str_ProgremVersion">程序版本</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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
115
SpineViewer/Services/DialogService.cs
Normal file
115
SpineViewer/Services/DialogService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,22 +7,17 @@
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.15.0</Version>
|
||||
<Version>0.15.1</Version>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);NETSDK1206</NoWarn>
|
||||
<ApplicationIcon>appicon.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Remove="System.Windows.Forms" />
|
||||
<Using Remove="System.Drawing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appicon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
120
SpineViewer/Utils/JsonHelper.cs
Normal file
120
SpineViewer/Utils/JsonHelper.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SpineViewer.Extensions
|
||||
namespace SpineViewer.Utils
|
||||
{
|
||||
public class ObservableCollectionWithLock<T> : ObservableCollection<T>
|
||||
{
|
||||
@@ -7,7 +7,7 @@ using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace SpineViewer.Extensions
|
||||
namespace SpineViewer.Utils
|
||||
{
|
||||
public class StringFormatMultiValueConverter : IMultiValueConverter
|
||||
{
|
||||
@@ -16,7 +16,7 @@ namespace SpineViewer.Extensions
|
||||
if (values == null || values.Length <= 0)
|
||||
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);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
|
||||
namespace SpineViewer.ViewModels
|
||||
{
|
||||
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
|
||||
{
|
||||
|
||||
@@ -5,7 +5,9 @@ using SFMLRenderer;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -100,18 +102,19 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
/// </summary>
|
||||
protected void SetAutoResolutionStatic(BaseExporter exporter, params SpineObject[] spines)
|
||||
{
|
||||
var bounds = spines[0].GetAnimationBounds();
|
||||
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetAnimationBounds());
|
||||
var bounds = spines[0].GetCurrentBounds();
|
||||
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetCurrentBounds());
|
||||
SetAutoResolution(exporter, bounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用提供的模型设置导出器的自动分辨率和视区参数, 动画画面
|
||||
/// </summary>
|
||||
protected void SetAutoResolutionAnimated(BaseExporter exporter, params SpineObject[] spines)
|
||||
protected void SetAutoResolutionAnimated(VideoExporter exporter, params SpineObject[] spines)
|
||||
{
|
||||
var bounds = spines[0].GetAnimationBounds();
|
||||
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetAnimationBounds());
|
||||
var fps = exporter.Fps;
|
||||
var bounds = spines[0].GetAnimationBounds(fps);
|
||||
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetAnimationBounds(fps));
|
||||
SetAutoResolution(exporter, bounds);
|
||||
}
|
||||
|
||||
@@ -132,9 +135,21 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
|
||||
namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
@@ -46,11 +47,10 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void Export_Execute(IList? args)
|
||||
protected override void Export(SpineObjectModel[] models)
|
||||
{
|
||||
if (args is null || args.Count <= 0) return;
|
||||
if (!ExporterDialogService.ShowCustomFFmpegExporterDialog(this)) return;
|
||||
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
|
||||
if (!DialogService.ShowCustomFFmpegExporterDialog(this)) return;
|
||||
SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
|
||||
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_CustomFFmpegExporterTitle);
|
||||
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)
|
||||
{
|
||||
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
|
||||
Duration = _duration,
|
||||
Fps = _fps,
|
||||
KeepLast = _keepLast,
|
||||
Format = _format,
|
||||
@@ -90,6 +89,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
var output = Path.Combine(_outputDir!, filename);
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
||||
|
||||
exporter.ProgressReporter = (total, done, text) =>
|
||||
{
|
||||
|
||||
@@ -12,12 +12,13 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Collections.Immutable;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
|
||||
namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
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); }
|
||||
protected FFmpegVideoExporter.VideoFormat _format = FFmpegVideoExporter.VideoFormat.Mp4;
|
||||
@@ -33,11 +34,10 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
|
||||
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 (!ExporterDialogService.ShowFFmpegVideoExporterDialog(this)) return;
|
||||
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
|
||||
if (!DialogService.ShowFFmpegVideoExporterDialog(this)) return;
|
||||
SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
|
||||
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FFmpegVideoExporterTitle);
|
||||
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)
|
||||
{
|
||||
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
|
||||
Duration = _duration,
|
||||
Fps = _fps,
|
||||
KeepLast = _keepLast,
|
||||
Format = _format,
|
||||
@@ -75,6 +74,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
var output = Path.Combine(_outputDir!, filename);
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
||||
|
||||
exporter.ProgressReporter = (total, done, text) =>
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -19,7 +20,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
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); }
|
||||
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 (!ExporterDialogService.ShowFrameExporterDialog(this)) return;
|
||||
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject(true)).ToArray();
|
||||
if (!DialogService.ShowFrameExporterDialog(this)) return;
|
||||
SpineObject[] spines = models.Select(m => m.GetSpineObject(true)).ToArray();
|
||||
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameExporterTitle);
|
||||
foreach (var sp in spines) sp.Dispose();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -16,11 +17,10 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
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 (!ExporterDialogService.ShowFrameSequenceExporterDialog(this)) return;
|
||||
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
|
||||
if (!DialogService.ShowFrameSequenceExporterDialog(this)) return;
|
||||
SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
|
||||
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameSequenceExporterTitle);
|
||||
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)
|
||||
{
|
||||
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
|
||||
Duration = _duration,
|
||||
Fps = _fps,
|
||||
KeepLast = _keepLast
|
||||
};
|
||||
@@ -54,6 +53,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
var output = Path.Combine(_outputDir!, folderName);
|
||||
|
||||
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
|
||||
if (_duration < 0) exporter.Duration = spines.Select(sp => sp.GetAnimationMaxDuration()).DefaultIfEmpty(0).Max();
|
||||
|
||||
exporter.ProgressReporter = (total, done, text) =>
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -17,14 +18,5 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
|
||||
public bool KeepLast { get => _keepLast; set => SetProperty(ref _keepLast, value); }
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -22,7 +21,7 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shell;
|
||||
|
||||
namespace SpineViewer.ViewModels
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
public class ExplorerListViewModel : ObservableObject
|
||||
{
|
||||
@@ -95,7 +94,7 @@ namespace SpineViewer.ViewModels
|
||||
/// </summary>
|
||||
public RelayCommand Cmd_ChangeCurrentDirectory => _cmd_ChangeCurrentDirectory ??= new(() =>
|
||||
{
|
||||
if (OpenFolderService.OpenFolder(out var selectedPath))
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
_currentDirectory = selectedPath;
|
||||
RefreshItems();
|
||||
@@ -1,25 +1,13 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HandyControl.Controls;
|
||||
using NLog;
|
||||
using SFMLRenderer;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
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 SpineViewer.Utils;
|
||||
using System.Windows.Shell;
|
||||
|
||||
namespace SpineViewer.ViewModels
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// MainWindow 上下文对象
|
||||
@@ -34,9 +22,10 @@ namespace SpineViewer.ViewModels
|
||||
_explorerListViewModel = new(this);
|
||||
_spineObjectListViewModel = new(this);
|
||||
_sfmlRendererViewModel = new(this);
|
||||
_preferenceViewModel = new(this);
|
||||
}
|
||||
|
||||
public string Title => $"SpineViewer - {App.Version}";
|
||||
public string Title => $"SpineViewer - v{App.Version}";
|
||||
|
||||
/// <summary>
|
||||
/// SFML 渲染对象
|
||||
@@ -56,6 +45,9 @@ namespace SpineViewer.ViewModels
|
||||
public ObservableCollectionWithLock<SpineObjectModel> SpineObjects => _spineObjectModels;
|
||||
private readonly ObservableCollectionWithLock<SpineObjectModel> _spineObjectModels = [];
|
||||
|
||||
public PreferenceViewModel PreferenceViewModel => _preferenceViewModel;
|
||||
private readonly PreferenceViewModel _preferenceViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 浏览页列表 ViewModel
|
||||
/// </summary>
|
||||
@@ -80,18 +72,63 @@ namespace SpineViewer.ViewModels
|
||||
public SFMLRendererViewModel 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>
|
||||
public RelayCommand Cmd_ShowDiagnosticsDialog => _cmd_ShowDiagnosticsDialog ??= new(() => { DiagnosticsDialogService.ShowDiagnosticsDialog(); });
|
||||
public RelayCommand Cmd_ShowDiagnosticsDialog => _cmd_ShowDiagnosticsDialog ??= new(() => { DialogService.ShowDiagnosticsDialog(); });
|
||||
private RelayCommand? _cmd_ShowDiagnosticsDialog;
|
||||
|
||||
/// <summary>
|
||||
/// 显示关于对话框
|
||||
/// </summary>
|
||||
public RelayCommand Cmd_ShowAboutDialog => _cmd_ShowAboutDialog ??= new(() => { AboutDialogService.ShowAboutDialog(); });
|
||||
public RelayCommand Cmd_ShowAboutDialog => _cmd_ShowAboutDialog ??= new(() => { DialogService.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>
|
||||
@@ -101,31 +138,8 @@ namespace SpineViewer.ViewModels
|
||||
private void Debug_Execute()
|
||||
{
|
||||
#if DEBUG
|
||||
var path = @"C:\Users\ljh\Desktop\a.mp4";
|
||||
|
||||
using var exporter = new FFmpegVideoExporter(_sfmlRenderer.Resolution);
|
||||
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);
|
||||
}, "测试一下");
|
||||
MessagePopupService.Quest("测试一下");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
241
SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs
Normal file
241
SpineViewer/ViewModels/MainWindow/PreferenceViewModel.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
@@ -18,7 +19,7 @@ using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SpineViewer.ViewModels
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
public class SFMLRendererViewModel : ObservableObject
|
||||
{
|
||||
@@ -31,6 +32,16 @@ namespace SpineViewer.ViewModels
|
||||
private readonly ObservableCollectionWithLock<SpineObjectModel> _models;
|
||||
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>
|
||||
@@ -78,55 +89,55 @@ namespace SpineViewer.ViewModels
|
||||
public uint ResolutionX
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
@@ -139,7 +150,7 @@ namespace SpineViewer.ViewModels
|
||||
public Color BackgroundColor
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -381,6 +392,18 @@ namespace SpineViewer.ViewModels
|
||||
sp.Update(0); // 避免物理效果出现问题
|
||||
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;
|
||||
_renderer.Draw(sp);
|
||||
sp.EnableDebug = false;
|
||||
@@ -401,5 +424,41 @@ namespace SpineViewer.ViewModels
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections;
|
||||
@@ -17,7 +18,7 @@ using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Shell;
|
||||
|
||||
namespace SpineViewer.ViewModels
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
public class SpineObjectListViewModel : ObservableObject
|
||||
{
|
||||
@@ -100,7 +101,7 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
private void AddSpineObject_Execute()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
MessagePopupService.Info("Not Implemented, try next version :)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -111,7 +112,7 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
private void RemoveSpineObject_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
if (!RemoveSpineObject_CanExecute(args)) return;
|
||||
|
||||
if (args.Count > 1)
|
||||
{
|
||||
@@ -121,7 +122,7 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
lock (_spineObjectModels.Lock)
|
||||
{
|
||||
// XXX: 这里必须要浅拷贝一次, 不能直接对会被修改的绑定数据 args 进行 foreach 遍历
|
||||
// NOTE: 这里必须要浅拷贝一次, 不能直接对会被修改的绑定数据 args 进行 foreach 遍历
|
||||
foreach (var sp in args.Cast<SpineObjectModel>().ToArray())
|
||||
{
|
||||
_spineObjectModels.Remove(sp);
|
||||
@@ -137,6 +138,121 @@ namespace SpineViewer.ViewModels
|
||||
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>
|
||||
@@ -145,8 +261,7 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
private void MoveUpSpineObject_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
if (args.Count != 1) return;
|
||||
if (!MoveUpSpineObject_CanExecute(args)) return;
|
||||
var sp = (SpineObjectModel)args[0];
|
||||
lock (_spineObjectModels.Lock)
|
||||
{
|
||||
@@ -171,8 +286,7 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
private void MoveDownSpineObject_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
if (args.Count != 1) return;
|
||||
if (!MoveDownSpineObject_CanExecute(args)) return;
|
||||
var sp = (SpineObjectModel)args[0];
|
||||
lock (_spineObjectModels.Lock)
|
||||
{
|
||||
@@ -189,18 +303,6 @@ namespace SpineViewer.ViewModels
|
||||
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>
|
||||
@@ -209,10 +311,9 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
private void CopySpineObjectConfig_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
if (args.Count != 1) return;
|
||||
if (!CopySpineObjectConfig_CanExecute(args)) return;
|
||||
var sp = (SpineObjectModel)args[0];
|
||||
_copiedSpineObjectConfigModel = sp.Dump();
|
||||
_copiedSpineObjectConfigModel = sp.ObjectConfig;
|
||||
_logger.Info("Copy config from model: {0}", sp.Name);
|
||||
}
|
||||
|
||||
@@ -231,12 +332,10 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
private void ApplySpineObjectConfig_Execute(IList? args)
|
||||
{
|
||||
if (_copiedSpineObjectConfigModel is null) return;
|
||||
if (args is null) return;
|
||||
if (args.Count <= 0) return;
|
||||
if (!ApplySpineObjectConfig_CanExecute(args)) return;
|
||||
foreach (SpineObjectModel sp in args)
|
||||
{
|
||||
sp.Load(_copiedSpineObjectConfigModel);
|
||||
sp.ObjectConfig = _copiedSpineObjectConfigModel;
|
||||
_logger.Info("Apply config to model: {0}", sp.Name);
|
||||
}
|
||||
}
|
||||
@@ -249,6 +348,51 @@ namespace SpineViewer.ViewModels
|
||||
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>
|
||||
@@ -289,17 +433,7 @@ namespace SpineViewer.ViewModels
|
||||
}
|
||||
else if (validPaths.Count > 0)
|
||||
{
|
||||
var skelPath = 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);
|
||||
}
|
||||
AddSpineObject(validPaths[0]);
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
}
|
||||
@@ -326,18 +460,10 @@ namespace SpineViewer.ViewModels
|
||||
var skelPath = paths[i];
|
||||
reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}";
|
||||
|
||||
try
|
||||
{
|
||||
var sp = new SpineObjectModel(skelPath);
|
||||
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
|
||||
if (AddSpineObject(skelPath))
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
|
||||
else
|
||||
error++;
|
||||
}
|
||||
|
||||
reporter.Done = i + 1;
|
||||
reporter.ProgressText = $"[{i + 1}/{totalCount}] {skelPath}";
|
||||
@@ -352,5 +478,132 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewer.ViewModels
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
public class SpineObjectTabViewModel : ObservableObject
|
||||
{
|
||||
@@ -49,15 +49,15 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
IEnumerable<string> commonSkinNames = _selectedObjects[0].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;
|
||||
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();
|
||||
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();
|
||||
@@ -611,7 +611,7 @@ namespace SpineViewer.ViewModels
|
||||
// 但是目前无法识别是否增加了轨道, 因此总是重建列表
|
||||
|
||||
// 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行
|
||||
App.Current.Dispatcher.BeginInvoke(
|
||||
Application.Current.Dispatcher.BeginInvoke(
|
||||
() =>
|
||||
{
|
||||
_animationTracks.Clear();
|
||||
@@ -786,7 +786,7 @@ namespace SpineViewer.ViewModels
|
||||
{
|
||||
get
|
||||
{
|
||||
/// XXX: 空轨道和多选不相同都会返回 null
|
||||
// XXX: 空轨道和多选不相同都会返回 null
|
||||
if (_spines.Length <= 0) return null;
|
||||
var val = _spines[0].GetAnimation(_trackIndex);
|
||||
if (_spines.Skip(1).Any(it => it.GetAnimation(_trackIndex) != val)) return null;
|
||||
@@ -63,6 +63,7 @@ namespace SpineViewer.ViewModels
|
||||
|
||||
private void Cancel_Execute()
|
||||
{
|
||||
if (!Cancel_CanExecute()) return;
|
||||
if (!MessagePopupService.Quest(AppResource.Str_CancelQuest)) return;
|
||||
_cts.Cancel();
|
||||
Cmd_Cancel.NotifyCanExecuteChanged();
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
|
||||
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (OpenFolderService.OpenFolder(out var selectedPath))
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
var vm = (CustomFFmpegExporterViewModel)DataContext;
|
||||
vm.OutputDir = selectedPath;
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
|
||||
<!-- 视频格式 -->
|
||||
<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}"/>
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
|
||||
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (OpenFolderService.OpenFolder(out var selectedPath))
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
var vm = (FFmpegVideoExporterViewModel)DataContext;
|
||||
vm.OutputDir = selectedPath;
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
<!-- 图像格式 -->
|
||||
<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}"/>
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
|
||||
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (OpenFolderService.OpenFolder(out var selectedPath))
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
var vm = (FrameExporterViewModel)DataContext;
|
||||
vm.OutputDir = selectedPath;
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
|
||||
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (OpenFolderService.OpenFolder(out var selectedPath))
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
var vm = (FrameSequenceExporterViewModel)DataContext;
|
||||
vm.OutputDir = selectedPath;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:vm="clr-namespace:SpineViewer.ViewModels"
|
||||
xmlns:ext="clr-namespace:SpineViewer.Extensions"
|
||||
xmlns:vm="clr-namespace:SpineViewer.ViewModels.MainWindow"
|
||||
xmlns:utils="clr-namespace:SpineViewer.Utils"
|
||||
xmlns:SFMLRenderer="clr-namespace:SFMLRenderer;assembly=SFMLRenderer"
|
||||
mc:Ignorable="d"
|
||||
Title="{Binding Title}"
|
||||
@@ -35,7 +35,7 @@
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<ext:StringFormatMultiValueConverter x:Key="StrFmtCvter"/>
|
||||
<utils:StringFormatMultiValueConverter x:Key="StrFmtCvter"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Window.TaskbarItemInfo>
|
||||
@@ -56,9 +56,10 @@
|
||||
<!-- 菜单 -->
|
||||
<Menu x:Name="_mainMenu">
|
||||
<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/>
|
||||
<MenuItem Header="{DynamicResource Str_Preference}"/>
|
||||
<MenuItem Header="{DynamicResource Str_PreferenceWithDots}" Command="{Binding PreferenceViewModel.Cmd_ShowPreferenceDialog}"/>
|
||||
<!--<MenuItem Header="{DynamicResource Str_Exit}" InputGestureText="Alt+F4"/>-->
|
||||
</MenuItem>
|
||||
<!--<MenuItem Header="{DynamicResource Str_Tool}"/>-->
|
||||
@@ -244,11 +245,11 @@
|
||||
</i:Interaction.Triggers>
|
||||
|
||||
<ListView.InputBindings>
|
||||
<KeyBinding Gesture="Ctrl+O" Command="{Binding Cmd_AddSpineObject}"/>
|
||||
<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+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+V" Command="{Binding Cmd_ApplySpineObjectConfig}" CommandParameter="{Binding SelectedItems, ElementName=_spinesListView}"/>
|
||||
</ListView.InputBindings>
|
||||
@@ -256,12 +257,18 @@
|
||||
<ListView.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="{DynamicResource Str_AddSpineObject}"
|
||||
InputGestureText="Ctrl+O"
|
||||
Command="{Binding Cmd_AddSpineObject}"/>
|
||||
<MenuItem Header="{DynamicResource Str_RemoveSpineObject}"
|
||||
InputGestureText="Delete"
|
||||
Command="{Binding Cmd_RemoveSpineObject}"
|
||||
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/>
|
||||
<MenuItem Header="{DynamicResource Str_MoveUpSpineObject}"
|
||||
InputGestureText="Alt+W"
|
||||
@@ -272,10 +279,6 @@
|
||||
Command="{Binding Cmd_MoveDownSpineObject}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{DynamicResource Str_AddSpineObjectFromClipboard}"
|
||||
InputGestureText="Ctrl+V"
|
||||
Command="{Binding Cmd_AddSpineObjectFromClipboard}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{DynamicResource Str_CopySpineObjectConfig}"
|
||||
InputGestureText="Ctrl+Shift+C"
|
||||
Command="{Binding Cmd_CopySpineObjectConfig}"
|
||||
@@ -284,9 +287,12 @@
|
||||
InputGestureText="Ctrl+Shift+V"
|
||||
Command="{Binding Cmd_ApplySpineObjectConfig}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{DynamicResource Str_ApplySpineObjectConfigFromFile}"/>
|
||||
<MenuItem Header="{DynamicResource Str_SaveSpineObjectConfigToFile}"/>
|
||||
<MenuItem Header="{DynamicResource Str_ApplySpineObjectConfigFromFile}"
|
||||
Command="{Binding Cmd_ApplySpineObjectConfigFromFile}"
|
||||
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/>
|
||||
<MenuItem Header="{DynamicResource Str_Export}">
|
||||
<MenuItem Header="{DynamicResource Str_ExportFrame}"
|
||||
@@ -687,7 +693,6 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 水平分辨率 -->
|
||||
@@ -726,17 +731,13 @@
|
||||
<Label Grid.Row="8" Grid.Column="0" Content="{DynamicResource Str_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}"/>
|
||||
<ToggleButton Grid.Row="10" Grid.Column="1" IsChecked="{Binding ShowAxis}"/>
|
||||
<Label Grid.Row="9" Grid.Column="0" Content="{DynamicResource Str_ShowAxis}"/>
|
||||
<ToggleButton Grid.Row="9" Grid.Column="1" IsChecked="{Binding ShowAxis}"/>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<Label Grid.Row="11" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<TextBox Grid.Row="11" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<Label Grid.Row="10" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<TextBox Grid.Row="10" Grid.Column="1" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
|
||||
<!-- 背景图案 -->
|
||||
<!-- 背景图案模式 -->
|
||||
|
||||
@@ -5,7 +5,7 @@ using NLog.Targets;
|
||||
using Spine;
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.ViewModels;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
@@ -61,6 +61,9 @@ public partial class MainWindow : Window
|
||||
vm.FlipY = true;
|
||||
vm.MaxFps = 30;
|
||||
vm.StartRender();
|
||||
|
||||
// 加载首选项
|
||||
_vm.PreferenceViewModel.LoadPreference();
|
||||
}
|
||||
|
||||
private void MainWindow_Closed(object? sender, EventArgs e)
|
||||
|
||||
@@ -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>
|
||||
162
SpineViewer/Views/PreferenceDialog.xaml
Normal file
162
SpineViewer/Views/PreferenceDialog.xaml
Normal 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>
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -15,13 +17,23 @@ using System.Windows.Shapes;
|
||||
namespace SpineViewer.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// MessageBoxWindow.xaml 的交互逻辑
|
||||
/// PreferenceDialog.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class MessageBoxWindow : Window
|
||||
public partial class PreferenceDialog : Window
|
||||
{
|
||||
public MessageBoxWindow()
|
||||
public PreferenceDialog()
|
||||
{
|
||||
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
79
SpineViewer/app.manifest
Normal 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>
|
||||
BIN
img/preview.webp
BIN
img/preview.webp
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 177 KiB |
Reference in New Issue
Block a user