增加工作区功能

This commit is contained in:
ww-rm
2025-06-18 00:53:02 +08:00
parent 7bd3e3669b
commit 5039bc666f
22 changed files with 495 additions and 252 deletions

View File

@@ -5,6 +5,7 @@ using SFMLRenderer;
using Spine;
using Spine.Exporters;
using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.ViewModels.MainWindow;
using System;
@@ -134,9 +135,21 @@ namespace SpineViewer.ViewModels.Exporters
return null;
}
public RelayCommand<IList?> Cmd_Export => _cmd_Export ??= new(Export_Execute, args => args is not null && args.Count > 0);
public RelayCommand<IList?> Cmd_Export => _cmd_Export ??= new(Export_Execute, Export_CanExecute);
private RelayCommand<IList?>? _cmd_Export;
protected abstract void Export_Execute(IList? args);
private void Export_Execute(IList? args)
{
if (!Export_CanExecute(args)) return;
Export(args.Cast<SpineObjectModel>().ToArray());
// XXX: 导出途中应该停掉渲染好一些, 让性能专注在导出上
}
private bool Export_CanExecute(IList? args)
{
return args is not null && args.Count > 0;
}
protected abstract void Export(SpineObjectModel[] models);
}
}

View File

@@ -47,11 +47,10 @@ namespace SpineViewer.ViewModels.Exporters
return null;
}
protected override void Export_Execute(IList? args)
protected override void Export(SpineObjectModel[] models)
{
if (args is null || args.Count <= 0) return;
if (!DialogService.ShowCustomFFmpegExporterDialog(this)) return;
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_CustomFFmpegExporterTitle);
foreach (var sp in spines) sp.Dispose();
}

View File

@@ -34,11 +34,10 @@ namespace SpineViewer.ViewModels.Exporters
private string FormatSuffix => $".{_format.ToString().ToLower()}";
protected override void Export_Execute(IList? args)
protected override void Export(SpineObjectModel[] models)
{
if (args is null || args.Count <= 0) return;
if (!DialogService.ShowFFmpegVideoExporterDialog(this)) return;
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FFmpegVideoExporterTitle);
foreach (var sp in spines) sp.Dispose();
}

View File

@@ -38,11 +38,10 @@ namespace SpineViewer.ViewModels.Exporters
}
}
protected override void Export_Execute(IList? args)
protected override void Export(SpineObjectModel[] models)
{
if (args is null || args.Count <= 0) return;
if (!DialogService.ShowFrameExporterDialog(this)) return;
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject(true)).ToArray();
SpineObject[] spines = models.Select(m => m.GetSpineObject(true)).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameExporterTitle);
foreach (var sp in spines) sp.Dispose();
}

View File

