diff --git a/CHANGELOG.md b/CHANGELOG.md index 317ca0a7..be265f9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +### Changed + +* Changed the return values of `compas_cgal.straight_skeleton_2.create_interior_straight_skeleton` and `compas_cgal.straight_skeleton_2.create_interior_straight_skeleton_with_holes`. +* Changed the return values of `compas_cgal.create_interior_straight_skeleton`. + +### Removed + + ## [0.7.0] 2024-05-14 ### Added diff --git a/docs/examples/straight_skeleton_2.py b/docs/examples/straight_skeleton_2.py index 03c1b0b3..0be3eb33 100644 --- a/docs/examples/straight_skeleton_2.py +++ b/docs/examples/straight_skeleton_2.py @@ -1,5 +1,3 @@ -from compas.datastructures import Graph -from compas.geometry import Polygon from compas_cgal.straight_skeleton_2 import create_interior_straight_skeleton from compas_viewer import Viewer @@ -15,15 +13,22 @@ (2.92, 4.03, 0.0), (-1.91, 3.59, 0.0), ] -polygon = Polygon(points) -lines = create_interior_straight_skeleton(points) -graph = Graph.from_lines(lines) + + +graph = create_interior_straight_skeleton(points) # ============================================================================== # Viz # ============================================================================== viewer = Viewer(width=1600, height=900) -viewer.scene.add(graph, edgecolor=(1.0, 0.0, 0.0)) -viewer.scene.add(polygon) +for edge in graph.edges(): + line = graph.edge_line(edge) + if graph.edge_attribute(edge, "inner_bisector"): + print(edge, "inner_bisector") + viewer.add(line, linecolor=(1.0, 0.0, 0.0), linewidth=2) + elif graph.edge_attribute(edge, "bisector"): + viewer.add(line, linecolor=(0.0, 0.0, 1.0)) + else: + viewer.add(line, linecolor=(0.0, 0.0, 0.0)) viewer.show() diff --git a/docs/examples/straight_skeleton_2_holes.py b/docs/examples/straight_skeleton_2_holes.py index bc2cd655..b1b91bfc 100644 --- a/docs/examples/straight_skeleton_2_holes.py +++ b/docs/examples/straight_skeleton_2_holes.py @@ -1,4 +1,3 @@ -from compas.datastructures import Graph from compas.geometry import Polygon from compas_viewer import Viewer @@ -26,17 +25,20 @@ polygon = Polygon(points) holes = [Polygon(hole) for hole in holes] -lines = create_interior_straight_skeleton_with_holes(polygon, holes) -graph = Graph.from_lines(lines) +graph = create_interior_straight_skeleton_with_holes(polygon, holes) # ============================================================================== # 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) + +for edge in graph.edges(): + line = graph.edge_line(edge) + if graph.edge_attribute(edge, "inner_bisector"): + viewer.add(line, linecolor=(1.0, 0.0, 0.0), linewidth=2) + elif graph.edge_attribute(edge, "bisector"): + viewer.add(line, linecolor=(0.0, 0.0, 1.0)) + else: + viewer.add(line, linecolor=(0.0, 0.0, 0.0)) viewer.show() diff --git a/src/compas_cgal/straight_skeleton_2.py b/src/compas_cgal/straight_skeleton_2.py index 31d94178..3da46494 100644 --- a/src/compas_cgal/straight_skeleton_2.py +++ b/src/compas_cgal/straight_skeleton_2.py @@ -1,24 +1,66 @@ +from typing import Tuple +from typing import Union + import numpy as np +from compas.datastructures import Graph from compas.geometry import Polygon from compas.geometry import normal_polygon from compas.tolerance import TOL from compas_cgal._cgal import straight_skeleton_2 -from .types import PolylinesNumpy +from .types import IntNx1 +from .types import IntNx2 +from .types import VerticesNumpy + +def graph_from_skeleton_data(points: VerticesNumpy, indices: IntNx1, edges: IntNx2, edge_types: IntNx1) -> Graph: + """Create a graph from the skeleton data. + + Parameters + ---------- + points : :class:`numpy.ndarray` + The vertices of the skeleton, each vertex defined by 3 spatial coordinates. + indices : :class:`numpy.ndarray` + The vertex indices of the skeleton, corresponding to the points. + edges : :class:`numpy.ndarray` + The edges of the skeleton, each edge defined by 2 vertex indices. + edge_types : :class:`numpy.ndarray` + The type per edge, `0` for inner bisector, `1` for bisector, and `2` for boundary. -def create_interior_straight_skeleton(points) -> PolylinesNumpy: + Returns + ------- + :class:`compas.datastructures.Graph` + The skeleton as a graph. + """ + graph = Graph() + for pt, i in zip(points, indices): + graph.add_node(key=i, x=pt[0], y=pt[1], z=pt[2]) + + for edge, etype in zip(edges, edge_types): + edge = graph.add_edge(*edge) + if etype == 0: + graph.edge_attribute(edge, "inner_bisector", True) + elif etype == 1: + graph.edge_attribute(edge, "bisector", True) + else: + graph.edge_attribute(edge, "boundary", True) + return graph + + +def create_interior_straight_skeleton(points, as_graph=True) -> Union[Graph, Tuple[VerticesNumpy, IntNx1, IntNx2, IntNx1]]: """Compute the skeleton of a polygon. Parameters ---------- points : list of point coordinates or :class:`compas.geometry.Polygon` The points of the polygon. + as_graph : bool, optional + Whether the skeleton should be returned as a graph, defaults to `True`. Returns ------- - :attr:`compas_cgal.types.PolylinesNumpy` + :attr:`compas.datastructures.Graph` or tuple of (vertices, indices, edges, edge_types) The skeleton of the polygon. Raises @@ -31,10 +73,13 @@ def create_interior_straight_skeleton(points) -> PolylinesNumpy: 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) + points, indices, edges, edge_types = straight_skeleton_2.create_interior_straight_skeleton(V) + if as_graph: + return graph_from_skeleton_data(points, indices, edges, edge_types) + return points, indices, edges, edge_types -def create_interior_straight_skeleton_with_holes(points, holes) -> PolylinesNumpy: +def create_interior_straight_skeleton_with_holes(points, holes, as_graph=True) -> Union[Graph, Tuple[VerticesNumpy, IntNx1, IntNx2, IntNx1]]: """Compute the skeleton of a polygon with holes. Parameters @@ -43,10 +88,12 @@ def create_interior_straight_skeleton_with_holes(points, holes) -> PolylinesNump The points of the polygon. holes : list of list of point coordinates or list of :class:`compas.geometry.Polygon` The holes of the polygon. + as_graph : bool, optional + Whether the skeleton should be returned as a graph, defaults to `True`. Returns ------- - :attr:`compas_cgal.types.PolylinesNumpy` + :attr:`compas.datastructures.Graph` or tuple of (vertices, indices, edges, edge_types) The skeleton of the polygon. Raises @@ -69,7 +116,10 @@ def create_interior_straight_skeleton_with_holes(points, holes) -> PolylinesNump 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) + points, indices, edges, edge_types = straight_skeleton_2.create_interior_straight_skeleton_with_holes(V, H) + if as_graph: + return graph_from_skeleton_data(points, indices, edges, edge_types) + return points, indices, edges, edge_types def create_offset_polygons_2(points, offset) -> list[Polygon]: diff --git a/src/compas_cgal/types.py b/src/compas_cgal/types.py index 319aabc4..ff5f6770 100644 --- a/src/compas_cgal/types.py +++ b/src/compas_cgal/types.py @@ -12,6 +12,8 @@ FloatNx3 = Annotated[NDArray[float64], Literal["N", 3]] IntNx3 = Annotated[NDArray[int64], Literal["N", 3]] +IntNx2 = Annotated[NDArray[int64], Literal["N", 2]] +IntNx1 = Annotated[NDArray[int64], Literal["N", 1]] VerticesNumpy = FloatNx3 """An array of vertices, with each vertex defined by 3 spatial coordinates.""" diff --git a/src/straight_skeleton_2.cpp b/src/straight_skeleton_2.cpp index dcbde6aa..95b04a86 100644 --- a/src/straight_skeleton_2.cpp +++ b/src/straight_skeleton_2.cpp @@ -18,35 +18,64 @@ 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) -{ - Polygon_2 poly; - for (int i = 0; i < V.rows(); i++) - { - poly.push_back(Point(V(i, 0), V(i, 1))); + +std::tuple, compas::RowMatrixXi, std::vector> mesh_data_from_skeleton(boost::shared_ptr &iss){ + std::size_t v = iss->size_of_vertices(); + std::size_t e = iss->size_of_halfedges() / 2; // halfedges are stored twice + + compas::RowMatrixXd Mv(v, 3); + std::vector Mvi; // to save the vertex ids + compas::RowMatrixXi Me(e, 2); + std::vector Mei; // to save the edge type: 0: inner bisector, 1: bisector, 2: boundary + + std::size_t i = 0; + for(auto hit = iss->vertices_begin(); hit != iss->vertices_end(); ++hit){ + const Vertex_const_handle vh = hit; + Mv(i, 0) = (double)vh->point().x(); + Mv(i, 1) = (double)vh->point().y(); + Mv(i, 2) = 0; + Mvi.push_back((int)vh->id()); + i++; } - SsPtr iss = CGAL::create_interior_straight_skeleton_2(poly.vertices_begin(), poly.vertices_end()); - compas::Edges edgelist; + i = 0; 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); + Me(i, 0) = (int)v1->id(); + Me(i, 1) = (int)v2->id(); + + if(h->is_inner_bisector()){ + Mei.push_back(0); + } + else if(h->is_bisector()){ + Mei.push_back(1); + }else{ + Mei.push_back(2); + } + i++; } + } + std::tuple, compas::RowMatrixXi, std::vector> result = std::make_tuple(Mv, Mvi, Me, Mei); + return result; +} +std::tuple, compas::RowMatrixXi, std::vector> pmp_create_interior_straight_skeleton( + Eigen::Ref &V) +{ + Polygon_2 poly; + for (int i = 0; i < V.rows(); i++) + { + poly.push_back(Point(V(i, 0), V(i, 1))); } - return edgelist; + SsPtr iss = CGAL::create_interior_straight_skeleton_2(poly.vertices_begin(), poly.vertices_end()); + + return mesh_data_from_skeleton(iss); }; -compas::Edges pmp_create_interior_straight_skeleton_with_holes( +std::tuple, compas::RowMatrixXi, std::vector> pmp_create_interior_straight_skeleton_with_holes( Eigen::Ref &V, std::vector> &holes) { @@ -57,7 +86,6 @@ compas::Edges pmp_create_interior_straight_skeleton_with_holes( } Polygon_with_holes poly(outer); - for (auto hit : holes) { compas::RowMatrixXd H = hit; @@ -71,24 +99,7 @@ compas::Edges pmp_create_interior_straight_skeleton_with_holes( } 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; - + return mesh_data_from_skeleton(iss); } std::vector pmp_create_offset_polygons_2_inner(Eigen::Ref &V, double &offset){ diff --git a/src/straight_skeleton_2.h b/src/straight_skeleton_2.h index 3b1cbadf..1d5f250d 100644 --- a/src/straight_skeleton_2.h +++ b/src/straight_skeleton_2.h @@ -4,11 +4,11 @@ #include -compas::Edges pmp_create_interior_straight_skeleton( +std::tuple, compas::RowMatrixXi, std::vector> pmp_create_interior_straight_skeleton( Eigen::Ref &V); -compas::Edges pmp_create_interior_straight_skeleton_with_holes( +std::tuple, compas::RowMatrixXi, std::vector> pmp_create_interior_straight_skeleton_with_holes( Eigen::Ref &V, std::vector> &holes); diff --git a/tests/test_straight_skeleton_2.py b/tests/test_straight_skeleton_2.py index 6286ed32..2bc5ba7e 100644 --- a/tests/test_straight_skeleton_2.py +++ b/tests/test_straight_skeleton_2.py @@ -1,5 +1,3 @@ -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 @@ -17,8 +15,8 @@ def test_straight_polygon(): (-1, 1, 0), (-12, 0, 0), ] - lines = create_interior_straight_skeleton(points) - assert len(lines) == 8 + graph = create_interior_straight_skeleton(points) + assert graph.number_of_edges() == 16 def test_straight_skeleton_with_holes(): @@ -33,48 +31,8 @@ def test_straight_skeleton_with_holes(): (-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), - (-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), - ] - lines = create_interior_straight_skeleton(points) - - expected = [ - [[-1.91, 3.59, 0.0], [-0.139446292, 1.191439787, 0.0]], - [[-5.53, -5.22, 0.0], [-0.139446292, 1.191439787, 0.0]], - [[-0.39, -1.98, 0.0], [0.008499564, 1.241560466, 0.0]], - [[2.98, -5.51, 0.0], [2.44972507, -1.674799065, 0.0]], - [[4.83, -2.02, 0.0], [4.228131167, -0.522007766, 0.0]], - [[8.663865218, -1.084821998, 0.0], [9.7, -3.63, 0.0]], - [[12.23, 1.25, 0.0], [8.663865218, -1.084821998, 0.0]], - [[3.42, 0.66, 0.0], [1.755862468, -1.404991433, 0.0]], - [[2.92, 4.03, 0.0], [0.563706846, 1.033296141, 0.0]], - [[4.228131167, -0.522007766, 0.0], [2.44972507, -1.674799065, 0.0]], - [[4.228131167, -0.522007766, 0.0], [8.663865218, -1.084821998, 0.0]], - [[1.755862468, -1.404991433, 0.0], [2.44972507, -1.674799065, 0.0]], - [[0.563706846, 1.033296141, 0.0], [1.755862468, -1.404991433, 0.0]], - [[-0.139446292, 1.191439787, 0.0], [0.008499564, 1.241560466, 0.0]], - [[0.563706846, 1.033296141, 0.0], [0.008499564, 1.241560466, 0.0]], - ] - for act, exp in zip(lines, expected): - sa, ea = act - se, ee = exp - # 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) + graph = create_interior_straight_skeleton_with_holes(points, [hole]) + assert graph.number_of_edges() == 32 def test_offset():