Search code examples
c#.net-4.8

Rect.Contains on boundary points


A somewhat intermittent issue with supposedly Rect.Contains(Point) method. To be exact, it only happened once and I haven't been able to reproduce it ever since. There's a block of code that among other things finds an upper bound of a group of points such that the first point in group and the upper boundary point are within a rect of a certain size.

List<Point> points = ...; // a list of points order by X
double groupSize = 10000.0;
while (points.Any())
{
    double left = points[0].X;
    Rect searchBound = new Rect(left, points[0].Y - groupSize, groupSize, 2.0 * groupSize);
    double top = points
        .Where(o => searchBound.Contains(o))
        .Min(o => o.Y);
        
    ...
}

The problem is Min() throwing an InvalidOperationException: the sequence contains no elements. But the searchBound at least should contain the first point in sequence that is located exactly on the left boundary. Am I missing something?

I've run a series of tests with random points generator but never been able to reproduce the exception. The Rect.Contains(Point) docs also do not state anything about the boundary points.


Solution

  • The method of

            /// <summary>
            /// ContainsInternal - Performs just the "point inside" logic
            /// </summary>
            /// <returns>
            /// bool - true if the point is inside the rect
            /// </returns>
            /// <param name="x"> The x-coord of the point to test </param>
            /// <param name="y"> The y-coord of the point to test </param>
            private bool ContainsInternal(double x, double y)
            {
                // We include points on the edge as "contained".
                // We do "x - _width <= _x" instead of "x <= _x + _width"
                // so that this check works when _width is PositiveInfinity
                // and _x is NegativeInfinity.
                return ((x >= _x) && (x - _width <= _x) &&
                        (y >= _y) && (y - _height <= _y));
            }
    

    is being called from Contains at:

            /// <summary>
            /// Contains - Returns true if the Point represented by x,y is within the rectangle inclusive of the edges.
            /// Returns false otherwise.
            /// </summary>
            /// <param name="x"> X coordinate of the point which is being tested </param>
            /// <param name="y"> Y coordinate of the point which is being tested </param>
            /// <returns>
            /// Returns true if the Point represented by x,y is within the rectangle.
            /// Returns false otherwise.
            /// </returns>
            public bool Contains(double x, double y)
            {
                if (IsEmpty)
                {
                    return false;
                }
     
                return ContainsInternal(x,y);
            }
    

    We can see that it receives a double for x and one for y and it really does the comparison we would expect in ContainsInternal and Contains, the method you are calling calls ContainsInternal as long as IsEmpty is false. IsEmpty is

            /// <summary>
            /// IsEmpty - this returns true if this rect is the Empty rectangle.
            /// Note: If width or height are 0 this Rectangle still contains a 0 or 1 dimensional set
            /// of points, so this method should not be used to check for 0 area.
            /// </summary>
            public bool IsEmpty
            {
                get
                {
                    // The funny width and height tests are to handle NaNs
                    Debug.Assert((!(_width < 0) && !(_height < 0)) || (this == Empty));
     
                    return _width < 0;
                }
            }
    

    We see that the size of _width is being checked for being positive, but the _height is not being checked for.

    So, the possible causes for your problem seem to be:

    • height might have been negative
    • _x or _y might have had some strange values
    • maybe you expected a point to be in the rect but it was not really inside (it may happen)

    So, you would need to make sure that you test your rect with all kinds of values, among which:

    • point surely inside the rect
    • point on the edge, but not on the corner of the rect
    • point on the corner of the rect (all of top-left, top-right, bottom-left, bottom-right)
    • funny values, such as NaN

    if all your tests are passed, then it's perfectly valid to consider this as a non-issue unless it reoccurs at some point.