Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1677bda48 | ||
|
|
83d054382c | ||
|
|
608de7f2e8 | ||
|
|
6f2eb504ab | ||
|
|
2b53e0b60b | ||
|
|
e96db328fa | ||
|
|
874404e3d3 | ||
|
|
8e771fbaa4 | ||
|
|
fb319e09d8 | ||
|
|
849f1813be | ||
|
|
763d2e295e | ||
|
|
d6ca3cca92 | ||
|
|
32c826a3db | ||
|
|
b17f3bec79 | ||
|
|
246f70fd4d | ||
|
|
7eb140a030 | ||
|
|
c68a91fe3f | ||
|
|
dc2cb61219 | ||
|
|
decbb10fcb | ||
|
|
c538fd8960 | ||
|
|
cc884f7f5b | ||
|
|
dcfe48912b | ||
|
|
3c77365f60 | ||
|
|
baff9579e5 | ||
|
|
44f4367f3e | ||
|
|
5823d58dca | ||
|
|
7bb76d508e | ||
|
|
b4c7579d24 | ||
|
|
aafe487d96 | ||
|
|
dc2f6a2dad | ||
|
|
10b691d897 | ||
|
|
50219946ec | ||
|
|
659f6fb690 | ||
|
|
af5cb97f1a | ||
|
|
1be9e9e75f | ||
|
|
b0308db977 | ||
|
|
2dbcfe4ea7 | ||
|
|
9040e02025 | ||
|
|
b3ba073368 | ||
|
|
332019a667 | ||
|
|
add9cf157d | ||
|
|
8b0ea750d8 | ||
|
|
733739921d | ||
|
|
e0f46f521a | ||
|
|
aa4245ef2a | ||
|
|
a262538eba | ||
|
|
2e4a5a75c0 | ||
|
|
9331656431 | ||
|
|
64bc12db06 | ||
|
|
7a29fee641 | ||
|
|
49f95ddbb7 | ||
|
|
317ee71882 | ||
|
|
7780fbda28 | ||
|
|
b54c6a1777 | ||
|
|
617157044c | ||
|
|
29d7e8d9d8 | ||
|
|
701d1fcf90 | ||
|
|
df36d46528 | ||
|
|
3459f3af03 | ||
|
|
5498508700 | ||
|
|
a61bb43250 | ||
|
|
aace461ae0 | ||
|
|
c02cec9a18 | ||
|
|
31daed9e81 | ||
|
|
997d55350d | ||
|
|
cc6d1b6c00 | ||
|
|
e14c54c3a4 | ||
|
|
5eba515eac | ||
|
|
f878530184 | ||
|
|
81d9224658 | ||
|
|
9d9edb8bc4 | ||
|
|
d3b5814c6f | ||
|
|
aade44cffb | ||
|
|
c4956b9c16 | ||
|
|
7ca431b214 | ||
|
|
74538ddf74 | ||
|
|
779500ee8e | ||
|
|
ee7c9e9e54 | ||
|
|
d335645dc1 | ||
|
|
6a17ec0397 |
@@ -11,6 +11,10 @@ jobs:
|
||||
build-release:
|
||||
if: ${{ github.event.pull_request.merged == true }}
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
version: ${{ steps.extract_version.outputs.version }}
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
|
||||
env:
|
||||
PROJECT_NAME: SpineViewer
|
||||
PROJ_CLI_NAME: SpineViewerCLI
|
||||
@@ -27,21 +31,15 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Extract version from csproj
|
||||
id: extract_version
|
||||
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".Trim()
|
||||
"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"
|
||||
echo "Version tag found: $VERSION_TAG"
|
||||
echo "version=$VERSION_TAG" >> $env:GITHUB_OUTPUT
|
||||
echo "VERSION=$VERSION_TAG" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Tag merge commit
|
||||
shell: pwsh
|
||||
@@ -63,19 +61,11 @@ jobs:
|
||||
dotnet publish "$env:PROJECT_NAME\$env:PROJECT_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
|
||||
dotnet publish "$env:PROJ_CLI_NAME\$env:PROJ_CLI_NAME.csproj" -c Release -r win-x64 --sc true -o "publish\$env:PROJECT_NAME-$env:VERSION-SelfContained"
|
||||
|
||||
- name: Create release directory
|
||||
- name: Compress Windows builds
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- name: Create GitHub Release
|
||||
@@ -89,7 +79,7 @@ jobs:
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload FrameworkDependent zip
|
||||
- name: Upload Windows FrameworkDependent zip
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -99,7 +89,7 @@ jobs:
|
||||
asset_name: ${{ env.PROJECT_NAME }}-${{ env.VERSION }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload SelfContained zip
|
||||
- name: Upload Windows SelfContained zip
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -108,3 +98,43 @@ jobs:
|
||||
asset_path: release/${{ env.PROJECT_NAME }}-${{ env.VERSION }}-SelfContained.zip
|
||||
asset_name: ${{ env.PROJECT_NAME }}-${{ env.VERSION }}-SelfContained.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
|
||||
build-release-linux:
|
||||
needs: build-release
|
||||
if: ${{ github.event.pull_request.merged == true }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PROJ_CLI_NAME: SpineViewerCLI
|
||||
VERSION: ${{ needs.build-release.outputs.version }}
|
||||
|
||||
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"
|
||||
|
||||
- name: Publish Linux SelfContained version
|
||||
run: |
|
||||
dotnet publish "$PROJ_CLI_NAME/$PROJ_CLI_NAME.csproj" -c Release -r linux-x64 --sc true -o "publish/${PROJ_CLI_NAME}-${VERSION}-Linux-SelfContained"
|
||||
|
||||
- name: Compress Linux build
|
||||
run: |
|
||||
mkdir -p release
|
||||
cd publish
|
||||
zip -r "../release/${PROJ_CLI_NAME}-${VERSION}-Linux-SelfContained.zip" "${PROJ_CLI_NAME}-${VERSION}-Linux-SelfContained"
|
||||
|
||||
- name: Upload Linux zip to GitHub Release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.build-release.outputs.upload_url }}
|
||||
asset_path: release/${{ env.PROJ_CLI_NAME }}-${{ env.VERSION }}-Linux-SelfContained.zip
|
||||
asset_name: ${{ env.PROJ_CLI_NAME }}-${{ env.VERSION }}-Linux-SelfContained.zip
|
||||
asset_content_type: application/zip
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -396,3 +396,5 @@ FodyWeavers.xsd
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
launchSettings.json
|
||||
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,5 +1,29 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.16.12
|
||||
|
||||
- 修复 label 控件文字显示问题
|
||||
- 增强报错日志输出
|
||||
- 增加实时帧率显示
|
||||
- 首选项增加预览画面和投影最大帧率设置,移除用户状态和工作区帧率记忆
|
||||
- 优化某些性能
|
||||
|
||||
## v0.16.11
|
||||
|
||||
- 增加 shift 切换缩放倍数
|
||||
- 改善后台性能
|
||||
- 修复字体显示颜色问题
|
||||
- 调整浏览目录参数保存至用户状态
|
||||
- 调整浏览面板至最后
|
||||
|
||||
## v0.16.10
|
||||
|
||||
- 增加 Linux 平台 CLI 工具构建
|
||||
|
||||
## v0.16.9
|
||||
|
||||
- 重构 CLI 工具
|
||||
|
||||
## v0.16.8
|
||||
|
||||
- 去除首次的最小化提示弹框
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
|
||||
188
README.en.md
188
README.en.md
@@ -1,139 +1,154 @@
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-release.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||

|
||||
|
||||
[中文](README.md) | [English](README.en.md)
|
||||
|
||||
A simple and user-friendly Spine file viewer and exporter with multi-language support (Chinese/English/Japanese).
|
||||
Spine file viewer & exporter, also a dynamic wallpaper program supporting Spine animations.
|
||||
|
||||

