0

I have a simple rectangular PathGeometry and want to test if the point is inside the PathGeometry. The obvious way is to call FillContains but it doesn't work as expected. There is also an overload function which has a tolerance parameter, although by adjusting the tolerance to high values FillContains may return true but due to the given tolerance calling FillContains on other geometries may also return true.

So I wrote this extension method to have a correct FillContains for this specific rectangular PathGemoetry:

    public static bool Contains(this PathGeometry geo, Point pt)
    {
        var match = System.Text.RegularExpressions.Regex.Match(geo.Figures.ToString(), @"M(\d*.\d*),(\d*.\d*)L(\d*.\d*),(\d*.\d*) (\d*.\d*),(\d*.\d*) (\d*.\d*),(\d*.\d*)z");

        float ulx = float.Parse(match.Groups[1].Value);
        float uly = float.Parse(match.Groups[2].Value);

        float urx = float.Parse(match.Groups[3].Value);
        float ury = float.Parse(match.Groups[4].Value);

        float lrx = float.Parse(match.Groups[5].Value);
        float lry = float.Parse(match.Groups[6].Value);

        float llx = float.Parse(match.Groups[7].Value);
        float lly = float.Parse(match.Groups[8].Value);

        Rect rect = new Rect(ulx, uly, urx - ulx, lly - uly);

        return rect.Contains(pt);
    }

And the result for a sample:

// Point: {188.981887817383,507.910125732422}
// Region: M188.759994506836,501.910003662109L216.580001831055,501.910003662109 216.580001831055,511.910003662109 188.759994506836,511.910003662109z

// returns false
var test1 = region.FillContains(pt);

// returns true
var test2  = region.Contains(pt);

Since I have lots of such PathGemoetry objects then is there any better implementation of mine for faster hit testing or is there anything I missed while using FillContains resulting in unexpected result?

Edit

Just noticed that my PathGeometry had a Transform applied to it which result in point not fit inside.

I fixed it by using this to bypass the Transform in hit-testing:

PathGeometry.Parse(region.Figures.ToString()).FillContains(pt)
Mohsen Afshin
  • 13,273
  • 10
  • 65
  • 90
  • FillContains works for me with your test data. How do you actually create the PathGeometry from your Region data? In XAML it would produce a StreamGeometry, not a PathGeometry. – Clemens Mar 02 '15 at 20:16
  • @Clemens, silly mistake, there is a `Transform` applied to the geometry, look at my edit – Mohsen Afshin Mar 02 '15 at 20:29
  • 1
    Maybe more efficient: `region.FillContains(region.Transform.Transform(pt))`. – Clemens Mar 02 '15 at 20:31

2 Answers2

0

Have you tried

VisualTreeHelper.HitTest(Visual reference,  Point point)

VisualTreeHelper.HitTest

Hit Testing Using Geometry

Michal Ciechan
  • 13,492
  • 11
  • 76
  • 118
0

In order to solve the same problem, I wrote the following functions:

public static Drawing? GeometryHitTest( DrawingGroup drawingGroup, Point point )
{
    if( drawingGroup.Transform != null )
        point = drawingGroup.Transform.Inverse!.Transform( point );
    for( int i = drawingGroup.Children.Count - 1; i >= 0; i-- )
    {
        Drawing? found = GeometryHitTest( drawingGroup.Children[i], point );
        if( found != null )
            return found;
    }
    return null;
}

public static Drawing? GeometryHitTest( Drawing drawing, Point point ) //
    => drawing switch
    {
        DrawingGroup group => GeometryHitTest( group, point ),
        GeometryDrawing geometryDrawing => GeometryHitTest( geometryDrawing, point ),
        GlyphRunDrawing geometryDrawing => GeometryHitTest( geometryDrawing, point ),
        _ => throw new Exception()
    };

private static readonly Pen hitTestPen = new( new SolidColorBrush( Colors.Red ), 5 );

public static Drawing? GeometryHitTest( GeometryDrawing geometryDrawing, Point point ) //
    => geometryDrawing.Geometry.FillContains( point ) || //
            geometryDrawing.Geometry.StrokeContains( hitTestPen, point ) ?
                geometryDrawing : null;

public static Drawing? GeometryHitTest( GlyphRunDrawing glyphRunDrawing, Point point ) //
    => glyphRunDrawing.Bounds.Contains( point ) ? glyphRunDrawing : null;

Call them like this:

DrawingGroup drawingGroup = VisualTreeHelper.GetDrawing( visual ) ?? throw new Exception();
Drawing? drawing = GeometryHitTest( drawingGroup, point );

The important bits are as follows:

  • Follow transforms with the following lines:

    if( drawingGroup.Transform != null )
        point = drawingGroup.Transform.Inverse!.Transform( point );
    

    If there is a possibility that you have transforms attached not only to DrawingGroup but also to GeometryDrawing and GlyphRunDrawing, then you should add similar code in the respective functions that handle individual drawing types.

  • Visit children from the top of the z-order towards the bottom with the following line:

    for( int i = drawingGroup.Children.Count - 1; i >= 0; i-- )
    

Another thing worth noting is the hitTestPen. I had to declare this otherwise the hit-testing would never succeed on the stroke of a geometry. I am not sure why. I am still researching this to find some solution better than a hard-coded pen-width of 5.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142