From 2bdc7860c1a945abc4e9061e3149c7510c11c306 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 23 Jun 2025 20:13:36 -0400 Subject: [PATCH 1/8] Make path-to-string conversion a little safer ... by replacing double pointers by fixed-size `std::array`, or a return `tuple`. With gcc (and optimization enabled?), this has no effect on code size, but gives compile-time (and better runtime) checks that there are no out-of-bounds access. --- src/_path.h | 52 +++++++++++++++++++++---------------------- src/_path_wrapper.cpp | 7 +----- 2 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/_path.h b/src/_path.h index c03703776760..6f315f2109e1 100644 --- a/src/_path.h +++ b/src/_path.h @@ -3,12 +3,12 @@ #ifndef MPL_PATH_H #define MPL_PATH_H -#include -#include -#include -#include #include +#include +#include +#include #include +#include #include "agg_conv_contour.h" #include "agg_conv_curve.h" @@ -1051,15 +1051,14 @@ void cleanup_path(PathIterator &path, void quad2cubic(double x0, double y0, double x1, double y1, double x2, double y2, - double *outx, double *outy) + std::array &outx, std::array &outy) { - - outx[0] = x0 + 2./3. * (x1 - x0); - outy[0] = y0 + 2./3. * (y1 - y0); - outx[1] = outx[0] + 1./3. * (x2 - x0); - outy[1] = outy[0] + 1./3. * (y2 - y0); - outx[2] = x2; - outy[2] = y2; + std::get<0>(outx) = x0 + 2./3. * (x1 - x0); + std::get<0>(outy) = y0 + 2./3. * (y1 - y0); + std::get<1>(outx) = std::get<0>(outx) + 1./3. * (x2 - x0); + std::get<1>(outy) = std::get<0>(outy) + 1./3. * (y2 - y0); + std::get<2>(outx) = x2; + std::get<2>(outy) = y2; } @@ -1104,27 +1103,27 @@ void __add_number(double val, char format_code, int precision, template bool __convert_to_string(PathIterator &path, int precision, - char **codes, + const std::array &codes, bool postfix, std::string& buffer) { const char format_code = 'f'; - double x[3]; - double y[3]; + std::array x; + std::array y; double last_x = 0.0; double last_y = 0.0; unsigned code; - while ((code = path.vertex(&x[0], &y[0])) != agg::path_cmd_stop) { + while ((code = path.vertex(&std::get<0>(x), &std::get<0>(y))) != agg::path_cmd_stop) { if (code == CLOSEPOLY) { - buffer += codes[4]; + buffer += std::get<4>(codes); } else if (code < 5) { size_t size = NUM_VERTICES[code]; for (size_t i = 1; i < size; ++i) { - unsigned subcode = path.vertex(&x[i], &y[i]); + unsigned subcode = path.vertex(&x.at(i), &y.at(i)); if (subcode != code) { return false; } @@ -1133,29 +1132,29 @@ bool __convert_to_string(PathIterator &path, /* For formats that don't support quad curves, convert to cubic curves */ if (code == CURVE3 && codes[code - 1][0] == '\0') { - quad2cubic(last_x, last_y, x[0], y[0], x[1], y[1], x, y); + quad2cubic(last_x, last_y, x.at(0), y.at(0), x.at(1), y.at(1), x, y); code++; size = 3; } if (!postfix) { - buffer += codes[code - 1]; + buffer += codes.at(code - 1); buffer += ' '; } for (size_t i = 0; i < size; ++i) { - __add_number(x[i], format_code, precision, buffer); + __add_number(x.at(i), format_code, precision, buffer); buffer += ' '; - __add_number(y[i], format_code, precision, buffer); + __add_number(y.at(i), format_code, precision, buffer); buffer += ' '; } if (postfix) { - buffer += codes[code - 1]; + buffer += codes.at(code - 1); } - last_x = x[size - 1]; - last_y = y[size - 1]; + last_x = x.at(size - 1); + last_y = y.at(size - 1); } else { // Unknown code value return false; @@ -1174,7 +1173,7 @@ bool convert_to_string(PathIterator &path, bool simplify, SketchParams sketch_params, int precision, - char **codes, + const std::array &codes, bool postfix, std::string& buffer) { @@ -1211,7 +1210,6 @@ bool convert_to_string(PathIterator &path, sketch_t sketch(curve, sketch_params.scale, sketch_params.length, sketch_params.randomness); return __convert_to_string(sketch, precision, codes, postfix, buffer); } - } template diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 2a297e49ac92..6375351cb52e 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -252,16 +252,11 @@ static py::object Py_convert_to_string(mpl::PathIterator path, agg::trans_affine trans, agg::rect_d cliprect, std::optional simplify, SketchParams sketch, int precision, - std::array codes_obj, bool postfix) + const std::array &codes, bool postfix) { - char *codes[5]; std::string buffer; bool status; - for (auto i = 0; i < 5; ++i) { - codes[i] = const_cast(codes_obj[i].c_str()); - } - if (!simplify.has_value()) { simplify = path.should_simplify(); } From 6d5dd9a5947058644603fafd2d67bc952ec41db8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 23 Jun 2025 20:21:35 -0400 Subject: [PATCH 2/8] Make path clipping a bit safer ... by avoiding double pointers. From 5ba391b8723753b06a55a6e507309445e79accb8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 9 Sep 2025 03:58:15 -0400 Subject: [PATCH 3/8] MNT: Fix type of _finalize_polygon::closed_only It is `bool` for the Python wrapper, while internally `int`, but can be `bool` consistently. Also mark it as `inline` since it's used in a template and the compiler warns about a possible ODR violation (which isn't a problem since it's only used in one file.) --- src/_path.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/_path.h b/src/_path.h index 6f315f2109e1..dc9058535f5f 100644 --- a/src/_path.h +++ b/src/_path.h @@ -43,7 +43,8 @@ struct XY typedef std::vector Polygon; -void _finalize_polygon(std::vector &result, int closed_only) +inline void +_finalize_polygon(std::vector &result, bool closed_only) { if (result.size() == 0) { return; @@ -691,12 +692,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); + _finalize_polygon(results, true); results.push_back(polygon1); } } while (code != agg::path_cmd_stop); - _finalize_polygon(results, 1); + _finalize_polygon(results, true); } template @@ -956,7 +957,7 @@ void convert_path_to_polygons(PathIterator &path, agg::trans_affine &trans, double width, double height, - int closed_only, + bool closed_only, std::vector &result) { typedef agg::conv_transform transformed_path_t; @@ -980,7 +981,7 @@ 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, 1); + _finalize_polygon(result, true); polygon = &result.emplace_back(); } else { if (code == agg::path_cmd_move_to) { From 3f920bf45785fc7a854b82f19ec0fa774dd2f2da Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 9 Sep 2025 04:56:54 -0400 Subject: [PATCH 4/8] path: Simplify extent_limits implementation a bit By using the existing `XY` type to replace x/y pairs, and taking advantage of struct methods. --- src/_path.h | 70 ++++++++++++++++++++----------------------- src/_path_wrapper.cpp | 12 ++++---- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/_path.h b/src/_path.h index dc9058535f5f..de6749e21995 100644 --- a/src/_path.h +++ b/src/_path.h @@ -312,43 +312,39 @@ inline bool point_on_path( struct extent_limits { - double x0; - double y0; - double x1; - double y1; - double xm; - double ym; -}; + XY start; + XY end; + /* minpos is the minimum positive values in the data; used by log scaling. */ + XY minpos; -void reset_limits(extent_limits &e) -{ - e.x0 = std::numeric_limits::infinity(); - e.y0 = std::numeric_limits::infinity(); - e.x1 = -std::numeric_limits::infinity(); - e.y1 = -std::numeric_limits::infinity(); - /* xm and ym are the minimum positive values in the data, used - by log scaling */ - e.xm = std::numeric_limits::infinity(); - e.ym = std::numeric_limits::infinity(); -} + extent_limits() : start{0,0}, end{0,0}, minpos{0,0} { + reset(); + } -inline void update_limits(double x, double y, extent_limits &e) -{ - if (x < e.x0) - e.x0 = x; - if (y < e.y0) - e.y0 = y; - if (x > e.x1) - e.x1 = x; - if (y > e.y1) - e.y1 = y; - /* xm and ym are the minimum positive values in the data, used - by log scaling */ - if (x > 0.0 && x < e.xm) - e.xm = x; - if (y > 0.0 && y < e.ym) - e.ym = y; -} + void reset() + { + start.x = std::numeric_limits::infinity(); + start.y = std::numeric_limits::infinity(); + end.x = -std::numeric_limits::infinity(); + end.y = -std::numeric_limits::infinity(); + minpos.x = std::numeric_limits::infinity(); + minpos.y = std::numeric_limits::infinity(); + } + + void update(double x, double y) + { + start.x = std::min(start.x, x); + start.y = std::min(start.y, y); + end.x = std::max(end.x, x); + end.y = std::max(end.y, y); + if (x > 0.0) { + minpos.x = std::min(minpos.x, x); + } + if (y > 0.0) { + minpos.y = std::min(minpos.y, y); + } + } +}; template void update_path_extents(PathIterator &path, agg::trans_affine &trans, extent_limits &extents) @@ -367,7 +363,7 @@ void update_path_extents(PathIterator &path, agg::trans_affine &trans, extent_li if ((code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly) { continue; } - update_limits(x, y, extents); + extents.update(x, y); } } @@ -390,7 +386,7 @@ void get_path_collection_extents(agg::trans_affine &master_transform, agg::trans_affine trans; - reset_limits(extent); + extent.reset(); for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path(paths(i % Npaths)); diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 6375351cb52e..fd80b02b85ae 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -68,15 +68,15 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, py::ssize_t dims[] = { 2, 2 }; py::array_t extents(dims); - *extents.mutable_data(0, 0) = e.x0; - *extents.mutable_data(0, 1) = e.y0; - *extents.mutable_data(1, 0) = e.x1; - *extents.mutable_data(1, 1) = e.y1; + *extents.mutable_data(0, 0) = e.start.x; + *extents.mutable_data(0, 1) = e.start.y; + *extents.mutable_data(1, 0) = e.end.x; + *extents.mutable_data(1, 1) = e.end.y; py::ssize_t minposdims[] = { 2 }; py::array_t minpos(minposdims); - *minpos.mutable_data(0) = e.xm; - *minpos.mutable_data(1) = e.ym; + *minpos.mutable_data(0) = e.minpos.x; + *minpos.mutable_data(1) = e.minpos.y; return py::make_tuple(extents, minpos); } From b64f3d74f413508ca2cb7babd23f7c7081ac24d8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 9 Sep 2025 05:15:31 -0400 Subject: [PATCH 5/8] path: Simplify parts of clip_path_to_rect Use `XY` type to shorten internals, and `agg::rect_d::normalize` to shorten initialization. --- src/_path.h | 93 ++++++++++++++++++++----------------------- src/_path_wrapper.cpp | 4 +- 2 files changed, 44 insertions(+), 53 deletions(-) diff --git a/src/_path.h b/src/_path.h index de6749e21995..226d60231682 100644 --- a/src/_path.h +++ b/src/_path.h @@ -26,6 +26,8 @@ struct XY double x; double y; + XY() : x(0), y(0) {} + XY(double x_, double y_) : x(x_), y(y_) { } @@ -521,12 +523,14 @@ struct bisectx { } - inline void bisect(double sx, double sy, double px, double py, double *bx, double *by) const + inline XY bisect(const XY s, const XY p) const { - *bx = m_x; - double dx = px - sx; - double dy = py - sy; - *by = sy + dy * ((m_x - sx) / dx); + double dx = p.x - s.x; + double dy = p.y - s.y; + return { + m_x, + s.y + dy * ((m_x - s.x) / dx), + }; } }; @@ -536,9 +540,9 @@ struct xlt : public bisectx { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return x <= m_x; + return point.x <= m_x; } }; @@ -548,9 +552,9 @@ struct xgt : public bisectx { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return x >= m_x; + return point.x >= m_x; } }; @@ -562,12 +566,14 @@ struct bisecty { } - inline void bisect(double sx, double sy, double px, double py, double *bx, double *by) const + inline XY bisect(const XY s, const XY p) const { - *by = m_y; - double dx = px - sx; - double dy = py - sy; - *bx = sx + dx * ((m_y - sy) / dy); + double dx = p.x - s.x; + double dy = p.y - s.y; + return { + s.x + dx * ((m_y - s.y) / dy), + m_y, + }; } }; @@ -577,9 +583,9 @@ struct ylt : public bisecty { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return y <= m_y; + return point.y <= m_y; } }; @@ -589,9 +595,9 @@ struct ygt : public bisecty { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return y >= m_y; + return point.y >= m_y; } }; } @@ -606,46 +612,30 @@ inline void clip_to_rect_one_step(const Polygon &polygon, Polygon &result, const return; } - auto [sx, sy] = polygon.back(); - for (auto [px, py] : polygon) { - sinside = filter.is_inside(sx, sy); - pinside = filter.is_inside(px, py); + auto s = polygon.back(); + for (auto p : polygon) { + sinside = filter.is_inside(s); + pinside = filter.is_inside(p); if (sinside ^ pinside) { - double bx, by; - filter.bisect(sx, sy, px, py, &bx, &by); - result.emplace_back(bx, by); + result.emplace_back(filter.bisect(s, p)); } if (pinside) { - result.emplace_back(px, py); + result.emplace_back(p); } - sx = px; - sy = py; + s = p; } } template -void -clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vector &results) +auto +clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside) { - double xmin, ymin, xmax, ymax; - if (rect.x1 < rect.x2) { - xmin = rect.x1; - xmax = rect.x2; - } else { - xmin = rect.x2; - xmax = rect.x1; - } - - if (rect.y1 < rect.y2) { - ymin = rect.y1; - ymax = rect.y2; - } else { - ymin = rect.y2; - ymax = rect.y1; - } + rect.normalize(); + auto xmin = rect.x1, xmax = rect.x2; + auto ymin = rect.y1, ymax = rect.y2; if (!inside) { std::swap(xmin, xmax); @@ -656,26 +646,27 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vecto curve_t curve(path); Polygon polygon1, polygon2; - double x = 0, y = 0; + XY point; unsigned code = 0; curve.rewind(0); + std::vector results; do { // Grab the next subpath and store it in polygon1 polygon1.clear(); do { if (code == agg::path_cmd_move_to) { - polygon1.emplace_back(x, y); + polygon1.emplace_back(point); } - code = curve.vertex(&x, &y); + code = curve.vertex(&point.x, &point.y); if (code == agg::path_cmd_stop) { break; } if (code != agg::path_cmd_move_to) { - polygon1.emplace_back(x, y); + polygon1.emplace_back(point); } } while ((code & agg::path_cmd_end_poly) != agg::path_cmd_end_poly); @@ -694,6 +685,8 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vecto } while (code != agg::path_cmd_stop); _finalize_polygon(results, true); + + return results; } template diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index fd80b02b85ae..802189c428d3 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -109,9 +109,7 @@ Py_path_in_path(mpl::PathIterator a, agg::trans_affine atrans, static py::list Py_clip_path_to_rect(mpl::PathIterator path, agg::rect_d rect, bool inside) { - std::vector result; - - clip_path_to_rect(path, rect, inside, result); + auto result = clip_path_to_rect(path, rect, inside); return convert_polygon_vector(result); } From a55ad731bc48d73c1b63259663dc9ba983756831 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 11 Sep 2025 01:56:57 -0400 Subject: [PATCH 6/8] Implement a 3D version of the clipping algorithm --- src/_path.h | 204 +++++++++++++++++++++++++++++++++++------- src/_path_wrapper.cpp | 25 +++++- 2 files changed, 194 insertions(+), 35 deletions(-) diff --git a/src/_path.h b/src/_path.h index 226d60231682..3e4286f0c41a 100644 --- a/src/_path.h +++ b/src/_path.h @@ -45,14 +45,36 @@ struct XY typedef std::vector Polygon; -inline void -_finalize_polygon(std::vector &result, bool closed_only) +struct XYZ +{ + double x; + double y; + double z; + + XYZ() : x(0), y(0), z(0) {} + XYZ(double x_, double y_, double z_ = 0.0) : x(x_), y(y_), z(z_) {} + + bool operator==(const XYZ& o) + { + return (x == o.x && y == o.y && z == o.z); + } + + bool operator!=(const XYZ& o) + { + return (x != o.x || y != o.y || z != o.z); + } +}; + +typedef std::vector Polygon3D; + +template +void _finalize_polygon(std::vector &result, bool closed_only) { if (result.size() == 0) { return; } - Polygon &polygon = result.back(); + PolygonT &polygon = result.back(); /* Clean up the last polygon in the result. */ if (polygon.size() == 0) { @@ -511,10 +533,13 @@ bool path_in_path(PathIterator1 &a, namespace clip_to_rect_filters { -/* There are four different passes needed to create/remove - vertices (one for each side of the rectangle). The differences - between those passes are encapsulated in these functor classes. +/* In 2D, there are four different passes needed to create/remove vertices (one for each + * side of the rectangle). In 3d, there are six passes instead (one for each side of the + * cube). + * + * The differences between those passes are encapsulated in these functor classes. */ +template struct bisectx { double m_x; @@ -523,41 +548,49 @@ struct bisectx { } - inline XY bisect(const XY s, const XY p) const + inline PointT bisect(const PointT s, const PointT p) const { double dx = p.x - s.x; double dy = p.y - s.y; - return { + auto result = PointT{ m_x, s.y + dy * ((m_x - s.x) / dx), }; + if constexpr (std::is_same_v) { + double dz = p.z - s.z; + result.z = s.z + dz * ((m_x - s.x) / dx); + } + return result; } }; -struct xlt : public bisectx +template +struct xlt : public bisectx { - xlt(double x) : bisectx(x) + xlt(double x) : bisectx(x) { } - inline bool is_inside(const XY point) const + inline bool is_inside(const PointT point) const { - return point.x <= m_x; + return point.x <= this->m_x; } }; -struct xgt : public bisectx +template +struct xgt : public bisectx { - xgt(double x) : bisectx(x) + xgt(double x) : bisectx(x) { } - inline bool is_inside(const XY point) const + inline bool is_inside(const PointT point) const { - return point.x >= m_x; + return point.x >= this->m_x; } }; +template struct bisecty { double m_y; @@ -566,44 +599,90 @@ struct bisecty { } - inline XY bisect(const XY s, const XY p) const + inline PointT bisect(const PointT s, const PointT p) const { double dx = p.x - s.x; double dy = p.y - s.y; - return { + auto result = PointT{ s.x + dx * ((m_y - s.y) / dy), m_y, }; + if constexpr (std::is_same_v) { + double dz = p.z - s.z; + result.z = s.z + dz * ((m_y - s.y) / dy); + } + return result; } }; -struct ylt : public bisecty +template +struct ylt : public bisecty { - ylt(double y) : bisecty(y) + ylt(double y) : bisecty(y) { } - inline bool is_inside(const XY point) const + inline bool is_inside(const PointT point) const { - return point.y <= m_y; + return point.y <= this->m_y; } }; -struct ygt : public bisecty +template +struct ygt : public bisecty { - ygt(double y) : bisecty(y) + ygt(double y) : bisecty(y) { } - inline bool is_inside(const XY point) const + inline bool is_inside(const PointT point) const { - return point.y >= m_y; + return point.y >= this->m_y; + } +}; + +struct bisectz +{ + double m_z; + + bisectz(double z) : m_z(z) {} + + inline XYZ bisect(const XYZ s, const XYZ p) const + { + double dx = p.x - s.x; + double dy = p.y - s.y; + double dz = p.z - s.z; + return { + s.x + dx * ((m_z - s.z) / dz), + s.y + dy * ((m_z - s.z) / dz), + m_z, + }; + } +}; + +struct zlt : public bisectz +{ + zlt(double z) : bisectz(z) {} + + inline bool is_inside(const XYZ point) const + { + return point.z <= m_z; + } +}; + +struct zgt : public bisectz +{ + zgt(double z) : bisectz(z) {} + + inline bool is_inside(const XYZ point) const + { + return point.z >= m_z; } }; } -template -inline void clip_to_rect_one_step(const Polygon &polygon, Polygon &result, const Filter &filter) +template +inline void clip_to_rect_one_step(const PolygonT &polygon, PolygonT &result, const Filter &filter) { bool sinside, pinside; result.clear(); @@ -672,10 +751,10 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside) // The result of each step is fed into the next (note the // swapping of polygon1 and polygon2 at each step). - clip_to_rect_one_step(polygon1, polygon2, clip_to_rect_filters::xlt(xmax)); - clip_to_rect_one_step(polygon2, polygon1, clip_to_rect_filters::xgt(xmin)); - clip_to_rect_one_step(polygon1, polygon2, clip_to_rect_filters::ylt(ymax)); - clip_to_rect_one_step(polygon2, polygon1, clip_to_rect_filters::ygt(ymin)); + clip_to_rect_one_step(polygon1, polygon2, clip_to_rect_filters::xlt(xmax)); + clip_to_rect_one_step(polygon2, polygon1, clip_to_rect_filters::xgt(xmin)); + clip_to_rect_one_step(polygon1, polygon2, clip_to_rect_filters::ylt(ymax)); + clip_to_rect_one_step(polygon2, polygon1, clip_to_rect_filters::ygt(ymin)); // Empty polygons aren't very useful, so skip them if (polygon1.size()) { @@ -689,6 +768,67 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside) return results; } +inline std::vector +clip_paths_to_box(py::array_t paths, + std::array, 3> &box, + bool inside) +{ + auto xmin = std::get<0>(box).first, xmax = std::get<0>(box).second; + auto ymin = std::get<1>(box).first, ymax = std::get<1>(box).second; + auto zmin = std::get<2>(box).first, zmax = std::get<2>(box).second; + + if (xmin > xmax) { + std::swap(xmin, xmax); + } + if (ymin > ymax) { + std::swap(ymin, ymax); + } + if (zmin > zmax) { + std::swap(zmin, zmax); + } + + if (!inside) { + std::swap(xmin, xmax); + std::swap(ymin, ymax); + std::swap(zmin, zmax); + } + + Polygon3D polygon1, polygon2; + std::vector results; + + auto paths_iter = paths.unchecked<3>(); + for (auto i = 0; i < paths.shape(0); i++) { + // Grab the next subpath and store it in polygon1 + polygon1.clear(); + for (auto j = 0; j < paths.shape(1); j++) { + polygon1.emplace_back( + paths_iter(i, j, 0), + paths_iter(i, j, 1), + paths_iter(i, j, 2) + ); + } + + // The result of each step is fed into the next (note the + // swapping of polygon1 and polygon2 at each step). + clip_to_rect_one_step(polygon1, polygon2, clip_to_rect_filters::xlt(xmax)); + clip_to_rect_one_step(polygon2, polygon1, clip_to_rect_filters::xgt(xmin)); + clip_to_rect_one_step(polygon1, polygon2, clip_to_rect_filters::ylt(ymax)); + clip_to_rect_one_step(polygon2, polygon1, clip_to_rect_filters::ygt(ymin)); + clip_to_rect_one_step(polygon1, polygon2, clip_to_rect_filters::zlt(zmax)); + clip_to_rect_one_step(polygon2, polygon1, clip_to_rect_filters::zgt(zmin)); + + // Empty polygons aren't very useful, so skip them + if (polygon1.size()) { + _finalize_polygon(results, true); + results.push_back(polygon1); + } + } + + _finalize_polygon(results, true); + + return results; +} + template void affine_transform_2d(VerticesArray &vertices, agg::trans_affine &trans, ResultArray &result) { diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 802189c428d3..a198d7a9ac76 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -2,9 +2,9 @@ #include #include -#include #include #include +#include #include #include "_path.h" @@ -16,14 +16,21 @@ namespace py = pybind11; using namespace pybind11::literals; +template py::list -convert_polygon_vector(std::vector &polygons) +convert_polygon_vector(std::vector &polygons) { + static_assert(std::is_same_v || std::is_same_v, + "Vector must contain Polygon or Polygon3D"); + auto result = py::list(polygons.size()); for (size_t i = 0; i < polygons.size(); ++i) { const auto& poly = polygons[i]; - py::ssize_t dims[] = { static_cast(poly.size()), 2 }; + py::ssize_t dims[] = { + static_cast(poly.size()), + sizeof(typename T::value_type) / sizeof(double) + }; result[i] = py::array(dims, reinterpret_cast(poly.data())); } @@ -114,6 +121,16 @@ Py_clip_path_to_rect(mpl::PathIterator path, agg::rect_d rect, bool inside) return convert_polygon_vector(result); } +static py::list +Py_clip_paths_to_box(py::array_t paths, + std::array, 3> box, + bool inside) +{ + auto result = clip_paths_to_box(paths, box, inside); + + return convert_polygon_vector(result); +} + static py::object Py_affine_transform(py::array_t vertices_arr, agg::trans_affine trans) @@ -319,6 +336,8 @@ PYBIND11_MODULE(_path, m, py::mod_gil_not_used()) "path_a"_a, "trans_a"_a, "path_b"_a, "trans_b"_a); m.def("clip_path_to_rect", &Py_clip_path_to_rect, "path"_a, "rect"_a, "inside"_a); + m.def("clip_paths_to_box", &Py_clip_paths_to_box, + "path"_a, "box"_a, "inside"_a); m.def("affine_transform", &Py_affine_transform, "points"_a, "trans"_a); m.def("count_bboxes_overlapping_bbox", &Py_count_bboxes_overlapping_bbox, From 341809c267c0f81f3b58e94ea9176eabf80c0021 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 11 Sep 2025 21:29:00 -0400 Subject: [PATCH 7/8] Implement full clipping for Poly3DCollection --- lib/mpl_toolkits/mplot3d/art3d.py | 60 +++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index e051e44fb23d..366aeb3bfae0 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -97,6 +97,33 @@ def _viewlim_mask(xs, ys, zs, axes): return mask +def _clip_to_axes_bbox(axes, paths): + """ + Return an Axes-clipped copy of paths. + + Parameters + ---------- + axes : Axes3D + The axes to clip the paths against. + paths : array-like + A list of polygons, each comprised of a path of vertices. May also be a 3D NumPy + array, if all polygons contain paths of the same length. + + Returns + ------- + list of np.ndarray + A copy of *paths* with each polygon clipped to the *axes*. + """ + result = mpath._path.clip_paths_to_box( + paths, + [axes.xy_viewLim.intervalx, + axes.xy_viewLim.intervaly, + axes.zz_viewLim.intervalx], + True) # TODO: Pass in mask? + + return result + + class Text3D(mtext.Text): """ Text object with 3D position and direction. @@ -1339,22 +1366,28 @@ def do_3d_projection(self): if self._edge_is_mapped: self._edgecolor3d = self._edgecolors - needs_masking = np.any(self._invalid_vertices) - num_faces = len(self._faces) - mask = self._invalid_vertices + if self._axlim_clip: + # TODO: Apply `self._invalid_vertices` for proper masking. + pfaces = _clip_to_axes_bbox(self.axes, self._faces) + num_faces = len(pfaces) + num_verts = np.fromiter(map(len, pfaces), dtype=np.intp) + max_verts = num_verts.max(initial=0) + segments = np.empty((num_faces, max_verts, 3)) + for i, face in enumerate(pfaces): + segments[i, :len(face)] = face + pfaces = segments + mask = np.arange(max_verts) >= num_verts[:, None] + needs_masking = np.any(mask) + else: + pfaces = self._faces + needs_masking = np.any(self._invalid_vertices) + num_faces = len(self._faces) + mask = self._invalid_vertices # Some faces might contain masked vertices, so we want to ignore any # errors that those might cause with np.errstate(invalid='ignore', divide='ignore'): - pfaces = proj3d._proj_transform_vectors(self._faces, self.axes.M) - - if self._axlim_clip: - viewlim_mask = _viewlim_mask(self._faces[..., 0], self._faces[..., 1], - self._faces[..., 2], self.axes) - if np.any(viewlim_mask): - needs_masking = True - mask = mask | viewlim_mask - + pfaces = proj3d._proj_transform_vectors(pfaces, self.axes.M) pzs = pfaces[..., 2] if needs_masking: pzs = np.ma.MaskedArray(pzs, mask=mask) @@ -1385,8 +1418,7 @@ def do_3d_projection(self): if self._codes3d is not None and len(self._codes3d) > 0: if needs_masking: segment_mask = ~mask[face_order, :] - faces_2d = [face[mask, :] for face, mask - in zip(faces_2d, segment_mask)] + faces_2d = [face[mask, :] for face, mask in zip(faces_2d, segment_mask)] codes = [self._codes3d[idx] for idx in face_order] PolyCollection.set_verts_and_codes(self, faces_2d, codes) else: From 2fda01ea80e9a4d1bea407aaae1c8908e787e178 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 12 Sep 2025 05:27:14 -0400 Subject: [PATCH 8/8] Implement full clipping for Patch3D --- lib/mpl_toolkits/mplot3d/art3d.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 366aeb3bfae0..e409618a2f82 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -607,9 +607,11 @@ def get_path(self): def do_3d_projection(self): s = self._segment3d if self._axlim_clip: - mask = _viewlim_mask(*zip(*s), self.axes) - xs, ys, zs = np.ma.array(zip(*s), - dtype=float, mask=mask).filled(np.nan) + clipped = _clip_to_axes_bbox(self.axes, [s]) + if not clipped: + self._path2d = mpath.Path(np.empty((0, 2))) + return 0 + xs, ys, zs = clipped[0].T else: xs, ys, zs = zip(*s) vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs,