Compare commits
130 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 | ||
|
|
0893bd4b54 | ||
|
|
862926b43e | ||
|
|
0324ba7971 | ||
|
|
6a17ec0397 | ||
|
|
53a7700798 | ||
|
|
30608e05bc | ||
|
|
3dcd7b22ca | ||
|
|
dae5d0b7c7 | ||
|
|
f5d3f93cde | ||
|
|
dbd7c13c32 | ||
|
|
b662d8f68a | ||
|
|
02445d36e5 | ||
|
|
b178e48e84 | ||
|
|
c90713ffe7 | ||
|
|
dc472cf2a8 | ||
|
|
03c599264e | ||
|
|
8f7297bea5 | ||
|
|
e4d655012b | ||
|
|
4b23c779d3 | ||
|
|
f5684a50dc | ||
|
|
579ce9f944 | ||
|
|
7aa88089b8 | ||
|
|
be983f8407 | ||
|
|
249b930602 | ||
|
|
6472f378b7 | ||
|
|
8672f0571c | ||
|
|
e7a990c1bd | ||
|
|
6727fa8e8f | ||
|
|
66d8c489b5 | ||
|
|
1931c4713a | ||
|
|
f19f172e7c | ||
|
|
092fa76124 | ||
|
|
a0b7db0a70 | ||
|
|
6438b46ea0 | ||
|
|
2bf73db9d3 | ||
|
|
03c4974c9f | ||
|
|
760fa3a451 | ||
|
|
018d8f5330 | ||
|
|
c9730e1a11 | ||
|
|
1f6e19e544 | ||
|
|
a1a0777791 | ||
|
|
887e3f76d2 | ||
|
|
8b622050fa | ||
|
|
20369aaf43 | ||
|
|
07c0e84b7d | ||
|
|
6770acaffd | ||
|
|
6201ccc7d1 | ||
|
|
965d1c469e | ||
|
|
b448ca8cb0 | ||
|
|
2204eb6c75 | ||
|
|
0abe063899 |
@@ -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
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,5 +1,59 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.16.12
|
||||
|
||||
- 修复 label 控件文字显示问题
|
||||
- 增强报错日志输出
|
||||
- 增加实时帧率显示
|
||||
- 首选项增加预览画面和投影最大帧率设置,移除用户状态和工作区帧率记忆
|
||||
- 优化某些性能
|
||||
|
||||
## v0.16.11
|
||||
|
||||
- 增加 shift 切换缩放倍数
|
||||
- 改善后台性能
|
||||
- 修复字体显示颜色问题
|
||||
- 调整浏览目录参数保存至用户状态
|
||||
- 调整浏览面板至最后
|
||||
|
||||
## v0.16.10
|
||||
|
||||
- 增加 Linux 平台 CLI 工具构建
|
||||
|
||||
## v0.16.9
|
||||
|
||||
- 重构 CLI 工具
|
||||
|
||||
## v0.16.8
|
||||
|
||||
- 去除首次的最小化提示弹框
|
||||
- 窗口布局改变后实时保存
|
||||
- 增加侧边栏图标和折叠功能
|
||||
- 增加皮肤和插槽参数面板的全部启用/禁用菜单项
|
||||
- 修改窗口默认大小
|
||||
- 支持复制并应用单独的模型皮肤或插槽参数
|
||||
|
||||
## v0.16.7
|
||||
|
||||
- 修复空帧导致的包围盒计算错误
|
||||
- 修复重复启动程序无法唤出界面的问题
|
||||
|
||||
## v0.16.6
|
||||
|
||||
- 修复控件尺寸为0时导致的画面缩放错误
|
||||
|
||||
## v0.16.5
|
||||
|
||||
- 修复对于无 size 行的旧 atlas 格式读取错误
|
||||
- 修复托盘化之后无法联动显示窗口的问题
|
||||
|
||||
## v0.16.4
|
||||
|
||||
- 增加 apng 导出格式
|
||||
- 增加颜色拾取器面板
|
||||
- 增加程序皮肤(主题颜色)首选项
|
||||
- 优化部分使用体验
|
||||
|
||||
## v0.16.3
|
||||
|
||||
- 修复加载工作区时的顺序错误
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -240,8 +240,8 @@ namespace SFMLRenderer
|
||||
if (RenderWindow is null) return;
|
||||
float parentW = (float)sizeInfo.NewSize.Width;
|
||||
float parentH = (float)sizeInfo.NewSize.Height;
|
||||
float renderW = (float)_hwndHost.ActualWidth;
|
||||
float renderH = (float)_hwndHost.ActualHeight;
|
||||
float renderW = _resolution.X;
|
||||
float renderH = _resolution.Y;
|
||||
float scale = Math.Min(parentW / renderW, parentH / renderH); // 两方向取较小值, 保证 parent 覆盖 render
|
||||
renderW *= scale;
|
||||
renderH *= scale;
|
||||
|
||||
@@ -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,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.0</Version>
|
||||
<Version>0.16.6</Version>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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>
|
||||
/// 获取适合指定画布参数下能够覆盖包围盒的画布视区包围盒
|
||||
@@ -53,31 +49,11 @@ namespace SpineViewer.Extensions
|
||||
public static FloatRect GetBounds(this View self)
|
||||
{
|
||||
return new(
|
||||
self.Center.X - self.Size.X / 2,
|
||||
self.Center.Y - self.Size.Y / 2,
|
||||
self.Size.X,
|
||||
self.Center.X - self.Size.X / 2,
|
||||
self.Center.Y - self.Size.Y / 2,
|
||||
self.Size.X,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,40 @@ namespace Spine.Exporters
|
||||
{
|
||||
Gif,
|
||||
Webp,
|
||||
Apng,
|
||||
Mp4,
|
||||
Webm,
|
||||
Mkv,
|
||||
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>
|
||||
@@ -41,34 +69,40 @@ namespace Spine.Exporters
|
||||
private VideoFormat _format = VideoFormat.Mp4;
|
||||
|
||||
/// <summary>
|
||||
/// 动图是否循环 [Gif/Webp]
|
||||
/// [Gif/Webp/Apng] 动图是否循环
|
||||
/// </summary>
|
||||
public bool Loop { get => _loop; set => _loop = value; }
|
||||
private bool _loop = true;
|
||||
|
||||
/// <summary>
|
||||
/// 质量 [Webp]
|
||||
/// [Webp] 质量
|
||||
/// </summary>
|
||||
public int Quality { get => _quality; set => _quality = Math.Clamp(value, 0, 100); }
|
||||
private int _quality = 75;
|
||||
|
||||
/// <summary>
|
||||
/// 无损压缩 [Webp]
|
||||
/// [Webp] 无损压缩
|
||||
/// </summary>
|
||||
public bool Lossless { get => _lossless; set => _lossless = value; }
|
||||
private bool _lossless = false;
|
||||
|
||||
/// <summary>
|
||||
/// CRF [Mp4/Webm/Mkv]
|
||||
/// [Apng] 预测器算法
|
||||
/// </summary>
|
||||
public ApngPredMethod PredMethod { get => _predMethod; set => _predMethod = value; }
|
||||
private ApngPredMethod _predMethod = ApngPredMethod.Mixed;
|
||||
|
||||
/// <summary>
|
||||
/// [Mp4/Webm/Mkv] CRF
|
||||
/// </summary>
|
||||
public int Crf { get => _crf; set => _crf = Math.Clamp(value, 0, 63); }
|
||||
private int _crf = 23;
|
||||
|
||||
/// <summary>
|
||||
/// prores_ks 编码器的配置等级, -1 是自动, 越高质量越好, 只有 4 及以上才有透明通道 [Mov]
|
||||
/// [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>
|
||||
/// 获取的一帧, 结果是预乘的
|
||||
@@ -93,6 +127,7 @@ namespace Spine.Exporters
|
||||
{
|
||||
VideoFormat.Gif => SetGifOptions,
|
||||
VideoFormat.Webp => SetWebpOptions,
|
||||
VideoFormat.Apng => SetApngOptions,
|
||||
VideoFormat.Mp4 => SetMp4Options,
|
||||
VideoFormat.Webm => SetWebmOptions,
|
||||
VideoFormat.Mkv => SetMkvOptions,
|
||||
@@ -109,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);
|
||||
}
|
||||
}
|
||||
@@ -132,6 +167,13 @@ namespace Spine.Exporters
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
|
||||
private void SetApngOptions(FFMpegArgumentOptions options)
|
||||
{
|
||||
var customArgs = $"-vf unpremultiply=inplace=1 -plays {(_loop ? 0 : 1)} -pred {(int)_predMethod}";
|
||||
options.ForceFormat("apng").WithVideoCodec("apng").ForcePixelFormat("rgba")
|
||||
.WithCustomArgument(customArgs);
|
||||
}
|
||||
|
||||
private void SetMp4Options(FFMpegArgumentOptions options)
|
||||
{
|
||||
// XXX: windows 默认播放器在播放 MP4 格式时对于 libx264 编码器只支持 yuv420p 的像素格式
|
||||
@@ -164,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++;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,14 @@ namespace Spine.Implementations
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Load(SpineRuntime34.AtlasPage page, string path)
|
||||
@@ -147,6 +155,14 @@ namespace Spine.Implementations
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Load(SpineRuntime35.AtlasPage page, string path)
|
||||
@@ -182,6 +198,14 @@ namespace Spine.Implementations
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Load(SpineRuntime36.AtlasPage page, string path)
|
||||
@@ -217,6 +241,14 @@ namespace Spine.Implementations
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Load(SpineRuntime37.AtlasPage page, string path)
|
||||
@@ -252,6 +284,14 @@ namespace Spine.Implementations
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Load(SpineRuntime38.AtlasPage page, string path)
|
||||
@@ -288,9 +328,13 @@ namespace Spine.Implementations
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 似乎是不需要设置的, 因为存在某些 png 和 atlas 大小不同的情况, 一般是有一些缩放, 如果设置了反而渲染异常
|
||||
// page.width = (int)texture.Size.X;
|
||||
// page.height = (int)texture.Size.Y;
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Load(SpineRuntime40.AtlasPage page, string path)
|
||||
@@ -326,6 +370,14 @@ namespace Spine.Implementations
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Load(SpineRuntime41.AtlasPage page, string path)
|
||||
@@ -361,6 +413,14 @@ namespace Spine.Implementations
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Load(SpineRuntime42.AtlasPage page, string path)
|
||||
@@ -396,6 +456,14 @@ namespace Spine.Implementations
|
||||
if (ForceMipmap) texture.GenerateMipmap();
|
||||
|
||||
page.rendererObject = texture;
|
||||
|
||||
// 有些旧的 atlas 会省略 size 行, 这时需要在读取纹理时赋值
|
||||
if (page.width <= 0 || page.height <= 0)
|
||||
{
|
||||
var texSize = texture.Size;
|
||||
page.width = (int)texSize.X;
|
||||
page.height = (int)texSize.Y;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Unload(object texture)
|
||||
|
||||
@@ -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.3</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" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NLog;
|
||||
using Spine.Interfaces;
|
||||
using Spine.Interfaces.Attachments;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -6,7 +7,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Spine.Interfaces
|
||||
namespace Spine
|
||||
{
|
||||
/// <summary>
|
||||
/// 命中测试等级枚举值
|
||||
@@ -109,20 +110,42 @@ namespace Spine.Interfaces
|
||||
break;
|
||||
}
|
||||
|
||||
for (int ii = 0; ii + 1 < verticesLength; ii += 2)
|
||||
if (verticesLength > 0)
|
||||
{
|
||||
float vx = vertices[ii];
|
||||
float vy = vertices[ii + 1];
|
||||
minX = Math.Min(minX, vx);
|
||||
minY = Math.Min(minY, vy);
|
||||
maxX = Math.Max(maxX, vx);
|
||||
maxY = Math.Max(maxY, vy);
|
||||
for (int ii = 0; ii + 1 < verticesLength; ii += 2)
|
||||
{
|
||||
float vx = vertices[ii];
|
||||
float vy = vertices[ii + 1];
|
||||
minX = Math.Min(minX, vx);
|
||||
minY = Math.Min(minY, vy);
|
||||
maxX = Math.Max(maxX, vx);
|
||||
maxY = Math.Max(maxY, vy);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var boneX = slot.Bone.WorldX;
|
||||
var boneY = slot.Bone.WorldY;
|
||||
minX = Math.Min(minX, boneX);
|
||||
minY = Math.Min(minY, boneY);
|
||||
maxX = Math.Max(maxX, boneX);
|
||||
maxY = Math.Max(maxY, boneY);
|
||||
}
|
||||
}
|
||||
x = minX;
|
||||
y = minY;
|
||||
w = maxX - minX;
|
||||
h = maxY - minY;
|
||||
if (minX >= int.MaxValue || minY >= int.MaxValue || maxX <= int.MinValue || maxY <= int.MinValue)
|
||||
{
|
||||
x = self.X;
|
||||
y = self.Y;
|
||||
w = 0;
|
||||
h = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = minX;
|
||||
y = minY;
|
||||
w = maxX - minX;
|
||||
h = maxY - minY;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,7 +159,7 @@ namespace Spine.Interfaces
|
||||
if (HitTestLevel == HitTestLevel.None || HitTestLevel == HitTestLevel.Bounds)
|
||||
{
|
||||
self.GetBounds(out var bx, out var by, out var bw, out var bh);
|
||||
return x >= bx && x <= (bx + bw) && y >= by && y <= (by + bh);
|
||||
return x >= bx && x <= bx + bw && y >= by && y <= by + bh;
|
||||
}
|
||||
else if (HitTestLevel == HitTestLevel.Meshes || HitTestLevel == HitTestLevel.Pixels)
|
||||
{
|
||||
@@ -179,7 +202,7 @@ namespace Spine.Interfaces
|
||||
float c2 = Cross(x2, y2, x0, y0);
|
||||
|
||||
// 判断是否全部同号 (或为 0, 点在边上)
|
||||
if ((c0 >= 0 && c1 >= 0 && c2 >= 0) || (c0 <= 0 && c1 <= 0 && c2 <= 0))
|
||||
if (c0 >= 0 && c1 >= 0 && c2 >= 0 || c0 <= 0 && c1 <= 0 && c2 <= 0)
|
||||
{
|
||||
if (HitTestLevel == HitTestLevel.Meshes)
|
||||
return true;
|
||||
@@ -229,7 +252,7 @@ namespace Spine.Interfaces
|
||||
if (HitTestLevel == HitTestLevel.None)
|
||||
{
|
||||
self.GetBounds(out var bx, out var by, out var bw, out var bh);
|
||||
return x >= bx && x <= (bx + bw) && y >= by && y <= (by + bh);
|
||||
return x >= bx && x <= bx + bw && y >= by && y <= by + bh;
|
||||
}
|
||||
|
||||
bool hit = false;
|
||||
@@ -246,7 +269,7 @@ namespace Spine.Interfaces
|
||||
|
||||
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
|
||||
|
||||
@@ -8,28 +8,11 @@
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Geometries.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Strings/zh.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Skins/Light.xaml"/>
|
||||
<ResourceDictionary Source="/Resources/Theme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource ToggleButtonSwitch}">
|
||||
<Setter Property="hc:VisualElement.HighlightBrush" Value="{StaticResource DarkSuccessBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type ListBox}" BasedOn="{StaticResource ListBoxBaseStyle}">
|
||||
<Setter Property="SelectionMode" Value="Extended"/>
|
||||
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Visible"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type ListView}" BasedOn="{StaticResource ListViewBaseStyle}">
|
||||
<Setter Property="SelectionMode" Value="Extended"/>
|
||||
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -1,6 +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;
|
||||
@@ -12,6 +14,8 @@ using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using SpineViewer.Extensions;
|
||||
|
||||
namespace SpineViewer
|
||||
{
|
||||
@@ -21,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);
|
||||
@@ -54,21 +60,23 @@ 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 通信
|
||||
_instanceMutex = new Mutex(true, MutexName, out var createdNew);
|
||||
if (!createdNew)
|
||||
{
|
||||
ShowExistedInstance();
|
||||
SendCommandLineArgs();
|
||||
ConnectAndSendArgs();
|
||||
Environment.Exit(0); // 不再启动新实例
|
||||
return;
|
||||
}
|
||||
@@ -83,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,
|
||||
};
|
||||
@@ -98,62 +108,34 @@ namespace SpineViewer
|
||||
LogManager.Configuration = config;
|
||||
}
|
||||
|
||||
private static void ShowExistedInstance()
|
||||
private static void ConnectAndSendArgs()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 遍历同名进程
|
||||
var processes = Process.GetProcessesByName(ProcessName);
|
||||
foreach (var p in processes)
|
||||
{
|
||||
// 跳过当前进程
|
||||
if (p.Id == Process.GetCurrentProcess().Id)
|
||||
continue;
|
||||
|
||||
IntPtr hWnd = p.MainWindowHandle;
|
||||
if (hWnd != IntPtr.Zero)
|
||||
{
|
||||
// 3. 显示并置顶窗口
|
||||
if (User32.IsIconic(hWnd))
|
||||
{
|
||||
User32.ShowWindow(hWnd, User32.SW_RESTORE);
|
||||
}
|
||||
User32.SetForegroundWindow(hWnd);
|
||||
break; // 找到一个就可以退出
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略异常,不影响当前进程退出
|
||||
}
|
||||
}
|
||||
|
||||
private static void SendCommandLineArgs()
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
|
||||
if (args.Length <= 0)
|
||||
return;
|
||||
|
||||
_logger.Info("Send command line args to existed instance, \"{0}\"", string.Join(", ", args));
|
||||
try
|
||||
{
|
||||
// 已有实例在运行,把参数通过命名管道发过去
|
||||
using (var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out))
|
||||
{
|
||||
client.Connect(10000); // 10 秒超时
|
||||
using (var writer = new StreamWriter(client))
|
||||
// 只要启动了实例就要进行连接, 10 秒超时
|
||||
client.Connect(10000);
|
||||
|
||||
// 但是只有有参数的时候才发送参数
|
||||
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
|
||||
if (args.Length > 0)
|
||||
{
|
||||
foreach (var v in args)
|
||||
_logger.Info("Send command line args to existed instance, \"{0}\"", string.Join(", ", args));
|
||||
using (var writer = new StreamWriter(client))
|
||||
{
|
||||
writer.WriteLine(v);
|
||||
foreach (var v in args)
|
||||
{
|
||||
writer.WriteLine(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -162,6 +144,7 @@ namespace SpineViewer
|
||||
{
|
||||
var t = new Task(() =>
|
||||
{
|
||||
// 防止实例和窗口还没创建好
|
||||
while (Current is null) Thread.Sleep(10);
|
||||
while (true)
|
||||
{
|
||||
@@ -174,26 +157,48 @@ namespace SpineViewer
|
||||
}
|
||||
while (true)
|
||||
{
|
||||
using (var server = new NamedPipeServerStream(PipeName, PipeDirection.In))
|
||||
try
|
||||
{
|
||||
server.WaitForConnection();
|
||||
using (var reader = new StreamReader(server))
|
||||
using (var server = new NamedPipeServerStream(PipeName, PipeDirection.In))
|
||||
{
|
||||
var args = new List<string>();
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
args.Add(line);
|
||||
server.WaitForConnection();
|
||||
|
||||
if (args.Count > 0)
|
||||
// 只要收到连接就可以显示窗口了
|
||||
Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Current.Dispatcher.Invoke(() =>
|
||||
var window = (MainWindow)Current.MainWindow;
|
||||
window.Show();
|
||||
if (window.WindowState == WindowState.Minimized)
|
||||
{
|
||||
var vm = (MainWindowViewModel)((MainWindow)Current.MainWindow).DataContext;
|
||||
vm.SpineObjectListViewModel.AddSpineObjectFromFileList(args);
|
||||
});
|
||||
window.WindowState = WindowState.Normal;
|
||||
}
|
||||
window.Activate();
|
||||
});
|
||||
using (var reader = new StreamReader(server))
|
||||
{
|
||||
var args = new List<string>();
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
args.Add(line);
|
||||
|
||||
if (args.Count > 0)
|
||||
{
|
||||
Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
// 尝试加载参数内容
|
||||
var window = (MainWindow)Current.MainWindow;
|
||||
var vm = (MainWindowViewModel)window.DataContext;
|
||||
vm.SpineObjectListViewModel.AddSpineObjectFromFileList(args);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to process arguments, {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
}, default, TaskCreationOptions.LongRunning);
|
||||
t.Start();
|
||||
@@ -212,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
|
||||
@@ -231,8 +237,8 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to query autorun registry key, {0}", ex.Message);
|
||||
_logger.Trace(ex.ToString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -259,8 +265,8 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to set autorun registry key, {0}", ex.Message);
|
||||
_logger.Trace(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,7 +341,7 @@ namespace SpineViewer
|
||||
get => _language;
|
||||
set
|
||||
{
|
||||
var uri = $"Resources/Strings/{value.ToString().ToLower()}.xaml";
|
||||
var uri = $"Resources/Strings/{value.ToString().ToLowerInvariant()}.xaml";
|
||||
try
|
||||
{
|
||||
Resources.MergedDictionaries.Add(new() { Source = new(uri, UriKind.Relative) });
|
||||
@@ -343,12 +349,35 @@ namespace SpineViewer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to switch language to {0}, {1}", value, ex.Message);
|
||||
_logger.Trace(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
private AppLanguage _language = AppLanguage.ZH;
|
||||
|
||||
public AppSkin Skin
|
||||
{
|
||||
get => _skin;
|
||||
set
|
||||
{
|
||||
var uri = $"Resources/Skins/{value.ToString().ToLowerInvariant()}.xaml";
|
||||
try
|
||||
{
|
||||
Resources.MergedDictionaries.Add(new() { Source = new(uri, UriKind.Relative) });
|
||||
Resources.MergedDictionaries.Add(new() { Source = new("Resources/Theme.xaml", UriKind.Relative) });
|
||||
Current.MainWindow.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
Current.MainWindow.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
_skin = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to switch skin to {0}, {1}", value, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
private AppSkin _skin = AppSkin.Light;
|
||||
}
|
||||
|
||||
public enum AppLanguage
|
||||
@@ -357,4 +386,11 @@ namespace SpineViewer
|
||||
EN,
|
||||
JA
|
||||
}
|
||||
|
||||
public enum AppSkin
|
||||
{
|
||||
Light,
|
||||
Dark,
|
||||
Violet,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Spine;
|
||||
using Spine.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -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,9 +60,21 @@ 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)?.ToLower();
|
||||
// var ext = Path.GetExtension(path)?.ToLowerInvariant();
|
||||
// BitmapEncoder encoder = ext switch
|
||||
// {
|
||||
// ".jpg" or ".jpeg" => new JpegBitmapEncoder(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Spine.Interfaces;
|
||||
using Spine;
|
||||
using SpineViewer.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -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(() =>
|
||||
@@ -87,19 +103,16 @@ namespace SpineViewer.Models
|
||||
private AppLanguage _appLanguage;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _renderSelectedOnly;
|
||||
|
||||
[ObservableProperty]
|
||||
private HitTestLevel _hitTestLevel;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _logHitSlots;
|
||||
private AppSkin _appSkin;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _wallpaperView;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool? _closeToTray = null;
|
||||
private uint _wallpaperMaxFps = 30;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _closeToTray;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _autoRun;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Spine.Interfaces;
|
||||
using SpineViewer.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -13,6 +14,15 @@ namespace SpineViewer.Models
|
||||
{
|
||||
public class SpineObjectConfigModel
|
||||
{
|
||||
public class TrackConfigModel
|
||||
{
|
||||
public string AnimationName { get; set; } = "";
|
||||
|
||||
public float TimeScale { get; set; } = 1f;
|
||||
|
||||
public float Alpha { get; set; } = 1f;
|
||||
}
|
||||
|
||||
public bool UsePma { get; set; }
|
||||
|
||||
public string Physics { get; set; } = ISkeleton.Physics.Update.ToString();
|
||||
@@ -57,14 +67,6 @@ namespace SpineViewer.Models
|
||||
|
||||
public bool DebugClippings { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class TrackConfigModel
|
||||
{
|
||||
public string AnimationName { get; set; } = "";
|
||||
|
||||
public float TimeScale { get; set; } = 1f;
|
||||
|
||||
public float Alpha { get; set; } = 1f;
|
||||
public override string ToString() => JsonHelper.Serialize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ namespace SpineViewer.Models
|
||||
|
||||
public event EventHandler<TrackPropertyChangedEventArgs>? TrackPropertyChanged;
|
||||
|
||||
#region 参数面板实现
|
||||
|
||||
public SpineVersion Version => _spineObject.Version;
|
||||
|
||||
public string AssetsDir => _spineObject.AssetsDir;
|
||||
@@ -407,6 +409,8 @@ namespace SpineViewer.Models
|
||||
set { lock (_lock) SetProperty(_spineObject.DebugClippings, value, v => _spineObject.DebugClippings = v); }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Update(float delta)
|
||||
{
|
||||
lock (_lock) _spineObject.Update(delta);
|
||||
@@ -493,41 +497,57 @@ namespace SpineViewer.Models
|
||||
return config;
|
||||
}
|
||||
}
|
||||
set
|
||||
set => ApplyObjectConfig(value);
|
||||
}
|
||||
|
||||
public void ApplyObjectConfig(SpineObjectConfigModel m, SpineObjectConfigApplyFlag flag = SpineObjectConfigApplyFlag.All)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
lock (_lock)
|
||||
if (flag == SpineObjectConfigApplyFlag.All)
|
||||
{
|
||||
_spineObject.Skeleton.ScaleX = value.Scale;
|
||||
_spineObject.Skeleton.ScaleY = value.Scale;
|
||||
_spineObject.Skeleton.ScaleX = m.Scale;
|
||||
_spineObject.Skeleton.ScaleY = m.Scale;
|
||||
OnPropertyChanged(nameof(Scale));
|
||||
SetProperty(_spineObject.Skeleton.ScaleX < 0, value.FlipX, v => _spineObject.Skeleton.ScaleX *= -1, nameof(FlipX));
|
||||
SetProperty(_spineObject.Skeleton.ScaleY < 0, value.FlipY, v => _spineObject.Skeleton.ScaleY *= -1, nameof(FlipY));
|
||||
SetProperty(_spineObject.Skeleton.X, value.X, v => _spineObject.Skeleton.X = v, nameof(X));
|
||||
SetProperty(_spineObject.Skeleton.Y, value.Y, v => _spineObject.Skeleton.Y = v, nameof(Y));
|
||||
SetProperty(_spineObject.UsePma, value.UsePma, v => _spineObject.UsePma = v, nameof(UsePma));
|
||||
SetProperty(_spineObject.Physics, Enum.Parse<ISkeleton.Physics>(value.Physics ?? "Update", true), v => _spineObject.Physics = v, nameof(Physics));
|
||||
SetProperty(_spineObject.AnimationState.TimeScale, value.TimeScale, v => _spineObject.AnimationState.TimeScale = v, nameof(TimeScale));
|
||||
SetProperty(_spineObject.Skeleton.ScaleX < 0, m.FlipX, v => _spineObject.Skeleton.ScaleX *= -1, nameof(FlipX));
|
||||
SetProperty(_spineObject.Skeleton.ScaleY < 0, m.FlipY, v => _spineObject.Skeleton.ScaleY *= -1, nameof(FlipY));
|
||||
SetProperty(_spineObject.Skeleton.X, m.X, v => _spineObject.Skeleton.X = v, nameof(X));
|
||||
SetProperty(_spineObject.Skeleton.Y, m.Y, v => _spineObject.Skeleton.Y = v, nameof(Y));
|
||||
SetProperty(_spineObject.UsePma, m.UsePma, v => _spineObject.UsePma = v, nameof(UsePma));
|
||||
SetProperty(_spineObject.Physics, Enum.Parse<ISkeleton.Physics>(m.Physics ?? "Update", true), v => _spineObject.Physics = v, nameof(Physics));
|
||||
SetProperty(_spineObject.AnimationState.TimeScale, m.TimeScale, v => _spineObject.AnimationState.TimeScale = v, nameof(TimeScale));
|
||||
}
|
||||
|
||||
foreach (var name in _spineObject.Data.Skins.Select(v => v.Name).Except(value.LoadedSkins))
|
||||
if (flag == SpineObjectConfigApplyFlag.All || flag == SpineObjectConfigApplyFlag.Skin)
|
||||
{
|
||||
foreach (var name in _spineObject.Data.Skins.Select(v => v.Name).Except(m.LoadedSkins))
|
||||
if (_spineObject.SetSkinStatus(name, false))
|
||||
SkinStatusChanged?.Invoke(this, new(name, false));
|
||||
foreach (var name in value.LoadedSkins)
|
||||
foreach (var name in m.LoadedSkins)
|
||||
if (_spineObject.SetSkinStatus(name, true))
|
||||
SkinStatusChanged?.Invoke(this, new(name, true));
|
||||
}
|
||||
|
||||
foreach (var (slotName, attachmentName) in value.SlotAttachment)
|
||||
if (flag == SpineObjectConfigApplyFlag.SlotAttachement)
|
||||
{
|
||||
foreach (var (slotName, attachmentName) in m.SlotAttachment)
|
||||
if (_spineObject.SetAttachment(slotName, attachmentName))
|
||||
SlotAttachmentChanged?.Invoke(this, new(slotName, attachmentName));
|
||||
}
|
||||
|
||||
foreach (var slotName in value.DisabledSlots)
|
||||
if (flag == SpineObjectConfigApplyFlag.SlotVisibility)
|
||||
{
|
||||
foreach (var slotName in m.DisabledSlots)
|
||||
if (_spineObject.SetSlotVisible(slotName, false))
|
||||
SlotVisibleChanged?.Invoke(this, new(slotName, false));
|
||||
}
|
||||
|
||||
if (flag == SpineObjectConfigApplyFlag.All)
|
||||
{
|
||||
// XXX: 处理空动画
|
||||
_spineObject.AnimationState.ClearTracks();
|
||||
int trackIndex = 0;
|
||||
foreach (var trConfig in value.Animations)
|
||||
foreach (var trConfig in m.Animations)
|
||||
{
|
||||
if (trConfig is not null && !string.IsNullOrEmpty(trConfig.AnimationName))
|
||||
{
|
||||
@@ -545,16 +565,16 @@ namespace SpineViewer.Models
|
||||
_spineObject.Skeleton.SetSlotsToSetupPose();
|
||||
}
|
||||
|
||||
SetProperty(_spineObject.DebugTexture, value.DebugTexture, v => _spineObject.DebugTexture = v, nameof(DebugTexture));
|
||||
SetProperty(_spineObject.DebugBounds, value.DebugBounds, v => _spineObject.DebugBounds = v, nameof(DebugBounds));
|
||||
SetProperty(_spineObject.DebugBones, value.DebugBones, v => _spineObject.DebugBones = v, nameof(DebugBones));
|
||||
SetProperty(_spineObject.DebugRegions, value.DebugRegions, v => _spineObject.DebugRegions = v, nameof(DebugRegions));
|
||||
SetProperty(_spineObject.DebugMeshHulls, value.DebugMeshHulls, v => _spineObject.DebugMeshHulls = v, nameof(DebugMeshHulls));
|
||||
SetProperty(_spineObject.DebugMeshes, value.DebugMeshes, v => _spineObject.DebugMeshes = v, nameof(DebugMeshes));
|
||||
SetProperty(_spineObject.DebugBoundingBoxes, value.DebugBoundingBoxes, v => _spineObject.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes));
|
||||
SetProperty(_spineObject.DebugPaths, value.DebugPaths, v => _spineObject.DebugPaths = v, nameof(DebugPaths));
|
||||
SetProperty(_spineObject.DebugPoints, value.DebugPoints, v => _spineObject.DebugPoints = v, nameof(DebugPoints));
|
||||
SetProperty(_spineObject.DebugClippings, value.DebugClippings, v => _spineObject.DebugClippings = v, nameof(DebugClippings));
|
||||
SetProperty(_spineObject.DebugTexture, m.DebugTexture, v => _spineObject.DebugTexture = v, nameof(DebugTexture));
|
||||
SetProperty(_spineObject.DebugBounds, m.DebugBounds, v => _spineObject.DebugBounds = v, nameof(DebugBounds));
|
||||
SetProperty(_spineObject.DebugBones, m.DebugBones, v => _spineObject.DebugBones = v, nameof(DebugBones));
|
||||
SetProperty(_spineObject.DebugRegions, m.DebugRegions, v => _spineObject.DebugRegions = v, nameof(DebugRegions));
|
||||
SetProperty(_spineObject.DebugMeshHulls, m.DebugMeshHulls, v => _spineObject.DebugMeshHulls = v, nameof(DebugMeshHulls));
|
||||
SetProperty(_spineObject.DebugMeshes, m.DebugMeshes, v => _spineObject.DebugMeshes = v, nameof(DebugMeshes));
|
||||
SetProperty(_spineObject.DebugBoundingBoxes, m.DebugBoundingBoxes, v => _spineObject.DebugBoundingBoxes = v, nameof(DebugBoundingBoxes));
|
||||
SetProperty(_spineObject.DebugPaths, m.DebugPaths, v => _spineObject.DebugPaths = v, nameof(DebugPaths));
|
||||
SetProperty(_spineObject.DebugPoints, m.DebugPoints, v => _spineObject.DebugPoints = v, nameof(DebugPoints));
|
||||
SetProperty(_spineObject.DebugClippings, m.DebugClippings, v => _spineObject.DebugClippings = v, nameof(DebugClippings));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -613,6 +633,17 @@ namespace SpineViewer.Models
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可选的应用部分模型参数项
|
||||
/// </summary>
|
||||
public enum SpineObjectConfigApplyFlag
|
||||
{
|
||||
All,
|
||||
Skin,
|
||||
SlotAttachement,
|
||||
SlotVisibility,
|
||||
}
|
||||
|
||||
public class SkinStatusChangedEventArgs(string name, bool status) : EventArgs
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Windows.Media;
|
||||
|
||||
namespace SpineViewer.Models
|
||||
{
|
||||
public class LastStateModel
|
||||
public class UserStateModel
|
||||
{
|
||||
#region 画面布局状态
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace SpineViewer.Models
|
||||
public double WindowHeight { get; set; }
|
||||
public WindowState WindowState { get; set; }
|
||||
|
||||
public bool RootGridCol0Folded { get; set; }
|
||||
public double RootGridCol0Width { get; set; }
|
||||
public double RootGridCol2Width { get; set; }
|
||||
|
||||
@@ -32,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;
|
||||
|
||||
@@ -12,7 +12,102 @@ namespace SpineViewer.Resources
|
||||
/// </summary>
|
||||
public static class AppResource
|
||||
{
|
||||
public static T Get<T>(string key) => (T)App.Current.FindResource(key);
|
||||
private static T Get<T>(string key) => (T)App.Current.FindResource(key);
|
||||
|
||||
#region Colors
|
||||
|
||||
public static Color Color_LightPrimary => Get<Color>("LightPrimaryColor");
|
||||
public static Color Color_Primary => Get<Color>("PrimaryColor");
|
||||
public static Color Color_DarkPrimary => Get<Color>("DarkPrimaryColor");
|
||||
|
||||
public static Color Color_LightDanger => Get<Color>("LightDangerColor");
|
||||
public static Color Color_Danger => Get<Color>("DangerColor");
|
||||
public static Color Color_DarkDanger => Get<Color>("DarkDangerColor");
|
||||
|
||||
public static Color Color_LightWarning => Get<Color>("LightWarningColor");
|
||||
public static Color Color_Warning => Get<Color>("WarningColor");
|
||||
public static Color Color_DarkWarning => Get<Color>("DarkWarningColor");
|
||||
|
||||
public static Color Color_LightInfo => Get<Color>("LightInfoColor");
|
||||
public static Color Color_Info => Get<Color>("InfoColor");
|
||||
public static Color Color_DarkInfo => Get<Color>("DarkInfoColor");
|
||||
|
||||
public static Color Color_LightSuccess => Get<Color>("LightSuccessColor");
|
||||
public static Color Color_Success => Get<Color>("SuccessColor");
|
||||
public static Color Color_DarkSuccess => Get<Color>("DarkSuccessColor");
|
||||
|
||||
public static Color Color_PrimaryText => Get<Color>("PrimaryTextColor");
|
||||
public static Color Color_SecondaryText => Get<Color>("SecondaryTextColor");
|
||||
public static Color Color_ThirdlyText => Get<Color>("ThirdlyTextColor");
|
||||
public static Color Color_ReverseText => Get<Color>("ReverseTextColor");
|
||||
public static Color Color_TextIcon => Get<Color>("TextIconColor");
|
||||
|
||||
public static Color Color_Border => Get<Color>("BorderColor");
|
||||
public static Color Color_SecondaryBorder => Get<Color>("SecondaryBorderColor");
|
||||
public static Color Color_Background => Get<Color>("BackgroundColor");
|
||||
public static Color Color_Region => Get<Color>("RegionColor");
|
||||
public static Color Color_SecondaryRegion => Get<Color>("SecondaryRegionColor");
|
||||
public static Color Color_ThirdlyRegion => Get<Color>("ThirdlyRegionColor");
|
||||
public static Color Color_Title => Get<Color>("TitleColor");
|
||||
public static Color Color_SecondaryTitle => Get<Color>("SecondaryTitleColor");
|
||||
|
||||
public static Color Color_Default => Get<Color>("DefaultColor");
|
||||
public static Color Color_DarkDefault => Get<Color>("DarkDefaultColor");
|
||||
|
||||
public static Color Color_Accent => Get<Color>("AccentColor");
|
||||
public static Color Color_DarkAccent => Get<Color>("DarkAccentColor");
|
||||
|
||||
public static Color Color_DarkMask => Get<Color>("DarkMaskColor");
|
||||
public static Color Color_DarkOpacity => Get<Color>("DarkOpacityColor");
|
||||
|
||||
#endregion
|
||||
|
||||
#region Brushes
|
||||
|
||||
public static SolidColorBrush Brush_LightPrimary => Get<SolidColorBrush>("LightPrimaryBrush");
|
||||
public static LinearGradientBrush Brush_Primary => Get<LinearGradientBrush>("PrimaryBrush");
|
||||
public static SolidColorBrush Brush_DarkPrimary => Get<SolidColorBrush>("DarkPrimaryBrush");
|
||||
|
||||
public static SolidColorBrush Brush_PrimaryText => Get<SolidColorBrush>("PrimaryTextBrush");
|
||||
public static SolidColorBrush Brush_SecondaryText => Get<SolidColorBrush>("SecondaryTextBrush");
|
||||
public static SolidColorBrush Brush_ThirdlyText => Get<SolidColorBrush>("ThirdlyTextBrush");
|
||||
public static SolidColorBrush Brush_ReverseText => Get<SolidColorBrush>("ReverseTextBrush");
|
||||
public static SolidColorBrush Brush_TextIcon => Get<SolidColorBrush>("TextIconBrush");
|
||||
|
||||
public static SolidColorBrush Brush_Border => Get<SolidColorBrush>("BorderBrush");
|
||||
public static SolidColorBrush Brush_SecondaryBorder => Get<SolidColorBrush>("SecondaryBorderBrush");
|
||||
public static SolidColorBrush Brush_Background => Get<SolidColorBrush>("BackgroundBrush");
|
||||
public static SolidColorBrush Brush_Region => Get<SolidColorBrush>("RegionBrush");
|
||||
public static SolidColorBrush Brush_SecondaryRegion => Get<SolidColorBrush>("SecondaryRegionBrush");
|
||||
public static SolidColorBrush Brush_ThirdlyRegion => Get<SolidColorBrush>("ThirdlyRegionBrush");
|
||||
public static LinearGradientBrush Brush_Title => Get<LinearGradientBrush>("TitleBrush");
|
||||
|
||||
public static SolidColorBrush Brush_Default => Get<SolidColorBrush>("DefaultBrush");
|
||||
public static SolidColorBrush Brush_DarkDefault => Get<SolidColorBrush>("DarkDefaultBrush");
|
||||
|
||||
public static SolidColorBrush Brush_LightDanger => Get<SolidColorBrush>("LightDangerBrush");
|
||||
public static LinearGradientBrush Brush_Danger => Get<LinearGradientBrush>("DangerBrush");
|
||||
public static SolidColorBrush Brush_DarkDanger => Get<SolidColorBrush>("DarkDangerBrush");
|
||||
|
||||
public static SolidColorBrush Brush_LightWarning => Get<SolidColorBrush>("LightWarningBrush");
|
||||
public static LinearGradientBrush Brush_Warning => Get<LinearGradientBrush>("WarningBrush");
|
||||
public static SolidColorBrush Brush_DarkWarning => Get<SolidColorBrush>("DarkWarningBrush");
|
||||
|
||||
public static SolidColorBrush Brush_LightInfo => Get<SolidColorBrush>("LightInfoBrush");
|
||||
public static LinearGradientBrush Brush_Info => Get<LinearGradientBrush>("InfoBrush");
|
||||
public static SolidColorBrush Brush_DarkInfo => Get<SolidColorBrush>("DarkInfoBrush");
|
||||
|
||||
public static SolidColorBrush Brush_LightSuccess => Get<SolidColorBrush>("LightSuccessBrush");
|
||||
public static LinearGradientBrush Brush_Success => Get<LinearGradientBrush>("SuccessBrush");
|
||||
public static SolidColorBrush Brush_DarkSuccess => Get<SolidColorBrush>("DarkSuccessBrush");
|
||||
|
||||
public static SolidColorBrush Brush_Accent => Get<SolidColorBrush>("AccentBrush");
|
||||
public static SolidColorBrush Brush_DarkAccent => Get<SolidColorBrush>("DarkAccentBrush");
|
||||
|
||||
public static SolidColorBrush Brush_DarkMask => Get<SolidColorBrush>("DarkMaskBrush");
|
||||
public static SolidColorBrush Brush_DarkOpacity => Get<SolidColorBrush>("DarkOpacityBrush");
|
||||
|
||||
#endregion
|
||||
|
||||
#region Strings
|
||||
|
||||
|
||||
@@ -13,4 +13,9 @@
|
||||
<Geometry x:Key="Geo_Stop" o:Freeze="True">M320 96c17.7 0 32 14.3 32 32l0 256c0 17.7-14.3 32-32 32L64 416c-17.7 0-32-14.3-32-32l0-256c0-17.7 14.3-32 32-32l256 0zM64 64C28.7 64 0 92.7 0 128L0 384c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-256c0-35.3-28.7-64-64-64L64 64z</Geometry>
|
||||
<Geometry x:Key="Geo_Folder" o:Freeze="True">M0 96C0 60.7 28.7 32 64 32l132.1 0c19.1 0 37.4 7.6 50.9 21.1L289.9 96 448 96c35.3 0 64 28.7 64 64l0 256c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM64 80c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l384 0c8.8 0 16-7.2 16-16l0-256c0-8.8-7.2-16-16-16l-161.4 0c-10.6 0-20.8-4.2-28.3-11.7L213.1 87c-4.5-4.5-10.6-7-17-7L64 80z</Geometry>
|
||||
<Geometry x:Key="Geo_ArrowRotateRight" o:Freeze="True">M472 224c13.3 0 24-10.7 24-24l0-144c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 80.1-20-23.5C387 63.4 325.1 32 256 32C132.3 32 32 132.3 32 256s100.3 224 224 224c50.4 0 97-16.7 134.4-44.8c10.6-8 12.7-23 4.8-33.6s-23-12.7-33.6-4.8C332.2 418.9 295.7 432 256 432c-97.2 0-176-78.8-176-176s78.8-176 176-176c54.3 0 102.9 24.6 135.2 63.4l.1 .2s0 0 0 0L418.9 176 328 176c-13.3 0-24 10.7-24 24s10.7 24 24 24l144 0z</Geometry>
|
||||
<Geometry x:Key="Geo_Cubes" o:Freeze="True">M348 62.7C330.7 52.7 309.3 52.7 292 62.7L207.8 111.3C190.5 121.3 179.8 139.8 179.8 159.8L179.8 261.7L91.5 312.7C74.2 322.7 63.5 341.2 63.5 361.2L63.5 458.5C63.5 478.5 74.2 497 91.5 507L175.8 555.6C193.1 565.6 214.5 565.6 231.8 555.6L320.1 504.6L408.4 555.6C425.7 565.6 447.1 565.6 464.4 555.6L548.5 507C565.8 497 576.5 478.5 576.5 458.5L576.5 361.2C576.5 341.2 565.8 322.7 548.5 312.7L460.2 261.7L460.2 159.8C460.2 139.8 449.5 121.3 432.2 111.3L348 62.7zM135.5 342.7L203.8 303.3L272.1 342.7L203.8 382.1L135.5 342.7zM111.5 384.3L179.8 423.7L179.8 502.5L115.5 465.4C113 464 111.5 461.3 111.5 458.5L111.5 384.3zM227.8 502.5L227.8 423.7L296.1 384.3L296.1 463.1L227.8 502.5zM344 384.3L412.3 423.7L412.3 502.5L344 463.1L344 384.3zM460.3 502.5L460.3 423.7L528.6 384.3L528.6 458.5C528.6 461.4 527.1 464 524.6 465.4L460.3 502.5zM504.6 342.7L436.3 382.1L368 342.7L436.3 303.3L504.6 342.7zM344 301.2L344 222.4L412.3 183L412.3 261.8L344 301.2zM388.3 141.4L320 180.8L251.8 141.4L316 104.3C318.5 102.9 321.5 102.9 324 104.3L388.3 141.4zM227.8 182.9L296.1 222.3L296.1 301.1L227.8 261.7L227.8 182.9z</Geometry>
|
||||
<Geometry x:Key="Geo_Image" o:Freeze="True">M160 144C151.2 144 144 151.2 144 160L144 480C144 488.8 151.2 496 160 496L480 496C488.8 496 496 488.8 496 480L496 160C496 151.2 488.8 144 480 144L160 144zM96 160C96 124.7 124.7 96 160 96L480 96C515.3 96 544 124.7 544 160L544 480C544 515.3 515.3 544 480 544L160 544C124.7 544 96 515.3 96 480L96 160zM224 192C241.7 192 256 206.3 256 224C256 241.7 241.7 256 224 256C206.3 256 192 241.7 192 224C192 206.3 206.3 192 224 192zM360 264C368.5 264 376.4 268.5 380.7 275.8L460.7 411.8C465.1 419.2 465.1 428.4 460.8 435.9C456.5 443.4 448.6 448 440 448L200 448C191.1 448 182.8 443 178.7 435.1C174.6 427.2 175.2 417.6 180.3 410.3L236.3 330.3C240.8 323.9 248.1 320.1 256 320.1C263.9 320.1 271.2 323.9 275.7 330.3L292.9 354.9L339.4 275.9C343.7 268.6 351.6 264.1 360.1 264.1z</Geometry>
|
||||
<Geometry x:Key="Geo_File" o:Freeze="True">M304 112L192 112C183.2 112 176 119.2 176 128L176 512C176 520.8 183.2 528 192 528L448 528C456.8 528 464 520.8 464 512L464 272L376 272C336.2 272 304 239.8 304 200L304 112zM444.1 224L352 131.9L352 200C352 213.3 362.7 224 376 224L444.1 224zM128 128C128 92.7 156.7 64 192 64L325.5 64C342.5 64 358.8 70.7 370.8 82.7L493.3 205.3C505.3 217.3 512 233.6 512 250.6L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 128z</Geometry>
|
||||
<Geometry x:Key="Geo_Gear" o:Freeze="True">M269.5 156.7L283.2 96L356.8 96L370.5 156.7C372.2 164.1 377.3 170.3 384.3 173.4C395.1 178.2 405.3 184.1 414.7 191C420.8 195.5 428.8 196.9 436.1 194.6L495.6 176.1L532.4 239.9L486.6 282.2C481 287.4 478.2 294.9 479 302.4C480.3 313.9 480.3 326.1 479 337.6C478.2 345.2 481 352.7 486.6 357.8L532.4 400.1L495.6 463.9L436.1 445.4C428.8 443.1 420.9 444.5 414.7 449C405.3 455.9 395.1 461.9 384.3 466.6C377.3 469.7 372.2 475.9 370.5 483.3L356.8 544L283.2 544L269.5 483.3C267.8 475.9 262.7 469.7 255.7 466.6C244.9 461.8 234.7 455.9 225.3 449C219.2 444.5 211.2 443.1 203.9 445.4L144.4 463.9L107.6 400.1L153.4 357.8C159 352.6 161.8 345.1 161 337.6C159.7 326.1 159.7 313.9 161 302.4C161.8 294.8 159 287.3 153.4 282.2L107.6 239.9L144.4 176.1L203.9 194.6C211.2 196.9 219.1 195.5 225.3 191C234.7 184.1 244.9 178.1 255.7 173.4C262.7 170.3 267.8 164.1 269.5 156.7zM276.8 48C258.1 48 241.9 61 237.8 79.2L225.2 134.8C218.9 138 212.9 141.5 207 145.3L152.6 128.4C134.7 122.8 115.4 130.4 106.1 146.6L62.9 221.4C53.6 237.6 56.7 258.1 70.4 270.8L112.3 309.5C112 316.4 112 323.5 112.3 330.5L70.4 369.2C56.7 381.9 53.5 402.4 62.9 418.6L106.1 493.4C115.4 509.6 134.8 517.1 152.6 511.6L207.1 494.7C213 498.5 219 502 225.3 505.2L237.9 560.8C242 579 258.2 592 276.9 592L363.3 592C382 592 398.2 579 402.3 560.8L414.9 505.2C421.2 502 427.2 498.5 433.1 494.7L487.6 511.6C505.5 517.2 524.8 509.6 534.1 493.4L577.3 418.6C586.6 402.4 583.5 381.9 569.8 369.2L527.9 330.5C528.2 323.6 528.2 316.5 527.9 309.5L569.8 270.8C583.5 258.1 586.6 237.6 577.3 221.4L534 146.6C524.6 130.4 505.3 122.9 487.5 128.4L433 145.3C427.1 141.5 421.1 138 414.8 134.8L402.3 79.2C398.1 61 381.9 48 363.2 48L276.8 48zM368 320C368 346.5 346.5 368 320 368C293.5 368 272 346.5 272 320C272 293.5 293.5 272 320 272C346.5 272 368 293.5 368 320zM320 224C267 224 224 267 224 320C224 373 267 416 320 416C373 416 416 373 416 320C416 267 373 224 320 224z</Geometry>
|
||||
<Geometry x:Key="Geo_Sliders" o:Freeze="True">M88 136C74.7 136 64 146.7 64 160C64 173.3 74.7 184 88 184L179.7 184C189.9 216.5 220.2 240 256 240C291.8 240 322.1 216.5 332.3 184L552 184C565.3 184 576 173.3 576 160C576 146.7 565.3 136 552 136L332.3 136C322.1 103.5 291.8 80 256 80C220.2 80 189.9 103.5 179.7 136L88 136zM88 296C74.7 296 64 306.7 64 320C64 333.3 74.7 344 88 344L339.7 344C349.9 376.5 380.2 400 416 400C451.8 400 482.1 376.5 492.3 344L552 344C565.3 344 576 333.3 576 320C576 306.7 565.3 296 552 296L492.3 296C482.1 263.5 451.8 240 416 240C380.2 240 349.9 263.5 339.7 296L88 296zM88 456C74.7 456 64 466.7 64 480C64 493.3 74.7 504 88 504L147.7 504C157.9 536.5 188.2 560 224 560C259.8 560 290.1 536.5 300.3 504L552 504C565.3 504 576 493.3 576 480C576 466.7 565.3 456 552 456L300.3 456C290.1 423.5 259.8 400 224 400C188.2 400 157.9 423.5 147.7 456L88 456zM224 512C206.3 512 192 497.7 192 480C192 462.3 206.3 448 224 448C241.7 448 256 462.3 256 480C256 497.7 241.7 512 224 512zM416 352C398.3 352 384 337.7 384 320C384 302.3 398.3 288 416 288C433.7 288 448 302.3 448 320C448 337.7 433.7 352 416 352zM224 160C224 142.3 238.3 128 256 128C273.7 128 288 142.3 288 160C288 177.7 273.7 192 256 192C238.3 192 224 177.7 224 160z</Geometry>
|
||||
</ResourceDictionary>
|
||||
|
||||
2
SpineViewer/Resources/Skins/dark.xaml
Normal file
2
SpineViewer/Resources/Skins/dark.xaml
Normal file
@@ -0,0 +1,2 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
Source="pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml"/>
|
||||
2
SpineViewer/Resources/Skins/light.xaml
Normal file
2
SpineViewer/Resources/Skins/light.xaml
Normal file
@@ -0,0 +1,2 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||
2
SpineViewer/Resources/Skins/violet.xaml
Normal file
2
SpineViewer/Resources/Skins/violet.xaml
Normal file
@@ -0,0 +1,2 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
Source="pack://application:,,,/HandyControl;component/Themes/SkinViolet.xaml"/>
|
||||
@@ -45,7 +45,11 @@
|
||||
<s:String x:Key="Str_Reload">Reload</s:String>
|
||||
<s:String x:Key="Str_MoveUpSpineObject">Move Up</s:String>
|
||||
<s:String x:Key="Str_MoveDownSpineObject">Move Down</s:String>
|
||||
<s:String x:Key="Str_SpineObjectConfig">Model Config</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectConfig">Copy Config</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSkinConfig">Copy Parameters (Skin Only)</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSlotAttachmentConfig">Copy Parameters (Slot Attachments Only)</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSlotVisibilityConfig">Copy Parameters (Slot Visibility Only)</s:String>
|
||||
<s:String x:Key="Str_ApplySpineObjectConfig">Apply Config</s:String>
|
||||
<s:String x:Key="Str_SaveSpineObjectConfigToFile">Save Config to File...</s:String>
|
||||
<s:String x:Key="Str_ApplySpineObjectConfigFromFile">Apply Config from File...</s:String>
|
||||
@@ -78,13 +82,13 @@
|
||||
<s:String x:Key="Str_Y">Y</s:String>
|
||||
|
||||
<s:String x:Key="Str_Skin">Skin</s:String>
|
||||
<s:String x:Key="Str_EnableSkins">Enable Skins</s:String>
|
||||
<s:String x:Key="Str_DisableSkins">Disable Skins</s:String>
|
||||
<s:String x:Key="Str_Enable">Enable</s:String>
|
||||
<s:String x:Key="Str_Disable">Disable</s:String>
|
||||
<s:String x:Key="Str_EnableAll">Enable All</s:String>
|
||||
<s:String x:Key="Str_DisableAll">Disable All</s:String>
|
||||
|
||||
<s:String x:Key="Str_Slot">Slot</s:String>
|
||||
<s:String x:Key="Str_ClearSlotsAttachment">Clear Slots Attachment</s:String>
|
||||
<s:String x:Key="Str_EnableSlots">Enable Slots</s:String>
|
||||
<s:String x:Key="Str_DisableSlots">Disable Slots</s:String>
|
||||
|
||||
<s:String x:Key="Str_Animation">Animation</s:String>
|
||||
<s:String x:Key="Str_AppendTrack">Add</s:String>
|
||||
@@ -119,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>
|
||||
@@ -136,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>
|
||||
@@ -197,15 +206,17 @@
|
||||
|
||||
<s:String x:Key="Str_VideoFormat">Video Format</s:String>
|
||||
<s:String x:Key="Str_LoopPlay">Loop Play</s:String>
|
||||
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp]
Whether the animation loops</s:String>
|
||||
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp/Apng]
Whether the animation loops</s:String>
|
||||
<s:String x:Key="Str_QualityParameter">Quality Parameter</s:String>
|
||||
<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_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>
|
||||
@@ -242,6 +253,7 @@
|
||||
|
||||
<s:String x:Key="Str_AppPreference">Application Options</s:String>
|
||||
<s:String x:Key="Str_Language">Language</s:String>
|
||||
<s:String x:Key="Str_AppSkin">Skin</s:String>
|
||||
<s:String x:Key="Str_CloseToTray">Minimize to tray when closing</s:String>
|
||||
<s:String x:Key="Str_AutoRun">Auto Start</s:String>
|
||||
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">Auto-load Workspace File on Startup</s:String>
|
||||
|
||||
@@ -45,7 +45,11 @@
|
||||
<s:String x:Key="Str_Reload">再読み込み</s:String>
|
||||
<s:String x:Key="Str_MoveUpSpineObject">上へ移動</s:String>
|
||||
<s:String x:Key="Str_MoveDownSpineObject">下へ移動</s:String>
|
||||
<s:String x:Key="Str_SpineObjectConfig">モデル設定</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectConfig">パラメーターをコピー</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSkinConfig">パラメーターをコピー(スキンのみ)</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSlotAttachmentConfig">パラメーターをコピー(スロットのアタッチメントのみ)</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSlotVisibilityConfig">パラメーターをコピー(スロットの表示状態のみ)</s:String>
|
||||
<s:String x:Key="Str_ApplySpineObjectConfig">パラメーターを適用</s:String>
|
||||
<s:String x:Key="Str_SaveSpineObjectConfigToFile">パラメータファイルを保存...</s:String>
|
||||
<s:String x:Key="Str_ApplySpineObjectConfigFromFile">パラメータファイルを適用...</s:String>
|
||||
@@ -78,13 +82,13 @@
|
||||
<s:String x:Key="Str_Y">Y座標</s:String>
|
||||
|
||||
<s:String x:Key="Str_Skin">スキン</s:String>
|
||||
<s:String x:Key="Str_EnableSkins">有効</s:String>
|
||||
<s:String x:Key="Str_DisableSkins">無効</s:String>
|
||||
<s:String x:Key="Str_Enable">有効にする</s:String>
|
||||
<s:String x:Key="Str_Disable">無効にする</s:String>
|
||||
<s:String x:Key="Str_EnableAll">すべて有効にする</s:String>
|
||||
<s:String x:Key="Str_DisableAll">すべて無効にする</s:String>
|
||||
|
||||
<s:String x:Key="Str_Slot">スロット</s:String>
|
||||
<s:String x:Key="Str_ClearSlotsAttachment">アタッチメントをクリア</s:String>
|
||||
<s:String x:Key="Str_EnableSlots">有効</s:String>
|
||||
<s:String x:Key="Str_DisableSlots">無効</s:String>
|
||||
|
||||
<s:String x:Key="Str_Animation">アニメーション</s:String>
|
||||
<s:String x:Key="Str_AppendTrack">追加</s:String>
|
||||
@@ -119,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>
|
||||
@@ -136,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>
|
||||
@@ -197,15 +206,17 @@
|
||||
|
||||
<s:String x:Key="Str_VideoFormat">ビデオフォーマット</s:String>
|
||||
<s:String x:Key="Str_LoopPlay">ループ再生</s:String>
|
||||
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp]
アニメーションをループ再生するかどうか</s:String>
|
||||
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp/Apng]
アニメーションをループ再生するかどうか</s:String>
|
||||
<s:String x:Key="Str_QualityParameter">品質パラメータ</s:String>
|
||||
<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_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>
|
||||
@@ -242,6 +253,7 @@
|
||||
|
||||
<s:String x:Key="Str_AppPreference">アプリケーションプション</s:String>
|
||||
<s:String x:Key="Str_Language">言語</s:String>
|
||||
<s:String x:Key="Str_AppSkin">スキン</s:String>
|
||||
<s:String x:Key="Str_CloseToTray">閉じるときにトレイに最小化する</s:String>
|
||||
<s:String x:Key="Str_AutoRun">自動起動</s:String>
|
||||
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">起動時にワークスペースファイルを自動読み込み</s:String>
|
||||
|
||||
@@ -45,7 +45,11 @@
|
||||
<s:String x:Key="Str_Reload">重新加载</s:String>
|
||||
<s:String x:Key="Str_MoveUpSpineObject">上移</s:String>
|
||||
<s:String x:Key="Str_MoveDownSpineObject">下移</s:String>
|
||||
<s:String x:Key="Str_SpineObjectConfig">模型参数</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectConfig">复制参数</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSkinConfig">复制参数(仅皮肤)</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSlotAttachmentConfig">复制参数(仅插槽附件)</s:String>
|
||||
<s:String x:Key="Str_CopySpineObjectSlotVisibilityConfig">复制参数(仅插槽可见性)</s:String>
|
||||
<s:String x:Key="Str_ApplySpineObjectConfig">应用参数</s:String>
|
||||
<s:String x:Key="Str_SaveSpineObjectConfigToFile">保存参数文件...</s:String>
|
||||
<s:String x:Key="Str_ApplySpineObjectConfigFromFile">应用参数文件...</s:String>
|
||||
@@ -78,13 +82,13 @@
|
||||
<s:String x:Key="Str_Y">纵坐标</s:String>
|
||||
|
||||
<s:String x:Key="Str_Skin">皮肤</s:String>
|
||||
<s:String x:Key="Str_EnableSkins">启用</s:String>
|
||||
<s:String x:Key="Str_DisableSkins">禁用</s:String>
|
||||
<s:String x:Key="Str_Enable">启用</s:String>
|
||||
<s:String x:Key="Str_Disable">禁用</s:String>
|
||||
<s:String x:Key="Str_EnableAll">全部启用</s:String>
|
||||
<s:String x:Key="Str_DisableAll">全部禁用</s:String>
|
||||
|
||||
<s:String x:Key="Str_Slot">插槽</s:String>
|
||||
<s:String x:Key="Str_ClearSlotsAttachment">清除附件</s:String>
|
||||
<s:String x:Key="Str_EnableSlots">启用</s:String>
|
||||
<s:String x:Key="Str_DisableSlots">禁用</s:String>
|
||||
|
||||
<s:String x:Key="Str_Animation">动画</s:String>
|
||||
<s:String x:Key="Str_AppendTrack">添加</s:String>
|
||||
@@ -119,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>
|
||||
@@ -136,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>
|
||||
@@ -197,15 +206,17 @@
|
||||
|
||||
<s:String x:Key="Str_VideoFormat">视频格式</s:String>
|
||||
<s:String x:Key="Str_LoopPlay">循环播放</s:String>
|
||||
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp]
动图是否循环播放</s:String>
|
||||
<s:String x:Key="Str_LoopPlayTooltip" xml:space="preserve">[Gif/Webp/Apng]
动图是否循环播放</s:String>
|
||||
<s:String x:Key="Str_QualityParameter">质量参数</s:String>
|
||||
<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_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>
|
||||
@@ -242,6 +253,7 @@
|
||||
|
||||
<s:String x:Key="Str_AppPreference">应用程序选项</s:String>
|
||||
<s:String x:Key="Str_Language">语言</s:String>
|
||||
<s:String x:Key="Str_AppSkin">皮肤</s:String>
|
||||
<s:String x:Key="Str_CloseToTray">关闭时最小化至托盘图标</s:String>
|
||||
<s:String x:Key="Str_AutoRun">开机自启</s:String>
|
||||
<s:String x:Key="Str_AutoRunWorkspaceConfigPath">自启动加载工作区文件</s:String>
|
||||
|
||||
122
SpineViewer/Resources/Theme.xaml
Normal file
122
SpineViewer/Resources/Theme.xaml
Normal file
@@ -0,0 +1,122 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
|
||||
xmlns:utils="clr-namespace:SpineViewer.Utils"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<utils:StringFormatMultiValueConverter x:Key="StrFmtCvter"/>
|
||||
<utils:BackgroundToForegroundConverter x:Key="Bg2FgCvter"/>
|
||||
|
||||
<Style x:Key="MyGridSplitterBaseStyle" TargetType="GridSplitter">
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryBorderBrush}"/>
|
||||
<Setter Property="ShowsPreview" Value="False"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="ResizeDirection" Value="Columns">
|
||||
<Setter Property="Width" Value="3"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
</Trigger>
|
||||
<Trigger Property="ResizeDirection" Value="Rows">
|
||||
<Setter Property="Height" Value="3"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyToggleButtonBaseStyle" TargetType="ToggleButton" BasedOn="{StaticResource ToggleButtonSwitch}">
|
||||
<Setter Property="hc:VisualElement.HighlightBrush" Value="{DynamicResource DarkSuccessBrush}"/>
|
||||
</Style>
|
||||
|
||||
<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="ListBoxItem" BasedOn="{StaticResource ListBoxItemBaseStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<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="GroupBox" BasedOn="{StaticResource GroupBoxTab}">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
|
||||
</Style>
|
||||
|
||||
<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="ScrollViewer" BasedOn="{StaticResource ScrollViewerNativeBaseStyle}">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="ComputedVerticalScrollBarVisibility" Value="Visible">
|
||||
<Setter Property="Padding" Value="0 0 5 0"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyTabItemHeaderPathStyle" TargetType="Path">
|
||||
<Setter Property="Stretch" Value="Uniform"/>
|
||||
<Setter Property="Stroke" Value="{DynamicResource PrimaryTextBrush}"/>
|
||||
<Setter Property="StrokeThickness" Value="20"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TabItem}, Path=IsSelected}" Value="True">
|
||||
<Setter Property="Stroke" Value="{DynamicResource PrimaryBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MyTabItemHeaderContainerStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource RegionBrush}"/>
|
||||
<Setter Property="Margin" Value="-10 -5 -8 -5"/>
|
||||
<Setter Property="Padding" Value="10 5"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Background" Value="{DynamicResource SecondaryRegionBrush}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<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.3</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>
|
||||
|
||||
35
SpineViewer/Utils/BackgroundColorToForegroundColor.cs
Normal file
35
SpineViewer/Utils/BackgroundColorToForegroundColor.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SpineViewer.Utils
|
||||
{
|
||||
public class BackgroundToForegroundConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var color = Colors.White;
|
||||
if (value is SolidColorBrush brush)
|
||||
{
|
||||
color = brush.Color;
|
||||
}
|
||||
else if (value is Color c)
|
||||
{
|
||||
color = c;
|
||||
}
|
||||
|
||||
if (color.A < 128)
|
||||
return Brushes.Black;
|
||||
|
||||
// 计算亮度 (使用标准加权公式)
|
||||
double brightness = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255.0;
|
||||
return brightness < 0.5 ? Brushes.White : Brushes.Black;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace SpineViewer.Utils
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
IndentSize = 4,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
@@ -47,7 +48,6 @@ namespace SpineViewer.Utils
|
||||
if (!quietForNotExist)
|
||||
{
|
||||
_logger.Error("Json file {0} not found", path);
|
||||
MessagePopupService.Error($"Json file {path} not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -62,13 +62,11 @@ namespace SpineViewer.Utils
|
||||
return true;
|
||||
}
|
||||
_logger.Error("Null data in file {0}", path);
|
||||
MessagePopupService.Error($"Null data in file {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to read json file {0}, {1}", path, ex.Message);
|
||||
_logger.Trace(ex.ToString());
|
||||
MessagePopupService.Error($"Failed to read json file {path}, {ex.ToString()}");
|
||||
}
|
||||
}
|
||||
obj = default;
|
||||
@@ -88,13 +86,26 @@ namespace SpineViewer.Utils
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to save json file {0}, {1}", path, ex.Message);
|
||||
_logger.Trace(ex.ToString());
|
||||
MessagePopupService.Error($"Failed to save json file {path}, {ex.ToString()}");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string Serialize<T>(T obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Serialize(obj, _jsonOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to serialize json object {0}", ex.Message);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ColorJsonConverter : JsonConverter<Color>
|
||||
|
||||
31
SpineViewer/Utils/PropertyWatcher.cs
Normal file
31
SpineViewer/Utils/PropertyWatcher.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewer.Utils
|
||||
{
|
||||
public static class PropertyWatcher
|
||||
{
|
||||
public static IDisposable Watch(DependencyObject target, DependencyProperty property, Action callback)
|
||||
{
|
||||
var dpd = DependencyPropertyDescriptor.FromProperty(property, target.GetType());
|
||||
if (dpd == null) return null;
|
||||
|
||||
EventHandler handler = (s, e) => callback();
|
||||
dpd.AddValueChanged(target, handler);
|
||||
|
||||
return new Unsubscriber(() => dpd.RemoveValueChanged(target, handler));
|
||||
}
|
||||
|
||||
private class Unsubscriber : IDisposable
|
||||
{
|
||||
private readonly Action _dispose;
|
||||
public Unsubscriber(Action dispose) => _dispose = dispose;
|
||||
public void Dispose() => _dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Spine.Exporters;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
using System;
|
||||
using System.Collections;
|
||||
@@ -74,6 +75,16 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
public uint MaxResolution { get => _maxResolution; set => SetProperty(ref _maxResolution, value); }
|
||||
protected uint _maxResolution = 2048;
|
||||
|
||||
public RelayCommand Cmd_SelectOutputDir => _cmd_SelectOutputDir ??= new(() =>
|
||||
{
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
_outputDir = selectedPath;
|
||||
OnPropertyChanged(nameof(OutputDir));
|
||||
}
|
||||
});
|
||||
protected RelayCommand _cmd_SelectOutputDir;
|
||||
|
||||
/// <summary>
|
||||
/// 使用提供的包围盒设置自动分辨率
|
||||
/// </summary>
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
public string? CustomArgs { get => _customArgs; set => SetProperty(ref _customArgs, value); }
|
||||
protected string? _customArgs;
|
||||
|
||||
private string FormatSuffix => $".{_format.ToString().ToLower()}";
|
||||
private string FormatSuffix => $".{_format.ToString().ToLowerInvariant()}";
|
||||
|
||||
public override string? Validate()
|
||||
{
|
||||
@@ -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,26 +19,67 @@ 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 { get => _format; set => SetProperty(ref _format, value); }
|
||||
public FFmpegVideoExporter.VideoFormat Format
|
||||
{
|
||||
get => _format;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _format, value))
|
||||
return;
|
||||
OnPropertyChanged(nameof(EnableParamLoop));
|
||||
OnPropertyChanged(nameof(EnableParamQuality));
|
||||
OnPropertyChanged(nameof(EnableParamLossless));
|
||||
OnPropertyChanged(nameof(EnableParamApngPred));
|
||||
OnPropertyChanged(nameof(EnableParamCrf));
|
||||
OnPropertyChanged(nameof(EnableParamProfile));
|
||||
}
|
||||
}
|
||||
protected FFmpegVideoExporter.VideoFormat _format = FFmpegVideoExporter.VideoFormat.Mp4;
|
||||
|
||||
public bool Loop { get => _loop; set => SetProperty(ref _loop, value); }
|
||||
protected bool _loop = true;
|
||||
|
||||
public bool EnableParamLoop =>
|
||||
_format == FFmpegVideoExporter.VideoFormat.Gif ||
|
||||
_format == FFmpegVideoExporter.VideoFormat.Webp ||
|
||||
_format == FFmpegVideoExporter.VideoFormat.Apng;
|
||||
|
||||
public int Quality { get => _quality; set => SetProperty(ref _quality, Math.Clamp(value, 0, 100)); }
|
||||
protected int _quality = 75;
|
||||
|
||||
public bool EnableParamQuality =>
|
||||
_format == FFmpegVideoExporter.VideoFormat.Webp;
|
||||
|
||||
public bool Lossless { get => _lossless; set => SetProperty(ref _lossless, value); }
|
||||
protected bool _lossless = false;
|
||||
|
||||
public bool EnableParamLossless =>
|
||||
_format == FFmpegVideoExporter.VideoFormat.Webp;
|
||||
|
||||
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;
|
||||
|
||||
public int Crf { get => _crf; set => SetProperty(ref _crf, Math.Clamp(value, 0, 63)); }
|
||||
protected int _crf = 23;
|
||||
|
||||
public int Profile { get => _profile; set => SetProperty(ref _profile, Math.Clamp(value, -1, 5)); }
|
||||
protected int _profile = 5;
|
||||
public bool EnableParamCrf =>
|
||||
_format == FFmpegVideoExporter.VideoFormat.Mp4 ||
|
||||
_format == FFmpegVideoExporter.VideoFormat.Webm ||
|
||||
_format == FFmpegVideoExporter.VideoFormat.Mkv;
|
||||
|
||||
private string FormatSuffix => $".{_format.ToString().ToLower()}";
|
||||
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;
|
||||
|
||||
private string FormatSuffix => $".{_format.ToString().ToLowerInvariant()}";
|
||||
|
||||
protected override void Export(SpineObjectModel[] models)
|
||||
{
|
||||
@@ -63,6 +104,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
Loop = _loop,
|
||||
Quality = _quality,
|
||||
Lossless = _lossless,
|
||||
PredMethod = _predMethod,
|
||||
Crf = _crf,
|
||||
Profile = _profile,
|
||||
};
|
||||
@@ -107,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;
|
||||
@@ -164,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
|
||||
{
|
||||
@@ -34,7 +38,7 @@ namespace SpineViewer.ViewModels.Exporters
|
||||
{
|
||||
if (_format == SKEncodedImageFormat.Heif) return ".jpeg";
|
||||
else if (_format == SKEncodedImageFormat.Jpegxl) return ".jpeg";
|
||||
else return $".{_format.ToString().ToLower()}";
|
||||
else return $".{_format.ToString().ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shell;
|
||||
using Spine.Interfaces;
|
||||
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
@@ -170,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();
|
||||
@@ -222,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++;
|
||||
}
|
||||
@@ -262,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);
|
||||
}
|
||||
}
|
||||
@@ -303,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++;
|
||||
}
|
||||
@@ -334,14 +333,14 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(_currentDirectory, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var lowerPath = file.ToLower();
|
||||
var lowerPath = file.ToLowerInvariant();
|
||||
if (SpineObject.PossibleSuffixMapping.Keys.Any(lowerPath.EndsWith))
|
||||
_items.Add(new(file));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to enumerate files in dir: {0}, {1}", _currentDirectory, ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -409,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,20 +27,41 @@ 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>
|
||||
/// 指示是否通过托盘图标进行退出
|
||||
/// </summary>
|
||||
public bool IsShuttingDownFromTray => _isShuttingDownFromTray;
|
||||
public bool IsShuttingDownFromTray
|
||||
{
|
||||
get => _isShuttingDownFromTray;
|
||||
private set => SetProperty(ref _isShuttingDownFromTray, value);
|
||||
}
|
||||
private bool _isShuttingDownFromTray;
|
||||
|
||||
public bool? CloseToTray
|
||||
public bool CloseToTray
|
||||
{
|
||||
get => _closeToTray;
|
||||
set => SetProperty(ref _closeToTray, value);
|
||||
}
|
||||
private bool? _closeToTray = null;
|
||||
private bool _closeToTray;
|
||||
|
||||
public string AutoRunWorkspaceConfigPath
|
||||
{
|
||||
@@ -109,8 +130,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
|
||||
public RelayCommand Cmd_ExitFromTray => _cmd_ExitFromTray ??= new(() =>
|
||||
{
|
||||
_isShuttingDownFromTray = true;
|
||||
OnPropertyChanged(nameof(IsShuttingDownFromTray));
|
||||
IsShuttingDownFromTray = true;
|
||||
App.Current.Shutdown();
|
||||
});
|
||||
private RelayCommand? _cmd_ExitFromTray;
|
||||
@@ -161,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;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Win32;
|
||||
using NLog;
|
||||
using Spine;
|
||||
using Spine.Implementations;
|
||||
using Spine.Interfaces;
|
||||
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,11 +107,15 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
DebugPoints = DebugPoints,
|
||||
DebugClippings = DebugClippings,
|
||||
|
||||
AppLanguage = AppLanguage,
|
||||
RenderSelectedOnly = RenderSelectedOnly,
|
||||
HitTestLevel = HitTestLevel,
|
||||
LogHitSlots = LogHitSlots,
|
||||
MaxFps = MaxFps,
|
||||
|
||||
AppLanguage = AppLanguage,
|
||||
AppSkin = AppSkin,
|
||||
WallpaperView = WallpaperView,
|
||||
WallpaperMaxFps = WallpaperMaxFps,
|
||||
CloseToTray = CloseToTray,
|
||||
AutoRun = AutoRun,
|
||||
AutoRunWorkspaceConfigPath = AutoRunWorkspaceConfigPath,
|
||||
@@ -139,11 +142,15 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
DebugPoints = value.DebugPoints;
|
||||
DebugClippings = value.DebugClippings;
|
||||
|
||||
AppLanguage = value.AppLanguage;
|
||||
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;
|
||||
@@ -249,18 +256,10 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
|
||||
#endregion
|
||||
|
||||
#region 程序选项
|
||||
|
||||
public static ImmutableArray<AppLanguage> AppLanguageOptions { get; } = Enum.GetValues<AppLanguage>().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 bool RenderSelectedOnly
|
||||
{
|
||||
get => _vmMain.SFMLRendererViewModel.RenderSelectedOnly;
|
||||
@@ -279,13 +278,45 @@ 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 bool? CloseToTray
|
||||
public uint WallpaperMaxFps
|
||||
{
|
||||
get => _vmMain.SFMLRendererViewModel.WallpaperMaxFps;
|
||||
set => SetProperty(_vmMain.SFMLRendererViewModel.WallpaperMaxFps, value, v => _vmMain.SFMLRendererViewModel.WallpaperMaxFps = v);
|
||||
}
|
||||
|
||||
public bool CloseToTray
|
||||
{
|
||||
get => _vmMain.CloseToTray;
|
||||
set => SetProperty(_vmMain.CloseToTray, value, v => _vmMain.CloseToTray = v);
|
||||
|
||||
@@ -47,15 +47,10 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray _selectedBackgroundVertices = new(SFML.Graphics.PrimitiveType.Quads, 4); // XXX: 暂时未使用 Dispose 释放
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color _axisColor = new(220, 220, 220);
|
||||
|
||||
/// <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>
|
||||
/// 帧间隔计时器
|
||||
@@ -66,6 +61,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
private Task? _renderTask = null;
|
||||
private Task? _wallpaperRenderTask = null;
|
||||
private CancellationTokenSource? _cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
@@ -92,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>
|
||||
@@ -161,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;
|
||||
@@ -178,11 +198,22 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get => Color.FromRgb(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B);
|
||||
set => SetProperty(BackgroundColor, value, v => _backgroundColor = new(value.R, value.G, value.B));
|
||||
set
|
||||
{
|
||||
if (!SetProperty(BackgroundColor, value, v => _backgroundColor = new(value.R, value.G, value.B)))
|
||||
return;
|
||||
var b = (0.299 * value.R + 0.587 * value.G + 0.114 * value.B) / 255.0;
|
||||
_axisColor = b < 0.5 ? SFML.Graphics.Color.White : SFML.Graphics.Color.Black;
|
||||
}
|
||||
}
|
||||
private SFML.Graphics.Color _backgroundColor = new(105, 105, 105);
|
||||
|
||||
public string BackgroundImagePath
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// </summary>
|
||||
private SFML.Graphics.Color _axisColor = SFML.Graphics.Color.White;
|
||||
|
||||
public string? BackgroundImagePath
|
||||
{
|
||||
get => _backgroundImagePath;
|
||||
set => SetProperty(_backgroundImagePath, value, v =>
|
||||
@@ -231,7 +262,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
});
|
||||
}
|
||||
private string _backgroundImagePath;
|
||||
private string? _backgroundImagePath;
|
||||
|
||||
public Stretch BackgroundImageMode
|
||||
{
|
||||
@@ -314,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); // 滚轮缩放限制一下缩放范围
|
||||
@@ -440,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;
|
||||
@@ -468,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);
|
||||
}
|
||||
}
|
||||
@@ -603,7 +726,6 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
Rotation = Rotation,
|
||||
FlipX = FlipX,
|
||||
FlipY = FlipY,
|
||||
MaxFps = MaxFps,
|
||||
Speed = Speed,
|
||||
ShowAxis = ShowAxis,
|
||||
BackgroundColor = BackgroundColor,
|
||||
@@ -620,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;
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
/// 临时对象, 存储复制的模型参数
|
||||
/// </summary>
|
||||
private SpineObjectConfigModel? _copiedSpineObjectConfigModel = null;
|
||||
private SpineObjectConfigApplyFlag _copiedConfigFlag = SpineObjectConfigApplyFlag.All;
|
||||
|
||||
public SpineObjectListViewModel(MainWindowViewModel mainViewModel)
|
||||
{
|
||||
@@ -99,6 +100,127 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从路径列表添加对象
|
||||
/// </summary>
|
||||
/// <param name="paths">可以是文件和文件夹</param>
|
||||
public void AddSpineObjectFromFileList(IEnumerable<string> paths)
|
||||
{
|
||||
List<string> validPaths = [];
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var lowerPath = path.ToLowerInvariant();
|
||||
if (SpineObject.PossibleSuffixMapping.Keys.Any(lowerPath.EndsWith))
|
||||
validPaths.Add(path);
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var lowerPath = file.ToLowerInvariant();
|
||||
if (SpineObject.PossibleSuffixMapping.Keys.Any(lowerPath.EndsWith))
|
||||
validPaths.Add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validPaths.Count > 1)
|
||||
{
|
||||
if (validPaths.Count > 100)
|
||||
{
|
||||
if (!MessagePopupService.OKCancel(string.Format(AppResource.Str_TooManyItemsToAddQuest, validPaths.Count)))
|
||||
return;
|
||||
}
|
||||
ProgressService.RunAsync((pr, ct) => AddSpineObjectsTask(
|
||||
validPaths.ToArray(), pr, ct),
|
||||
AppResource.Str_AddSpineObjectsTitle
|
||||
);
|
||||
}
|
||||
else if (validPaths.Count > 0)
|
||||
{
|
||||
InsertSpineObject(validPaths[0]);
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于后台添加模型的任务方法
|
||||
/// </summary>
|
||||
private void AddSpineObjectsTask(string[] paths, IProgressReporter reporter, CancellationToken ct)
|
||||
{
|
||||
int totalCount = paths.Length;
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
|
||||
_vmMain.ProgressState = TaskbarItemProgressState.Normal;
|
||||
_vmMain.ProgressValue = 0;
|
||||
|
||||
reporter.Total = totalCount;
|
||||
reporter.Done = 0;
|
||||
reporter.ProgressText = $"[0/{totalCount}]";
|
||||
for (int i = 0; i < totalCount; i++)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
var skelPath = paths[i];
|
||||
reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}";
|
||||
|
||||
if (InsertSpineObject(skelPath))
|
||||
success++;
|
||||
else
|
||||
error++;
|
||||
|
||||
reporter.Done = i + 1;
|
||||
reporter.ProgressText = $"[{i + 1}/{totalCount}] {skelPath}";
|
||||
_vmMain.ProgressValue = (i + 1f) / totalCount;
|
||||
}
|
||||
_vmMain.ProgressState = TaskbarItemProgressState.None;
|
||||
|
||||
if (error > 0)
|
||||
_logger.Warn("Batch load {0} successfully, {1} failed", success, error);
|
||||
else
|
||||
_logger.Info("{0} skel loaded successfully", success);
|
||||
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安全地在列表头添加一个模型, 发生错误会输出日志
|
||||
/// </summary>
|
||||
/// <returns>是否添加成功</returns>
|
||||
private bool InsertSpineObject(string skelPath, string? atlasPath = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sp = new SpineObjectModel(skelPath, atlasPath);
|
||||
lock (_spineObjectModels.Lock) _spineObjectModels.Insert(0, sp);
|
||||
if (Application.Current.Dispatcher.CheckAccess())
|
||||
{
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
|
||||
}
|
||||
else
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#region 模型列表管理菜单项实现
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗添加单模型命令
|
||||
/// </summary>
|
||||
@@ -218,8 +340,8 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
|
||||
_logger.Trace(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,8 +401,8 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
catch (Exception ex)
|
||||
{
|
||||
error++;
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Error("Failed to reload spine {0}, {1}", sp.SkelPath, ex.Message);
|
||||
_logger.Trace(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,18 +471,53 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 模型参数管理菜单项实现
|
||||
|
||||
/// <summary>
|
||||
/// 复制模型参数
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_CopySpineObjectConfig => _cmd_CopySpineObjectConfig ??= new(CopySpineObjectConfig_Execute, CopySpineObjectConfig_CanExecute);
|
||||
public RelayCommand<IList?> Cmd_CopySpineObjectConfig => _cmd_CopySpineObjectConfig ??= new(
|
||||
args => CopySpineObjectConfig_Execute(args, SpineObjectConfigApplyFlag.All),
|
||||
CopySpineObjectConfig_CanExecute
|
||||
);
|
||||
private RelayCommand<IList?>? _cmd_CopySpineObjectConfig;
|
||||
|
||||
private void CopySpineObjectConfig_Execute(IList? args)
|
||||
/// <summary>
|
||||
/// 复制模型参数 (仅皮肤)
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_CopySpineObjectSkinConfig => _cmd_CopySpineObjectSkinConfig ??= new(
|
||||
args => CopySpineObjectConfig_Execute(args, SpineObjectConfigApplyFlag.Skin),
|
||||
CopySpineObjectConfig_CanExecute
|
||||
);
|
||||
private RelayCommand<IList?>? _cmd_CopySpineObjectSkinConfig;
|
||||
|
||||
/// <summary>
|
||||
/// 复制模型参数 (仅插槽附件)
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_CopySpineObjectSlotAttachmentConfig => _cmd_CopySpineObjectSlotAttachmentConfig ??= new(
|
||||
args => CopySpineObjectConfig_Execute(args, SpineObjectConfigApplyFlag.SlotAttachement),
|
||||
CopySpineObjectConfig_CanExecute
|
||||
);
|
||||
private RelayCommand<IList?>? _cmd_CopySpineObjectSlotAttachmentConfig;
|
||||
|
||||
/// <summary>
|
||||
/// 复制模型参数 (仅插槽可见性)
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_CopySpineObjectSlotVisibilityConfig => _cmd_CopySpineObjectSlotVisibilityConfig ??= new(
|
||||
args => CopySpineObjectConfig_Execute(args, SpineObjectConfigApplyFlag.SlotVisibility),
|
||||
CopySpineObjectConfig_CanExecute
|
||||
);
|
||||
private RelayCommand<IList?>? _cmd_CopySpineObjectSlotVisibilityConfig;
|
||||
|
||||
private void CopySpineObjectConfig_Execute(IList? args, SpineObjectConfigApplyFlag flag)
|
||||
{
|
||||
if (!CopySpineObjectConfig_CanExecute(args)) return;
|
||||
var sp = (SpineObjectModel)args[0];
|
||||
_copiedSpineObjectConfigModel = sp.ObjectConfig;
|
||||
_logger.Info("Copy config from model: {0}", sp.Name);
|
||||
_copiedConfigFlag = flag;
|
||||
_logger.Info("Copy config[{0}] from model: {1}", flag, sp.Name);
|
||||
}
|
||||
|
||||
private bool CopySpineObjectConfig_CanExecute(IList? args)
|
||||
@@ -381,8 +538,8 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
if (!ApplySpineObjectConfig_CanExecute(args)) return;
|
||||
foreach (SpineObjectModel sp in args)
|
||||
{
|
||||
sp.ObjectConfig = _copiedSpineObjectConfigModel;
|
||||
_logger.Info("Apply config to model: {0}", sp.Name);
|
||||
sp.ApplyObjectConfig(_copiedSpineObjectConfigModel, _copiedConfigFlag);
|
||||
_logger.Info("Apply config[{0}] to model: {1}", _copiedConfigFlag, sp.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,124 +596,9 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从路径列表添加对象
|
||||
/// </summary>
|
||||
/// <param name="paths">可以是文件和文件夹</param>
|
||||
public void AddSpineObjectFromFileList(IEnumerable<string> paths)
|
||||
{
|
||||
List<string> validPaths = [];
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var lowerPath = path.ToLower();
|
||||
if (SpineObject.PossibleSuffixMapping.Keys.Any(lowerPath.EndsWith))
|
||||
validPaths.Add(path);
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var lowerPath = file.ToLower();
|
||||
if (SpineObject.PossibleSuffixMapping.Keys.Any(lowerPath.EndsWith))
|
||||
validPaths.Add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
if (validPaths.Count > 1)
|
||||
{
|
||||
if (validPaths.Count > 100)
|
||||
{
|
||||
if (!MessagePopupService.OKCancel(string.Format(AppResource.Str_TooManyItemsToAddQuest, validPaths.Count)))
|
||||
return;
|
||||
}
|
||||
ProgressService.RunAsync((pr, ct) => AddSpineObjectsTask(
|
||||
validPaths.ToArray(), pr, ct),
|
||||
AppResource.Str_AddSpineObjectsTitle
|
||||
);
|
||||
}
|
||||
else if (validPaths.Count > 0)
|
||||
{
|
||||
InsertSpineObject(validPaths[0]);
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于后台添加模型的任务方法
|
||||
/// </summary>
|
||||
private void AddSpineObjectsTask(string[] paths, IProgressReporter reporter, CancellationToken ct)
|
||||
{
|
||||
int totalCount = paths.Length;
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
|
||||
_vmMain.ProgressState = TaskbarItemProgressState.Normal;
|
||||
_vmMain.ProgressValue = 0;
|
||||
|
||||
reporter.Total = totalCount;
|
||||
reporter.Done = 0;
|
||||
reporter.ProgressText = $"[0/{totalCount}]";
|
||||
for (int i = 0; i < totalCount; i++)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
var skelPath = paths[i];
|
||||
reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}";
|
||||
|
||||
if (InsertSpineObject(skelPath))
|
||||
success++;
|
||||
else
|
||||
error++;
|
||||
|
||||
reporter.Done = i + 1;
|
||||
reporter.ProgressText = $"[{i + 1}/{totalCount}] {skelPath}";
|
||||
_vmMain.ProgressValue = (i + 1f) / totalCount;
|
||||
}
|
||||
_vmMain.ProgressState = TaskbarItemProgressState.None;
|
||||
|
||||
if (error > 0)
|
||||
_logger.Warn("Batch load {0} successfully, {1} failed", success, error);
|
||||
else
|
||||
_logger.Info("{0} skel loaded successfully", success);
|
||||
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安全地在列表头添加一个模型, 发生错误会输出日志
|
||||
/// </summary>
|
||||
/// <returns>是否添加成功</returns>
|
||||
private bool InsertSpineObject(string skelPath, string? atlasPath = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sp = new SpineObjectModel(skelPath, atlasPath);
|
||||
lock (_spineObjectModels.Lock) _spineObjectModels.Insert(0, sp);
|
||||
if (Application.Current.Dispatcher.CheckAccess())
|
||||
{
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
|
||||
}
|
||||
else
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#region 工作区参数实现
|
||||
|
||||
public List<SpineObjectWorkspaceConfigModel> LoadedSpineObjects
|
||||
{
|
||||
@@ -676,10 +718,12 @@ 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;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +346,18 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
);
|
||||
private RelayCommand<IList?> _cmd_DisableSkins;
|
||||
|
||||
public RelayCommand Cmd_EnableAllSkins => _cmd_EnableAllSkins ??= new(
|
||||
() => { if (_skins.Count <= 0) return; foreach (var s in _skins) s.Status = true; },
|
||||
() => { return _skins.Count > 0; }
|
||||
);
|
||||
private RelayCommand _cmd_EnableAllSkins;
|
||||
|
||||
public RelayCommand Cmd_DisableAllSkins => _cmd_DisableAllSkins ??= new(
|
||||
() => { if (_skins.Count <= 0) return; foreach (var s in _skins) s.Status = false; },
|
||||
() => { return _skins.Count > 0; }
|
||||
);
|
||||
private RelayCommand _cmd_DisableAllSkins;
|
||||
|
||||
public ObservableCollection<SlotViewModel> Slots => _slots;
|
||||
|
||||
public RelayCommand<IList?> Cmd_EnableSlots => _cmd_EnableSlots ??= new (
|
||||
@@ -360,6 +372,18 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
);
|
||||
private RelayCommand<IList?> _cmd_DisableSlots;
|
||||
|
||||
public RelayCommand Cmd_EnableAllSlots => _cmd_EnableAllSlots ??= new(
|
||||
() => { if (_slots.Count <= 0) return; foreach (var s in _slots) s.Visible = true; },
|
||||
() => { return _slots.Count > 0; }
|
||||
);
|
||||
private RelayCommand _cmd_EnableAllSlots;
|
||||
|
||||
public RelayCommand Cmd_DisableAllSlots => _cmd_DisableAllSlots ??= new(
|
||||
() => { if (_slots.Count <= 0) return; foreach (var s in _slots) s.Visible = false; },
|
||||
() => { return _slots.Count > 0; }
|
||||
);
|
||||
private RelayCommand _cmd_DisableAllSlots;
|
||||
|
||||
public ObservableCollection<AnimationTrackViewModel> AnimationTracks => _animationTracks;
|
||||
|
||||
public RelayCommand Cmd_AppendTrack => _cmd_AppendTrack ??= new(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
d:DataContext="{d:DesignInstance Type=viewmodels:AboutDialogViewModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{DynamicResource Str_Abount}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Height="300"
|
||||
Width="500"
|
||||
ShowInTaskbar="False"
|
||||
@@ -18,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>
|
||||
@@ -26,25 +27,31 @@
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 程序版本 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ProgremVersion}"/>
|
||||
<Label Grid.Row="0" Grid.Column="1" Content="{Binding ProgramVersion}"/>
|
||||
<StackPanel Grid.IsSharedSizeScope="True">
|
||||
<!-- 程序版本 -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ProgremVersion}"/>
|
||||
<Label Grid.Column="1" Content="{Binding ProgramVersion}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 项目地址 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ProjectUrl}"/>
|
||||
<Label Grid.Row="1" Grid.Column="1">
|
||||
<Hyperlink NavigateUri="{Binding ProjectUrl}" Command="{Binding Cmd_OpenProjectUrl}">
|
||||
<Run Text="{Binding ProjectUrl, Mode=OneWay}"/>
|
||||
</Hyperlink>
|
||||
</Label>
|
||||
<!-- 项目地址 -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ProjectUrl}"/>
|
||||
<Label Grid.Column="1">
|
||||
<Hyperlink NavigateUri="{Binding ProjectUrl}" Command="{Binding Cmd_OpenProjectUrl}">
|
||||
<Run Text="{Binding ProjectUrl, Mode=OneWay}"/>
|
||||
</Hyperlink>
|
||||
</Label>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -8,9 +10,11 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views
|
||||
{
|
||||
@@ -22,6 +26,13 @@ namespace SpineViewer.Views
|
||||
public AboutDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += AboutDialog_SourceInitialized;
|
||||
}
|
||||
|
||||
private void AboutDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
d:DataContext="{d:DesignInstance Type=viewmodels:DiagnosticsDialogViewModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{DynamicResource Str_Diagnostics}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Height="450"
|
||||
Width="800"
|
||||
ShowInTaskbar="False"
|
||||
@@ -21,9 +22,9 @@
|
||||
<Button Width="120" Content="{DynamicResource Str_CopyDiagnosticsInfo}" Command="{Binding Cmd_CopyToClipboard}"/>
|
||||
</Border>
|
||||
|
||||
<Border Grid.IsSharedSizeScope="True">
|
||||
<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>
|
||||
@@ -31,8 +32,8 @@
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
</Style>
|
||||
</Border.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="30 10">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Grid.IsSharedSizeScope="True" Margin="30 10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
@@ -134,7 +135,6 @@
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding HandyControlVersion, Mode=OneWay}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -8,9 +10,11 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views
|
||||
{
|
||||
@@ -22,6 +26,13 @@ namespace SpineViewer.Views
|
||||
public DiagnosticsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += DiagnosticsDialog_SourceInitialized;
|
||||
}
|
||||
|
||||
private void DiagnosticsDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:local="clr-namespace:SpineViewer.Views.ExporterDialogs"
|
||||
xmlns:utils="clr-namespace:SpineViewer.Utils"
|
||||
xmlns:exporters="clr-namespace:SpineViewer.ViewModels.Exporters"
|
||||
d:DataContext="{d:DesignInstance Type=exporters:CustomFFmpegExporterViewModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{DynamicResource Str_CustomFFmpegExporterTitle}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Width="450"
|
||||
Height="800"
|
||||
ShowInTaskbar="False"
|
||||
@@ -29,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>
|
||||
@@ -39,173 +41,235 @@
|
||||
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
|
||||
<Setter Property="BorderBrush" Value="LightGray"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
|
||||
<Setter Property="Margin" Value="0 5 0 10"/>
|
||||
</Style>
|
||||
</Border.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
|
||||
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
|
||||
<StackPanel Grid.IsSharedSizeScope="True">
|
||||
<GroupBox Header="{DynamicResource Str_ExportBaseArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 水平分辨率 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 垂直分辨率 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 是否导出单个 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 输出文件夹 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
<DockPanel Grid.Row="3" Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Click="ButtonSelectOutputDir_Click"/>
|
||||
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
<DockPanel Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectOutputDir}"/>
|
||||
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<DockPanel Grid.Row="4" Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Click="ButtonPickColor_Click">
|
||||
<Button.Background>
|
||||
<SolidColorBrush Color="{Binding BackgroundColor}"/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
<TextBox x:Name="_colorTextBox" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<DockPanel Grid.Column="1">
|
||||
<Border DockPanel.Dock="Right">
|
||||
<Popup x:Name="_colorPopup" Placement="Right">
|
||||
<hc:ColorPicker Confirmed="ColorPicker_Confirmed" Canceled="ColorPicker_Canceled"/>
|
||||
</Popup>
|
||||
</Border>
|
||||
<Border DockPanel.Dock="Right" Background="White" CornerRadius="{DynamicResource DefaultCornerRadius}">
|
||||
<Button Content="..."
|
||||
Foreground="{Binding BackgroundColor, Converter={StaticResource Bg2FgCvter}}"
|
||||
Click="ButtonPickColor_Click">
|
||||
<Button.Background>
|
||||
<SolidColorBrush Color="{Binding BackgroundColor}"/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
</Border>
|
||||
<TextBox Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 四周边距 -->
|
||||
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- 自动分辨率 -->
|
||||
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 最大分辨率 -->
|
||||
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
</Grid>
|
||||
<Grid IsEnabled="{Binding AutoResolution}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{DynamicResource Str_ExportVideoArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 导出时长 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 导出帧率 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Fps}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 导出速度 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 是否保留最后一帧 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{DynamicResource Str_ExportOtherArgs}">
|
||||
<Grid>
|
||||
<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>
|
||||
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="60"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 导出格式 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_FFmpegFormat}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Format}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_FFmpegFormat}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Format}" ToolTip="{DynamicResource Str_FFmpegFormatTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 编码器 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_FFmpegCodec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Codec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_FFmpegCodec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Codec}" ToolTip="{DynamicResource Str_FFmpegCodecTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 像素格式 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_FFmpegPixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding PixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_FFmpegPixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding PixelFormat}" ToolTip="{DynamicResource Str_FFmpegPixelFormatTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 比特率 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_FFmpegBitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Bitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_FFmpegBitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Bitrate}" ToolTip="{DynamicResource Str_FFmpegBitrateTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 滤镜 -->
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_FFmpegFilter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Filter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_FFmpegFilter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Filter}" ToolTip="{DynamicResource Str_FFmpegFilterTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 自定义参数 -->
|
||||
<Label Grid.Row="5" Grid.Column="0"
|
||||
VerticalAlignment="Top"
|
||||
Content="{DynamicResource Str_FFmpegCustomArgs}"
|
||||
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
|
||||
<TextBox Grid.Row="5" Grid.Column="1"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalContentAlignment="Top"
|
||||
TextWrapping="Wrap"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Text="{Binding CustomArgs}"
|
||||
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label VerticalAlignment="Top"
|
||||
Content="{DynamicResource Str_FFmpegCustomArgs}"
|
||||
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
|
||||
<TextBox Grid.Column="1"
|
||||
Height="75"
|
||||
HorizontalContentAlignment="Left"
|
||||
VerticalContentAlignment="Top"
|
||||
TextWrapping="Wrap"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Text="{Binding CustomArgs}"
|
||||
ToolTip="{DynamicResource Str_FFmpegCustomArgsTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using SpineViewer.Services;
|
||||
using Win32Natives;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -10,9 +12,11 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using SpineViewer.Extensions;
|
||||
|
||||
namespace SpineViewer.Views.ExporterDialogs
|
||||
{
|
||||
@@ -24,6 +28,13 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
public CustomFFmpegExporterDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += CustomFFmpegExporterDialog_SourceInitialized;
|
||||
}
|
||||
|
||||
private void CustomFFmpegExporterDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
@@ -42,18 +53,22 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
DialogResult = false;
|
||||
}
|
||||
|
||||
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
var vm = (CustomFFmpegExporterViewModel)DataContext;
|
||||
vm.OutputDir = selectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = !_colorPopup.IsOpen;
|
||||
}
|
||||
|
||||
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
var color = e.Info;
|
||||
var vm = (BaseExporterViewModel)DataContext;
|
||||
vm.BackgroundColor = color;
|
||||
}
|
||||
|
||||
private void ColorPicker_Canceled(object sender, EventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:local="clr-namespace:SpineViewer.Views.ExporterDialogs"
|
||||
xmlns:utils="clr-namespace:SpineViewer.Utils"
|
||||
xmlns:vmexp="clr-namespace:SpineViewer.ViewModels.Exporters"
|
||||
d:DataContext="{d:DesignInstance Type=vmexp:FFmpegVideoExporterViewModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{DynamicResource Str_FFmpegVideoExporterTitle}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Width="450"
|
||||
Height="750"
|
||||
ShowInTaskbar="False"
|
||||
@@ -29,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>
|
||||
@@ -39,150 +41,242 @@
|
||||
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
|
||||
<Setter Property="BorderBrush" Value="LightGray"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
|
||||
<Setter Property="Margin" Value="0 5 0 10"/>
|
||||
</Style>
|
||||
</Border.Resources>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
|
||||
<StackPanel Grid.IsSharedSizeScope="True">
|
||||
<GroupBox Header="{DynamicResource Str_ExportBaseArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 水平分辨率 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 垂直分辨率 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 是否导出单个 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 输出文件夹 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
<DockPanel Grid.Row="3" Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Click="ButtonSelectOutputDir_Click"/>
|
||||
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
<DockPanel Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectOutputDir}"/>
|
||||
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<DockPanel Grid.Row="4" Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Click="ButtonPickColor_Click">
|
||||
<Button.Background>
|
||||
<SolidColorBrush Color="{Binding BackgroundColor}"/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
<TextBox x:Name="_colorTextBox" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<DockPanel Grid.Column="1">
|
||||
<Border DockPanel.Dock="Right">
|
||||
<Popup x:Name="_colorPopup" Placement="Right">
|
||||
<hc:ColorPicker Confirmed="ColorPicker_Confirmed" Canceled="ColorPicker_Canceled"/>
|
||||
</Popup>
|
||||
</Border>
|
||||
<Border DockPanel.Dock="Right" Background="White" CornerRadius="{DynamicResource DefaultCornerRadius}">
|
||||
<Button Content="..."
|
||||
Foreground="{Binding BackgroundColor, Converter={StaticResource Bg2FgCvter}}"
|
||||
Click="ButtonPickColor_Click">
|
||||
<Button.Background>
|
||||
<SolidColorBrush Color="{Binding BackgroundColor}"/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
</Border>
|
||||
<TextBox Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 四周边距 -->
|
||||
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- 自动分辨率 -->
|
||||
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 最大分辨率 -->
|
||||
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
</Grid>
|
||||
<Grid IsEnabled="{Binding AutoResolution}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{DynamicResource Str_ExportVideoArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 导出时长 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 导出帧率 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Fps}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 导出速度 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 是否保留最后一帧 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{DynamicResource Str_ExportOtherArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 视频格式 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{x:Static vmexp:FFmpegVideoExporterViewModel.VideoFormatOptions}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_VideoFormat}"/>
|
||||
<ComboBox Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{x:Static vmexp:FFmpegVideoExporterViewModel.VideoFormatOptions}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 动图是否循环 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
|
||||
<ToggleButton Grid.Row="1" Grid.Column="1" IsChecked="{Binding Loop}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
|
||||
<Grid Visibility="{Binding EnableParamLoop, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding Loop}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 质量参数 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_QualityParameter}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
|
||||
<Grid Visibility="{Binding EnableParamQuality, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_QualityParameter}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_QualityParameterTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 无损压缩 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_LosslessParam}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
|
||||
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding Lossless}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
|
||||
<Grid Visibility="{Binding EnableParamLossless, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_LosslessParam}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding Lossless}" ToolTip="{DynamicResource Str_LosslessParamTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 预测器方法 -->
|
||||
<Grid Visibility="{Binding EnableParamApngPred, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<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 参数 -->
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_CrfParameter}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Crf}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
|
||||
<Grid Visibility="{Binding EnableParamCrf, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_CrfParameter}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Crf}" ToolTip="{DynamicResource Str_CrfParameterTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Profile 参数 -->
|
||||
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_ProfileParameter}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Profile}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
|
||||
</Grid>
|
||||
<Grid Visibility="{Binding EnableParamProfile, Converter={StaticResource Boolean2VisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ProfileParameter}" ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
SelectedItem="{Binding Profile}"
|
||||
ItemsSource="{x:Static vmexp:FFmpegVideoExporterViewModel.MovProfileOptions}"
|
||||
ToolTip="{DynamicResource Str_ProfileParameterTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -10,9 +12,11 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views.ExporterDialogs
|
||||
{
|
||||
@@ -24,6 +28,13 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
public FFmpegVideoExporterDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += FFmpegVideoExporterDialog_SourceInitialized;
|
||||
}
|
||||
|
||||
private void FFmpegVideoExporterDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
@@ -42,18 +53,22 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
DialogResult = false;
|
||||
}
|
||||
|
||||
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
var vm = (FFmpegVideoExporterViewModel)DataContext;
|
||||
vm.OutputDir = selectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = !_colorPopup.IsOpen;
|
||||
}
|
||||
|
||||
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
var color = e.Info;
|
||||
var vm = (BaseExporterViewModel)DataContext;
|
||||
vm.BackgroundColor = color;
|
||||
}
|
||||
|
||||
private void ColorPicker_Canceled(object sender, EventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:local="clr-namespace:SpineViewer.Views"
|
||||
xmlns:utils="clr-namespace:SpineViewer.Utils"
|
||||
xmlns:vmexp="clr-namespace:SpineViewer.ViewModels.Exporters"
|
||||
d:DataContext="{d:DesignInstance Type=vmexp:FrameExporterViewModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{DynamicResource Str_FrameExporterTitle}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Width="450"
|
||||
Height="480"
|
||||
ShowInTaskbar="False"
|
||||
@@ -29,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>
|
||||
@@ -39,99 +41,142 @@
|
||||
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
|
||||
<Setter Property="BorderBrush" Value="LightGray"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
|
||||
<Setter Property="Margin" Value="0 5 0 10"/>
|
||||
</Style>
|
||||
</Border.Resources>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
|
||||
<StackPanel Grid.IsSharedSizeScope="True">
|
||||
<GroupBox Header="{DynamicResource Str_ExportBaseArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 水平分辨率 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 垂直分辨率 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 是否导出单个 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 输出文件夹 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
<DockPanel Grid.Row="3" Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Click="ButtonSelectOutputDir_Click"/>
|
||||
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
<DockPanel Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectOutputDir}"/>
|
||||
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<DockPanel Grid.Row="4" Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Click="ButtonPickColor_Click">
|
||||
<Button.Background>
|
||||
<SolidColorBrush Color="{Binding BackgroundColor}"/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
<TextBox x:Name="_colorTextBox" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<DockPanel Grid.Column="1">
|
||||
<Border DockPanel.Dock="Right">
|
||||
<Popup x:Name="_colorPopup" Placement="Right">
|
||||
<hc:ColorPicker Confirmed="ColorPicker_Confirmed" Canceled="ColorPicker_Canceled"/>
|
||||
</Popup>
|
||||
</Border>
|
||||
<Border DockPanel.Dock="Right" Background="White" CornerRadius="{DynamicResource DefaultCornerRadius}">
|
||||
<Button Content="..."
|
||||
Foreground="{Binding BackgroundColor, Converter={StaticResource Bg2FgCvter}}"
|
||||
Click="ButtonPickColor_Click">
|
||||
<Button.Background>
|
||||
<SolidColorBrush Color="{Binding BackgroundColor}"/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
</Border>
|
||||
<TextBox Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 四周边距 -->
|
||||
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- 自动分辨率 -->
|
||||
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 最大分辨率 -->
|
||||
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
</Grid>
|
||||
<Grid IsEnabled="{Binding AutoResolution}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{DynamicResource Str_ExportOtherArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 图像格式 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ImageFormat}"/>
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{x:Static vmexp:FrameExporterViewModel.FrameFormatOptions}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ImageFormat}"/>
|
||||
<ComboBox Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{x:Static vmexp:FrameExporterViewModel.FrameFormatOptions}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 图像质量 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ImageQuality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ImageQuality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Quality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -10,9 +12,11 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views.ExporterDialogs
|
||||
{
|
||||
@@ -24,6 +28,13 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
public FrameExporterDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += FrameExporterDialog_SourceInitialized;
|
||||
}
|
||||
|
||||
private void FrameExporterDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
@@ -42,18 +53,22 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
DialogResult = false;
|
||||
}
|
||||
|
||||
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
var vm = (FrameExporterViewModel)DataContext;
|
||||
vm.OutputDir = selectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
|
||||
_colorPopup.IsOpen = !_colorPopup.IsOpen;
|
||||
}
|
||||
|
||||
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
var color = e.Info;
|
||||
var vm = (BaseExporterViewModel)DataContext;
|
||||
vm.BackgroundColor = color;
|
||||
}
|
||||
|
||||
private void ColorPicker_Canceled(object sender, EventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:local="clr-namespace:SpineViewer.Views"
|
||||
xmlns:utils="clr-namespace:SpineViewer.Utils"
|
||||
xmlns:vmexp="clr-namespace:SpineViewer.ViewModels.Exporters"
|
||||
d:DataContext="{d:DesignInstance Type=vmexp:FrameSequenceExporterViewModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{DynamicResource Str_FrameSequenceExporterTitle}"
|
||||
Title="{DynamicResource Str_FrameSequenceExporterTitle}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Width="450"
|
||||
Height="550"
|
||||
ShowInTaskbar="False"
|
||||
@@ -29,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>
|
||||
@@ -39,109 +41,162 @@
|
||||
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
|
||||
<Setter Property="BorderBrush" Value="LightGray"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
|
||||
<Setter Property="Margin" Value="0 5 0 10"/>
|
||||
</Style>
|
||||
</Border.Resources>
|
||||
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
|
||||
<StackPanel Grid.IsSharedSizeScope="True">
|
||||
<GroupBox Header="{DynamicResource Str_ExportBaseArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 水平分辨率 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ResolutionX}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionX, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 垂直分辨率 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<Grid IsEnabled="{Binding AutoResolution, Converter={StaticResource Boolean2BooleanReConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ResolutionY}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" IsReadOnly="True" Text="{Binding ResolutionY, Mode=OneWay}" ToolTip="{DynamicResource Str_ResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 是否导出单个 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<ToggleButton Grid.Row="2" Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding ExportSingle}" ToolTip="{DynamicResource Str_ExportSingleTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 输出文件夹 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
<DockPanel Grid.Row="3" Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Click="ButtonSelectOutputDir_Click"/>
|
||||
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
<DockPanel Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Command="{Binding Cmd_SelectOutputDir}"/>
|
||||
<TextBox Text="{Binding OutputDir}" ToolTip="{DynamicResource Str_OutputDirTooltip}"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 背景颜色 -->
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<DockPanel Grid.Row="4" Grid.Column="1">
|
||||
<Button DockPanel.Dock="Right" Content="..." Click="ButtonPickColor_Click">
|
||||
<Button.Background>
|
||||
<SolidColorBrush Color="{Binding BackgroundColor}"/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
<TextBox x:Name="_colorTextBox" Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
</DockPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
<DockPanel Grid.Column="1">
|
||||
<Border DockPanel.Dock="Right">
|
||||
<Popup x:Name="_colorPopup" Placement="Right">
|
||||
<hc:ColorPicker Confirmed="ColorPicker_Confirmed" Canceled="ColorPicker_Canceled"/>
|
||||
</Popup>
|
||||
</Border>
|
||||
<Border DockPanel.Dock="Right" Background="White" CornerRadius="{DynamicResource DefaultCornerRadius}">
|
||||
<Button Content="..."
|
||||
Foreground="{Binding BackgroundColor, Converter={StaticResource Bg2FgCvter}}"
|
||||
Click="ButtonPickColor_Click">
|
||||
<Button.Background>
|
||||
<SolidColorBrush Color="{Binding BackgroundColor}"/>
|
||||
</Button.Background>
|
||||
</Button>
|
||||
</Border>
|
||||
<TextBox Text="{Binding BackgroundColor}" ToolTip="#AARRGGBB"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 四周边距 -->
|
||||
<Label Grid.Row="5" Grid.Column="0" Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Margin}" ToolTip="{DynamicResource Str_MarginTooltip}"/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- 自动分辨率 -->
|
||||
<Label Grid.Row="6" Grid.Column="0" Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<ToggleButton Grid.Row="6" Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoResolution}" ToolTip="{DynamicResource Str_AutoResolutionTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 最大分辨率 -->
|
||||
<Label Grid.Row="7" Grid.Column="0" Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
</Grid>
|
||||
<Grid IsEnabled="{Binding AutoResolution}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding MaxResolution}" ToolTip="{DynamicResource Str_MaxResolutionTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="{DynamicResource Str_ExportVideoArgs}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="Col1"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 导出时长 -->
|
||||
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Duration}" ToolTip="{DynamicResource Str_ExportDurationTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 导出帧率 -->
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Fps}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_Fps}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Fps}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 导出速度 -->
|
||||
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ExportSpeed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Speed}" ToolTip="{DynamicResource Str_ExportSpeedTooltip}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 是否保留最后一帧 -->
|
||||
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Row="3" Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_KeepLastFrame}" ToolTip="{DynamicResource Str_KeepLastFrameTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding KeepLast}" ToolTip="{DynamicResource Str_KeelLastFrameTooltip}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -10,9 +12,11 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views.ExporterDialogs
|
||||
{
|
||||
@@ -24,6 +28,13 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
public FrameSequenceExporterDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += FrameSequenceExporterDialog_SourceInitialized;
|
||||
}
|
||||
|
||||
private void FrameSequenceExporterDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
@@ -42,18 +53,22 @@ namespace SpineViewer.Views.ExporterDialogs
|
||||
DialogResult = false;
|
||||
}
|
||||
|
||||
private void ButtonSelectOutputDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DialogService.ShowOpenFolderDialog(out var selectedPath))
|
||||
{
|
||||
var vm = (FrameSequenceExporterViewModel)DataContext;
|
||||
vm.OutputDir = selectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = !_colorPopup.IsOpen;
|
||||
}
|
||||
|
||||
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
var color = e.Info;
|
||||
var vm = (BaseExporterViewModel)DataContext;
|
||||
vm.BackgroundColor = color;
|
||||
}
|
||||
|
||||
private void ColorPicker_Canceled(object sender, EventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,19 @@
|
||||
using NLog;
|
||||
using NLog.Layouts;
|
||||
using NLog.Targets;
|
||||
using SFMLRenderer;
|
||||
using Spine;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Natives;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Utils;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using SpineViewer.ViewModels.MainWindow;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -23,6 +23,8 @@ using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views;
|
||||
|
||||
@@ -34,7 +36,7 @@ public partial class MainWindow : Window
|
||||
/// <summary>
|
||||
/// 上一次状态文件保存路径
|
||||
/// </summary>
|
||||
public static readonly string LastStateFilePath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "laststate.json");
|
||||
public static readonly string UserStateFilePath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "userstate.json");
|
||||
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
@@ -44,6 +46,52 @@ public partial class MainWindow : Window
|
||||
private readonly SFMLRenderWindow _wallpaperRenderWindow;
|
||||
private readonly MainWindowViewModel _vm;
|
||||
|
||||
private readonly List<IDisposable> _userStateWatchers = [];
|
||||
private DispatcherTimer _saveUserStateTimer;
|
||||
private readonly TimeSpan _saveTimerDelay = TimeSpan.FromSeconds(1);
|
||||
|
||||
public bool RootGridCol0Folded
|
||||
{
|
||||
get => ((ContentPresenter)_mainTabControl.Template.FindName("PART_SelectedContentHost", _mainTabControl)).Visibility != Visibility.Visible;
|
||||
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)
|
||||
{
|
||||
// 寄存折叠前的宽度比例
|
||||
_rootGrid.ColumnDefinitions[0].Tag = _rootGrid.ColumnDefinitions[0].Width;
|
||||
_rootGrid.ColumnDefinitions[1].Tag = _rootGrid.ColumnDefinitions[1].Width;
|
||||
_rootGrid.ColumnDefinitions[2].Tag = _rootGrid.ColumnDefinitions[2].Width;
|
||||
|
||||
// 进行折叠
|
||||
mainTabContentHost.Visibility = Visibility.Collapsed;
|
||||
_rootGrid.ColumnDefinitions[0].Width = GridLength.Auto;
|
||||
_rootGrid.ColumnDefinitions[1].Width = new(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 解除折叠
|
||||
_rootGrid.ColumnDefinitions[0].Width = (GridLength)_rootGrid.ColumnDefinitions[0].Tag;
|
||||
_rootGrid.ColumnDefinitions[1].Width = (GridLength)_rootGrid.ColumnDefinitions[1].Tag;
|
||||
_rootGrid.ColumnDefinitions[2].Width = (GridLength)_rootGrid.ColumnDefinitions[2].Tag;
|
||||
mainTabContentHost.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -51,24 +99,24 @@ 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);
|
||||
|
||||
// XXX: hc 的 NotifyIcon 的 Text 似乎没法双向绑定
|
||||
_notifyIcon.Text = _vm.Title;
|
||||
|
||||
SourceInitialized += MainWindow_SourceInitialized;
|
||||
Loaded += MainWindow_Loaded;
|
||||
ContentRendered += MainWindow_ContentRendered;
|
||||
Closing += MainWindow_Closing;
|
||||
Closed += MainWindow_Closed;
|
||||
|
||||
_vm.SpineObjectListViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
|
||||
_vm.SFMLRendererViewModel.RequestSelectionChanging += SpinesListView_RequestSelectionChanging;
|
||||
|
||||
@@ -84,99 +132,32 @@ 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"));
|
||||
rtbTarget.WordColoringRules.Add(new("[I]", "DimGray", "Empty"));
|
||||
rtbTarget.WordColoringRules.Add(new("[W]", "DarkOrange", "Empty"));
|
||||
rtbTarget.WordColoringRules.Add(new("[E]", "Red", "Empty"));
|
||||
rtbTarget.WordColoringRules.Add(new("[F]", "DarkRed", "Empty"));
|
||||
rtbTarget.WordColoringRules.Add(new("[F]", "White", "DarkRed"));
|
||||
|
||||
LogManager.Configuration.AddTarget(rtbTarget);
|
||||
LogManager.Configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, rtbTarget);
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
|
||||
private void LoadLastState()
|
||||
{
|
||||
if (JsonHelper.Deserialize<LastStateModel>(LastStateFilePath, out var m, true))
|
||||
{
|
||||
Left = m.WindowLeft;
|
||||
Top = m.WindowTop;
|
||||
Width = m.WindowWidth;
|
||||
Height = m.WindowHeight;
|
||||
if (m.WindowState == WindowState.Maximized)
|
||||
{
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
_rootGrid.ColumnDefinitions[0].Width = new(m.RootGridCol0Width, GridUnitType.Star);
|
||||
_rootGrid.ColumnDefinitions[2].Width = new(m.RootGridCol2Width, GridUnitType.Star);
|
||||
|
||||
_modelListGrid.RowDefinitions[0].Height = new(m.ModelListRow0Height, GridUnitType.Star);
|
||||
_modelListGrid.RowDefinitions[2].Height = new(m.ModelListRow2Height, GridUnitType.Star);
|
||||
|
||||
_explorerGrid.RowDefinitions[0].Height = new(m.ExplorerGridRow0Height, GridUnitType.Star);
|
||||
_explorerGrid.RowDefinitions[2].Height = new(m.ExplorerGridRow2Height, GridUnitType.Star);
|
||||
|
||||
_rightPanelGrid.RowDefinitions[0].Height = new(m.RightPanelGridRow0Height, GridUnitType.Star);
|
||||
_rightPanelGrid.RowDefinitions[2].Height = new(m.RightPanelGridRow2Height, GridUnitType.Star);
|
||||
|
||||
_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;
|
||||
_vm.SFMLRendererViewModel.BackgroundImageMode = m.BackgroundImageMode;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveLastState()
|
||||
{
|
||||
var rb = RestoreBounds;
|
||||
var m = new LastStateModel()
|
||||
{
|
||||
WindowLeft = rb.Left,
|
||||
WindowTop = rb.Top,
|
||||
WindowWidth = rb.Width,
|
||||
WindowHeight = rb.Height,
|
||||
WindowState = WindowState,
|
||||
|
||||
RootGridCol0Width = _rootGrid.ColumnDefinitions[0].Width.Value,
|
||||
RootGridCol2Width = _rootGrid.ColumnDefinitions[2].Width.Value,
|
||||
|
||||
ModelListRow0Height = _modelListGrid.RowDefinitions[0].Height.Value,
|
||||
ModelListRow2Height = _modelListGrid.RowDefinitions[2].Height.Value,
|
||||
|
||||
ExplorerGridRow0Height = _explorerGrid.RowDefinitions[0].Height.Value,
|
||||
ExplorerGridRow2Height = _explorerGrid.RowDefinitions[2].Height.Value,
|
||||
|
||||
RightPanelGridRow0Height = _rightPanelGrid.RowDefinitions[0].Height.Value,
|
||||
RightPanelGridRow2Height = _rightPanelGrid.RowDefinitions[2].Height.Value,
|
||||
|
||||
ResolutionX = _vm.SFMLRendererViewModel.ResolutionX,
|
||||
ResolutionY = _vm.SFMLRendererViewModel.ResolutionY,
|
||||
MaxFps = _vm.SFMLRendererViewModel.MaxFps,
|
||||
Speed = _vm.SFMLRendererViewModel.Speed,
|
||||
ShowAxis = _vm.SFMLRendererViewModel.ShowAxis,
|
||||
BackgroundColor = _vm.SFMLRendererViewModel.BackgroundColor,
|
||||
BackgroundImageMode = _vm.SFMLRendererViewModel.BackgroundImageMode,
|
||||
};
|
||||
|
||||
JsonHelper.Serialize(m, LastStateFilePath);
|
||||
}
|
||||
|
||||
#region MainWindow 事件处理
|
||||
|
||||
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var vm = _vm.SFMLRendererViewModel;
|
||||
@@ -197,7 +178,9 @@ public partial class MainWindow : Window
|
||||
// 加载首选项
|
||||
_vm.PreferenceViewModel.LoadPreference();
|
||||
|
||||
LoadLastState();
|
||||
// 还原上一次用户历史状态并开启监听器
|
||||
LoadUserState();
|
||||
AddUserStateListeners();
|
||||
}
|
||||
|
||||
private void MainWindow_ContentRendered(object? sender, EventArgs e)
|
||||
@@ -229,11 +212,6 @@ public partial class MainWindow : Window
|
||||
{
|
||||
if (!_vm.IsShuttingDownFromTray)
|
||||
{
|
||||
if (_vm.CloseToTray is null)
|
||||
{
|
||||
_vm.PreferenceViewModel.CloseToTray = MessagePopupService.YesNo(AppResource.Str_CloseToTrayQuest);
|
||||
_vm.PreferenceViewModel.SavePreference();
|
||||
}
|
||||
if (_vm.CloseToTray is true)
|
||||
{
|
||||
Hide();
|
||||
@@ -242,7 +220,10 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
SaveLastState();
|
||||
// 移除监听器并保存当前用户状态
|
||||
RemoveUserStateListensers();
|
||||
SaveUserState();
|
||||
|
||||
_vm.SFMLRendererViewModel.StopRender();
|
||||
}
|
||||
|
||||
@@ -251,12 +232,212 @@ public partial class MainWindow : Window
|
||||
|
||||
}
|
||||
|
||||
private void LoadUserState()
|
||||
{
|
||||
if (JsonHelper.Deserialize<UserStateModel>(UserStateFilePath, out var m, true))
|
||||
{
|
||||
Left = m.WindowLeft;
|
||||
Top = m.WindowTop;
|
||||
Width = m.WindowWidth;
|
||||
Height = m.WindowHeight;
|
||||
if (m.WindowState == WindowState.Maximized)
|
||||
{
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
if (m.RootGridCol0Folded)
|
||||
{
|
||||
RootGridCol0Folded = true;
|
||||
_rootGrid.ColumnDefinitions[0].Tag = new GridLength(m.RootGridCol0Width, GridUnitType.Star);
|
||||
_rootGrid.ColumnDefinitions[2].Tag = new GridLength(m.RootGridCol2Width, GridUnitType.Star);
|
||||
}
|
||||
else
|
||||
{
|
||||
RootGridCol0Folded = false;
|
||||
_rootGrid.ColumnDefinitions[0].Width = new(m.RootGridCol0Width, GridUnitType.Star);
|
||||
_rootGrid.ColumnDefinitions[2].Width = new(m.RootGridCol2Width, GridUnitType.Star);
|
||||
}
|
||||
|
||||
_modelListGrid.RowDefinitions[0].Height = new(m.ModelListRow0Height, GridUnitType.Star);
|
||||
_modelListGrid.RowDefinitions[2].Height = new(m.ModelListRow2Height, GridUnitType.Star);
|
||||
|
||||
_explorerGrid.RowDefinitions[0].Height = new(m.ExplorerGridRow0Height, GridUnitType.Star);
|
||||
_explorerGrid.RowDefinitions[2].Height = new(m.ExplorerGridRow2Height, GridUnitType.Star);
|
||||
|
||||
_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.Speed = m.Speed;
|
||||
_vm.SFMLRendererViewModel.ShowAxis = m.ShowAxis;
|
||||
_vm.SFMLRendererViewModel.BackgroundColor = m.BackgroundColor;
|
||||
_vm.SFMLRendererViewModel.BackgroundImageMode = m.BackgroundImageMode;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveUserState()
|
||||
{
|
||||
var rb = RestoreBounds;
|
||||
var m = new UserStateModel()
|
||||
{
|
||||
WindowLeft = rb.Left,
|
||||
WindowTop = rb.Top,
|
||||
WindowWidth = rb.Width,
|
||||
WindowHeight = rb.Height,
|
||||
WindowState = WindowState,
|
||||
|
||||
RootGridCol0Folded = RootGridCol0Folded,
|
||||
RootGridCol0Width = _rootGrid.ColumnDefinitions[0].Width.Value,
|
||||
RootGridCol2Width = _rootGrid.ColumnDefinitions[2].Width.Value,
|
||||
|
||||
ModelListRow0Height = _modelListGrid.RowDefinitions[0].Height.Value,
|
||||
ModelListRow2Height = _modelListGrid.RowDefinitions[2].Height.Value,
|
||||
|
||||
ExplorerGridRow0Height = _explorerGrid.RowDefinitions[0].Height.Value,
|
||||
ExplorerGridRow2Height = _explorerGrid.RowDefinitions[2].Height.Value,
|
||||
|
||||
RightPanelGridRow0Height = _rightPanelGrid.RowDefinitions[0].Height.Value,
|
||||
RightPanelGridRow2Height = _rightPanelGrid.RowDefinitions[2].Height.Value,
|
||||
|
||||
ExploringDirectory = _vm.ExplorerListViewModel.CurrentDirectory,
|
||||
|
||||
ResolutionX = _vm.SFMLRendererViewModel.ResolutionX,
|
||||
ResolutionY = _vm.SFMLRendererViewModel.ResolutionY,
|
||||
Speed = _vm.SFMLRendererViewModel.Speed,
|
||||
ShowAxis = _vm.SFMLRendererViewModel.ShowAxis,
|
||||
BackgroundColor = _vm.SFMLRendererViewModel.BackgroundColor,
|
||||
BackgroundImageMode = _vm.SFMLRendererViewModel.BackgroundImageMode,
|
||||
};
|
||||
|
||||
if (m.RootGridCol0Folded)
|
||||
{
|
||||
m.RootGridCol0Width = ((GridLength)_rootGrid.ColumnDefinitions[0].Tag).Value;
|
||||
m.RootGridCol2Width = ((GridLength)_rootGrid.ColumnDefinitions[2].Tag).Value;
|
||||
}
|
||||
|
||||
JsonHelper.Serialize(m, UserStateFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SaveUserState"/> 的延时版本, 避免一次性大量执行
|
||||
/// </summary>
|
||||
private void DelayedSaveUserState()
|
||||
{
|
||||
// 第一次调用时创建定时器
|
||||
if (_saveUserStateTimer == null)
|
||||
{
|
||||
_saveUserStateTimer = new() { Interval = _saveTimerDelay };
|
||||
_saveUserStateTimer.Tick += (s, e) =>
|
||||
{
|
||||
_saveUserStateTimer.Stop();
|
||||
SaveUserState();
|
||||
};
|
||||
}
|
||||
|
||||
// 每次触发都重置间隔和计时
|
||||
_saveUserStateTimer.Stop();
|
||||
_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.Speed):
|
||||
case nameof(SFMLRendererViewModel.ShowAxis):
|
||||
case nameof(SFMLRendererViewModel.BackgroundColor):
|
||||
case nameof(SFMLRendererViewModel.BackgroundImageMode):
|
||||
DelayedSaveUserState();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ColorPicker 弹窗事件处理
|
||||
|
||||
private void ButtonPickColor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = !_colorPopup.IsOpen;
|
||||
}
|
||||
|
||||
private void ColorPicker_Confirmed(object sender, HandyControl.Data.FunctionEventArgs<Color> e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
var color = e.Info;
|
||||
var vm = ((MainWindowViewModel)DataContext).SFMLRendererViewModel;
|
||||
vm.BackgroundColor = color;
|
||||
}
|
||||
|
||||
private void ColorPicker_Canceled(object sender, EventArgs e)
|
||||
{
|
||||
_colorPopup.IsOpen = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ViewModel PropertyChanged 事件处理
|
||||
|
||||
private void SFMLRendererViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// XXX: 资源管理器重启后窗口会有问题无法重新显示, 需要重启应用, 否则要重新创建窗口
|
||||
if (e.PropertyName == nameof(SFMLRendererViewModel.WallpaperView))
|
||||
{
|
||||
var wnd = _wallpaperRenderWindow;
|
||||
@@ -268,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
|
||||
@@ -290,6 +479,34 @@ public partial class MainWindow : Window
|
||||
|
||||
#endregion
|
||||
|
||||
#region _mainTabControl 事件处理
|
||||
|
||||
private void MainTabControlHeader_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 全屏状态忽略该功能
|
||||
if (_fullScreenLayout.Visibility == Visibility.Visible)
|
||||
return;
|
||||
|
||||
if (sender is not FrameworkElement fe)
|
||||
return;
|
||||
|
||||
// 找到包含这个 Border 的 TabItem
|
||||
var tabItem = VisualFindParent<TabItem>(fe);
|
||||
if (tabItem is null)
|
||||
return;
|
||||
|
||||
if (_mainTabControl.SelectedItem == tabItem)
|
||||
{
|
||||
RootGridCol0Folded = !RootGridCol0Folded;
|
||||
}
|
||||
else
|
||||
{
|
||||
RootGridCol0Folded = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region _spinesListView 事件处理
|
||||
|
||||
private void SpinesListView_RequestSelectionChanging(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
@@ -409,13 +626,6 @@ public partial class MainWindow : Window
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static T? VisualUpwardSearch<T>(DependencyObject? source) where T : DependencyObject
|
||||
{
|
||||
while (source != null && source is not T)
|
||||
source = VisualTreeHelper.GetParent(source);
|
||||
return source as T;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region _spineFilesListBox 事件
|
||||
@@ -456,6 +666,8 @@ public partial class MainWindow : Window
|
||||
|
||||
if (_fullScreenLayout.Visibility == Visibility.Visible) return;
|
||||
|
||||
RootGridCol0Folded = false; // 取消折叠
|
||||
|
||||
IntPtr hwnd = new WindowInteropHelper(this).Handle;
|
||||
if (User32.GetScreenResolution(hwnd, out var resX, out var resY))
|
||||
{
|
||||
@@ -488,7 +700,7 @@ public partial class MainWindow : Window
|
||||
_renderPanelButtonsPopupContainer.Child = _renderPanelButtonsPanel;
|
||||
|
||||
_loggerBoxContainer.Child = null;
|
||||
_loggerBoxPopupContainer.Child = _loggerRichTextBox;
|
||||
_loggerBoxPopupContainer.Child = _loggerBoxPanel;
|
||||
}
|
||||
|
||||
private void SwitchToNormalLayout()
|
||||
@@ -498,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;
|
||||
@@ -706,14 +918,26 @@ public partial class MainWindow : Window
|
||||
|
||||
#endregion
|
||||
|
||||
private static T? VisualUpwardSearch<T>(DependencyObject? source) where T : DependencyObject
|
||||
{
|
||||
while (source != null && source is not T)
|
||||
source = VisualTreeHelper.GetParent(source);
|
||||
return source as T;
|
||||
}
|
||||
|
||||
public static T? VisualFindParent<T>(DependencyObject child) where T : DependencyObject
|
||||
=> VisualUpwardSearch<T>(VisualTreeHelper.GetParent(child));
|
||||
|
||||
private void DebugMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
#if DEBUG
|
||||
var a = _rootGrid.ColumnDefinitions[0].Width;
|
||||
var b = _rootGrid.ColumnDefinitions[1].Width;
|
||||
var c = _rootGrid.ColumnDefinitions[2].Width;
|
||||
Debug.WriteLine(a);
|
||||
Debug.WriteLine(_rootGrid.ColumnDefinitions[0].Width.IsStar);
|
||||
_logger.Debug("Debug");
|
||||
_logger.Info("Info");
|
||||
_logger.Warn("Warn");
|
||||
_logger.Error("Error");
|
||||
_logger.Fatal("Fatal");
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
d:DataContext="{d:DesignInstance Type=models:PreferenceModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{DynamicResource Str_Preference}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Height="650"
|
||||
Width="600"
|
||||
ShowInTaskbar="False"
|
||||
@@ -30,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>
|
||||
@@ -40,24 +41,22 @@
|
||||
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource ComboBoxBaseStyle}">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButton}">
|
||||
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource MyToggleButtonBaseStyle}">
|
||||
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||
</Style>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource GroupBoxTab}">
|
||||
<Setter Property="BorderBrush" Value="LightGray"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="hc:TitleElement.Background" Value="Transparent"/>
|
||||
<Style TargetType="{x:Type GroupBox}" BasedOn="{StaticResource MyGroupBoxBaseStyle}">
|
||||
<Setter Property="Margin" Value="0 5 0 10"/>
|
||||
</Style>
|
||||
</Border.Resources>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Grid.IsSharedSizeScope="True" Margin="0 0 0 50">
|
||||
|
||||
<ScrollViewer Style="{StaticResource MyVerticalScrollViewerBaseStyle}">
|
||||
<StackPanel Grid.IsSharedSizeScope="True">
|
||||
<GroupBox Header="{DynamicResource Str_TextureLoadPreference}">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding ForcePremul}" ToolTip="{DynamicResource Str_ForcePremulTooltip}"/>
|
||||
@@ -66,7 +65,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ForceNearest}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding ForceNearest}"/>
|
||||
@@ -75,7 +74,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding ForceMipmap}" ToolTip="{DynamicResource Str_ForceMipmapTooltip}"/>
|
||||
@@ -88,7 +87,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_IsShown}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding IsShown}"/>
|
||||
@@ -97,7 +96,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_UsePma}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding UsePma}"/>
|
||||
@@ -106,7 +105,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugTexture}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugTexture}"/>
|
||||
@@ -115,7 +114,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugBounds}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBounds}"/>
|
||||
@@ -124,7 +123,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugBones}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBones}"/>
|
||||
@@ -133,7 +132,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugRegions}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugRegions}"/>
|
||||
@@ -142,7 +141,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugMeshHulls}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugMeshHulls}"/>
|
||||
@@ -151,7 +150,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugMeshes}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugMeshes}"/>
|
||||
@@ -160,7 +159,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugClippings}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugClippings}"/>
|
||||
@@ -169,7 +168,7 @@
|
||||
<!-- <Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugBoundingBoxes}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugBoundingBoxes}"/>
|
||||
@@ -178,7 +177,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugPaths}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugPaths}"/>
|
||||
@@ -187,7 +186,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_DebugPoints}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding DebugPoints}"/>
|
||||
@@ -196,23 +195,12 @@
|
||||
</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="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_RenderSelectedOnly}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding RenderSelectedOnly}"/>
|
||||
@@ -221,7 +209,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_HitTestLevel}"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
@@ -232,7 +220,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_LogHitSlots}" ToolTip="{DynamicResource Str_LogHitSlotsTooltip}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding LogHitSlots}" ToolTip="{DynamicResource Str_LogHitSlotsTooltip}"/>
|
||||
@@ -241,7 +229,42 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<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"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_WallpaperView}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding WallpaperView}"/>
|
||||
@@ -250,7 +273,16 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<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"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_CloseToTray}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding CloseToTray}"/>
|
||||
@@ -259,7 +291,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_AutoRun}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding AutoRun}"/>
|
||||
@@ -268,7 +300,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_AutoRunWorkspaceConfigPath}"
|
||||
ToolTip="{DynamicResource Str_AutoRunWorkspaceConfigPathTooltip}"/>
|
||||
@@ -285,7 +317,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label Content="{DynamicResource Str_AssociateFileSuffix}"/>
|
||||
<ToggleButton Grid.Column="1" IsChecked="{Binding AssociateFileSuffix}"/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -10,9 +12,11 @@ using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using Win32Natives;
|
||||
|
||||
namespace SpineViewer.Views
|
||||
{
|
||||
@@ -24,6 +28,13 @@ namespace SpineViewer.Views
|
||||
public PreferenceDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += PreferenceDialog_SourceInitialized;
|
||||
}
|
||||
|
||||
private void PreferenceDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ButtonOK_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
d:DataContext="{d:DesignInstance Type=vm:ProgressDialogViewModel}"
|
||||
mc:Ignorable="d"
|
||||
Title="{Binding Title}"
|
||||
Background="{DynamicResource RegionBrush}"
|
||||
Width="550"
|
||||
Height="250"
|
||||
ResizeMode="NoResize"
|
||||
@@ -21,7 +22,11 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding ProgressText}" Padding="20" LineHeight="25" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding ProgressText}"
|
||||
Foreground="{DynamicResource PrimaryTextBrush}"
|
||||
Padding="20"
|
||||
LineHeight="25"
|
||||
TextWrapping="Wrap"/>
|
||||
<ProgressBar Grid.Row="1"
|
||||
Value="{Binding Done}"
|
||||
Maximum="{Binding Total}"
|
||||
@@ -29,7 +34,7 @@
|
||||
VerticalAlignment="Center"
|
||||
Margin="20 5"/>
|
||||
<Button Grid.Row="2"
|
||||
Content="{StaticResource Str_Cancel}"
|
||||
Content="{DynamicResource Str_Cancel}"
|
||||
Command="{Binding Cmd_Cancel}"
|
||||
Width="100"
|
||||
Margin="15"/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using SpineViewer.ViewModels;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.ViewModels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -14,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
|
||||
{
|
||||
@@ -25,9 +28,16 @@ namespace SpineViewer.Views
|
||||
public ProgressDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += ProgressDialog_SourceInitialized;
|
||||
Loaded += ProgressWindow_Loaded;
|
||||
}
|
||||
|
||||
private void ProgressDialog_SourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
this.SetWindowTextColor(AppResource.Color_PrimaryText);
|
||||
this.SetWindowCaptionColor(AppResource.Color_Region);
|
||||
}
|
||||
|
||||
private void ProgressWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
|
||||
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,241 +1,100 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using NLog;
|
||||
using SkiaSharp;
|
||||
using Spectre.Console;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
using Spine.Interfaces;
|
||||
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] [--pma] [--fps INT] [--loop] [--crf 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
|
||||
--animation STR Animation name
|
||||
--pma Use premultiplied alpha, default false
|
||||
--fps INT Frames per second, default 24
|
||||
--loop Whether to loop the animation, default false
|
||||
--crf INT Constant Rate Factor i.e. video quality, from 0 (lossless) to 51 (worst), default 23
|
||||
--width INT Output width, default 512
|
||||
--height INT Output height, default 512
|
||||
--centerx INT Center X offset, default automatically finds bounds
|
||||
--centery INT Center Y offset, default automatically finds bounds
|
||||
--zoom FLOAT Zoom level, default 1.0
|
||||
--speed FLOAT Speed of animation, default 1.0
|
||||
--color HEX Background color as a hex RGBA color, default 000000ff (opaque black)
|
||||
--quiet Removes console progress log, default false
|
||||
";
|
||||
|
||||
public static void Main(string[] args)
|
||||
public static Option<bool> OptQuiet { get; } = new("--quiet", "-q")
|
||||
{
|
||||
string? skelPath = null;
|
||||
string? atlasPath = null;
|
||||
string? output = null;
|
||||
string? animation = null;
|
||||
bool pma = false;
|
||||
uint fps = 24;
|
||||
bool loop = false;
|
||||
int crf = 23;
|
||||
uint? width = null;
|
||||
uint? height = null;
|
||||
int? centerx = null;
|
||||
int? centery = null;
|
||||
float zoom = 1;
|
||||
float speed = 1;
|
||||
Color backgroundColor = Color.Black;
|
||||
bool quiet = false;
|
||||
Description = "Suppress console logging (quiet mode).",
|
||||
Recursive = true,
|
||||
};
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
InitializeFileLog();
|
||||
|
||||
var cmdRoot = new RootCommand("Root Command")
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--help":
|
||||
Console.Write(USAGE);
|
||||
Environment.Exit(0);
|
||||
break;
|
||||
case "--skel":
|
||||
skelPath = args[++i];
|
||||
break;
|
||||
case "--atlas":
|
||||
atlasPath = args[++i];
|
||||
break;
|
||||
case "--output":
|
||||
output = args[++i];
|
||||
break;
|
||||
case "--animation":
|
||||
animation = args[++i];
|
||||
break;
|
||||
case "--pma":
|
||||
pma = true;
|
||||
break;
|
||||
case "--fps":
|
||||
fps = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--loop":
|
||||
loop = true;
|
||||
break;
|
||||
case "--crf":
|
||||
crf = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--width":
|
||||
width = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--height":
|
||||
height = uint.Parse(args[++i]);
|
||||
break;
|
||||
case "--centerx":
|
||||
centerx = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--centery":
|
||||
centery = int.Parse(args[++i]);
|
||||
break;
|
||||
case "--zoom":
|
||||
zoom = float.Parse(args[++i]);
|
||||
break;
|
||||
case "--speed":
|
||||
speed = float.Parse(args[++i]);
|
||||
break;
|
||||
case "--color":
|
||||
backgroundColor = new Color(uint.Parse(args[++i], NumberStyles.HexNumber));
|
||||
break;
|
||||
case "--quiet":
|
||||
quiet = true;
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown argument: {args[i]}");
|
||||
Environment.Exit(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
OptQuiet,
|
||||
new QueryCommand(),
|
||||
new PreviewCommand(),
|
||||
new ExportCommand(),
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(skelPath))
|
||||
var result = cmdRoot.Parse(args);
|
||||
|
||||
if (!result.GetValue(OptQuiet))
|
||||
InitializeConsoleLog();
|
||||
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine("Missing --skel");
|
||||
Environment.Exit(2);
|
||||
return result.Invoke();
|
||||
}
|
||||
if (string.IsNullOrEmpty(output))
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("Missing --output");
|
||||
Environment.Exit(2);
|
||||
_logger.Debug(ex.ToString());
|
||||
_logger.Fatal("Failed to execute, {0}", ex.Message);
|
||||
return -1;
|
||||
}
|
||||
if (!Enum.TryParse<FFmpegVideoExporter.VideoFormat>(Path.GetExtension(output).TrimStart('.'), true, out var videoFormat))
|
||||
{
|
||||
var validExtensions = string.Join(", ", Enum.GetNames(typeof(FFmpegVideoExporter.VideoFormat)));
|
||||
Console.Error.WriteLine($"Invalid output extension. Supported formats are: {validExtensions}");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
|
||||
var sp = new SpineObject(skelPath, atlasPath);
|
||||
sp.UsePma = pma;
|
||||
|
||||
if (string.IsNullOrEmpty(animation))
|
||||
{
|
||||
var availableAnimations = string.Join(", ", sp.Data.Animations);
|
||||
Console.Error.WriteLine($"Missing --animation. Available animations for {sp.Name}: {availableAnimations}");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
var trackEntry = sp.AnimationState.SetAnimation(0, animation, loop);
|
||||
sp.Update(0);
|
||||
|
||||
FFmpegVideoExporter exporter;
|
||||
if (width is uint w && height is uint h && centerx is int cx && centery is int cy)
|
||||
{
|
||||
exporter = new FFmpegVideoExporter(w, h)
|
||||
{
|
||||
Center = (cx, cy),
|
||||
Size = (w / zoom, -h / zoom),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var bounds = GetFloatRectCanvasBounds(GetSpineObjectAnimationBounds(sp, fps), new(width ?? 512, height ?? 512));
|
||||
exporter = new FFmpegVideoExporter(width ?? (uint)Math.Ceiling(bounds.Width), height ?? (uint)Math.Ceiling(bounds.Height))
|
||||
{
|
||||
Center = bounds.Position + bounds.Size / 2,
|
||||
Size = (bounds.Width, -bounds.Height),
|
||||
};
|
||||
}
|
||||
exporter.Duration = trackEntry.Animation.Duration;
|
||||
exporter.Fps = fps;
|
||||
exporter.Format = videoFormat;
|
||||
exporter.Loop = loop;
|
||||
exporter.Crf = crf;
|
||||
exporter.Speed = speed;
|
||||
exporter.BackgroundColor = backgroundColor;
|
||||
|
||||
if (!quiet)
|
||||
exporter.ProgressReporter = (total, done, text) => Console.Write($"\r{text}");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
exporter.Export(output, cts.Token, sp);
|
||||
|
||||
if (!quiet)
|
||||
Console.WriteLine();
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
public static SpineObject CopySpineObject(SpineObject sp)
|
||||
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>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user