diff --git a/.gitignore b/.gitignore index 794aebff..6c47577b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ __pycache__ igl.egg-info libigl.egg-info *.code-workspace -out.obj +out.* test.off .vscode @@ -41,3 +41,4 @@ junit dist/ igl/* CLAUDE.md +wheelhouse/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 6975bde2..341a6f95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,8 @@ option(LIBIGL_COPYLEFT_CGAL "Build target igl_copyleft::cgal" ON) option(LIBIGL_EMBREE "Build target igl::embree" ON) option(LIBIGL_COPYLEFT_TETGEN "Build target igl_copyleft::tetgen" ON) option(LIBIGL_RESTRICTED_TRIANGLE "Build target igl_restricted::triangle" ON) +option(LIBIGL_SPECTRA "Build igl::spectra bindings" ON) +option(LIBIGL_PREDICATES "Build igl::predicates bindings" ON) FetchContent_Declare( libigl GIT_REPOSITORY https://github.com/libigl/libigl.git @@ -182,6 +184,12 @@ endif() if(LIBIGL_RESTRICTED_TRIANGLE) pyigl_include("restricted" "triangle") endif() +if(LIBIGL_SPECTRA) + pyigl_include("" "spectra") +endif() +if(LIBIGL_PREDICATES) + pyigl_include("" "predicates") +endif() diff --git a/README.md b/README.md index f38e79c4..2ed171e7 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ libigl_version = importlib.metadata.version('libigl') The version of libigl is defined in the [pyproject.toml](./pyproject.toml) file. -## Compiling and modifying the bindiings +## Compiling and modifying the bindings According to the [scikit-build-core documentation](https://scikit-build-core.readthedocs.io/en/latest/configuration.html#editable-installs), the way to make an editable (incremental) build is to: @@ -69,6 +69,20 @@ test in `tests/test_all.py` to ensure the binding can at least be called as expected. +### Packaging a wheel from an existing build + +If you already have a compiled build directory, you can package it into a +`.whl` without recompiling and install that wheel into other virtual +environments on the same machine: + +``` +python -m pip wheel --no-build-isolation --no-deps -Cbuild-dir=build -w wheelhouse . +pip install wheelhouse/libigl-*.whl +``` + +The `--no-deps` flag skips bundling numpy/scipy (the target environment must +have them already). No compiler or CMake is required in the target environment. + ## Testing cibuildwheel locally Install whichever version of Python from the [official website](https://www.python.org/downloads/) and then run: diff --git a/pyproject.toml b/pyproject.toml index 0b992f68..c4d4d064 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ build-backend = "scikit_build_core.build" [project] name = "libigl" -version = "2.6.3.dev2" +version = "2.6.3.dev3" description = "libigl: A simple C++ geometry processing library" readme = "README.md" requires-python = ">=3.8" diff --git a/src/lexicographic_triangulation.cpp b/src/lexicographic_triangulation.cpp new file mode 100644 index 00000000..e7cdf970 --- /dev/null +++ b/src/lexicographic_triangulation.cpp @@ -0,0 +1,47 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + auto lexicographic_triangulation( + const Eigen::MatrixXN &P) + { + auto orient2D = [](const Numeric* pa, const Numeric* pb, const Numeric* pc) -> int + { + const Numeric cross = + (pb[0] - pa[0]) * (pc[1] - pa[1]) - + (pb[1] - pa[1]) * (pc[0] - pa[0]); + return (cross > 0) ? 1 : (cross < 0) ? -1 : 0; + }; + Eigen::MatrixXi F; + try + { + igl::lexicographic_triangulation(P, orient2D, F); + } + catch(const char* e) + { + throw std::runtime_error(e); + } + return F; + } +} + +void bind_lexicographic_triangulation(nb::module_ &m) +{ + m.def( + "lexicographic_triangulation", + &pyigl::lexicographic_triangulation, + "P"_a, +R"(Compute a lexicographic triangulation of a 2D point set. + +Uses a floating-point cross-product orientation predicate; may be inaccurate +for nearly-collinear inputs. + +@param[in] P #P by 2 list of 2D vertex positions +@param[out] F #F by 3 list of face indices into P)"); +} diff --git a/src/predicates/Orientation.cpp b/src/predicates/Orientation.cpp new file mode 100644 index 00000000..257f22a6 --- /dev/null +++ b/src/predicates/Orientation.cpp @@ -0,0 +1,21 @@ +#include "default_types.h" +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +void bind_Orientation(nb::module_ &m) +{ + nb::enum_(m, "Orientation") + .value("POSITIVE", igl::predicates::Orientation::POSITIVE) + .value("INSIDE", igl::predicates::Orientation::INSIDE) + .value("NEGATIVE", igl::predicates::Orientation::NEGATIVE) + .value("OUTSIDE", igl::predicates::Orientation::OUTSIDE) + .value("COLLINEAR", igl::predicates::Orientation::COLLINEAR) + .value("COPLANAR", igl::predicates::Orientation::COPLANAR) + .value("COCIRCULAR", igl::predicates::Orientation::COCIRCULAR) + .value("COSPHERICAL", igl::predicates::Orientation::COSPHERICAL) + .value("DEGENERATE", igl::predicates::Orientation::DEGENERATE) + .export_values(); +} diff --git a/src/predicates/delaunay_triangulation.cpp b/src/predicates/delaunay_triangulation.cpp new file mode 100644 index 00000000..820abe5d --- /dev/null +++ b/src/predicates/delaunay_triangulation.cpp @@ -0,0 +1,30 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + Eigen::MatrixXi delaunay_triangulation( + const Eigen::MatrixXN &V) + { + Eigen::MatrixXi F; + igl::predicates::delaunay_triangulation(V, F); + return F; + } +} + +void bind_delaunay_triangulation(nb::module_ &m) +{ + m.def( + "delaunay_triangulation", + &pyigl::delaunay_triangulation, + "V"_a, +R"(Compute a Delaunay triangulation of a 2D point set using exact predicates. + +@param[in] V #V by 2 list of 2D vertex positions +@return F #F by 3 list of triangle indices into V.)"); +} diff --git a/src/predicates/ear_clipping.cpp b/src/predicates/ear_clipping.cpp new file mode 100644 index 00000000..9d1a2b82 --- /dev/null +++ b/src/predicates/ear_clipping.cpp @@ -0,0 +1,56 @@ +#include "default_types.h" +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + std::tuple ear_clipping( + const Eigen::MatrixXN &P) + { + Eigen::MatrixXi eF; + bool ok = igl::predicates::ear_clipping(P, eF); + return {ok, eF}; + } + + std::tuple ear_clipping_with_reserved( + const Eigen::MatrixXN &P, + const Eigen::MatrixXI &RT) + { + Eigen::MatrixXi eF; + Eigen::VectorXi I; + Eigen::VectorXi RT_int = RT.cast(); + igl::predicates::ear_clipping(P, RT_int, eF, I); + Eigen::MatrixXI I_out = I.cast(); + return {eF, I_out}; + } +} + +void bind_ear_clipping(nb::module_ &m) +{ + m.def( + "ear_clipping", + &pyigl::ear_clipping, + "P"_a, +R"(Ear-clipping triangulation of a 2D polygon. + +Reverses P if necessary so output orientation matches input. + +@param[in] P #P by 2 list of 2D polygon vertices (CCW for proper mesh) +@return (ok, eF) where ok is true if mesh is proper (input is a simple polygon) + and eF is #eF by 3 list of triangle indices into P.)"); + m.def( + "ear_clipping", + &pyigl::ear_clipping_with_reserved, + "P"_a, "RT"_a, +R"(Ear-clipping triangulation with reserved (non-clippable) vertices. + +@param[in] P #P by 2 list of 2D polygon vertices +@param[in] RT #P list, vertices marked 1 are preserved (not clipped) +@return (eF, I) where eF is the clipped ears (original indices into P) and + I maps indices of the remaining polygon to original P indices.)"); +} diff --git a/src/predicates/find_intersections.cpp b/src/predicates/find_intersections.cpp new file mode 100644 index 00000000..10dae56c --- /dev/null +++ b/src/predicates/find_intersections.cpp @@ -0,0 +1,49 @@ +#include "default_types.h" +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + std::tuple find_intersections( + const Eigen::MatrixXN &V1, + const Eigen::MatrixXI &F1, + const Eigen::MatrixXN &V2, + const Eigen::MatrixXI &F2, + const bool first_only) + { + Eigen::MatrixXi IF; + Eigen::Matrix CP_bool; + Eigen::MatrixXi F1_int = F1.cast(); + Eigen::MatrixXi F2_int = F2.cast(); + bool found = igl::predicates::find_intersections( + V1, F1_int, V2, F2_int, first_only, IF, CP_bool); + Eigen::MatrixXI IF_out = IF.cast(); + Eigen::MatrixXI CP_out = CP_bool.cast(); + return {found, IF_out, CP_out}; + } +} + +void bind_find_intersections(nb::module_ &m) +{ + m.def( + "find_intersections", + &pyigl::find_intersections, + "V1"_a, "F1"_a, "V2"_a, "F2"_a, "first_only"_a = false, +R"(Find intersecting triangle pairs between two meshes. + +Uses AABB tree and exact predicates. + +@param[in] V1 #V1 by 3 vertex positions of first mesh +@param[in] F1 #F1 by 3 triangle indices of first mesh +@param[in] V2 #V2 by 3 vertex positions of second mesh +@param[in] F2 #F2 by 3 triangle indices of second mesh +@param[in] first_only stop after finding the first intersection +@return (found, IF, CP) where found is true if any intersections exist, + IF is #IF by 2 list of intersecting face index pairs (into F1, F2), + CP is #IF list of whether each intersection is coplanar.)"); +} diff --git a/src/predicates/find_self_intersections.cpp b/src/predicates/find_self_intersections.cpp new file mode 100644 index 00000000..49b10716 --- /dev/null +++ b/src/predicates/find_self_intersections.cpp @@ -0,0 +1,43 @@ +#include "default_types.h" +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + std::tuple find_self_intersections( + const Eigen::MatrixXN &V, + const Eigen::MatrixXI &F, + const bool first_only) + { + Eigen::MatrixXi IF; + Eigen::Matrix CP_bool; + Eigen::MatrixXi F_int = F.cast(); + bool found = igl::predicates::find_self_intersections(V, F_int, first_only, IF, CP_bool); + Eigen::MatrixXI IF_out = IF.cast(); + Eigen::MatrixXI CP_out = CP_bool.cast(); + return {found, IF_out, CP_out}; + } +} + +void bind_find_self_intersections(nb::module_ &m) +{ + m.def( + "find_self_intersections", + &pyigl::find_self_intersections, + "V"_a, "F"_a, "first_only"_a = false, +R"(Find self-intersecting triangle pairs within a mesh. + +Uses AABB tree and exact predicates. + +@param[in] V #V by 3 vertex positions +@param[in] F #F by 3 triangle indices +@param[in] first_only stop after finding the first intersection +@return (found, IF, CP) where found is true if any self-intersections exist, + IF is #IF by 2 list of intersecting face index pairs, + CP is #IF list of whether each intersection is coplanar.)"); +} diff --git a/src/predicates/incircle.cpp b/src/predicates/incircle.cpp new file mode 100644 index 00000000..82b9965d --- /dev/null +++ b/src/predicates/incircle.cpp @@ -0,0 +1,37 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + igl::predicates::Orientation incircle( + const Eigen::Vector2d &pa, + const Eigen::Vector2d &pb, + const Eigen::Vector2d &pc, + const Eigen::Vector2d &pd) + { + return igl::predicates::incircle(pa, pb, pc, pd); + } +} + +void bind_incircle(nb::module_ &m) +{ + m.def( + "incircle", + &pyigl::incircle, + "pa"_a, "pb"_a, "pc"_a, "pd"_a, +R"(Decide whether a 2D point is inside/outside/on a circle. + +Uses exact arithmetic (Shewchuk predicates). + +@param[in] pa 2D point on circle +@param[in] pb 2D point on circle +@param[in] pc 2D point on circle +@param[in] pd 2D query point +@return INSIDE if pd is inside the circle defined by pa,pb,pc, + OUTSIDE if outside, COCIRCULAR if exactly on the circle.)"); +} diff --git a/src/predicates/insphere.cpp b/src/predicates/insphere.cpp new file mode 100644 index 00000000..9483d192 --- /dev/null +++ b/src/predicates/insphere.cpp @@ -0,0 +1,39 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + igl::predicates::Orientation insphere( + const Eigen::Vector3d &pa, + const Eigen::Vector3d &pb, + const Eigen::Vector3d &pc, + const Eigen::Vector3d &pd, + const Eigen::Vector3d &pe) + { + return igl::predicates::insphere(pa, pb, pc, pd, pe); + } +} + +void bind_insphere(nb::module_ &m) +{ + m.def( + "insphere", + &pyigl::insphere, + "pa"_a, "pb"_a, "pc"_a, "pd"_a, "pe"_a, +R"(Decide whether a 3D point is inside/outside/on a sphere. + +Uses exact arithmetic (Shewchuk predicates). + +@param[in] pa 3D point on sphere +@param[in] pb 3D point on sphere +@param[in] pc 3D point on sphere +@param[in] pd 3D point on sphere +@param[in] pe 3D query point +@return INSIDE if pe is inside the sphere defined by pa,pb,pc,pd, + OUTSIDE if outside, COSPHERICAL if exactly on the sphere.)"); +} diff --git a/src/predicates/lexicographic_triangulation.cpp b/src/predicates/lexicographic_triangulation.cpp new file mode 100644 index 00000000..e928c044 --- /dev/null +++ b/src/predicates/lexicographic_triangulation.cpp @@ -0,0 +1,30 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + Eigen::MatrixXi lexicographic_triangulation( + const Eigen::MatrixXN &V) + { + Eigen::MatrixXi F; + igl::predicates::lexicographic_triangulation(V, F); + return F; + } +} + +void bind_lexicographic_triangulation(nb::module_ &m) +{ + m.def( + "lexicographic_triangulation", + &pyigl::lexicographic_triangulation, + "V"_a, +R"(Compute a lexicographic triangulation of a 2D point set using exact predicates. + +@param[in] V #V by 2 list of 2D vertex positions +@return F #F by 3 list of triangle indices into V.)"); +} diff --git a/src/predicates/module.cpp b/src/predicates/module.cpp new file mode 100644 index 00000000..6b5e8801 --- /dev/null +++ b/src/predicates/module.cpp @@ -0,0 +1,9 @@ +#include +#include +namespace nb = nanobind; +#include "predicates/BINDING_DECLARATIONS.in" +NB_MODULE(pyigl_predicates, m) { + m.doc() = "libigl predicates module python bindings"; + igl::predicates::exactinit(); + #include "predicates/BINDING_INVOCATIONS.in" +} diff --git a/src/predicates/orient2d.cpp b/src/predicates/orient2d.cpp new file mode 100644 index 00000000..de14f38a --- /dev/null +++ b/src/predicates/orient2d.cpp @@ -0,0 +1,35 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + igl::predicates::Orientation orient2d( + const Eigen::Vector2d &pa, + const Eigen::Vector2d &pb, + const Eigen::Vector2d &pc) + { + return igl::predicates::orient2d(pa, pb, pc); + } +} + +void bind_orient2d(nb::module_ &m) +{ + m.def( + "orient2d", + &pyigl::orient2d, + "pa"_a, "pb"_a, "pc"_a, +R"(Compute the orientation of the triangle formed by pa, pb, pc. + +Uses exact arithmetic (Shewchuk predicates). + +@param[in] pa 2D point +@param[in] pb 2D point +@param[in] pc 2D point +@return POSITIVE if pa,pb,pc are counterclockwise, NEGATIVE if clockwise, + COLLINEAR if collinear.)"); +} diff --git a/src/predicates/orient3d.cpp b/src/predicates/orient3d.cpp new file mode 100644 index 00000000..923782f6 --- /dev/null +++ b/src/predicates/orient3d.cpp @@ -0,0 +1,59 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + igl::predicates::Orientation orient3d_scalar( + const Eigen::Vector3d &pa, + const Eigen::Vector3d &pb, + const Eigen::Vector3d &pc, + const Eigen::Vector3d &pd) + { + return igl::predicates::orient3d(pa, pb, pc, pd); + } + + Eigen::VectorXi orient3d_vectorized( + const Eigen::MatrixXN &A, + const Eigen::MatrixXN &B, + const Eigen::MatrixXN &C, + const Eigen::MatrixXN &D) + { + Eigen::VectorXi R; + igl::predicates::orient3d(A, B, C, D, R); + return R; + } +} + +void bind_orient3d(nb::module_ &m) +{ + m.def( + "orient3d", + &pyigl::orient3d_scalar, + "pa"_a, "pb"_a, "pc"_a, "pd"_a, +R"(Compute the orientation of the tetrahedron formed by pa, pb, pc, pd. + +Uses exact arithmetic (Shewchuk predicates). + +@param[in] pa 3D point +@param[in] pb 3D point +@param[in] pc 3D point +@param[in] pd 3D query point +@return POSITIVE if pd is below the plane oriented by pa,pb,pc, + NEGATIVE if above, COPLANAR if on the plane.)"); + m.def( + "orient3d", + &pyigl::orient3d_vectorized, + "A"_a, "B"_a, "C"_a, "D"_a, +R"(Vectorized orient3d: compute orientation for each row-tuple (A[i], B[i], C[i], D[i]). + +@param[in] A #P by 3 matrix of 3D points +@param[in] B #P by 3 matrix of 3D points +@param[in] C #P by 3 matrix of 3D points +@param[in] D #P by 3 matrix of 3D points +@return #P vector of orientation values (+1, -1, or 0))"); +} diff --git a/src/predicates/point_inside_convex_polygon.cpp b/src/predicates/point_inside_convex_polygon.cpp new file mode 100644 index 00000000..3200005c --- /dev/null +++ b/src/predicates/point_inside_convex_polygon.cpp @@ -0,0 +1,30 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + bool point_inside_convex_polygon( + const Eigen::MatrixX2d &P, + const Eigen::RowVector2d &q) + { + return igl::predicates::point_inside_convex_polygon(P, q); + } +} + +void bind_point_inside_convex_polygon(nb::module_ &m) +{ + m.def( + "point_inside_convex_polygon", + &pyigl::point_inside_convex_polygon, + "P"_a, "q"_a, +R"(Check whether a 2D point lies inside a 2D convex polygon. + +@param[in] P #P by 2 list of polygon vertices (n >= 3) +@param[in] q 2D query point +@return true if q is inside P.)"); +} diff --git a/src/predicates/polygons_to_triangles.cpp b/src/predicates/polygons_to_triangles.cpp new file mode 100644 index 00000000..788b7386 --- /dev/null +++ b/src/predicates/polygons_to_triangles.cpp @@ -0,0 +1,40 @@ +#include "default_types.h" +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + std::tuple polygons_to_triangles( + const Eigen::MatrixXN &V, + const Eigen::VectorXI &I, + const Eigen::VectorXI &C) + { + Eigen::VectorXi I_int = I.cast(); + Eigen::VectorXi C_int = C.cast(); + Eigen::MatrixXi F; + Eigen::VectorXi J; + igl::predicates::polygons_to_triangles(V, I_int, C_int, F, J); + Eigen::VectorXI J_out = J.cast(); + return {F, J_out}; + } +} + +void bind_polygons_to_triangles(nb::module_ &m) +{ + m.def( + "polygons_to_triangles", + &pyigl::polygons_to_triangles, + "V"_a, "I"_a, "C"_a, +R"(Triangulate each polygon of a polygon mesh with a fan. + +@param[in] V #V by dim vertex positions (used for orientation in 3D) +@param[in] I #I list of polygon corner indices into rows of V +@param[in] C #polygons+1 cumulative polygon sizes (C[i+1]-C[i] = size of polygon i) +@return (F, J) where F is #F by 3 list of triangle indices and J is #F list of + source polygon indices.)"); +} diff --git a/src/predicates/segment_segment_intersect.cpp b/src/predicates/segment_segment_intersect.cpp new file mode 100644 index 00000000..087999e0 --- /dev/null +++ b/src/predicates/segment_segment_intersect.cpp @@ -0,0 +1,34 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + bool segment_segment_intersect( + const Eigen::Vector2d &A, + const Eigen::Vector2d &B, + const Eigen::Vector2d &C, + const Eigen::Vector2d &D) + { + return igl::predicates::segment_segment_intersect(A, B, C, D); + } +} + +void bind_segment_segment_intersect(nb::module_ &m) +{ + m.def( + "segment_segment_intersect", + &pyigl::segment_segment_intersect, + "A"_a, "B"_a, "C"_a, "D"_a, +R"(Test whether two 2D segments intersect using exact orient2d predicates. + +@param[in] A 1st endpoint of segment 1 +@param[in] B 2nd endpoint of segment 1 +@param[in] C 1st endpoint of segment 2 +@param[in] D 2nd endpoint of segment 2 +@return true if the segments intersect.)"); +} diff --git a/src/predicates/triangle_triangle_intersect.cpp b/src/predicates/triangle_triangle_intersect.cpp new file mode 100644 index 00000000..2bec5b3b --- /dev/null +++ b/src/predicates/triangle_triangle_intersect.cpp @@ -0,0 +1,43 @@ +#include "default_types.h" +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + std::tuple triangle_triangle_intersect( + const Eigen::Vector3d &a1, + const Eigen::Vector3d &a2, + const Eigen::Vector3d &a3, + const Eigen::Vector3d &b1, + const Eigen::Vector3d &b2, + const Eigen::Vector3d &b3) + { + bool coplanar = false; + bool result = igl::predicates::triangle_triangle_intersect( + a1, a2, a3, b1, b2, b3, coplanar); + return {result, coplanar}; + } +} + +void bind_triangle_triangle_intersect(nb::module_ &m) +{ + m.def( + "triangle_triangle_intersect", + &pyigl::triangle_triangle_intersect, + "a1"_a, "a2"_a, "a3"_a, "b1"_a, "b2"_a, "b3"_a, +R"(Test whether two 3D triangles intersect using exact predicates. + +@param[in] a1 1st vertex of triangle A +@param[in] a2 2nd vertex of triangle A +@param[in] a3 3rd vertex of triangle A +@param[in] b1 1st vertex of triangle B +@param[in] b2 2nd vertex of triangle B +@param[in] b3 3rd vertex of triangle B +@return (intersects, coplanar) where intersects is true if the triangles + intersect, and coplanar is true if they are coplanar.)"); +} diff --git a/src/spectra/EigsType.cpp b/src/spectra/EigsType.cpp new file mode 100644 index 00000000..328bcf4e --- /dev/null +++ b/src/spectra/EigsType.cpp @@ -0,0 +1,12 @@ +#include +#include + +namespace nb = nanobind; + +void bind_EigsType(nb::module_ &m) +{ + nb::enum_(m, "EigsType") + .value("EIGS_TYPE_SM", igl::EigsType::EIGS_TYPE_SM) + .value("EIGS_TYPE_LM", igl::EigsType::EIGS_TYPE_LM) + .export_values(); +} diff --git a/src/spectra/eigs.cpp b/src/spectra/eigs.cpp new file mode 100644 index 00000000..79f0ffbd --- /dev/null +++ b/src/spectra/eigs.cpp @@ -0,0 +1,78 @@ +#include "default_types.h" +#include +#include +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + auto eigs_type( + const Eigen::SparseMatrixN &A, + const Eigen::SparseMatrixN &B, + const int k, + const igl::EigsType type) + { + Eigen::MatrixXN U; + Eigen::VectorXN S; + if(!igl::spectra::eigs(A, B, k, type, U, S)) + { + throw std::runtime_error("igl::spectra::eigs failed"); + } + return std::make_tuple(U, S); + } + + auto eigs_sigma( + const Eigen::SparseMatrixN &A, + const Eigen::SparseMatrixN &B, + const int k, + const Numeric sigma) + { + Eigen::MatrixXN U; + Eigen::VectorXN S; + if(!igl::spectra::eigs(A, B, k, sigma, U, S)) + { + throw std::runtime_error("igl::spectra::eigs failed"); + } + return std::make_tuple(U, S); + } +} + +void bind_eigs(nb::module_ &m) +{ + m.def( + "eigs", + &pyigl::eigs_type, + "A"_a, "B"_a, "k"_a, "type"_a, +R"(Compute the first/last k eigen pairs of the generalized eigenvalue problem: + + A u = s B u + +Solutions are approximate and sorted. Only EIGS_TYPE_SM (smallest magnitude) +is well supported. + +@param[in] A #A by #A symmetric sparse matrix +@param[in] B #A by #A symmetric positive-definite sparse matrix +@param[in] k number of eigen pairs to compute +@param[in] type EigsType.EIGS_TYPE_SM or EigsType.EIGS_TYPE_LM +@param[out] U #A by k matrix of sorted eigenvectors (descending) +@param[out] S k vector of sorted eigenvalues (descending) +@return Tuple (U, S))"); + + m.def( + "eigs", + &pyigl::eigs_sigma, + "A"_a, "B"_a, "k"_a, "sigma"_a, +R"(Compute k eigen pairs of A u = s B u with a shift: A ← A + sigma * B. + +@param[in] A #A by #A symmetric sparse matrix +@param[in] B #A by #A symmetric positive-definite sparse matrix +@param[in] k number of eigen pairs to compute +@param[in] sigma shift to apply to A +@param[out] U #A by k matrix of sorted eigenvectors (descending) +@param[out] S k vector of sorted eigenvalues (descending) +@return Tuple (U, S))"); +} diff --git a/src/spectra/lscm.cpp b/src/spectra/lscm.cpp new file mode 100644 index 00000000..c8e9f6f0 --- /dev/null +++ b/src/spectra/lscm.cpp @@ -0,0 +1,40 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + auto lscm( + const nb::DRef &V, + const nb::DRef &F) + { + Eigen::MatrixXN UV; + if(!igl::spectra::lscm(V, F, UV)) + { + throw std::runtime_error("igl::spectra::lscm failed"); + } + return UV; + } +} + +void bind_lscm(nb::module_ &m) +{ + m.def( + "lscm", + &pyigl::lscm, + "V"_a, "F"_a, +R"(Compute a free-boundary least-squares conformal map parametrization using +spectral decomposition. Assumes the mesh is a single connected component +topologically equivalent to a disk. + +Implements "Spectral Conformal Parameterization" [Mullen et al. 2008]. + +@param[in] V #V by 3 list of mesh vertex positions +@param[in] F #F by 3 list of mesh face indices (triangles) +@param[out] UV #V by 2 list of UV coordinates +@return UV coordinates)"); +} diff --git a/src/spectra/module.cpp b/src/spectra/module.cpp new file mode 100644 index 00000000..a6ff62ca --- /dev/null +++ b/src/spectra/module.cpp @@ -0,0 +1,11 @@ +#include +namespace nb = nanobind; + +// generated by cmake +#include "spectra/BINDING_DECLARATIONS.in" + +NB_MODULE(pyigl_spectra, m) { + m.doc() = "libigl spectra module python bindings"; + // generated by cmake +#include "spectra/BINDING_INVOCATIONS.in" +} diff --git a/tests/test_all.py b/tests/test_all.py index 52718783..6539eb4e 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -11,6 +11,8 @@ import igl.copyleft.tetgen import igl.copyleft.cgal import igl.embree +import igl.spectra +import igl.predicates @pytest.fixture def icosahedron(): @@ -1373,3 +1375,173 @@ def test_new_triangle_algorithms(): assert V_ref.shape[1] == 2 assert F_ref.shape[1] == 3 assert V_ref.shape[0] >= V_tri.shape[0] + + +def test_lexicographic_triangulation(): + # Simple square: 4 points in general position + P = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], dtype=np.float64) + F = igl.lexicographic_triangulation(P) + assert F.shape[1] == 3 + # 4 points in convex position => 2 triangles + assert F.shape[0] == 2 + # all indices are valid + assert F.min() >= 0 + assert F.max() < P.shape[0] + + +def test_spectra_lscm(): + V, F = igl.icosahedron() + UV = igl.spectra.lscm(V, F) + assert UV.shape == (V.shape[0], 2) + assert np.all(np.isfinite(UV)) + + +def test_spectra_eigs(): + V, F = igl.icosahedron() + L = igl.cotmatrix(V, F) + M = igl.massmatrix(V, F) + k = 3 + # smallest-magnitude generalized eigenpairs: L u = s M u + U, S = igl.spectra.eigs(L, M, k, igl.spectra.EigsType.EIGS_TYPE_SM) + assert U.shape == (V.shape[0], k) + assert S.shape == (k,) + assert np.all(np.isfinite(U)) + assert np.all(np.isfinite(S)) + + +def test_predicates_orientation_enum(): + assert igl.predicates.Orientation.POSITIVE.value == 1 + assert igl.predicates.Orientation.NEGATIVE.value == -1 + assert igl.predicates.Orientation.COLLINEAR.value == 0 + # aliases share the same integer value + assert igl.predicates.Orientation.INSIDE.value == igl.predicates.Orientation.POSITIVE.value + assert igl.predicates.Orientation.OUTSIDE.value == igl.predicates.Orientation.NEGATIVE.value + + +def test_predicates_orient2d(): + a = np.array([0.0, 0.0]) + b = np.array([1.0, 0.0]) + c = np.array([0.0, 1.0]) + assert igl.predicates.orient2d(a, b, c) == igl.predicates.Orientation.POSITIVE + assert igl.predicates.orient2d(a, c, b) == igl.predicates.Orientation.NEGATIVE + assert igl.predicates.orient2d(a, b, np.array([2.0, 0.0])) == igl.predicates.Orientation.COLLINEAR + + +def test_predicates_orient3d(): + a = np.array([0.0, 0.0, 0.0]) + b = np.array([1.0, 0.0, 0.0]) + c = np.array([0.0, 1.0, 0.0]) + d_above = np.array([0.0, 0.0, 1.0]) + d_below = np.array([0.0, 0.0, -1.0]) + r_above = igl.predicates.orient3d(a, b, c, d_above) + r_below = igl.predicates.orient3d(a, b, c, d_below) + assert r_above != r_below + # vectorized + A = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + B = np.array([[1.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) + C = np.array([[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]]) + D = np.array([[0.0, 0.0, 1.0], [0.0, 0.0, -1.0]]) + R = igl.predicates.orient3d(A, B, C, D) + assert R.shape == (2,) + + +def test_predicates_incircle(): + a = np.array([1.0, 0.0]) + b = np.array([0.0, 1.0]) + c = np.array([-1.0, 0.0]) + inside = np.array([0.0, 0.0]) + outside = np.array([2.0, 0.0]) + assert igl.predicates.incircle(a, b, c, inside).value == 1 # INSIDE == POSITIVE == 1 + assert igl.predicates.incircle(a, b, c, outside).value == -1 # OUTSIDE == NEGATIVE == -1 + + +def test_predicates_insphere(): + a = np.array([1.0, 0.0, 0.0]) + b = np.array([-1.0, 0.0, 0.0]) + c = np.array([0.0, 1.0, 0.0]) + d = np.array([0.0, 0.0, 1.0]) + inside = np.array([0.0, 0.0, 0.0]) + assert igl.predicates.insphere(a, b, c, d, inside).value == 1 # INSIDE == POSITIVE == 1 + + +def test_predicates_delaunay_triangulation(): + P = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], dtype=np.float64) + F = igl.predicates.delaunay_triangulation(P) + assert F.shape[1] == 3 + assert F.shape[0] == 2 + assert F.min() >= 0 + assert F.max() < P.shape[0] + + +def test_predicates_lexicographic_triangulation(): + P = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], dtype=np.float64) + F = igl.predicates.lexicographic_triangulation(P) + assert F.shape[1] == 3 + assert F.shape[0] == 2 + + +def test_predicates_ear_clipping(): + P = np.array([[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]], dtype=np.float64) + ok, eF = igl.predicates.ear_clipping(P) + assert ok + assert eF.shape == (1, 3) + + +def test_predicates_segment_segment_intersect(): + # crossing segments + A = np.array([0.0, 0.0]) + B = np.array([1.0, 1.0]) + C = np.array([0.0, 1.0]) + D = np.array([1.0, 0.0]) + assert igl.predicates.segment_segment_intersect(A, B, C, D) + # non-crossing + E = np.array([2.0, 0.0]) + F_ = np.array([3.0, 1.0]) + assert not igl.predicates.segment_segment_intersect(A, B, E, F_) + + +def test_predicates_triangle_triangle_intersect(): + a1 = np.array([0.0, 0.0, 0.0]) + a2 = np.array([1.0, 0.0, 0.0]) + a3 = np.array([0.0, 1.0, 0.0]) + b1 = np.array([0.5, 0.0, -0.5]) + b2 = np.array([0.5, 0.0, 0.5]) + b3 = np.array([0.5, 1.0, 0.0]) + intersects, coplanar = igl.predicates.triangle_triangle_intersect(a1, a2, a3, b1, b2, b3) + assert intersects + assert not coplanar + + +def test_predicates_find_self_intersections(): + V, F = igl.icosahedron() + # icosahedron has no self-intersections + found, IF, CP = igl.predicates.find_self_intersections(V, F) + assert not found + assert IF.shape[0] == 0 + + +def test_predicates_find_intersections(): + V, F = igl.icosahedron() + # test same mesh against itself — should find intersections + found, IF, CP = igl.predicates.find_intersections(V, F, V, F) + # Should find adjacent triangle "intersections" (shared edges/verts) + assert IF.shape[1] == 2 + + +def test_predicates_point_inside_convex_polygon(): + P = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], dtype=np.float64) + inside = np.array([0.5, 0.5]) + outside = np.array([2.0, 2.0]) + assert igl.predicates.point_inside_convex_polygon(P, inside) + assert not igl.predicates.point_inside_convex_polygon(P, outside) + + +def test_predicates_polygons_to_triangles(): + V = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], + [0.0, 1.0, 0.0], [2.0, 0.0, 0.0], [2.0, 1.0, 0.0]], dtype=np.float64) + I = np.array([0, 1, 2, 3, 1, 4, 5, 2], dtype=np.int64) + C = np.array([0, 4, 8], dtype=np.int64) + F, J = igl.predicates.polygons_to_triangles(V, I, C) + assert F.shape[1] == 3 + assert F.shape[0] == 4 # two quads -> 4 triangles + assert J.shape[0] == F.shape[0]