dotnet / dotnet/wpf

Bug in calculation algorithm of IncrementalStrokeHitTester

Open
#11,059 0 comments 2 reactions 0 assignees View on GitHub
Bug
Dominant language
C#
Stars
7.7k
Forks
1.3k
Avg merge
1d 11h
Merged PRs (30d)
61

Description

### Description

There is a calculation error in WPF’s IncrementalStrokeHitTester. Occasionally, when I use IncrementalStrokeHitTester and provide an eraser trajectory that should only split the stroke into two new strokes, the whole stroke is erased instead.

I have managed to reproduce this issue with a minimal test case. Using the following data:

```
{X=445.3333333333333,Y=456,P=0.7618542313575745},
{X=448,Y=456,P=0.7618542313575745},
{X=450,Y=455.3333333333333,P=0.7742698192596436},
{X=452,Y=455.3333333333333,P=0.7923250794410706},
{X=454,Y=455.3333333333333,P=0.7947957515716553},
{X=456,Y=455.3333333333333,P=0.7871016263961792},
{X=458,Y=455.3333333333333,P=0.7873656749725342},
{X=460,Y=455.3333333333333,P=0.7871317267417908},
{X=462,Y=455.3333333333333,P=0.7867388129234314},
{X=464.66666666666663,Y=456,P=0.7443132400512695},
{X=466.66666666666663,Y=456,P=0.7917653322219849},
{X=468.66666666666663,Y=456.66666666666663,P=0.7867511510848999},
{X=472,Y=456.66666666666663,P=0.7043896913528442},
{X=474,Y=457.3333333333333,P=0.7800122499465942},
{X=476,Y=457.3333333333333,P=0.7910786271095276},
{X=478,Y=458,P=0.7841806411743164},
{X=482,Y=458.66666666666663,P=0.6613860726356506},
{X=484.66666666666663,Y=459.3333333333333,P=0.7315411567687988},
{X=486.66666666666663,Y=460,P=0.7758287191390991},
{X=488.66666666666663,Y=460,P=0.785304844379425},
{X=492,Y=460.66666666666663,P=0.6987720131874084},
{X=496,Y=461.3333333333333,P=0.6533879637718201},
{X=500,Y=462,P=0.6456292271614075},
{X=503.3333333333333,Y=462,P=0.6683430075645447},
{X=505.3333333333333,Y=462.66666666666663,P=0.7513852119445801},
{X=510,Y=463.3333333333333,P=0.5926470756530762},
{X=514,Y=464,P=0.621231198310852},
{X=516,Y=464.66666666666663,P=0.7479339241981506},
{X=518,Y=465.3333333333333,P=0.7547337412834167},
{X=520.6666666666666,Y=465.3333333333333,P=0.7243622541427612},
{X=526.6666666666666,Y=466,P=0.5270907282829285},
{X=529.3333333333333,Y=467.3333333333333,P=0.6958931684494019},
{X=534,Y=468,P=0.6012994050979614},
{X=538,Y=468.66666666666663,P=0.6275204420089722},
{X=542.6666666666666,Y=469.3333333333333,P=0.5978454351425171},
{X=546,Y=470,P=0.6662331819534302},
{X=552.6666666666666,Y=470.66666666666663,P=0.5002615451812744},
{X=555.3333333333333,Y=471.3333333333333,P=0.6958120465278625},
{X=562,Y=472,P=0.4994753301143646},
{X=567.3333333333333,Y=472.66666666666663,P=0.5455721616744995},
{X=573.3333333333333,Y=474,P=0.5134470462799072},
{X=580.6666666666666,Y=474.66666666666663,P=0.4518575370311737},
{X=585.3333333333333,Y=475.3333333333333,P=0.5670377612113953},
{X=589.3333333333333,Y=476,P=0.609724223613739},
{X=594.6666666666666,Y=476.66666666666663,P=0.5481007099151611},
{X=601.3333333333333,Y=477.3333333333333,P=0.4885927140712738},
{X=606.6666666666666,Y=478,P=0.5432637929916382},
{X=610,Y=478.66666666666663,P=0.6355723142623901},
{X=616.6666666666666,Y=479.3333333333333,P=0.47008851170539856},
{X=622,Y=480,P=0.5248528122901917},
{X=628.6666666666666,Y=480,P=0.46539586782455444},
{X=636.6666666666666,Y=480.66666666666663,P=0.39966750144958496},
{X=642,Y=480.66666666666663,P=0.509570300579071},
{X=647.3333333333333,Y=482,P=0.5069840550422668},
{X=652,Y=482,P=0.56694495677948},
{X=654,Y=482,P=0.7375491857528687},
{X=662,Y=482.66666666666663,P=0.4425170123577118},
{X=666.6666666666666,Y=482.66666666666663,P=0.5797702074050903},
{X=669.3333333333333,Y=482.66666666666663,P=0.7043377757072449},
{X=676,Y=482.66666666666663,P=0.49506327509880066},
{X=680,Y=482.66666666666663,P=0.6218303442001343},
{X=684.6666666666666,Y=482.66666666666663,P=0.5944092869758606},
{X=689.3333333333333,Y=482.66666666666663,P=0.5880598425865173},
{X=694,Y=482.66666666666663,P=0.5764558911323547},
{X=696,Y=482.66666666666663,P=0.7410839796066284},
{X=700,Y=482.66666666666663,P=0.626220166683197},
{X=704,Y=482.66666666666663,P=0.6229243278503418},
{X=707.3333333333333,Y=482.66666666666663,P=0.6664982438087463},
{X=714.6666666666666,Y=482.66666666666663,P=0.45310068130493164},
{X=720.6666666666666,Y=482.66666666666663,P=0.4970242381095886},
{X=724,Y=482.66666666666663,P=0.6377686858177185},
{X=728.6666666666666,Y=482.66666666666663,P=0.5830304622650146},
{X=733.3333333333333,Y=482,P=0.5852826237678528},
{X=735.3333333333333,Y=482,P=0.7534965872764587},
{X=739.3333333333333,Y=481.3333333333333,P=0.6395262479782104},
{X=742,Y=481.3333333333333,P=0.7220669984817505},
{X=746,Y=480.66666666666663,P=0.6478912830352783},
{X=750,Y=480,P=0.6395424604415894},
{X=754,Y=478.66666666666663,P=0.629923403263092},
{X=759.3333333333333,Y=478,P=0.5632662177085876},
{X=762.6666666666666,Y=477.3333333333333,P=0.6644474864006042},
{X=765.3333333333333,Y=476.66666666666663,P=0.7117174863815308},
{X=768.6666666666666,Y=476,P=0.6776741743087769},
{X=773.3333333333333,Y=474.66666666666663,P=0.5986015200614929},
{X=777.3333333333333,Y=473.3333333333333,P=0.6267250180244446},
{X=781.3333333333333,Y=471.3333333333333,P=0.6099798679351807},
{X=784.6666666666666,Y=470.66666666666663,P=0.6539067625999451},
{X=790.6666666666666,Y=468.66666666666663,P=0.49772119522094727},
{X=792.6666666666666,Y=468,P=0.7275203466415405},
{X=796,Y=466.66666666666663,P=0.6488282680511475},
{X=800,Y=465.3333333333333,P=0.6072457432746887},
{X=804,Y=463.3333333333333,P=0.5964605212211609},
{X=807.3333333333333,Y=462,P=0.6561574339866638},
{X=810,Y=460.66666666666663,P=0.6968104839324951},
{X=813.3333333333333,Y=458.66666666666663,P=0.6486343741416931},
{X=818,Y=456.66666666666663,P=0.5831184983253479},
{X=820.6666666666666,Y=455.3333333333333,P=0.6963025331497192},
{X=822.6666666666666,Y=454,P=0.7399721741676331},
{X=825.3333333333333,Y=452.66666666666663,P=0.7092769145965576},
{X=828.6666666666666,Y=451.3333333333333,P=0.6766396164894104},
{X=833.3333333333333,Y=448.66666666666663,P=0.5740551948547363},
{X=835.3333333333333,Y=447.3333333333333,P=0.7345168590545654},
{X=837.3333333333333,Y=446.66666666666663,P=0.7650047540664673},
{X=840.6666666666666,Y=445.3333333333333,P=0.6749776005744934},
{X=844.6666666666666,Y=442.66666666666663,P=0.5888175368309021},
{X=848.6666666666666,Y=440.66666666666663,P=0.5972996354103088},
{X=852,Y=438.66666666666663,P=0.6244710683822632},
{X=854,Y=437.3333333333333,P=0.7188596725463867},
{X=856.6666666666666,Y=436,P=0.6917470097541809},
{X=858.6666666666666,Y=434.66666666666663,P=0.7351683378219604},
{X=862.6666666666666,Y=432.66666666666663,P=0.6261351704597473},
{X=864.6666666666666,Y=431.3333333333333,P=0.7430726289749146},
{X=866.6666666666666,Y=430.66666666666663,P=0.7724213600158691},
{X=868.6666666666666,Y=429.3333333333333,P=0.7554715871810913},
{X=871.3333333333333,Y=428.66666666666663,P=0.7384610176086426},
{X=875.3333333333333,Y=426.66666666666663,P=0.6147278547286987},
{X=877.3333333333333,Y=426,P=0.7556149363517761},
{X=880,Y=425.3333333333333,P=0.7179256677627563},
{X=882,Y=424.66666666666663,P=0.7604349851608276},
{X=884,Y=424,P=0.7805911302566528},
{X=886,Y=423.3333333333333,P=0.7828847765922546},
{X=888,Y=422.66666666666663,P=0.78639817237854},
{X=890,Y=422,P=0.7878957390785217},
{X=892,Y=421.3333333333333,P=0.7883377075195312},
{X=894,Y=421.3333333333333,P=0.7963496446609497},
{X=896,Y=421.3333333333333,P=0.7972534894943237},
{X=898,Y=420.66666666666663,P=0.7801092267036438},
{X=900,Y=420.66666666666663,P=0.7869842052459717},
{X=902,Y=420.66666666666663,P=0.7963632345199585},
{X=904,Y=420,P=0.7890701293945312},
{X=906,Y=420,P=0.7968790531158447},
{X=908,Y=420,P=0.8703685998916626},
```

