修改结构
This commit is contained in:
410
SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs
Normal file
410
SpineViewer/ViewModels/MainWindow/ExplorerListViewModel.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NLog;
|
||||
using SFML.Audio;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shell;
|
||||
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
public class ExplorerListViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// 预览图的保存质量
|
||||
/// </summary>
|
||||
public static int PreviewQuality { get; set; } = 80;
|
||||
|
||||
/// <summary>
|
||||
/// 缩略图文件名格式字符串, 需要一个参数
|
||||
/// </summary>
|
||||
public static string PreviewFileNameFormat => ".{0}.preview.webp";
|
||||
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly MainWindowViewModel _vmMain;
|
||||
|
||||
/// <summary>
|
||||
/// 当前目录路径
|
||||
/// </summary>
|
||||
private string? _currentDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// 当前目录下文件项缓存
|
||||
/// </summary>
|
||||
private readonly List<ExplorerItemViewModel> _items = [];
|
||||
|
||||
public ExplorerListViewModel(MainWindowViewModel vmMain)
|
||||
{
|
||||
_vmMain = vmMain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 筛选字符串
|
||||
/// </summary>
|
||||
public string? FilterString
|
||||
{
|
||||
get => _filterString;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _filterString, value)) return;
|
||||
if (string.IsNullOrWhiteSpace(_filterString))
|
||||
{
|
||||
_shownItems = _items.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_shownItems = [];
|
||||
_shownItems.AddRange(_items.Where(it => it.FileName.Contains(_filterString)));
|
||||
}
|
||||
OnPropertyChanged(nameof(ShownItems));
|
||||
}
|
||||
}
|
||||
private string? _filterString;
|
||||
|
||||
/// <summary>
|
||||
/// 当前目录下的所有子项文件, 含递归目录
|
||||
/// </summary>
|
||||
public List<ExplorerItemViewModel> ShownItems => _shownItems;
|
||||
private List<ExplorerItemViewModel> _shownItems = [];
|
||||
|
||||
/// <summary>
|
||||
/// 选择项, 显示某一项的具体信息和预览图
|
||||
/// </summary>
|
||||
public ExplorerItemViewModel? SelectedItem => _selectedItem;
|
||||
private ExplorerItemViewModel? _selectedItem;
|
||||
|
||||
/// <summary>
|
||||
/// 选择文件夹命令
|
||||
/// </summary>
|
||||
public RelayCommand Cmd_ChangeCurrentDirectory => _cmd_ChangeCurrentDirectory ??= new(() =>
|
||||
{
|
||||
if (OpenFolderService.OpenFolder(out var selectedPath))
|
||||
{
|
||||
_currentDirectory = selectedPath;
|
||||
RefreshItems();
|
||||
}
|
||||
});
|
||||
private RelayCommand? _cmd_ChangeCurrentDirectory;
|
||||
|
||||
public RelayCommand Cmd_RefreshItems => _cmd_RefreshItems ??= new(RefreshItems);
|
||||
private RelayCommand? _cmd_RefreshItems;
|
||||
|
||||
/// <summary>
|
||||
/// 选中项发生变化命令
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_SelectionChanged => _cmd_SelectionChanged ??= new(args =>
|
||||
{
|
||||
if (args is null || args.Count != 1)
|
||||
{
|
||||
SetProperty(ref _selectedItem, null, nameof(SelectedItem));
|
||||
}
|
||||
else
|
||||
{
|
||||
SetProperty(ref _selectedItem, args[0] as ExplorerItemViewModel, nameof(SelectedItem));
|
||||
}
|
||||
});
|
||||
private RelayCommand<IList?>? _cmd_SelectionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 右键菜单, 添加到模型列表
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_AddSelectedItems => _cmd_AddSelectedItems ??= new(AddSelectedItems_Execute, args => args is not null && args.Count > 0);
|
||||
private RelayCommand<IList?>? _cmd_AddSelectedItems;
|
||||
|
||||
private void AddSelectedItems_Execute(IList? args)
|
||||
{
|
||||
if (args is null || args.Count <= 0) return;
|
||||
_vmMain.SpineObjectListViewModel.AddSpineObjectFromFileList(args.Cast<ExplorerItemViewModel>().Select(m => m.FullPath).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对参数项生成预览图
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_GeneratePreviews => _cmd_GeneratePreviews ??= new(GeneratePreview_Execute, args => args is not null && args.Count > 0);
|
||||
private RelayCommand<IList?>? _cmd_GeneratePreviews;
|
||||
|
||||
private void GeneratePreview_Execute(IList? args)
|
||||
{
|
||||
if (args is null || args.Count <= 0) return;
|
||||
|
||||
if (args.Count <= 1)
|
||||
{
|
||||
var m = (ExplorerItemViewModel)args[0];
|
||||
try
|
||||
{
|
||||
using var sp = new SpineObject(m.FullPath);
|
||||
sp.Skeleton.GetBounds(out var x, out var y, out var w, out var h);
|
||||
var bounds = new SFML.Graphics.FloatRect(x, y, w, h).GetCanvasBounds(new(510, 510), 2);
|
||||
using var exporter = new FrameExporter(512, 512)
|
||||
{
|
||||
Center = bounds.Position + bounds.Size / 2,
|
||||
Size = new(bounds.Width, -bounds.Height),
|
||||
Format = SkiaSharp.SKEncodedImageFormat.Webp,
|
||||
Quality = PreviewQuality,
|
||||
};
|
||||
exporter.Export(m.PreviewFilePath, sp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to generate preview: {0}, {1}", m.PreviewFilePath, ex.Message);
|
||||
}
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
else
|
||||
{
|
||||
ProgressService.RunAsync((pr, ct) => GeneratePreviewTask(
|
||||
args.Cast<ExplorerItemViewModel>().ToArray(), pr, ct),
|
||||
AppResource.Str_GeneratePreviewsTitle
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void GeneratePreviewTask(ExplorerItemViewModel[] models, IProgressReporter reporter, CancellationToken ct)
|
||||
{
|
||||
int totalCount = models.Length;
|
||||
int success = 0;
|
||||
int error = 0;
|
||||
|
||||
_vmMain.ProgressState = TaskbarItemProgressState.Normal;
|
||||
_vmMain.ProgressValue = 0;
|
||||
|
||||
reporter.Total = totalCount;
|
||||
reporter.Done = 0;
|
||||
reporter.ProgressText = $"[0/{totalCount}]";
|
||||
|
||||
using var exporter = new FrameExporter(512, 512)
|
||||
{
|
||||
Format = SkiaSharp.SKEncodedImageFormat.Webp,
|
||||
Quality = PreviewQuality,
|
||||
};
|
||||
for (int i = 0; i < totalCount; i++)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
|
||||
var m = models[i];
|
||||
reporter.ProgressText = $"[{i}/{totalCount}] {m.FullPath}";
|
||||
|
||||
try
|
||||
{
|
||||
using var sp = new SpineObject(m.FullPath);
|
||||
sp.Skeleton.GetBounds(out var x, out var y, out var w, out var h);
|
||||
var bounds = new SFML.Graphics.FloatRect(x, y, w, h).GetCanvasBounds(new(510, 510), 2);
|
||||
exporter.Center = bounds.Position + bounds.Size / 2;
|
||||
exporter.Size = new(bounds.Width, -bounds.Height);
|
||||
exporter.Export(m.PreviewFilePath, sp);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to generate preview: {0}, {1}", m.PreviewFilePath, ex.Message);
|
||||
error++;
|
||||
}
|
||||
|
||||
reporter.Done = i + 1;
|
||||
reporter.ProgressText = $"[{i + 1}/{totalCount}] {m}";
|
||||
_vmMain.ProgressValue = (i + 1f) / totalCount;
|
||||
}
|
||||
_vmMain.ProgressState = TaskbarItemProgressState.None;
|
||||
|
||||
if (error > 0)
|
||||
_logger.Warn("Preview generation {0} successfully, {1} failed", success, error);
|
||||
else
|
||||
_logger.Info("{0} previews generated successfully", success);
|
||||
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除参数项的预览图
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_DeletePreviews => _cmd_DeletePreviews ??= new(DeletePreview_Execute, args => args is not null && args.Count > 0);
|
||||
private RelayCommand<IList?>? _cmd_DeletePreviews;
|
||||
|
||||
private void DeletePreview_Execute(IList? args)
|
||||
{
|
||||
if (args is null || args.Count <= 0) return;
|
||||
if (!MessagePopupService.Quest(string.Format(AppResource.Str_DeleteItemsQuest, args.Count))) return;
|
||||
|
||||
if (args.Count <= 10)
|
||||
{
|
||||
foreach (var m in args.Cast<ExplorerItemViewModel>())
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(m.PreviewFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to delete preview: {0}, {1}", m.PreviewFilePath, ex.Message);
|
||||
}
|
||||
}
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
else
|
||||
{
|
||||
ProgressService.RunAsync((pr, ct) => DeletePreviewTask(
|
||||
args.Cast<ExplorerItemViewModel>().ToArray(), pr, ct),
|
||||
AppResource.Str_DeletePreviewsTitle
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeletePreviewTask(ExplorerItemViewModel[] models, IProgressReporter reporter, CancellationToken ct)
|
||||
{
|
||||
int totalCount = models.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 m = models[i];
|
||||
reporter.ProgressText = $"[{i}/{totalCount}] {m.FullPath}";
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(m.PreviewFilePath);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to delete preview: {0}, {1}", m.PreviewFilePath, ex.Message);
|
||||
error++;
|
||||
}
|
||||
|
||||
reporter.Done = i + 1;
|
||||
reporter.ProgressText = $"[{i + 1}/{totalCount}] {m}";
|
||||
_vmMain.ProgressValue = (i + 1f) / totalCount;
|
||||
}
|
||||
_vmMain.ProgressState = TaskbarItemProgressState.None;
|
||||
|
||||
if (error > 0)
|
||||
_logger.Warn("Preview deletion {0} successfully, {1} failed", success, error);
|
||||
else
|
||||
_logger.Info("{0} previews deleted successfully", success);
|
||||
|
||||
_logger.LogCurrentProcessMemoryUsage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新显示, 可以更新文件夹项缓存
|
||||
/// </summary>
|
||||
public void RefreshItems()
|
||||
{
|
||||
_items.Clear();
|
||||
if (Directory.Exists(_currentDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(_currentDirectory, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var lowerPath = file.ToLower();
|
||||
if (SpineObject.PossibleSuffixMapping.Keys.Any(lowerPath.EndsWith))
|
||||
_items.Add(new(file));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to enumerate files in dir: {0}, {1}", _currentDirectory, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
_shownItems = [];
|
||||
if (string.IsNullOrWhiteSpace(_filterString))
|
||||
{
|
||||
_shownItems = _items.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_shownItems = [];
|
||||
_shownItems.AddRange(_items.Where(it => it.FileName.Contains(_filterString)));
|
||||
}
|
||||
OnPropertyChanged(nameof(ShownItems));
|
||||
}
|
||||
}
|
||||
|
||||
public class ExplorerItemViewModel : ObservableObject
|
||||
{
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public ExplorerItemViewModel(string path)
|
||||
{
|
||||
FullPath = Path.GetFullPath(path);
|
||||
FileDirectory = Path.GetDirectoryName(FullPath) ?? "";
|
||||
FileName = Path.GetFileName(FullPath);
|
||||
PreviewFilePath = Path.Combine(FileDirectory, string.Format(ExplorerListViewModel.PreviewFileNameFormat, FileName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完整路径
|
||||
/// </summary
|
||||
public string FullPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件所处目录
|
||||
/// </summary>
|
||||
public string FileDirectory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 预览图路径
|
||||
/// </summary>
|
||||
public string PreviewFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 预览图
|
||||
/// </summary>
|
||||
public ImageSource? PreviewImage
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return WpfExtension.LoadWebpWithAlpha(PreviewFilePath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Warn("Failed to load preview image for {0}, {1}", FullPath, ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs
Normal file
109
SpineViewer/ViewModels/MainWindow/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HandyControl.Controls;
|
||||
using NLog;
|
||||
using SFMLRenderer;
|
||||
using Spine;
|
||||
using Spine.Exporters;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Shell;
|
||||
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// MainWindow 上下文对象
|
||||
/// </summary>
|
||||
public class MainWindowViewModel : ObservableObject
|
||||
{
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public MainWindowViewModel(ISFMLRenderer sfmlRenderer)
|
||||
{
|
||||
_sfmlRenderer = sfmlRenderer;
|
||||
_explorerListViewModel = new(this);
|
||||
_spineObjectListViewModel = new(this);
|
||||
_sfmlRendererViewModel = new(this);
|
||||
}
|
||||
|
||||
public string Title => $"SpineViewer - v{App.Version}";
|
||||
|
||||
/// <summary>
|
||||
/// SFML 渲染对象
|
||||
/// </summary>
|
||||
public ISFMLRenderer SFMLRenderer => _sfmlRenderer;
|
||||
private readonly ISFMLRenderer _sfmlRenderer;
|
||||
|
||||
public TaskbarItemProgressState ProgressState { get => _progressState; set => SetProperty(ref _progressState, value); }
|
||||
private TaskbarItemProgressState _progressState = TaskbarItemProgressState.None;
|
||||
|
||||
public float ProgressValue { get => _progressValue; set => SetProperty(ref _progressValue, value); }
|
||||
private float _progressValue = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 已加载的 Spine 对象
|
||||
/// </summary>
|
||||
public ObservableCollectionWithLock<SpineObjectModel> SpineObjects => _spineObjectModels;
|
||||
private readonly ObservableCollectionWithLock<SpineObjectModel> _spineObjectModels = [];
|
||||
|
||||
/// <summary>
|
||||
/// 浏览页列表 ViewModel
|
||||
/// </summary>
|
||||
public ExplorerListViewModel ExplorerListViewModel => _explorerListViewModel;
|
||||
private readonly ExplorerListViewModel _explorerListViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 模型列表 ViewModel
|
||||
/// </summary>
|
||||
public SpineObjectListViewModel SpineObjectListViewModel => _spineObjectListViewModel;
|
||||
private readonly SpineObjectListViewModel _spineObjectListViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 模型属性页 ViewModel
|
||||
/// </summary>
|
||||
public SpineObjectTabViewModel SpineObjectTabViewModel => _spineObjectTabViewModel;
|
||||
private readonly SpineObjectTabViewModel _spineObjectTabViewModel = new();
|
||||
|
||||
/// <summary>
|
||||
/// SFML 渲染 ViewModel
|
||||
/// </summary>
|
||||
public SFMLRendererViewModel SFMLRendererViewModel => _sfmlRendererViewModel;
|
||||
private readonly SFMLRendererViewModel _sfmlRendererViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 显示诊断信息对话框
|
||||
/// </summary>
|
||||
public RelayCommand Cmd_ShowDiagnosticsDialog => _cmd_ShowDiagnosticsDialog ??= new(() => { DiagnosticsDialogService.ShowDiagnosticsDialog(); });
|
||||
private RelayCommand? _cmd_ShowDiagnosticsDialog;
|
||||
|
||||
/// <summary>
|
||||
/// 显示关于对话框
|
||||
/// </summary>
|
||||
public RelayCommand Cmd_ShowAboutDialog => _cmd_ShowAboutDialog ??= new(() => { AboutDialogService.ShowAboutDialog(); });
|
||||
private RelayCommand? _cmd_ShowAboutDialog;
|
||||
|
||||
/// <summary>
|
||||
/// 调试命令
|
||||
/// </summary>
|
||||
public RelayCommand Cmd_Debug => _cmd_Debug ??= new(Debug_Execute);
|
||||
private RelayCommand? _cmd_Debug;
|
||||
|
||||
private void Debug_Execute()
|
||||
{
|
||||
#if DEBUG
|
||||
|
||||
MessagePopupService.Quest("测试一下");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
427
SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs
Normal file
427
SpineViewer/ViewModels/MainWindow/SFMLRendererViewModel.cs
Normal file
@@ -0,0 +1,427 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using HandyControl.Expression.Shapes;
|
||||
using NLog;
|
||||
using SFMLRenderer;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
public class SFMLRendererViewModel : ObservableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志器
|
||||
/// </summary>
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly MainWindowViewModel _vmMain;
|
||||
private readonly ObservableCollectionWithLock<SpineObjectModel> _models;
|
||||
private readonly ISFMLRenderer _renderer;
|
||||
|
||||
/// <summary>
|
||||
/// 被选中对象的背景颜色
|
||||
/// </summary>
|
||||
private static readonly SFML.Graphics.Color _selectedBackgroundColor = new(255, 255, 255, 50);
|
||||
|
||||
/// <summary>
|
||||
/// 被选中对象背景顶点缓冲区
|
||||
/// </summary>
|
||||
private readonly SFML.Graphics.VertexArray _selectedBackgroundVertices = new(SFML.Graphics.PrimitiveType.Quads, 4); // XXX: 暂时未使用 Dispose 释放
|
||||
|
||||
/// <summary>
|
||||
/// 预览画面坐标轴颜色
|
||||
/// </summary>
|
||||
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 释放
|
||||
|
||||
/// <summary>
|
||||
/// 帧间隔计时器
|
||||
/// </summary>
|
||||
private readonly SFML.System.Clock _clock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 渲染任务
|
||||
/// </summary>
|
||||
private Task? _renderTask = null;
|
||||
private CancellationTokenSource? _cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// 快进时间量
|
||||
/// </summary>
|
||||
private float _forwardDelta = 0;
|
||||
private readonly object _forwardDeltaLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 临时变量, 记录拖放世界源点
|
||||
/// </summary>
|
||||
private SFML.System.Vector2f? _draggingSrc = null;
|
||||
|
||||
public SFMLRendererViewModel(MainWindowViewModel vmMain)
|
||||
{
|
||||
_vmMain = vmMain;
|
||||
_models = _vmMain.SpineObjects;
|
||||
_renderer = _vmMain.SFMLRenderer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求选中项发生变化
|
||||
/// </summary>
|
||||
public event NotifyCollectionChangedEventHandler? RequestSelectionChanging;
|
||||
|
||||
public uint ResolutionX
|
||||
{
|
||||
get => _renderer.Resolution.X;
|
||||
set => SetProperty(_renderer.Resolution.X, value, _renderer, (r, v) => r.Resolution = new(v, r.Resolution.Y));
|
||||
}
|
||||
|
||||
public uint ResolutionY
|
||||
{
|
||||
get => _renderer.Resolution.Y;
|
||||
set => SetProperty(_renderer.Resolution.Y, value, _renderer, (r, v) => r.Resolution = new(r.Resolution.X, v));
|
||||
}
|
||||
|
||||
public float CenterX
|
||||
{
|
||||
get => _renderer.Center.X;
|
||||
set => SetProperty(_renderer.Center.X, value, _renderer, (r, v) => r.Center = new(v, r.Center.Y));
|
||||
}
|
||||
|
||||
public float CenterY
|
||||
{
|
||||
get => _renderer.Center.Y;
|
||||
set => SetProperty(_renderer.Center.Y, value, _renderer, (r, v) => r.Center = new(r.Center.X, v));
|
||||
}
|
||||
|
||||
public float Zoom
|
||||
{
|
||||
get => _renderer.Zoom;
|
||||
set => SetProperty(_renderer.Zoom, value, _renderer, (r, v) => r.Zoom = value);
|
||||
}
|
||||
|
||||
public float Rotation
|
||||
{
|
||||
get => _renderer.Rotation;
|
||||
set => SetProperty(_renderer.Rotation, value, _renderer, (r, v) => r.Rotation = value);
|
||||
}
|
||||
|
||||
public bool FlipX
|
||||
{
|
||||
get => _renderer.FlipX;
|
||||
set => SetProperty(_renderer.FlipX, value, _renderer, (r, v) => r.FlipX = value);
|
||||
}
|
||||
|
||||
public bool FlipY
|
||||
{
|
||||
get => _renderer.FlipY;
|
||||
set => SetProperty(_renderer.FlipY, value, _renderer, (r, v) => r.FlipY = value);
|
||||
}
|
||||
|
||||
public uint MaxFps
|
||||
{
|
||||
get => _renderer.MaxFps;
|
||||
set => SetProperty(_renderer.MaxFps, value, _renderer, (r, v) => r.MaxFps = value);
|
||||
}
|
||||
|
||||
public bool ShowAxis
|
||||
{
|
||||
get => _showAxis;
|
||||
set => SetProperty(ref _showAxis, value);
|
||||
}
|
||||
private bool _showAxis = true;
|
||||
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get => Color.FromRgb(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B);
|
||||
set => SetProperty(BackgroundColor, value, this, (m, v) => m._backgroundColor = new(value.R, value.G, value.B));
|
||||
}
|
||||
private SFML.Graphics.Color _backgroundColor = new(105, 105, 105);
|
||||
|
||||
public bool RenderSelectedOnly
|
||||
{
|
||||
get => _renderSelectedOnly;
|
||||
set => SetProperty(ref _renderSelectedOnly, value);
|
||||
}
|
||||
private bool _renderSelectedOnly = false;
|
||||
|
||||
public bool IsUpdating
|
||||
{
|
||||
get => _isUpdating;
|
||||
private set
|
||||
{
|
||||
if (value == _isUpdating) return;
|
||||
_isUpdating = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Geo_PlayPause));
|
||||
}
|
||||
}
|
||||
private bool _isUpdating = true;
|
||||
|
||||
public RelayCommand Cmd_Stop => _cmd_Stop ??= new(() =>
|
||||
{
|
||||
IsUpdating = false;
|
||||
lock (_models) foreach (var sp in _models) sp.ResetAnimationsTime();
|
||||
});
|
||||
private RelayCommand? _cmd_Stop;
|
||||
|
||||
public RelayCommand Cmd_PlayPause => _cmd_PlayPause ??= new(() => IsUpdating = !IsUpdating);
|
||||
private RelayCommand? _cmd_PlayPause;
|
||||
|
||||
public Geometry Geo_PlayPause => _isUpdating ? AppResource.Geo_Pause : AppResource.Geo_Play;
|
||||
|
||||
public RelayCommand Cmd_Restart => _cmd_Restart ??= new(() =>
|
||||
{
|
||||
lock (_models) foreach (var sp in _models) sp.ResetAnimationsTime();
|
||||
IsUpdating = true;
|
||||
});
|
||||
private RelayCommand? _cmd_Restart;
|
||||
|
||||
public RelayCommand Cmd_ForwardStep => _cmd_ForwardStep ??= new(() =>
|
||||
{
|
||||
lock (_forwardDeltaLock) _forwardDelta += _renderer.MaxFps > 0 ? 1f / _renderer.MaxFps : 0.001f;
|
||||
});
|
||||
private RelayCommand? _cmd_ForwardStep;
|
||||
|
||||
public RelayCommand Cmd_ForwardFast => _cmd_ForwardFast ??= new(() =>
|
||||
{
|
||||
lock (_forwardDeltaLock) _forwardDelta += _renderer.MaxFps > 0 ? 10f / _renderer.MaxFps : 0.01f;
|
||||
});
|
||||
private RelayCommand? _cmd_ForwardFast;
|
||||
|
||||
public void CanvasMouseWheelScrolled(object? s, SFML.Window.MouseWheelScrollEventArgs e)
|
||||
{
|
||||
var factor = e.Delta > 0 ? 1.1f : 0.9f;
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Control) == 0)
|
||||
{
|
||||
Zoom = Math.Clamp(Zoom * factor, 0.001f, 1000f); // 滚轮缩放限制一下缩放范围
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_models.Lock)
|
||||
{
|
||||
foreach (var sp in _models.Where(it => it.IsShown && it.IsSelected))
|
||||
{
|
||||
sp.Scale = Math.Clamp(sp.Scale * factor, 0.001f, 1000f); // 滚轮缩放限制一下缩放范围
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CanvasMouseButtonPressed(object? s, SFML.Window.MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.Button == SFML.Window.Mouse.Button.Right)
|
||||
{
|
||||
_draggingSrc = _renderer.MapPixelToCoords(new(e.X, e.Y));
|
||||
}
|
||||
else if (e.Button == SFML.Window.Mouse.Button.Left && !SFML.Window.Mouse.IsButtonPressed(SFML.Window.Mouse.Button.Right))
|
||||
{
|
||||
var _src = _renderer.MapPixelToCoords(new(e.X, e.Y));
|
||||
var src = new Point(_src.X, _src.Y);
|
||||
_draggingSrc = _src;
|
||||
|
||||
lock (_models.Lock)
|
||||
{
|
||||
// 仅渲染选中模式禁止在画面里选择对象
|
||||
if (_renderSelectedOnly)
|
||||
{
|
||||
// 只在被选中的对象里判断是否有效命中
|
||||
bool hit = _models.Any(m => m.IsSelected && m.GetCurrentBounds().Contains(src));
|
||||
|
||||
// 如果没点到被选中的模型, 则不允许拖动
|
||||
if (!hit) _draggingSrc = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Control) == 0)
|
||||
{
|
||||
// 没按 Ctrl 的情况下, 如果命中了已选中对象, 则就算普通命中
|
||||
bool hit = false;
|
||||
foreach (var sp in _models)
|
||||
{
|
||||
if (!sp.IsShown) continue;
|
||||
if (!sp.GetCurrentBounds().Contains(src)) continue;
|
||||
|
||||
hit = true;
|
||||
|
||||
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
|
||||
if (!sp.IsSelected)
|
||||
{
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果点了空白的地方, 就清空选中列表
|
||||
if (!hit) RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 按下 Ctrl 的情况就执行多选, 并且点空白处也不会清空选中, 如果点击了本来就是选中的则取消选中
|
||||
if (_models.FirstOrDefault(m => m.IsShown && m.GetCurrentBounds().Contains(src), null) is SpineObjectModel sp)
|
||||
{
|
||||
if (sp.IsSelected)
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Remove, sp));
|
||||
else
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CanvasMouseMove(object? s, SFML.Window.MouseMoveEventArgs e)
|
||||
{
|
||||
if (_draggingSrc is null) return;
|
||||
|
||||
var src = (SFML.System.Vector2f)_draggingSrc;
|
||||
var dst = _renderer.MapPixelToCoords(new(e.X, e.Y));
|
||||
var delta = dst - src;
|
||||
|
||||
if (SFML.Window.Mouse.IsButtonPressed(SFML.Window.Mouse.Button.Right))
|
||||
{
|
||||
_renderer.Center -= delta;
|
||||
OnPropertyChanged(nameof(CenterX));
|
||||
OnPropertyChanged(nameof(CenterY));
|
||||
}
|
||||
else if (SFML.Window.Mouse.IsButtonPressed(SFML.Window.Mouse.Button.Left))
|
||||
{
|
||||
lock (_models.Lock)
|
||||
{
|
||||
foreach (var sp in _models.Where(it => it.IsShown && it.IsSelected))
|
||||
{
|
||||
sp.X += delta.X;
|
||||
sp.Y += delta.Y;
|
||||
}
|
||||
}
|
||||
_draggingSrc = dst;
|
||||
}
|
||||
}
|
||||
|
||||
public void CanvasMouseButtonReleased(object? s, SFML.Window.MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.Button == SFML.Window.Mouse.Button.Right)
|
||||
{
|
||||
_draggingSrc = null;
|
||||
}
|
||||
else if (e.Button == SFML.Window.Mouse.Button.Left && !SFML.Window.Mouse.IsButtonPressed(SFML.Window.Mouse.Button.Right))
|
||||
{
|
||||
_draggingSrc = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void StartRender()
|
||||
{
|
||||
if (_renderTask is not null) return;
|
||||
_cancelToken = new();
|
||||
_renderTask = new Task(RenderTask, _cancelToken.Token, TaskCreationOptions.LongRunning);
|
||||
_renderTask.Start();
|
||||
IsUpdating = true;
|
||||
}
|
||||
|
||||
public void StopRender()
|
||||
{
|
||||
IsUpdating = false;
|
||||
if (_renderTask is null || _cancelToken is null) return;
|
||||
_cancelToken.Cancel();
|
||||
_renderTask.Wait();
|
||||
_cancelToken = null;
|
||||
_renderTask = null;
|
||||
}
|
||||
|
||||
private void RenderTask()
|
||||
{
|
||||
try
|
||||
{
|
||||
_renderer.SetActive(true);
|
||||
|
||||
float delta;
|
||||
while (!_cancelToken?.IsCancellationRequested ?? false)
|
||||
{
|
||||
delta = _clock.ElapsedTime.AsSeconds();
|
||||
_clock.Restart();
|
||||
|
||||
// 停止更新的时候只是时间不前进, 但是坐标变换还是要更新, 否则无法移动对象
|
||||
if (!_isUpdating) delta = 0;
|
||||
|
||||
// 加上要快进的量
|
||||
lock (_forwardDeltaLock)
|
||||
{
|
||||
delta += _forwardDelta;
|
||||
_forwardDelta = 0;
|
||||
}
|
||||
|
||||
_renderer.Clear(_backgroundColor);
|
||||
|
||||
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; // 提前中止
|
||||
|
||||
sp.Update(0); // 避免物理效果出现问题
|
||||
sp.Update(delta);
|
||||
|
||||
// 为选中对象绘制一个半透明背景
|
||||
if (sp.IsSelected)
|
||||
{
|
||||
var rc = sp.GetCurrentBounds().ToFloatRect();
|
||||
_selectedBackgroundVertices[0] = new(new(rc.Left, rc.Top), _selectedBackgroundColor);
|
||||
_selectedBackgroundVertices[1] = new(new(rc.Left + rc.Width, rc.Top), _selectedBackgroundColor);
|
||||
_selectedBackgroundVertices[2] = new(new(rc.Left + rc.Width, rc.Top + rc.Height), _selectedBackgroundColor);
|
||||
_selectedBackgroundVertices[3] = new(new(rc.Left, rc.Top + rc.Height), _selectedBackgroundColor);
|
||||
_renderer.Draw(_selectedBackgroundVertices);
|
||||
}
|
||||
|
||||
// 仅在预览画面临时启用调试模式
|
||||
sp.EnableDebug = true;
|
||||
_renderer.Draw(sp);
|
||||
sp.EnableDebug = false;
|
||||
}
|
||||
}
|
||||
|
||||
_renderer.Display();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Fatal("Render task stopped, {0}", ex.Message);
|
||||
MessagePopupService.Error(ex.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
_renderer.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
356
SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs
Normal file
356
SpineViewer/ViewModels/MainWindow/SpineObjectListViewModel.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NLog;
|
||||
using Spine;
|
||||
using SpineViewer.Extensions;
|
||||
using SpineViewer.Models;
|
||||
using SpineViewer.Resources;
|
||||
using SpineViewer.Services;
|
||||
using SpineViewer.ViewModels.Exporters;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Shell;
|
||||
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
public class SpineObjectListViewModel : ObservableObject
|
||||
{
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 主窗口视图模型引用
|
||||
/// </summary>
|
||||
private readonly MainWindowViewModel _vmMain;
|
||||
|
||||
/// <summary>
|
||||
/// 临时对象, 存储复制的模型参数
|
||||
/// </summary>
|
||||
private SpineObjectConfigModel? _copiedSpineObjectConfigModel = null;
|
||||
|
||||
public SpineObjectListViewModel(MainWindowViewModel mainViewModel)
|
||||
{
|
||||
_vmMain = mainViewModel;
|
||||
_spineObjectModels = _vmMain.SpineObjects; // 缓存对象
|
||||
|
||||
_frameExporterViewModel = new(_vmMain);
|
||||
_frameSequenceExporterViewModel = new(_vmMain);
|
||||
_ffmpegVideoExporterViewModel = new(_vmMain);
|
||||
_customFFmpegExporterViewModel = new(_vmMain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单帧导出 ViewModel
|
||||
/// </summary>
|
||||
public FrameExporterViewModel FrameExporterViewModel => _frameExporterViewModel;
|
||||
private readonly FrameExporterViewModel _frameExporterViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 帧序列 ViewModel
|
||||
/// </summary>
|
||||
public FrameSequenceExporterViewModel FrameSequenceExporterViewModel => _frameSequenceExporterViewModel;
|
||||
private readonly FrameSequenceExporterViewModel _frameSequenceExporterViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 动图/视频 ViewModel
|
||||
/// </summary>
|
||||
public FFmpegVideoExporterViewModel FFmpegVideoExporterViewModel => _ffmpegVideoExporterViewModel;
|
||||
private readonly FFmpegVideoExporterViewModel _ffmpegVideoExporterViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 动图/视频 ViewModel
|
||||
/// </summary>
|
||||
public CustomFFmpegExporterViewModel CustomFFmpegExporterViewModel => _customFFmpegExporterViewModel;
|
||||
private readonly CustomFFmpegExporterViewModel _customFFmpegExporterViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 已加载的 Spine 对象
|
||||
/// </summary>
|
||||
public ObservableCollectionWithLock<SpineObjectModel> SpineObjects => _spineObjectModels;
|
||||
private readonly ObservableCollectionWithLock<SpineObjectModel> _spineObjectModels;
|
||||
|
||||
/// <summary>
|
||||
/// 列表视图选中项发生改变时同步内部模型列表状态
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_ListViewSelectionChanged => _cmd_ListViewSelectionChanged ??= new(ListViewSelectionChanged_Execute);
|
||||
private RelayCommand<IList?>? _cmd_ListViewSelectionChanged;
|
||||
|
||||
private void ListViewSelectionChanged_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
lock (_spineObjectModels.Lock)
|
||||
{
|
||||
var selectedItems = args.Cast<SpineObjectModel>().ToArray();
|
||||
foreach (var it in _spineObjectModels.Except(selectedItems)) it.IsSelected = false;
|
||||
foreach (var it in selectedItems) it.IsSelected = true;
|
||||
_vmMain.SpineObjectTabViewModel.SelectedObjects = selectedItems;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗添加单模型命令
|
||||
/// </summary>
|
||||
public RelayCommand Cmd_AddSpineObject => _cmd_AddSpineObject ??= new(AddSpineObject_Execute);
|
||||
private RelayCommand? _cmd_AddSpineObject;
|
||||
|
||||
private void AddSpineObject_Execute()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除给定模型
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_RemoveSpineObject => _cmd_RemoveSpineObject ??= new(RemoveSpineObject_Execute, RemoveSpineObject_CanExecute);
|
||||
private RelayCommand<IList?>? _cmd_RemoveSpineObject;
|
||||
|
||||
private void RemoveSpineObject_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
|
||||
if (args.Count > 1)
|
||||
{
|
||||
if (!MessagePopupService.Quest(string.Format(AppResource.Str_RemoveItemsQuest, args.Count)))
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_spineObjectModels.Lock)
|
||||
{
|
||||
// XXX: 这里必须要浅拷贝一次, 不能直接对会被修改的绑定数据 args 进行 foreach 遍历
|
||||
foreach (var sp in args.Cast<SpineObjectModel>().ToArray())
|
||||
{
|
||||
_spineObjectModels.Remove(sp);
|
||||
sp.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemoveSpineObject_CanExecute(IList? args)
|
||||
{
|
||||
if (args is null) return false;
|
||||
if (args.Count <= 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模型上移一位
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_MoveUpSpineObject => _cmd_MoveUpSpineObject ??= new(MoveUpSpineObject_Execute, MoveUpSpineObject_CanExecute);
|
||||
private RelayCommand<IList?>? _cmd_MoveUpSpineObject;
|
||||
|
||||
private void MoveUpSpineObject_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
if (args.Count != 1) return;
|
||||
var sp = (SpineObjectModel)args[0];
|
||||
lock (_spineObjectModels.Lock)
|
||||
{
|
||||
var idx = _spineObjectModels.IndexOf(sp);
|
||||
if (idx <= 0) return;
|
||||
_spineObjectModels.Move(idx, idx - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private bool MoveUpSpineObject_CanExecute(IList? args)
|
||||
{
|
||||
if (args is null) return false;
|
||||
if (args.Count != 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模型下移一位
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_MoveDownSpineObject => _cmd_MoveDownSpineObject ??= new(MoveDownSpineObject_Execute, MoveDownSpineObject_CanExecute);
|
||||
private RelayCommand<IList?>? _cmd_MoveDownSpineObject;
|
||||
|
||||
private void MoveDownSpineObject_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
if (args.Count != 1) return;
|
||||
var sp = (SpineObjectModel)args[0];
|
||||
lock (_spineObjectModels.Lock)
|
||||
{
|
||||
var idx = _spineObjectModels.IndexOf(sp);
|
||||
if (idx < 0 || idx >= _spineObjectModels.Count - 1) return;
|
||||
_spineObjectModels.Move(idx, idx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private bool MoveDownSpineObject_CanExecute(IList? args)
|
||||
{
|
||||
if (args is null) return false;
|
||||
if (args.Count != 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从剪贴板文件列表添加模型
|
||||
/// </summary>
|
||||
public RelayCommand Cmd_AddSpineObjectFromClipboard => _cmd_AddSpineObjectFromClipboard ??= new(AddSpineObjectFromClipboard_Execute);
|
||||
private RelayCommand? _cmd_AddSpineObjectFromClipboard;
|
||||
|
||||
private void AddSpineObjectFromClipboard_Execute()
|
||||
{
|
||||
if (!Clipboard.ContainsFileDropList()) return;
|
||||
AddSpineObjectFromFileList(Clipboard.GetFileDropList().Cast<string>().ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复制模型参数
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_CopySpineObjectConfig => _cmd_CopySpineObjectConfig ??= new(CopySpineObjectConfig_Execute, CopySpineObjectConfig_CanExecute);
|
||||
private RelayCommand<IList?>? _cmd_CopySpineObjectConfig;
|
||||
|
||||
private void CopySpineObjectConfig_Execute(IList? args)
|
||||
{
|
||||
if (args is null) return;
|
||||
if (args.Count != 1) return;
|
||||
var sp = (SpineObjectModel)args[0];
|
||||
_copiedSpineObjectConfigModel = sp.Dump();
|
||||
_logger.Info("Copy config from model: {0}", sp.Name);
|
||||
}
|
||||
|
||||
private bool CopySpineObjectConfig_CanExecute(IList? args)
|
||||
{
|
||||
if (args is null) return false;
|
||||
if (args.Count != 1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用复制的模型参数
|
||||
/// </summary>
|
||||
public RelayCommand<IList?> Cmd_ApplySpineObjectConfig => _cmd_ApplySpineObjectConfig ??= new(ApplySpineObjectConfig_Execute, ApplySpineObjectConfig_CanExecute);
|
||||
private RelayCommand<IList?>? _cmd_ApplySpineObjectConfig;
|
||||
|
||||
private void ApplySpineObjectConfig_Execute(IList? args)
|
||||
{
|
||||
if (_copiedSpineObjectConfigModel is null) return;
|
||||
if (args is null) return;
|
||||
if (args.Count <= 0) return;
|
||||
foreach (SpineObjectModel sp in args)
|
||||
{
|
||||
sp.Load(_copiedSpineObjectConfigModel);
|
||||
_logger.Info("Apply config to model: {0}", sp.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ApplySpineObjectConfig_CanExecute(IList? args)
|
||||
{
|
||||
if (_copiedSpineObjectConfigModel is null) return false;
|
||||
if (args is null) return false;
|
||||
if (args.Count <= 0) return false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validPaths.Count > 1)
|
||||
{
|
||||
if (validPaths.Count > 100)
|
||||
{
|
||||
if (!MessagePopupService.Quest(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)
|
||||
{
|
||||
var skelPath = validPaths[0];
|
||||
try
|
||||
{
|
||||
var sp = new SpineObjectModel(skelPath);
|
||||
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
|
||||
}
|
||||
_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}";
|
||||
|
||||
try
|
||||
{
|
||||
var sp = new SpineObjectModel(skelPath);
|
||||
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Trace(ex.ToString());
|
||||
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
830
SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs
Normal file
830
SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs
Normal file
@@ -0,0 +1,830 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Spine;
|
||||
using Spine.SpineWrappers;
|
||||
using SpineViewer.Models;
|
||||
using System.Collections;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
|
||||
namespace SpineViewer.ViewModels.MainWindow
|
||||
{
|
||||
public class SpineObjectTabViewModel : ObservableObject
|
||||
{
|
||||
private SpineObjectModel[] _selectedObjects = [];
|
||||
private readonly ObservableCollection<SkinViewModel> _skins = [];
|
||||
private readonly ObservableCollection<SlotAttachmentViewModel> _slots = [];
|
||||
private readonly ObservableCollection<AnimationTrackViewModel> _animationTracks = [];
|
||||
|
||||
public ImmutableArray<ISkeleton.Physics> PhysicsOptions { get; } = Enum.GetValues<ISkeleton.Physics>().ToImmutableArray();
|
||||
|
||||
public SpineObjectModel[] SelectedObjects
|
||||
{
|
||||
get => _selectedObjects;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_selectedObjects, value)) return;
|
||||
|
||||
// 清空之前的所有内容
|
||||
foreach (var obj in _selectedObjects)
|
||||
{
|
||||
obj.PropertyChanged -= SingleModel_PropertyChanged;
|
||||
obj.AnimationChanged -= SingleModel_AnimationChanged;
|
||||
}
|
||||
_skins.Clear();
|
||||
_slots.Clear();
|
||||
_animationTracks.Clear();
|
||||
|
||||
// 生成新的内容
|
||||
_selectedObjects = value ?? [];
|
||||
if (_selectedObjects.Length > 0)
|
||||
{
|
||||
foreach (var obj in _selectedObjects)
|
||||
{
|
||||
obj.PropertyChanged += SingleModel_PropertyChanged;
|
||||
obj.AnimationChanged += SingleModel_AnimationChanged;
|
||||
}
|
||||
|
||||
IEnumerable<string> commonSkinNames = _selectedObjects[0].Skins;
|
||||
foreach (var obj in _selectedObjects.Skip(1)) commonSkinNames = commonSkinNames.Intersect(obj.Skins);
|
||||
foreach (var name in commonSkinNames) _skins.Add(new(name, _selectedObjects));
|
||||
|
||||
IEnumerable<string> commonSlotNames = _selectedObjects[0].SlotAttachments.Keys;
|
||||
foreach (var obj in _selectedObjects.Skip(1)) commonSlotNames = commonSlotNames.Intersect(obj.SlotAttachments.Keys);
|
||||
foreach (var name in commonSlotNames) _slots.Add(new(name, _selectedObjects));
|
||||
|
||||
IEnumerable<int> commonTrackIndices = _selectedObjects[0].GetTrackIndices();
|
||||
foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
|
||||
foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects));
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
Cmd_AppendTrack.NotifyCanExecuteChanged();
|
||||
|
||||
OnPropertyChanged(nameof(Version));
|
||||
OnPropertyChanged(nameof(AssetsDir));
|
||||
OnPropertyChanged(nameof(SkelPath));
|
||||
OnPropertyChanged(nameof(AtlasPath));
|
||||
OnPropertyChanged(nameof(Name));
|
||||
OnPropertyChanged(nameof(FileVersion));
|
||||
|
||||
OnPropertyChanged(nameof(IsShown));
|
||||
OnPropertyChanged(nameof(UsePma));
|
||||
OnPropertyChanged(nameof(Physics));
|
||||
|
||||
OnPropertyChanged(nameof(Scale));
|
||||
OnPropertyChanged(nameof(FlipX));
|
||||
OnPropertyChanged(nameof(FlipY));
|
||||
OnPropertyChanged(nameof(X));
|
||||
OnPropertyChanged(nameof(Y));
|
||||
|
||||
OnPropertyChanged(nameof(DebugTexture));
|
||||
OnPropertyChanged(nameof(DebugBounds));
|
||||
OnPropertyChanged(nameof(DebugBones));
|
||||
OnPropertyChanged(nameof(DebugRegions));
|
||||
OnPropertyChanged(nameof(DebugMeshHulls));
|
||||
OnPropertyChanged(nameof(DebugMeshes));
|
||||
OnPropertyChanged(nameof(DebugBoundingBoxes));
|
||||
OnPropertyChanged(nameof(DebugPaths));
|
||||
OnPropertyChanged(nameof(DebugPoints));
|
||||
OnPropertyChanged(nameof(DebugClippings));
|
||||
}
|
||||
}
|
||||
|
||||
public SpineVersion? Version
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].Version;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.Version != val)) return null;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
public string? AssetsDir
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].AssetsDir;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.AssetsDir != val)) return null;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
public string? SkelPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].SkelPath;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.SkelPath != val)) return null;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
public string? AtlasPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].AtlasPath;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.AtlasPath != val)) return null;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
public string? Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].Name;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.Name != val)) return null;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
public string? FileVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].FileVersion;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.FileVersion != val)) return null;
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
public bool? IsShown
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].IsShown;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.IsShown != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.IsShown = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? UsePma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].UsePma;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.UsePma != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.UsePma = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ISkeleton.Physics? Physics
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].Physics;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.Physics != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.Physics = (ISkeleton.Physics)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float? Scale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].Scale;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.Scale != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.Scale = (float)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? FlipX
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].FlipX;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.FlipX != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.FlipX = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? FlipY
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].FlipY;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.FlipY != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.FlipY = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float? X
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].X;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.X != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.X = (float)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float? Y
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].Y;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.Y != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.Y = (float)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<SkinViewModel> Skins => _skins;
|
||||
|
||||
public RelayCommand<IList?> Cmd_EnableSkins { get; } = new(
|
||||
args => { if (args is null) return; foreach (var s in args.OfType<SkinViewModel>()) s.Status = true; },
|
||||
args => { return args is not null && args.OfType<SkinViewModel>().Any(); }
|
||||
);
|
||||
|
||||
public RelayCommand<IList?> Cmd_DisableSkins { get; } = new(
|
||||
args => { if (args is null) return; foreach (var s in args.OfType<SkinViewModel>()) s.Status = false; },
|
||||
args => { return args is not null && args.OfType<SkinViewModel>().Any(); }
|
||||
);
|
||||
|
||||
public ObservableCollection<SlotAttachmentViewModel> Slots => _slots;
|
||||
|
||||
public RelayCommand<IList?> Cmd_ClearSlotsAttachment { get; } = new(
|
||||
args => { if (args is null) return; foreach (var s in args.OfType<SlotAttachmentViewModel>()) s.AttachmentName = null; },
|
||||
args => { return args is not null && args.OfType<SlotAttachmentViewModel>().Any(); }
|
||||
);
|
||||
|
||||
public ObservableCollection<AnimationTrackViewModel> AnimationTracks => _animationTracks;
|
||||
|
||||
public RelayCommand Cmd_AppendTrack => _cmd_AppendTrack ??= new(
|
||||
() =>
|
||||
{
|
||||
if (_selectedObjects.Length != 1) return;
|
||||
var sp = _selectedObjects[0];
|
||||
if (sp.Animations.Length <= 0) return;
|
||||
sp.SetAnimation(sp.GetTrackIndices().LastOrDefault(-1) + 1, sp.Animations[0]);
|
||||
},
|
||||
() => { return _selectedObjects.Length == 1; }
|
||||
);
|
||||
private RelayCommand? _cmd_AppendTrack;
|
||||
|
||||
public RelayCommand<IList?> Cmd_InsertTrack => _cmd_InsertTrack ??= new(
|
||||
args =>
|
||||
{
|
||||
if (_selectedObjects.Length != 1) return;
|
||||
var sp = _selectedObjects[0];
|
||||
|
||||
if (sp.Animations.Length <= 0) return;
|
||||
if (args is null) return;
|
||||
if (args.Count != 1) return;
|
||||
if (args[0] is not AnimationTrackViewModel vm) return;
|
||||
var idx = vm.TrackIndex;
|
||||
|
||||
if (idx <= 0) return;
|
||||
if (sp.GetTrackIndices().Contains(idx - 1)) return;
|
||||
sp.SetAnimation(idx - 1, sp.Animations[0]);
|
||||
},
|
||||
args =>
|
||||
{
|
||||
if (_selectedObjects.Length != 1) return false;
|
||||
var sp = _selectedObjects[0];
|
||||
|
||||
if (sp.Animations.Length <= 0) return false;
|
||||
if (args is null) return false;
|
||||
if (args.Count != 1) return false;
|
||||
if (args[0] is not AnimationTrackViewModel vm) return false;
|
||||
var idx = vm.TrackIndex;
|
||||
|
||||
if (idx <= 0) return false;
|
||||
if (sp.GetTrackIndices().Contains(idx - 1)) return false;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
private RelayCommand<IList?>? _cmd_InsertTrack;
|
||||
|
||||
public bool? DebugTexture
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugTexture;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugTexture != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugTexture = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugBounds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugBounds;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugBounds != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugBounds = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugBones
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugBones;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugBones != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugBones = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugRegions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugRegions;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugRegions != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugRegions = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugMeshHulls
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugMeshHulls;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugMeshHulls != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugMeshHulls = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugMeshes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugMeshes;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugMeshes != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugMeshes = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugBoundingBoxes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugBoundingBoxes;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugBoundingBoxes != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugBoundingBoxes = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugPaths
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugPaths;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugPaths != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugPaths = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugPoints
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugPoints;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugPoints != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugPoints = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool? DebugClippings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return null;
|
||||
var val = _selectedObjects[0].DebugClippings;
|
||||
if (_selectedObjects.Skip(1).Any(it => it.DebugClippings != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_selectedObjects.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _selectedObjects) sp.DebugClippings = (bool)value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听单个模型属性发生变化, 则更新聚合属性值
|
||||
/// </summary>
|
||||
private void SingleModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(SpineObjectModel.IsShown)) OnPropertyChanged(nameof(IsShown));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.UsePma)) OnPropertyChanged(nameof(UsePma));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.Physics)) OnPropertyChanged(nameof(Physics));
|
||||
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.Scale)) OnPropertyChanged(nameof(Scale));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.FlipX)) OnPropertyChanged(nameof(FlipX));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.FlipY)) OnPropertyChanged(nameof(FlipY));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.X)) OnPropertyChanged(nameof(X));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.Y)) OnPropertyChanged(nameof(Y));
|
||||
|
||||
// Skins 变化在 SkinViewModel 中监听
|
||||
// Slots 变化在 SlotAttachmentViewModel 中监听
|
||||
// AnimationTracks 变化在 AnimationTrackViewModel 中监听
|
||||
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugTexture)) OnPropertyChanged(nameof(DebugTexture));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugBounds)) OnPropertyChanged(nameof(DebugBounds));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugBones)) OnPropertyChanged(nameof(DebugBones));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugRegions)) OnPropertyChanged(nameof(DebugRegions));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugMeshHulls)) OnPropertyChanged(nameof(DebugMeshHulls));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugMeshes)) OnPropertyChanged(nameof(DebugMeshes));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugBoundingBoxes)) OnPropertyChanged(nameof(DebugBoundingBoxes));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugPaths)) OnPropertyChanged(nameof(DebugPaths));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugPoints)) OnPropertyChanged(nameof(DebugPoints));
|
||||
else if (e.PropertyName == nameof(SpineObjectModel.DebugClippings)) OnPropertyChanged(nameof(DebugClippings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听单个模型动画轨道发生变化, 则重建聚合后的动画列表
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
private void SingleModel_AnimationChanged(object? sender, AnimationChangedEventArgs e)
|
||||
{
|
||||
// XXX: 这里应该有更好的实现, 当 e.AnimationName == null 的时候代表删除轨道需要重新构建列表
|
||||
// 但是目前无法识别是否增加了轨道, 因此总是重建列表
|
||||
|
||||
// 由于某些原因, 直接使用 Clear 会和 UI 逻辑冲突产生报错, 因此需要放到 Dispatcher 里延迟执行
|
||||
Application.Current.Dispatcher.BeginInvoke(
|
||||
() =>
|
||||
{
|
||||
_animationTracks.Clear();
|
||||
IEnumerable<int> commonTrackIndices = _selectedObjects[0].GetTrackIndices();
|
||||
foreach (var obj in _selectedObjects.Skip(1)) commonTrackIndices = commonTrackIndices.Intersect(obj.GetTrackIndices());
|
||||
foreach (var idx in commonTrackIndices) _animationTracks.Add(new(idx, _selectedObjects));
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public class SkinViewModel : ObservableObject
|
||||
{
|
||||
private readonly SpineObjectModel[] _spines;
|
||||
private readonly string _name;
|
||||
|
||||
public SkinViewModel(string name, SpineObjectModel[] spines)
|
||||
{
|
||||
_spines = spines;
|
||||
_name = name;
|
||||
|
||||
// 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件
|
||||
foreach (var sp in _spines)
|
||||
{
|
||||
WeakEventManager<SpineObjectModel, SkinStatusChangedEventArgs>.AddHandler(
|
||||
sp,
|
||||
nameof(sp.SkinStatusChanged),
|
||||
SingleModel_SkinStatusChanged
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public string Name => _name;
|
||||
|
||||
public bool? Status
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_spines.Length <= 0) return null;
|
||||
var val = _spines[0].GetSkinStatus(_name);
|
||||
if (_spines.Skip(1).Any(it => it.GetSkinStatus(_name) != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_spines.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
bool changed = false;
|
||||
foreach (var sp in _spines) if (sp.SetSkinStatus(_name, (bool)value)) changed = true;
|
||||
if (changed) OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SingleModel_SkinStatusChanged(object? sender, SkinStatusChangedEventArgs e)
|
||||
{
|
||||
if (e.Name == _name) OnPropertyChanged(nameof(Status));
|
||||
}
|
||||
}
|
||||
|
||||
public class SlotAttachmentViewModel : ObservableObject
|
||||
{
|
||||
private readonly SpineObjectModel[] _spines;
|
||||
private readonly string[] _attachmentNames = [];
|
||||
private readonly string _slotName;
|
||||
|
||||
public SlotAttachmentViewModel(string slotName, SpineObjectModel[] spines)
|
||||
{
|
||||
_spines = spines;
|
||||
_slotName = slotName;
|
||||
|
||||
if (_spines.Length > 0)
|
||||
{
|
||||
IEnumerable<string> attachmentNames = _spines[0].SlotAttachments[_slotName];
|
||||
foreach (var sp in _spines.Skip(1))
|
||||
attachmentNames = attachmentNames.Union(sp.SlotAttachments[_slotName]);
|
||||
_attachmentNames = attachmentNames.ToArray();
|
||||
}
|
||||
|
||||
// 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件
|
||||
foreach (var sp in _spines)
|
||||
{
|
||||
WeakEventManager<SpineObjectModel, SlotAttachmentChangedEventArgs>.AddHandler(
|
||||
sp,
|
||||
nameof(sp.SlotAttachmentChanged),
|
||||
SingleModel_SlotAttachmentChanged
|
||||
);
|
||||
WeakEventManager<SpineObjectModel, SkinStatusChangedEventArgs>.AddHandler(
|
||||
sp,
|
||||
nameof(sp.SkinStatusChanged),
|
||||
SingleModel_SkinStatusChanged
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand Cmd_ClearAttachment => _cmd_ClearAttachment ??= new(() => AttachmentName = null);
|
||||
private RelayCommand? _cmd_ClearAttachment;
|
||||
|
||||
public ReadOnlyCollection<string> AttachmentNames => _attachmentNames.AsReadOnly();
|
||||
|
||||
public string SlotName => _slotName;
|
||||
|
||||
public string? AttachmentName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_spines.Length <= 0) return null;
|
||||
var val = _spines[0].GetAttachment(_slotName);
|
||||
if (_spines.Skip(1).Any(it => it.GetAttachment(_slotName) != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_spines.Length <= 0) return;
|
||||
bool changed = false;
|
||||
foreach (var sp in _spines) if (sp.SetAttachment(_slotName, value)) changed = true;
|
||||
if (changed) OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SingleModel_SlotAttachmentChanged(object? sender, SlotAttachmentChangedEventArgs e)
|
||||
{
|
||||
if (e.SlotName == _slotName) OnPropertyChanged(nameof(AttachmentName));
|
||||
}
|
||||
|
||||
private void SingleModel_SkinStatusChanged(object? sender, SkinStatusChangedEventArgs e)
|
||||
{
|
||||
// 如果皮肤发生改变, 则直接触发附件属性变化事件
|
||||
OnPropertyChanged(nameof(AttachmentName));
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimationTrackViewModel : ObservableObject
|
||||
{
|
||||
private readonly SpineObjectModel[] _spines;
|
||||
private readonly string[] _animationNames = [];
|
||||
private readonly int _trackIndex;
|
||||
|
||||
public AnimationTrackViewModel(int trackIndex, SpineObjectModel[] spines)
|
||||
{
|
||||
_spines = spines;
|
||||
_trackIndex = trackIndex;
|
||||
|
||||
if (_spines.Length > 0)
|
||||
{
|
||||
IEnumerable<string> animationNames = _spines[0].Animations;
|
||||
foreach (var sp in _spines.Skip(1))
|
||||
animationNames = animationNames.Union(sp.Animations);
|
||||
_animationNames = animationNames.ToArray();
|
||||
}
|
||||
|
||||
// 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件
|
||||
foreach (var sp in _spines)
|
||||
{
|
||||
WeakEventManager<SpineObjectModel, AnimationChangedEventArgs>.AddHandler(
|
||||
sp,
|
||||
nameof(sp.AnimationChanged),
|
||||
SingleModel_AnimationChanged
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand Cmd_ClearTrack => _cmd_ClearTrack ??= new(() => { foreach (var sp in _spines) sp.ClearTrack(_trackIndex); });
|
||||
private RelayCommand? _cmd_ClearTrack;
|
||||
|
||||
public ReadOnlyCollection<string> AnimationNames => _animationNames.AsReadOnly();
|
||||
|
||||
public int TrackIndex => _trackIndex;
|
||||
|
||||
public string? AnimationName
|
||||
{
|
||||
get
|
||||
{
|
||||
/// XXX: 空轨道和多选不相同都会返回 null
|
||||
if (_spines.Length <= 0) return null;
|
||||
var val = _spines[0].GetAnimation(_trackIndex);
|
||||
if (_spines.Skip(1).Any(it => it.GetAnimation(_trackIndex) != val)) return null;
|
||||
return val;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (_spines.Length <= 0) return;
|
||||
if (value is null) return;
|
||||
foreach (var sp in _spines) sp.SetAnimation(_trackIndex, value);
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(AnimationDuration));
|
||||
}
|
||||
}
|
||||
|
||||
public float? AnimationDuration
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_spines.Length <= 0) return null;
|
||||
var ani = _spines[0].GetAnimation(_trackIndex);
|
||||
if (ani is null) return null;
|
||||
var val = _spines[0].GetAnimationDuration(ani);
|
||||
foreach (var sp in _spines.Skip(1))
|
||||
{
|
||||
var a = sp.GetAnimation(_trackIndex);
|
||||
if (a is null) return null;
|
||||
if (sp.GetAnimationDuration(a) != val) return null;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
private void SingleModel_AnimationChanged(object? sender, AnimationChangedEventArgs e)
|
||||
{
|
||||
if (e.TrackIndex == _trackIndex) OnPropertyChanged(nameof(AnimationName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user