Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding weighted and unweighted polygon offsets using polygon skeletons #31

Merged
merged 12 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion docs/PLACEHOLDER

This file was deleted.

1 change: 0 additions & 1 deletion docs/_images/PLACEHOLDER

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions docs/examples/straight_skeleton_2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 37 additions & 0 deletions docs/examples/straight_skeleton_2_offset.py
Original file line number Diff line number Diff line change
@@ -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()
37 changes: 37 additions & 0 deletions docs/examples/straight_skeleton_2_offset_weighted.py
Original file line number Diff line number Diff line change
@@ -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()
92 changes: 85 additions & 7 deletions src/compas_cgal/straight_skeleton_2.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
from compas.geometry import Polygon
from compas.geometry import normal_polygon
from compas.tolerance import TOL

Expand Down Expand Up @@ -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)

Expand All @@ -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]
134 changes: 134 additions & 0 deletions src/straight_skeleton_2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#include <CGAL/Polygon_2.h>
#include <CGAL/create_straight_skeleton_2.h>
#include <CGAL/create_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/create_offset_polygons_2.h>
#include <CGAL/create_weighted_offset_polygons_from_polygon_with_holes_2.h>
#include <CGAL/create_weighted_straight_skeleton_2.h>

typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_2 Point;
Expand All @@ -12,6 +15,8 @@ typedef CGAL::Straight_skeleton_2<K> Ss;
typedef boost::shared_ptr<Ss> SsPtr;
typedef CGAL::Straight_skeleton_2<K>::Halfedge_const_handle Halfedge_const_handle;
typedef CGAL::Straight_skeleton_2<K>::Vertex_const_handle Vertex_const_handle;
typedef boost::shared_ptr<Polygon_2> PolygonPtr;
typedef std::vector<PolygonPtr> PolygonPtrVector;

compas::Edges pmp_create_interior_straight_skeleton(
Eigen::Ref<const compas::RowMatrixXd> &V)
Expand Down Expand Up @@ -86,6 +91,108 @@ compas::Edges pmp_create_interior_straight_skeleton_with_holes(

}

std::vector<compas::RowMatrixXd> pmp_create_offset_polygons_2_inner(Eigen::Ref<const compas::RowMatrixXd> &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<compas::RowMatrixXd> 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<compas::RowMatrixXd> pmp_create_offset_polygons_2_outer(Eigen::Ref<const compas::RowMatrixXd> &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<compas::RowMatrixXd> 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<compas::RowMatrixXd> pmp_create_weighted_offset_polygons_2_inner(Eigen::Ref<const compas::RowMatrixXd> &V, double &offset, Eigen::Ref<const compas::RowMatrixXd> &weights){
Polygon_2 poly;
for (int i = 0; i < V.rows(); i++){
poly.push_back(Point(V(i, 0), V(i, 1)));
}
std::vector<double> 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<Polygon_2>(offset, *iss);

std::vector<compas::RowMatrixXd> 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<compas::RowMatrixXd> pmp_create_weighted_offset_polygons_2_outer(Eigen::Ref<const compas::RowMatrixXd> &V, double &offset, Eigen::Ref<const compas::RowMatrixXd> &weights){
Polygon_2 poly;
for (int i = 0; i < V.rows(); i++){
poly.push_back(Point(V(i, 0), V(i, 1)));
}
std::vector<double> 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<Polygon_2>(offset, *iss);

std::vector<compas::RowMatrixXd> 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
// ===========================================================================
Expand All @@ -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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think noconvert is needed on basic types, but i am not sure...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i left that one as is


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());

};
Loading
Loading