When passing the point (684.9383585999957, 446.44199735085795) to IncrementalStrokeHitTester, it erases the entire stroke. However, using `Stroke.GetEraseResult` with the same input correctly returns two segments as expected.

Here is my minimal reproduction demo code (full project uploaded to GitHub: https://github.com/lindexi/lindexi_gd/tree/7f7f914c05829be1feb090ab05b0514df4e46e97/WPFDemo/RarlereninemniBohinabairhalljere ):

```csharp
var stylusPointCollection = GetTestData();

var stroke = new Stroke(stylusPointCollection);

var x = 684.9383585999957;
var y = 446.44199735085795;

var strokeCollection = new StrokeCollection([stroke]);
var rectangleStylusShape = new RectangleStylusShape(50, 70);
var incrementalStrokeHitTester = strokeCollection.GetIncrementalStrokeHitTester(rectangleStylusShape);

var point = new Point(x, y);

incrementalStrokeHitTester.StrokeHit += (o, args) =>
{
var pointEraseResults = args.GetPointEraseResults();
if (pointEraseResults.Count == 0)
{
Debugger.Break();

GC.KeepAlive(point);
}
};

incrementalStrokeHitTester.AddPoint(point);

private static StylusPointCollection GetTestData()
{
var test =
"""
{X=445.3333333333333,Y=456,P=0.7618542313575745},
{X=448,Y=456,P=0.7618542313575745},
{X=450,Y=455.3333333333333,P=0.7742698192596436},
{X=452,Y=455.3333333333333,P=0.7923250794410706},
{X=454,Y=455.3333333333333,P=0.7947957515716553},
{X=456,Y=455.3333333333333,P=0.7871016263961792},
{X=458,Y=455.3333333333333,P=0.7873656749725342},
{X=460,Y=455.3333333333333,P=0.7871317267417908},
{X=462,Y=455.3333333333333,P=0.7867388129234314},
{X=464.66666666666663,Y=456,P=0.7443132400512695},
{X=466.66666666666663,Y=456,P=0.7917653322219849},
{X=468.66666666666663,Y=456.66666666666663,P=0.7867511510848999},
{X=472,Y=456.66666666666663,P=0.7043896913528442},
{X=474,Y=457.3333333333333,P=0.7800122499465942},
{X=476,Y=457.3333333333333,P=0.7910786271095276},
{X=478,Y=458,P=0.7841806411743164},
{X=482,Y=458.66666666666663,P=0.6613860726356506},
{X=484.66666666666663,Y=459.3333333333333,P=0.7315411567687988},
{X=486.66666666666663,Y=460,P=0.7758287191390991},
{X=488.66666666666663,Y=460,P=0.785304844379425},
{X=492,Y=460.66666666666663,P=0.6987720131874084},
{X=496,Y=461.3333333333333,P=0.6533879637718201},
{X=500,Y=462,P=0.6456292271614075},
{X=503.3333333333333,Y=462,P=0.6683430075645447},
{X=505.3333333333333,Y=462.66666666666663,P=0.7513852119445801},
{X=510,Y=463.3333333333333,P=0.5926470756530762},
{X=514,Y=464,P=0.621231198310852},
{X=516,Y=464.66666666666663,P=0.7479339241981506},
{X=518,Y=465.3333333333333,P=0.7547337412834167},
{X=520.6666666666666,Y=465.3333333333333,P=0.7243622541427612},
{X=526.6666666666666,Y=466,P=0.5270907282829285},
{X=529.3333333333333,Y=467.3333333333333,P=0.6958931684494019},
{X=534,Y=468,P=0.6012994050979614},
{X=538,Y=468.66666666666663,P=0.6275204420089722},
{X=542.6666666666666,Y=469.3333333333333,P=0.5978454351425171},
{X=546,Y=470,P=0.6662331819534302},
{X=552.6666666666666,Y=470.66666666666663,P=0.5002615451812744},
{X=555.3333333333333,Y=471.3333333333333,P=0.6958120465278625},
{X=562,Y=472,P=0.4994753301143646},
{X=567.3333333333333,Y=472.66666666666663,P=0.5455721616744995},
{X=573.3333333333333,Y=474,P=0.5134470462799072},
{X=580.6666666666666,Y=474.66666666666663,P=0.4518575370311737},
{X=585.3333333333333,Y=475.3333333333333,P=0.5670377612113953},
{X=589.3333333333333,Y=476,P=0.609724223613739},
{X=594.6666666666666,Y=476.66666666666663,P=0.5481007099151611},
{X=601.3333333333333,Y=477.3333333333333,P=0.4885927140712738},
{X=606.6666666666666,Y=478,P=0.5432637929916382},
{X=610,Y=478.66666666666663,P=0.6355723142623901},
{X=616.6666666666666,Y=479.3333333333333,P=0.47008851170539856},
{X=622,Y=480,P=0.5248528122901917},
{X=628.6666666666666,Y=480,P=0.46539586782455444},
{X=636.6666666666666,Y=480.66666666666663,P=0.39966750144958496},
{X=642,Y=480.66666666666663,P=0.509570300579071},
{X=647.3333333333333,Y=482,P=0.5069840550422668},
{X=652,Y=482,P=0.56694495677948},
{X=654,Y=482,P=0.7375491857528687},
{X=662,Y=482.66666666666663,P=0.4425170123577118},
{X=666.6666666666666,Y=482.66666666666663,P=0.5797702074050903},
{X=669.3333333333333,Y=482.66666666666663,P=0.7043377757072449},
{X=676,Y=482.66666666666663,P=0.49506327509880066},
{X=680,Y=482.66666666666663,P=0.6218303442001343},
{X=684.6666666666666,Y=482.66666666666663,P=0.5944092869758606},
{X=689.3333333333333,Y=482.66666666666663,P=0.5880598425865173},
{X=694,Y=482.66666666666663,P=0.5764558911323547},
{X=696,Y=482.66666666666663,P=0.7410839796066284},
{X=700,Y=482.66666666666663,P=0.626220166683197},
{X=704,Y=482.66666666666663,P=0.6229243278503418},
{X=707.3333333333333,Y=482.66666666666663,P=0.6664982438087463},
{X=714.6666666666666,Y=482.66666666666663,P=0.45310068130493164},
{X=720.6666666666666,Y=482.66666666666663,P=0.4970242381095886},
{X=724,Y=482.66666666666663,P=0.6377686858177185},
{X=728.6666666666666,Y=482.66666666666663,P=0.5830304622650146},
{X=733.3333333333333,Y=482,P=0.5852826237678528},
{X=735.3333333333333,Y=482,P=0.7534965872764587},
{X=739.3333333333333,Y=481.3333333333333,P=0.6395262479782104},
{X=742,Y=481.3333333333333,P=0.7220669984817505},
{X=746,Y=480.66666666666663,P=0.6478912830352783},
{X=750,Y=480,P=0.6395424604415894},
{X=754,Y=478.66666666666663,P=0.629923403263092},
{X=759.3333333333333,Y=478,P=0.5632662177085876},
{X=762.6666666666666,Y=477.3333333333333,P=0.6644474864006042},
{X=765.3333333333333,Y=476.66666666666663,P=0.7117174863815308},
{X=768.6666666666666,Y=476,P=0.6776741743087769},
{X=773.3333333333333,Y=474.66666666666663,P=0.5986015200614929},
{X=777.3333333333333,Y=473.3333333333333,P=0.6267250180244446},
{X=781.3333333333333,Y=471.3333333333333,P=0.6099798679351807},
{X=784.6666666666666,Y=470.66666666666663,P=0.6539067625999451},
{X=790.6666666666666,Y=468.66666666666663,P=0.49772119522094727},
{X=792.6666666666666,Y=468,P=0.7275203466415405},
{X=796,Y=466.66666666666663,P=0.6488282680511475},
{X=800,Y=465.3333333333333,P=0.6072457432746887},
{X=804,Y=463.3333333333333,P=0.5964605212211609},
{X=807.3333333333333,Y=462,P=0.6561574339866638},
{X=810,Y=460.66666666666663,P=0.6968104839324951},
{X=813.3333333333333,Y=458.66666666666663,P=0.6486343741416931},
{X=818,Y=456.66666666666663,P=0.5831184983253479},
{X=820.6666666666666,Y=455.3333333333333,P=0.6963025331497192},
{X=822.6666666666666,Y=454,P=0.7399721741676331},
{X=825.3333333333333,Y=452.66666666666663,P=0.7092769145965576},
{X=828.6666666666666,Y=451.3333333333333,P=0.6766396164894104},
{X=833.3333333333333,Y=448.66666666666663,P=0.5740551948547363},
{X=835.3333333333333,Y=447.3333333333333,P=0.7345168590545654},
{X=837.3333333333333,Y=446.66666666666663,P=0.7650047540664673},
{X=840.6666666666666,Y=445.3333333333333,P=0.6749776005744934},
{X=844.6666666666666,Y=442.66666666666663,P=0.5888175368309021},
{X=848.6666666666666,Y=440.66666666666663,P=0.5972996354103088},
{X=852,Y=438.66666666666663,P=0.6244710683822632},
{X=854,Y=437.3333333333333,P=0.7188596725463867},
{X=856.6666666666666,Y=436,P=0.6917470097541809},
{X=858.6666666666666,Y=434.66666666666663,P=0.7351683378219604},
{X=862.6666666666666,Y=432.66666666666663,P=0.6261351704597473},
{X=864.6666666666666,Y=431.3333333333333,P=0.7430726289749146},
{X=866.6666666666666,Y=430.66666666666663,P=0.7724213600158691},
{X=868.6666666666666,Y=429.3333333333333,P=0.7554715871810913},
{X=871.3333333333333,Y=428.66666666666663,P=0.7384610176086426},
{X=875.3333333333333,Y=426.66666666666663,P=0.6147278547286987},
{X=877.3333333333333,Y=426,P=0.7556149363517761},
{X=880,Y=425.3333333333333,P=0.7179256677627563},
{X=882,Y=424.66666666666663,P=0.7604349851608276},
{X=884,Y=424,P=0.7805911302566528},
{X=886,Y=423.3333333333333,P=0.7828847765922546},
{X=888,Y=422.66666666666663,P=0.78639817237854},
{X=890,Y=422,P=0.7878957390785217},
{X=892,Y=421.3333333333333,P=0.7883377075195312},
{X=894,Y=421.3333333333333,P=0.7963496446609497},
{X=896,Y=421.3333333333333,P=0.7972534894943237},
{X=898,Y=420.66666666666663,P=0.7801092267036438},
{X=900,Y=420.66666666666663,P=0.7869842052459717},
{X=902,Y=420.66666666666663,P=0.7963632345199585},
{X=904,Y=420,P=0.7890701293945312},
{X=906,Y=420,P=0.7968790531158447},
{X=908,Y=420,P=0.8703685998916626},
""";

var stylusPointCollection = new StylusPointCollection();

var stringReader = new StringReader(test);
while (stringReader.ReadLine() is { } line)
{
var regex = GetPointInfoRegex();
var match = regex.Match(line);
if (match.Success)
{
var x = match.Groups[1].Value;
var y = match.Groups[2].Value;
var p = match.Groups[3].Value;
// Do something with the extracted values
//Console.WriteLine($"X: {x}, Y: {y}, P: {p}");
stylusPointCollection.Add(new StylusPoint(double.Parse(x), double.Parse(y), float.Parse(p)));
}
else
{
Console.WriteLine("No match found.");
}
}

return stylusPointCollection;
}

[GeneratedRegex(@"\{X=(?-?\d+(?:\.\d+)?),\s*Y=(?-?\d+(?:\.\d+)?),\s*P=(?

-?\d+(?:\.\d+)?)}\,?")]
private static partial Regex GetPointInfoRegex();
```

Running this code shows that calling `incrementalStrokeHitTester.AddPoint(point)` triggers the StrokeHit event, but `StrokeHitEventArgs.GetPointEraseResults()` returns a StrokeCollection with 0 strokes—meaning the whole stroke was erased when only a split should occur at XY=(684,446), WH=(50,70).

To make this issue clearer, I’ve visualized both the stroke and eraser range in the UI with the following code:

```csharp
var geometry = stroke.GetGeometry();
Canvas.Children.Add(new Path()
{
Data = geometry,
Fill = Brushes.Black
});
Canvas.Children.Add(new Rectangle()
{
Margin = new Thickness(x, y, 0, 0),
Width = rectangleStylusShape.Width,
Height = rectangleStylusShape.Height,
Stroke = Brushes.Red,
StrokeThickness = 2
});

var eraseResult = stroke.GetEraseResult(new Rect(x,y, rectangleStylusShape.Width, rectangleStylusShape.Height));
if (eraseResult.Count > 0)
{
// It can get the correct result from GetEraseResult
}
```

The result looks like this:

Image

This demonstrates that IncrementalStrokeHitTester has a calculation bug in this scenario.

### Reproduction Steps

Clone my demo project code: https://github.com/lindexi/lindexi_gd/tree/7f7f914c05829be1feb090ab05b0514df4e46e97/WPFDemo/RarlereninemniBohinabairhalljere

You can clone my project by the command line:

```
mkdir RarlereninemniBohinabairhalljere && cd RarlereninemniBohinabairhalljere
git init
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 7f7f914c05829be1feb090ab05b0514df4e46e97
cd WPFDemo/RarlereninemniBohinabairhalljere
```

Run the project in VisualStudio, and you can find the VisualStudio enter the break point.

### Expected behavior

The IncrementalStrokeHitTester can split into two segments.

### Actual behavior

The IncrementalStrokeHitTester erases the entire stroke. As the code shows, we can find it can get the correct result from Stroke.GetEraseResult

### Regression?

_No response_

### Known Workarounds

_No response_

### Impact

_No response_

### Configuration

_No response_

### Other information

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.