Merge pull request #116 from ww-rm/dev/wpf

v0.16.3
This commit is contained in:
ww-rm
2025-10-02 14:22:11 +08:00
committed by GitHub
30 changed files with 165 additions and 175 deletions

View File

@@ -1,5 +1,11 @@
# CHANGELOG
## v0.16.3
- 修复加载工作区时的顺序错误
- 调整部分调试渲染的逻辑
- 完善命中检测逻辑
## v0.16.2
- 修复批量添加时的添加顺序错误

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V21
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V34
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V35
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V36
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V37
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V38
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V40
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -52,6 +52,7 @@ namespace Spine.Implementations.V41
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -60,6 +60,7 @@ namespace Spine.Implementations.V42
public Skeleton InnerObject => _o;
public string Name => _o.Data.Name;
public float R { get => _o.R; set => _o.R = value; }
public float G { get => _o.G; set => _o.G = value; }
public float B { get => _o.B; set => _o.B = value; }

View File

@@ -15,6 +15,11 @@ namespace Spine.Interfaces
/// </summary>
public enum Physics { None, Reset, Update, Pose }
/// <summary>
/// 名称
/// </summary>
public string Name { get; }
/// <summary>
/// R
/// </summary>

View File

@@ -1,4 +1,4 @@
using SkiaSharp;
using NLog;
using Spine.Interfaces.Attachments;
using System;
using System.Collections.Generic;
@@ -8,8 +8,33 @@ using System.Threading.Tasks;
namespace Spine.Interfaces
{
/// <summary>
/// 命中测试等级枚举值
/// </summary>
public enum HitTestLevel { None, Bounds, Meshes, Pixels }
public static class SpineExtension
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private static readonly SFML.Graphics.RenderTexture _renderTex; // XXX: 在此保留一个静态变量, 并且没有使用 Dispose 进行资源释放
static SpineExtension()
{
_renderTex = new(1, 1);
_renderTex.SetActive(false);
}
/// <summary>
/// 命中检测精确度等级
/// </summary>
public static HitTestLevel HitTestLevel { get; set; }
/// <summary>
/// 命中测试时输出命中的插槽名称
/// </summary>
public static bool LogHitSlots { get; set; }
/// <summary>
/// 获取当前状态包围盒
/// </summary>
@@ -103,19 +128,17 @@ namespace Spine.Interfaces
/// <summary>
/// 命中测试, 当插槽全透明或者处于禁用或者骨骼处于未激活则无法命中
/// </summary>
/// <param name="precise">是否精确命中检测, 否则仅使用包围盒进行命中检测</param>
/// <param name="cache">调用方管理的缓存表</param>
public static bool HitTest(this ISlot self, float x, float y, bool precise = false, Dictionary<SFML.Graphics.Texture, SFML.Graphics.Image> cache = null)
public static bool HitTest(this ISlot self, float x, float y)
{
if (self.A <= 0 || !self.Bone.Active || self.Disabled)
return false;
if (!precise)
if (HitTestLevel == HitTestLevel.None || HitTestLevel == HitTestLevel.Bounds)
{
self.GetBounds(out var bx, out var by, out var bw, out var bh);
return x >= bx && x <= (bx + bw) && y >= by && y <= (by + bh);
}
else
else if (HitTestLevel == HitTestLevel.Meshes || HitTestLevel == HitTestLevel.Pixels)
{
float[] vertices = new float[8];
int[] triangles;
@@ -140,22 +163,7 @@ namespace Spine.Interfaces
return false;
}
SFML.Graphics.Image img = null;
if (cache is not null)
{
if (!cache.TryGetValue(tex, out img))
{
img = cache[tex] = tex.CopyToImage();
}
}
else
{
img = tex.CopyToImage();
}
bool hit = false;
var trianglesLength = triangles.Length;
var texSize = img.Size;
for (int i = 0; i + 2 < trianglesLength; i += 3)
{
var idx0 = triangles[i] << 1;
@@ -173,6 +181,9 @@ namespace Spine.Interfaces
// 判断是否全部同号 (或为 0, 点在边上)
if ((c0 >= 0 && c1 >= 0 && c2 >= 0) || (c0 <= 0 && c1 <= 0 && c2 <= 0))
{
if (HitTestLevel == HitTestLevel.Meshes)
return true;
float u0 = uvs[idx0], v0 = uvs[idx0 + 1];
float u1 = uvs[idx1], v1 = uvs[idx1 + 1];
float u2 = uvs[idx2], v2 = uvs[idx2 + 1];
@@ -182,44 +193,62 @@ namespace Spine.Interfaces
float w2 = c0 * inv;
float u = u0 * w0 + u1 * w1 + u2 * w2;
float v = v0 * w0 + v1 * w1 + v2 * w2;
var texW = tex.Size.X;
var texH = tex.Size.Y;
var pixel = img.GetPixel((uint)(u * texSize.X), (uint)(v * texSize.Y));
hit = pixel.A > 0;
break;
// 把要判断的那个像素点渲出来
using var view = _renderTex.GetView();
using var vertexArray = new SFML.Graphics.VertexArray(SFML.Graphics.PrimitiveType.Points, 1);
vertexArray[0] = new(view.Center, new SFML.System.Vector2f(u * texW, v * texH));
// XXX: 此处 RenderTexture 不能临时用临时释放, 由于未知原因如果短时间快速创建释放 RenderTexture 可能让程序卡死
_renderTex.SetActive(true);
_renderTex.Clear(SFML.Graphics.Color.Transparent);
_renderTex.Draw(vertexArray, new(tex));
_renderTex.Display();
_renderTex.SetActive(false);
using var img = _renderTex.Texture.CopyToImage();
var pixel = img.GetPixel(0, 0);
return pixel.A > 0;
}
}
// 无缓存需要立即释放资源
if (cache is null)
return false;
}
else
{
img.Dispose();
}
return hit;
throw new NotImplementedException(HitTestLevel.ToString());
}
}
/// <summary>
/// 逐插槽的命中测试, 命中后会提前返回结果中止计算
/// </summary>
public static bool HitTest(this ISkeleton self, float x, float y, bool precise = false)
public static bool HitTest(this ISkeleton self, float x, float y)
{
var cache = new Dictionary<SFML.Graphics.Texture, SFML.Graphics.Image>();
bool hit = self.IterDrawOrder().Any(st => st.HitTest(x, y, precise, cache));
foreach (var img in cache.Values) img.Dispose();
return hit;
if (HitTestLevel == HitTestLevel.None)
{
self.GetBounds(out var bx, out var by, out var bw, out var bh);
return x >= bx && x <= (bx + bw) && y >= by && y <= (by + bh);
}
/// <summary>
/// 逐插槽的命中测试, 会完整计算所有插槽的命中情况并按顶层至底层的顺序返回命中的插槽
/// /// <param name="precise">是否精确命中检测, 否则仅使用每个插槽的包围盒进行命中检测</param>
/// </summary>
public static ISlot[] HitTestFull(this ISkeleton self, float x, float y, bool precise = false)
bool hit = false;
string hitSlotName = "";
foreach (var st in self.IterDrawOrder().Reverse())
{
var cache = new Dictionary<SFML.Graphics.Texture, SFML.Graphics.Image>();
var hitSlots = self.IterDrawOrder().Where(st => st.HitTest(x, y, precise, cache)).Reverse().ToArray();
foreach (var img in cache.Values) img.Dispose();
return hitSlots;
if (st.HitTest(x, y))
{
hit = true;
hitSlotName = st.Name;
break;
}
}
if (hit && LogHitSlots)
{
_logger.Debug("Hit ({0}): [{1}]", self.Name, hitSlotName);
}
return hit;
}
/// <summary>

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.2</Version>
<Version>0.16.3</Version>
</PropertyGroup>
<PropertyGroup>

