Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/api/api_changes/2017-02-26-MB_intersects_bbox.rst
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 6 additions & 7 deletions lib/matplotlib/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
60 changes: 60 additions & 0 deletions src/_path.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 34 additions & 0 deletions src/_path_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)";

Expand Down Expand Up @@ -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__},
Expand Down
Loading