增加单独的参数拷贝方式

This commit is contained in:
ww-rm
2025-10-19 17:59:33 +08:00
parent 30608e05bc
commit 53a7700798
9 changed files with 280 additions and 169 deletions

View File

@@ -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);
}
}

View File

@@ -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)
{
_spineObject.Skeleton.ScaleX = value.Scale;
_spineObject.Skeleton.ScaleY = value.Scale;
if (flag == SpineObjectConfigApplyFlag.All)
{
_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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -34,7 +34,7 @@
<Style x:Key="MyListBoxBaseStyle" TargetType="{x:Type ListBox}" BasedOn="{StaticResource ListBoxBaseStyle}">
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>
<!--<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>-->
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Visible"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
@@ -49,7 +49,7 @@
<Style x:Key="MyListViewBaseStyle" TargetType="{x:Type ListView}" BasedOn="{StaticResource ListViewBaseStyle}">
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>
<!--<Setter Property="VirtualizingPanel.IsVirtualizing" Value="False"/>-->
<Setter Property="Background" Value="Transparent"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource ListViewItemBaseStyle.Small}"/>
</Style>

View File

@@ -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.Trace(ex.ToString());
_logger.Error("Failed to read json file {0}, {1}", path, ex.Message);
MessagePopupService.Error($"Failed to read json file {path}, {ex.ToString()}");
}
}
obj = default;
@@ -90,11 +88,24 @@ namespace SpineViewer.Utils
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to save json file {0}, {1}", path, ex.Message);
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.Trace(ex.ToString());
_logger.Error("Failed to serialize json object {0}", ex.Message);
return string.Empty;
}
}
}
public class ColorJsonConverter : JsonConverter<Color>

View File

@@ -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.Trace(ex.ToString());
_logger.Error("Failed to load: {0}, {1}", skelPath, ex.Message);
}
return false;
}
#region
/// <summary>
/// 弹窗添加单模型命令
/// </summary>
@@ -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.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);
}
}
}
#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
{
@@ -681,5 +723,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
return false;
}
#endregion
}
}

View File

@@ -170,21 +170,32 @@
Command="{Binding Cmd_MoveDownSpineObject}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/>
<MenuItem Header="{DynamicResource Str_SpineObjectConfig}">
<MenuItem Header="{DynamicResource Str_CopySpineObjectConfig}"
InputGestureText="Ctrl+Shift+C"
Command="{Binding Cmd_CopySpineObjectConfig}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_CopySpineObjectSkinConfig}"
Command="{Binding Cmd_CopySpineObjectSkinConfig}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_CopySpineObjectSlotAttachmentConfig}"
Command="{Binding Cmd_CopySpineObjectSlotAttachmentConfig}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_CopySpineObjectSlotVisibilityConfig}"
Command="{Binding Cmd_CopySpineObjectSlotVisibilityConfig}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_ApplySpineObjectConfig}"
InputGestureText="Ctrl+Shift+V"
Command="{Binding Cmd_ApplySpineObjectConfig}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/>
<MenuItem Header="{DynamicResource Str_ApplySpineObjectConfigFromFile}"
Command="{Binding Cmd_ApplySpineObjectConfigFromFile}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<MenuItem Header="{DynamicResource Str_SaveSpineObjectConfigToFile}"
Command="{Binding Cmd_SaveSpineObjectConfigToFile}"
CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
<Separator/>
</MenuItem>
<MenuItem Header="{DynamicResource Str_Export}">
<MenuItem Header="{DynamicResource Str_ExportFrame}"
Command="{Binding FrameExporterViewModel.Cmd_Export}"