diff --git a/CHANGELOG.md b/CHANGELOG.md index de361a5..317ca0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Added `compas_cgal.straight_skeleton_2.create_interior_straight_skeleton`. +* Added `compas_cgal.straight_skeleton_2.create_interior_straight_skeleton_with_holes`. +* Added `compas_cgal.straight_skeleton_2.create_offset_polygons_2_inner`. +* Added `compas_cgal.straight_skeleton_2.create_offset_polygons_2_outer`. +* Added `compas_cgal.straight_skeleton_2.create_weighted_offset_polygons_2_inner`. +* Added `compas_cgal.straight_skeleton_2.create_weighted_offset_polygons_2_outer`. ### Changed diff --git a/docs/PLACEHOLDER b/docs/PLACEHOLDER deleted file mode 100644 index f9768c1..0000000 --- a/docs/PLACEHOLDER +++ /dev/null @@ -1 +0,0 @@ -# container for the source files of the documentation diff --git a/docs/_images/PLACEHOLDER b/docs/_images/PLACEHOLDER deleted file mode 100644 index 48f73eb..0000000 --- a/docs/_images/PLACEHOLDER +++ /dev/null @@ -1 +0,0 @@ -# container for images to be included in the docs diff --git a/docs/_images/cgal_straight_skeleton_2_holes.png b/docs/_images/cgal_straight_skeleton_2_holes.png new file mode 100644 index 0000000..b535e92 Binary files /dev/null and b/docs/_images/cgal_straight_skeleton_2_holes.png differ diff --git a/docs/_images/cgal_straight_skeleton_2_offset.png b/docs/_images/cgal_straight_skeleton_2_offset.png new file mode 100644 index 0000000..0868a50 Binary files /dev/null and b/docs/_images/cgal_straight_skeleton_2_offset.png differ diff --git a/docs/_images/cgal_straight_skeleton_2_offset_weighted.png b/docs/_images/cgal_straight_skeleton_2_offset_weighted.png new file mode 100644 index 0000000..f9ffa8b Binary files /dev/null and b/docs/_images/cgal_straight_skeleton_2_offset_weighted.png differ diff --git a/docs/api/compas_cgal.straight_skeleton_2.rst b/docs/api/compas_cgal.straight_skeleton_2.rst index 356ed84..3f37685 100644 --- a/docs/api/compas_cgal.straight_skeleton_2.rst +++ b/docs/api/compas_cgal.straight_skeleton_2.rst @@ -9,3 +9,4 @@ compas_cgal.straight_skeleton_2 :nosignatures: create_interior_straight_skeleton + create_interior_straight_skeleton_with_holes diff --git a/docs/examples/straight_skeleton_2.rst b/docs/examples/straight_skeleton_2.rst index 6da398d..9d33bbb 100644 --- a/docs/examples/straight_skeleton_2.rst +++ b/docs/examples/straight_skeleton_2.rst @@ -9,3 +9,30 @@ .. literalinclude:: straight_skeleton_2.py :language: python + + +.. figure:: /_images/cgal_straight_skeleton_2_holes.png + :figclass: figure + :class: figure-img img-fluid + + +.. literalinclude:: straight_skeleton_2_holes.py + :language: python + + +.. figure:: /_images/cgal_straight_skeleton_2_offset.png + :figclass: figure + :class: figure-img img-fluid + + +.. literalinclude:: straight_skeleton_2_offset.py + :language: python + + +.. figure:: /_images/cgal_straight_skeleton_2_offset_weighted.png + :figclass: figure + :class: figure-img img-fluid + + +.. literalinclude:: straight_skeleton_2_offset_weighted.py + :language: python diff --git a/docs/examples/straight_skeleton_2_holes.py b/docs/examples/straight_skeleton_2_holes.py new file mode 100644 index 0000000..bc2cd65 --- /dev/null +++ b/docs/examples/straight_skeleton_2_holes.py @@ -0,0 +1,42 @@ +from compas.datastructures import Graph +from compas.geometry import Polygon +from compas_viewer import Viewer + +from compas_cgal.straight_skeleton_2 import create_interior_straight_skeleton_with_holes + +points = [ + (-1.91, 3.59, 0.0), + (-5.53, -5.22, 0.0), + (-0.39, -1.98, 0.0), + (2.98, -5.51, 0.0), + (4.83, -2.02, 0.0), + (9.70, -3.63, 0.0), + (12.23, 1.25, 0.0), + (3.42, 0.66, 0.0), + (2.92, 4.03, 0.0), + (-1.91, 3.59, 0.0), +] + +holes = [ + [(0.42, 0.88, 0.0), (1.1, -1.0, 0.0), (-1.97, -0.93, 0.0), (-1.25, 1.82, 0.0)], + [(4.25, -0.64, 0.0), (2.9, -3.03, 0.0), (2.12, -2.16, 0.0), (2.89, -0.36, 0.0)], + [(10.6, 0.29, 0.0), (9.48, -1.54, 0.0), (5.48, -1.26, 0.0), (5.98, -0.04, 0.0)], +] + + +polygon = Polygon(points) +holes = [Polygon(hole) for hole in holes] +lines = create_interior_straight_skeleton_with_holes(polygon, holes) +graph = Graph.from_lines(lines) + +# ============================================================================== +# Viz +# ============================================================================== + +viewer = Viewer(width=1600, height=900) +viewer.renderer_config.show_grid = False +viewer.scene.add(graph, edgecolor=(1.0, 0.0, 0.0)) +viewer.scene.add(polygon) +for hole in holes: + viewer.scene.add(hole) +viewer.show() diff --git a/docs/examples/straight_skeleton_2_offset.py b/docs/examples/straight_skeleton_2_offset.py new file mode 100644 index 0000000..c2dfd46 --- /dev/null +++ b/docs/examples/straight_skeleton_2_offset.py @@ -0,0 +1,37 @@ +from compas.geometry import Polygon +from compas_viewer import Viewer + +from compas_cgal.straight_skeleton_2 import create_offset_polygons_2 + +points = [ + (-1.91, 3.59, 0.0), + (-5.53, -5.22, 0.0), + (-0.39, -1.98, 0.0), + (2.98, -5.51, 0.0), + (4.83, -2.02, 0.0), + (9.70, -3.63, 0.0), + (12.23, 1.25, 0.0), + (3.42, 0.66, 0.0), + (2.92, 4.03, 0.0), + (-1.91, 3.59, 0.0), +] +polygon = Polygon(points) +offset = 1.5 + +offset_polygons_inner = create_offset_polygons_2(points, offset) +offset_polygons_outer = create_offset_polygons_2(points, -offset) + +# ============================================================================== +# Viz +# ============================================================================== + +viewer = Viewer(width=1600, height=900) +viewer.scene.add(polygon) +viewer.config.renderer.show_grid = False + +for opolygon in offset_polygons_inner: + viewer.scene.add(opolygon, linecolor=(1.0, 0.0, 0.0), facecolor=(1.0, 1.0, 1.0, 0.0)) +for opolygon in offset_polygons_outer: + viewer.scene.add(opolygon, linecolor=(0.0, 0.0, 1.0), facecolor=(1.0, 1.0, 1.0, 0.0)) + +viewer.show() diff --git a/docs/examples/straight_skeleton_2_offset_weighted.py b/docs/examples/straight_skeleton_2_offset_weighted.py new file mode 100644 index 0000000..b7ce2b0 --- /dev/null +++ b/docs/examples/straight_skeleton_2_offset_weighted.py @@ -0,0 +1,37 @@ +from compas.geometry import Polygon +from compas_viewer import Viewer + +from compas_cgal.straight_skeleton_2 import create_weighted_offset_polygons_2 + +points = [ + (-1.91, 3.59, 0.0), + (-5.53, -5.22, 0.0), + (-0.39, -1.98, 0.0), + (2.98, -5.51, 0.0), + (4.83, -2.02, 0.0), + (9.70, -3.63, 0.0), + (12.23, 1.25, 0.0), + (3.42, 0.66, 0.0), + (2.92, 4.03, 0.0), + (-1.91, 3.59, 0.0), +] +polygon = Polygon(points) + + +distances = [0.1, 0.3, 0.6, 0.1, 0.7, 0.5, 0.2, 0.4, 0.8, 0.2] +weights = [1.0 / d for d in distances] +offset = 1.0 +offset_polygons_outer = create_weighted_offset_polygons_2(points, -offset, weights) + +# ============================================================================== +# Viz +# ============================================================================== + +viewer = Viewer(width=1600, height=900) +viewer.scene.add(polygon) +viewer.config.renderer.show_grid = False + +for opolygon in offset_polygons_outer: + viewer.scene.add(opolygon, linecolor=(0.0, 0.0, 1.0), facecolor=(1.0, 1.0, 1.0, 0.0)) + +viewer.show() diff --git a/src/compas_cgal/straight_skeleton_2.py b/src/compas_cgal/straight_skeleton_2.py index 2385647..31d9417 100644 --- a/src/compas_cgal/straight_skeleton_2.py +++ b/src/compas_cgal/straight_skeleton_2.py @@ -1,4 +1,5 @@ import numpy as np +from compas.geometry import Polygon from compas.geometry import normal_polygon from compas.tolerance import TOL @@ -26,7 +27,120 @@ def create_interior_straight_skeleton(points) -> PolylinesNumpy: If the normal of the polygon is not [0, 0, 1]. """ points = list(points) - if not TOL.is_allclose(normal_polygon(points, True), [0, 0, 1]): - raise ValueError("Please pass a polygon with a normal vector of [0, 0, 1].") + normal = normal_polygon(points, True) + if not TOL.is_allclose(normal, [0, 0, 1]): + raise ValueError("The normal of the polygon should be [0, 0, 1]. The normal of the provided polygon is {}".format(normal)) V = np.asarray(points, dtype=np.float64) return straight_skeleton_2.create_interior_straight_skeleton(V) + + +def create_interior_straight_skeleton_with_holes(points, holes) -> PolylinesNumpy: + """Compute the skeleton of a polygon with holes. + + Parameters + ---------- + points : list of point coordinates or :class:`compas.geometry.Polygon` + The points of the polygon. + holes : list of list of point coordinates or list of :class:`compas.geometry.Polygon` + The holes of the polygon. + + Returns + ------- + :attr:`compas_cgal.types.PolylinesNumpy` + The skeleton of the polygon. + + Raises + ------ + ValueError + If the normal of the polygon is not [0, 0, 1]. + If the normal of a hole is not [0, 0, -1]. + """ + points = list(points) + normal = normal_polygon(points, True) + if not TOL.is_allclose(normal, [0, 0, 1]): + raise ValueError("The normal of the polygon should be [0, 0, 1]. The normal of the provided polygon is {}".format(normal)) + V = np.asarray(points, dtype=np.float64) + + H = [] + for i, hole in enumerate(holes): + points = list(hole) + normal_hole = normal_polygon(points, True) + if not TOL.is_allclose(normal_hole, [0, 0, -1]): + raise ValueError("The normal of the hole should be [0, 0, -1]. The normal of the provided {}-th hole is {}".format(i, normal_hole)) + hole = np.asarray(points, dtype=np.float64) + H.append(hole) + return straight_skeleton_2.create_interior_straight_skeleton_with_holes(V, H) + + +def create_offset_polygons_2(points, offset) -> list[Polygon]: + """Compute the polygon offset. + + Parameters + ---------- + points : list of point coordinates or :class:`compas.geometry.Polygon` + The points of the polygon. + offset : float + The offset distance. If negative, the offset is outside the polygon, otherwise inside. + + Returns + ------- + list[:class:`Polygon`] + The offset polygon(s). + + Raises + ------ + ValueError + If the normal of the polygon is not [0, 0, 1]. + """ + points = list(points) + normal = normal_polygon(points, True) + if not TOL.is_allclose(normal, [0, 0, 1]): + raise ValueError("The normal of the polygon should be [0, 0, 1]. The normal of the provided polygon is {}".format(normal)) + V = np.asarray(points, dtype=np.float64) + offset = float(offset) + if offset < 0: # outside + offset_polygons = straight_skeleton_2.create_offset_polygons_2_outer(V, abs(offset))[1:] # first one is box + else: # inside + offset_polygons = straight_skeleton_2.create_offset_polygons_2_inner(V, offset) + return [Polygon(points.tolist()) for points in offset_polygons] + + +def create_weighted_offset_polygons_2(points, offset, weights) -> list[Polygon]: + """Compute the polygon offset with weights. + + Parameters + ---------- + points : list of point coordinates or :class:`compas.geometry.Polygon` + The points of the polygon. + offset : float + The offset distance. If negative, the offset is outside the polygon, otherwise inside. + weights : list of float + The weights for each edge, starting with the edge between the last and the first point. + + Returns + ------- + list[:class:`Polygon`] + The offset polygon(s). + + Raises + ------ + ValueError + If the normal of the polygon is not [0, 0, 1]. + ValueError + If the number of weights does not match the number of points. + """ + points = list(points) + normal = normal_polygon(points, True) + if not TOL.is_allclose(normal, [0, 0, 1]): + raise ValueError("The normal of the polygon should be [0, 0, 1]. The normal of the provided polygon is {}".format(normal)) + + V = np.asarray(points, dtype=np.float64) + offset = float(offset) + W = np.asarray(weights, dtype=np.float64) + if W.shape[0] != V.shape[0]: + raise ValueError("The number of weights should be equal to the number of points %d != %d." % (W.shape[0], V.shape[0])) + if offset < 0: + offset_polygons = straight_skeleton_2.create_weighted_offset_polygons_2_outer(V, abs(offset), W)[1:] + else: + offset_polygons = straight_skeleton_2.create_weighted_offset_polygons_2_inner(V, offset, W) + return [Polygon(points.tolist()) for points in offset_polygons] diff --git a/src/straight_skeleton_2.cpp b/src/straight_skeleton_2.cpp index 5194546..dcbde6a 100644 --- a/src/straight_skeleton_2.cpp +++ b/src/straight_skeleton_2.cpp @@ -2,14 +2,21 @@ #include "straight_skeleton_2.h" #include #include +#include +#include +#include +#include typedef CGAL::Exact_predicates_inexact_constructions_kernel K; typedef K::Point_2 Point; typedef CGAL::Polygon_2 Polygon_2; +typedef CGAL::Polygon_with_holes_2 Polygon_with_holes; typedef CGAL::Straight_skeleton_2 Ss; typedef boost::shared_ptr SsPtr; typedef CGAL::Straight_skeleton_2::Halfedge_const_handle Halfedge_const_handle; typedef CGAL::Straight_skeleton_2::Vertex_const_handle Vertex_const_handle; +typedef boost::shared_ptr PolygonPtr; +typedef std::vector PolygonPtrVector; compas::Edges pmp_create_interior_straight_skeleton( Eigen::Ref &V) @@ -39,6 +46,152 @@ compas::Edges pmp_create_interior_straight_skeleton( return edgelist; }; +compas::Edges pmp_create_interior_straight_skeleton_with_holes( + Eigen::Ref &V, + std::vector> &holes) +{ + Polygon_2 outer; + for (int i = 0; i < V.rows(); i++) + { + outer.push_back(Point(V(i, 0), V(i, 1))); + } + Polygon_with_holes poly(outer); + + + for (auto hit : holes) + { + compas::RowMatrixXd H = hit; + Polygon_2 hole; + for (int i = 0; i < H.rows(); i++) + { + hole.push_back(Point(H(i, 0), H(i, 1))); + } + poly.add_hole(hole); + + } + + SsPtr iss = CGAL::create_interior_straight_skeleton_2(poly); + compas::Edges edgelist; + for(auto hit = iss->halfedges_begin(); hit != iss->halfedges_end(); ++hit){ + const Halfedge_const_handle h = hit; + if(!h->is_bisector()){ + continue; + } + const Vertex_const_handle& v1 = h->vertex(); + const Vertex_const_handle& v2 = h->opposite()->vertex(); + if(&*v1 < &*v2){ + std::vector s_vec = {v1->point().x(), v1->point().y(), 0}; + std::vector t_vec = {v2->point().x(), v2->point().y(), 0}; + compas::Edge edge = std::make_tuple(s_vec, t_vec); + edgelist.push_back(edge); + } + + } + return edgelist; + +} + +std::vector pmp_create_offset_polygons_2_inner(Eigen::Ref &V, double &offset){ + Polygon_2 poly; + for (int i = 0; i < V.rows(); i++){ + poly.push_back(Point(V(i, 0), V(i, 1))); + } + PolygonPtrVector offset_polygons = CGAL::create_interior_skeleton_and_offset_polygons_2(offset, poly); + + std::vector result; + for(auto pi = offset_polygons.begin(); pi != offset_polygons.end(); ++pi){ + std::size_t n = (*pi)->size(); + compas::RowMatrixXd points(n, 3); + int j = 0; + for (auto vi = (*pi)->vertices_begin(); vi != (*pi)->vertices_end(); ++vi){ + points(j, 0) = (double)(*vi).x(); + points(j, 1) = (double)(*vi).y(); + points(j, 2) = 0; + j++; + } + result.push_back(points); + } + return result; +} + +std::vector pmp_create_offset_polygons_2_outer(Eigen::Ref &V, double &offset){ + Polygon_2 poly; + for (int i = 0; i < V.rows(); i++){ + poly.push_back(Point(V(i, 0), V(i, 1))); + } + PolygonPtrVector offset_polygons = CGAL::create_exterior_skeleton_and_offset_polygons_2(offset, poly); + + std::vector result; + for(auto pi = offset_polygons.begin(); pi != offset_polygons.end(); ++pi){ + std::size_t n = (*pi)->size(); + compas::RowMatrixXd points(n, 3); + int j = 0; + for (auto vi = (*pi)->vertices_begin(); vi != (*pi)->vertices_end(); ++vi){ + points(j, 0) = (double)(*vi).x(); + points(j, 1) = (double)(*vi).y(); + points(j, 2) = 0; + j++; + } + result.push_back(points); + } + return result; +} + +std::vector pmp_create_weighted_offset_polygons_2_inner(Eigen::Ref &V, double &offset, Eigen::Ref &weights){ + Polygon_2 poly; + for (int i = 0; i < V.rows(); i++){ + poly.push_back(Point(V(i, 0), V(i, 1))); + } + std::vector weights_vec; + for (int i = 0; i < weights.rows(); i++){ + weights_vec.push_back(weights(i, 0)); + } + SsPtr iss = CGAL::create_interior_weighted_straight_skeleton_2(poly, weights_vec); + PolygonPtrVector offset_polygons = CGAL::create_offset_polygons_2(offset, *iss); + + std::vector result; + for(auto pi = offset_polygons.begin(); pi != offset_polygons.end(); ++pi){ + std::size_t n = (*pi)->size(); + compas::RowMatrixXd points(n, 3); + int j = 0; + for (auto vi = (*pi)->vertices_begin(); vi != (*pi)->vertices_end(); ++vi){ + points(j, 0) = (double)(*vi).x(); + points(j, 1) = (double)(*vi).y(); + points(j, 2) = 0; + j++; + } + result.push_back(points); + } + return result; +} + +std::vector pmp_create_weighted_offset_polygons_2_outer(Eigen::Ref &V, double &offset, Eigen::Ref &weights){ + Polygon_2 poly; + for (int i = 0; i < V.rows(); i++){ + poly.push_back(Point(V(i, 0), V(i, 1))); + } + std::vector weights_vec; + for (int i = 0; i < weights.rows(); i++){ + weights_vec.push_back(weights(i, 0)); + } + SsPtr iss = CGAL::create_exterior_weighted_straight_skeleton_2(offset, weights_vec, poly); + PolygonPtrVector offset_polygons = CGAL::create_offset_polygons_2(offset, *iss); + + std::vector result; + for(auto pi = offset_polygons.begin(); pi != offset_polygons.end(); ++pi){ + std::size_t n = (*pi)->size(); + compas::RowMatrixXd points(n, 3); + int j = 0; + for (auto vi = (*pi)->vertices_begin(); vi != (*pi)->vertices_end(); ++vi){ + points(j, 0) = (double)(*vi).x(); + points(j, 1) = (double)(*vi).y(); + points(j, 2) = 0; + j++; + } + result.push_back(points); + } + return result; +} // =========================================================================== // PyBind11 @@ -52,4 +205,37 @@ void init_straight_skeleton_2(pybind11::module &m) "create_interior_straight_skeleton", &pmp_create_interior_straight_skeleton, pybind11::arg("V").noconvert()); + + submodule.def( + "create_interior_straight_skeleton_with_holes", + &pmp_create_interior_straight_skeleton_with_holes, + pybind11::arg("V").noconvert(), + pybind11::arg("holes").noconvert()); + + submodule.def( + "create_offset_polygons_2_inner", + &pmp_create_offset_polygons_2_inner, + pybind11::arg("V").noconvert(), + pybind11::arg("offset").noconvert()); + + submodule.def( + "create_offset_polygons_2_outer", + &pmp_create_offset_polygons_2_outer, + pybind11::arg("V").noconvert(), + pybind11::arg("offset").noconvert()); + + submodule.def( + "create_weighted_offset_polygons_2_inner", + &pmp_create_weighted_offset_polygons_2_inner, + pybind11::arg("V").noconvert(), + pybind11::arg("offset").noconvert(), + pybind11::arg("weights").noconvert()); + + submodule.def( + "create_weighted_offset_polygons_2_outer", + &pmp_create_weighted_offset_polygons_2_outer, + pybind11::arg("V").noconvert(), + pybind11::arg("offset").noconvert(), + pybind11::arg("weights").noconvert()); + }; diff --git a/src/straight_skeleton_2.h b/src/straight_skeleton_2.h index d84b3c3..3b1cbad 100644 --- a/src/straight_skeleton_2.h +++ b/src/straight_skeleton_2.h @@ -7,4 +7,27 @@ compas::Edges pmp_create_interior_straight_skeleton( Eigen::Ref &V); + +compas::Edges pmp_create_interior_straight_skeleton_with_holes( + Eigen::Ref &V, + std::vector> &holes); + +std::vector pmp_create_offset_polygons_2_inner( + Eigen::Ref &V, + double &offset); + +std::vector pmp_create_offset_polygons_2_outer( + Eigen::Ref &V, + double &offset); + +std::vector pmp_create_weighted_offset_polygons_2_inner( + Eigen::Ref &V, + double &offset, + Eigen::Ref &weights); + +std::vector pmp_create_weighted_offset_polygons_2_outer( + Eigen::Ref &V, + double &offset, + Eigen::Ref &weights); + #endif /* COMPAS_STRAIGHT_SKELETON_2_H */ diff --git a/tests/test_straight_skeleton_2.py b/tests/test_straight_skeleton_2.py index 47e4fad..6286ed3 100644 --- a/tests/test_straight_skeleton_2.py +++ b/tests/test_straight_skeleton_2.py @@ -1,6 +1,10 @@ -from compas_cgal.straight_skeleton_2 import create_interior_straight_skeleton from compas.tolerance import TOL +from compas_cgal.straight_skeleton_2 import create_interior_straight_skeleton +from compas_cgal.straight_skeleton_2 import create_interior_straight_skeleton_with_holes +from compas_cgal.straight_skeleton_2 import create_offset_polygons_2 +from compas_cgal.straight_skeleton_2 import create_weighted_offset_polygons_2 + def test_straight_polygon(): points = [ @@ -17,6 +21,22 @@ def test_straight_polygon(): assert len(lines) == 8 +def test_straight_skeleton_with_holes(): + points = [ + (-1, -1, 0), + (0, -12, 0), + (1, -1, 0), + (12, 0, 0), + (1, 1, 0), + (0, 12, 0), + (-1, 1, 0), + (-12, 0, 0), + ] + hole = [(-1, 0, 0), (0, 1, 0), (1, 0, 0), (0, -1, 0)] + lines = create_interior_straight_skeleton_with_holes(points, [hole]) + assert len(lines) == 20 + + def test_straight_polygon_2_compare(): points = [ (-1.91, 3.59, 0.0), @@ -55,3 +75,26 @@ def test_straight_polygon_2_compare(): # the line direction sometimes changes ... assert TOL.is_allclose(sa, se) or TOL.is_allclose(sa, ee) assert TOL.is_allclose(ea, ee) or TOL.is_allclose(ea, se) + + +def test_offset(): + points = [ + (-1, -1, 0), + (0, -12, 0), + (1, -1, 0), + (12, 0, 0), + (1, 1, 0), + (0, 12, 0), + (-1, 1, 0), + (-12, 0, 0), + ] + offset = 0.5 + polygons = create_offset_polygons_2(points, offset) + assert len(polygons) == 1, len(polygons) + polygons = create_offset_polygons_2(points, -offset) + assert len(polygons) == 1, len(polygons) + weights = [0.1, 0.5, 0.3, 0.3, 0.9, 1.0, 0.2, 1.0] + polygons = create_weighted_offset_polygons_2(points, offset, weights) + assert len(polygons) == 1, len(polygons) + polygons = create_weighted_offset_polygons_2(points, -offset, weights) + assert len(polygons) == 1, len(polygons)