@@ -17,11 +17,10 @@ namespace SpineViewer.ViewModels.Exporters
{
public class FrameSequenceExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{
protected override void Export_Execute(IList? args)
protected override void Export(SpineObjectModel[] models)
{
if (args is null || args.Count <= 0) return;
if (!DialogService.ShowFrameSequenceExporterDialog(this)) return;
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
SpineObject[] spines = models.Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameSequenceExporterTitle);
foreach (var sp in spines) sp.Dispose();
}

View File

@@ -1,22 +1,10 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HandyControl.Controls;
using NLog;
using SFMLRenderer;
using Spine;
using Spine.Exporters;
using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Services;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using SpineViewer.Utils;
using System.Windows.Shell;
namespace SpineViewer.ViewModels.MainWindow
@@ -84,6 +72,34 @@ namespace SpineViewer.ViewModels.MainWindow
public SFMLRendererViewModel SFMLRendererViewModel => _sfmlRendererViewModel;
private readonly SFMLRendererViewModel _sfmlRendererViewModel;
/// <summary>
/// 打开工作区
/// </summary>
public RelayCommand Cmd_OpenWorkspace => _cmd_OpenWorkspace ??= new(OpenWorkspace_Execute);
private RelayCommand? _cmd_OpenWorkspace;
private void OpenWorkspace_Execute()
{
if (!DialogService.ShowOpenJsonDialog(out var fileName)) return;
if (JsonHelper.Deserialize<WorkspaceModel>(fileName, out var obj))
{
Workspace = obj;
}
}
/// <summary>
/// 保存工作区
/// </summary>
public RelayCommand Cmd_SaveWorkspace => _cmd_SaveWorkspace ??= new(SaveWorkspace_Execute);
private RelayCommand? _cmd_SaveWorkspace;
private void SaveWorkspace_Execute()
{
string fileName = "workspace.jcfg";
if (!DialogService.ShowSaveJsonDialog(ref fileName)) return;
JsonHelper.Serialize(Workspace, fileName);
}
/// <summary>
/// 显示诊断信息对话框
/// </summary>
@@ -96,6 +112,23 @@ namespace SpineViewer.ViewModels.MainWindow
public RelayCommand Cmd_ShowAboutDialog => _cmd_ShowAboutDialog ??= new(() => { DialogService.ShowAboutDialog(); });
private RelayCommand? _cmd_ShowAboutDialog;
public WorkspaceModel Workspace
{
get
{
return new()
{
RendererConfig = _sfmlRendererViewModel.WorkspaceConfig,
LoadedSpineObjects = _spineObjectListViewModel.LoadedSpineObjects
};
}
set
{
_sfmlRendererViewModel.WorkspaceConfig = value.RendererConfig;
_spineObjectListViewModel.LoadedSpineObjects = value.LoadedSpineObjects;
}
}
/// <summary>
/// 调试命令
/// </summary>

View File

@@ -4,6 +4,7 @@ using NLog;
using Spine.SpineWrappers;
using SpineViewer.Models;
using SpineViewer.Services;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -49,15 +50,7 @@ namespace SpineViewer.ViewModels.MainWindow
private static void SavePreference(PreferenceModel m)
{
try
{
m.Serialize(PreferenceFilePath);
}
catch (Exception ex)
{
_logger.Error("Failed to save preference to {0}, {1}", PreferenceFilePath, ex.Message);
_logger.Trace(ex.ToString());
}
JsonHelper.Serialize(m, PreferenceFilePath);
}
/// <summary>
@@ -70,18 +63,8 @@ namespace SpineViewer.ViewModels.MainWindow
/// </summary>
public void LoadPreference()
{
if (!File.Exists(PreferenceFilePath)) return;
try
{
var m = PreferenceModel.Deserialize(PreferenceFilePath);
Preference = m;
}
catch (Exception ex)
{
_logger.Error("Failed to load preference from {0}, {1}", PreferenceFilePath, ex.Message);
_logger.Trace(ex.ToString());
}
if (JsonHelper.Deserialize<PreferenceModel>(PreferenceFilePath, out var obj))
Preference = obj;
}
/// <summary>

View File

@@ -7,6 +7,7 @@ using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
@@ -423,5 +424,41 @@ namespace SpineViewer.ViewModels.MainWindow
_renderer.SetActive(false);
}
}
public RendererWorkspaceConfigModel WorkspaceConfig
{
// TODO: 背景图片
get
{
return new()
{
ResolutionX = ResolutionX,
ResolutionY = ResolutionY,
CenterX = CenterX,
CenterY = CenterY,
Zoom = Zoom,
Rotation = Rotation,
FlipX = FlipX,
FlipY = FlipY,
MaxFps = MaxFps,
ShowAxis = ShowAxis,
BackgroundColor = BackgroundColor,
};
}
set
{
ResolutionX = value.ResolutionX;
ResolutionY = value.ResolutionY;
CenterX = value.CenterX;
CenterY = value.CenterY;
Zoom = value.Zoom;
Rotation = value.Rotation;
FlipX = value.FlipX;
FlipY = value.FlipY;
MaxFps = value.MaxFps;
ShowAxis = value.ShowAxis;
BackgroundColor = value.BackgroundColor;
}
}
}
}

