增加命中测试等级选项
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using SkiaSharp;
|
||||
using NLog;
|
||||
using SkiaSharp;
|
||||
using Spine.Interfaces.Attachments;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -8,8 +9,25 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Spine.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 命中测试等级枚举值
|
||||
/// </summary>
|
||||
public enum HitTestLevel { Bounds, Meshes, Pixels }
|
||||
|
||||
public static class SpineExtension
|
||||
{
|
||||
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 命中检测精确度等级
|
||||
/// </summary>
|
||||
public static HitTestLevel HitTestLevel { get; set; } = HitTestLevel.Bounds;
|
||||
|
||||
/// <summary>
|
||||
/// 命中测试时输出命中的插槽名称
|
||||
/// </summary>
|
||||
public static bool LogHitSlots { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前状态包围盒
|
||||
/// </summary>
|
||||
@@ -105,12 +123,12 @@ namespace Spine.Interfaces
|
||||
/// </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, Dictionary<SFML.Graphics.Texture, SFML.Graphics.Image> cache = null)
|
||||
{
|
||||
if (self.A <= 0 || !self.Bone.Active || self.Disabled)
|
||||
return false;
|
||||
|
||||
if (!precise)
|
||||
if (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);
|
||||
@@ -140,22 +158,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 +176,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];
|
||||
@@ -183,43 +189,61 @@ namespace Spine.Interfaces
|
||||
float u = u0 * w0 + u1 * w1 + u2 * w2;
|
||||
float v = v0 * w0 + v1 * w1 + v2 * w2;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
var texSize = img.Size;
|
||||
var pixel = img.GetPixel((uint)(u * texSize.X), (uint)(v * texSize.Y));
|
||||
hit = pixel.A > 0;
|
||||
break;
|
||||
bool hit = pixel.A > 0;
|
||||
|
||||
// 无缓存需要立即释放资源
|
||||
if (cache is null)
|
||||
{
|
||||
img.Dispose();
|
||||
}
|
||||
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
|
||||
// 无缓存需要立即释放资源
|
||||
if (cache is null)
|
||||
{
|
||||
img.Dispose();
|
||||
}
|
||||
|
||||
return hit;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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));
|
||||
bool hit = false;
|
||||
List<string> slotNames = [];
|
||||
foreach (var st in self.IterDrawOrder().Reverse())
|
||||
{
|
||||
if (st.HitTest(x, y, cache))
|
||||
{
|
||||
hit = true;
|
||||
if (!LogHitSlots)
|
||||
break;
|
||||
slotNames.Add(st.Name);
|
||||
}
|
||||
}
|
||||
foreach (var img in cache.Values) img.Dispose();
|
||||
return hit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逐插槽的命中测试, 会完整计算所有插槽的命中情况并按顶层至底层的顺序返回命中的插槽
|
||||
/// /// <param name="precise">是否精确命中检测, 否则仅使用每个插槽的包围盒进行命中检测</param>
|
||||
/// </summary>
|
||||
public static ISlot[] HitTestFull(this ISkeleton self, float x, float y, bool precise = false)
|
||||
{
|
||||
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 (LogHitSlots && slotNames.Count > 0)
|
||||
{
|
||||
_logger.Debug("Hit ({0}): [{1}]", self.Name, string.Join(", ", slotNames));
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
|
||||
AppLanguage = AppLanguage,
|
||||
RenderSelectedOnly = RenderSelectedOnly,
|
||||
UsePreciseHitTest = UsePreciseHitTest,
|
||||
HitTestLevel = HitTestLevel,
|
||||
LogHitSlots = LogHitSlots,
|
||||
WallpaperView = WallpaperView,
|
||||
CloseToTray = CloseToTray,
|
||||
@@ -140,7 +140,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 +252,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 +266,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
|
||||
|
||||
@@ -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,40 +362,19 @@ namespace SpineViewer.ViewModels.MainWindow
|
||||
// 没按 Ctrl 的情况下, 如果命中了已选中对象, 则就算普通命中
|
||||
bool hit = false;
|
||||
|
||||
if (!_logHitSlots)
|
||||
foreach (var sp in _models.Where(m => m.IsShown))
|
||||
{
|
||||
foreach (var sp in _models.Where(m => m.IsShown))
|
||||
if (!sp.HitTest(src.X, src.Y)) continue;
|
||||
|
||||
hit = true;
|
||||
|
||||
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
|
||||
if (!sp.IsSelected)
|
||||
{
|
||||
if (!sp.HitTest(src.X, src.Y, _usePreciseHitTest)) continue;
|
||||
|
||||
hit = true;
|
||||
|
||||
// 如果点到了没被选中的东西, 则清空原先选中的, 改为只选中这一次点的
|
||||
if (!sp.IsSelected)
|
||||
{
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
|
||||
}
|
||||
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));
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Reset));
|
||||
RequestSelectionChanging?.Invoke(this, new(NotifyCollectionChangedAction.Add, sp));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果点了空白的地方, 就清空选中列表
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user