diff --git a/core/bind/core_bind.cpp b/core/bind/core_bind.cpp index ba595b96273..7562c82e8a6 100644 --- a/core/bind/core_bind.cpp +++ b/core/bind/core_bind.cpp @@ -1491,6 +1491,11 @@ PoolVector _Geometry::segment_intersects_convex(const Vector3 &p_from, return r; } +bool _Geometry::is_polygon_clockwise(const Vector &p_polygon) { + + return Geometry::is_polygon_clockwise(p_polygon); +} + Vector _Geometry::triangulate_polygon(const Vector &p_polygon) { return Geometry::triangulate_polygon(p_polygon); @@ -1506,6 +1511,107 @@ Vector _Geometry::clip_polygon(const Vector &p_points, const P return Geometry::clip_polygon(p_points, p_plane); } +Array _Geometry::merge_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b) { + + Vector > polys = Geometry::merge_polygons_2d(p_polygon_a, p_polygon_b); + + Array ret; + + for (int i = 0; i < polys.size(); ++i) { + ret.push_back(polys[i]); + } + return ret; +} + +Array _Geometry::clip_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b) { + + Vector > polys = Geometry::clip_polygons_2d(p_polygon_a, p_polygon_b); + + Array ret; + + for (int i = 0; i < polys.size(); ++i) { + ret.push_back(polys[i]); + } + return ret; +} + +Array _Geometry::intersect_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b) { + + Vector > polys = Geometry::intersect_polygons_2d(p_polygon_a, p_polygon_b); + + Array ret; + + for (int i = 0; i < polys.size(); ++i) { + ret.push_back(polys[i]); + } + return ret; +} + +Array _Geometry::exclude_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b) { + + Vector > polys = Geometry::exclude_polygons_2d(p_polygon_a, p_polygon_b); + + Array ret; + + for (int i = 0; i < polys.size(); ++i) { + ret.push_back(polys[i]); + } + return ret; +} + +Array _Geometry::clip_polyline_with_polygon_2d(const Vector &p_polyline, const Vector &p_polygon) { + + Vector > polys = Geometry::clip_polyline_with_polygon_2d(p_polyline, p_polygon); + + Array ret; + + for (int i = 0; i < polys.size(); ++i) { + ret.push_back(polys[i]); + } + return ret; +} + +Array _Geometry::intersect_polyline_with_polygon_2d(const Vector &p_polyline, const Vector &p_polygon) { + + Vector > polys = Geometry::intersect_polyline_with_polygon_2d(p_polyline, p_polygon); + + Array ret; + + for (int i = 0; i < polys.size(); ++i) { + ret.push_back(polys[i]); + } + return ret; +} + +Array _Geometry::offset_polygon_2d(const Vector &p_polygon, real_t p_delta, PolyJoinType p_join_type) { + + Vector > polys = Geometry::offset_polygon_2d(p_polygon, p_delta, Geometry::PolyJoinType(p_join_type)); + + Array ret; + + for (int i = 0; i < polys.size(); ++i) { + ret.push_back(polys[i]); + } + return ret; +} + +Array _Geometry::offset_polyline_2d(const Vector &p_polygon, real_t p_delta, PolyJoinType p_join_type, PolyEndType p_end_type) { + + Vector > polys = Geometry::offset_polyline_2d(p_polygon, p_delta, Geometry::PolyJoinType(p_join_type), Geometry::PolyEndType(p_end_type)); + + Array ret; + + for (int i = 0; i < polys.size(); ++i) { + ret.push_back(polys[i]); + } + return ret; +} + +Vector _Geometry::transform_points_2d(const Vector &p_points, const Transform2D &p_mat) { + + return Geometry::transform_points_2d(p_points, p_mat); +} + Dictionary _Geometry::make_atlas(const Vector &p_rects) { Dictionary ret; @@ -1566,11 +1672,40 @@ void _Geometry::_bind_methods() { ClassDB::bind_method(D_METHOD("segment_intersects_convex", "from", "to", "planes"), &_Geometry::segment_intersects_convex); ClassDB::bind_method(D_METHOD("point_is_inside_triangle", "point", "a", "b", "c"), &_Geometry::point_is_inside_triangle); + ClassDB::bind_method(D_METHOD("is_polygon_clockwise", "polygon"), &_Geometry::is_polygon_clockwise); ClassDB::bind_method(D_METHOD("triangulate_polygon", "polygon"), &_Geometry::triangulate_polygon); ClassDB::bind_method(D_METHOD("convex_hull_2d", "points"), &_Geometry::convex_hull_2d); ClassDB::bind_method(D_METHOD("clip_polygon", "points", "plane"), &_Geometry::clip_polygon); + ClassDB::bind_method(D_METHOD("merge_polygons_2d", "polygon_a", "polygon_b"), &_Geometry::merge_polygons_2d); + ClassDB::bind_method(D_METHOD("clip_polygons_2d", "polygon_a", "polygon_b"), &_Geometry::clip_polygons_2d); + ClassDB::bind_method(D_METHOD("intersect_polygons_2d", "polygon_a", "polygon_b"), &_Geometry::intersect_polygons_2d); + ClassDB::bind_method(D_METHOD("exclude_polygons_2d", "polygon_a", "polygon_b"), &_Geometry::exclude_polygons_2d); + + ClassDB::bind_method(D_METHOD("clip_polyline_with_polygon_2d", "polyline", "polygon"), &_Geometry::clip_polyline_with_polygon_2d); + ClassDB::bind_method(D_METHOD("intersect_polyline_with_polygon_2d", "polyline", "polygon"), &_Geometry::intersect_polyline_with_polygon_2d); + + ClassDB::bind_method(D_METHOD("offset_polygon_2d", "polygon", "delta", "join_type"), &_Geometry::offset_polygon_2d, DEFVAL(JOIN_SQUARE)); + ClassDB::bind_method(D_METHOD("offset_polyline_2d", "polyline", "delta", "join_type", "end_type"), &_Geometry::offset_polyline_2d, DEFVAL(JOIN_SQUARE), DEFVAL(END_SQUARE)); + + ClassDB::bind_method(D_METHOD("transform_points_2d", "points", "transform"), &_Geometry::transform_points_2d); + ClassDB::bind_method(D_METHOD("make_atlas", "sizes"), &_Geometry::make_atlas); + + BIND_ENUM_CONSTANT(OPERATION_UNION); + BIND_ENUM_CONSTANT(OPERATION_DIFFERENCE); + BIND_ENUM_CONSTANT(OPERATION_INTERSECTION); + BIND_ENUM_CONSTANT(OPERATION_XOR); + + BIND_ENUM_CONSTANT(JOIN_SQUARE); + BIND_ENUM_CONSTANT(JOIN_ROUND); + BIND_ENUM_CONSTANT(JOIN_MITER); + + BIND_ENUM_CONSTANT(END_POLYGON); + BIND_ENUM_CONSTANT(END_JOINED); + BIND_ENUM_CONSTANT(END_BUTT); + BIND_ENUM_CONSTANT(END_SQUARE); + BIND_ENUM_CONSTANT(END_ROUND); } _Geometry::_Geometry() { diff --git a/core/bind/core_bind.h b/core/bind/core_bind.h index 2906de4a4a0..561449f29ee 100644 --- a/core/bind/core_bind.h +++ b/core/bind/core_bind.h @@ -402,15 +402,54 @@ public: real_t segment_intersects_circle(const Vector2 &p_from, const Vector2 &p_to, const Vector2 &p_circle_pos, real_t p_circle_radius); int get_uv84_normal_bit(const Vector3 &p_vector); + bool is_polygon_clockwise(const Vector &p_polygon); Vector triangulate_polygon(const Vector &p_polygon); Vector convex_hull_2d(const Vector &p_points); Vector clip_polygon(const Vector &p_points, const Plane &p_plane); + enum PolyBooleanOperation { + OPERATION_UNION, + OPERATION_DIFFERENCE, + OPERATION_INTERSECTION, + OPERATION_XOR + }; + // 2D polygon boolean operations + Array merge_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b); // union (add) + Array clip_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b); // difference (subtract) + Array intersect_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b); // common area (multiply) + Array exclude_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b); // all but common area (xor) + + // 2D polyline vs polygon operations + Array clip_polyline_with_polygon_2d(const Vector &p_polyline, const Vector &p_polygon); // cut + Array intersect_polyline_with_polygon_2d(const Vector &p_polyline, const Vector &p_polygon); // chop + + // 2D offset polygons/polylines + enum PolyJoinType { + JOIN_SQUARE, + JOIN_ROUND, + JOIN_MITER + }; + enum PolyEndType { + END_POLYGON, + END_JOINED, + END_BUTT, + END_SQUARE, + END_ROUND + }; + Array offset_polygon_2d(const Vector &p_polygon, real_t p_delta, PolyJoinType p_join_type = JOIN_SQUARE); + Array offset_polyline_2d(const Vector &p_polygon, real_t p_delta, PolyJoinType p_join_type = JOIN_SQUARE, PolyEndType p_end_type = END_SQUARE); + + Vector transform_points_2d(const Vector &p_points, const Transform2D &p_mat); + Dictionary make_atlas(const Vector &p_rects); _Geometry(); }; +VARIANT_ENUM_CAST(_Geometry::PolyBooleanOperation); +VARIANT_ENUM_CAST(_Geometry::PolyJoinType); +VARIANT_ENUM_CAST(_Geometry::PolyEndType); + class _File : public Reference { GDCLASS(_File, Reference); diff --git a/core/math/geometry.cpp b/core/math/geometry.cpp index 0ab8707d3a3..8314cb827c5 100644 --- a/core/math/geometry.cpp +++ b/core/math/geometry.cpp @@ -31,8 +31,11 @@ #include "geometry.h" #include "core/print_string.h" +#include "thirdparty/misc/clipper.hpp" #include "thirdparty/misc/triangulator.h" +#define SCALE_FACTOR 100000.0 // based on CMP_EPSILON + /* this implementation is very inefficient, commenting unless bugs happen. See the other one. bool Geometry::is_point_in_polygon(const Vector2 &p_point, const Vector &p_polygon) { @@ -1134,3 +1137,106 @@ void Geometry::make_atlas(const Vector &p_rects, Vector &r_resu r_size = Size2(results[best].max_w, results[best].max_h); } + +Vector > Geometry::_polypaths_do_operation(PolyBooleanOperation p_op, const Vector &p_polypath_a, const Vector &p_polypath_b, bool is_a_open) { + + using namespace ClipperLib; + + ClipType op = ctUnion; + + switch (p_op) { + case OPERATION_UNION: op = ctUnion; break; + case OPERATION_DIFFERENCE: op = ctDifference; break; + case OPERATION_INTERSECTION: op = ctIntersection; break; + case OPERATION_XOR: op = ctXor; break; + } + Path path_a, path_b; + + // Need to scale points (Clipper's requirement for robust computation) + for (int i = 0; i != p_polypath_a.size(); ++i) { + path_a << IntPoint(p_polypath_a[i].x * SCALE_FACTOR, p_polypath_a[i].y * SCALE_FACTOR); + } + for (int i = 0; i != p_polypath_b.size(); ++i) { + path_b << IntPoint(p_polypath_b[i].x * SCALE_FACTOR, p_polypath_b[i].y * SCALE_FACTOR); + } + Clipper clp; + clp.AddPath(path_a, ptSubject, !is_a_open); // forward compatible with Clipper 10.0.0 + clp.AddPath(path_b, ptClip, true); // polylines cannot be set as clip + + Paths paths; + + if (is_a_open) { + PolyTree tree; // needed to populate polylines + clp.Execute(op, tree); + OpenPathsFromPolyTree(tree, paths); + } else { + clp.Execute(op, paths); // works on closed polygons only + } + // Have to scale points down now + Vector > polypaths; + + for (Paths::size_type i = 0; i < paths.size(); ++i) { + Vector polypath; + + const Path &scaled_path = paths[i]; + + for (Paths::size_type j = 0; j < scaled_path.size(); ++j) { + polypath.push_back(Point2( + static_cast(scaled_path[j].X) / SCALE_FACTOR, + static_cast(scaled_path[j].Y) / SCALE_FACTOR)); + } + polypaths.push_back(polypath); + } + return polypaths; +} + +Vector > Geometry::_polypath_offset(const Vector &p_polypath, real_t p_delta, PolyJoinType p_join_type, PolyEndType p_end_type) { + + using namespace ClipperLib; + + JoinType jt = jtSquare; + + switch (p_join_type) { + case JOIN_SQUARE: jt = jtSquare; break; + case JOIN_ROUND: jt = jtRound; break; + case JOIN_MITER: jt = jtMiter; break; + } + + EndType et = etClosedPolygon; + + switch (p_end_type) { + case END_POLYGON: et = etClosedPolygon; break; + case END_JOINED: et = etClosedLine; break; + case END_BUTT: et = etOpenButt; break; + case END_SQUARE: et = etOpenSquare; break; + case END_ROUND: et = etOpenRound; break; + } + ClipperOffset co; + Path path; + + // Need to scale points (Clipper's requirement for robust computation) + for (int i = 0; i != p_polypath.size(); ++i) { + path << IntPoint(p_polypath[i].x * SCALE_FACTOR, p_polypath[i].y * SCALE_FACTOR); + } + co.AddPath(path, jt, et); + + Paths paths; + co.Execute(paths, p_delta * SCALE_FACTOR); // inflate/deflate + + // Have to scale points down now + Vector > polypaths; + + for (Paths::size_type i = 0; i < paths.size(); ++i) { + Vector polypath; + + const Path &scaled_path = paths[i]; + + for (Paths::size_type j = 0; j < scaled_path.size(); ++j) { + polypath.push_back(Point2( + static_cast(scaled_path[j].X) / SCALE_FACTOR, + static_cast(scaled_path[j].Y) / SCALE_FACTOR)); + } + polypaths.push_back(polypath); + } + return polypaths; +} diff --git a/core/math/geometry.h b/core/math/geometry.h index 0b2adf95133..c62573dd130 100644 --- a/core/math/geometry.h +++ b/core/math/geometry.h @@ -785,6 +785,78 @@ public: return clipped; } + enum PolyBooleanOperation { + OPERATION_UNION, + OPERATION_DIFFERENCE, + OPERATION_INTERSECTION, + OPERATION_XOR + }; + enum PolyJoinType { + JOIN_SQUARE, + JOIN_ROUND, + JOIN_MITER + }; + enum PolyEndType { + END_POLYGON, + END_JOINED, + END_BUTT, + END_SQUARE, + END_ROUND + }; + + static Vector > merge_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b) { + + return _polypaths_do_operation(OPERATION_UNION, p_polygon_a, p_polygon_b); + } + + static Vector > clip_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b) { + + return _polypaths_do_operation(OPERATION_DIFFERENCE, p_polygon_a, p_polygon_b); + } + + static Vector > intersect_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b) { + + return _polypaths_do_operation(OPERATION_INTERSECTION, p_polygon_a, p_polygon_b); + } + + static Vector > exclude_polygons_2d(const Vector &p_polygon_a, const Vector &p_polygon_b) { + + return _polypaths_do_operation(OPERATION_XOR, p_polygon_a, p_polygon_b); + } + + static Vector > clip_polyline_with_polygon_2d(const Vector &p_polyline, const Vector &p_polygon) { + + return _polypaths_do_operation(OPERATION_DIFFERENCE, p_polyline, p_polygon, true); + } + + static Vector > intersect_polyline_with_polygon_2d(const Vector &p_polyline, const Vector &p_polygon) { + + return _polypaths_do_operation(OPERATION_INTERSECTION, p_polyline, p_polygon, true); + } + + static Vector > offset_polygon_2d(const Vector &p_polygon, real_t p_delta, PolyJoinType p_join_type) { + + return _polypath_offset(p_polygon, p_delta, p_join_type, END_POLYGON); + } + + static Vector > offset_polyline_2d(const Vector &p_polygon, real_t p_delta, PolyJoinType p_join_type, PolyEndType p_end_type) { + + ERR_EXPLAIN("Attempt to offset a polyline like a polygon (use offset_polygon_2d instead)."); + ERR_FAIL_COND_V(p_end_type == END_POLYGON, Vector >()); + + return _polypath_offset(p_polygon, p_delta, p_join_type, p_end_type); + } + + static Vector transform_points_2d(const Vector &p_points, const Transform2D &p_mat) { + + Vector points; + + for (int i = 0; i < p_points.size(); ++i) { + points.push_back(p_mat.xform(p_points[i])); + } + return points; + } + static Vector triangulate_polygon(const Vector &p_polygon) { Vector triangles; @@ -951,7 +1023,6 @@ public: H.resize(k); return H; } - static Vector > decompose_polygon_in_convex(Vector polygon); static MeshData build_convex_mesh(const PoolVector &p_planes); @@ -961,6 +1032,10 @@ public: static PoolVector build_capsule_planes(real_t p_radius, real_t p_height, int p_sides, int p_lats, Vector3::Axis p_axis = Vector3::AXIS_Z); static void make_atlas(const Vector &p_rects, Vector &r_result, Size2i &r_size); + +private: + static Vector > _polypaths_do_operation(PolyBooleanOperation p_op, const Vector &p_polypath_a, const Vector &p_polypath_b, bool is_a_open = false); + static Vector > _polypath_offset(const Vector &p_polypath, real_t p_delta, PolyJoinType p_join_type, PolyEndType p_end_type); }; #endif diff --git a/doc/classes/Geometry.xml b/doc/classes/Geometry.xml index 68539d7ecb9..b95b888cf1e 100644 --- a/doc/classes/Geometry.xml +++ b/doc/classes/Geometry.xml @@ -59,6 +59,29 @@ Clips the polygon defined by the points in [code]points[/code] against the [code]plane[/code] and returns the points of the clipped polygon. + + + + + + + + + Clips [code]polygon_a[/code] against [code]polygon_b[/code] and returns an array of clipped polygons. This performs [code]OPERATION_DIFFERENCE[/code] between polygons. Returns an empty array if [code]polygon_b[/code] completely overlaps [code]polygon_a[/code]. + If [code]polygon_b[/code] is enclosed by [code]polygon_a[/code], returns an outer polygon (boundary) and inner polygon (hole) which could be distiguished by calling [method is_polygon_clockwise]. + + + + + + + + + + + Clips [code]polyline[/code] against [code]polygon[/code] and returns an array of clipped polylines. This performs [code]OPERATION_DIFFERENCE[/code] between the polyline and the polygon. This operation can be thought of as cutting a line with a closed shape. + + @@ -68,6 +91,18 @@ Given an array of [Vector2]s, returns the convex hull as a list of points in counter-clockwise order. The last point is the same as the first one. + + + + + + + + + Mutually excludes common area defined by intersection of [code]polygon_a[/code] and [code]polygon_b[/code] (see [method intersect_polygons_2d]) and returns an array of excluded polygons. This performs [code]OPERATION_XOR[/code] between polygons. In other words, returns all but common area between polygons. + The operation may result in an outer polygon (boundary) and inner polygon (hole) produced which could be distiguished by calling [method is_polygon_clockwise]. + + @@ -158,6 +193,38 @@ + + + + + + + + + Intersects [code]polygon_a[/code] with [code]polygon_b[/code] and returns an array of intersected polygons. This performs [code]OPERATION_INTERSECTION[/code] between polygons. In other words, returns common area shared by polygons. Returns an empty array if no intersection occurs. + The operation may result in an outer polygon (boundary) and inner polygon (hole) produced which could be distiguished by calling [method is_polygon_clockwise]. + + + + + + + + + + + Intersects [code]polyline[/code] with [code]polygon[/code] and returns an array of intersected polylines. This performs [code]OPERATION_INTERSECTION[/code] between the polyline and the polygon. This operation can be thought of as chopping a line with a closed shape. + + + + + + + + + Returns [code]true[/code] if [code]polygon[/code]'s vertices are ordered in clockwise order, otherwise returns [code]false[/code]. + + @@ -182,6 +249,51 @@ Given an array of [Vector2]s representing tiles, builds an atlas. The returned dictionary has two keys: [code]points[/code] is a vector of [Vector2] that specifies the positions of each tile, [code]size[/code] contains the overall size of the whole atlas as [Vector2]. + + + + + + + + + Merges (combines) [code]polygon_a[/code] and [code]polygon_b[/code] and returns an array of merged polygons. This performs [code]OPERATION_UNION[/code] between polygons. + The operation may result in an outer polygon (boundary) and inner polygon (hole) produced which could be distiguished by calling [method is_polygon_clockwise]. + + + + + + + + + + + + + Inflates or deflates [code]polygon[/code] by [code]delta[/code] units (pixels). If [code]delta[/code] is positive, makes the polygon grow outward. If [code]delta[/code] is negative, shrinks the polygon inward. Returns an array of polygons because inflating/deflating may result in multiple discrete polygons. Returns an empty array if [code]delta[/code] is negative and the absolute value of it approximately exceeds the minimum bounding rectangle dimensions of the polygon. + Each polygon's vertices will be rounded as determined by [code]join_type[/code], see [enum Geometry.PolyJoinType]. + The operation may result in an outer polygon (boundary) and inner polygon (hole) produced which could be distiguished by calling [method is_polygon_clockwise]. + + + + + + + + + + + + + + + Inflates or deflates [code]polyline[/code] by [code]delta[/code] units (pixels), producing polygons. If [code]delta[/code] is positive, makes the polyline grow outward. Returns an array of polygons because inflating/deflating may result in multiple discrete polygons. If [code]delta[/code] is negative, returns an empty array. + Each polygon's vertices will be rounded as determined by [code]join_type[/code], see [enum Geometry.PolyJoinType]. + Each polygon's endpoints will be rounded as determined by [code]end_type[/code], see [enum Geometry.PolyEndType]. + The operation may result in an outer polygon (boundary) and inner polygon (hole) produced which could be distiguished by calling [method is_polygon_clockwise]. + + @@ -304,6 +416,18 @@ Tests if the segment ([code]from[/code], [code]to[/code]) intersects the triangle [code]a[/code], [code]b[/code], [code]c[/code]. If yes, returns the point of intersection as [Vector3]. If no intersection takes place, an empty [Variant] is returned. + + + + + + + + + Transforms an array of points by [code]transform[/code] and returns the result. + Can be useful in conjuction with performing polygon boolean operations in CSG manner, see [method merge_polygons_2d], [method clip_polygons_2d], [method intersect_polygons_2d], [method exclude_polygons_2d]. + + @@ -315,5 +439,41 @@ + + Create regions where either subject or clip polygons (or both) are filled. + + + Create regions where subject polygons are filled except where clip polygons are filled. + + + Create regions where both subject and clip polygons are filled. + + + Create regions where either subject or clip polygons are filled but not where both are filled. + + + Squaring is applied uniformally at all convex edge joins at [code]1 * delta[/code]. + + + While flattened paths can never perfectly trace an arc, they are approximated by a series of arc chords. + + + There's a necessary limit to mitered joins since offsetting edges that join at very acute angles will produce excessively long and narrow 'spikes'. For any given edge join, when miter offsetting would exceed that maximum distance, 'square' joining is applied. + + + Endpoints are joined using the [enum PolyJoinType] value and the path filled as a polygon. + + + Endpoints are joined using the [enum PolyJoinType] value and the path filled as a polyline. + + + Endpoints are squared off with no extension. + + + Endpoints are squared off and extended by [code]delta[/code] units. + + + Endpoints are rounded off and extended by [code]delta[/code] units. +