diff --git a/Ink Canvas/Ink/Native/NativeInkInputRouter.cs b/Ink Canvas/Ink/Native/NativeInkInputRouter.cs index 7868b8dba..8178700b9 100644 --- a/Ink Canvas/Ink/Native/NativeInkInputRouter.cs +++ b/Ink Canvas/Ink/Native/NativeInkInputRouter.cs @@ -300,18 +300,21 @@ private static bool TryGetPalmEraserWidth( if (policy.IsSpecialScreen && policy.TouchMultiplier == 0) return false; - var boundWidth = policy.IsQuadIr - ? Math.Sqrt(Math.Max(0, pointer.ContactWidthDip * pointer.ContactHeightDip)) - : pointer.ContactWidthDip; + var boundWidth = PalmEraserGeometry.GetEffectiveContactWidthDip( + pointer.ContactWidthDip, + pointer.ContactHeightDip, + policy.IsQuadIr); var threshold = policy.BoundsWidthDip * policy.ThresholdFactor * policy.SensitivityMultiplier; if (boundWidth <= policy.BoundsWidthDip || boundWidth <= threshold) return false; - eraserWidthDip = boundWidth - * policy.EraserSizeFactor - * (policy.IsSpecialScreen ? policy.TouchMultiplier : 1); + eraserWidthDip = PalmEraserGeometry.ApplyPalmEraserSize( + boundWidth, + policy.EraserSizeFactor, + policy.IsSpecialScreen, + policy.TouchMultiplier); return true; } diff --git a/Ink Canvas/Ink/PalmEraserGeometry.cs b/Ink Canvas/Ink/PalmEraserGeometry.cs new file mode 100644 index 000000000..6b18694de --- /dev/null +++ b/Ink Canvas/Ink/PalmEraserGeometry.cs @@ -0,0 +1,59 @@ +using System; + +namespace Ink_Canvas.Ink +{ + /// + /// 手掌擦接触尺寸计算。 + /// 上下红外等触摸框可能把接触矩形某一轴异常放大(例如接近全屏宽度的长条), + /// 这里把异常细长接触折算为几何平均宽度,并限制最终手掌橡皮大小。 + /// + internal static class PalmEraserGeometry + { + /// + /// 手掌橡皮最大宽度(DIP)。用于防止上下红外等设备上报的异常接触矩形 + /// 把橡皮放大到远超真实手掌的程度。 + /// + internal const double MaxPalmEraserWidthDip = 200; + + /// + /// 接触矩形长宽比超过该值时,认为某一轴可能是红外框的异常放大结果, + /// 改用几何平均 sqrt(width*height) 来估算有效接触宽度。 + /// + private const double ElongatedAspectRatioThreshold = 3.0; + + internal static double GetEffectiveContactWidthDip( + double contactWidthDip, + double contactHeightDip, + bool isQuadIr) + { + if (contactWidthDip <= 0) + return 0; + + // 部分设备不提供高度,仍回退到宽度,避免破坏原有行为。 + if (contactHeightDip <= 0) + return contactWidthDip; + + var min = Math.Min(contactWidthDip, contactHeightDip); + var max = Math.Max(contactWidthDip, contactHeightDip); + var elongated = min <= 0 || max / min > ElongatedAspectRatioThreshold; + + if (isQuadIr || elongated) + return Math.Sqrt(Math.Max(0, contactWidthDip * contactHeightDip)); + + return contactWidthDip; + } + + internal static double ApplyPalmEraserSize( + double effectiveContactWidthDip, + double eraserSizeFactor, + bool isSpecialScreen, + double touchMultiplier) + { + var widthDip = effectiveContactWidthDip * eraserSizeFactor; + if (isSpecialScreen) + widthDip *= touchMultiplier; + + return Math.Min(widthDip, MaxPalmEraserWidthDip); + } + } +} diff --git a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs index f28475679..cc825f5e6 100644 --- a/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs +++ b/Ink Canvas/MainWindow_cs/MW_TouchEvents.cs @@ -1864,8 +1864,10 @@ private void Main_Grid_TouchDown(object sender, TouchEventArgs e) public double GetTouchBoundWidth(TouchEventArgs e) { var args = e.GetTouchPoint(null).Bounds; - if (!Settings.Advanced.IsQuadIR) return args.Width; - return Math.Sqrt(args.Width * args.Height); + return Ink_Canvas.Ink.PalmEraserGeometry.GetEffectiveContactWidthDip( + args.Width, + args.Height, + Settings.Advanced.IsQuadIR); } /// @@ -2086,18 +2088,21 @@ private void InkCanvas_PreviewTouchDown(object sender, TouchEventArgs e) if (boundWidth > BoundsWidth * EraserThresholdValue * thresholdMultiplier) { - boundWidth *= Settings.Startup.IsEnableNibMode + var eraserSizeFactor = Settings.Startup.IsEnableNibMode ? Settings.Advanced.NibModeBoundsWidthEraserSize : Settings.Advanced.FingerModeBoundsWidthEraserSize; + var palmEraserWidth = Ink_Canvas.Ink.PalmEraserGeometry.ApplyPalmEraserSize( + boundWidth, + eraserSizeFactor, + Settings.Advanced.IsSpecialScreen, + Settings.Advanced.TouchMultiplier); - if (Settings.Advanced.IsSpecialScreen) - boundWidth *= Settings.Advanced.TouchMultiplier; palmEraserPreviousEditingMode = inkCanvas.EditingMode; inkCanvas.EditingMode = InkCanvasEditingMode.EraseByPoint; isPalmEraserActive = true; EnableEraserOverlay(); - eraserWidth = boundWidth; + eraserWidth = palmEraserWidth; UpdateEraserStyle(); touchPoint = e.GetTouchPoint(inkCanvas); EraserOverlay_PointerDown(sender); diff --git a/InkCanvas.NativeInk.Tests/InkCanvas.NativeInk.Tests.csproj b/InkCanvas.NativeInk.Tests/InkCanvas.NativeInk.Tests.csproj index 06b683fd3..0915233fe 100644 --- a/InkCanvas.NativeInk.Tests/InkCanvas.NativeInk.Tests.csproj +++ b/InkCanvas.NativeInk.Tests/InkCanvas.NativeInk.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/InkCanvas.NativeInk.Tests/NativeInkCoreTests.cs b/InkCanvas.NativeInk.Tests/NativeInkCoreTests.cs index afe16fe4c..e929ae8a9 100644 --- a/InkCanvas.NativeInk.Tests/NativeInkCoreTests.cs +++ b/InkCanvas.NativeInk.Tests/NativeInkCoreTests.cs @@ -40,6 +40,9 @@ private static void Main() Run(nameof(RouterMapsLogicalTools), RouterMapsLogicalTools); Run(nameof(RouterPrefersMultiTouchWritingOverPalmErase), RouterPrefersMultiTouchWritingOverPalmErase); Run(nameof(RouterAppliesQuadIrPalmThresholdAndSpecialMultiplier), RouterAppliesQuadIrPalmThresholdAndSpecialMultiplier); + Run(nameof(RouterUsesGeometricMeanForElongatedTopBottomIrPalm), RouterUsesGeometricMeanForElongatedTopBottomIrPalm); + Run(nameof(RouterKeepsWidthForNormalPalmContact), RouterKeepsWidthForNormalPalmContact); + Run(nameof(RouterCapsPalmEraserWidth), RouterCapsPalmEraserWidth); Run(nameof(RouterAllowsDelayedTwoFingerTakeover), RouterAllowsDelayedTwoFingerTakeover); Run(nameof(RouterKeepsCapturedInkAndSuppressesBarrelPoints), RouterKeepsCapturedInkAndSuppressesBarrelPoints); Run(nameof(PointerBatchCopiesSamples), PointerBatchCopiesSamples); @@ -540,6 +543,67 @@ private static void RouterAppliesQuadIrPalmThresholdAndSpecialMultiplier() Equal(NativeInputRoute.Ink, disabledOnSpecialScreen.Route); } + private static void RouterUsesGeometricMeanForElongatedTopBottomIrPalm() + { + // 上下红外框常见异常:接触矩形接近全宽长条(width 异常大、height 接近真实手掌)。 + // 非四边红外模式下也应使用几何平均,避免橡皮被放大到远超手掌。 + var decision = NativeInkInputRouter.DecideDown( + Pointer(NativeInkInputKind.Touch, contactWidthDip: 1920, contactHeightDip: 120), + Context( + LogicalInkTool.Pen, + palm: Palm( + enabled: true, + isQuadIr: false, + isSpecialScreen: true, + boundsWidthDip: 10, + thresholdFactor: 2, + sensitivityMultiplier: 2, + eraserSizeFactor: 0.8, + touchMultiplier: 0.3))); + Equal(NativeInputRoute.PointErase, decision.Route); + True(Math.Abs(decision.PalmEraserWidthDip - 115.2d) < 0.001); + } + + private static void RouterKeepsWidthForNormalPalmContact() + { + // 普通触摸屏接触矩形不细长时,继续使用宽度,避免改变原有行为。 + var decision = NativeInkInputRouter.DecideDown( + Pointer(NativeInkInputKind.Touch, contactWidthDip: 80, contactHeightDip: 100), + Context( + LogicalInkTool.Pen, + palm: Palm( + enabled: true, + isQuadIr: false, + isSpecialScreen: false, + boundsWidthDip: 10, + thresholdFactor: 2, + sensitivityMultiplier: 2, + eraserSizeFactor: 0.8, + touchMultiplier: 1))); + Equal(NativeInputRoute.PointErase, decision.Route); + Equal(64d, decision.PalmEraserWidthDip); + } + + private static void RouterCapsPalmEraserWidth() + { + // 即使两个轴都异常大,最终手掌橡皮也不能超过保护上限。 + var decision = NativeInkInputRouter.DecideDown( + Pointer(NativeInkInputKind.Touch, contactWidthDip: 500, contactHeightDip: 500), + Context( + LogicalInkTool.Pen, + palm: Palm( + enabled: true, + isQuadIr: false, + isSpecialScreen: false, + boundsWidthDip: 10, + thresholdFactor: 2, + sensitivityMultiplier: 2, + eraserSizeFactor: 1, + touchMultiplier: 1))); + Equal(NativeInputRoute.PointErase, decision.Route); + Equal(Ink_Canvas.Ink.PalmEraserGeometry.MaxPalmEraserWidthDip, decision.PalmEraserWidthDip); + } + private static void RouterAllowsDelayedTwoFingerTakeover() { var decision = NativeInkInputRouter.DecideDown(