From f60aa887dc2e1698bde6d9009f6a79d364f4a034 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Sat, 16 Jul 2016 13:35:16 -0400 Subject: [PATCH 1/2] Support returning polylines from to_polygons --- lib/matplotlib/path.py | 34 ++++++++++++++------ lib/matplotlib/tests/test_path.py | 21 ++++++++++++ src/_path.h | 53 +++++++++++++++++-------------- src/_path_wrapper.cpp | 39 ++++++++++------------- 4 files changed, 91 insertions(+), 56 deletions(-) diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 17f3ee0de332..d5b0763e7736 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -582,17 +582,25 @@ def interpolated(self, steps): new_codes = None return Path(vertices, new_codes) - def to_polygons(self, transform=None, width=0, height=0): + def to_polygons(self, transform=None, width=0, height=0, closed_only=True): """ - Convert this path to a list of polygons. Each polygon is an - Nx2 array of vertices. In other words, each polygon has no - ``MOVETO`` instructions or curves. This is useful for - displaying in backends that do not support compound paths or - Bezier curves, such as GDK. + Convert this path to a list of polygons or polylines. Each + polygon/polyline is an Nx2 array of vertices. In other words, + each polygon has no ``MOVETO`` instructions or curves. This + is useful for displaying in backends that do not support + compound paths or Bezier curves, such as GDK. If *width* and *height* are both non-zero then the lines will be simplified so that vertices outside of (0, 0), (width, height) will be clipped. + + If *closed_only* is `True` (default), only closed polygons, + with the last point being the same as the first point, will be + returned. Any unclosed polylines in the path will be + explicitly closed. If *closed_only* is `False`, any unclosed + polygons in the path will be returned as unclosed polygons, + and the closed polygons will be returned explicitly closed by + setting the last point to the same as the first point. """ if len(self.vertices) == 0: return [] @@ -601,14 +609,22 @@ def to_polygons(self, transform=None, width=0, height=0): transform = transform.frozen() if self.codes is None and (width == 0 or height == 0): + vertices = self.vertices + if closed_only: + if len(vertices) < 3: + return [] + elif np.any(vertices[0] != vertices[-1]): + vertices = list(vertices) + [vertices[0]] + if transform is None: - return [self.vertices] + return [vertices] else: - return [transform.transform(self.vertices)] + return [transform.transform(vertices)] # Deal with the case where there are curves and/or multiple # subpaths (using extension code) - return _path.convert_path_to_polygons(self, transform, width, height) + return _path.convert_path_to_polygons( + self, transform, width, height, closed_only) _unit_rectangle = None diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py index 229b5cfeacfc..107dd5abe238 100644 --- a/lib/matplotlib/tests/test_path.py +++ b/lib/matplotlib/tests/test_path.py @@ -5,6 +5,8 @@ import numpy as np +from numpy.testing import assert_array_equal + from matplotlib.path import Path from matplotlib.patches import Polygon from nose.tools import assert_raises, assert_equal @@ -149,6 +151,25 @@ def test_path_no_doubled_point_in_to_polygon(): assert np.all(poly_clipped[-1] == poly_clipped[0]) +def test_path_to_polygons(): + data = [[10, 10], [20, 20]] + p = Path(data) + + assert_array_equal(p.to_polygons(width=40, height=40), []) + assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), [data]) + assert_array_equal(p.to_polygons(), []) + assert_array_equal(p.to_polygons(closed_only=False), [data]) + + data = [[10, 10], [20, 20], [30, 30]] + closed_data = [[10, 10], [20, 20], [30, 30], [10, 10]] + p = Path(data) + + assert_array_equal(p.to_polygons(width=40, height=40), [closed_data]) + assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), [data]) + assert_array_equal(p.to_polygons(), [closed_data]) + assert_array_equal(p.to_polygons(closed_only=False), [data]) + + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/src/_path.h b/src/_path.h index bb730be71ff2..e847210ea1db 100644 --- a/src/_path.h +++ b/src/_path.h @@ -40,6 +40,28 @@ struct XY } }; +typedef std::vector Polygon; + +void _finalize_polygon(std::vector &result, int closed_only) +{ + if (result.size() == 0) { + return; + } + + Polygon &polygon = result.back(); + + /* Clean up the last polygon in the result. */ + if (polygon.size() == 0) { + result.pop_back(); + } else if (closed_only) { + if (polygon.size() < 3) { + result.pop_back(); + } else if (polygon.front() != polygon.back()) { + polygon.push_back(polygon.front()); + } + } +} + // // The following function was found in the Agg 2.3 examples (interactive_polygon.cpp). // It has been generalized to work on (possibly curved) polylines, rather than @@ -509,8 +531,6 @@ bool path_in_path(PathIterator1 &a, http://en.wikipedia.org/wiki/Sutherland-Hodgman_clipping_algorithm */ -typedef std::vector Polygon; - namespace clip_to_rect_filters { /* There are four different passes needed to create/remove @@ -696,9 +716,12 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vecto // Empty polygons aren't very useful, so skip them if (polygon1.size()) { + _finalize_polygon(results, 1); results.push_back(polygon1); } } while (code != agg::path_cmd_stop); + + _finalize_polygon(results, 1); } template @@ -849,30 +872,12 @@ bool path_intersects_path(PathIterator1 &p1, PathIterator2 &p2) return false; } -void _finalize_polygon(std::vector &result) -{ - Polygon &polygon = result.back(); - - if (result.size() == 0) { - return; - } - - /* Clean up the last polygon in the result. If less than a - triangle, remove it. */ - if (polygon.size() < 3) { - result.pop_back(); - } else { - if (polygon.front() != polygon.back()) { - polygon.push_back(polygon.front()); - } - } -} - template void convert_path_to_polygons(PathIterator &path, agg::trans_affine &trans, double width, double height, + int closed_only, std::vector &result) { typedef agg::conv_transform transformed_path_t; @@ -897,12 +902,12 @@ void convert_path_to_polygons(PathIterator &path, while ((code = curve.vertex(&x, &y)) != agg::path_cmd_stop) { if ((code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly) { - _finalize_polygon(result); + _finalize_polygon(result, 1); result.push_back(Polygon()); polygon = &result.back(); } else { if (code == agg::path_cmd_move_to) { - _finalize_polygon(result); + _finalize_polygon(result, closed_only); result.push_back(Polygon()); polygon = &result.back(); } @@ -910,7 +915,7 @@ void convert_path_to_polygons(PathIterator &path, } } - _finalize_polygon(result); + _finalize_polygon(result, closed_only); } template diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 45c1e288e86c..28fbaf282b57 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -15,23 +15,11 @@ PyObject *convert_polygon_vector(std::vector &polygons) npy_intp dims[2]; dims[1] = 2; - if (poly.front() != poly.back()) { - /* Make last point same as first, if not already */ - dims[0] = (npy_intp)poly.size() + 1; - fix_endpoints = true; - } else { - dims[0] = (npy_intp)poly.size(); - fix_endpoints = false; - } + dims[0] = (npy_intp)poly.size(); numpy::array_view subresult(dims); memcpy(subresult.data(), &poly[0], sizeof(double) * poly.size() * 2); - if (fix_endpoints) { - subresult(poly.size(), 0) = poly.front().x; - subresult(poly.size(), 1) = poly.front().y; - } - if (PyList_SetItem(pyresult, i, subresult.pyobj())) { Py_DECREF(pyresult); return NULL; @@ -542,21 +530,26 @@ static PyObject *Py_convert_path_to_polygons(PyObject *self, PyObject *args, PyO py::PathIterator path; agg::trans_affine trans; double width = 0.0, height = 0.0; + int closed_only = 1; std::vector result; + const char *names[] = { "path", "transform", "width", "height", "closed_only", NULL }; - if (!PyArg_ParseTuple(args, - "O&O&|dd:convert_path_to_polygons", - &convert_path, - &path, - &convert_trans_affine, - &trans, - &width, - &height)) { + if (!PyArg_ParseTupleAndKeywords(args, + kwds, + "O&O&|ddi:convert_path_to_polygons", + (char **)names, + &convert_path, + &path, + &convert_trans_affine, + &trans, + &width, + &height, + &closed_only)) { return NULL; } CALL_CPP("convert_path_to_polygons", - (convert_path_to_polygons(path, trans, width, height, result))); + (convert_path_to_polygons(path, trans, width, height, closed_only, result))); return convert_polygon_vector(result); } @@ -827,7 +820,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__}, - {"convert_path_to_polygons", (PyCFunction)Py_convert_path_to_polygons, METH_VARARGS, Py_convert_path_to_polygons__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__}, {"is_sorted", (PyCFunction)Py_is_sorted, METH_O, Py_is_sorted__doc__}, From d02b01dfacc6ed07a62da9b8e2f3c7d4e3c6abb6 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Sat, 16 Jul 2016 15:17:57 -0400 Subject: [PATCH 2/2] PEP8 --- lib/matplotlib/tests/test_path.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py index 107dd5abe238..5e31a919a58a 100644 --- a/lib/matplotlib/tests/test_path.py +++ b/lib/matplotlib/tests/test_path.py @@ -156,7 +156,8 @@ def test_path_to_polygons(): p = Path(data) assert_array_equal(p.to_polygons(width=40, height=40), []) - assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), [data]) + assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), + [data]) assert_array_equal(p.to_polygons(), []) assert_array_equal(p.to_polygons(closed_only=False), [data]) @@ -165,7 +166,8 @@ def test_path_to_polygons(): p = Path(data) assert_array_equal(p.to_polygons(width=40, height=40), [closed_data]) - assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), [data]) + assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), + [data]) assert_array_equal(p.to_polygons(), [closed_data]) assert_array_equal(p.to_polygons(closed_only=False), [data])