|
||||
|
||||
https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0
|
||||
[https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0](https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0)
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- Multiple versions of Spine files
|
||||
- Batch file opening via drag-and-drop or copy-paste
|
||||
- Batch preview
|
||||
- List-based multi-skeleton viewing and render order management
|
||||
- Multi-selection in lists for batch skeleton parameter settings
|
||||
- Multi-track animation settings
|
||||
- Skin and custom slot attachment settings
|
||||
- Custom slot visibility
|
||||
- Debug rendering
|
||||
- Playback speed adjustment for view/model/track timelines
|
||||
- Track alpha blending parameter settings
|
||||
- Fullscreen preview
|
||||
- Export to single frame, image sequence, animated GIF, or video file
|
||||
- Automatic resolution batch export
|
||||
- Custom export with FFmpeg
|
||||
- Program parameter saving
|
||||
- File extension association
|
||||
- Texture images in formats other than PNG
|
||||
- Launch at startup with persistent dynamic wallpaper
|
||||
- Supports multiple Spine file versions (`2.1.x; 3.4.x - 4.2.-`)
|
||||
- List-based multi-skeleton view with rendering order management
|
||||
- Supports multi-track animations
|
||||
- Supports skin/slot/attachment settings
|
||||
- Debug rendering support
|
||||
- Frame rate / model / track time scale adjustment
|
||||
- Track alpha blending control
|
||||
- Export single frame / GIF / video
|
||||
- Custom export via FFmpeg
|
||||
- Supports non-PNG texture formats
|
||||
- Desktop dynamic wallpaper with auto-start support
|
||||
- ......
|
||||
|
||||
### Supported Spine Versions
|
||||
|
||||
| Version | View & Export |
|
||||
| :-----: | :------------------: |
|
||||
| `2.1.x` | :white\_check\_mark: |
|
||||
| `3.4.x` | :white\_check\_mark: |
|
||||
| `3.5.x` | :white\_check\_mark: |
|
||||
| `3.6.x` | :white\_check\_mark: |
|
||||
| `3.7.x` | :white\_check\_mark: |
|
||||
| `3.8.x` | :white\_check\_mark: |
|
||||
| `4.0.x` | :white\_check\_mark: |
|
||||
| `4.1.x` | :white\_check\_mark: |
|
||||
| `4.2.x` | :white\_check\_mark: |
|
||||
| `4.3.x` | |
|
||||
|
||||
More versions under development \:rocket: \:rocket: \:rocket:
|
||||
|
||||
### Supported Export Formats
|
||||
|
||||
| Format | Use Case |
|
||||
| -------------- | ----------------------------------------------------------------------------- |
|
||||
| Single Frame | Generate high-resolution images of models; manually adjust the desired frame. |
|
||||
| Frame Sequence | Supports PNG format with transparency and lossless compression. |
|
||||
| GIF/Video | Export preview animations or common video formats. |
|
||||
| Custom Export | Supports arbitrary FFmpeg parameters for custom, complex export needs. |
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Download the compressed package from the [Release](https://github.com/ww-rm/SpineViewer/releases) page.
|
||||
Download the compressed package from the [Releases](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.
|
||||
The program requires the [.NET Desktop Runtime 8.0.x](https://dotnet.microsoft.com/download/dotnet/8.0) to be installed.
|
||||
|
||||
Alternatively, download the package with the `SelfContained` suffix for standalone execution.
|
||||
You can also download packages with the `SelfContained` suffix, which can run independently without additional installations.
|
||||
|
||||
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).
|
||||
Exporting GIF/MP4 or other animated/video formats requires **ffmpeg** installed locally and added to the system PATH. Download [FFmpeg for Windows](https://ffmpeg.org/download.html#build-windows) or the latest full build [ffmpeg-release-full.7z](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z).
|
||||
|
||||
---
|
||||
|
||||
## Changing Display Language
|
||||
|
||||
Currently, the program supports the following interface languages:
|
||||
|
||||
- `ZH` (Chinese)
|
||||
- `EN` (English)
|
||||
- `JA` (Japanese)
|
||||
|
||||
Change the language via the menu: **File → Preferences… → Language**, then confirm.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### How to Change the Display Language
|
||||
### Overview
|
||||
|
||||
In the menu, go to "File" -> "Preferences..." -> "Language," select your desired language, and confirm the change.
|
||||
The program uses a left-right layout: the left panel contains controls, the right panel displays the preview.
|
||||
|
||||
### Basic Overview
|
||||
The left panel contains three sub-panels:
|
||||
|
||||
The program is organized into a left-right layout:
|
||||
- **Models**: Lists imported and rendered models. Set model parameters, rendering order, and other model-related functions here.
|
||||
- **Browser**: Preview files in a folder without actually importing them. Generate WebP previews or import selected models.
|
||||
- **Canvas**: Set parameters for the right-side preview display.
|
||||
|
||||
- **Left Panel:** Functionality panel.
|
||||
- **Right Panel:** Preview display.
|
||||
Most buttons, labels, or input fields show help text on hover.
|
||||
|
||||
The left panel includes three sub-panels:
|
||||
---
|
||||
|
||||
- **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.
|
||||
### Importing Skeletons
|
||||
|
||||
Hover your mouse over buttons, labels, or input fields to see help text for most UI elements.
|
||||
Drag-and-drop or paste skeleton files/folders directly into the **Models** panel.
|
||||
|
||||
### Skeleton Import
|
||||
Alternatively, use the right-click menu in the **Browser** panel to import selected items.
|
||||
|
||||
Drag-and-drop or paste skeleton files/directories into the Model panel.
|
||||
---
|
||||
|
||||
Alternatively, use the right-click menu in the Browse panel to import selected items.
|
||||
### Adjusting Content
|
||||
|
||||
### Content Adjustment
|
||||
The **Models** panel supports right-click menus, some hotkeys, and batch editing via multi-selection.
|
||||
|
||||
The Model panel supports right-click menus, some shortcuts, and batch adjustments of model parameters through multi-selection.
|
||||
Mouse interactions in the preview panel:
|
||||
|
||||
For preview display adjustments:
|
||||
- **Left click**: select and drag models. Hold `Ctrl` for multi-selection (synchronized with the model list).
|
||||
- **Right click**: drag the entire canvas.
|
||||
- **Mouse wheel**: zoom in/out. Hold `Ctrl` to scale selected models together, use `Shift` to switch zoom factor.
|
||||
- **Render selected only**: preview only the selected models, selection can only be changed via the left panel.
|
||||
|
||||
- **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.
|
||||
Playback controls below the preview allow time adjustment, acting as a simple player.
|
||||
|
||||
The buttons below the preview display allow time adjustments, serving as a simple playback control.
|
||||
---
|
||||
|
||||
### Content Export
|
||||
### Exporting Content
|
||||
|
||||
Export follows the **WYSIWYG (What You See Is What You Get)** principle, meaning the preview display reflects the exported output.
|
||||
Right-click on models in the list to access export options.
|
||||
|
||||
Use the right-click menu in the Model panel to export selected items.
|
||||
Key export parameters:
|
||||
|
||||
Key export parameters include:
|
||||
- **Output folder**: Optional. If not provided, outputs go to each model’s folder. Otherwise, all outputs go to the specified folder.
|
||||
- **Single export**: Default exports each model separately. If enabled, all selected models are rendered together in one output.
|
||||
- **Auto resolution**: Ignores preview canvas resolution; exported resolution matches the actual size of content. For animations or videos, ensures full display of the animation.
|
||||
|
||||
- **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.
|
||||
---
|
||||
|
||||
### Dynamic Wallpaper
|
||||
|
||||
Dynamic wallpaper is implemented through desktop projection, allowing the content of the current preview to be projected onto the desktop in real time.
|
||||
The dynamic wallpaper projects the current preview content to the desktop in real time.
|
||||
|
||||
You can enable or disable desktop projection from the program preferences or the right-click menu of the tray icon. After adjusting the model and display parameters, you can save the current configuration as a workspace file for convenient restoration later.
|
||||
Enable or disable via program preferences or the tray icon menu. Save workspace files to preserve model and canvas settings.
|
||||
|
||||
If you want the wallpaper to stay active after startup, you can enable auto-start in the preferences and specify which workspace file should be loaded when the program launches.
|
||||
Auto-start with Windows can also be enabled, along with loading a specific workspace on startup.
|
||||
|
||||
### 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).
|
||||
### Command-line Tool
|
||||
|
||||
The project includes a CLI tool `SpineViewerCLI` for simple operations on a single model (querying parameters, exporting, etc.). Windows and Linux binaries are provided in Releases.
|
||||
|
||||
```bash
|
||||
$ SpineViewerCLI -h
|
||||
Description:
|
||||
Root Command
|
||||
|
||||
Usage:
|
||||
SpineViewerCLI [command] [options]
|
||||
|
||||
Options:
|
||||
-q, --quiet Suppress console logging (quiet mode).
|
||||
-?, -h, --help Show help and usage information
|
||||
--version Show version information
|
||||
|
||||
Commands:
|
||||
query <skel> Query information of single model
|
||||
preview <skel> Preview a model
|
||||
export <skel> Export single model
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### More
|
||||
|
||||
Detailed instructions and usage guides can be found in the [Wiki](https://github.com/ww-rm/SpineViewer/wiki).
|
||||
Report issues or bugs via [GitHub Issues](https://github.com/ww-rm/SpineViewer/issues).
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -143,9 +158,10 @@ For detailed usage and documentation, see the [Wiki](https://github.com/ww-rm/Sp
|
||||
- [HandyControl](https://github.com/HandyOrg/HandyControl)
|
||||
- [NLog](https://github.com/NLog/NLog)
|
||||
- [SkiaSharp](https://github.com/mono/SkiaSharp)
|
||||
- [Spectre.Console](https://github.com/spectreconsole/spectre.console)
|
||||
|
||||
---
|
||||
|
||||
*If you find this project helpful, please give it a \:star: and share it with others! :)*
|
||||
*If you like this project, please give it a :star: and share it with others! :\)*
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
|
||||
92
README.md
92
README.md
@@ -1,6 +1,6 @@
|
||||
# [SpineViewer](https://github.com/ww-rm/SpineViewer)
|
||||
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-desktop.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/actions/workflows/dotnet-release.yml)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
[](https://github.com/ww-rm/SpineViewer/releases)
|
||||
|
||||
@@ -16,53 +16,19 @@ https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0
|
||||
|
||||
## 功能
|
||||
|
||||
- 支持多版本 spine 文件
|
||||
- 支持拖拽/复制粘贴批量打开文件
|
||||
- 支持批量预览
|
||||
- 支持多版本 spine 文件 (`2.1.x; 3.4.x - 4.2.x`)
|
||||
- 支持列表式多骨骼查看和渲染层级管理
|
||||
- 支持列表多选批量设置骨骼参数
|
||||
- 支持多轨道动画设置
|
||||
- 支持皮肤/自定义插槽附件设置
|
||||
- 支持自定义插槽可见性
|
||||
- 支持多轨道动画
|
||||
- 支持皮肤/插槽/附件设置
|
||||
- 支持调试渲染
|
||||
- 支持画面/模型/轨道时间倍速设置
|
||||
- 支持设置轨道 Alpha 混合参数
|
||||
- 支持全屏预览
|
||||
- 支持单帧/动图/视频文件导出
|
||||
- 支持自动分辨率批量导出
|
||||
- 支持 FFmpeg 自定义导出
|
||||
- 支持程序参数保存
|
||||
- 支持文件后缀关联
|
||||
- 支持非 png 格式的纹理图片格式
|
||||
- 支持非 PNG 格式的纹理图片格式
|
||||
- 支持开机自启常驻动态壁纸
|
||||
- ......
|
||||
|
||||
### Spine 版本支持
|
||||
|
||||
| 版本 | 查看&导出 |
|
||||
| :---: | :---: |
|
||||
| `2.1.x` | :white_check_mark: |
|
||||
| `3.4.x` | :white_check_mark: |
|
||||
| `3.5.x` | :white_check_mark: |
|
||||
| `3.6.x` | :white_check_mark: |
|
||||
| `3.7.x` | :white_check_mark: |
|
||||
| `3.8.x` | :white_check_mark: |
|
||||
| `4.0.x` | :white_check_mark: |
|
||||
| `4.1.x` | :white_check_mark: |
|
||||
| `4.2.x` | :white_check_mark: |
|
||||
| `4.3.x` | |
|
||||
|
||||
更多版本正在施工 :rocket: :rocket: :rocket:
|
||||
|
||||
### 导出格式支持
|
||||
|
||||
| 导出格式 | 适用场景 |
|
||||
| --- | --- |
|
||||
| 单帧画面 | 支持生成高清模型画面图像, 可手动调节需要的一帧. |
|
||||
| 帧序列 | 支持 PNG 格式帧序列, 可保留透明通道且无损压缩. |
|
||||
| 动图/视频 | 可以生成预览动图或者常见格式视频. |
|
||||
| 自定义导出 | 除上述预设方案, 支持提供任意 FFmpeg 参数进行导出, 满足自定义复杂需求. |
|
||||
|
||||
## 安装
|
||||
|
||||
前往 [Release](https://github.com/ww-rm/SpineViewer/releases) 界面下载压缩包.
|
||||
@@ -73,20 +39,26 @@ https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0
|
||||
|
||||
导出 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).
|
||||
|
||||
## 修改显示语言
|
||||
|
||||
本项目目前支持以下界面显示语言:
|
||||
|
||||
- `ZH` (中文)
|
||||
- `EN` (English)
|
||||
- `JA` (日本語)
|
||||
|
||||
可以通过窗口菜单的 "文件" -> "首选项..." -> "语言", 选择你需要的语言并确认修改.
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 如何修改显示语言
|
||||
|
||||
窗口菜单的 "文件" -> "首选项..." -> "语言", 选择你需要的语言并确认修改.
|
||||
|
||||
### 基本介绍
|
||||
|
||||
程序大致是左右布局, 左侧是功能面板, 右侧是画面.
|
||||
|
||||
左侧有三个子面板, 分别是:
|
||||
|
||||
- **浏览**. 该面板用于预览指定文件夹的内容, 并没有真正导入文件到程序. 在该面板可以为模型生成 webp 格式的预览图, 或者导入选中的模型.
|
||||
- **模型**. 该面板记录导入并进行渲染的模型列表, 可以在这个面板设置与模型渲染相关的参数和渲染顺序, 以及一些与模型有关的功能.
|
||||
- **浏览**. 该面板用于预览指定文件夹的内容, 并没有真正导入文件到程序. 在该面板可以为模型生成 webp 格式的预览图, 或者导入选中的模型.
|
||||
- **画面**. 该面板用于设置右侧预览画面的参数.
|
||||
|
||||
绝大部分按钮或者标签或者输入框都可以通过鼠标指针悬停来获取帮助文本.
|
||||
@@ -105,16 +77,14 @@ https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0
|
||||
|
||||
- 左键可以选择和拖拽模型, 按下 `Ctrl` 键可以实现多选, 与左侧列表选择是联动的.
|
||||
- 右键对整体画面进行拖动.
|
||||
- 滚轮进行画面缩放, 按住 `Ctrl` 可以对选中的模型进行批量缩放.
|
||||
- 滚轮进行画面缩放, 按住 `Ctrl` 可以对选中的模型进行批量缩放, `Shift` 可以切换缩放倍数.
|
||||
- 仅渲染选中模式, 在该模式下, 预览画面仅包含被选中的模型, 并且只能通过左侧列表改变选中状态.
|
||||
|
||||
预览画面下方按钮支持对画面时间进行调整, 可以当作一个简易的播放器.
|
||||
|
||||
### 内容导出
|
||||
|
||||
导出遵循 "所见即所得" 原则, 即实时预览的画面就是你导出的画面.
|
||||
|
||||
在模型面板里, 右键菜单可以对选中项进行导出操作.
|
||||
在模型列表里, 右键单击选中的模型, 弹出菜单里可以对选中项执行导出操作.
|
||||
|
||||
导出有以下几个关键参数:
|
||||
|
||||
@@ -130,6 +100,29 @@ https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0
|
||||
|
||||
如果希望开机自启常驻壁纸, 也可以在首选项中启用开机自启, 并且设置启动后需要加载的工作区文件.
|
||||
|
||||
### 命令行工具
|
||||
|
||||
项目附带一个纯命令行工具 `SpineViewerCLI`, 目前支持对单个模型执行一些简单操作, 例如参数值查询以及导出等, 并且 Release 界面提供 Windows 和 Linux 多平台二进制文件.
|
||||
|
||||
```bash
|
||||
$ SpineViewerCLI -h
|
||||
Description:
|
||||
Root Command
|
||||
|
||||
Usage:
|
||||
SpineViewerCLI [command] [options]
|
||||
|
||||
Options:
|
||||
-q, --quiet Suppress console logging (quiet mode).
|
||||
-?, -h, --help Show help and usage information
|
||||
--version Show version information
|
||||
|
||||
Commands:
|
||||
query <skel> Query information of single model
|
||||
preview <skel> Preview a model
|
||||
export <skel> Export single model
|
||||
```
|
||||
|
||||
### 更多
|
||||
|
||||
更为详细的使用方法和说明见 [Wiki](https://github.com/ww-rm/SpineViewer/wiki), 有使用上的问题或者 BUG 可以提个 [Issue](https://github.com/ww-rm/SpineViewer/issues).
|
||||
@@ -142,9 +135,10 @@ https://github.com/user-attachments/assets/37b6b730-088a-4352-827a-c338127a16f0
|
||||
- [HandyControl](https://github.com/HandyOrg/HandyControl)
|
||||
- [NLog](https://github.com/NLog/NLog)
|
||||
- [SkiaSharp](https://github.com/mono/SkiaSharp)
|
||||
- [Spectre.Console](https://github.com/spectreconsole/spectre.console)
|
||||
|
||||
---
|
||||
|
||||
*如果你觉得这个项目不错请给个 :star:, 并分享给更多人知道! :)*
|
||||
*如果你觉得这个项目不错请给个 :star:, 并分享给更多人知道! :\)*
|
||||
|
||||
[](https://starchart.cc/ww-rm/SpineViewer)
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SFMLRenderer
|
||||
{
|
||||
@@ -19,6 +20,14 @@ namespace SFMLRenderer
|
||||
SetActive(false);
|
||||
_timer.Tick += (s, e) => DispatchEvents();
|
||||
_timer.Start();
|
||||
|
||||
SetVisible(false);
|
||||
|
||||
var handle = SystemHandle;
|
||||
var exStyle = User32.GetWindowLong(handle, User32.GWL_EXSTYLE) | User32.WS_EX_LAYERED;
|
||||
User32.SetWindowLong(handle, User32.GWL_EXSTYLE, exStyle);
|
||||
User32.SetLayeredWindowAttributes(handle, 0, byte.MaxValue, User32.LWA_ALPHA);
|
||||
|
||||
RendererCreated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
@@ -19,4 +20,8 @@
|
||||
<PackageReference Include="SFML.Net" Version="2.6.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Win32Natives\Win32Natives.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace Spine.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewer.Extensions
|
||||
namespace Spine.Exporters
|
||||
{
|
||||
public static class SFMLExtension
|
||||
public static class Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
|
||||
@@ -59,25 +55,5 @@ namespace SpineViewer.Extensions
|
||||
self.Size.Y
|
||||
);
|
||||
}
|
||||
|
||||
public static FloatRect ToFloatRect(this Rect self)
|
||||
{
|
||||
return new((float)self.X, (float)self.Y, (float)self.Width, (float)self.Height);
|
||||
}
|
||||
|
||||
public static Vector2f ToVector2f(this Size self)
|
||||
{
|
||||
return new((float)self.Width, (float)self.Height);
|
||||
}
|
||||
|
||||
public static Vector2u ToVector2u(this Size self)
|
||||
{
|
||||
return new((uint)self.Width, (uint)self.Height);
|
||||
}
|
||||
|
||||
public static Vector2i ToVector2i(this Size self)
|
||||
{
|
||||
return new((int)self.Width, (int)self.Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,33 @@ namespace Spine.Exporters
|
||||
Mov,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apng 格式预测器算法
|
||||
/// </summary>
|
||||
public enum ApngPredMethod
|
||||
{
|
||||
None = 0,
|
||||
Sub = 1,
|
||||
Up = 2,
|
||||
Avg = 3,
|
||||
Paeth = 4,
|
||||
Mixed = 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mov prores_ks 编码器 profile 参数
|
||||
/// </summary>
|
||||
public enum MovProfile
|
||||
{
|
||||
Auto = -1,
|
||||
Proxy = 0,
|
||||
Light = 1,
|
||||
Standard = 2,
|
||||
High = 3,
|
||||
Yuv4444 = 4,
|
||||
Yuv4444Extreme = 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视频格式
|
||||
/// </summary>
|
||||
@@ -60,10 +87,10 @@ namespace Spine.Exporters
|
||||
private bool _lossless = false;
|
||||
|
||||
/// <summary>
|
||||
/// [Apng] 预测器算法, 取值范围 0-5, 分别对应 none, sub, up, avg, paeth, mixed
|
||||
/// [Apng] 预测器算法
|
||||
/// </summary>
|
||||
public int ApngPred { get => _apngPred; set => _apngPred = Math.Clamp(value, 0, 5); }
|
||||
private int _apngPred = 5;
|
||||
public ApngPredMethod PredMethod { get => _predMethod; set => _predMethod = value; }
|
||||
private ApngPredMethod _predMethod = ApngPredMethod.Mixed;
|
||||
|
||||
/// <summary>
|
||||
/// [Mp4/Webm/Mkv] CRF
|
||||
@@ -72,10 +99,10 @@ namespace Spine.Exporters
|
||||
private int _crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// [Mov] prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道
|
||||
/// [Mov] prores_ks 编码器的配置等级, 越高质量越好, 只有 <see cref="MovProfile.Yuv4444"> 及以上才有透明通道
|
||||
/// </summary>
|
||||
public int Profile { get => _profile; set => _profile = Math.Clamp(value, -1, 5); }
|
||||
private int _profile = 5;
|
||||
public MovProfile Profile { get => _profile; set => _profile = value; }
|
||||
private MovProfile _profile = MovProfile.Yuv4444Extreme;
|
||||
|
||||
/// <summary>
|
||||
/// 获取的一帧, 结果是预乘的
|
||||
@@ -117,7 +144,7 @@ namespace Spine.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0} {1}, {2}", _format, output, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -142,7 +169,7 @@ namespace Spine.Exporters
|
||||
|
||||
private void SetApngOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
var customArgs = $"-vf unpremultiply=inplace=1 -plays {(_loop ? 0 : 1)} -pred {_apngPred}";
|
||||
var customArgs = $"-vf unpremultiply=inplace=1 -plays {(_loop ? 0 : 1)} -pred {(int)_predMethod}";
|
||||
options.ForceFormat("apng").WithVideoCodec("apng").ForcePixelFormat("rgba")
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
@@ -179,7 +206,7 @@ namespace Spine.Exporters
|
||||
var customArgs = "-vf unpremultiply=inplace=1";
|
||||
options.ForceFormat("mov").WithVideoCodec("prores_ks").ForcePixelFormat("yuva444p10le")
|
||||
.WithFastStart()
|
||||
.WithCustomArgument($"-profile {_profile}")
|
||||
.WithCustomArgument($"-profile {(int)_profile}")
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,27 @@ namespace Spine.Exporters
|
||||
public FrameExporter(uint width = 100, uint height = 100) : base(width, height) { }
|
||||
public FrameExporter(Vector2u resolution) : base(resolution) { }
|
||||
|
||||
public SKEncodedImageFormat Format { get => _format; set => _format = value; }
|
||||
public SKEncodedImageFormat Format
|
||||
{
|
||||
get => _format;
|
||||
set {
|
||||
switch (value)
|
||||
{
|
||||
case SKEncodedImageFormat.Jpeg:
|
||||
case SKEncodedImageFormat.Png:
|
||||
case SKEncodedImageFormat.Webp:
|
||||
_format = value;
|
||||
break;
|
||||
default:
|
||||
_logger.Warn("Omit unsupported exporter format: {0}", value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;
|
||||
|
||||
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
|
||||
protected int _quality = 80;
|
||||
protected int _quality = 100;
|
||||
|
||||
public override void Export(string output, params SpineObject[] spines)
|
||||
{
|
||||
@@ -33,5 +49,15 @@ namespace Spine.Exporters
|
||||
using var stream = File.OpenWrite(output);
|
||||
data.SaveTo(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取帧图像, 结果是预乘的
|
||||
/// </summary>
|
||||
public SKImage ExportMemoryImage(params SpineObject[] spines)
|
||||
{
|
||||
using var frame = GetFrame(spines);
|
||||
var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
|
||||
return SKImage.FromPixelCopy(info, frame.Image.Pixels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Spine.Exporters
|
||||
int frameCount = GetFrameCount();
|
||||
int frameIdx = 0;
|
||||
|
||||
_progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
|
||||
_progressReporter?.Invoke(frameCount, 0, $"[0/{frameCount}] {output}"); // 导出帧序列单独在此处调用进度报告
|
||||
foreach (var frame in GetFrames(spines))
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
@@ -37,7 +37,7 @@ namespace Spine.Exporters
|
||||
var savePath = Path.Combine(output, $"frame_{_fps}_{frameIdx:d6}.png");
|
||||
var info = new SKImageInfo(frame.Width, frame.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
|
||||
|
||||
_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {savePath}");
|
||||
_progressReporter?.Invoke(frameCount, frameIdx + 1, $"[{frameIdx + 1}/{frameCount}] {savePath}");
|
||||
try
|
||||
{
|
||||
using var skImage = SKImage.FromPixelCopy(info, frame.Image.Pixels);
|
||||
@@ -47,7 +47,7 @@ namespace Spine.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to save frame {0}, {1}", savePath, ex.Message);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace Spine.Exporters
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成帧序列
|
||||
/// 生成帧序列, 用于导出帧序列
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines)
|
||||
{
|
||||
@@ -121,14 +121,14 @@ namespace Spine.Exporters
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成帧序列, 支持中途取消和进度输出
|
||||
/// 生成帧序列, 支持中途取消和进度输出, 用于动图视频等单个文件输出
|
||||
/// </summary>
|
||||
protected IEnumerable<SFMLImageVideoFrame> GetFrames(SpineObject[] spines, string output, CancellationToken ct)
|
||||
{
|
||||
int frameCount = GetFrameCount();
|
||||
int frameIdx = 0;
|
||||
|
||||
_progressReporter?.Invoke(frameCount, 0, $"[{frameIdx}/{frameCount}] {output}");
|
||||
_progressReporter?.Invoke(frameCount, 0, $"[0/{frameCount}] {output}");
|
||||
foreach (var frame in GetFrames(spines))
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
@@ -138,7 +138,7 @@ namespace Spine.Exporters
|
||||
break;
|
||||
}
|
||||
|
||||
_progressReporter?.Invoke(frameCount, frameIdx, $"[{frameIdx + 1}/{frameCount}] {output}");
|
||||
_progressReporter?.Invoke(frameCount, frameIdx + 1, $"[{frameIdx + 1}/{frameCount}] {output}");
|
||||
yield return frame;
|
||||
frameIdx++;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Spine.Implementations.V21
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Spine.Implementations.V21
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Spine.Implementations.V21
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Spine.Implementations.V21
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Spine.Implementations.V34
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Spine.Implementations.V34
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Spine.Implementations.V34
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Spine.Implementations.V34
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Spine.Implementations.V35
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Spine.Implementations.V35
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Spine.Implementations.V35
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Spine.Implementations.V35
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Spine.Implementations.V36
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Spine.Implementations.V36
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Spine.Implementations.V36
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Spine.Implementations.V36
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Spine.Implementations.V37
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Spine.Implementations.V37
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Spine.Implementations.V37
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Spine.Implementations.V37
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Spine.Implementations.V38
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Spine.Implementations.V38
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ namespace Spine.Implementations.V38
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ namespace Spine.Implementations.V38
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Spine.Implementations.V40
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Spine.Implementations.V40
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Spine.Implementations.V40
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Spine.Implementations.V40
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Spine.Implementations.V41
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Spine.Implementations.V41
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Spine.Implementations.V41
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Spine.Implementations.V41
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Spine.Implementations.V42
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load atlas '{atlasPath}'");
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Spine.Implementations.V42
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonBinary(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ namespace Spine.Implementations.V42
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_skeletonData = new SkeletonJson(_atlas).ReadSkeletonData(skelPath);
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Spine.Implementations.V42
|
||||
catch (Exception ex)
|
||||
{
|
||||
_atlas.Dispose();
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load skeleton file {skelPath}");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.16.8</Version>
|
||||
<Version>0.16.11</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -21,6 +22,10 @@
|
||||
<PackageReference Include="SkiaSharp" Version="3.119.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' Or '$(RuntimeIdentifier)' == 'linux-arm64'">
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SpineRuntimes\SpineRuntime21\SpineRuntime21.csproj" />
|
||||
<ProjectReference Include="..\SpineRuntimes\SpineRuntime34\SpineRuntime34.csproj" />
|
||||
|
||||
@@ -269,7 +269,7 @@ namespace Spine
|
||||
|
||||
if (hit && LogHitSlots)
|
||||
{
|
||||
_logger.Debug("Hit ({0}): [{1}]", self.Name, hitSlotName);
|
||||
_logger.Info("Hit ({0}): [{1}]", self.Name, hitSlotName);
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Spine
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Warn("Failed to detect version for skel {0}, try all available versions", skelPath);
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ namespace Spine
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
throw new InvalidDataException($"Failed to load spine with version '{version}'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Spine.Utils
|
||||
/// </summary>
|
||||
private const string FRAGMENT_VertexAlpha =
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
|
||||
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
|
||||
"p.rgb *= p.a * gl_Color.a;" +
|
||||
"gl_FragColor = gl_Color * p; }"
|
||||
;
|
||||
@@ -24,7 +24,7 @@ namespace Spine.Utils
|
||||
/// </summary>
|
||||
private const string FRAGMENT_VertexAlphaPma =
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
|
||||
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
|
||||
"p.rgb *= gl_Color.a;" +
|
||||
"gl_FragColor = gl_Color * p; }"
|
||||
;
|
||||
@@ -34,8 +34,8 @@ namespace Spine.Utils
|
||||
/// </summary>
|
||||
private const string FRAGMENT_InvPma =
|
||||
"uniform sampler2D t;" +
|
||||
"void main() { vec4 p = texture(t, gl_TexCoord[0].xy);" +
|
||||
"if (p.a > 0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
|
||||
"void main() { vec4 p = texture2D(t, gl_TexCoord[0].xy);" +
|
||||
"if (p.a > 0.0) p.rgb /= max(max(max(p.r, p.g), p.b), p.a);" +
|
||||
"gl_FragColor = p; }"
|
||||
;
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>2.1.25</Version>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.4.2</Version>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.5.51</Version>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.6.53</Version>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.7.94</Version>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>3.8.99</Version>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.0.64</Version>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.1.54</Version>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>4.2.74</Version>
|
||||
|
||||
@@ -43,6 +43,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime35", "SpineRunt
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime34", "SpineRuntimes\SpineRuntime34\SpineRuntime34.csproj", "{348605F7-3FF4-1DE0-4B91-7AEFE7BC5C55}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Win32Natives", "Win32Natives\Win32Natives.csproj", "{48864874-7307-950E-A667-62BB66357C62}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
@@ -105,6 +107,10 @@ Global
|
||||
{348605F7-3FF4-1DE0-4B91-7AEFE7BC5C55}.Debug|x64.Build.0 = Debug|x64
|
||||
{348605F7-3FF4-1DE0-4B91-7AEFE7BC5C55}.Release|x64.ActiveCfg = Release|x64
|
||||
{348605F7-3FF4-1DE0-4B91-7AEFE7BC5C55}.Release|x64.Build.0 = Release|x64
|
||||
{48864874-7307-950E-A667-62BB66357C62}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{48864874-7307-950E-A667-62BB66357C62}.Debug|x64.Build.0 = Debug|x64
|
||||
{48864874-7307-950E-A667-62BB66357C62}.Release|x64.ActiveCfg = Release|x64
|
||||
{48864874-7307-950E-A667-62BB66357C62}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Microsoft.Win32;
|
||||
using NLog;
|
||||
using SpineViewer.Natives;
|
||||
using Win32Natives;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
using SpineViewer.Views;
|
||||
using System.Collections.Frozen;
|
||||
@@ -14,6 +15,7 @@ using System.IO.Pipes;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using SpineViewer.Extensions;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
@@ -23,16 +25,18 @@ namespace SpineViewer
|
||||
public partial class App : Application
|
||||
{
|
||||
#if DEBUG
|
||||
public const bool IsDebug = true;
|
||||
public const string AppName = "SpineViewer_D";
|
||||
public const string ProgId = "SpineViewer_D.skel";
|
||||
#else
|
||||
public const bool IsDebug = false;
|
||||
public const string AppName = "SpineViewer";
|
||||
public const string ProgId = "SpineViewer.skel";
|
||||
#endif
|
||||
|
||||
public const string AutoRunFlag = "--autorun";
|
||||
private const string MutexName = "__SpineViewerInstance__";
|
||||
private const string PipeName = "__SpineViewerPipe__";
|
||||
private const string MutexName = $"__{AppName}_Instance__";
|
||||
private const string PipeName = $"_{AppName}_Pipe__";
|
||||
|
||||
public static readonly string ProcessPath = Environment.ProcessPath;
|
||||
public static readonly string ProcessDirectory = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
@@ -56,13 +60,16 @@ namespace SpineViewer
|
||||
|
||||
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
|
||||
{
|
||||
_logger.Debug(e.ExceptionObject.ToString());
|
||||
_logger.Fatal("Unhandled exception: {0}", e.ExceptionObject);
|
||||
MessagePopupService.Error(e.ExceptionObject.ToString());
|
||||
};
|
||||
TaskScheduler.UnobservedTaskException += (s, e) =>
|
||||
{
|
||||
_logger.Trace(e.Exception.ToString());
|
||||
_logger.Error("Unobserved task exception: {0}", e.Exception.Message);
|
||||
_logger.Debug(e.Exception.ToString());
|
||||
_logger.Fatal("Unobserved task exception: {0}", e.Exception.Message);
|
||||
e.SetObserved();
|
||||
MessagePopupService.Error(e.Exception.ToString());
|
||||
};
|
||||
|
||||
// 单例模式加 IPC 通信
|
||||
@@ -84,12 +91,14 @@ namespace SpineViewer
|
||||
var fileTarget = new NLog.Targets.FileTarget("fileTarget")
|
||||
{
|
||||
Encoding = System.Text.Encoding.UTF8,
|
||||
Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${processid} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}",
|
||||
AutoFlush = true,
|
||||
CreateDirs = true,
|
||||
FileName = "${basedir}/logs/app.log",
|
||||
ArchiveFileName = "${basedir}/logs/app.{#}.log",
|
||||
ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling,
|
||||
ArchiveAboveSize = 1048576,
|
||||
MaxArchiveFiles = 5,
|
||||
Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${processid} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}",
|
||||
ConcurrentWrites = true,
|
||||
KeepFileOpen = false,
|
||||
};
|
||||
@@ -126,7 +135,7 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to pass command line args to existed instance, {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -187,7 +196,7 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to process arguments, {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -208,9 +217,10 @@ namespace SpineViewer
|
||||
|
||||
private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
_logger.Trace(e.Exception.ToString());
|
||||
_logger.Error("Dispatcher unhandled exception: {0}", e.Exception.Message);
|
||||
_logger.Debug(e.Exception.ToString());
|
||||
_logger.Fatal("Dispatcher unhandled exception: {0}", e.Exception.Message);
|
||||
e.Handled = true;
|
||||
MessagePopupService.Error(e.Exception.ToString());
|
||||
}
|
||||
|
||||
public bool AutoRun
|
||||
@@ -227,7 +237,7 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to query autorun registry key, {0}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
@@ -255,7 +265,7 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to set autorun registry key, {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -339,7 +349,7 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to switch language to {0}, {1}", value, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -356,14 +366,13 @@ namespace SpineViewer
|
||||
{
|
||||
Resources.MergedDictionaries.Add(new() { Source = new(uri, UriKind.Relative) });
|
||||
Resources.MergedDictionaries.Add(new() { Source = new("Resources/Theme.xaml", UriKind.Relative) });
|
||||
var hwnd = new WindowInteropHelper(Current.MainWindow).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
Current.MainWindow.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
Current.MainWindow.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
_skin = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to switch skin to {0}, {1}", value, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,39 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Runtime.InteropServices;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Extensions
|
||||
{
|
||||
public static class WpfExtension
|
||||
{
|
||||
public static SFML.Graphics.FloatRect ToFloatRect(this Rect self)
|
||||
{
|
||||
return new((float)self.X, (float)self.Y, (float)self.Width, (float)self.Height);
|
||||
}
|
||||
|
||||
public static SFML.System.Vector2f ToVector2f(this Size self)
|
||||
{
|
||||
return new((float)self.Width, (float)self.Height);
|
||||
}
|
||||
|
||||
public static SFML.System.Vector2u ToVector2u(this Size self)
|
||||
{
|
||||
return new((uint)self.Width, (uint)self.Height);
|
||||
}
|
||||
|
||||
public static SFML.System.Vector2i ToVector2i(this Size self)
|
||||
{
|
||||
return new((int)self.Width, (int)self.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从本地 WebP 文件读取,并保留透明度,返回一个可以直接用于 WPF Image.Source 的 BitmapSource。
|
||||
/// </summary>
|
||||
@@ -38,6 +60,18 @@ namespace SpineViewer.Extensions
|
||||
return wb;
|
||||
}
|
||||
|
||||
public static void SetWindowTextColor(this Window self, Color color)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(self).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, color.R, color.G, color.B);
|
||||
}
|
||||
|
||||
public static void SetWindowCaptionColor(this Window self, Color color)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(self).Handle;
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, color.R, color.G, color.B);
|
||||
}
|
||||
|
||||
//public static void SaveToFile(this BitmapSource bitmap, string path)
|
||||
//{
|
||||
// var ext = Path.GetExtension(path)?.ToLowerInvariant();
|
||||
|
||||
@@ -73,6 +73,22 @@ namespace SpineViewer.Models
|
||||
|
||||
#endregion
|
||||
|
||||
#region 预览画面首选项
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _renderSelectedOnly;
|
||||
|
||||
[ObservableProperty]
|
||||
private HitTestLevel _hitTestLevel;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _logHitSlots;
|
||||
|
||||
[ObservableProperty]
|
||||
private uint _maxFps = 30;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 程序选项
|
||||
|
||||
public RelayCommand Cmd_SelectAutoRunWorkspaceConfigPath => _cmd_SelectAutoRunWorkspaceConfigPath ??= new(() =>
|
||||
@@ -89,18 +105,12 @@ namespace SpineViewer.Models
|
||||
[ObservableProperty]
|
||||
private AppSkin _appSkin;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _renderSelectedOnly;
|
||||
|
||||
[ObservableProperty]
|
||||
private HitTestLevel _hitTestLevel;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _logHitSlots;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _wallpaperView;
|
||||
|
||||
[ObservableProperty]
|
||||
private uint _wallpaperMaxFps = 30;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _closeToTray;
|
||||
|
||||
|
||||
@@ -33,11 +33,16 @@ namespace SpineViewer.Models
|
||||
|
||||
#endregion
|
||||
|
||||
#region 浏览页面状态
|
||||
|
||||
public string? ExploringDirectory { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 预览画面状态
|
||||
|
||||
public uint ResolutionX { get; set; } = 1500;
|
||||
public uint ResolutionY { get; set; } = 1000;
|
||||
public uint MaxFps { get; set; } = 30;
|
||||
public float Speed { get; set; } = 1f;
|
||||
public bool ShowAxis { get; set; } = true;
|
||||
public Color BackgroundColor { get; set; } = Color.FromRgb(105, 105, 105);
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace SpineViewer.Models
|
||||
{
|
||||
public class WorkspaceModel
|
||||
{
|
||||
public string? ExploringDirectory { get; set; }
|
||||
public RendererWorkspaceConfigModel RendererConfig { get; set; } = new();
|
||||
public List<SpineObjectWorkspaceConfigModel> LoadedSpineObjects { get; set; } = [];
|
||||
}
|
||||
@@ -35,8 +34,6 @@ namespace SpineViewer.Models
|
||||
|
||||
public bool FlipY { get; set; } = true;
|
||||
|
||||
public uint MaxFps { get; set; } = 30;
|
||||
|
||||
public float Speed { get; set; } = 1f;
|
||||
|
||||
public bool ShowAxis { get; set; } = true;
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
<s:String x:Key="Str_MaxFpsTooltip">Maximum frame rate of the preview. Set to 0 for no limit.</s:String>
|
||||
<s:String x:Key="Str_PlaySpeed">Playback Speed</s:String>
|
||||
<s:String x:Key="Str_WallpaperView">Wallpaper View</s:String>
|
||||
<s:String x:Key="Str_WallpaperMaxFps">Max FPS of Wallpaper View</s:String>
|
||||
<s:String x:Key="Str_WallpaperMaxFpsTooltip">Maximum frame rate of the wallpaper view. Set to 0 for no limit.</s:String>
|
||||
<s:String x:Key="Str_RenderSelectedOnly">Render Selected Only</s:String>
|
||||
<s:String x:Key="Str_HitTestLevel">Hit Test Accuracy Level</s:String>
|
||||
<s:String x:Key="Str_LogHitSlots">Output Hit Test Slot Names</s:String>
|
||||
@@ -140,6 +142,9 @@
|
||||
<s:String x:Key="Str_ForwardFastTooltip">Forward 10 Frames</s:String>
|
||||
<s:String x:Key="Str_FullScreenTooltip">Window/Fullscreen; F11</s:String>
|
||||
|
||||
<!-- 日志框下方附加信息 -->
|
||||
<s:String x:Key="Str_RealTimeFps">Real-time FPS: {0:F1}/{1:F1}</s:String>
|
||||
|
||||
<!-- 弹窗文本 -->
|
||||
<s:String x:Key="Str_OK">OK</s:String>
|
||||
<s:String x:Key="Str_Cancel">Cancel</s:String>
|
||||
@@ -206,12 +211,12 @@
|
||||
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]
Quality parameter, range 0-100, higher value means better quality</s:String>
|
||||
<s:String x:Key="Str_LosslessParam">Lossless Compression</s:String>
|
||||
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]
Lossless compression, quality parameter will be ignored</s:String>
|
||||
<s:String x:Key="Str_ApngPred">Predictor Method</s:String>
|
||||
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]
Pred parameter, value range 0-5, corresponding to different encoding strategies: none, sub, up, avg, paeth, and mixed.
It affects encoding time and file size.</s:String>
|
||||
<s:String x:Key="Str_PredMethod">Predictor Method</s:String>
|
||||
<s:String x:Key="Str_PredMethodTooltip" xml:space="preserve">[Apng]
Pred parameter, value range 0-5, corresponding to different encoding strategies: none, sub, up, avg, paeth, and mixed.
It affects encoding time and file size.</s:String>
|
||||
<s:String x:Key="Str_CrfParameter">CRF Parameter</s:String>
|
||||
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]
CRF parameter, range 0-63, lower value means higher quality</s:String>
|
||||
<s:String x:Key="Str_ProfileParameter">Profile Parameter</s:String>
|
||||
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]
Profile parameter, integer between -1 and 5,
-1 means automatic, higher values indicate higher quality,
Alpha channel encoding is only available when value is 4 or higher</s:String>
|
||||
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]
Profile parameter, an integer between -1 and 5,
corresponding to: auto, proxy, lt, standard, hq, 4444, and 4444xq.
Alpha channel encoding is available only when the value is 4 or higher.</s:String>
|
||||
|
||||
<s:String x:Key="Str_FFmpegFormat">Export Format</s:String>
|
||||
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpeg export format (equivalent to "-f"), e.g. "mp4", "webm"</s:String>
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
<s:String x:Key="Str_MaxFpsTooltip">プレビュー画面の最大フレームレート。0 に設定すると制限なし。</s:String>
|
||||
<s:String x:Key="Str_PlaySpeed">再生速度</s:String>
|
||||
<s:String x:Key="Str_WallpaperView">壁紙表示</s:String>
|
||||
<s:String x:Key="Str_WallpaperMaxFps">壁紙ビューの最大FPS</s:String>
|
||||
<s:String x:Key="Str_WallpaperMaxFpsTooltip">壁紙ビューの最大フレームレート。0に設定すると制限がなし。</s:String>
|
||||
<s:String x:Key="Str_RenderSelectedOnly">選択のみレンダリング</s:String>
|
||||
<s:String x:Key="Str_HitTestLevel">ヒットテスト精度レベル</s:String>
|
||||
<s:String x:Key="Str_LogHitSlots">ヒットテスト結果のスロット名を出力</s:String>
|
||||
@@ -140,6 +142,9 @@
|
||||
<s:String x:Key="Str_ForwardFastTooltip">10フレーム進める</s:String>
|
||||
<s:String x:Key="Str_FullScreenTooltip">ウィンドウ/フルスクリーン; F11</s:String>
|
||||
|
||||
<!-- 日志框下方附加信息 -->
|
||||
<s:String x:Key="Str_RealTimeFps">リアルタイムFPS:{0:F1}/{1:F1}</s:String>
|
||||
|
||||
<!-- 弹窗文本 -->
|
||||
<s:String x:Key="Str_OK">OK</s:String>
|
||||
<s:String x:Key="Str_Cancel">キャンセル</s:String>
|
||||
@@ -206,12 +211,12 @@
|
||||
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]
品質パラメータ、範囲は0-100。値が高いほど品質が良い</s:String>
|
||||
<s:String x:Key="Str_LosslessParam">無損失圧縮</s:String>
|
||||
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]
無損失圧縮、品質パラメータは無視されます</s:String>
|
||||
<s:String x:Key="Str_ApngPred">予測器方式</s:String>
|
||||
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]
Pred パラメータ。値の範囲は 0~5 で、それぞれ none、sub、up、avg、paeth、mixed の異なるエンコード方式に対応します。
エンコード時間とファイルサイズに影響します。</s:String>
|
||||
<s:String x:Key="Str_PredMethod">予測器方式</s:String>
|
||||
<s:String x:Key="Str_PredMethodTooltip" xml:space="preserve">[Apng]
Pred パラメータ。値の範囲は 0~5 で、それぞれ none、sub、up、avg、paeth、mixed の異なるエンコード方式に対応します。
エンコード時間とファイルサイズに影響します。</s:String>
|
||||
<s:String x:Key="Str_CrfParameter">CRF パラメータ</s:String>
|
||||
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]
CRF パラメータ、範囲0-63。値が小さいほど品質が高い</s:String>
|
||||
<s:String x:Key="Str_ProfileParameter">プロファイルパラメータ</s:String>
|
||||
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]
プロファイルパラメータ、-1から5の整数、
-1は自動、値が大きいほど品質が高い、
値が4以上の場合のみアルファチャンネルをエンコード可能</s:String>
|
||||
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]
Profile パラメータ。値は -1 ~ 5 の整数で、
それぞれ auto、proxy、lt、standard、hq、4444、4444xq に対応します。
値が 4 以上の場合のみアルファチャンネルのエンコードが可能です。</s:String>
|
||||
|
||||
<s:String x:Key="Str_FFmpegFormat">エクスポートフォーマット</s:String>
|
||||
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpegエクスポートフォーマット。パラメーター“-f”に相当します。例: “mp4”、“webm”</s:String>
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
<s:String x:Key="Str_MaxFpsTooltip">预览画面的最大帧率,设置为 0 时则无帧率限制</s:String>
|
||||
<s:String x:Key="Str_PlaySpeed">播放速度</s:String>
|
||||
<s:String x:Key="Str_WallpaperView">桌面投影</s:String>
|
||||
<s:String x:Key="Str_WallpaperMaxFps">桌面投影最大帧率</s:String>
|
||||
<s:String x:Key="Str_WallpaperMaxFpsTooltip">桌面投影的最大帧率,设置为 0 时则无帧率限制</s:String>
|
||||
<s:String x:Key="Str_RenderSelectedOnly">仅渲染选中</s:String>
|
||||
<s:String x:Key="Str_HitTestLevel">命中检测准确度等级</s:String>
|
||||
<s:String x:Key="Str_LogHitSlots">输出命中检测结果的插槽名称</s:String>
|
||||
@@ -140,6 +142,9 @@
|
||||
<s:String x:Key="Str_ForwardFastTooltip">快进 10 帧</s:String>
|
||||
<s:String x:Key="Str_FullScreenTooltip">窗口/全屏; F11</s:String>
|
||||
|
||||
<!-- 日志框下方附加信息 -->
|
||||
<s:String x:Key="Str_RealTimeFps">实时帧率:{0:F1}/{1:F1}</s:String>
|
||||
|
||||
<!-- 弹窗文本 -->
|
||||
<s:String x:Key="Str_OK">确认</s:String>
|
||||
<s:String x:Key="Str_Cancel">取消</s:String>
|
||||
@@ -206,12 +211,12 @@
|
||||
<s:String x:Key="Str_QualityParameterTooltip" xml:space="preserve">[Webp]
质量参数,取值范围 0-100,越高质量越好</s:String>
|
||||
<s:String x:Key="Str_LosslessParam">无损压缩</s:String>
|
||||
<s:String x:Key="Str_LosslessParamTooltip" xml:space="preserve">[Webp]
无损压缩,会忽略质量参数</s:String>
|
||||
<s:String x:Key="Str_ApngPred">预测器方法</s:String>
|
||||
<s:String x:Key="Str_ApngPredTooltip" xml:space="preserve">[Apng]
Pred 参数,取值范围 0-5,分别对应 none、sub、up、avg、paeth、mixed 几种不同的编码策略,
影响编码时间和文件大小</s:String>
|
||||
<s:String x:Key="Str_PredMethod">预测器方法</s:String>
|
||||
<s:String x:Key="Str_PredMethodTooltip" xml:space="preserve">[Apng]
Pred 参数,取值范围 0-5,分别对应 none、sub、up、avg、paeth、mixed 几种不同的编码策略,
影响编码时间和文件大小</s:String>
|
||||
<s:String x:Key="Str_CrfParameter">CRF 参数</s:String>
|
||||
<s:String x:Key="Str_CrfParameterTooltip" xml:space="preserve">[Mp4/Webm/Mkv]
CRF 参数,取值范围 0-63,越小质量越高</s:String>
|
||||
<s:String x:Key="Str_ProfileParameter">Profile 参数</s:String>
|
||||
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]
Profile 参数,取值集合为 -1 到 5 之间的整数,
-1 表示自动,0-5 取值越高质量越高,
仅在取值大于等于 4 时可以编码透明度通道</s:String>
|
||||
<s:String x:Key="Str_ProfileParameterTooltip" xml:space="preserve">[Mov]
Profile 参数,取值范围为 -1 到 5 之间的整数,
分别对应 auto、proxy、lt、standard、hq、4444、4444xq 几种配置,
仅在取值大于等于 4 时可以编码透明度通道</s:String>
|
||||
|
||||
<s:String x:Key="Str_FFmpegFormat">导出格式</s:String>
|
||||
<s:String x:Key="Str_FFmpegFormatTooltip">FFmpeg 导出格式,等价于参数 “-f”,例如 “mp4”、“webm”</s:String>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<utils:StringFormatMultiValueConverter x:Key="StrFmtCvter"/>
|
||||
<utils:BackgroundToForegroundConverter x:Key="Bg2FgCvter"/>
|
||||
|
||||
<Style x:Key="MyGridSplitterBaseStyle" TargetType="{x:Type GridSplitter}">
|
||||
<Style x:Key="MyGridSplitterBaseStyle" TargetType="GridSplitter">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryBorderBrush}"/>
|
||||
<Setter Property="ShowsPreview" Value="False"/>
|
||||
<Style.Triggers>
|
||||
@@ -28,17 +28,17 @@
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyToggleButtonBaseStyle" TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource ToggleButtonSwitch}">
|
||||
<Style x:Key="MyToggleButtonBaseStyle" TargetType="ToggleButton" BasedOn="{StaticResource ToggleButtonSwitch}">
|
||||
<Setter Property="hc:VisualElement.HighlightBrush" Value="{DynamicResource DarkSuccessBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyListBoxBaseStyle" TargetType="{x:Type ListBox}" BasedOn="{StaticResource ListBoxBaseStyle}">
|
||||
<Style x:Key="MyListBoxBaseStyle" TargetType="ListBox" BasedOn="{StaticResource ListBoxBaseStyle}">
|
||||
<Setter Property="SelectionMode" Value="Extended"/>
|
||||
<!--<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>-->
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Visible"/>
|
||||
<Setter Property="ItemContainerStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource ListBoxItemBaseStyle}">
|
||||
<Style TargetType="ListBoxItem" BasedOn="{StaticResource ListBoxItemBaseStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
@@ -47,26 +47,26 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyListViewBaseStyle" TargetType="{x:Type ListView}" BasedOn="{StaticResource ListViewBaseStyle}">
|
||||
<Style x:Key="MyListViewBaseStyle" TargetType="ListView" BasedOn="{StaticResource ListViewBaseStyle}">
|
||||
<Setter Property="SelectionMode" Value="Extended"/>
|
||||
<!--<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>-->
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="ItemContainerStyle" Value="{StaticResource ListViewItemBaseStyle.Small}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyGroupBoxBaseStyle" TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
|
||||
<Style x:Key="MyGroupBoxBaseStyle" TargetType="GroupBox" BasedOn="{StaticResource GroupBoxTab}">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyLogRichTextBoxStyle" TargetType="{x:Type RichTextBox}" BasedOn="{StaticResource RichTextBoxBaseStyle}">
|
||||
<Style x:Key="MyLogRichTextBoxStyle" TargetType="RichTextBox" BasedOn="{StaticResource RichTextBoxBaseStyle}">
|
||||
<Setter Property="IsReadOnly" Value="True"/>
|
||||
<Setter Property="FontFamily" Value="Consolas"/>
|
||||
<Setter Property="Block.LineHeight" Value="3"/>
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Visible"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyVerticalScrollViewerBaseStyle" TargetType="{x:Type ScrollViewer}" BasedOn="{StaticResource ScrollViewerNativeBaseStyle}">
|
||||
<Style x:Key="MyVerticalScrollViewerBaseStyle" TargetType="ScrollViewer" BasedOn="{StaticResource ScrollViewerNativeBaseStyle}">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Style.Triggers>
|
||||
@@ -99,10 +99,24 @@
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type GridSplitter}" BasedOn="{StaticResource MyGridSplitterBaseStyle}"/>
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}"/>
|
||||
<Style TargetType="{x:Type ListBox}" BasedOn="{StaticResource MyListBoxBaseStyle}"/>
|
||||
<Style TargetType="{x:Type ListView}" BasedOn="{StaticResource MyListViewBaseStyle}"/>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}"/>
|
||||
<Style x:Key="MyLabelStyle" TargetType="Label" BasedOn="{StaticResource LabelDefault}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Label">
|
||||
<Border CornerRadius="{Binding Path=(hc:BorderElement.CornerRadius),RelativeSource={RelativeSource TemplatedParent}}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
|
||||
<!-- 直接复制的原本 LabelDefault 的样式, 但是去除了 RecognizesAccessKey 防止不显示第一个下划线 -->
|
||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" RecognizesAccessKey="False" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="GridSplitter" BasedOn="{StaticResource MyGridSplitterBaseStyle}"/>
|
||||
<Style TargetType="ToggleButton" BasedOn="{StaticResource MyToggleButtonBaseStyle}"/>
|
||||
<Style TargetType="ListBox" BasedOn="{StaticResource MyListBoxBaseStyle}"/>
|
||||
<Style TargetType="ListView" BasedOn="{StaticResource MyListViewBaseStyle}"/>
|
||||
<Style TargetType="GroupBox" BasedOn="{StaticResource MyGroupBoxBaseStyle}"/>
|
||||
<Style TargetType="Label" BasedOn="{StaticResource MyLabelStyle}"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.16.8</Version>
|
||||
<Version>0.16.12</Version>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
@@ -40,5 +41,6 @@
|
||||
<ProjectReference Include="..\NLog.Windows.Wpf\NLog.Windows.Wpf.csproj" />
|
||||
<ProjectReference Include="..\SFMLRenderer\SFMLRenderer.csproj" />
|
||||
<ProjectReference Include="..\Spine\Spine.csproj" />
|
||||
<ProjectReference Include="..\Win32Natives\Win32Natives.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace SpineViewer.Utils
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to read json file {0}, {1}", path, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ namespace SpineViewer.Utils
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to save json file {0}, {1}", path, ex.Message);
|
||||
return false;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ namespace SpineViewer.Utils
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to serialize json object {0}", ex.Message);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
|
||||
}
|
||||
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
|
||||
@@ -169,7 +169,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
public class FFmpegVideoExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
|
||||
{
|
||||
public static ImmutableArray<FFmpegVideoExporter.VideoFormat> VideoFormatOptions { get; } = Enum.GetValues<FFmpegVideoExporter.VideoFormat>().ToImmutableArray();
|
||||
public static ImmutableArray<FFmpegVideoExporter.ApngPredMethod> ApngPredMethodOptions { get; } = Enum.GetValues<FFmpegVideoExporter.ApngPredMethod>().ToImmutableArray();
|
||||
public static ImmutableArray<FFmpegVideoExporter.MovProfile> MovProfileOptions { get; } = Enum.GetValues<FFmpegVideoExporter.MovProfile>().ToImmutableArray();
|
||||
|
||||
public FFmpegVideoExporter.VideoFormat Format
|
||||
{
|
||||
@@ -57,8 +59,8 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
public bool EnableParamLossless =>
|
||||
_format == FFmpegVideoExporter.VideoFormat.Webp;
|
||||
|
||||
public int ApngPred { get => _apngPred; set => SetProperty(ref _apngPred, Math.Clamp(value, 0, 5)); }
|
||||
protected int _apngPred = 5;
|
||||
public FFmpegVideoExporter.ApngPredMethod PredMethod { get => _predMethod; set => SetProperty(ref _predMethod, value); }
|
||||
protected FFmpegVideoExporter.ApngPredMethod _predMethod = FFmpegVideoExporter.ApngPredMethod.Mixed;
|
||||
|
||||
public bool EnableParamApngPred =>
|
||||
_format == FFmpegVideoExporter.VideoFormat.Apng;
|
||||
@@ -71,8 +73,8 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
_format == FFmpegVideoExporter.VideoFormat.Webm ||
|
||||
_format == FFmpegVideoExporter.VideoFormat.Mkv;
|
||||
|
||||
public int Profile { get => _profile; set => SetProperty(ref _profile, Math.Clamp(value, -1, 5)); }
|
||||
protected int _profile = 5;
|
||||
public FFmpegVideoExporter.MovProfile Profile { get => _profile; set => SetProperty(ref _profile, value); }
|
||||
protected FFmpegVideoExporter.MovProfile _profile = FFmpegVideoExporter.MovProfile.Yuv4444Extreme;
|
||||
|
||||
public bool EnableParamProfile =>
|
||||
_format == FFmpegVideoExporter.VideoFormat.Mov;
|
||||
@@ -102,7 +104,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
Loop = _loop,
|
||||
Quality = _quality,
|
||||
Lossless = _lossless,
|
||||
ApngPred = _apngPred,
|
||||
PredMethod = _predMethod,
|
||||
Crf = _crf,
|
||||
Profile = _profile,
|
||||
};
|
||||
@@ -147,7 +149,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
|
||||
}
|
||||
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
|
||||
@@ -204,7 +206,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,17 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
public class FrameExporterViewModel(MainWindowViewModel vmMain) : BaseExporterViewModel(vmMain)
|
||||
{
|
||||
public static ImmutableArray<SKEncodedImageFormat> FrameFormatOptions { get; } = Enum.GetValues<SKEncodedImageFormat>().ToImmutableArray();
|
||||
public static ImmutableArray<SKEncodedImageFormat> FrameFormatOptions { get; } = [
|
||||
SKEncodedImageFormat.Png,
|
||||
SKEncodedImageFormat.Webp,
|
||||
SKEncodedImageFormat.Jpeg,
|
||||
];
|
||||
|
||||
public SKEncodedImageFormat Format { get => _format; set => SetProperty(ref _format, value); }
|
||||
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;
|
||||
|
||||
public int Quality { get => _quality; set => SetProperty(ref _quality, Math.Clamp(value, 0, 100)); }
|
||||
protected int _quality = 80;
|
||||
protected int _quality = 100;
|
||||
|
||||
private string FormatSuffix
|
||||
{
|
||||
@@ -81,7 +85,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -117,7 +121,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
|
||||
}
|
||||
done++;
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
|
||||
}
|
||||
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
|
||||
@@ -133,7 +133,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to generate preview: {0}, {1}", m.PreviewFilePath, ex.Message);
|
||||
}
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
@@ -221,7 +221,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to generate preview: {0}, {1}", m.PreviewFilePath, ex.Message);
|
||||
error++;
|
||||
}
|
||||
@@ -261,7 +261,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to delete preview: {0}, {1}", m.PreviewFilePath, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -302,7 +302,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to delete preview: {0}, {1}", m.PreviewFilePath, ex.Message);
|
||||
error++;
|
||||
}
|
||||
@@ -340,7 +340,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to enumerate files in dir: {0}, {1}", _currentDirectory, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -408,7 +408,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Warn("Failed to load preview image for {0}, {1}", FullPath, ex.Message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,24 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
_preferenceViewModel = new(this);
|
||||
}
|
||||
|
||||
public string Title => $"SpineViewer - v{App.Version}";
|
||||
public bool IsDebug => App.IsDebug;
|
||||
|
||||
public string Title => $"{App.AppName} - v{App.Version}";
|
||||
|
||||
public Visibility Visibility
|
||||
{
|
||||
get => _visibility;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _visibility, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsVisible));
|
||||
}
|
||||
}
|
||||
}
|
||||
private Visibility _visibility = Visibility.Visible;
|
||||
|
||||
public bool IsVisible => _visibility == Visibility.Visible;
|
||||
|
||||
/// <summary>
|
||||
/// 指示是否通过托盘图标进行退出
|
||||
@@ -164,14 +181,12 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExploringDirectory = _explorerListViewModel.CurrentDirectory,
|
||||
RendererConfig = _sfmlRendererViewModel.WorkspaceConfig,
|
||||
LoadedSpineObjects = _spineObjectListViewModel.LoadedSpineObjects
|
||||
};
|
||||
}
|
||||
set
|
||||
{
|
||||
_explorerListViewModel.CurrentDirectory = value.ExploringDirectory;
|
||||
_sfmlRendererViewModel.WorkspaceConfig = value.RendererConfig;
|
||||
_spineObjectListViewModel.LoadedSpineObjects = value.LoadedSpineObjects;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using NLog;
|
||||
using Spine;
|
||||
using Spine.Implementations;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
@@ -76,7 +75,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to load some prefereneces, {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -108,12 +107,15 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
DebugPoints = DebugPoints,
|
||||
DebugClippings = DebugClippings,
|
||||
|
||||
AppLanguage = AppLanguage,
|
||||
AppSkin = AppSkin,
|
||||
RenderSelectedOnly = RenderSelectedOnly,
|
||||
HitTestLevel = HitTestLevel,
|
||||
LogHitSlots = LogHitSlots,
|
||||
MaxFps = MaxFps,
|
||||
|
||||
AppLanguage = AppLanguage,
|
||||
AppSkin = AppSkin,
|
||||
WallpaperView = WallpaperView,
|
||||
WallpaperMaxFps = WallpaperMaxFps,
|
||||
CloseToTray = CloseToTray,
|
||||
AutoRun = AutoRun,
|
||||
AutoRunWorkspaceConfigPath = AutoRunWorkspaceConfigPath,
|
||||
@@ -140,12 +142,15 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
DebugPoints = value.DebugPoints;
|
||||
DebugClippings = value.DebugClippings;
|
||||
|
||||
AppLanguage = value.AppLanguage;
|
||||
AppSkin = value.AppSkin;
|
||||
RenderSelectedOnly = value.RenderSelectedOnly;
|
||||
HitTestLevel = value.HitTestLevel;
|
||||
LogHitSlots = value.LogHitSlots;
|
||||
MaxFps = value.MaxFps;
|
||||
|
||||
AppLanguage = value.AppLanguage;
|
||||
AppSkin = value.AppSkin;
|
||||
WallpaperView = value.WallpaperView;
|
||||
WallpaperMaxFps = value.WallpaperMaxFps;
|
||||
CloseToTray = value.CloseToTray;
|
||||
AutoRun = value.AutoRun;
|
||||
AutoRunWorkspaceConfigPath = value.AutoRunWorkspaceConfigPath;
|
||||
@@ -251,26 +256,10 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
|
||||
#endregion
|
||||
|
||||
#region 程序选项
|
||||
|
||||
public static ImmutableArray<AppLanguage> AppLanguageOptions { get; } = Enum.GetValues<AppLanguage>().ToImmutableArray();
|
||||
|
||||
public static ImmutableArray<AppSkin> AppSkinOptions { get; } = Enum.GetValues<AppSkin>().ToImmutableArray();
|
||||
#region 预览画面首选项
|
||||
|
||||
public static ImmutableArray<HitTestLevel> HitTestLevelOptions { get; } = Enum.GetValues<HitTestLevel>().ToImmutableArray();
|
||||
|
||||
public AppLanguage AppLanguage
|
||||
{
|
||||
get => ((App)App.Current).Language;
|
||||
set => SetProperty(((App)App.Current).Language, value, v => ((App)App.Current).Language = v);
|
||||
}
|
||||
|
||||
public AppSkin AppSkin
|
||||
{
|
||||
get => ((App)App.Current).Skin;
|
||||
set => SetProperty(((App)App.Current).Skin, value, v => ((App)App.Current).Skin = v);
|
||||
}
|
||||
|
||||
public bool RenderSelectedOnly
|
||||
{
|
||||
get => _vmMain.SFMLRendererViewModel.RenderSelectedOnly;
|
||||
@@ -289,12 +278,44 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
set => SetProperty(SpineExtension.LogHitSlots, value, v => SpineExtension.LogHitSlots = v);
|
||||
}
|
||||
|
||||
public uint MaxFps
|
||||
{
|
||||
get => _vmMain.SFMLRendererViewModel.MaxFps;
|
||||
set => SetProperty(_vmMain.SFMLRendererViewModel.MaxFps, value, v => _vmMain.SFMLRendererViewModel.MaxFps = v);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 程序选项
|
||||
|
||||
public static ImmutableArray<AppLanguage> AppLanguageOptions { get; } = Enum.GetValues<AppLanguage>().ToImmutableArray();
|
||||
|
||||
public static ImmutableArray<AppSkin> AppSkinOptions { get; } = Enum.GetValues<AppSkin>().ToImmutableArray();
|
||||
|
||||
public AppLanguage AppLanguage
|
||||
{
|
||||
get => ((App)App.Current).Language;
|
||||
set => SetProperty(((App)App.Current).Language, value, v => ((App)App.Current).Language = v);
|
||||
}
|
||||
|
||||
public AppSkin AppSkin
|
||||
{
|
||||
get => ((App)App.Current).Skin;
|
||||
set => SetProperty(((App)App.Current).Skin, value, v => ((App)App.Current).Skin = v);
|
||||
}
|
||||
|
||||
public bool WallpaperView
|
||||
{
|
||||
get => _vmMain.SFMLRendererViewModel.WallpaperView;
|
||||
set => SetProperty(_vmMain.SFMLRendererViewModel.WallpaperView, value, v => _vmMain.SFMLRendererViewModel.WallpaperView = v);
|
||||
}
|
||||
|
||||
public uint WallpaperMaxFps
|
||||
{
|
||||
get => _vmMain.SFMLRendererViewModel.WallpaperMaxFps;
|
||||
set => SetProperty(_vmMain.SFMLRendererViewModel.WallpaperMaxFps, value, v => _vmMain.SFMLRendererViewModel.WallpaperMaxFps = v);
|
||||
}
|
||||
|
||||
public bool CloseToTray
|
||||
{
|
||||
get => _vmMain.CloseToTray;
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
/// <summary>
|
||||
/// 坐标轴顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray _axisVertices = new(SFML.Graphics.PrimitiveType.Lines, 2); // XXX: 暂时未使用 Dispose 释放
|
||||
private readonly SFML.Graphics.VertexArray _axisVertices = new(SFML.Graphics.PrimitiveType.Lines, 4); // XXX: 暂时未使用 Dispose 释放
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
@@ -61,6 +61,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
private Task? _renderTask = null;
|
||||
private Task? _wallpaperRenderTask = null;
|
||||
private CancellationTokenSource? _cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
@@ -87,6 +88,12 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
_models = _vmMain.SpineObjects;
|
||||
_renderer = _vmMain.SFMLRenderer;
|
||||
_wallpaperRenderer = _vmMain.WallpaperRenderer;
|
||||
|
||||
// 画一个很长的坐标轴, 用 1e9 比较合适
|
||||
_axisVertices[0] = new(new(-1e9f, 0), _axisColor);
|
||||
_axisVertices[1] = new(new(1e9f, 0), _axisColor);
|
||||
_axisVertices[2] = new(new(0, -1e9f), _axisColor);
|
||||
_axisVertices[3] = new(new(0, 1e9f), _axisColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -156,6 +163,24 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
set => SetProperty(_renderer.MaxFps, value, v => _renderer.MaxFps = value);
|
||||
}
|
||||
|
||||
public uint WallpaperMaxFps
|
||||
{
|
||||
get => _wallpaperRenderer.MaxFps;
|
||||
set => SetProperty(_wallpaperRenderer.MaxFps, value, v => _wallpaperRenderer.MaxFps = value);
|
||||
}
|
||||
|
||||
public float RealTimeFps => _realTimeFps;
|
||||
private float _realTimeFps;
|
||||
|
||||
private float _accumFpsTime;
|
||||
private int _accumFpsCount;
|
||||
|
||||
public float WallpaperRealTimeFps => _wallpaperRealTimeFps;
|
||||
private float _wallpaperRealTimeFps;
|
||||
|
||||
private int _accumWallpaperFpsCount;
|
||||
private readonly object _accumWallpaperFpsCountLock = new();
|
||||
|
||||
public float Speed
|
||||
{
|
||||
get => _speed;
|
||||
@@ -188,7 +213,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
/// </summary>
|
||||
private SFML.Graphics.Color _axisColor = SFML.Graphics.Color.White;
|
||||
|
||||
public string BackgroundImagePath
|
||||
public string? BackgroundImagePath
|
||||
{
|
||||
get => _backgroundImagePath;
|
||||
set => SetProperty(_backgroundImagePath, value, v =>
|
||||
@@ -237,7 +262,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
});
|
||||
}
|
||||
private string _backgroundImagePath;
|
||||
private string? _backgroundImagePath;
|
||||
|
||||
public Stretch BackgroundImageMode
|
||||
{
|
||||
@@ -320,7 +345,8 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
|
||||
public void CanvasMouseWheelScrolled(object? s, SFML.Window.MouseWheelScrollEventArgs e)
|
||||
{
|
||||
var factor = e.Delta > 0 ? 1.1f : 0.9f;
|
||||
float delta = ((Keyboard.Modifiers & ModifierKeys.Shift) == 0) ? 0.1f : 0.01f;
|
||||
var factor = e.Delta > 0 ? (1f + delta) : (1f - delta);
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Control) == 0)
|
||||
{
|
||||
Zoom = Math.Clamp(Zoom * factor, 0.001f, 1000f); // 滚轮缩放限制一下缩放范围
|
||||
@@ -446,26 +472,29 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
if (_renderTask is not null) return;
|
||||
_cancelToken = new();
|
||||
_renderTask = new Task(RenderTask, _cancelToken.Token, TaskCreationOptions.LongRunning);
|
||||
_renderTask = new(RenderTask, _cancelToken.Token, TaskCreationOptions.LongRunning);
|
||||
_wallpaperRenderTask = new(WallpaperRenderTask, _cancelToken.Token, TaskCreationOptions.LongRunning);
|
||||
_renderTask.Start();
|
||||
_wallpaperRenderTask.Start();
|
||||
IsUpdating = true;
|
||||
}
|
||||
|
||||
public void StopRender()
|
||||
{
|
||||
IsUpdating = false;
|
||||
if (_renderTask is null || _cancelToken is null) return;
|
||||
if (_cancelToken is null || _renderTask is null || _wallpaperRenderTask is null) return;
|
||||
_cancelToken.Cancel();
|
||||
_wallpaperRenderTask.Wait();
|
||||
_renderTask.Wait();
|
||||
_cancelToken = null;
|
||||
_wallpaperRenderTask = null;
|
||||
_renderTask = null;
|
||||
_cancelToken = null;
|
||||
}
|
||||
|
||||
private void RenderTask()
|
||||
{
|
||||
try
|
||||
{
|
||||
_wallpaperRenderer.SetActive(true);
|
||||
_renderer.SetActive(true);
|
||||
|
||||
float delta;
|
||||
@@ -474,123 +503,211 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
delta = _clock.ElapsedTime.AsSeconds();
|
||||
_clock.Restart();
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!_isUpdating) delta = 0;
|
||||
UpdateLogicFrame(delta);
|
||||
UpdateRenderFrame();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Fatal("Render task stopped, {0}", ex.Message);
|
||||
MessagePopupService.Error(ex.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
_renderer.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 加上要快进的量
|
||||
lock (_forwardDeltaLock)
|
||||
private void UpdateLogicFrame(float delta)
|
||||
{
|
||||
// 计算实时帧率, 1 秒刷新一次
|
||||
_accumFpsTime += delta;
|
||||
if (_accumFpsTime > 1f)
|
||||
{
|
||||
_realTimeFps = _accumFpsCount / _accumFpsTime;
|
||||
_accumFpsCount = 0;
|
||||
|
||||
lock (_accumWallpaperFpsCountLock)
|
||||
{
|
||||
_wallpaperRealTimeFps = _accumWallpaperFpsCount / _accumFpsTime;
|
||||
_accumWallpaperFpsCount = 0;
|
||||
}
|
||||
|
||||
_accumFpsTime = 0f;
|
||||
OnPropertyChanged(nameof(RealTimeFps));
|
||||
OnPropertyChanged(nameof(WallpaperRealTimeFps));
|
||||
}
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!_isUpdating) delta = 0;
|
||||
|
||||
// 加上要快进的量
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
delta += _forwardDelta;
|
||||
_forwardDelta = 0;
|
||||
}
|
||||
|
||||
// 更新模型对象时间
|
||||
lock (_models.Lock)
|
||||
{
|
||||
foreach (var sp in _models.Where(sp => sp.IsShown && (!_renderSelectedOnly || sp.IsSelected)).Reverse())
|
||||
{
|
||||
if (_cancelToken?.IsCancellationRequested ?? true) break; // 提前中止
|
||||
|
||||
sp.Update(0); // 避免物理效果出现问题
|
||||
sp.Update(delta * _speed);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新背景图位置和缩放
|
||||
lock (_bgLock)
|
||||
{
|
||||
if (_backgroundImageSprite is not null)
|
||||
{
|
||||
using var view = _renderer.GetView();
|
||||
var bg = _backgroundImageSprite;
|
||||
var viewSize = view.Size;
|
||||
var bgSize = bg.Texture.Size;
|
||||
var scaleX = Math.Abs(viewSize.X / bgSize.X);
|
||||
var scaleY = Math.Abs(viewSize.Y / bgSize.Y);
|
||||
var signX = Math.Sign(viewSize.X);
|
||||
var signY = Math.Sign(viewSize.Y);
|
||||
if (_backgroundImageMode == Stretch.None)
|
||||
{
|
||||
delta += _forwardDelta;
|
||||
_forwardDelta = 0;
|
||||
scaleX = scaleY = 1f / _renderer.Zoom;
|
||||
}
|
||||
else if (_backgroundImageMode == Stretch.Uniform)
|
||||
{
|
||||
scaleX = scaleY = Math.Min(scaleX, scaleY);
|
||||
}
|
||||
else if (_backgroundImageMode == Stretch.UniformToFill)
|
||||
{
|
||||
scaleX = scaleY = Math.Max(scaleX, scaleY);
|
||||
}
|
||||
bg.Scale = new(signX * scaleX, signY * scaleY);
|
||||
bg.Position = view.Center;
|
||||
bg.Rotation = view.Rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRenderFrame()
|
||||
{
|
||||
if (!_vmMain.IsVisible)
|
||||
{
|
||||
// 必须休眠一会, 否则会空转影响整体渲染循环
|
||||
Thread.Sleep(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除背景
|
||||
_renderer.Clear(_backgroundColor);
|
||||
|
||||
// 渲染背景
|
||||
lock (_bgLock)
|
||||
{
|
||||
if (_backgroundImageSprite is not null)
|
||||
{
|
||||
_renderer.Draw(_backgroundImageSprite);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染坐标轴
|
||||
if (_showAxis)
|
||||
{
|
||||
_renderer.Draw(_axisVertices);
|
||||
}
|
||||
|
||||
// 渲染 Spine
|
||||
lock (_models.Lock)
|
||||
{
|
||||
foreach (var sp in _models.Where(sp => sp.IsShown && (!_renderSelectedOnly || sp.IsSelected)).Reverse())
|
||||
{
|
||||
if (_cancelToken?.IsCancellationRequested ?? true) break; // 提前中止
|
||||
|
||||
// 为选中对象绘制一个半透明背景
|
||||
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);
|
||||
}
|
||||
|
||||
using var v = _renderer.GetView();
|
||||
_renderer.Clear(_backgroundColor);
|
||||
// 仅在预览画面临时启用调试模式
|
||||
sp.EnableDebug = true;
|
||||
_renderer.Draw(sp);
|
||||
sp.EnableDebug = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_wallpaperView)
|
||||
// 显示内容
|
||||
_renderer.Display();
|
||||
|
||||
// 帧数加一
|
||||
_accumFpsCount++;
|
||||
}
|
||||
|
||||
private void WallpaperRenderTask()
|
||||
{
|
||||
try
|
||||
{
|
||||
_wallpaperRenderer.SetActive(true);
|
||||
while (!_cancelToken?.IsCancellationRequested ?? false)
|
||||
{
|
||||
if (!_wallpaperView)
|
||||
{
|
||||
_wallpaperRenderer.SetView(v);
|
||||
_wallpaperRenderer.Clear(_backgroundColor);
|
||||
Thread.Sleep(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 同步视图
|
||||
using var view = _renderer.GetView();
|
||||
_wallpaperRenderer.SetView(view);
|
||||
|
||||
// 清除背景
|
||||
_wallpaperRenderer.Clear(_backgroundColor);
|
||||
|
||||
// 渲染背景
|
||||
lock (_bgLock)
|
||||
{
|
||||
if (_backgroundImageSprite is not null)
|
||||
{
|
||||
using var view = _renderer.GetView();
|
||||
var bg = _backgroundImageSprite;
|
||||
var viewSize = view.Size;
|
||||
var bgSize = bg.Texture.Size;
|
||||
var scaleX = Math.Abs(viewSize.X / bgSize.X);
|
||||
var scaleY = Math.Abs(viewSize.Y / bgSize.Y);
|
||||
var signX = Math.Sign(viewSize.X);
|
||||
var signY = Math.Sign(viewSize.Y);
|
||||
if (_backgroundImageMode == Stretch.None)
|
||||
{
|
||||
scaleX = scaleY = 1f / _renderer.Zoom;
|
||||
}
|
||||
else if (_backgroundImageMode == Stretch.Uniform)
|
||||
{
|
||||
scaleX = scaleY = Math.Min(scaleX, scaleY);
|
||||
}
|
||||
else if (_backgroundImageMode == Stretch.UniformToFill)
|
||||
{
|
||||
scaleX = scaleY = Math.Max(scaleX, scaleY);
|
||||
}
|
||||
bg.Scale = new(signX * scaleX, signY * scaleY);
|
||||
bg.Position = view.Center;
|
||||
bg.Rotation = view.Rotation;
|
||||
_renderer.Draw(bg);
|
||||
|
||||
if (_wallpaperView)
|
||||
{
|
||||
_wallpaperRenderer.Draw(bg);
|
||||
}
|
||||
_wallpaperRenderer.Draw(_backgroundImageSprite);
|
||||
}
|
||||
}
|
||||
|
||||
if (_showAxis)
|
||||
{
|
||||
// 画一个很长的坐标轴, 用 1e9 比较合适
|
||||
_axisVertices[0] = new(new(-1e9f, 0), _axisColor);
|
||||
_axisVertices[1] = new(new(1e9f, 0), _axisColor);
|
||||
_renderer.Draw(_axisVertices);
|
||||
_axisVertices[0] = new(new(0, -1e9f), _axisColor);
|
||||
_axisVertices[1] = new(new(0, 1e9f), _axisColor);
|
||||
_renderer.Draw(_axisVertices);
|
||||
}
|
||||
|
||||
// 渲染 Spine
|
||||
lock (_models.Lock)
|
||||
{
|
||||
foreach (var sp in _models.Where(sp => sp.IsShown && (!_renderSelectedOnly || sp.IsSelected)).Reverse())
|
||||
{
|
||||
if (_cancelToken?.IsCancellationRequested ?? true) break; // 提前中止
|
||||
if (_cancelToken?.IsCancellationRequested ?? true)
|
||||
break; // 提前中止
|
||||
|
||||
sp.Update(0); // 避免物理效果出现问题
|
||||
sp.Update(delta * _speed);
|
||||
|
||||
// 为选中对象绘制一个半透明背景
|
||||
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;
|
||||
|
||||
if (_wallpaperView)
|
||||
{
|
||||
_wallpaperRenderer.Draw(sp);
|
||||
}
|
||||
_wallpaperRenderer.Draw(sp);
|
||||
}
|
||||
}
|
||||
|
||||
_renderer.Display();
|
||||
// 显示渲染
|
||||
_wallpaperRenderer.Display();
|
||||
|
||||
if (_wallpaperView)
|
||||
{
|
||||
_wallpaperRenderer.Display();
|
||||
}
|
||||
// 帧数加一
|
||||
lock (_accumWallpaperFpsCountLock) _accumWallpaperFpsCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Fatal("Render task stopped, {0}", ex.Message);
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Fatal("Wallpaper render task stopped, {0}", ex.Message);
|
||||
MessagePopupService.Error(ex.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
_renderer.SetActive(false);
|
||||
_wallpaperRenderer.SetActive(false);
|
||||
}
|
||||
}
|
||||
@@ -609,7 +726,6 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
Rotation = Rotation,
|
||||
FlipX = FlipX,
|
||||
FlipY = FlipY,
|
||||
MaxFps = MaxFps,
|
||||
Speed = Speed,
|
||||
ShowAxis = ShowAxis,
|
||||
BackgroundColor = BackgroundColor,
|
||||
@@ -626,7 +742,6 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
Rotation = value.Rotation;
|
||||
FlipX = value.FlipX;
|
||||
FlipY = value.FlipY;
|
||||
MaxFps = value.MaxFps;
|
||||
Speed = value.Speed;
|
||||
ShowAxis = value.ShowAxis;
|
||||
BackgroundColor = value.BackgroundColor;
|
||||
|
||||
@@ -213,7 +213,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
|
||||
}
|
||||
return false;
|
||||
@@ -340,7 +340,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -401,7 +401,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
catch (Exception ex)
|
||||
{
|
||||
error++;
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -718,7 +718,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to load: {0}, {1}", cfg.SkelPath, ex.Message);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace SpineViewer.ViewModels
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to finish work: {0}, {1}", _title, ex.Message);
|
||||
WorkFinished?.Invoke(this, false);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<Grid Margin="30">
|
||||
<Grid.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
</Style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -14,6 +14,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views
|
||||
{
|
||||
@@ -30,9 +31,8 @@ namespace SpineViewer.Views
|
||||
|
||||
private void AboutDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<Border>
|
||||
<Border.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
</Style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -14,6 +14,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views
|
||||
{
|
||||
@@ -30,9 +31,8 @@ namespace SpineViewer.Views
|
||||
|
||||
private void DiagnosticsDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<Border>
|
||||
<Border.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Natives;
|
||||
using Win32Natives;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
@@ -16,6 +16,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using SpineViewer.Extensions;
|
||||
|
||||
namespace SpineViewer.Views.ExporterDialogs
|
||||
{
|
||||
@@ -32,9 +33,8 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
|
||||
private void CustomFFmpegExporterDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<Border>
|
||||
<Border.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
@@ -247,8 +247,11 @@
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ApngPred}" ToolTip="{DynamicResource Str_ApngPredTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding ApngPred}" ToolTip="{DynamicResource Str_ApngPredTooltip}"/>
|
||||
<Label Content="{DynamicResource Str_PredMethod}" ToolTip="{DynamicResource Str_PredMethodTooltip}"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
SelectedItem="{Binding PredMethod}"
|
||||
ItemsSource="{x:Static vmexp:FFmpegVideoExporterViewModel.ApngPredMethodOptions}"
|
||||
ToolTip="{DynamicResource Str_PredMethodTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- CRF 参数 -->
|
||||
@@ -268,7 +271,10 @@
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ProfileParameter}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Profile}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
SelectedItem="{Binding Profile}"
|
||||
ItemsSource="{x:Static vmexp:FFmpegVideoExporterViewModel.MovProfileOptions}"
|
||||
ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
@@ -16,6 +16,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views.ExporterDialogs
|
||||
{
|
||||
@@ -32,9 +33,8 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
|
||||
private void FFmpegVideoExporterDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<Border>
|
||||
<Border.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
@@ -16,6 +16,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views.ExporterDialogs
|
||||
{
|
||||
@@ -32,9 +33,8 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
|
||||
private void FrameExporterDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<Border>
|
||||
<Border.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
@@ -16,6 +16,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views.ExporterDialogs
|
||||
{
|
||||
@@ -32,9 +33,8 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
|
||||
private void FrameSequenceExporterDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
Height="720"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Visibility="{Binding Visibility, Mode=OneWayToSource}"
|
||||
PreviewKeyDown="MainWindow_PreviewKeyDown"
|
||||
LocationChanged="MainWindow_LocationChanged"
|
||||
SizeChanged="MainWindow_SizeChanged">
|
||||
@@ -31,6 +32,7 @@
|
||||
MouseDoubleClick="_notifyIcon_MouseDoubleClick">
|
||||
<hc:NotifyIcon.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="{DynamicResource Str_Debug}" Click="DebugMenuItem_Click" Visibility="{Binding IsDebug, Mode=OneWay, Converter={StaticResource Boolean2VisibilityConverter}}"/>
|
||||
<MenuItem Header="{DynamicResource Str_WallpaperView}" Command="{Binding Cmd_SwitchWallpaperView}" IsChecked="{Binding PreferenceViewModel.WallpaperView}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{DynamicResource Str_Exit}" Command="{Binding Cmd_ExitFromTray}"/>
|
||||
@@ -63,7 +65,7 @@
|
||||
<MenuItem Header="{DynamicResource Str_Diagnostics}" Command="{Binding Cmd_ShowDiagnosticsDialog}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{DynamicResource Str_Abount}" Command="{Binding Cmd_ShowAboutDialog}"/>
|
||||
<MenuItem Header="{DynamicResource Str_Debug}" Click="DebugMenuItem_Click"/>
|
||||
<MenuItem Header="{DynamicResource Str_Debug}" Click="DebugMenuItem_Click" Visibility="{Binding IsDebug, Mode=OneWay, Converter={StaticResource Boolean2VisibilityConverter}}"/>
|
||||
</MenuItem>
|
||||
<!--<MenuItem Header="{DynamicResource Str_Experiment}"/>-->
|
||||
</Menu>
|
||||
@@ -237,7 +239,7 @@
|
||||
TabStripPlacement="Bottom"
|
||||
DataContext="{Binding SpineObjectTabViewModel}">
|
||||
<TabControl.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
@@ -682,125 +684,6 @@
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- 浏览页 -->
|
||||
<TabItem DataContext="{Binding ExplorerListViewModel}">
|
||||
<TabItem.Header>
|
||||
<Border Style="{StaticResource MyTabItemHeaderContainerStyle}"
|
||||
MouseLeftButtonDown="MainTabControlHeader_MouseLeftButtonDown">
|
||||
<Viewbox Width="24" Height="24" ToolTip="{DynamicResource Str_Explorer}">
|
||||
<Path Data="{StaticResource Geo_Image}" Style="{StaticResource MyTabItemHeaderPathStyle}"/>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</TabItem.Header>
|
||||
<Grid x:Name="_explorerGrid">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel>
|
||||
<Grid DockPanel.Dock="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<hc:TextBox hc:InfoElement.Placeholder="{DynamicResource Str_Filter}"
|
||||
Text="{Binding FilterString, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Grid.Column="1"
|
||||
hc:IconElement.Geometry="{StaticResource Geo_Folder}"
|
||||
Command="{Binding Cmd_ChangeCurrentDirectory}"
|
||||
ToolTip="{DynamicResource Str_ChangeCurrentDirectoryTooltip}"/>
|
||||
<Button Grid.Column="2"
|
||||
hc:IconElement.Geometry="{StaticResource Geo_ArrowRotateRight}"
|
||||
Command="{Binding Cmd_RefreshItems}"
|
||||
ToolTip="{DynamicResource Str_RefreshItemsTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<StatusBar DockPanel.Dock="Bottom">
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource StrFmtCvter}" ConverterParameter="Str_ListViewStatusBar">
|
||||
<Binding Path="Items.Count" ElementName="_spineFilesListBox"/>
|
||||
<Binding Path="SelectedItems.Count" ElementName="_spineFilesListBox"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</StatusBar>
|
||||
|
||||
<ListBox x:Name="_spineFilesListBox"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
ItemsSource="{Binding ShownItems}"
|
||||
DisplayMemberPath="FileName"
|
||||
MouseLeftButtonDown="SpineFilesListBox_MouseLeftButtonDown">
|
||||
<i:Interaction.Triggers>
|
||||
<i:EventTrigger EventName="SelectionChanged">
|
||||
<i:InvokeCommandAction Command="{Binding Cmd_SelectionChanged}"
|
||||
CommandParameter="{Binding SelectedItems, ElementName=_spineFilesListBox}"/>
|
||||
</i:EventTrigger>
|
||||
</i:Interaction.Triggers>
|
||||
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="{DynamicResource Str_AddSelectedItems}"
|
||||
Command="{Binding Cmd_AddSelectedItems}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{DynamicResource Str_GeneratePreviewForSelected}"
|
||||
Command="{Binding Cmd_GeneratePreviews}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
|
||||
<MenuItem Header="{DynamicResource Str_DeletePreviewsForSelected}"
|
||||
Command="{Binding Cmd_DeletePreviews}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
|
||||
<GridSplitter Grid.Row="1" ResizeDirection="Rows"/>
|
||||
|
||||
<Grid Grid.Row="2" DataContext="{Binding SelectedItem}">
|
||||
<Grid.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>
|
||||
</Grid.Resources>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 文件目录 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_FileDirectory}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1"
|
||||
Text="{Binding FileDirectory, Mode=OneWay}"
|
||||
IsReadOnly="True"
|
||||
ToolTip="{Binding Text, RelativeSource={RelativeSource Mode=Self}}"/>
|
||||
|
||||
<!-- 文件名 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_FileName}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1"
|
||||
Text="{Binding FileName, Mode=OneWay}"
|
||||
IsReadOnly="True"/>
|
||||
|
||||
<!-- 预览图 -->
|
||||
<Border Grid.Row="2" Grid.ColumnSpan="2" Background="{DynamicResource DarkDefaultBrush}">
|
||||
<Image Source="{Binding PreviewImage, Mode=OneWay}" Stretch="Uniform"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- 画面参数页 -->
|
||||
<TabItem DataContext="{Binding SFMLRendererViewModel}">
|
||||
<TabItem.Header>
|
||||
@@ -812,7 +695,7 @@
|
||||
</Border>
|
||||
</TabItem.Header>
|
||||
<TabItem.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
@@ -910,16 +793,6 @@
|
||||
|
||||
<Separator Margin="0 5"/>
|
||||
|
||||
<!-- 最大帧率 -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_MaxFps}" ToolTip="{DynamicResource Str_MaxFpsTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding MaxFps}" ToolTip="{DynamicResource Str_MaxFpsTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 播放速度 -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -991,6 +864,128 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<!-- 浏览页 -->
|
||||
<TabItem DataContext="{Binding ExplorerListViewModel}">
|
||||
<TabItem.Header>
|
||||
<Border Style="{StaticResource MyTabItemHeaderContainerStyle}"
|
||||
MouseLeftButtonDown="MainTabControlHeader_MouseLeftButtonDown">
|
||||
<Viewbox Width="24" Height="24" ToolTip="{DynamicResource Str_Explorer}">
|
||||
<Path Data="{StaticResource Geo_Image}" Style="{StaticResource MyTabItemHeaderPathStyle}"/>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</TabItem.Header>
|
||||
<Grid x:Name="_explorerGrid">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel>
|
||||
<Grid DockPanel.Dock="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<hc:TextBox hc:InfoElement.Placeholder="{DynamicResource Str_Filter}"
|
||||
Text="{Binding FilterString, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Grid.Column="1"
|
||||
hc:IconElement.Geometry="{StaticResource Geo_Folder}"
|
||||
Command="{Binding Cmd_ChangeCurrentDirectory}"
|
||||
ToolTip="{DynamicResource Str_ChangeCurrentDirectoryTooltip}"/>
|
||||
<Button Grid.Column="2"
|
||||
hc:IconElement.Geometry="{StaticResource Geo_ArrowRotateRight}"
|
||||
Command="{Binding Cmd_RefreshItems}"
|
||||
ToolTip="{DynamicResource Str_RefreshItemsTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<StatusBar DockPanel.Dock="Bottom">
|
||||
<StatusBarItem>
|
||||
<TextBlock Foreground="{DynamicResource PrimaryTextBrush}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource StrFmtCvter}" ConverterParameter="Str_ListViewStatusBar">
|
||||
<Binding Path="Items.Count" ElementName="_spineFilesListBox"/>
|
||||
<Binding Path="SelectedItems.Count" ElementName="_spineFilesListBox"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
|
||||
<ListBox x:Name="_spineFilesListBox"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
ItemsSource="{Binding ShownItems}"
|
||||
DisplayMemberPath="FileName"
|
||||
MouseLeftButtonDown="SpineFilesListBox_MouseLeftButtonDown">
|
||||
<i:Interaction.Triggers>
|
||||
<i:EventTrigger EventName="SelectionChanged">
|
||||
<i:InvokeCommandAction Command="{Binding Cmd_SelectionChanged}"
|
||||
CommandParameter="{Binding SelectedItems, ElementName=_spineFilesListBox}"/>
|
||||
</i:EventTrigger>
|
||||
</i:Interaction.Triggers>
|
||||
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="{DynamicResource Str_AddSelectedItems}"
|
||||
Command="{Binding Cmd_AddSelectedItems}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="{DynamicResource Str_GeneratePreviewForSelected}"
|
||||
Command="{Binding Cmd_GeneratePreviews}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
|
||||
<MenuItem Header="{DynamicResource Str_DeletePreviewsForSelected}"
|
||||
Command="{Binding Cmd_DeletePreviews}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
|
||||
<GridSplitter Grid.Row="1" ResizeDirection="Rows"/>
|
||||
|
||||
<Grid Grid.Row="2" DataContext="{Binding SelectedItem}">
|
||||
<Grid.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<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>
|
||||
</Grid.Resources>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 文件目录 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_FileDirectory}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1"
|
||||
Text="{Binding FileDirectory, Mode=OneWay}"
|
||||
IsReadOnly="True"
|
||||
ToolTip="{Binding Text, RelativeSource={RelativeSource Mode=Self}}"/>
|
||||
|
||||
<!-- 文件名 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_FileName}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1"
|
||||
Text="{Binding FileName, Mode=OneWay}"
|
||||
IsReadOnly="True"/>
|
||||
|
||||
<!-- 预览图 -->
|
||||
<Border Grid.Row="2" Grid.ColumnSpan="2" Background="{DynamicResource DarkDefaultBrush}">
|
||||
<Image Source="{Binding PreviewImage, Mode=OneWay}" Stretch="Uniform"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
</Border>
|
||||
|
||||
@@ -1049,7 +1044,23 @@
|
||||
|
||||
<!-- 日志框容器 -->
|
||||
<Border Grid.Row="2" Name="_loggerBoxContainer">
|
||||
<RichTextBox x:Name="_loggerRichTextBox" Grid.Row="2" Style="{StaticResource MyLogRichTextBoxStyle}"/>
|
||||
<Border x:Name="_loggerBoxPanel" DataContext="{Binding SFMLRendererViewModel}">
|
||||
<DockPanel>
|
||||
<StatusBar DockPanel.Dock="Bottom">
|
||||
<StatusBarItem>
|
||||
<TextBlock Foreground="{DynamicResource PrimaryTextBrush}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource StrFmtCvter}" ConverterParameter="Str_RealTimeFps">
|
||||
<Binding Path="RealTimeFps"/>
|
||||
<Binding Path="WallpaperRealTimeFps"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
<RichTextBox x:Name="_loggerRichTextBox" Grid.Row="2" Style="{StaticResource MyLogRichTextBoxStyle}"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using NLog;
|
||||
using SFMLRenderer;
|
||||
using Spine;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Utils;
|
||||
@@ -24,6 +24,7 @@ using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views;
|
||||
|
||||
@@ -47,7 +48,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private readonly List<IDisposable> _userStateWatchers = [];
|
||||
private DispatcherTimer _saveUserStateTimer;
|
||||
private readonly TimeSpan _saveTimerDelay = TimeSpan.FromSeconds(3);
|
||||
private readonly TimeSpan _saveTimerDelay = TimeSpan.FromSeconds(1);
|
||||
|
||||
public bool RootGridCol0Folded
|
||||
{
|
||||
@@ -55,8 +56,19 @@ public partial class MainWindow : Window
|
||||
set
|
||||
{
|
||||
var mainTabContentHost = (ContentPresenter)_mainTabControl.Template.FindName("PART_SelectedContentHost", _mainTabControl);
|
||||
if (mainTabContentHost is null)
|
||||
{
|
||||
_mainTabControl.ApplyTemplate();
|
||||
mainTabContentHost = (ContentPresenter)_mainTabControl.Template.FindName("PART_SelectedContentHost", _mainTabControl);
|
||||
}
|
||||
if (mainTabContentHost is null)
|
||||
{
|
||||
_logger.Warn("Failed to set property {0}", nameof(RootGridCol0Folded));
|
||||
return;
|
||||
}
|
||||
if ((mainTabContentHost.Visibility != Visibility.Visible) == value)
|
||||
return;
|
||||
|
||||
if (value)
|
||||
{
|
||||
// 寄存折叠前的宽度比例
|
||||
@@ -87,13 +99,13 @@ public partial class MainWindow : Window
|
||||
|
||||
// Initialize Wallpaper RenderWindow
|
||||
_wallpaperRenderWindow = new(new(1, 1), "SpineViewerWallpaper", SFML.Window.Styles.None);
|
||||
_wallpaperRenderWindow.SetVisible(false);
|
||||
_wallpaperRenderWindow.MaxFps = 30;
|
||||
|
||||
var handle = _wallpaperRenderWindow.SystemHandle;
|
||||
var style = User32.GetWindowLong(handle, User32.GWL_STYLE) | User32.WS_POPUP;
|
||||
var exStyle = User32.GetWindowLong(handle, User32.GWL_EXSTYLE) | User32.WS_EX_LAYERED | User32.WS_EX_TOOLWINDOW;
|
||||
var exStyle = User32.GetWindowLong(handle, User32.GWL_EXSTYLE) | User32.WS_EX_TOOLWINDOW;
|
||||
User32.SetWindowLong(handle, User32.GWL_STYLE, style);
|
||||
User32.SetWindowLong(handle, User32.GWL_EXSTYLE, exStyle);
|
||||
User32.SetLayeredWindowAttributes(handle, 0, byte.MaxValue, User32.LWA_ALPHA);
|
||||
|
||||
DataContext = _vm = new(_renderPanel, _wallpaperRenderWindow);
|
||||
|
||||
@@ -120,11 +132,11 @@ public partial class MainWindow : Window
|
||||
var rtbTarget = new NLog.Windows.Wpf.RichTextBoxTarget
|
||||
{
|
||||
Name = "rtbTarget",
|
||||
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}",
|
||||
WindowName = _mainWindow.Name,
|
||||
ControlName = _loggerRichTextBox.Name,
|
||||
AutoScroll = true,
|
||||
MaxLines = 3000,
|
||||
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}",
|
||||
};
|
||||
|
||||
rtbTarget.WordColoringRules.Add(new("[D]", "Gray", "Empty"));
|
||||
@@ -142,9 +154,8 @@ public partial class MainWindow : Window
|
||||
|
||||
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
@@ -167,29 +178,9 @@ public partial class MainWindow : Window
|
||||
// 加载首选项
|
||||
_vm.PreferenceViewModel.LoadPreference();
|
||||
|
||||
// 还原上一次用户历史状态
|
||||
// 还原上一次用户历史状态并开启监听器
|
||||
LoadUserState();
|
||||
|
||||
// 添加用户状态监听器
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.WidthProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.HeightProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.LeftProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.TopProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.WindowStateProperty, DelayedSaveUserState));
|
||||
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_rootGrid.ColumnDefinitions[0], ColumnDefinition.WidthProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_rootGrid.ColumnDefinitions[2], ColumnDefinition.WidthProperty, DelayedSaveUserState));
|
||||
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_modelListGrid.RowDefinitions[0], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_modelListGrid.RowDefinitions[2], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_explorerGrid.RowDefinitions[0], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_explorerGrid.RowDefinitions[2], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_rightPanelGrid.RowDefinitions[0], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_rightPanelGrid.RowDefinitions[2], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
|
||||
_vm.SFMLRendererViewModel.PropertyChanged += SFMLRendererUserStateChanged;
|
||||
AddUserStateListeners();
|
||||
}
|
||||
|
||||
private void MainWindow_ContentRendered(object? sender, EventArgs e)
|
||||
@@ -229,14 +220,10 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
// 保存当前用户状态
|
||||
// 移除监听器并保存当前用户状态
|
||||
RemoveUserStateListensers();
|
||||
SaveUserState();
|
||||
|
||||
// 撤除所有状态监听器
|
||||
_vm.SFMLRendererViewModel.PropertyChanged -= SFMLRendererUserStateChanged;
|
||||
foreach (var w in _userStateWatchers) w.Dispose();
|
||||
_userStateWatchers.Clear();
|
||||
|
||||
_vm.SFMLRendererViewModel.StopRender();
|
||||
}
|
||||
|
||||
@@ -284,8 +271,9 @@ public partial class MainWindow : Window
|
||||
_rightPanelGrid.RowDefinitions[0].Height = new(m.RightPanelGridRow0Height, GridUnitType.Star);
|
||||
_rightPanelGrid.RowDefinitions[2].Height = new(m.RightPanelGridRow2Height, GridUnitType.Star);
|
||||
|
||||
_vm.ExplorerListViewModel.CurrentDirectory = m.ExploringDirectory;
|
||||
|
||||
_vm.SFMLRendererViewModel.SetResolution(m.ResolutionX, m.ResolutionY);
|
||||
_vm.SFMLRendererViewModel.MaxFps = m.MaxFps;
|
||||
_vm.SFMLRendererViewModel.Speed = m.Speed;
|
||||
_vm.SFMLRendererViewModel.ShowAxis = m.ShowAxis;
|
||||
_vm.SFMLRendererViewModel.BackgroundColor = m.BackgroundColor;
|
||||
@@ -317,9 +305,10 @@ public partial class MainWindow : Window
|
||||
RightPanelGridRow0Height = _rightPanelGrid.RowDefinitions[0].Height.Value,
|
||||
RightPanelGridRow2Height = _rightPanelGrid.RowDefinitions[2].Height.Value,
|
||||
|
||||
ExploringDirectory = _vm.ExplorerListViewModel.CurrentDirectory,
|
||||
|
||||
ResolutionX = _vm.SFMLRendererViewModel.ResolutionX,
|
||||
ResolutionY = _vm.SFMLRendererViewModel.ResolutionY,
|
||||
MaxFps = _vm.SFMLRendererViewModel.MaxFps,
|
||||
Speed = _vm.SFMLRendererViewModel.Speed,
|
||||
ShowAxis = _vm.SFMLRendererViewModel.ShowAxis,
|
||||
BackgroundColor = _vm.SFMLRendererViewModel.BackgroundColor,
|
||||
@@ -356,13 +345,59 @@ public partial class MainWindow : Window
|
||||
_saveUserStateTimer.Start();
|
||||
}
|
||||
|
||||
private void AddUserStateListeners()
|
||||
{
|
||||
// 添加用户状态监听器
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.WidthProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.HeightProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.LeftProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.TopProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(this, MainWindow.WindowStateProperty, DelayedSaveUserState));
|
||||
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_rootGrid.ColumnDefinitions[0], ColumnDefinition.WidthProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_rootGrid.ColumnDefinitions[2], ColumnDefinition.WidthProperty, DelayedSaveUserState));
|
||||
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_modelListGrid.RowDefinitions[0], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_modelListGrid.RowDefinitions[2], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_explorerGrid.RowDefinitions[0], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_explorerGrid.RowDefinitions[2], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_rightPanelGrid.RowDefinitions[0], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
_userStateWatchers.Add(PropertyWatcher.Watch(_rightPanelGrid.RowDefinitions[2], RowDefinition.HeightProperty, DelayedSaveUserState));
|
||||
|
||||
_vm.ExplorerListViewModel.PropertyChanged += ExplorerListUserStateChanged;
|
||||
_vm.SFMLRendererViewModel.PropertyChanged += SFMLRendererUserStateChanged;
|
||||
}
|
||||
|
||||
private void RemoveUserStateListensers()
|
||||
{
|
||||
// 撤除所有状态监听器
|
||||
_vm.SFMLRendererViewModel.PropertyChanged -= SFMLRendererUserStateChanged;
|
||||
_vm.ExplorerListViewModel.PropertyChanged -= ExplorerListUserStateChanged;
|
||||
foreach (var w in _userStateWatchers) w.Dispose();
|
||||
_userStateWatchers.Clear();
|
||||
|
||||
}
|
||||
|
||||
private void ExplorerListUserStateChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(ExplorerListViewModel.CurrentDirectory):
|
||||
DelayedSaveUserState();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SFMLRendererUserStateChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(SFMLRendererViewModel.ResolutionX):
|
||||
case nameof(SFMLRendererViewModel.ResolutionY):
|
||||
case nameof(SFMLRendererViewModel.MaxFps):
|
||||
case nameof(SFMLRendererViewModel.Speed):
|
||||
case nameof(SFMLRendererViewModel.ShowAxis):
|
||||
case nameof(SFMLRendererViewModel.BackgroundColor):
|
||||
@@ -402,6 +437,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private void SFMLRendererViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// XXX: 资源管理器重启后窗口会有问题无法重新显示, 需要重启应用, 否则要重新创建窗口
|
||||
if (e.PropertyName == nameof(SFMLRendererViewModel.WallpaperView))
|
||||
{
|
||||
var wnd = _wallpaperRenderWindow;
|
||||
@@ -413,17 +449,25 @@ public partial class MainWindow : Window
|
||||
_logger.Error("Failed to enable wallpaper view, WorkerW not found");
|
||||
return;
|
||||
}
|
||||
var handle = wnd.SystemHandle;
|
||||
|
||||
User32.GetPrimaryScreenResolution(out var sw, out var sh);
|
||||
_vm.SFMLRendererViewModel.SetResolution(sw, sh);
|
||||
|
||||
User32.SetParent(handle, workerw);
|
||||
var handle = wnd.SystemHandle;
|
||||
|
||||
// 每次都进行设置, 确保会成为顶层子窗口
|
||||
var lastParent = User32.SetParent(handle, workerw);
|
||||
Debug.WriteLine($"0x{lastParent:x8} = SetParent(0x{handle:x8}, 0x{workerw:x8})");
|
||||
User32.SetLayeredWindowAttributes(handle, 0, byte.MaxValue, User32.LWA_ALPHA);
|
||||
|
||||
_vm.SFMLRendererViewModel.SetResolution(sw, sh);
|
||||
// XXX: 每次新设置成桌面子窗口之后, 要确保窗口 Size 发生一次变化来触发 SFML 内部的渲染视图更新
|
||||
var ssize = new SFML.System.Vector2u(sw, sh);
|
||||
if (lastParent != workerw && ssize == wnd.Size)
|
||||
{
|
||||
wnd.Size = new(sw + 1, sh);
|
||||
}
|
||||
wnd.Position = new(0, 0);
|
||||
wnd.Size = new(sw + 1, sh);
|
||||
wnd.Size = new(sw, sh);
|
||||
wnd.Size = ssize;
|
||||
wnd.SetVisible(true);
|
||||
}
|
||||
else
|
||||
@@ -656,7 +700,7 @@ public partial class MainWindow : Window
|
||||
_renderPanelButtonsPopupContainer.Child = _renderPanelButtonsPanel;
|
||||
|
||||
_loggerBoxContainer.Child = null;
|
||||
_loggerBoxPopupContainer.Child = _loggerRichTextBox;
|
||||
_loggerBoxPopupContainer.Child = _loggerBoxPanel;
|
||||
}
|
||||
|
||||
private void SwitchToNormalLayout()
|
||||
@@ -666,7 +710,7 @@ public partial class MainWindow : Window
|
||||
HandyControl.Controls.IconElement.SetGeometry(_fullScreenButton, AppResource.Geo_ArrowsMaximize);
|
||||
|
||||
_loggerBoxPopupContainer.Child = null;
|
||||
_loggerBoxContainer.Child = _loggerRichTextBox;
|
||||
_loggerBoxContainer.Child = _loggerBoxPanel;
|
||||
|
||||
_renderPanelButtonsPopupContainer.Child = null;
|
||||
_renderPanelButtonsContainer.Child = _renderPanelButtonsPanel;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<Border>
|
||||
<Border.Resources>
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource LabelDefault}">
|
||||
<Style TargetType="{x:Type Label}" BasedOn="{StaticResource MyLabelStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
@@ -195,30 +195,8 @@
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{DynamicResource Str_AppPreference}">
|
||||
<GroupBox Header="{DynamicResource Str_RendererPreference}">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Language}"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
SelectedItem="{Binding AppLanguage}"
|
||||
ItemsSource="{x:Static vm:PreferenceViewModel.AppLanguageOptions}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Skin}"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
SelectedItem="{Binding AppSkin}"
|
||||
ItemsSource="{x:Static vm:PreferenceViewModel.AppSkinOptions}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
@@ -248,6 +226,41 @@
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding LogHitSlots}" ToolTip="{DynamicResource Str_LogHitSlotsTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_MaxFps}" ToolTip="{DynamicResource Str_MaxFpsTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding MaxFps}" ToolTip="{DynamicResource Str_MaxFpsTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{DynamicResource Str_AppPreference}">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Language}"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
SelectedItem="{Binding AppLanguage}"
|
||||
ItemsSource="{x:Static vm:PreferenceViewModel.AppLanguageOptions}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Skin}"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
SelectedItem="{Binding AppSkin}"
|
||||
ItemsSource="{x:Static vm:PreferenceViewModel.AppSkinOptions}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
@@ -257,6 +270,15 @@
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding WallpaperView}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_WallpaperMaxFps}" ToolTip="{DynamicResource Str_WallpaperMaxFpsTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding WallpaperMaxFps}" ToolTip="{DynamicResource Str_WallpaperMaxFpsTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
@@ -16,6 +16,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views
|
||||
{
|
||||
@@ -32,9 +33,8 @@ namespace SpineViewer.Views
|
||||
|
||||
private void PreferenceDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.ViewModels;
|
||||
using System;
|
||||
@@ -16,6 +16,7 @@ using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views
|
||||
{
|
||||
@@ -33,9 +34,8 @@ namespace SpineViewer.Views
|
||||
|
||||
private void ProgressDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
Dwmapi.SetWindowTextColor(hwnd, AppResource.Color_PrimaryText);
|
||||
Dwmapi.SetWindowCaptionColor(hwnd, AppResource.Color_Region);
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ProgressWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
|
||||
217
SpineViewerCLI/CanvasAscii.cs
Normal file
217
SpineViewerCLI/CanvasAscii.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
using SkiaSharp;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
public class CanvasAscii : Renderable
|
||||
{
|
||||
private readonly SKColor?[,] _pixels;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the canvas.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the canvas.
|
||||
/// </summary>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the render width of the canvas.
|
||||
/// </summary>
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not
|
||||
/// to scale the canvas when rendering.
|
||||
/// </summary>
|
||||
public bool Scale { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pixel width.
|
||||
/// </summary>
|
||||
public int PixelWidth { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pixel characters, ordered by transparency.
|
||||
/// </summary>
|
||||
public string PixelCharacters { get; set; } = ".,:;-=+*?oSXBGWM$&%#@";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use pixel characters instead of spaces.
|
||||
/// </summary>
|
||||
public bool UsePixelCharacters { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CanvasAscii"/> class.
|
||||
/// </summary>
|
||||
/// <param name="width">The canvas width.</param>
|
||||
/// <param name="height">The canvas height.</param>
|
||||
public CanvasAscii(int width, int height)
|
||||
{
|
||||
if (width < 1)
|
||||
{
|
||||
throw new ArgumentException("Must be > 1", nameof(width));
|
||||
}
|
||||
|
||||
if (height < 1)
|
||||
{
|
||||
throw new ArgumentException("Must be > 1", nameof(height));
|
||||
}
|
||||
|
||||
Width = width;
|
||||
Height = height;
|
||||
|
||||
_pixels = new SKColor?[Width, Height];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a pixel with the specified color in the canvas at the specified location.
|
||||
/// </summary>
|
||||
/// <param name="x">The X coordinate for the pixel.</param>
|
||||
/// <param name="y">The Y coordinate for the pixel.</param>
|
||||
/// <param name="color">The pixel color.</param>
|
||||
/// <returns>The same <see cref="CanvasAscii"/> instance so that multiple calls can be chained.</returns>
|
||||
public CanvasAscii SetPixel(int x, int y, SKColor color)
|
||||
{
|
||||
_pixels[x, y] = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderOptions options, int maxWidth)
|
||||
{
|
||||
if (PixelWidth < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Pixel width must be greater than zero.");
|
||||
}
|
||||
|
||||
var width = MaxWidth ?? Width;
|
||||
|
||||
if (maxWidth < width * PixelWidth)
|
||||
{
|
||||
return new Measurement(maxWidth, maxWidth);
|
||||
}
|
||||
|
||||
return new Measurement(width * PixelWidth, width * PixelWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
|
||||
{
|
||||
if (PixelWidth < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Pixel width must be greater than zero.");
|
||||
}
|
||||
|
||||
if (UsePixelCharacters && PixelCharacters.Length <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Pixel letters can't be empty.");
|
||||
}
|
||||
|
||||
var pixels = _pixels;
|
||||
var emptyPixel = new string(' ', PixelWidth);
|
||||
var width = Width;
|
||||
var height = Height;
|
||||
|
||||
// Got a max width?
|
||||
if (MaxWidth != null)
|
||||
{
|
||||
height = (int)(height * ((float)MaxWidth.Value) / Width);
|
||||
width = MaxWidth.Value;
|
||||
}
|
||||
|
||||
// Exceed the max width when we take pixel width into account?
|
||||
if (width * PixelWidth > maxWidth)
|
||||
{
|
||||
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
|
||||
width = maxWidth / PixelWidth;
|
||||
|
||||
// If it's not possible to scale the canvas sufficiently, it's too small to render.
|
||||
if (height == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
// Need to rescale the pixel buffer?
|
||||
if (Scale && (width != Width || height != Height))
|
||||
{
|
||||
pixels = ScaleDown(width, height);
|
||||
}
|
||||
|
||||
if (UsePixelCharacters)
|
||||
{
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
var color = pixels[x, y];
|
||||
if (color.HasValue)
|
||||
{
|
||||
var c = color.Value;
|
||||
yield return new Segment(GetPixelChar(c), new Style(foreground: new(c.Red, c.Green, c.Blue)));
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new Segment(emptyPixel);
|
||||
}
|
||||
}
|
||||
|
||||
yield return Segment.LineBreak;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
var color = pixels[x, y];
|
||||
if (color.HasValue)
|
||||
{
|
||||
var c = color.Value;
|
||||
yield return new Segment(emptyPixel, new Style(background: new(c.Red, c.Green, c.Blue)));
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new Segment(emptyPixel);
|
||||
}
|
||||
}
|
||||
|
||||
yield return Segment.LineBreak;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SKColor?[,] ScaleDown(int newWidth, int newHeight)
|
||||
{
|
||||
var buffer = new SKColor?[newWidth, newHeight];
|
||||
var xRatio = ((Width << 16) / newWidth) + 1;
|
||||
var yRatio = ((Height << 16) / newHeight) + 1;
|
||||
|
||||
for (var i = 0; i < newHeight; i++)
|
||||
{
|
||||
for (var j = 0; j < newWidth; j++)
|
||||
{
|
||||
buffer[j, i] = _pixels[(j * xRatio) >> 16, (i * yRatio) >> 16];
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private string GetPixelChar(SKColor c)
|
||||
{
|
||||
var index = Math.Min((int)(c.Alpha / 255f * PixelCharacters.Length), PixelCharacters.Length - 1);
|
||||
return new(PixelCharacters[index], PixelWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
163
SpineViewerCLI/CanvasImageAscii.cs
Normal file
163
SpineViewerCLI/CanvasImageAscii.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using SkiaSharp;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
internal class CanvasImageAscii : Renderable
|
||||
{
|
||||
private static readonly SKSamplingOptions _defaultSamplingOptions = new(new SKCubicResampler());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image width.
|
||||
/// </summary>
|
||||
public int Width => Image.Width;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image height.
|
||||
/// </summary>
|
||||
public int Height => Image.Height;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the render width of the canvas.
|
||||
/// </summary>
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the render width of the canvas.
|
||||
/// </summary>
|
||||
public int PixelWidth { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pixel characters, ordered by transparency.
|
||||
/// </summary>
|
||||
public string PixelCharacters { get; set; } = ".,:;-=+*?oSXBGWM$&%#@";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use pixel characters instead of spaces.
|
||||
/// </summary>
|
||||
public bool UsePixelCharacters { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="SKSamplingOptions"/> that should
|
||||
/// be used when scaling the image. Defaults to bicubic sampling.
|
||||
/// </summary>
|
||||
public SKSamplingOptions? SamplingOptions { get; set; }
|
||||
|
||||
internal SKBitmap Image { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CanvasImageAscii"/> class.
|
||||
/// </summary>
|
||||
/// <param name="filename">The image filename.</param>
|
||||
public CanvasImageAscii(string filename)
|
||||
{
|
||||
Image = SKBitmap.Decode(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CanvasImageAscii"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Buffer containing an image.</param>
|
||||
public CanvasImageAscii(ReadOnlySpan<byte> data)
|
||||
{
|
||||
Image = SKBitmap.Decode(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CanvasImageAscii"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Stream containing an image.</param>
|
||||
public CanvasImageAscii(Stream data)
|
||||
{
|
||||
Image = SKBitmap.Decode(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CanvasImageAscii"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The <see cref="SKImage"/> object.</param>
|
||||
public CanvasImageAscii(SKImage image)
|
||||
{
|
||||
Image = SKBitmap.FromImage(image);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Measurement Measure(RenderOptions options, int maxWidth)
|
||||
{
|
||||
if (PixelWidth < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Pixel width must be greater than zero.");
|
||||
}
|
||||
|
||||
var width = MaxWidth ?? Width;
|
||||
if (maxWidth < width * PixelWidth)
|
||||
{
|
||||
return new Measurement(maxWidth, maxWidth);
|
||||
}
|
||||
|
||||
return new Measurement(width * PixelWidth, width * PixelWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
|
||||
{
|
||||
var image = Image;
|
||||
|
||||
var width = Width;
|
||||
var height = Height;
|
||||
|
||||
// Got a max width?
|
||||
if (MaxWidth != null)
|
||||
{
|
||||
height = (int)(height * ((float)MaxWidth.Value) / Width);
|
||||
width = MaxWidth.Value;
|
||||
}
|
||||
|
||||
// Exceed the max width when we take pixel width into account?
|
||||
if (width * PixelWidth > maxWidth)
|
||||
{
|
||||
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
|
||||
width = maxWidth / PixelWidth;
|
||||
}
|
||||
|
||||
// Need to rescale the pixel buffer?
|
||||
if (width != Width || height != Height)
|
||||
{
|
||||
var samplingOptions = SamplingOptions ?? _defaultSamplingOptions;
|
||||
image = image.Resize(new SKSizeI(width, height), samplingOptions);
|
||||
}
|
||||
|
||||
var canvas = new CanvasAscii(width, height)
|
||||
{
|
||||
MaxWidth = MaxWidth,
|
||||
PixelWidth = PixelWidth,
|
||||
PixelCharacters = PixelCharacters,
|
||||
UsePixelCharacters = UsePixelCharacters,
|
||||
Scale = false,
|
||||
};
|
||||
|
||||
// XXX: 也许是 SkiaSharp@3.119.0 的 bug, 此处像素值一定是非预乘的格式
|
||||
for (var y = 0; y < image.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < image.Width; x++)
|
||||
{
|
||||
var p = image.GetPixel(x, y);
|
||||
if (p.Alpha == 0) continue;
|
||||
float a = p.Alpha / 255f;
|
||||
byte r = (byte)(p.Red * a);
|
||||
byte g = (byte)(p.Green * a);
|
||||
byte b = (byte)(p.Blue * a);
|
||||
canvas.SetPixel(x, y, new(r, g, b, p.Alpha));
|
||||
}
|
||||
}
|
||||
|
||||
return ((IRenderable)canvas).Render(options, maxWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
471
SpineViewerCLI/ExportCommand.cs
Normal file
471
SpineViewerCLI/ExportCommand.cs
Normal file
@@ -0,0 +1,471 @@
|
||||
using NLog;
|
||||
using Spectre.Console;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
public enum ExportFormat
|
||||
{
|
||||
Png = 0x0100,
|
||||
Jpg = 0x0101,
|
||||
Webp = 0x0102,
|
||||
Frames = 0x0200,
|
||||
Gif = 0x0300,
|
||||
Webpa = 0x0301,
|
||||
Apng = 0x0302,
|
||||
Mp4 = 0x0303,
|
||||
Webm = 0x0304,
|
||||
Mkv = 0x0305,
|
||||
Mov = 0x0306,
|
||||
Custom = 0x0400,
|
||||
}
|
||||
|
||||
public class ExportCommand : Command
|
||||
{
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly string _name = "export";
|
||||
private static readonly string _desc = "Export single model";
|
||||
|
||||
#region >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 基本参数 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
public Argument<FileInfo> ArgSkel { get; } = new("skel")
|
||||
{
|
||||
Description = "Path of skel file.",
|
||||
};
|
||||
|
||||
public Option<ExportFormat> OptFormat { get; } = new("--format", "-f")
|
||||
{
|
||||
Description = "Export format.",
|
||||
Required = true,
|
||||
};
|
||||
|
||||
public Option<string> OptOutput { get; } = new("--output", "-o")
|
||||
{
|
||||
Description = "Output file or directory. Use a directory for frame sequence export.",
|
||||
Required = true,
|
||||
};
|
||||
|
||||
public Option<string[]> OptAnimations { get; } = new("--animations", "-a")
|
||||
{
|
||||
Description = "Animations to export. Supports multiple entries, placed in order on tracks starting from 0.",
|
||||
Required = true,
|
||||
Arity = ArgumentArity.OneOrMore,
|
||||
AllowMultipleArgumentsPerToken = true,
|
||||
};
|
||||
|
||||
public Option<FileInfo> OptAtlas { get; } = new("--atlas")
|
||||
{
|
||||
Description = "Path to the atlas file that matches the skel file.",
|
||||
};
|
||||
|
||||
public Option<float> OptScale { get; } = new("--scale")
|
||||
{
|
||||
Description = "Scale factor of the model.",
|
||||
DefaultValueFactory = _ => 1f,
|
||||
};
|
||||
|
||||
public Option<bool> OptPma { get; } = new("--pma")
|
||||
{
|
||||
Description = "Specifies whether the texture uses PMA (premultiplied alpha) format.",
|
||||
};
|
||||
|
||||
public Option<string[]> OptSkins { get; } = new("--skins")
|
||||
{
|
||||
Description = "Skins to export. Multiple skins can be specified.",
|
||||
Arity = ArgumentArity.OneOrMore,
|
||||
AllowMultipleArgumentsPerToken = true,
|
||||
};
|
||||
|
||||
public Option<string[]> OptDisableSlots { get; } = new("--disable-slots")
|
||||
{
|
||||
Description = "Slots to disable during export. Multiple slots can be specified.",
|
||||
Arity = ArgumentArity.OneOrMore,
|
||||
AllowMultipleArgumentsPerToken = true,
|
||||
};
|
||||
|
||||
public Option<float> OptWarmUp { get; } = new("--warm-up")
|
||||
{
|
||||
Description = "Warm-up duration of the animation, used to stabilize physics effects. A negative value will automatically warm up for the maximum duration among all animations.",
|
||||
DefaultValueFactory = _ => 0f,
|
||||
};
|
||||
|
||||
public Option<bool> OptNoProgress { get; } = new("--no-progress")
|
||||
{
|
||||
Description = "Do not display real-time progress.",
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 基本导出参数 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
public Option<SFML.Graphics.Color> OptColor { get; } = new("--color")
|
||||
{
|
||||
Description = "Background color of content.",
|
||||
//DefaultValueFactory = ...
|
||||
CustomParser = Utils.ParseColor
|
||||
};
|
||||
|
||||
public Option<uint> OptMargin { get; } = new("--margin")
|
||||
{
|
||||
Description = "Size of the margin (in pixels) around the content.",
|
||||
DefaultValueFactory = _ => 0u,
|
||||
};
|
||||
|
||||
public Option<uint> OptMaxResolution { get; } = new("--max-resolution")
|
||||
{
|
||||
Description = "Maximum width or height (in pixels) for exported images.",
|
||||
DefaultValueFactory = _ => 2048u,
|
||||
};
|
||||
|
||||
public Option<float> OptTime { get; } = new("--time")
|
||||
{
|
||||
Description = "Start time offset of the animation.",
|
||||
DefaultValueFactory = _ => 0f,
|
||||
};
|
||||
|
||||
public Option<float> OptDuration { get; } = new("--duration")
|
||||
{
|
||||
Description = "Export duration. Negative values indicate automatic duration calculation.",
|
||||
DefaultValueFactory = _ => -1f,
|
||||
};
|
||||
|
||||
public Option<uint> OptFps { get; } = new("--fps")
|
||||
{
|
||||
Description = "Frame rate for export.",
|
||||
DefaultValueFactory = _ => 30u,
|
||||
};
|
||||
|
||||
public Option<float> OptSpeed { get; } = new("--speed")
|
||||
{
|
||||
Description = "Speed factor for the exported animation.",
|
||||
DefaultValueFactory = _ => 1f,
|
||||
};
|
||||
|
||||
public Option<bool> OptDropLastFrame { get; } = new("--drop-last-frame")
|
||||
{
|
||||
Description = "Whether to drop the incomplete last frame.",
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 格式参数 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
public Option<uint> OptQuality { get; } = new("--quality")
|
||||
{
|
||||
Description = "Image quality.",
|
||||
DefaultValueFactory = _ => 80u,
|
||||
};
|
||||
|
||||
public Option<bool> OptLoop { get; } = new("--loop")
|
||||
{
|
||||
Description = "Whether the animation should loop.",
|
||||
};
|
||||
|
||||
public Option<bool> OptLossless { get; } = new("--lossless")
|
||||
{
|
||||
Description = "Whether to encode the WebP animation losslessly.",
|
||||
};
|
||||
|
||||
public Option<FFmpegVideoExporter.ApngPredMethod> OptApngPredMethod { get; } = new("--apng-pred")
|
||||
{
|
||||
Description = "Prediction method used for APNG animations.",
|
||||
DefaultValueFactory = _ => FFmpegVideoExporter.ApngPredMethod.Mixed,
|
||||
};
|
||||
|
||||
public Option<uint> OptCrf { get; } = new("--crf")
|
||||
{
|
||||
Description = "CRF (Constant Rate Factor) value for encoding.",
|
||||
DefaultValueFactory = _ => 23u,
|
||||
};
|
||||
|
||||
public Option<FFmpegVideoExporter.MovProfile> OptMovProfile { get; } = new("--mov-profile")
|
||||
{
|
||||
Description = "Profile setting for MOV format export.",
|
||||
DefaultValueFactory = _ => FFmpegVideoExporter.MovProfile.Yuv4444Extreme,
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 自定义导出格式参数 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
|
||||
public Option<string> OptFFFormat { get; } = new("--ff-format")
|
||||
{
|
||||
Description = "format option of ffmpeg",
|
||||
};
|
||||
|
||||
public Option<string> OptFFCodec { get; } = new("--ff-codec")
|
||||
{
|
||||
Description = "codec option of ffmpeg",
|
||||
};
|
||||
|
||||
public Option<string> OptFFPixelFormat { get; } = new("--ff-pixfmt")
|
||||
{
|
||||
Description = "pixel format option of ffmpeg",
|
||||
};
|
||||
|
||||
public Option<string> OptFFBitrate { get; } = new("--ff-bitrate")
|
||||
{
|
||||
Description = "bitrate option of ffmpeg",
|
||||
};
|
||||
|
||||
public Option<string> OptFFFilter { get; } = new("--ff-filter")
|
||||
{
|
||||
Description = "filter option of ffmpeg",
|
||||
};
|
||||
|
||||
public Option<string> OptFFArgs { get; } = new("--ff-args")
|
||||
{
|
||||
Description = "other arguments of ffmpeg",
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
public ExportCommand() : base(_name, _desc)
|
||||
{
|
||||
OptColor.DefaultValueFactory = r =>
|
||||
{
|
||||
var defVal = SFML.Graphics.Color.Black;
|
||||
try
|
||||
{
|
||||
switch (r.GetValue(OptFormat))
|
||||
{
|
||||
case ExportFormat.Png:
|
||||
case ExportFormat.Webp:
|
||||
case ExportFormat.Frames:
|
||||
case ExportFormat.Gif:
|
||||
case ExportFormat.Webpa:
|
||||
case ExportFormat.Apng:
|
||||
case ExportFormat.Webm:
|
||||
defVal = SFML.Graphics.Color.Transparent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException) { } // 未提供 OptFormat 的时候 GetValue 会报错
|
||||
return defVal;
|
||||
};
|
||||
OptScale.Validators.Add(r =>
|
||||
{
|
||||
if (r.Tokens.Count > 0 && float.TryParse(r.Tokens[0].Value, out var v) && v < 0)
|
||||
r.AddError($"{OptScale.Name} must be non-negative.");
|
||||
});
|
||||
OptTime.Validators.Add(r =>
|
||||
{
|
||||
if (r.Tokens.Count > 0 && float.TryParse(r.Tokens[0].Value, out var v) && v < 0)
|
||||
r.AddError($"{OptTime.Name} must be non-negative.");
|
||||
});
|
||||
OptSpeed.Validators.Add(r =>
|
||||
{
|
||||
if (r.Tokens.Count > 0 && float.TryParse(r.Tokens[0].Value, out var v) && v < 0)
|
||||
r.AddError($"{OptSpeed.Name} must be non-negative.");
|
||||
});
|
||||
|
||||
this.AddArgsAndOpts();
|
||||
SetAction(ExportAction);
|
||||
}
|
||||
|
||||
private void ExportAction(ParseResult result)
|
||||
{
|
||||
// 读取模型
|
||||
using var spine = new SpineObject(result.GetValue(ArgSkel)!.FullName, result.GetValue(OptAtlas)?.FullName);
|
||||
|
||||
// 设置模型参数
|
||||
spine.Skeleton.ScaleX = spine.Skeleton.ScaleY = result.GetValue(OptScale);
|
||||
spine.UsePma = result.GetValue(OptPma);
|
||||
|
||||
// 设置要导出的动画
|
||||
int trackIdx = 0;
|
||||
foreach (var name in result.GetValue(OptAnimations))
|
||||
{
|
||||
if (!spine.Data.AnimationsByName.ContainsKey(name))
|
||||
{
|
||||
_logger.Warn("No animation named '{0}', skip it", name);
|
||||
continue;
|
||||
}
|
||||
spine.AnimationState.SetAnimation(trackIdx, name, true);
|
||||
trackIdx++;
|
||||
}
|
||||
|
||||
// 设置需要启用的皮肤
|
||||
foreach (var name in result.GetValue(OptSkins))
|
||||
{
|
||||
if (!spine.SetSkinStatus(name, true))
|
||||
{
|
||||
_logger.Warn("Failed to enable skin '{0}'", name);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置需要屏蔽的插槽
|
||||
foreach (var name in result.GetValue(OptDisableSlots))
|
||||
{
|
||||
if (!spine.SetSlotVisible(name, false))
|
||||
{
|
||||
_logger.Warn("Failed to disable slot '{0}'", name);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 设置要启用的插槽
|
||||
|
||||
// 时间轴处理
|
||||
var warmup = result.GetValue(OptWarmUp);
|
||||
spine.Update(warmup < 0 ? spine.GetAnimationMaxDuration() : warmup);
|
||||
spine.Update(result.GetValue(OptTime));
|
||||
|
||||
using var exporter = GetExporterFilledWithArgs(result, spine);
|
||||
|
||||
// 创建输出目录
|
||||
string output = result.GetValue(OptOutput);
|
||||
Directory.CreateDirectory(exporter is FrameSequenceExporter ? output : Path.GetDirectoryName(output));
|
||||
|
||||
// 挂载进度报告函数
|
||||
if (exporter is VideoExporter ve && !result.GetValue(OptNoProgress))
|
||||
{
|
||||
AnsiConsole.Progress().Columns(
|
||||
[
|
||||
new TaskDescriptionColumn(),
|
||||
new ProgressBarColumn(),
|
||||
new PercentageColumn(),
|
||||
new RemainingTimeColumn(),
|
||||
new SpinnerColumn(),
|
||||
]).Start(ctx =>
|
||||
{
|
||||
var task = ctx.AddTask($"Exporting '{spine.Name}'");
|
||||
task.MaxValue = ve.GetFrameCount();
|
||||
ve.ProgressReporter = (total, done, text) => task.Value = done;
|
||||
ve.Export(output, spine);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
exporter.Export(output, spine);
|
||||
}
|
||||
|
||||
_logger.Info($"{spine.SkelPath} export completed");
|
||||
}
|
||||
|
||||
private BaseExporter GetExporterFilledWithArgs(ParseResult result, SpineObject spine)
|
||||
{
|
||||
var formatType = (int)result.GetValue(OptFormat) >> 8;
|
||||
|
||||
// 根据模型获取自动分辨率和视区参数
|
||||
var maxResolution = result.GetValue(OptMaxResolution);
|
||||
var margin = result.GetValue(OptMargin);
|
||||
var bounds = formatType == 0x01 ? spine.GetCurrentBounds() : spine.GetAnimationBounds(result.GetValue(OptFps));
|
||||
var resolution = new SFML.System.Vector2u((uint)bounds.Size.X, (uint)bounds.Size.Y);
|
||||
if (resolution.X >= maxResolution || resolution.Y >= maxResolution)
|
||||
{
|
||||
// 缩小到最大像素限制
|
||||
var scale = Math.Min(maxResolution / bounds.Width, maxResolution / bounds.Height);
|
||||
resolution.X = (uint)(bounds.Width * scale);
|
||||
resolution.Y = (uint)(bounds.Height * scale);
|
||||
}
|
||||
var viewBounds = bounds.GetCanvasBounds(resolution, margin);
|
||||
|
||||
var duration = result.GetValue(OptDuration);
|
||||
if (duration < 0) duration = spine.GetAnimationMaxDuration();
|
||||
|
||||
if (formatType == 0x01)
|
||||
{
|
||||
return new FrameExporter(resolution.X + margin * 2, resolution.Y + margin * 2)
|
||||
{
|
||||
Size = new(viewBounds.Width, -viewBounds.Height),
|
||||
Center = viewBounds.Position + viewBounds.Size / 2,
|
||||
Rotation = 0,
|
||||
BackgroundColor = result.GetValue(OptColor),
|
||||
|
||||
Format = result.GetValue(OptFormat) switch
|
||||
{
|
||||
ExportFormat.Png => SkiaSharp.SKEncodedImageFormat.Png,
|
||||
ExportFormat.Jpg => SkiaSharp.SKEncodedImageFormat.Jpeg,
|
||||
ExportFormat.Webp => SkiaSharp.SKEncodedImageFormat.Webp,
|
||||
var v => throw new InvalidOperationException($"{v}"),
|
||||
},
|
||||
Quality = (int)result.GetValue(OptQuality),
|
||||
};
|
||||
}
|
||||
else if (formatType == 0x02)
|
||||
{
|
||||
return new FrameSequenceExporter(resolution.X + margin * 2, resolution.Y + margin * 2)
|
||||
{
|
||||
Size = new(viewBounds.Width, -viewBounds.Height),
|
||||
Center = viewBounds.Position + viewBounds.Size / 2,
|
||||
Rotation = 0,
|
||||
BackgroundColor = result.GetValue(OptColor),
|
||||
|
||||
Fps = result.GetValue(OptFps),
|
||||
Speed = result.GetValue(OptSpeed),
|
||||
KeepLast = !result.GetValue(OptDropLastFrame),
|
||||
Duration = duration,
|
||||
};
|
||||
}
|
||||
else if (formatType == 0x03)
|
||||
{
|
||||
return new FFmpegVideoExporter(resolution.X + margin * 2, resolution.Y + margin * 2)
|
||||
{
|
||||
Size = new(viewBounds.Width, -viewBounds.Height),
|
||||
Center = viewBounds.Position + viewBounds.Size / 2,
|
||||
Rotation = 0,
|
||||
BackgroundColor = result.GetValue(OptColor),
|
||||
|
||||
Fps = result.GetValue(OptFps),
|
||||
Speed = result.GetValue(OptSpeed),
|
||||
KeepLast = !result.GetValue(OptDropLastFrame),
|
||||
Duration = duration,
|
||||
|
||||
Format = result.GetValue(OptFormat) switch
|
||||
{
|
||||
ExportFormat.Gif => FFmpegVideoExporter.VideoFormat.Gif,
|
||||
ExportFormat.Webpa => FFmpegVideoExporter.VideoFormat.Webp,
|
||||
ExportFormat.Apng => FFmpegVideoExporter.VideoFormat.Apng,
|
||||
ExportFormat.Mp4 => FFmpegVideoExporter.VideoFormat.Mp4,
|
||||
ExportFormat.Webm => FFmpegVideoExporter.VideoFormat.Webm,
|
||||
ExportFormat.Mkv => FFmpegVideoExporter.VideoFormat.Mkv,
|
||||
ExportFormat.Mov => FFmpegVideoExporter.VideoFormat.Mov,
|
||||
var v => throw new InvalidOperationException($"{v}"),
|
||||
},
|
||||
Quality = (int)result.GetValue(OptQuality),
|
||||
Loop = result.GetValue(OptLoop),
|
||||
Lossless = result.GetValue(OptLossless),
|
||||
PredMethod = result.GetValue(OptApngPredMethod),
|
||||
Crf = (int)result.GetValue(OptCrf),
|
||||
Profile = result.GetValue(OptMovProfile),
|
||||
}
|
||||
;
|
||||
}
|
||||
else if (formatType == 0x04)
|
||||
{
|
||||
return new CustomFFmpegExporter(resolution.X + margin * 2, resolution.Y + margin * 2)
|
||||
{
|
||||
Size = new(viewBounds.Width, -viewBounds.Height),
|
||||
Center = viewBounds.Position + viewBounds.Size / 2,
|
||||
Rotation = 0,
|
||||
BackgroundColor = result.GetValue(OptColor),
|
||||
|
||||
Fps = result.GetValue(OptFps),
|
||||
Speed = result.GetValue(OptSpeed),
|
||||
KeepLast = !result.GetValue(OptDropLastFrame),
|
||||
Duration = duration,
|
||||
|
||||
Format = result.GetValue(OptFFFormat),
|
||||
Codec = result.GetValue(OptFFCodec),
|
||||
PixelFormat = result.GetValue(OptFFPixelFormat),
|
||||
Bitrate = result.GetValue(OptFFBitrate),
|
||||
Filter = result.GetValue(OptFFFilter),
|
||||
CustomArgs = result.GetValue(OptFFArgs),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Unknown format type {formatType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
105
SpineViewerCLI/Extension.cs
Normal file
105
SpineViewerCLI/Extension.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
public static class Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取一个对象副本, 继承所有状态
|
||||
/// </summary>
|
||||
public static SpineObject Copy(this SpineObject self, bool keepTrackTime = false)
|
||||
{
|
||||
var spineObject = new SpineObject(self, true);
|
||||
|
||||
// 拷贝轨道动画, 但是仅拷贝第一个条目
|
||||
foreach (var tr in self.AnimationState.IterTracks().Where(t => t is not null))
|
||||
{
|
||||
var t = spineObject.AnimationState.SetAnimation(tr!.TrackIndex, tr.Animation, tr.Loop);
|
||||
t.TimeScale = tr.TimeScale;
|
||||
t.Alpha = tr.Alpha;
|
||||
if (keepTrackTime)
|
||||
t.TrackTime = tr.TrackTime;
|
||||
}
|
||||
|
||||
// XXX(#105): 部分 3.4.02 版本模型在设置动画后出现附件残留, 因此强制进行一次 Setup
|
||||
if (spineObject.Version == SpineVersion.V34)
|
||||
{
|
||||
spineObject.Skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
spineObject.Update(0);
|
||||
return spineObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前状态包围盒
|
||||
/// </summary>
|
||||
public static FloatRect GetCurrentBounds(this SpineObject self)
|
||||
{
|
||||
self.Skeleton.GetBounds(out var x, out var y, out var w, out var h);
|
||||
return new(x, y, Math.Max(w, 1e-6f), Math.Max(h, 1e-6f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算所有轨道第一个条目的动画时长最大值
|
||||
/// </summary>
|
||||
/// <param name="self"></param>
|
||||
/// <returns></returns>
|
||||
public static float GetAnimationMaxDuration(this SpineObject self)
|
||||
{
|
||||
return self.AnimationState.IterTracks().Select(t => t?.Animation.Duration ?? 0).DefaultIfEmpty(0).Max();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并另一个矩形
|
||||
/// </summary>
|
||||
public static FloatRect Union(this FloatRect self, FloatRect rect)
|
||||
{
|
||||
float left = Math.Min(self.Left, rect.Left);
|
||||
float top = Math.Min(self.Top, rect.Top);
|
||||
float right = Math.Max(self.Left + self.Width, rect.Left + rect.Width);
|
||||
float bottom = Math.Max(self.Top + self.Height, rect.Top + rect.Height);
|
||||
return new(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按给定的帧率获取所有轨道第一个条目动画全时长包围盒大小, 是一个耗时操作, 如果可能的话最好缓存结果
|
||||
/// </summary>
|
||||
public static FloatRect GetAnimationBounds(this SpineObject self, float fps = 30)
|
||||
{
|
||||
using var copy = self.Copy();
|
||||
var bounds = copy.GetCurrentBounds();
|
||||
var maxDuration = copy.GetAnimationMaxDuration();
|
||||
for (float tick = 0, delta = 1 / fps; tick < maxDuration; tick += delta)
|
||||
{
|
||||
bounds = bounds.Union(copy.GetCurrentBounds());
|
||||
copy.Update(delta);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动添加所有能找到的类型是 <see cref="Argument"/> 或者 <see cref="Option"/> 的公开属性
|
||||
/// </summary>
|
||||
/// <param name="self"></param>
|
||||
public static void AddArgsAndOpts(this Command self)
|
||||
{
|
||||
// 用反射查找自己所有的公开属性是 Argument 或者 Option 的
|
||||
foreach (var prop in self.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var value = prop.GetValue(self);
|
||||
if (value is Argument arg) self.Add(arg);
|
||||
else if (value is Option opt) self.Add(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
137
SpineViewerCLI/PreviewCommand.cs
Normal file
137
SpineViewerCLI/PreviewCommand.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using NLog;
|
||||
using Spectre.Console;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
public class PreviewCommand : Command
|
||||
{
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly string _name = "preview";
|
||||
private static readonly string _desc = "Preview a model";
|
||||
private static readonly int MaxResolution = 1024;
|
||||
|
||||
public Argument<FileInfo> ArgSkel { get; } = new("skel")
|
||||
{
|
||||
Description = "Path of skel file.",
|
||||
};
|
||||
|
||||
public Option<FileInfo> OptAtlas { get; } = new("--atlas")
|
||||
{
|
||||
Description = "Path to the atlas file that matches the skel file.",
|
||||
};
|
||||
|
||||
public Option<bool> OptPma { get; } = new("--pma")
|
||||
{
|
||||
Description = "Specifies whether the texture uses PMA (premultiplied alpha) format.",
|
||||
};
|
||||
|
||||
public Option<string[]> OptSkins { get; } = new("--skins")
|
||||
{
|
||||
Description = "Skins to enable. Multiple skins can be specified.",
|
||||
Arity = ArgumentArity.OneOrMore,
|
||||
AllowMultipleArgumentsPerToken = true,
|
||||
};
|
||||
|
||||
public Option<string[]> OptAnimations { get; } = new("--animations")
|
||||
{
|
||||
Description = "Animations to export. Supports multiple entries, placed in order on tracks starting from 0.",
|
||||
Arity = ArgumentArity.OneOrMore,
|
||||
AllowMultipleArgumentsPerToken = true,
|
||||
};
|
||||
|
||||
public Option<float> OptTime { get; } = new("--time")
|
||||
{
|
||||
Description = "Start time offset of the animation.",
|
||||
DefaultValueFactory = _ => 0f,
|
||||
};
|
||||
|
||||
public Option<bool> OptUseChars { get; } = new("--use-chars")
|
||||
{
|
||||
Description = "Whether to use characters instead of colored spaces for pixels",
|
||||
};
|
||||
|
||||
public PreviewCommand() : base(_name, _desc)
|
||||
{
|
||||
OptTime.Validators.Add(r =>
|
||||
{
|
||||
if (r.Tokens.Count > 0 && float.TryParse(r.Tokens[0].Value, out var v) && v < 0)
|
||||
r.AddError($"{OptTime.Name} must be non-negative.");
|
||||
});
|
||||
|
||||
this.AddArgsAndOpts();
|
||||
SetAction(PreviewAction);
|
||||
}
|
||||
|
||||
private void PreviewAction(ParseResult result)
|
||||
{
|
||||
// 读取模型
|
||||
using var spine = new SpineObject(result.GetValue(ArgSkel)!.FullName, result.GetValue(OptAtlas)?.FullName);
|
||||
|
||||
spine.UsePma = result.GetValue(OptPma);
|
||||
|
||||
// 设置要导出的动画
|
||||
int trackIdx = 0;
|
||||
foreach (var name in result.GetValue(OptAnimations))
|
||||
{
|
||||
if (!spine.Data.AnimationsByName.ContainsKey(name))
|
||||
{
|
||||
_logger.Warn("No animation named '{0}', skip it", name);
|
||||
continue;
|
||||
}
|
||||
spine.AnimationState.SetAnimation(trackIdx, name, true);
|
||||
trackIdx++;
|
||||
}
|
||||
|
||||
// 设置需要启用的皮肤
|
||||
foreach (var name in result.GetValue(OptSkins))
|
||||
{
|
||||
if (!spine.SetSkinStatus(name, true))
|
||||
{
|
||||
_logger.Warn("Failed to enable skin '{0}'", name);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置时间偏移量
|
||||
spine.Update(result.GetValue(OptTime));
|
||||
|
||||
using var exporter = GetExporterFilledWithArgs(result, spine);
|
||||
using var skImage = exporter.ExportMemoryImage(spine);
|
||||
var img = new CanvasImageAscii(skImage) { UsePixelCharacters = result.GetValue(OptUseChars) };
|
||||
AnsiConsole.Write(img);
|
||||
}
|
||||
|
||||
private FrameExporter GetExporterFilledWithArgs(ParseResult result, SpineObject spine)
|
||||
{
|
||||
// 根据模型获取自动分辨率和视区参数
|
||||
var bounds = spine.GetCurrentBounds();
|
||||
var resolution = new SFML.System.Vector2u((uint)bounds.Size.X, (uint)bounds.Size.Y);
|
||||
if (resolution.X >= MaxResolution || resolution.Y >= MaxResolution)
|
||||
{
|
||||
// 缩小到最大像素限制
|
||||
var scale = Math.Min(MaxResolution / bounds.Width, MaxResolution / bounds.Height);
|
||||
resolution.X = (uint)(bounds.Width * scale);
|
||||
resolution.Y = (uint)(bounds.Height * scale);
|
||||
}
|
||||
var viewBounds = bounds.GetCanvasBounds(resolution);
|
||||
|
||||
return new FrameExporter(resolution)
|
||||
{
|
||||
Size = new(viewBounds.Width, -viewBounds.Height),
|
||||
Center = viewBounds.Position + viewBounds.Size / 2,
|
||||
Rotation = 0,
|
||||
BackgroundColor = SFML.Graphics.Color.Transparent,
|
||||
|
||||
Format = SkiaSharp.SKEncodedImageFormat.Png,
|
||||
Quality = 100,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
121
SpineViewerCLI/QueryCommand.cs
Normal file
121
SpineViewerCLI/QueryCommand.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using NLog;
|
||||
using Spine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
public class QueryCommand : Command
|
||||
{
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly string _name = "query";
|
||||
private static readonly string _desc = "Query information of single model";
|
||||
|
||||
private static readonly string HalfHeader = new('>', 15);
|
||||
private static readonly char Separator = '\t';
|
||||
|
||||
public Argument<FileInfo> ArgSkel { get; } = new("skel")
|
||||
{
|
||||
Description = "Path of skel file.",
|
||||
};
|
||||
|
||||
public Option<FileInfo> OptAtlas { get; } = new("--atlas")
|
||||
{
|
||||
Description = "Path to the atlas file that matches the skel file.",
|
||||
};
|
||||
|
||||
public Option<bool> OptAll { get; } = new("--all")
|
||||
{
|
||||
Description = "Print all information",
|
||||
};
|
||||
|
||||
public Option<bool> OptSkin { get; } = new("--skin")
|
||||
{
|
||||
Description = "Print skins",
|
||||
};
|
||||
|
||||
public Option<bool> OptAnimation { get; } = new("--animation")
|
||||
{
|
||||
Description = "Print animations",
|
||||
};
|
||||
|
||||
public Option<bool> OptSlot { get; } = new("--slot")
|
||||
{
|
||||
Description = "Print slots",
|
||||
};
|
||||
|
||||
public QueryCommand() : base(_name, _desc)
|
||||
{
|
||||
this.AddArgsAndOpts();
|
||||
SetAction(QueryAction);
|
||||
}
|
||||
|
||||
private void QueryAction(ParseResult result)
|
||||
{
|
||||
// 读取模型
|
||||
using var spine = new SpineObject(result.GetValue(ArgSkel)!.FullName, result.GetValue(OptAtlas)?.FullName);
|
||||
|
||||
var all = result.GetValue(OptAll);
|
||||
|
||||
if (all || result.GetValue(OptSkin))
|
||||
{
|
||||
SkinRecord[] data = spine.Data.SkinsByName.Keys.Select(v => new SkinRecord(v)).ToArray();
|
||||
PrintData("Skins", SkinRecord.Headers, data);
|
||||
}
|
||||
if (all || result.GetValue(OptAnimation))
|
||||
{
|
||||
AnimationRecord[] data = spine.Data.Animations.Select(v => new AnimationRecord(v.Name, v.Duration)).ToArray();
|
||||
PrintData("Animations", AnimationRecord.Headers, data);
|
||||
}
|
||||
if (all || result.GetValue(OptSlot))
|
||||
{
|
||||
SlotRecord[] data = spine.Data.SlotAttachments.Select(v => new SlotRecord(v.Key, v.Value.Keys.ToArray())).ToArray();
|
||||
PrintData("Slots", SlotRecord.Headers, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintData(string dataName, string[] headers, RowRecord[] rows)
|
||||
{
|
||||
var header = $"{HalfHeader} {dataName} {HalfHeader}";
|
||||
var footer = new string('<', header.Length);
|
||||
|
||||
Console.WriteLine(header);
|
||||
Console.WriteLine(string.Join(Separator, headers));
|
||||
foreach (var row in rows)
|
||||
Console.WriteLine(string.Join(Separator, row.Values));
|
||||
Console.WriteLine(footer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public abstract record RowRecord
|
||||
{
|
||||
public abstract object[] Values { get; }
|
||||
}
|
||||
|
||||
public record SkinRecord(string Name) : RowRecord
|
||||
{
|
||||
public static string[] Headers { get; } = [nameof(Name)];
|
||||
|
||||
public override object[] Values => [Name];
|
||||
}
|
||||
|
||||
public record AnimationRecord(string Name, float Duration) : RowRecord
|
||||
{
|
||||
public static string[] Headers { get; } = [nameof(Name), nameof(Duration)];
|
||||
|
||||
public override object[] Values => [Name, Duration];
|
||||
}
|
||||
|
||||
public record SlotRecord(string Name, string[] Attachments) : RowRecord
|
||||
{
|
||||
public static string[] Headers { get; } = [nameof(Name), nameof(Attachments)];
|
||||
|
||||
public override object[] Values => [Name, string.Join(';', Attachments)];
|
||||
}
|
||||
}
|
||||
3
SpineViewerCLI/README.md
Normal file
3
SpineViewerCLI/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# SpineViewerCLI
|
||||
|
||||
基于 [System.Command](https://www.nuget.org/packages/System.CommandLine) 的命令行工具.
|
||||
@@ -1,352 +1,100 @@
|
||||
using System.Globalization;
|
||||
using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using NLog;
|
||||
using SkiaSharp;
|
||||
using Spectre.Console;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
using SkiaSharp;
|
||||
using System.CommandLine;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
public class CLI
|
||||
public static class SpineViewerCLI
|
||||
{
|
||||
const string USAGE = @"
|
||||
usage: SpineViewerCLI.exe [--skel PATH] [--atlas PATH] [--output PATH] [--animation STR] [--skin STR] [--hide-slot STR] [--pma] [--fps INT] [--loop] [--crf INT] [--time FLOAT] [--quality INT] [--width INT] [--height INT] [--centerx INT] [--centery INT] [--zoom FLOAT] [--speed FLOAT] [--color HEX] [--quiet]
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
options:
|
||||
--skel PATH Path to the .skel file
|
||||
--atlas PATH Path to the .atlas file, default searches in the skel file directory
|
||||
--output PATH Output file path. Extension determines export type (.mp4, .webm for video; .png, .jpg for frame)
|
||||
--animation STR Animation name
|
||||
--skin STR Skin name to apply. Can be used multiple times to stack skins.
|
||||
--hide-slot STR Slot name to hide. Can be used multiple times.
|
||||
--pma Use premultiplied alpha, default false
|
||||
--fps INT Frames per second (for video), default 24
|
||||
--loop Whether to loop the animation (for video), default false
|
||||
--crf INT Constant Rate Factor (for video), from 0 (lossless) to 51 (worst), default 23
|
||||
--time FLOAT Time in seconds to export a single frame. Providing this argument forces frame export mode.
|
||||
--quality INT Quality for lossy image formats (jpg, webp), from 0 to 100, default 80
|
||||
--width INT Output width, default 512
|
||||
--height INT Output height, default 512
|
||||
--centerx INT Center X offset, default automatically finds bounds
|
||||
--centery INT Center Y offset, default automatically finds bounds
|
||||
--zoom FLOAT Zoom level, default 1.0
|
||||
--speed FLOAT Speed of animation (for video), default 1.0
|
||||
--color HEX Background color as a hex RGBA color, default 000000ff (opaque black)
|
||||
--quiet Removes console progress log, default false
|
||||
";
|
||||
|
||||
public static void Main(string[] args)
|
||||
public static Option<bool> OptQuiet { get; } = new("--quiet", "-q")
|
||||
{
|
||||
string? skelPath = null;
|
||||
string? atlasPath = null;
|
||||
string? output = null;
|
||||
string? animation = null;
|
||||
var skins = new List<string>();
|
||||
var hideSlots = new List<string>();
|
||||
bool pma = false;
|
||||
uint fps = 24;
|
||||
bool loop = false;
|
||||
int crf = 23;
|
||||
float? time = null;
|
||||
int quality = 80;
|
||||
uint? width = null;
|
||||
uint? height = null;
|
||||
int? centerx = null;
|
||||
int? centery = null;
|
||||
float zoom = 1;
|
||||
float speed = 1;
|
||||
Color backgroundColor = Color.Black;
|
||||
bool quiet = false;
|
||||
Description = "Suppress console logging (quiet mode).",
|
||||
Recursive = true,
|
||||
};
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--help":
|
||||
Console.Write(USAGE);
|
||||
Environment.Exit(0);
|
||||
break;
|
||||
case "--skel":
|
||||
skelPath = args[++i];
|
||||
break;
|
||||
case "--atlas":
|
||||
atlasPath = args[++i];
|
||||
break;
|
||||
case "--output":
|
||||
output = args[++i];
|
||||
break;
|
||||
case "--animation":
|
||||
animation = args[++i];
|
||||
break;
|
||||
case "--skin":
|
||||
skins.Add(args[++i]);
|
||||
break;
|
||||
case "--hide-slot":
|
||||
hideSlots.Add(args[++i]);
|
||||
break;
|
||||
case "--pma":
|
||||
pma = true;
|
||||
break;
|
||||
case "--fps":
|
||||
fps = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--loop":
|
||||
loop = true;
|
||||
break;
|
||||
case "--crf":
|
||||
crf = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--time":
|
||||
time = float.Parse(args[++i]);
|
||||
break;
|
||||
case "--quality":
|
||||
quality = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--width":
|
||||
width = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--height":
|
||||
height = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--centerx":
|
||||
centerx = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--centery":
|
||||
centery = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--zoom":
|
||||
zoom = float.Parse(args[++i]);
|
||||
break;
|
||||
case "--speed":
|
||||
speed = float.Parse(args[++i]);
|
||||
break;
|
||||
case "--color":
|
||||
backgroundColor = new Color(uint.Parse(args[++i], NumberStyles.HexNumber));
|
||||
break;
|
||||
case "--quiet":
|
||||
quiet = true;
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown argument: {args[i]}");
|
||||
Environment.Exit(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(skelPath))
|
||||
{
|
||||
Console.Error.WriteLine("Missing --skel");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
Console.Error.WriteLine("Missing --output");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
var outputExtension = Path.GetExtension(output).TrimStart('.').ToLowerInvariant();
|
||||
|
||||
var sp = new SpineObject(skelPath, atlasPath);
|
||||
sp.UsePma = pma;
|
||||
|
||||
foreach (var skinName in skins)
|
||||
{
|
||||
if (!sp.SetSkinStatus(skinName, true))
|
||||
{
|
||||
var availableSkins = string.Join(", ", sp.Data.Skins.Select(s => s.Name));
|
||||
Console.Error.WriteLine($"Error: Skin '{skinName}' not found. Available skins: {availableSkins}");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(animation))
|
||||
{
|
||||
var availableAnimations = string.Join(", ", sp.Data.Animations.Select(a => a.Name));
|
||||
Console.Error.WriteLine($"Missing --animation. Available animations for {sp.Name}: {availableAnimations}");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
|
||||
var trackEntry = sp.AnimationState.SetAnimation(0, animation, loop);
|
||||
if (time.HasValue)
|
||||
{
|
||||
trackEntry.TrackTime = time.Value;
|
||||
}
|
||||
sp.Update(0);
|
||||
|
||||
foreach (var slotName in hideSlots)
|
||||
{
|
||||
if (!sp.SetSlotVisible(slotName, false))
|
||||
{
|
||||
if (!quiet) Console.WriteLine($"Warning: Slot '{slotName}' not found, cannot hide.");
|
||||
}
|
||||
}
|
||||
|
||||
if (time.HasValue)
|
||||
{
|
||||
if (TryGetImageFormat(outputExtension, out var imageFormat))
|
||||
{
|
||||
if (!quiet) Console.WriteLine($"Exporting single frame at {time.Value:F2}s to {output}...");
|
||||
|
||||
FrameExporter exporter;
|
||||
if (width is uint w && height is uint h && centerx is int cx && centery is int cy)
|
||||
{
|
||||
exporter = new FrameExporter(w, h)
|
||||
{
|
||||
Center = (cx, cy),
|
||||
Size = (w / zoom, -h / zoom),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var frameBounds = GetSpineObjectBounds(sp);
|
||||
var bounds = GetFloatRectCanvasBounds(frameBounds, new(width ?? 512, height ?? 512));
|
||||
exporter = new FrameExporter(width ?? (uint)Math.Ceiling(bounds.Width), height ?? (uint)Math.Ceiling(bounds.Height))
|
||||
{
|
||||
Center = bounds.Position + bounds.Size / 2,
|
||||
Size = (bounds.Width, -bounds.Height),
|
||||
};
|
||||
}
|
||||
exporter.Format = imageFormat;
|
||||
exporter.Quality = quality;
|
||||
exporter.BackgroundColor = backgroundColor;
|
||||
|
||||
exporter.Export(output, sp);
|
||||
|
||||
if (!quiet)
|
||||
Console.WriteLine("Frame export complete.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var validImageExtensions = "png, jpg, jpeg, webp, bmp";
|
||||
Console.Error.WriteLine($"Error: --time argument requires a valid image format extension. Supported formats are: {validImageExtensions}.");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
}
|
||||
else if (Enum.TryParse<FFmpegVideoExporter.VideoFormat>(outputExtension, true, out var videoFormat))
|
||||
{
|
||||
FFmpegVideoExporter exporter;
|
||||
if (width is uint w && height is uint h && centerx is int cx && centery is int cy)
|
||||
{
|
||||
exporter = new FFmpegVideoExporter(w, h)
|
||||
{
|
||||
Center = (cx, cy),
|
||||
Size = (w / zoom, -h / zoom),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var bounds = GetFloatRectCanvasBounds(GetSpineObjectAnimationBounds(sp, fps), new(width ?? 512, height ?? 512));
|
||||
exporter = new FFmpegVideoExporter(width ?? (uint)Math.Ceiling(bounds.Width), height ?? (uint)Math.Ceiling(bounds.Height))
|
||||
{
|
||||
Center = bounds.Position + bounds.Size / 2,
|
||||
Size = (bounds.Width, -bounds.Height),
|
||||
};
|
||||
}
|
||||
exporter.Duration = trackEntry.Animation.Duration;
|
||||
exporter.Fps = fps;
|
||||
exporter.Format = videoFormat;
|
||||
exporter.Loop = loop;
|
||||
exporter.Crf = crf;
|
||||
exporter.Speed = speed;
|
||||
exporter.BackgroundColor = backgroundColor;
|
||||
|
||||
if (!quiet)
|
||||
exporter.ProgressReporter = (total, done, text) => Console.Write($"\r{text}");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
exporter.Export(output, cts.Token, sp);
|
||||
|
||||
if (!quiet)
|
||||
Console.WriteLine("\nVideo export complete.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var validVideoExtensions = string.Join(", ", Enum.GetNames(typeof(FFmpegVideoExporter.VideoFormat)));
|
||||
var validImageExtensions = "png, jpg, jpeg, webp, bmp";
|
||||
Console.Error.WriteLine($"Invalid output extension or missing --time for image export. Supported video formats are: {validVideoExtensions}. Supported image formats (with --time) are: {validImageExtensions}.");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private static bool TryGetImageFormat(string extension, out SKEncodedImageFormat format)
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
switch (extension)
|
||||
InitializeFileLog();
|
||||
|
||||
var cmdRoot = new RootCommand("Root Command")
|
||||
{
|
||||
case "png":
|
||||
format = SKEncodedImageFormat.Png;
|
||||
return true;
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
format = SKEncodedImageFormat.Jpeg;
|
||||
return true;
|
||||
case "webp":
|
||||
format = SKEncodedImageFormat.Webp;
|
||||
return true;
|
||||
case "bmp":
|
||||
format = SKEncodedImageFormat.Bmp;
|
||||
return true;
|
||||
default:
|
||||
format = default;
|
||||
return false;
|
||||
OptQuiet,
|
||||
new QueryCommand(),
|
||||
new PreviewCommand(),
|
||||
new ExportCommand(),
|
||||
};
|
||||
|
||||
var result = cmdRoot.Parse(args);
|
||||
|
||||
if (!result.GetValue(OptQuiet))
|
||||
InitializeConsoleLog();
|
||||
|
||||
try
|
||||
{
|
||||
return result.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Fatal("Failed to execute, {0}", ex.Message);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static SpineObject CopySpineObject(SpineObject sp)
|
||||
private static void InitializeFileLog()
|
||||
{
|
||||
var spineObject = new SpineObject(sp, true);
|
||||
foreach (var tr in sp.AnimationState.IterTracks().Where(t => t is not null))
|
||||
// XXX: δ֪ԭ<D6AA><D4AD> linux ƽ̨<C6BD><CCA8><EFBFBD><EFBFBD><DEB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD>ļ<EFBFBD>
|
||||
var config = new NLog.Config.LoggingConfiguration();
|
||||
var fileTarget = new NLog.Targets.FileTarget("fileTarget")
|
||||
{
|
||||
var t = spineObject.AnimationState.SetAnimation(tr!.TrackIndex, tr.Animation, tr.Loop);
|
||||
}
|
||||
spineObject.Update(0);
|
||||
return spineObject;
|
||||
Encoding = System.Text.Encoding.UTF8,
|
||||
Layout = "${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${level:uppercase=true} - ${processid} - ${callsite-filename:includeSourcePath=false}:${callsite-linenumber} - ${message}",
|
||||
AutoFlush = true,
|
||||
CreateDirs = true,
|
||||
FileName = "${basedir}/logs/cli.log",
|
||||
ArchiveFileName = "${basedir}/logs/cli.{#}.log",
|
||||
ArchiveNumbering = NLog.Targets.ArchiveNumberingMode.Rolling,
|
||||
ArchiveAboveSize = 1048576,
|
||||
MaxArchiveFiles = 5,
|
||||
ConcurrentWrites = true,
|
||||
KeepFileOpen = false,
|
||||
};
|
||||
|
||||
config.AddTarget(fileTarget);
|
||||
config.AddRule(LogLevel.Trace, LogLevel.Fatal, fileTarget);
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
|
||||
static FloatRect GetSpineObjectBounds(SpineObject sp)
|
||||
private static void InitializeConsoleLog()
|
||||
{
|
||||
sp.Skeleton.GetBounds(out var x, out var y, out var w, out var h);
|
||||
return new(x, y, Math.Max(w, 1e-6f), Math.Max(h, 1e-6f));
|
||||
}
|
||||
static FloatRect FloatRectUnion(FloatRect a, FloatRect b)
|
||||
{
|
||||
float left = Math.Min(a.Left, b.Left);
|
||||
float top = Math.Min(a.Top, b.Top);
|
||||
float right = Math.Max(a.Left + a.Width, b.Left + b.Width);
|
||||
float bottom = Math.Max(a.Top + a.Height, b.Top + b.Height);
|
||||
return new FloatRect(left, top, right - left, bottom - top);
|
||||
}
|
||||
static FloatRect GetSpineObjectAnimationBounds(SpineObject sp, float fps = 10)
|
||||
{
|
||||
sp = CopySpineObject(sp);
|
||||
var bounds = GetSpineObjectBounds(sp);
|
||||
var maxDuration = sp.AnimationState.IterTracks().Select(t => t?.Animation.Duration ?? 0).DefaultIfEmpty(0).Max();
|
||||
sp.Update(0);
|
||||
for (float tick = 0, delta = 1 / fps; tick < maxDuration; tick += delta)
|
||||
var config = new NLog.Config.LoggingConfiguration();
|
||||
var consoleTarget = new NLog.Targets.ColoredConsoleTarget("consoleTarget")
|
||||
{
|
||||
bounds = FloatRectUnion(bounds, GetSpineObjectBounds(sp));
|
||||
sp.Update(delta);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
static FloatRect GetFloatRectCanvasBounds(FloatRect rect, Vector2u resolution)
|
||||
{
|
||||
float sizeW = rect.Width;
|
||||
float sizeH = rect.Height;
|
||||
float innerW = resolution.X;
|
||||
float innerH = resolution.Y;
|
||||
var scale = Math.Max(Math.Abs(sizeW / innerW), Math.Abs(sizeH / innerH));
|
||||
var scaleW = scale * Math.Sign(sizeW);
|
||||
var scaleH = scale * Math.Sign(sizeH);
|
||||
Encoding = System.Text.Encoding.UTF8,
|
||||
Layout = "[${level:format=OneLetter}]${date:format=yyyy-MM-dd HH\\:mm\\:ss} - ${message}",
|
||||
AutoFlush = true,
|
||||
DetectConsoleAvailable = true,
|
||||
StdErr = true,
|
||||
DetectOutputRedirected = true,
|
||||
};
|
||||
|
||||
innerW *= scaleW;
|
||||
innerH *= scaleH;
|
||||
consoleTarget.RowHighlightingRules.Add(new("level == LogLevel.Trace", NLog.Targets.ConsoleOutputColor.DarkGray, NLog.Targets.ConsoleOutputColor.NoChange));
|
||||
consoleTarget.RowHighlightingRules.Add(new("level == LogLevel.Debug", NLog.Targets.ConsoleOutputColor.DarkGray, NLog.Targets.ConsoleOutputColor.NoChange));
|
||||
consoleTarget.RowHighlightingRules.Add(new("level == LogLevel.Info", NLog.Targets.ConsoleOutputColor.DarkGray, NLog.Targets.ConsoleOutputColor.NoChange));
|
||||
consoleTarget.RowHighlightingRules.Add(new("level == LogLevel.Warn", NLog.Targets.ConsoleOutputColor.DarkYellow, NLog.Targets.ConsoleOutputColor.NoChange));
|
||||
consoleTarget.RowHighlightingRules.Add(new("level == LogLevel.Error", NLog.Targets.ConsoleOutputColor.Red, NLog.Targets.ConsoleOutputColor.NoChange));
|
||||
consoleTarget.RowHighlightingRules.Add(new("level == LogLevel.Fatal", NLog.Targets.ConsoleOutputColor.White, NLog.Targets.ConsoleOutputColor.DarkRed));
|
||||
|
||||
var x = rect.Left - (innerW - sizeW) / 2;
|
||||
var y = rect.Top - (innerH - sizeH) / 2;
|
||||
var w = resolution.X * scaleW;
|
||||
var h = resolution.Y * scaleH;
|
||||
return new(x, y, w, h);
|
||||
config.AddTarget(consoleTarget);
|
||||
config.AddRule(LogLevel.Trace, LogLevel.Fatal, consoleTarget);
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.0.1</Version>
|
||||
<Version>0.16.12</Version>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -16,7 +17,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SFMLRenderer\SFMLRenderer.csproj" />
|
||||
<PackageReference Include="NLog" Version="5.4.0" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.52.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Spine\Spine.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
67
SpineViewerCLI/Utils.cs
Normal file
67
SpineViewerCLI/Utils.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using SFML.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpineViewerCLI
|
||||
{
|
||||
public static class Utils
|
||||
{
|
||||
public static Color ParseColor(ArgumentResult result)
|
||||
{
|
||||
var token = result.Tokens.Count > 0 ? result.Tokens[0].Value : null;
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
return Color.Black;
|
||||
|
||||
try
|
||||
{
|
||||
// 去掉开头的 #
|
||||
var hex = token.Trim().TrimStart('#');
|
||||
|
||||
// 支持格式: RGB / ARGB / RRGGBB / AARRGGBB
|
||||
if (hex.Length == 3)
|
||||
{
|
||||
// #RGB → #RRGGBB
|
||||
var r = hex[0];
|
||||
var g = hex[1];
|
||||
var b = hex[2];
|
||||
hex = $"{r}{r}{g}{g}{b}{b}";
|
||||
hex = "FF" + hex; // 加上不透明 alpha
|
||||
}
|
||||
else if (hex.Length == 4)
|
||||
{
|
||||
// #ARGB → #AARRGGBB
|
||||
var a = hex[0];
|
||||
var r = hex[1];
|
||||
var g = hex[2];
|
||||
var b = hex[3];
|
||||
hex = $"{a}{a}{r}{r}{g}{g}{b}{b}";
|
||||
}
|
||||
else if (hex.Length == 6)
|
||||
{
|
||||
// #RRGGBB → #AARRGGBB
|
||||
hex = "FF" + hex;
|
||||
}
|
||||
else if (hex.Length != 8)
|
||||
{
|
||||
result.AddError("Invalid color format. Use #RGB, #ARGB, #RRGGBB, or #AARRGGBB.");
|
||||
return Color.Black;
|
||||
}
|
||||
|
||||
var aVal = Convert.ToByte(hex[..2], 16);
|
||||
var rVal = Convert.ToByte(hex.Substring(2, 2), 16);
|
||||
var gVal = Convert.ToByte(hex.Substring(4, 2), 16);
|
||||
var bVal = Convert.ToByte(hex.Substring(6, 2), 16);
|
||||
return new(rVal, gVal, bVal, aVal);
|
||||
}
|
||||
catch
|
||||
{
|
||||
result.AddError("Invalid color format.");
|
||||
return Color.Black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,8 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SpineViewer.Natives
|
||||
namespace Win32Natives
|
||||
{
|
||||
/// <summary>
|
||||
/// dwmapi.dll 包装类
|
||||
@@ -24,15 +23,15 @@ namespace SpineViewer.Natives
|
||||
[DllImport("dwmapi.dll")]
|
||||
private static extern int DwmSetWindowAttribute(IntPtr hwnd, uint dwAttribute, ref uint pvAttribute, int cbAttribute);
|
||||
|
||||
public static bool SetWindowCaptionColor(IntPtr hwnd, Color color)
|
||||
public static bool SetWindowCaptionColor(IntPtr hwnd, byte r, byte g, byte b)
|
||||
{
|
||||
int c = color.R | (color.G << 8) | (color.B << 16);
|
||||
int c = r | (g << 8) | (b << 16);
|
||||
return 0 == DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, ref c, sizeof(uint));
|
||||
}
|
||||
|
||||
public static bool SetWindowTextColor(IntPtr hwnd, Color color)
|
||||
public static bool SetWindowTextColor(IntPtr hwnd, byte r, byte g, byte b)
|
||||
{
|
||||
int c = color.R | (color.G << 8) | (color.B << 16);
|
||||
int c = r | (g << 8) | (b << 16);
|
||||
return 0 == DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, ref c, sizeof(uint));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewer.Natives
|
||||
namespace Win32Natives
|
||||
{
|
||||
/// <summary>
|
||||
/// gdi32.dll 包装类
|
||||
@@ -7,7 +7,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewer.Natives
|
||||
namespace Win32Natives
|
||||
{
|
||||
/// <summary>
|
||||
/// shell32.dll 包装类
|
||||
@@ -7,7 +7,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewer.Natives
|
||||
namespace Win32Natives
|
||||
{
|
||||
/// <summary>
|
||||
/// user32.dll 包装类
|
||||
@@ -316,7 +316,7 @@ namespace SpineViewer.Natives
|
||||
workerw = FindWindowEx(progman, IntPtr.Zero, "WorkerW", null);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"HWND(WorkerW): {workerw:x8}");
|
||||
Debug.WriteLine($"HWND(WorkerW): 0x{workerw:x8}");
|
||||
return workerw;
|
||||
}
|
||||
|
||||
14
Win32Natives/Win32Natives.csproj
Normal file
14
Win32Natives/Win32Natives.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Version>0.16.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user