diff --git a/Spine/Implementations/SpineWrappers/V21/Slot21.cs b/Spine/Implementations/SpineWrappers/V21/Slot21.cs index 19a7321..d88eb07 100644 --- a/Spine/Implementations/SpineWrappers/V21/Slot21.cs +++ b/Spine/Implementations/SpineWrappers/V21/Slot21.cs @@ -66,6 +66,8 @@ namespace Spine.Implementations.SpineWrappers.V21 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/Implementations/SpineWrappers/V34/Slot34.cs b/Spine/Implementations/SpineWrappers/V34/Slot34.cs index bcb406f..b98b193 100644 --- a/Spine/Implementations/SpineWrappers/V34/Slot34.cs +++ b/Spine/Implementations/SpineWrappers/V34/Slot34.cs @@ -73,6 +73,8 @@ namespace Spine.Implementations.SpineWrappers.V34 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/Implementations/SpineWrappers/V35/Slot35.cs b/Spine/Implementations/SpineWrappers/V35/Slot35.cs index 538b1d2..f202e03 100644 --- a/Spine/Implementations/SpineWrappers/V35/Slot35.cs +++ b/Spine/Implementations/SpineWrappers/V35/Slot35.cs @@ -73,6 +73,8 @@ namespace Spine.Implementations.SpineWrappers.V35 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/Implementations/SpineWrappers/V36/Slot36.cs b/Spine/Implementations/SpineWrappers/V36/Slot36.cs index 8b595d8..430c0b5 100644 --- a/Spine/Implementations/SpineWrappers/V36/Slot36.cs +++ b/Spine/Implementations/SpineWrappers/V36/Slot36.cs @@ -73,6 +73,8 @@ namespace Spine.Implementations.SpineWrappers.V36 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/Implementations/SpineWrappers/V37/Slot37.cs b/Spine/Implementations/SpineWrappers/V37/Slot37.cs index 28a68a7..55c5330 100644 --- a/Spine/Implementations/SpineWrappers/V37/Slot37.cs +++ b/Spine/Implementations/SpineWrappers/V37/Slot37.cs @@ -73,6 +73,8 @@ namespace Spine.Implementations.SpineWrappers.V37 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/Implementations/SpineWrappers/V38/Slot38.cs b/Spine/Implementations/SpineWrappers/V38/Slot38.cs index d640a1c..6255fca 100644 --- a/Spine/Implementations/SpineWrappers/V38/Slot38.cs +++ b/Spine/Implementations/SpineWrappers/V38/Slot38.cs @@ -74,6 +74,8 @@ namespace Spine.Implementations.SpineWrappers.V38 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/Implementations/SpineWrappers/V40/Slot40.cs b/Spine/Implementations/SpineWrappers/V40/Slot40.cs index 6467a5b..14f703d 100644 --- a/Spine/Implementations/SpineWrappers/V40/Slot40.cs +++ b/Spine/Implementations/SpineWrappers/V40/Slot40.cs @@ -73,6 +73,8 @@ namespace Spine.Implementations.SpineWrappers.V40 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/Implementations/SpineWrappers/V41/Slot41.cs b/Spine/Implementations/SpineWrappers/V41/Slot41.cs index 138edb6..9ddd689 100644 --- a/Spine/Implementations/SpineWrappers/V41/Slot41.cs +++ b/Spine/Implementations/SpineWrappers/V41/Slot41.cs @@ -73,6 +73,8 @@ namespace Spine.Implementations.SpineWrappers.V41 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/Implementations/SpineWrappers/V42/Slot42.cs b/Spine/Implementations/SpineWrappers/V42/Slot42.cs index 4f7dee0..283c011 100644 --- a/Spine/Implementations/SpineWrappers/V42/Slot42.cs +++ b/Spine/Implementations/SpineWrappers/V42/Slot42.cs @@ -73,6 +73,8 @@ namespace Spine.Implementations.SpineWrappers.V42 } } + public bool Disabled { get; set; } + public override string ToString() => _o.ToString(); } } diff --git a/Spine/SpineObject.cs b/Spine/SpineObject.cs index bc60f13..6914208 100644 --- a/Spine/SpineObject.cs +++ b/Spine/SpineObject.cs @@ -299,6 +299,30 @@ namespace Spine /// public bool DebugClippings { get; set; } + /// + /// 获取插槽可见性, 如果不存在则默认返回 false + /// + public bool GetSlotVisible(string slotName) + { + if (_skeleton.SlotsByName.TryGetValue(slotName, out var slot)) + return !slot.Disabled; + return false; + } + + /// + /// 设置插槽可见性, 插槽不可见后将不会在任何渲染中出现, 插槽不存在则忽略操作 + /// + /// 操作是否成功, 插槽不存在则返回 false + public bool SetSlotVisible(string slotName, bool visible) + { + if (_skeleton.SlotsByName.TryGetValue(slotName, out var slot)) + { + slot.Disabled = !visible; + return true; + } + return false; + } + /// /// 获取某个插槽上的附件名, 插槽不存在或者无附件均返回 null /// @@ -310,7 +334,7 @@ namespace Spine } /// - /// 设置某个插槽的附件, 如果不存在则忽略, 可以使用 null 来清除附件 + /// 设置某个插槽的附件, 如果不存在则忽略, 可以使用 null 来尝试清除附件 /// /// 是否操作成功 public bool SetAttachment(string slotName, string? attachmentName) @@ -471,7 +495,7 @@ namespace Spine foreach (var slot in _skeleton.IterDrawOrder()) { - if (slot.A <= 0 || !slot.Bone.Active) + if (slot.A <= 0 || !slot.Bone.Active || slot.Disabled) { _clipping.ClipEnd(slot); continue; @@ -602,7 +626,7 @@ namespace Spine if (DebugRegions) { vt.Color = AttachmentLineColor; - foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active)) + foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled)) { if (slot.Attachment is IRegionAttachment regionAttachment) { @@ -634,7 +658,7 @@ namespace Spine if (DebugMeshes) { vt.Color = MeshLineColor; - foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active)) + foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled)) { if (slot.Attachment is IMeshAttachment meshAttachment) { @@ -698,7 +722,7 @@ namespace Spine if (DebugMeshHulls) { vt.Color = AttachmentLineColor; - foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active)) + foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled)) { if (slot.Attachment is IMeshAttachment meshAttachment) { @@ -767,7 +791,7 @@ namespace Spine if (DebugClippings) { vt.Color = ClippingLineColor; - foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active)) + foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled)) { if (slot.Attachment is IClippingAttachment clippingAttachment) { diff --git a/Spine/SpineWrappers/ISlot.cs b/Spine/SpineWrappers/ISlot.cs index ab12933..27ac9ac 100644 --- a/Spine/SpineWrappers/ISlot.cs +++ b/Spine/SpineWrappers/ISlot.cs @@ -53,5 +53,10 @@ namespace Spine.SpineWrappers /// 使用的附件, 可以设置为 null 清空附件 /// public IAttachment? Attachment { get; set; } + + /// + /// 是否已禁用渲染该插槽 + /// + public bool Disabled { get; set; } } } diff --git a/SpineViewer/Models/SpineObjectConfigModel.cs b/SpineViewer/Models/SpineObjectConfigModel.cs index 27a1730..8b92b71 100644 --- a/SpineViewer/Models/SpineObjectConfigModel.cs +++ b/SpineViewer/Models/SpineObjectConfigModel.cs @@ -31,6 +31,8 @@ namespace SpineViewer.Models public Dictionary SlotAttachment { get; set; } = []; + public List DisabledSlots { get; set; } = []; + public List Animations { get; set; } = []; public bool DebugTexture { get; set; } = true; diff --git a/SpineViewer/Models/SpineObjectModel.cs b/SpineViewer/Models/SpineObjectModel.cs index 292a6db..71237f9 100644 --- a/SpineViewer/Models/SpineObjectModel.cs +++ b/SpineViewer/Models/SpineObjectModel.cs @@ -85,6 +85,8 @@ namespace SpineViewer.Models public event EventHandler? SkinStatusChanged; + public event EventHandler? SlotVisibleChanged; + public event EventHandler? SlotAttachmentChanged; public event EventHandler? AnimationChanged; @@ -200,6 +202,19 @@ namespace SpineViewer.Models public FrozenDictionary> SlotAttachments => _slotAttachments; + public bool GetSlotVisible(string slotName) + { + lock (_lock) return _spineObject.GetSlotVisible(slotName); + } + + public bool SetSlotVisible(string slotName, bool visible) + { + bool changed = false; + lock (_lock) changed = _spineObject.SetSlotVisible(slotName, visible); + if (changed) SlotVisibleChanged?.Invoke(this, new(slotName, visible)); + return changed; + } + public string? GetAttachment(string slotName) { lock (_lock) return _spineObject.GetAttachment(slotName); @@ -390,6 +405,8 @@ namespace SpineViewer.Models foreach (var slot in _spineObject.Skeleton.Slots) config.SlotAttachment[slot.Name] = slot.Attachment?.Name; + config.DisabledSlots = _spineObject.Skeleton.Slots.Where(it => it.Disabled).Select(it => it.Name).ToList(); + // XXX: 处理空动画 config.Animations.AddRange(_spineObject.AnimationState.IterTracks().Select(tr => tr?.Animation.Name)); @@ -422,6 +439,10 @@ namespace SpineViewer.Models if (_spineObject.SetAttachment(slotName, attachmentName)) SlotAttachmentChanged?.Invoke(this, new(slotName, attachmentName)); + foreach (var slotName in value.DisabledSlots) + if (_spineObject.SetSlotVisible(slotName, false)) + SlotVisibleChanged?.Invoke(this, new(slotName, false)); + // XXX: 处理空动画 _spineObject.AnimationState.ClearTracks(); int trackIndex = 0; @@ -507,6 +528,12 @@ namespace SpineViewer.Models public bool Status { get; } = status; } + public class SlotVisibleChangedEventArgs(string slotName, bool visible) : EventArgs + { + public string SlotName { get; } = slotName; + public bool Visible { get; } = visible; + } + public class SlotAttachmentChangedEventArgs(string slotName, string? attachmentName) : EventArgs { public string SlotName { get; } = slotName; diff --git a/SpineViewer/Resources/Strings/en.xaml b/SpineViewer/Resources/Strings/en.xaml index 4f4399d..2787fdb 100644 --- a/SpineViewer/Resources/Strings/en.xaml +++ b/SpineViewer/Resources/Strings/en.xaml @@ -78,6 +78,8 @@ Slot Clear Slots Attachment + Enable Slots + Disable Slots Animation Add diff --git a/SpineViewer/Resources/Strings/ja.xaml b/SpineViewer/Resources/Strings/ja.xaml index 86dcaf9..803a881 100644 --- a/SpineViewer/Resources/Strings/ja.xaml +++ b/SpineViewer/Resources/Strings/ja.xaml @@ -78,6 +78,8 @@ スロット アタッチメントをクリア + 有効 + 無効 アニメーション 追加 diff --git a/SpineViewer/Resources/Strings/zh.xaml b/SpineViewer/Resources/Strings/zh.xaml index 2a3eab0..773e757 100644 --- a/SpineViewer/Resources/Strings/zh.xaml +++ b/SpineViewer/Resources/Strings/zh.xaml @@ -78,6 +78,8 @@ 插槽 清除附件 + 启用 + 禁用 动画 添加 diff --git a/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs b/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs index 19fb81a..df0a56d 100644 --- a/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs +++ b/SpineViewer/ViewModels/MainWindow/SpineObjectTabViewModel.cs @@ -15,7 +15,7 @@ namespace SpineViewer.ViewModels.MainWindow { private SpineObjectModel[] _selectedObjects = []; private readonly ObservableCollection _skins = []; - private readonly ObservableCollection _slots = []; + private readonly ObservableCollection _slots = []; private readonly ObservableCollection _animationTracks = []; public ImmutableArray PhysicsOptions { get; } = Enum.GetValues().ToImmutableArray(); @@ -324,11 +324,16 @@ namespace SpineViewer.ViewModels.MainWindow args => { return args is not null && args.OfType().Any(); } ); - public ObservableCollection Slots => _slots; + public ObservableCollection Slots => _slots; - public RelayCommand Cmd_ClearSlotsAttachment { get; } = new( - args => { if (args is null) return; foreach (var s in args.OfType()) s.AttachmentName = null; }, - args => { return args is not null && args.OfType().Any(); } + public RelayCommand Cmd_EnableSlots { get; } = new( + args => { if (args is null) return; foreach (var s in args.OfType()) s.Visible = true; }, + args => { return args is not null && args.OfType().Any(); } + ); + + public RelayCommand Cmd_DisableSlots { get; } = new( + args => { if (args is null) return; foreach (var s in args.OfType()) s.Visible = false; }, + args => { return args is not null && args.OfType().Any(); } ); public ObservableCollection AnimationTracks => _animationTracks; @@ -672,13 +677,13 @@ namespace SpineViewer.ViewModels.MainWindow } } - public class SlotAttachmentViewModel : ObservableObject + public class SlotViewModel : ObservableObject { private readonly SpineObjectModel[] _spines; private readonly string[] _attachmentNames = []; private readonly string _slotName; - public SlotAttachmentViewModel(string slotName, SpineObjectModel[] spines) + public SlotViewModel(string slotName, SpineObjectModel[] spines) { _spines = spines; _slotName = slotName; @@ -694,6 +699,11 @@ namespace SpineViewer.ViewModels.MainWindow // 使用弱引用, 则此 ViewModel 被释放时无需显式退订事件 foreach (var sp in _spines) { + WeakEventManager.AddHandler( + sp, + nameof(sp.SlotVisibleChanged), + SingleModel_SlotVisibleChanged + ); WeakEventManager.AddHandler( sp, nameof(sp.SlotAttachmentChanged), @@ -707,9 +717,6 @@ namespace SpineViewer.ViewModels.MainWindow } } - public RelayCommand Cmd_ClearAttachment => _cmd_ClearAttachment ??= new(() => AttachmentName = null); - private RelayCommand? _cmd_ClearAttachment; - public ReadOnlyCollection AttachmentNames => _attachmentNames.AsReadOnly(); public string SlotName => _slotName; @@ -733,6 +740,30 @@ namespace SpineViewer.ViewModels.MainWindow } } + public bool? Visible + { + get + { + if (_spines.Length <= 0) return null; + var val = _spines[0].GetSlotVisible(_slotName); + if (_spines.Skip(1).Any(it => it.GetSlotVisible(_slotName) != val)) return null; + return val; + } + + set + { + if (_spines.Length <= 0) return; + if (value is null) return; + foreach (var sp in _spines) sp.SetSlotVisible(_slotName, (bool)value); + OnPropertyChanged(); + } + } + + private void SingleModel_SlotVisibleChanged(object? sender, SlotVisibleChangedEventArgs e) + { + if (e.SlotName == _slotName) OnPropertyChanged(nameof(Visible)); + } + private void SingleModel_SlotAttachmentChanged(object? sender, SlotAttachmentChangedEventArgs e) { if (e.SlotName == _slotName) OnPropertyChanged(nameof(AttachmentName)); diff --git a/SpineViewer/Views/MainWindow.xaml b/SpineViewer/Views/MainWindow.xaml index 90625c3..a23091b 100644 --- a/SpineViewer/Views/MainWindow.xaml +++ b/SpineViewer/Views/MainWindow.xaml @@ -524,8 +524,11 @@ - + @@ -540,9 +543,7 @@