View File

@@ -628,7 +628,7 @@ namespace Spine
if (DebugRegions)
{
vt.Color = AttachmentLineColor;
foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled))
foreach (var slot in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled))
{
if (slot.Attachment is IRegionAttachment regionAttachment)
{
@@ -660,7 +660,7 @@ namespace Spine
if (DebugMeshes)
{
vt.Color = MeshLineColor;
foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled))
foreach (var slot in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled))
{
if (slot.Attachment is IMeshAttachment meshAttachment)
{
@@ -696,7 +696,7 @@ namespace Spine
if (DebugMeshHulls)
{
vt.Color = AttachmentLineColor;
foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled))
foreach (var slot in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled))
{
if (slot.Attachment is IMeshAttachment meshAttachment)
{
@@ -742,7 +742,7 @@ namespace Spine
if (DebugClippings)
{
vt.Color = ClippingLineColor;
foreach (var slot in _skeleton.Slots.Where(s => s.Bone.Active && !s.Disabled))
foreach (var slot in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled))
{
if (slot.Attachment is IClippingAttachment clippingAttachment)
{
@@ -799,7 +799,7 @@ namespace Spine
if (DebugBones)
{
var width = Math.Max(Math.Abs(_skeleton.ScaleX), Math.Abs(_skeleton.ScaleY));
foreach (var bone in _skeleton.Bones.Where(b => b.Active))
foreach (var bone in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled).Select(st => st.Bone))
{
var boneLength = bone.Length;
var p1 = new SFML.System.Vector2f(bone.WorldX, bone.WorldY);
@@ -815,7 +815,7 @@ namespace Spine
if (DebugBones)
{
var radius = Math.Max(Math.Abs(_skeleton.ScaleX), Math.Abs(_skeleton.ScaleY));
foreach (var bone in _skeleton.Bones.Where(b => b.Active))
foreach (var bone in _skeleton.IterDrawOrder().Where(s => s.A > 0 && s.Bone.Active && !s.Disabled).Select(st => st.Bone))
{
DrawCirclePoint(target, new(bone.WorldX, bone.WorldY), BonePointColor, radius);
}

View File

@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Spine.Interfaces;
using SpineViewer.Services;
using System;
using System.Collections.Generic;
@@ -89,7 +90,7 @@ namespace SpineViewer.Models
private bool _renderSelectedOnly;
[ObservableProperty]
private bool _usePreciseHitTest;
private HitTestLevel _hitTestLevel;
[ObservableProperty]
private bool _logHitSlots;

View File

@@ -429,19 +429,11 @@ namespace SpineViewer.Models
}
/// <summary>
/// 命中检测, 可选是否使用精确检测, 会有性能损失
/// 命中检测
/// </summary>
public bool HitTest(float x, float y, bool precise = false)
public bool HitTest(float x, float y)
{
lock (_lock) return _spineObject.Skeleton.HitTest(x, y, precise);
}
/// <summary>
/// 完整的命中检测, 会检测所有插槽是否命中并返回命中的插槽名称
/// </summary>
public string[] HitTestFull(float x, float y, bool precise = false)
{
lock (_lock) return _spineObject.Skeleton.HitTestFull(x, y, precise).Select(v => v.Name).ToArray();
lock (_lock) return _spineObject.Skeleton.HitTest(x, y);
}
public SpineObjectConfigModel ObjectConfig

View File

@@ -120,10 +120,9 @@
<s:String x:Key="Str_PlaySpeed">Playback Speed</s:String>
<s:String x:Key="Str_WallpaperView">Wallpaper View</s:String>
<s:String x:Key="Str_RenderSelectedOnly">Render Selected Only</s:String>
<s:String x:Key="Str_UsePreciseHitTest">Use Precise Hit Testing</s:String>
<s:String x:Key="Str_UsePreciseHitTestTooltip">When enabled, click detection will be performed based on pixel transparency of the model.</s:String>
<s:String x:Key="Str_LogHitSlots">Log Hit Slot Names</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">When enabled, the log box will output the model and slot information hit by each click operation (will not output when using Ctrl for multi-selection).</s:String>
<s:String x:Key="Str_HitTestLevel">Hit Test Accuracy Level</s:String>
<s:String x:Key="Str_LogHitSlots">Output Hit Test Slot Names</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">When enabled, the log box will output the model and slot information for each click hit test.</s:String>
<s:String x:Key="Str_ShowAxis">Show Axis</s:String>
<s:String x:Key="Str_BackgroundColor">Background Color</s:String>
<s:String x:Key="Str_BackgroundImagePath">Background Image Path</s:String>

View File

@@ -120,10 +120,9 @@
<s:String x:Key="Str_PlaySpeed">再生速度</s:String>
<s:String x:Key="Str_WallpaperView">壁紙表示</s:String>
<s:String x:Key="Str_RenderSelectedOnly">選択のみレンダリング</s:String>
<s:String x:Key="Str_UsePreciseHitTest">精密ヒットテストを使用</s:String>
<s:String x:Key="Str_UsePreciseHitTestTooltip">有効にすると、モデルのピクセル透過度に基づいてクリック判定を行います。</s:String>
<s:String x:Key="Str_LogHitSlots">ヒットしたスロット名を出力</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">有効にすると、ログボックスに各クリック操作でヒットしたモデルとスロットの情報を出力しますCtrlを押しながら複数選択する場合は出力されません。</s:String>
<s:String x:Key="Str_HitTestLevel">ヒットテスト精度レベル</s:String>
<s:String x:Key="Str_LogHitSlots">ヒットテスト結果のスロット名を出力</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">有効にすると、ログボックスに各クリック操作で命中したモデルとスロットの情報が出力されます。</s:String>
<s:String x:Key="Str_ShowAxis">座標軸を表示</s:String>
<s:String x:Key="Str_BackgroundColor">背景色</s:String>
<s:String x:Key="Str_BackgroundImagePath">背景画像のパス</s:String>

View File

@@ -120,10 +120,9 @@
<s:String x:Key="Str_PlaySpeed">播放速度</s:String>
<s:String x:Key="Str_WallpaperView">桌面投影</s:String>
<s:String x:Key="Str_RenderSelectedOnly">仅渲染选中</s:String>
<s:String x:Key="Str_UsePreciseHitTest">使用精确命中检测</s:String>
<s:String x:Key="Str_UsePreciseHitTestTooltip">启用后将会按像素透明度来检测点击操作是否命中了模型</s:String>
<s:String x:Key="Str_LogHitSlots">输出命中的插槽名称</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">启用后将会在日志框内输出每一次点击操作命中的模型和插槽情况(按下 Ctrl 进行多选时不会输出)</s:String>
<s:String x:Key="Str_HitTestLevel">命中检测准确度等级</s:String>
<s:String x:Key="Str_LogHitSlots">输出命中检测结果的插槽名称</s:String>
<s:String x:Key="Str_LogHitSlotsTooltip">启用后将会在日志框内输出每一次点击操作命中的模型和插槽情况</s:String>
<s:String x:Key="Str_ShowAxis">显示坐标轴</s:String>
<s:String x:Key="Str_BackgroundColor">背景颜色</s:String>
<s:String x:Key="Str_BackgroundImagePath">背景图片路径</s:String>

View File

@@ -7,7 +7,7 @@
<TargetFramework>net8.0-windows</TargetFramework>
<BaseOutputPath>$(SolutionDir)out</BaseOutputPath>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Version>0.16.2</Version>
<Version>0.16.3</Version>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
</PropertyGroup>

View File

@@ -18,7 +18,7 @@ namespace SpineViewer.ViewModels.Exporters
{
public class FFmpegVideoExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{
public ImmutableArray<FFmpegVideoExporter.VideoFormat> VideoFormatOptions { get; } = Enum.GetValues<FFmpegVideoExporter.VideoFormat>().ToImmutableArray();
public static ImmutableArray<FFmpegVideoExporter.VideoFormat> VideoFormatOptions { get; } = Enum.GetValues<FFmpegVideoExporter.VideoFormat>().ToImmutableArray();
public FFmpegVideoExporter.VideoFormat Format { get => _format; set => SetProperty(ref _format, value); }
protected FFmpegVideoExporter.VideoFormat _format = FFmpegVideoExporter.VideoFormat.Mp4;

View File

@@ -20,7 +20,7 @@ namespace SpineViewer.ViewModels.Exporters
{
public class FrameExporterViewModel(MainWindowViewModel vmMain) : BaseExporterViewModel(vmMain)
{
public ImmutableArray<SKEncodedImageFormat> FrameFormatOptions { get; } = Enum.GetValues<SKEncodedImageFormat>().ToImmutableArray();
public static ImmutableArray<SKEncodedImageFormat> FrameFormatOptions { get; } = Enum.GetValues<SKEncodedImageFormat>().ToImmutableArray();
public SKEncodedImageFormat Format { get => _format; set => SetProperty(ref _format, value); }
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;

View File

@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using NLog;
using Spine.Implementations;
using Spine.Interfaces;
using SpineViewer.Models;
using SpineViewer.Natives;
using SpineViewer.Services;
@@ -109,7 +110,7 @@ namespace SpineViewer.ViewModels.MainWindow
AppLanguage = AppLanguage,
RenderSelectedOnly = RenderSelectedOnly,
UsePreciseHitTest = UsePreciseHitTest,
HitTestLevel = HitTestLevel,
LogHitSlots = LogHitSlots,
WallpaperView = WallpaperView,
CloseToTray = CloseToTray,
@@ -140,7 +141,7 @@ namespace SpineViewer.ViewModels.MainWindow
AppLanguage = value.AppLanguage;
RenderSelectedOnly = value.RenderSelectedOnly;
UsePreciseHitTest = value.UsePreciseHitTest;
HitTestLevel = value.HitTestLevel;
LogHitSlots = value.LogHitSlots;
WallpaperView = value.WallpaperView;
CloseToTray = value.CloseToTray;
@@ -252,6 +253,8 @@ namespace SpineViewer.ViewModels.MainWindow
public static ImmutableArray<AppLanguage> AppLanguageOptions { get; } = Enum.GetValues<AppLanguage>().ToImmutableArray();
public static ImmutableArray<HitTestLevel> HitTestLevelOptions { get; } = Enum.GetValues<HitTestLevel>().ToImmutableArray();
public AppLanguage AppLanguage
{
get => ((App)App.Current).Language;
@@ -264,16 +267,16 @@ namespace SpineViewer.ViewModels.MainWindow
set => SetProperty(_vmMain.SFMLRendererViewModel.RenderSelectedOnly, value, v => _vmMain.SFMLRendererViewModel.RenderSelectedOnly = v);
}
public bool UsePreciseHitTest
public HitTestLevel HitTestLevel
{
get => _vmMain.SFMLRendererViewModel.UsePreciseHitTest;
set => SetProperty(_vmMain.SFMLRendererViewModel.UsePreciseHitTest, value, v => _vmMain.SFMLRendererViewModel.UsePreciseHitTest = v);
get => SpineExtension.HitTestLevel;
set => SetProperty(SpineExtension.HitTestLevel, value, v => SpineExtension.HitTestLevel = v);
}
public bool LogHitSlots
{
get => _vmMain.SFMLRendererViewModel.LogHitSlots;
set => SetProperty(_vmMain.SFMLRendererViewModel.LogHitSlots, value, v => _vmMain.SFMLRendererViewModel.LogHitSlots = v);
get => SpineExtension.LogHitSlots;
set => SetProperty(SpineExtension.LogHitSlots, value, v => SpineExtension.LogHitSlots = v);
}
public bool WallpaperView

View File

@@ -25,7 +25,7 @@ namespace SpineViewer.ViewModels.MainWindow
{
public class SFMLRendererViewModel : ObservableObject
{
public ImmutableArray<Stretch> StretchOptions { get; } = Enum.GetValues<Stretch>().ToImmutableArray();
public static ImmutableArray<Stretch> StretchOptions { get; } = Enum.GetValues<Stretch>().ToImmutableArray();
/// <summary>
/// 日志器
@@ -250,26 +250,6 @@ namespace SpineViewer.ViewModels.MainWindow
}
private bool _renderSelectedOnly;
/// <summary>
/// 启用精确命中测试
/// </summary>
public bool UsePreciseHitTest
{
get => _usePreciseHitTest;
set => SetProperty(ref _usePreciseHitTest, value);
}
private bool _usePreciseHitTest;
/// <summary>
/// 启用完整的命中测试并在日志中输出命中测试的插槽结果
/// </summary>
public bool LogHitSlots
{
get => _logHitSlots;
set => SetProperty(ref _logHitSlots, value);
}
private bool _logHitSlots;
/// <summary>
/// 启用桌面投影
/// </summary>
@@ -368,22 +348,9 @@ namespace SpineViewer.ViewModels.MainWindow
if (_renderSelectedOnly)
{
bool hit = false;
if (!_logHitSlots)
{
// 只在被选中的对象里判断是否有效命中
hit = _models.Any(m => m.IsSelected && m.HitTest(src.X, src.Y, _usePreciseHitTest));
}
else
{
foreach (var sp in _models.Where(m => m.IsSelected))
{
var slotNames = sp.HitTestFull(src.X, src.Y, _usePreciseHitTest);
if (slotNames.Length <= 0) continue;
hit = true;
_logger.Debug("Model Hit ({0}): [{1}]", sp.Name, string.Join(", ", slotNames));
}
}
// 只在被选中的对象里判断是否有效命中
hit = _models.Any(m => m.IsSelected && m.HitTest(src.X, src.Y));
// 如果没点到被选中的模型, 则不允许拖动
if (!hit) _draggingSrc = null;
@@ -395,11 +362,9 @@ namespace SpineViewer.ViewModels.MainWindow
// 没按 Ctrl 的情况下, 如果命中了已选中对象, 则就算普通命中
bool hit = false;
if (!_logHitSlots)
{
foreach (var sp in _models.Where(m => m.IsShown))
{
if (!sp.HitTest(src.X, src.Y, _usePreciseHitTest)) continue;
if (!sp.HitTest(src.X, src.Y)) continue;
hit = true;
@@ -411,25 +376,6 @@ namespace SpineViewer.ViewModels.MainWindow
}
break;
}
}
else
{
foreach (var sp in _models.Where(m => m.IsShown))
{
var slotNames = sp.HitTestFull(src.X, src.Y, _usePreciseHitTest);
if (slotNames.Length <= 0) continue;
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
// 仅判断顶层对象 (首次命中)
if (!hit && !sp.IsSelected)
{
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
}
hit = true;
_logger.Debug("Model Hit ({0}): [{1}]", sp.Name, string.Join(", ", slotNames));
}
}
// 如果点了空白的地方, 就清空选中列表
if (!hit) RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
@@ -437,7 +383,7 @@ namespace SpineViewer.ViewModels.MainWindow
else
{
// 按下 Ctrl 的情况就执行多选, 并且点空白处也不会清空选中, 如果点击了本来就是选中的则取消选中
if (_models.FirstOrDefault(m => m.IsShown && m.HitTest(src.X, src.Y, _usePreciseHitTest), null) is SpineObjectModel sp)
if (_models.FirstOrDefault(m => m.IsShown && m.HitTest(src.X, src.Y), null) is SpineObjectModel sp)
{
if (sp.IsSelected)
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Remove, sp));

View File

@@ -111,7 +111,7 @@ namespace SpineViewer.ViewModels.MainWindow
return;
if (!DialogService.ShowOpenFileDialog(out var atlasFileName, AppResource.Str_OpenAtlasFileTitle))
return;
AddSpineObject(skelFileName, atlasFileName);
InsertSpineObject(skelFileName, atlasFileName);
_logger.LogCurrentProcessMemoryUsage();
}
@@ -479,7 +479,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
else if (validPaths.Count > 0)
{
AddSpineObject(validPaths[0]);
InsertSpineObject(validPaths[0]);
_logger.LogCurrentProcessMemoryUsage();
}
}
@@ -506,7 +506,7 @@ namespace SpineViewer.ViewModels.MainWindow
var skelPath = paths[i];
reporter.ProgressText = $"[{i}/{totalCount}] {skelPath}";
if (AddSpineObject(skelPath))
if (InsertSpineObject(skelPath))
success++;
else
error++;
@@ -529,7 +529,7 @@ namespace SpineViewer.ViewModels.MainWindow
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
private bool AddSpineObject(string skelPath, string? atlasPath = null)
private bool InsertSpineObject(string skelPath, string? atlasPath = null)
{
try
{
@@ -599,7 +599,7 @@ namespace SpineViewer.ViewModels.MainWindow
}
else if (models.Count > 0)
{
AddSpineObject(models[0]);
InsertSpineObject(models[0]);
_logger.LogCurrentProcessMemoryUsage();
}
}
@@ -620,10 +620,10 @@ namespace SpineViewer.ViewModels.MainWindow
{
if (ct.IsCancellationRequested) break;
var cfg = models[i];
var cfg = models[totalCount - 1 - i];
reporter.ProgressText = $"[{i}/{totalCount}] {cfg}";
if (AddSpineObject(cfg))
if (InsertSpineObject(cfg))
success++;
else
error++;
@@ -653,7 +653,7 @@ namespace SpineViewer.ViewModels.MainWindow
/// 安全地在列表头添加一个模型, 发生错误会输出日志
/// </summary>
/// <returns>是否添加成功</returns>
private bool AddSpineObject(SpineObjectWorkspaceConfigModel cfg)
private bool InsertSpineObject(SpineObjectWorkspaceConfigModel cfg)
{
try
{

View File

@@ -18,7 +18,7 @@ namespace SpineViewer.ViewModels.MainWindow
private readonly ObservableCollection<SlotViewModel> _slots = [];
private readonly ObservableCollection<AnimationTrackViewModel> _animationTracks = [];
public ImmutableArray<ISkeleton.Physics> PhysicsOptions { get; } = Enum.GetValues<ISkeleton.Physics>().ToImmutableArray();
public static ImmutableArray<ISkeleton.Physics> PhysicsOptions { get; } = Enum.GetValues<ISkeleton.Physics>().ToImmutableArray();
public SpineObjectModel[] SelectedObjects
{

View File

@@ -5,8 +5,8 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:local="clr-namespace:SpineViewer.Views.ExporterDialogs"
xmlns:exporters="clr-namespace:SpineViewer.ViewModels.Exporters"
d:DataContext="{d:DesignInstance Type=exporters:FFmpegVideoExporterViewModel}"
xmlns:vmexp="clr-namespace:SpineViewer.ViewModels.Exporters"
d:DataContext="{d:DesignInstance Type=vmexp:FFmpegVideoExporterViewModel}"
mc:Ignorable="d"
Title="{DynamicResource Str_FFmpegVideoExporterTitle}"
Width="450"
@@ -161,7 +161,7 @@
<!-- 视频格式 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_VideoFormat}"/>
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding VideoFormatOptions}"/>
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{x:Static vmexp:FFmpegVideoExporterViewModel.VideoFormatOptions}"/>
<!-- 动图是否循环 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_LoopPlay}" ToolTip="{DynamicResource Str_LoopPlayTooltip}"/>

View File

@@ -126,7 +126,7 @@
<!-- 图像格式 -->
<Label Grid.Row="0" Grid.Column="0" Content="{DynamicResource Str_ImageFormat}"/>
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{Binding FrameFormatOptions}"/>
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding Format}" ItemsSource="{x:Static vmexp:FrameExporterViewModel.FrameFormatOptions}"/>
<!-- 图像质量 -->
<Label Grid.Row="1" Grid.Column="0" Content="{DynamicResource Str_ImageQuality}" ToolTip="{DynamicResource Str_ImageQualityTooltip}"/>

View File

@@ -327,7 +327,7 @@
<!-- 物理 -->
<Label Grid.Row="2" Grid.Column="0" Content="{DynamicResource Str_Physics}"/>
<ComboBox Grid.Row="2" Grid.Column="1" SelectedValue="{Binding Physics}" ItemsSource="{Binding PhysicsOptions}"/>
<ComboBox Grid.Row="2" Grid.Column="1" SelectedValue="{Binding Physics}" ItemsSource="{x:Static vm:SpineObjectTabViewModel.PhysicsOptions}"/>
<!-- 时间因子 -->
<Label Grid.Row="3" Grid.Column="0" Content="{DynamicResource Str_TimeScale}" ToolTip="{DynamicResource Str_TimeScaleTootltip}"/>
@@ -790,7 +790,7 @@
<!-- 背景图案模式 -->
<Label Grid.Row="14" Grid.Column="0" Content="{DynamicResource Str_BackgroundImageMode}"/>
<ComboBox Grid.Row="14" Grid.Column="1" SelectedValue="{Binding BackgroundImageMode}" ItemsSource="{Binding StretchOptions}"/>
<ComboBox Grid.Row="14" Grid.Column="1" SelectedValue="{Binding BackgroundImageMode}" ItemsSource="{x:Static vm:SFMLRendererViewModel.StretchOptions}"/>
</Grid>
</TabItem>
</TabControl>

View File

@@ -51,7 +51,7 @@
</Style>
</Border.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Grid.IsSharedSizeScope="True">
<StackPanel Grid.IsSharedSizeScope="True" Margin="0 0 0 50">
<GroupBox Header="{DynamicResource Str_TextureLoadPreference}">
<StackPanel>
<Grid>
@@ -223,8 +223,10 @@
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelCol"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource Str_UsePreciseHitTest}" ToolTip="{DynamicResource Str_UsePreciseHitTestTooltip}"/>
<ToggleButton Grid.Column="1" IsChecked="{Binding UsePreciseHitTest}" ToolTip="{DynamicResource Str_UsePreciseHitTestTooltip}"/>
<Label Content="{DynamicResource Str_HitTestLevel}"/>
<ComboBox Grid.Column="1"
SelectedItem="{Binding HitTestLevel}"
ItemsSource="{x:Static vm:PreferenceViewModel.HitTestLevelOptions}"/>
</Grid>
<Grid>