View File

@@ -6,6 +6,7 @@ using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
using SpineViewer.Utils;
using SpineViewer.ViewModels.Exporters;
using System;
using System.Collections;
@@ -100,7 +101,7 @@ namespace SpineViewer.ViewModels.MainWindow
private void AddSpineObject_Execute()
{
throw new NotImplementedException();
MessagePopupService.Info("Not Implemented, try next version :)");
}
/// <summary>
@@ -170,7 +171,7 @@ namespace SpineViewer.ViewModels.MainWindow
try
{
var spNew = new SpineObjectModel(sp.SkelPath, sp.AtlasPath);
spNew.Load(sp.Dump());
spNew.ObjectConfig = sp.ObjectConfig;
_spineObjectModels[idx] = spNew;
sp.Dispose();
}
@@ -224,7 +225,7 @@ namespace SpineViewer.ViewModels.MainWindow
try
{
var spNew = new SpineObjectModel(sp.SkelPath, sp.AtlasPath);
spNew.Load(sp.Dump());
spNew.ObjectConfig = sp.ObjectConfig;
_spineObjectModels[idx] = spNew;
sp.Dispose();
success++;
@@ -312,7 +313,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (!CopySpineObjectConfig_CanExecute(args)) return;
var sp = (SpineObjectModel)args[0];
_copiedSpineObjectConfigModel = sp.Dump();
_copiedSpineObjectConfigModel = sp.ObjectConfig;
_logger.Info("Copy config from model: {0}", sp.Name);
}
@@ -334,7 +335,7 @@ namespace SpineViewer.ViewModels.MainWindow
if (!ApplySpineObjectConfig_CanExecute(args)) return;
foreach (SpineObjectModel sp in args)
{
sp.Load(_copiedSpineObjectConfigModel);
sp.ObjectConfig = _copiedSpineObjectConfigModel;
_logger.Info("Apply config to model: {0}", sp.Name);
}
}
@@ -353,22 +354,15 @@ namespace SpineViewer.ViewModels.MainWindow
private void ApplySpineObjectConfigFromFile_Execute(IList? args)
{
if (!ApplySpineObjectConfigFromFile_CanExecute(args)) return;
if (!DialogService.ShowOpenFileDialog(out var fileName, filter: "Json Config|*.jcfg|All|*.*")) return;
try
if (!DialogService.ShowOpenJsonDialog(out var fileName)) return;
if (JsonHelper.Deserialize<SpineObjectConfigModel>(fileName, out var config))
{
var config = SpineObjectConfigModel.Deserialize(fileName);
foreach (SpineObjectModel sp in args)
{
sp.Load(config);
sp.ObjectConfig = config;
_logger.Info("Apply config to model: {0}", sp.Name);
}
}
catch (Exception ex)
{
_logger.Error("Failed to apply config file {0}, {1}", fileName, ex.Message);
_logger.Trace(ex.ToString());
return;
}
}
private bool ApplySpineObjectConfigFromFile_CanExecute(IList? args)
@@ -385,27 +379,11 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (!SaveSpineObjectConfigToFile_CanExecute(args)) return;
var sp = (SpineObjectModel)args[0];
var config = sp.Dump();
var config = sp.ObjectConfig;
string fileName = $"{Path.ChangeExtension(Path.GetFileName(sp.SkelPath), ".jcfg")}";
if (!DialogService.ShowSaveFileDialog(
ref fileName,
initialDirectory: sp.AssetsDir,
defaultExt: ".jcfg",
filter:"Json Config|*.jcfg|All|*.*")
)
return;
try
{
sp.Dump().Serialize(fileName);
_logger.Info("{0} config save to {1}", sp.Name, fileName);
}
catch (Exception ex)
{
_logger.Error("Failed to save config file {0}, {1}", fileName, ex.Message);
_logger.Trace(ex.ToString());
return;
}
if (!DialogService.ShowSaveJsonDialog(ref fileName, sp.AssetsDir)) return;
JsonHelper.Serialize(sp.ObjectConfig, fileName);
}
private bool SaveSpineObjectConfigToFile_CanExecute(IList? args)
@@ -505,7 +483,7 @@ namespace SpineViewer.ViewModels.MainWindow
/// 安全地在末尾添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
public bool AddSpineObject(string skelPath, string? atlasPath = null)
private bool AddSpineObject(string skelPath, string? atlasPath = null)
{
try
{
@@ -520,5 +498,112 @@ namespace SpineViewer.ViewModels.MainWindow
}
return false;
}
private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
{
try
{
var sp = new SpineObjectModel(cfg);
lock (_spineObjectModels.Lock) _spineObjectModels.Add(sp);
return true;
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to load: {0}, {1}", cfg.SkelPath, ex.Message);
}
return false;
}
public List<SpineObjectWorkspaceConfigModel> LoadedSpineObjects
{
get
{
List<SpineObjectWorkspaceConfigModel> loadedSpineObjects = [];
lock (_spineObjectModels.Lock)
{
foreach (var sp in _spineObjectModels)
{
loadedSpineObjects.Add(sp.WorkspaceConfig);
}
}
return loadedSpineObjects;
}
set
{
AddSpineObjectFromWorkspaceList(value);
}
}
private void AddSpineObjectFromWorkspaceList(List<SpineObjectWorkspaceConfigModel> models)
{
lock (_spineObjectModels.Lock)
{
var spines = _spineObjectModels.ToArray();
_spineObjectModels.Clear();
foreach (var sp in spines)
{
sp.Dispose();
}
}
if (models.Count > 1)
{
ProgressService.RunAsync((pr, ct) => AddSpineObjectFromWorkspaceListTask(
models, pr, ct),
AppResource.Str_AddSpineObjectsTitle
);
}
else if (models.Count > 0)
{
AddSpineObject(models[0]);
_logger.LogCurrentProcessMemoryUsage();
}
}
private void AddSpineObjectFromWorkspaceListTask(List<SpineObjectWorkspaceConfigModel> models, IProgressReporter reporter, CancellationToken ct)
{
int totalCount = models.Count;
int success = 0;
int error = 0;
_vmMain.ProgressState = TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
reporter.Total = totalCount;
reporter.Done = 0;
reporter.ProgressText = $"[0/{totalCount}]";
for (int i = 0; i < totalCount; i++)
{
if (ct.IsCancellationRequested) break;
var cfg = models[i];
reporter.ProgressText = $"[{i}/{totalCount}] {cfg}";
if (AddSpineObject(cfg))
success++;
else
error++;
reporter.Done = i + 1;
reporter.ProgressText = $"[{i + 1}/{totalCount}] {cfg}";
_vmMain.ProgressValue = (i + 1f) / totalCount;
}
_vmMain.ProgressState = TaskbarItemProgressState.None;
if (error > 0)
_logger.Warn("Batch load {0} successfully, {1} failed", success, error);
else
_logger.Info("{0} skel loaded successfully", success);
_logger.LogCurrentProcessMemoryUsage();
// 从工作区加载需要同步一次时间轴
lock (_spineObjectModels.Lock)
{
foreach (var sp in _spineObjectModels)
sp.ResetAnimationsTime();
}
}
}
}

View File

@@ -786,7 +786,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
get
{
/// XXX: 空轨道和多选不相同都会返回 null
// XXX: 空轨道和多选不相同都会返回 null
if (_spines.Length <= 0) return null;
var val = _spines[0].GetAnimation(_trackIndex);
if (_spines.Skip(1).Any(it => it.GetAnimation(_trackIndex) != val)) return null;