Implement Path.intersects_bbox in C++ to speed up legend positioning. #8224

Merged
merged 2 commits into from Mar 8, 2017
@@ -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)
View
@@ -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):
"""
View
@@ -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 <class PathIterator>
+bool path_intersects_rectangle(PathIterator &path,
+ double rect_x1, double rect_y1,
+ double rect_x2, double rect_y2,
+ bool filled)
+{
+ typedef PathNanRemover<py::PathIterator> no_nans_t;
+ typedef agg::conv_curve<no_nans_t> 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 <class PathIterator>
void convert_path_to_polygons(PathIterator &path,
agg::trans_affine &trans,
View
@@ -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__},
Oops, something went wrong.