diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7c8b07..317ca0a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * 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 f9768c10..00000000 --- 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 48f73ebf..00000000 --- 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_offset.png b/docs/_images/cgal_straight_skeleton_2_offset.png new file mode 100644 index 00000000..0868a50b 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 00000000..f9ffa8b7 Binary files /dev/null and b/docs/_images/cgal_straight_skeleton_2_offset_weighted.png differ diff --git a/docs/examples/straight_skeleton_2.rst b/docs/examples/straight_skeleton_2.rst index cc1a9bf7..9d33bbbc 100644 --- a/docs/examples/straight_skeleton_2.rst +++ b/docs/examples/straight_skeleton_2.rst @@ -18,3 +18,21 @@ .. 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_offset.py b/docs/examples/straight_skeleton_2_offset.py new file mode 100644 index 00000000..c2dfd464 --- /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 00000000..b7ce2b05 --- /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 38b9eff5..31d94178 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,8 +27,9 @@ 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) @@ -54,15 +56,91 @@ def create_interior_straight_skeleton_with_holes(points, holes) -> PolylinesNump If the normal of a hole 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) H = [] - for hole in holes: + for i, hole in enumerate(holes): points = list(hole) - if not TOL.is_allclose(normal_polygon(points, True), [0, 0, -1]): - raise ValueError("Please pass a hole with a normal vector of [0, 0, -1].") + 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 02f16972..dcbde6aa 100644 --- a/src/straight_skeleton_2.cpp +++ b/src/straight_skeleton_2.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include typedef CGAL::Exact_predicates_inexact_constructions_kernel K; typedef K::Point_2 Point; @@ -12,6 +15,8 @@ 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) @@ -86,6 +91,108 @@ compas::Edges pmp_create_interior_straight_skeleton_with_holes( } +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 // =========================================================================== @@ -104,4 +211,31 @@ void init_straight_skeleton_2(pybind11::module &m) &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 baad3171..3b1cbadf 100644 --- a/src/straight_skeleton_2.h +++ b/src/straight_skeleton_2.h @@ -12,4 +12,22 @@ 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 80d419d4..6286ed32 100644 --- a/tests/test_straight_skeleton_2.py +++ b/tests/test_straight_skeleton_2.py @@ -2,6 +2,8 @@ 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(): @@ -73,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)