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(