diff --git a/doc/api/api_changes/2017-02-26-MB_intersects_bbox.rst b/doc/api/api_changes/2017-02-26-MB_intersects_bbox.rst new file mode 100644 index 000000000000..ed866b219362 --- /dev/null +++ b/doc/api/api_changes/2017-02-26-MB_intersects_bbox.rst @@ -0,0 +1,21 @@ +Path.intersects_bbox always treats the bounding box as filled +````````````````````````````````````````````````````````````` + +Previously, when ``Path.intersects_bbox`` was called with ``filled`` set to +``False``, it would treat both the path and the bounding box as unfilled. This +behavior was not well documented and it is usually not the desired behavior, +since bounding boxes are used to represent more complex shapes located inside +the bounding box. This behavior has now been changed: when ``filled`` is +``False``, the path will be treated as unfilled, but the bounding box is still +treated as filled. The old behavior was arguably an implementation bug. + +When ``Path.intersects_bbox`` is called with ``filled`` set to ``True`` +(the default value), there is no change in behavior. For those rare cases where +``Path.intersects_bbox`` was called with ``filled`` set to ``False`` and where +the old behavior is actually desired, the suggested workaround is to call +``Path.intersects_path`` with a rectangle as the path:: + + from matplotlib.path import Path + from matplotlib.transforms import Bbox, BboxTransformTo + rect = Path.unit_rectangle().transformed(BboxTransformTo(bbox)) + result = path.intersects_path(rect, filled=False) diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 58e984d2cc5d..73754f069ad9 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -558,14 +558,13 @@ def intersects_bbox(self, bbox, filled=True): :class:`~matplotlib.transforms.Bbox`. *filled*, when True, treats the path as if it was filled. - That is, if one path completely encloses the other, - :meth:`intersects_path` will return True. + That is, if the path completely encloses the bounding box, + :meth:`intersects_bbox` will return True. + + The bounding box is always considered filled. """ - from .transforms import BboxTransformTo - rectangle = self.unit_rectangle().transformed( - BboxTransformTo(bbox)) - result = self.intersects_path(rectangle, filled) - return result + return _path.path_intersects_rectangle(self, + bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled) def interpolated(self, steps): """ diff --git a/src/_path.h b/src/_path.h index e847210ea1db..b7076c6712c3 100644 --- a/src/_path.h +++ b/src/_path.h @@ -872,6 +872,66 @@ bool path_intersects_path(PathIterator1 &p1, PathIterator2 &p2) return false; } +// returns whether the segment from (x1,y1) to (x2,y2) +// intersects the rectangle centered at (cx,cy) with size (w,h) +// see doc/segment_intersects_rectangle.svg for a more detailed explanation +inline bool segment_intersects_rectangle(double x1, double y1, + double x2, double y2, + double cx, double cy, + double w, double h) +{ + return fabs(x1 + x2 - 2.0 * cx) < fabs(x1 - x2) + w && + fabs(y1 + y2 - 2.0 * cy) < fabs(y1 - y2) + h && + 2.0 * fabs((x1 - cx) * (y1 - y2) - (y1 - cy) * (x1 - x2)) < + w * fabs(y1 - y2) + h * fabs(x1 - x2); +} + +template +bool path_intersects_rectangle(PathIterator &path, + double rect_x1, double rect_y1, + double rect_x2, double rect_y2, + bool filled) +{ + typedef PathNanRemover no_nans_t; + typedef agg::conv_curve curve_t; + + if (path.total_vertices() == 0) { + return false; + } + + no_nans_t no_nans(path, true, path.has_curves()); + curve_t curve(no_nans); + + double cx = (rect_x1 + rect_x2) * 0.5, cy = (rect_y1 + rect_y2) * 0.5; + double w = fabs(rect_x1 - rect_x2), h = fabs(rect_y1 - rect_y2); + double xmin = std::min(rect_x1, rect_x2), xmax = std::max(rect_x1, rect_x2); + double ymin = std::min(rect_x1, rect_x2), ymax = std::max(rect_x1, rect_x2); + + double x1, y1, x2, y2; + + curve.vertex(&x1, &y1); + if (2.0 * fabs(x1 - cx) <= w && 2.0 * fabs(y1 - cy) <= h) { + return true; + } + + while (curve.vertex(&x2, &y2) != agg::path_cmd_stop) { + if (segment_intersects_rectangle(x1, y1, x2, y2, cx, cy, w, h)) { + return true; + } + x1 = x2; + y1 = y2; + } + + if (filled) { + agg::trans_affine trans; + if (point_in_path(cx, cy, 0.0, path, trans)) { + return true; + } + } + + return false; +} + template void convert_path_to_polygons(PathIterator &path, agg::trans_affine &trans, diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 047e8c5c3107..e19b85038161 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -521,6 +521,39 @@ static PyObject *Py_path_intersects_path(PyObject *self, PyObject *args, PyObjec } } +const char *Py_path_intersects_rectangle__doc__ = "path_intersects_rectangle(path, rect_x1, rect_y1, rect_x2, rect_y2, filled=False)"; + +static PyObject *Py_path_intersects_rectangle(PyObject *self, PyObject *args, PyObject *kwds) +{ + py::PathIterator path; + double rect_x1, rect_y1, rect_x2, rect_y2; + int filled = 0; + const char *names[] = { "path", "rect_x1", "rect_y1", "rect_x2", "rect_y2", "filled", NULL }; + bool result; + + if (!PyArg_ParseTupleAndKeywords(args, + kwds, + "O&dddd|i:path_intersects_rectangle", + (char **)names, + &convert_path, + &path, + &rect_x1, + &rect_y1, + &rect_x2, + &rect_y2, + &filled)) { + return NULL; + } + + CALL_CPP("path_intersects_rectangle", (result = path_intersects_rectangle(path, rect_x1, rect_y1, rect_x2, rect_y2, filled))); + + if (result) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + const char *Py_convert_path_to_polygons__doc__ = "convert_path_to_polygons(path, trans, width=0, height=0)"; @@ -819,6 +852,7 @@ extern "C" { {"affine_transform", (PyCFunction)Py_affine_transform, METH_VARARGS, Py_affine_transform__doc__}, {"count_bboxes_overlapping_bbox", (PyCFunction)Py_count_bboxes_overlapping_bbox, METH_VARARGS, Py_count_bboxes_overlapping_bbox__doc__}, {"path_intersects_path", (PyCFunction)Py_path_intersects_path, METH_VARARGS|METH_KEYWORDS, Py_path_intersects_path__doc__}, + {"path_intersects_rectangle", (PyCFunction)Py_path_intersects_rectangle, METH_VARARGS|METH_KEYWORDS, Py_path_intersects_rectangle__doc__}, {"convert_path_to_polygons", (PyCFunction)Py_convert_path_to_polygons, METH_VARARGS|METH_KEYWORDS, Py_convert_path_to_polygons__doc__}, {"cleanup_path", (PyCFunction)Py_cleanup_path, METH_VARARGS, Py_cleanup_path__doc__}, {"convert_to_string", (PyCFunction)Py_convert_to_string, METH_VARARGS, Py_convert_to_string__doc__}, diff --git a/src/doc/segment_intersects_rectangle.svg b/src/doc/segment_intersects_rectangle.svg new file mode 100644 index 000000000000..6c5f82fc2bac --- /dev/null +++ b/src/doc/segment_intersects_rectangle.svg @@ -0,0 +1,1689 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + We want to check whether a segmentof a path intersects a rectangle. + We do this by checking whether theMinkowski difference contains the origin. + (x1,y1) + (x2,y2) + (cx,cy) + + + w + h + + + + + + + + + + + + + + + + + + + + + (x1-cx,y1-cy) + ((x1+x2)/2 - cx, + (y1+y2)/2 - cy) + + + (x2-cx,y2-cy) + + + w/2 + + + + + + + h/2 + + + + + + + + w/2 + + + + + + h/2 + + + + + w/2 + + + + + + + h/2 + + + + + + + + w/2 + + + + + + h/2 + + + + + The Minkowski difference can be constructed as the intersection of three regions.The segment intersects the rectangle if and only if the origin is inside all three regions. + + + + + + + + + ((x1+x2)/2 - cx, + (y1+y2)/2 - cy) + + + + + abs((x1+x2)/2 - cx) < (abs(x1-x2) + w)/2 + abs((x1-cx) * (y1-y2) - (y1-cy) * (x1-x2)) + abs((y1+y2)/2 - cy) < (abs(y1-y2) + h)/2 + h + abs(y1-y2) + w + + + + + + + abs(x1-x2) + + + + + + + + + + + + + + + + + (0,0) + (0,0) + < w/2 * abs(y1-y2) + h/2 * abs(x1-x2) + +