Skip to content

Bug in calculation algorithm of IncrementalStrokeHitTester #11059

@lindexi

Description

@lindexi

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 ):

        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=(?<X>-?\d+(?:\.\d+)?),\s*Y=(?<Y>-?\d+(?:\.\d+)?),\s*P=(?<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:

        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

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugProduct bug (most likely)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions