[
  {
    "path": ".github/workflows/cibuildwheel_config.toml",
    "content": "[tool.cibuildwheel]\nskip = \"cp36-*\" # scikit-build-core requires >=3.7\nbuild-verbosity = 3\n\n[tool.cibuildwheel.linux]\nbefore-all = [\n    \"yum remove -y cmake\",\n]\n\n# musllinux builds on an Alpinx Linux image, no need to mess with cmake there\n[[tool.cibuildwheel.overrides]]\nselect = \"*-musllinux*\"\nbefore-all = \"\""
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Build and Publish\n\n# NOTE: build logic is duplicated here and in test_build.yml\n\n# Run on the main branch for commits only\non:\n  push:\n    branches:     \n      - master\n\njobs:\n  build_wheels:\n\n    # only run if the most recent commit contains '[ci publish]'\n    if: \"contains(github.event.head_commit.message, '[ci publish]')\"\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-15-intel, macos-latest]\n\n    name: Build wheels ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: 'recursive'\n      \n      - uses: actions/setup-python@v6\n        with:\n          python-version: '3.12' \n\n      - name: Package source distribution\n        # make sure this only happens on one of the runners, not repeated on all\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          python -m pip install build\n          python -m build --sdist\n\n      - name: Run cibuildwheel\n        uses: pypa/cibuildwheel@v3.3.1\n        with:\n          config-file: \".github/workflows/cibuildwheel_config.toml\"\n\n      - name: Copy source distribution into wheelhouse\n        if: matrix.os == 'ubuntu-latest'\n        run: mv dist/*.tar.gz wheelhouse/\n\n      # Upload binaries to the github artifact store\n      - name: Upload wheels\n        uses: actions/upload-artifact@v4\n        with:\n          name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}\n          path: |\n            ./wheelhouse/*.whl \n            ./wheelhouse/*.tar.gz\n          overwrite: true\n\n  # Push the resulting binaries to pypi on a tag starting with 'v'\n  upload_pypi:\n    name: Upload release to PyPI\n\n    # only run if the most recent commit contains '[ci publish]'\n    if: \"contains(github.event.head_commit.message, '[ci publish]')\"\n\n    needs: [build_wheels]\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n      url: https://pypi.org/p/robust-laplacian/\n    permissions: # we authenticate via PyPI's 'trusted publisher' workflow, this permission is required\n      id-token: write\n    steps:\n      - name: Download built wheels artifact # downloads from the jobs storage from the previous step\n        uses: actions/download-artifact@v4.2.1\n        with:\n          # omitting the `name: ` field downloads all artifacts from this workflow\n          path: dist\n\n      - name: List downloaded files from artifact\n        run: ls -lR dist\n\n        # dist directory has subdirs from the different jobs, merge them into one directory and delete\n        # the empty leftover dirs\n      - name: Flatten directory\n        run: find dist -mindepth 2 -type f -exec mv -t dist {} + && find dist -type d -empty -delete\n\n      - name: List downloaded files from artifact after flatten\n        run: ls -lR dist\n\n      - name: Publish package to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n        # with:\n          # To test: repository_url: https://test.pypi.org/legacy/\n\n"
  },
  {
    "path": ".github/workflows/test_build.yml",
    "content": "name: Test Build\n\n# NOTE: build logic is duplicated here and in publish.yml\n\n# Run on the master branch commit push and PRs to master (note conditional below)\non:\n  push:\n    branches:    \n      - master\n  pull_request:\n    branches:\n      - master\n\njobs:\n  build_wheels:\n\n    # Only run if the commit message contains '[ci build]'\n    if: \"contains(toJSON(github.event.commits.*.message), '[ci build]') || contains(toJSON(github.event.pull_request.title), '[ci build]')\"\n\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-15-intel, macos-latest]\n\n    name: Build wheels ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    \n    steps:\n      - uses: actions/checkout@v4\n        with:\n          submodules: 'recursive'\n      \n      - uses: actions/setup-python@v6\n        with:\n          python-version: '3.12' \n\n      - name: Package source distribution\n        # make sure this only happens on one of the runners, not repeated on all\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          python -m pip install build\n          python -m build --sdist\n\n      - name: Run cibuildwheel\n        uses: pypa/cibuildwheel@v3.3.1\n        with:\n          config-file: \".github/workflows/cibuildwheel_config.toml\"\n\n      - name: Copy source distribution into wheelhouse\n        if: matrix.os == 'ubuntu-latest'\n        run: mv dist/*.tar.gz wheelhouse/\n\n      # Upload binaries to the github artifact store\n      - name: Upload wheels\n        uses: actions/upload-artifact@v4\n        with:\n          name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}\n          path: |\n            ./wheelhouse/*.whl \n            ./wheelhouse/*.tar.gz\n          overwrite: true\n\n      # Upload binaries to the github artifact store\n      - name: Upload wheels\n        uses: actions/upload-artifact@v4\n        with:\n          name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}\n          path: |\n            ./wheelhouse/*.whl \n            ./wheelhouse/*.tar.gz\n          overwrite: true"
  },
  {
    "path": ".github/workflows/test_linux.yml",
    "content": "name: Test Linux\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    if: \"! contains(toJSON(github.event.commits.*.message), '[ci skip]')\"\n    steps:\n    - uses: actions/checkout@v1\n      with:\n        submodules: 'recursive'\n      \n    - uses: actions/setup-python@v5\n      name: Install Python\n      with:\n        python-version: '3.9'\n\n    - name: install python packages\n      run: python3 -m pip install numpy scipy\n\n    - name: configure\n      run: mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Debug -DPYTHON_EXECUTABLE=$(python3 -c \"import sys; print(sys.executable)\") ..\n\n    - name: build\n      run: cd build && make\n\n    - name: run test\n      run: python3 test/robust_laplacian_test.py\n"
  },
  {
    "path": ".github/workflows/test_macos.yml",
    "content": "name: Test macOS\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n    runs-on: macos-latest\n    if: \"! contains(toJSON(github.event.commits.*.message), '[ci skip]')\"\n    steps:\n    - uses: actions/checkout@v1\n      with:\n        submodules: 'recursive'\n\n    - uses: actions/setup-python@v5\n      name: Install Python\n      with:\n        python-version: '3.9'\n\n    - name: install python packages\n      run: python3 -m pip install numpy scipy\n\n    - name: configure\n      run: mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Debug ..\n\n    - name: build\n      run: cd build && make\n\n    - name: run test\n      run: python3 test/robust_laplacian_test.py\n"
  },
  {
    "path": ".github/workflows/test_windows.yml",
    "content": "name: Test Windows\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n    runs-on: windows-latest\n    if: \"! contains(toJSON(github.event.commits.*.message), '[ci skip]')\"\n    steps:\n    - uses: actions/checkout@v1\n      with:\n        submodules: 'recursive'\n    \n    - uses: actions/setup-python@v5\n      name: Install Python\n      with:\n        python-version: '3.9'\n    \n    - name: install python packages\n      run: python -m pip install numpy scipy\n\n    - name: configure\n      run: mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Debug ..\n\n    - name: build\n      run: cd build && cmake --build \".\"\n\n    - name: run test\n      run: python test/robust_laplacian_test.py\n"
  },
  {
    "path": ".gitignore",
    "content": "# Build directories\nbuild/\nbuild_debug/\ndist/\n*.pyc\n__pycache__/\n\n*.egg-info\n\n# Editor and OS things\nimgui.ini\n.polyscope.ini\n.DS_Store\n.vscode\n*.swp\ntags\n*.blend1\n\n# Prerequisites\n*.d\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n\n# Fortran module files\n*.mod\n*.smod\n\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n\n# Executables\n*.exe\n*.out\n*.app\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"deps/geometry-central\"]\n\tpath = deps/geometry-central\n\turl = https://github.com/nmwsharp/geometry-central.git\n[submodule \"deps/pybind11\"]\n\tpath = deps/pybind11\n\turl = https://github.com/pybind/pybind11.git\n[submodule \"deps/eigen\"]\n\tpath = deps/eigen\n\turl = https://gitlab.com/libeigen/eigen.git\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.5.0)\nproject(robust-laplacian-py)\n\n# Recurse in to pybind\nset(PYBIND11_NEWPYTHON ON)\nadd_subdirectory(deps/pybind11)\n\n# set location of eigen for geometry-central\nset(GC_EIGEN_LOCATION \"${CMAKE_CURRENT_SOURCE_DIR}/deps/eigen\" CACHE PATH \"my path\")\n\n# geometry-central\nset(CMAKE_POSITION_INDEPENDENT_CODE ON)\nadd_subdirectory(deps/geometry-central)\n\npybind11_add_module(robust_laplacian_bindings \n  src/cpp/point_cloud_utilities.cpp\n  src/cpp/core.cpp\n)\n\ninclude_directories(robust_laplacian_bindings ${CMAKE_CURRENT_SOURCE_DIR}/src/cpp)\ninclude_directories(robust_laplacian_bindings ${CMAKE_CURRENT_SOURCE_DIR}/deps/jc_voronoi/include)\n\ntarget_link_libraries(robust_laplacian_bindings PRIVATE geometry-central)\n\ninstall(TARGETS robust_laplacian_bindings LIBRARY DESTINATION .)"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Nicholas Sharp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md LICENSE\ninclude CMakeLists.txt\nrecursive-include deps/geometry-central *.cpp *.h *.ipp *.hpp *.cmake CMakeLists.txt\nrecursive-include deps/jc_voronoi *\nrecursive-include deps/eigen/Eigen *\ninclude deps/eigen/COPYING*\ninclude deps/eigen/README.md\nrecursive-include deps/pybind11 *.h CMakeLists.txt *.cmake\nrecursive-include src *.cpp *.h\nrecursive-include src/robust-laplacian *.py\nexclude dist\n"
  },
  {
    "path": "README.md",
    "content": "[![actions status linux](https://github.com/nmwsharp/robust-laplacians-py/workflows/Test%20Linux/badge.svg)](https://github.com/nmwsharp/robust-laplacians-py/actions)\n[![actions status macOS](https://github.com/nmwsharp/robust-laplacians-py/workflows/Test%20macOS/badge.svg)](https://github.com/nmwsharp/robust-laplacians-py/actions)\n[![actions status windows](https://github.com/nmwsharp/robust-laplacians-py/workflows/Test%20Windows/badge.svg)](https://github.com/nmwsharp/robust-laplacians-py/actions)\n[![PyPI](https://img.shields.io/pypi/v/robust-laplacian?style=plastic)](https://pypi.org/project/robust-laplacian/)\n\nA Python package for high-quality Laplace matrices on meshes and point clouds. `pip install robust_laplacian`\n\nThe Laplacian is at the heart of many algorithms across geometry processing, simulation, and machine learning. This library builds a high-quality, robust Laplace matrix which often improves the performance of these algorithms, and wraps it all up in a simple, single-function API! \n\n**Sample**: computing eigenvectors of the point cloud Laplacian\n![demo image of eigenvectors on point cloud](https://github.com/nmwsharp/robust-laplacians-py/blob/master/teaser_cloud.jpg?raw=true)\n\nGiven as input a triangle mesh with arbitrary connectivity (could be nonmanifold, have boundary, etc), OR a point cloud, this library builds an `NxN` sparse Laplace matrix, where `N` is the number of vertices/points. This Laplace matrix is similar to the _cotan-Laplacian_ used widely in geometric computing, but internally the algorithm constructs an _intrinsic Delaunay triangulation_ of the surface, which gives the Laplace matrix great numerical properties. The resulting Laplacian is always a symmetric positive-definite matrix, with all positive edge weights. Additionally, this library performs _intrinsic mollification_ to alleviate floating-point issues with degenerate triangles.  \n\nThe resulting Laplace matrix `L` is a \"weak\" Laplace matrix, so we also generate a diagonal lumped mass matrix `M`, where each diagonal entry holds an area associated with the mesh element. The \"strong\" Laplacian can then be formed as `M^-1 L`, or a Poisson problem could be solved as `L x = M y`. \n\nA [C++ implementation and demo](https://github.com/nmwsharp/nonmanifold-laplacian) is available.\n\nThis library implements the algorithm described in [A Laplacian for Nonmanifold Triangle Meshes](http://www.cs.cmu.edu/~kmcrane/Projects/NonmanifoldLaplace/NonmanifoldLaplace.pdf) by [Nicholas Sharp](http://nmwsharp.com) and [Keenan Crane](http://keenan.is/here) at SGP 2020 (where it won a best paper award!). See the paper for more details, and please use the citation given at the bottom if it contributes to academic work.\n\n### Example\n\nBuild a point cloud Laplacian, compute its first 10 eigenvectors, and visualize with [Polyscope](https://polyscope.run/py/)\n\n```shell\npip install numpy scipy plyfile polyscope robust_laplacian\n```\n\n```py\nimport robust_laplacian\nfrom plyfile import PlyData\nimport numpy as np\nimport polyscope as ps\nimport scipy.sparse.linalg as sla\n\n# Read input\nplydata = PlyData.read(\"/path/to/cloud.ply\")\npoints = np.vstack((\n    plydata['vertex']['x'],\n    plydata['vertex']['y'],\n    plydata['vertex']['z']\n)).T\n\n# Build point cloud Laplacian\nL, M = robust_laplacian.point_cloud_laplacian(points)\n\n# (or for a mesh)\n# L, M = robust_laplacian.mesh_laplacian(verts, faces)\n\n# Compute some eigenvectors\nn_eig = 10\nevals, evecs = sla.eigsh(L, n_eig, M, sigma=1e-8)\n\n# Visualize\nps.init()\nps_cloud = ps.register_point_cloud(\"my cloud\", points)\nfor i in range(n_eig):\n    ps_cloud.add_scalar_quantity(\"eigenvector_\"+str(i), evecs[:,i], enabled=True)\nps.show()\n```\n\n**_NOTE:_** No one can agree on the sign convention for the Laplacian. This library builds the _positive semi-definite_ Laplace matrix, where the diagonal entries are positive and off-diagonal entries are negative. This is the _opposite_ of the sign used by e.g. libIGL in `igl.cotmatrix`, so you may need to flip a sign when converting code.\n\n### API\n\nThis package exposes just two functions:\n\n- `mesh_laplacian(verts, faces, mollify_factor=1e-5)`\n  - `verts` is an `V x 3` numpy array of vertex positions\n  - `faces`  is an `F x 3` numpy array of face indices, where each is a 0-based index referring to a vertex\n  - `mollify_factor` amount of intrinsic mollifcation to perform. `0` disables, larger values will increase numerical stability, while very large values will slightly implicitly smooth out the geometry. The range of reasonable settings is roughly `0` to `1e-3`.  The default value should usually be sufficient.\n  - `return L, M` a pair of scipy sparse matrices for the Laplacian `L` and mass matrix `M` \n- `point_cloud_laplacian(points, mollify_factor=1e-5, n_neighbors=30)` \n  - `points` is an `V x 3` numpy array of point positions\n  - `mollify_factor` amount of intrinsic mollifcation to perform. `0` disables, larger values will increase numerical stability, while very large values will slightly implicitly smooth out the geometry. The range of reasonable settings is roughly `0` to `1e-3`.  The default value should usually be sufficient.\n  - `n_neighbors` is the number of nearest neighbors to use when constructing local triangulations. This parameter has little effect on the resulting matrices, and the default value is almost always sufficient.\n  - `return L, M` a pair of scipy sparse matrices for the Laplacian `L` and mass matrix `M` \n\n### Installation\n\nThe package is availabe via `pip`\n\n```\npip install robust_laplacian\n```\n\nThe underlying algorithm is implemented in C++; the pypi entry includes precompiled binaries for many platforms.\n\nVery old versions of `pip` might need to be upgraded like `pip install pip --upgrade` to use the precompiled binaries.\n\nAlternately, if no precompiled binary matches your system `pip` will attempt to compile from source on your machine.  This requires a working C++ toolchain, including cmake.\n\n### Known limitations\n\n- For point clouds, this repo uses a simple method to generate planar Delaunay triangulations, which may not be totally robust to collinear or degenerate point clouds.\n\n### Dependencies\n\nThis python library is mainly a wrapper around the implementation in the [geometry-central](http://geometry-central.net) library; see there for further dependencies. Additionally, this library uses [pybind11](https://github.com/pybind/pybind11) to generate bindings, and [jc_voronoi](https://github.com/JCash/voronoi) for 2D Delaunay triangulation on point clouds. All are permissively licensed.\n\n### Citation\n\n```\n@article{Sharp:2020:LNT,\n  author={Nicholas Sharp and Keenan Crane},\n  title={{A Laplacian for Nonmanifold Triangle Meshes}},\n  journal={Computer Graphics Forum (SGP)},\n  volume={39},\n  number={5},\n  year={2020}\n}\n```\n\n### For developers\n\nThis repo is configured with CI on github actions to build wheels across platform.\n\n### Deploy a new version\n\n- Commit the desired updates to the `master` branch (or via PR). Include the string `[ci build]` in the commit message to ensure a build happens.\n- Watch the github actions builds to ensure the test & build stages succeed and all wheels are compiled.\n- While you're waiting, update the docs.\n- Create a commit bumping the version in `pyproject.toml`. Include the string `[ci publish]` in the commit message and push.  This will kick off a new github actions build which deploys the wheels to PyPI after compilation. Use the github UI to create a new release + tag matching the version in `pyproject.toml`.\n"
  },
  {
    "path": "deps/jc_voronoi/include/jc_voronoi/jc_voronoi.h",
    "content": "// Copyright (c) 2015-2019 Mathias Westerdahl\n// For LICENSE (MIT), USAGE or HISTORY, see bottom of file\n\n#ifndef JC_VORONOI_H\n#define JC_VORONOI_H\n\n#include <math.h>\n#include <stddef.h>\n#include <stdlib.h>\n\n#include <assert.h>\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n#ifndef JCV_REAL_TYPE\n    #define JCV_REAL_TYPE float\n#endif\n\n#ifndef JCV_ATAN2\n    #define JCV_ATAN2(_Y_, _X_) atan2f(_Y_, _X_)\n#endif\n\n#ifndef JCV_SQRT\n    #define JCV_SQRT(_X_)       sqrtf(_X_)\n#endif\n\n#ifndef JCV_PI\n    #define JCV_PI 3.14159265358979323846264338327950288f\n#endif\n\n#ifndef JCV_FLT_MAX\n    #define JCV_FLT_MAX 3.402823466e+38F\n#endif\n\n#ifndef JCV_EDGE_INTERSECT_THRESHOLD\n    // Fix for Issue #40\n    #define JCV_EDGE_INTERSECT_THRESHOLD 1.0e-10F\n#endif\n\n\ntypedef JCV_REAL_TYPE jcv_real;\n\ntypedef struct _jcv_point       jcv_point;\ntypedef struct _jcv_rect        jcv_rect;\ntypedef struct _jcv_site        jcv_site;\ntypedef struct _jcv_edge        jcv_edge;\ntypedef struct _jcv_graphedge   jcv_graphedge;\ntypedef struct _jcv_diagram     jcv_diagram;\ntypedef struct _jcv_clipper     jcv_clipper;\ntypedef struct _jcv_context_internal jcv_context_internal;\n\n/// Tests if a point is inside the final shape\ntypedef int (*jcv_clip_test_point_fn)(const jcv_clipper* clipper, const jcv_point p);\n/** Given an edge, and the clipper, calculates the e->pos[0] and e->pos[1]\n * Returns 0 if not successful\n */\ntypedef int (*jcv_clip_edge_fn)(const jcv_clipper* clipper, jcv_edge* e);\n/** Given the clipper, the site and the last edge,\n * closes any gaps in the polygon by adding new edges that follow the bounding shape\n * The internal context is use when allocating new edges.\n */\ntypedef void (*jcv_clip_fillgap_fn)(const jcv_clipper* clipper, jcv_context_internal* allocator, jcv_site* s);\n\n\n\n/**\n * Uses malloc\n * If a clipper is not supplied, a default box clipper will be used\n * If rect is null, an automatic bounding box is calculated, with an extra padding of 10 units\n * All points will be culled against the bounding rect, and all edges will be clipped against it.\n */\nextern void jcv_diagram_generate( int num_points, const jcv_point* points, const jcv_rect* rect, const jcv_clipper* clipper, jcv_diagram* diagram );\n\ntypedef void* (*FJCVAllocFn)(void* userctx, size_t size);\ntypedef void (*FJCVFreeFn)(void* userctx, void* p);\n\n// Same as above, but allows the client to use a custom allocator\nextern void jcv_diagram_generate_useralloc( int num_points, const jcv_point* points, const jcv_rect* rect, const jcv_clipper* clipper, void* userallocctx, FJCVAllocFn allocfn, FJCVFreeFn freefn, jcv_diagram* diagram );\n\n// Uses free (or the registered custom free function)\nextern void jcv_diagram_free( jcv_diagram* diagram );\n\n// Returns an array of sites, where each index is the same as the original input point array.\nextern const jcv_site* jcv_diagram_get_sites( const jcv_diagram* diagram );\n\n// Returns a linked list of all the voronoi edges\n// excluding the ones that lie on the borders of the bounding box.\n// For a full list of edges, you need to iterate over the sites, and their graph edges.\nextern const jcv_edge* jcv_diagram_get_edges( const jcv_diagram* diagram );\n\n// Iterates over a list of edges, skipping invalid edges (where p0==p1)\nextern const jcv_edge* jcv_diagram_get_next_edge( const jcv_edge* edge );\n\n// For the default clipper\nextern int jcv_boxshape_test(const jcv_clipper* clipper, const jcv_point p);\nextern int jcv_boxshape_clip(const jcv_clipper* clipper, jcv_edge* e);\nextern void jcv_boxshape_fillgaps(const jcv_clipper* clipper, jcv_context_internal* allocator, jcv_site* s);\n\n\n#pragma pack(push, 1)\n\nstruct _jcv_point\n{\n    jcv_real x;\n    jcv_real y;\n};\n\nstruct _jcv_graphedge\n{\n    struct _jcv_graphedge*  next;\n    struct _jcv_edge*       edge;\n    struct _jcv_site*       neighbor;\n    jcv_point               pos[2];\n    jcv_real                angle;\n};\n\nstruct _jcv_site\n{\n    jcv_point       p;\n    int             index;  // Index into the original list of points\n    jcv_graphedge*  edges;  // The half edges owned by the cell\n};\n\n// The coefficients a, b and c are from the general line equation: ax * by + c = 0\nstruct _jcv_edge\n{\n    struct _jcv_edge*   next;\n    jcv_site*           sites[2];\n    jcv_point           pos[2];\n    jcv_real            a;\n    jcv_real            b;\n    jcv_real            c;\n};\n\nstruct _jcv_rect\n{\n    jcv_point   min;\n    jcv_point   max;\n};\n\nstruct _jcv_clipper\n{\n    jcv_clip_test_point_fn  test_fn;\n    jcv_clip_edge_fn        clip_fn;\n    jcv_clip_fillgap_fn     fill_fn;\n    jcv_point               min;        // The bounding rect min\n    jcv_point               max;        // The bounding rect max\n    void*                   ctx;        // User defined context\n};\n\nstruct _jcv_diagram\n{\n    jcv_context_internal*   internal;\n    jcv_edge*               edges;\n    jcv_site*               sites;\n    int                     numsites;\n    jcv_point               min;\n    jcv_point               max;\n};\n\n#pragma pack(pop)\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif // JC_VORONOI_H\n\n#ifdef JC_VORONOI_IMPLEMENTATION\n#undef JC_VORONOI_IMPLEMENTATION\n\n#include <memory.h>\n\n// INTERNAL FUNCTIONS\n\n#if defined(_MSC_VER) && !defined(__cplusplus)\n    #define inline __inline\n#endif\n\n// jcv_point\n\nstatic inline int jcv_point_cmp(const void* p1, const void* p2)\n{\n    const jcv_point* s1 = (const jcv_point*) p1;\n    const jcv_point* s2 = (const jcv_point*) p2;\n    return (s1->y != s2->y) ? (s1->y < s2->y ? -1 : 1) : (s1->x < s2->x ? -1 : 1);\n}\n\nstatic inline int jcv_point_less( const jcv_point* pt1, const jcv_point* pt2 )\n{\n    return (pt1->y == pt2->y) ? (pt1->x < pt2->x) : pt1->y < pt2->y;\n}\n\nstatic inline int jcv_point_eq( const jcv_point* pt1, const jcv_point* pt2 )\n{\n    return (pt1->y == pt2->y) && (pt1->x == pt2->x);\n}\n\nstatic inline int jcv_point_on_box_edge( const jcv_point* pt, const jcv_point* min, const jcv_point* max )\n{\n    return pt->x == min->x || pt->y == min->y || pt->x == max->x || pt->y == max->y;\n}\n\nstatic inline jcv_real jcv_point_dist_sq( const jcv_point* pt1, const jcv_point* pt2)\n{\n    jcv_real diffx = pt1->x - pt2->x;\n    jcv_real diffy = pt1->y - pt2->y;\n    return diffx * diffx + diffy * diffy;\n}\n\nstatic inline jcv_real jcv_point_dist( const jcv_point* pt1, const jcv_point* pt2 )\n{\n    return (jcv_real)(JCV_SQRT(jcv_point_dist_sq(pt1, pt2)));\n}\n\n// Structs\n\n#pragma pack(push, 1)\n\ntypedef struct _jcv_halfedge\n{\n    jcv_edge*               edge;\n    struct _jcv_halfedge*   left;\n    struct _jcv_halfedge*   right;\n    jcv_point               vertex;\n    jcv_real                y;\n    int                     direction; // 0=left, 1=right\n    int                     pqpos;\n} jcv_halfedge;\n\ntypedef struct _jcv_memoryblock\n{\n    size_t sizefree;\n    struct _jcv_memoryblock* next;\n    char*  memory;\n} jcv_memoryblock;\n\n\ntypedef int  (*FJCVPriorityQueuePrint)(const void* node, int pos);\n\ntypedef struct _jcv_priorityqueue\n{\n    // Implements a binary heap\n    int                         maxnumitems;\n    int                         numitems;\n    void**                      items;\n} jcv_priorityqueue;\n\n\nstruct _jcv_context_internal\n{\n    void*               mem;\n    jcv_edge*           edges;\n    jcv_halfedge*       beachline_start;\n    jcv_halfedge*       beachline_end;\n    jcv_halfedge*       last_inserted;\n    jcv_priorityqueue*  eventqueue;\n\n    jcv_site*           sites;\n    jcv_site*           bottomsite;\n    int                 numsites;\n    int                 currentsite;\n    int                 _padding;\n\n    jcv_memoryblock*    memblocks;\n    jcv_edge*           edgepool;\n    jcv_halfedge*       halfedgepool;\n    void**              eventmem;\n    jcv_clipper         clipper;\n\n    void*               memctx; // Given by the user\n    FJCVAllocFn         alloc;\n    FJCVFreeFn          free;\n\n    jcv_rect            rect;\n};\n\n#pragma pack(pop)\n\n\nstatic const int JCV_DIRECTION_LEFT  = 0;\nstatic const int JCV_DIRECTION_RIGHT = 1;\nstatic const jcv_real JCV_INVALID_VALUE = (jcv_real)-JCV_FLT_MAX;\n\n\nvoid jcv_diagram_free( jcv_diagram* d )\n{\n    jcv_context_internal* internal = d->internal;\n    void* memctx = internal->memctx;\n    FJCVFreeFn freefn = internal->free;\n    while(internal->memblocks)\n    {\n        jcv_memoryblock* p = internal->memblocks;\n        internal->memblocks = internal->memblocks->next;\n        freefn( memctx, p );\n    }\n\n    freefn( memctx, internal->mem );\n}\n\nconst jcv_site* jcv_diagram_get_sites( const jcv_diagram* diagram )\n{\n    return diagram->internal->sites;\n}\n\nconst jcv_edge* jcv_diagram_get_edges( const jcv_diagram* diagram )\n{\n    jcv_edge e;\n    e.next = diagram->internal->edges;\n    return jcv_diagram_get_next_edge(&e);\n}\n\nconst jcv_edge* jcv_diagram_get_next_edge( const jcv_edge* edge )\n{\n    const jcv_edge* e = edge->next;\n    while (e != 0 && jcv_point_eq(&e->pos[0], &e->pos[1])) {\n        e = e->next;\n    }\n    return e;\n}\n\nstatic void* jcv_alloc(jcv_context_internal* internal, size_t size)\n{\n    if( !internal->memblocks || internal->memblocks->sizefree < size )\n    {\n        size_t blocksize = 16 * 1024;\n        jcv_memoryblock* block = (jcv_memoryblock*)internal->alloc( internal->memctx, blocksize );\n        size_t offset = sizeof(jcv_memoryblock);\n        block->sizefree = blocksize - offset;\n        block->next = internal->memblocks;\n        block->memory = ((char*)block) + offset;\n        internal->memblocks = block;\n    }\n    void* p = internal->memblocks->memory;\n    internal->memblocks->memory += size;\n    internal->memblocks->sizefree -= size;\n    return p;\n}\n\nstatic jcv_edge* jcv_alloc_edge(jcv_context_internal* internal)\n{\n    return (jcv_edge*)jcv_alloc(internal, sizeof(jcv_edge));\n}\n\nstatic jcv_halfedge* jcv_alloc_halfedge(jcv_context_internal* internal)\n{\n    if( internal->halfedgepool )\n    {\n        jcv_halfedge* edge = internal->halfedgepool;\n        internal->halfedgepool = internal->halfedgepool->right;\n        return edge;\n    }\n\n    return (jcv_halfedge*)jcv_alloc(internal, sizeof(jcv_halfedge));\n}\n\nstatic jcv_graphedge* jcv_alloc_graphedge(jcv_context_internal* internal)\n{\n    return (jcv_graphedge*)jcv_alloc(internal, sizeof(jcv_graphedge));\n}\n\nstatic void* jcv_alloc_fn(void* memctx, size_t size)\n{\n    (void)memctx;\n    return malloc(size);\n}\n\nstatic void jcv_free_fn(void* memctx, void* p)\n{\n    (void)memctx;\n    free(p);\n}\n\n// jcv_edge\n\nstatic inline int jcv_is_valid(const jcv_point* p)\n{\n    return (p->x != JCV_INVALID_VALUE || p->y != JCV_INVALID_VALUE) ? 1 : 0;\n}\n\nstatic void jcv_edge_create(jcv_edge* e, jcv_site* s1, jcv_site* s2)\n{\n    e->next = 0;\n    e->sites[0] = s1;\n    e->sites[1] = s2;\n    e->pos[0].x = JCV_INVALID_VALUE;\n    e->pos[0].y = JCV_INVALID_VALUE;\n    e->pos[1].x = JCV_INVALID_VALUE;\n    e->pos[1].y = JCV_INVALID_VALUE;\n\n    // Create line equation between S1 and S2:\n    // jcv_real a = -1 * (s2->p.y - s1->p.y);\n    // jcv_real b = s2->p.x - s1->p.x;\n    // //jcv_real c = -1 * (s2->p.x - s1->p.x) * s1->p.y + (s2->p.y - s1->p.y) * s1->p.x;\n    //\n    // // create perpendicular line\n    // jcv_real pa = b;\n    // jcv_real pb = -a;\n    // //jcv_real pc = pa * s1->p.x + pb * s1->p.y;\n    //\n    // // Move to the mid point\n    // jcv_real mx = s1->p.x + dx * jcv_real(0.5);\n    // jcv_real my = s1->p.y + dy * jcv_real(0.5);\n    // jcv_real pc = ( pa * mx + pb * my );\n\n    jcv_real dx = s2->p.x - s1->p.x;\n    jcv_real dy = s2->p.y - s1->p.y;\n    int dx_is_larger = (dx*dx) > (dy*dy); // instead of fabs\n\n    // Simplify it, using dx and dy\n    e->c = dx * (s1->p.x + dx * (jcv_real)0.5) + dy * (s1->p.y + dy * (jcv_real)0.5);\n\n    if( dx_is_larger )\n    {\n        e->a = (jcv_real)1;\n        e->b = dy / dx;\n        e->c /= dx;\n    }\n    else\n    {\n        e->a = dx / dy;\n        e->b = (jcv_real)1;\n        e->c /= dy;\n    }\n}\n\n// CLIPPING\nint jcv_boxshape_test(const jcv_clipper* clipper, const jcv_point p)\n{\n    return p.x >= clipper->min.x && p.x <= clipper->max.x &&\n           p.y >= clipper->min.y && p.y <= clipper->max.y;\n}\n\n// The line equation: ax + by + c = 0\n// see jcv_edge_create\nint jcv_boxshape_clip(const jcv_clipper* clipper, jcv_edge* e)\n{\n    jcv_real pxmin = clipper->min.x;\n    jcv_real pxmax = clipper->max.x;\n    jcv_real pymin = clipper->min.y;\n    jcv_real pymax = clipper->max.y;\n\n    jcv_real x1, y1, x2, y2;\n    jcv_point* s1;\n    jcv_point* s2;\n    if (e->a == (jcv_real)1 && e->b >= (jcv_real)0)\n    {\n        s1 = jcv_is_valid(&e->pos[1]) ? &e->pos[1] : 0;\n        s2 = jcv_is_valid(&e->pos[0]) ? &e->pos[0] : 0;\n    }\n    else\n    {\n        s1 = jcv_is_valid(&e->pos[0]) ? &e->pos[0] : 0;\n        s2 = jcv_is_valid(&e->pos[1]) ? &e->pos[1] : 0;\n    };\n\n    if (e->a == (jcv_real)1) // delta x is larger\n    {\n        y1 = pymin;\n        if (s1 != 0 && s1->y > pymin)\n        {\n            y1 = s1->y;\n        }\n        if( y1 > pymax )\n        {\n            y1 = pymax;\n        }\n        x1 = e->c - e->b * y1;\n        y2 = pymax;\n        if (s2 != 0 && s2->y < pymax)\n            y2 = s2->y;\n\n        if( y2 < pymin )\n        {\n            y2 = pymin;\n        }\n        x2 = (e->c) - (e->b) * y2;\n        // Never occurs according to lcov\n        // if( ((x1 > pxmax) & (x2 > pxmax)) | ((x1 < pxmin) & (x2 < pxmin)) )\n        // {\n        //     return 0;\n        // }\n        if (x1 > pxmax)\n        {\n            x1 = pxmax;\n            y1 = (e->c - x1) / e->b;\n        }\n        else if (x1 < pxmin)\n        {\n            x1 = pxmin;\n            y1 = (e->c - x1) / e->b;\n        }\n        if (x2 > pxmax)\n        {\n            x2 = pxmax;\n            y2 = (e->c - x2) / e->b;\n        }\n        else if (x2 < pxmin)\n        {\n            x2 = pxmin;\n            y2 = (e->c - x2) / e->b;\n        }\n    }\n    else // delta y is larger\n    {\n        x1 = pxmin;\n        if( s1 != 0 && s1->x > pxmin )\n            x1 = s1->x;\n        if( x1 > pxmax )\n        {\n            x1 = pxmax;\n        }\n        y1 = e->c - e->a * x1;\n        x2 = pxmax;\n        if( s2 != 0 && s2->x < pxmax )\n            x2 = s2->x;\n        if( x2 < pxmin )\n        {\n            x2 = pxmin;\n        }\n        y2 = e->c - e->a * x2;\n        // Never occurs according to lcov\n        // if( ((y1 > pymax) & (y2 > pymax)) | ((y1 < pymin) & (y2 < pymin)) )\n        // {\n        //     return 0;\n        // }\n        if( y1 > pymax )\n        {\n            y1 = pymax;\n            x1 = (e->c - y1) / e->a;\n        }\n        else if( y1 < pymin )\n        {\n            y1 = pymin;\n            x1 = (e->c - y1) / e->a;\n        }\n        if( y2 > pymax )\n        {\n            y2 = pymax;\n            x2 = (e->c - y2) / e->a;\n        }\n        else if( y2 < pymin )\n        {\n            y2 = pymin;\n            x2 = (e->c - y2) / e->a;\n        };\n    };\n\n    e->pos[0].x = x1;\n    e->pos[0].y = y1;\n    e->pos[1].x = x2;\n    e->pos[1].y = y2;\n\n    // If the two points are equal, the result is invalid\n    return (x1 == x2 && y1 == y2) ? 0 : 1;\n}\n\n// The line equation: ax + by + c = 0\n// see jcv_edge_create\nstatic int jcv_edge_clipline(jcv_context_internal* internal, jcv_edge* e)\n{\n    return internal->clipper.clip_fn(&internal->clipper, e);\n}\n\nstatic jcv_edge* jcv_edge_new(jcv_context_internal* internal, jcv_site* s1, jcv_site* s2)\n{\n    jcv_edge* e = jcv_alloc_edge(internal);\n    jcv_edge_create(e, s1, s2);\n    return e;\n}\n\n\n// jcv_halfedge\n\nstatic void jcv_halfedge_link(jcv_halfedge* edge, jcv_halfedge* newedge)\n{\n    newedge->left = edge;\n    newedge->right = edge->right;\n    edge->right->left = newedge;\n    edge->right = newedge;\n}\n\nstatic inline void jcv_halfedge_unlink(jcv_halfedge* he)\n{\n    he->left->right = he->right;\n    he->right->left = he->left;\n    he->left  = 0;\n    he->right = 0;\n}\n\nstatic inline jcv_halfedge* jcv_halfedge_new(jcv_context_internal* internal, jcv_edge* e, int direction)\n{\n    jcv_halfedge* he = jcv_alloc_halfedge(internal);\n    he->edge        = e;\n    he->left        = 0;\n    he->right       = 0;\n    he->direction   = direction;\n    he->pqpos       = 0;\n    // These are set outside\n    //he->y\n    //he->vertex\n    return he;\n}\n\nstatic void jcv_halfedge_delete(jcv_context_internal* internal, jcv_halfedge* he)\n{\n    he->right = internal->halfedgepool;\n    internal->halfedgepool = he;\n}\n\nstatic inline jcv_site* jcv_halfedge_leftsite(const jcv_halfedge* he)\n{\n    return he->edge->sites[he->direction];\n}\n\nstatic inline jcv_site* jcv_halfedge_rightsite(const jcv_halfedge* he)\n{\n    return he->edge ? he->edge->sites[1 - he->direction] : 0;\n}\n\nstatic int jcv_halfedge_rightof(const jcv_halfedge* he, const jcv_point* p)\n{\n    const jcv_edge* e = he->edge;\n    const jcv_site* topsite = e->sites[1];\n\n    int right_of_site = (p->x > topsite->p.x) ? 1 : 0;\n    if (right_of_site && he->direction == JCV_DIRECTION_LEFT)\n        return 1;\n    if (!right_of_site && he->direction == JCV_DIRECTION_RIGHT)\n        return 0;\n\n    jcv_real dxp, dyp, dxs, t1, t2, t3, yl;\n\n    int above;\n    if (e->a == (jcv_real)1)\n    {\n        dyp = p->y - topsite->p.y;\n        dxp = p->x - topsite->p.x;\n        int fast = 0;\n        if( (!right_of_site & (e->b < (jcv_real)0)) | (right_of_site & (e->b >= (jcv_real)0)) )\n        {\n            above = dyp >= e->b * dxp;\n            fast = above;\n        }\n        else\n        {\n            above = (p->x + p->y * e->b) > e->c;\n            if (e->b < (jcv_real)0)\n                above = !above;\n            if (!above)\n                fast = 1;\n        };\n        if (!fast)\n        {\n            dxs = topsite->p.x - e->sites[0]->p.x;\n            above = e->b * (dxp * dxp - dyp * dyp)\n                    < dxs * dyp * ((jcv_real)1 + (jcv_real)2 * dxp / dxs + e->b * e->b);\n            if (e->b < (jcv_real)0)\n                above = !above;\n        };\n    }\n    else // e->b == 1\n    {\n        yl = e->c - e->a * p->x;\n        t1 = p->y - yl;\n        t2 = p->x - topsite->p.x;\n        t3 = yl - topsite->p.y;\n        above = t1 * t1 > (t2 * t2 + t3 * t3);\n    };\n    return (he->direction == JCV_DIRECTION_LEFT ? above : !above);\n}\n\n// Keeps the priority queue sorted with events sorted in ascending order\n// Return 1 if the edges needs to be swapped\nstatic inline int jcv_halfedge_compare( const jcv_halfedge* he1, const jcv_halfedge* he2 )\n{\n\treturn  (he1->y == he2->y) ? he1->vertex.x > he2->vertex.x : he1->y > he2->y;\n}\n\nstatic int jcv_halfedge_intersect(const jcv_halfedge* he1, const jcv_halfedge* he2, jcv_point* out)\n{\n    const jcv_edge* e1 = he1->edge;\n    const jcv_edge* e2 = he2->edge;\n\n    jcv_real d = e1->a * e2->b - e1->b * e2->a;\n    if( ((jcv_real)-JCV_EDGE_INTERSECT_THRESHOLD < d && d < (jcv_real)JCV_EDGE_INTERSECT_THRESHOLD) )\n    {\n        return 0;\n    }\n    out->x = (e1->c * e2->b - e1->b * e2->c) / d;\n    out->y = (e1->a * e2->c - e1->c * e2->a) / d;\n\n    const jcv_edge* e;\n    const jcv_halfedge* he;\n    if( jcv_point_less( &e1->sites[1]->p, &e2->sites[1]->p) )\n    {\n        he = he1;\n        e = e1;\n    }\n    else\n    {\n        he = he2;\n        e = e2;\n    }\n\n    int right_of_site = out->x >= e->sites[1]->p.x;\n    if ((right_of_site && he->direction == JCV_DIRECTION_LEFT) || (!right_of_site && he->direction == JCV_DIRECTION_RIGHT))\n    {\n        return 0;\n    }\n\n    return 1;\n}\n\n\n// Priority queue\n\nstatic int jcv_pq_moveup(jcv_priorityqueue* pq, int pos)\n{\n    jcv_halfedge** items = (jcv_halfedge**)pq->items;\n    jcv_halfedge* node = items[pos];\n\n    for( int parent = (pos >> 1);\n         pos > 1 && jcv_halfedge_compare(items[parent], node);\n         pos = parent, parent = parent >> 1)\n    {\n        items[pos] = items[parent];\n        items[pos]->pqpos = pos;\n    }\n\n    node->pqpos = pos;\n    items[pos] = node;\n    return pos;\n}\n\nstatic int jcv_pq_maxchild(jcv_priorityqueue* pq, int pos)\n{\n    int child = pos << 1;\n    if( child >= pq->numitems )\n        return 0;\n    jcv_halfedge** items = (jcv_halfedge**)pq->items;\n    if( (child + 1) < pq->numitems && jcv_halfedge_compare(items[child], items[child+1]) )\n        return child+1;\n    return child;\n}\n\nstatic int jcv_pq_movedown(jcv_priorityqueue* pq, int pos)\n{\n    jcv_halfedge** items = (jcv_halfedge**)pq->items;\n    jcv_halfedge* node = items[pos];\n\n    int child = jcv_pq_maxchild(pq, pos);\n    while( child && jcv_halfedge_compare(node, items[child]) )\n    {\n        items[pos] = items[child];\n        items[pos]->pqpos = pos;\n        pos = child;\n        child = jcv_pq_maxchild(pq, pos);\n    }\n\n    items[pos] = node;\n    items[pos]->pqpos = pos;\n    return pos;\n}\n\nstatic void jcv_pq_create(jcv_priorityqueue* pq, int capacity, void** buffer)\n{\n    pq->maxnumitems = capacity;\n    pq->numitems    = 1;\n    pq->items       = buffer;\n}\n\nstatic int jcv_pq_empty(jcv_priorityqueue* pq)\n{\n    return pq->numitems == 1 ? 1 : 0;\n}\n\nstatic int jcv_pq_push(jcv_priorityqueue* pq, void* node)\n{\n    assert(pq->numitems < pq->maxnumitems);\n    int n = pq->numitems++;\n    pq->items[n] = node;\n    return jcv_pq_moveup(pq, n);\n}\n\nstatic void* jcv_pq_pop(jcv_priorityqueue* pq)\n{\n    void* node = pq->items[1];\n    pq->items[1] = pq->items[--pq->numitems];\n    jcv_pq_movedown(pq, 1);\n    return node;\n}\n\nstatic void* jcv_pq_top(jcv_priorityqueue* pq)\n{\n    return pq->items[1];\n}\n\nstatic void jcv_pq_remove(jcv_priorityqueue* pq, jcv_halfedge* node)\n{\n    if( pq->numitems == 1 )\n        return;\n    int pos = node->pqpos;\n    if( pos == 0 )\n        return;\n\n    jcv_halfedge** items = (jcv_halfedge**)pq->items;\n\n    items[pos] = items[--pq->numitems];\n    if( jcv_halfedge_compare( node, items[pos] ) )\n        jcv_pq_moveup( pq, pos );\n    else\n        jcv_pq_movedown( pq, pos );\n    node->pqpos = pos;\n}\n\n// internal functions\n\nstatic inline jcv_site* jcv_nextsite(jcv_context_internal* internal)\n{\n    return (internal->currentsite < internal->numsites) ? &internal->sites[internal->currentsite++] : 0;\n}\n\nstatic jcv_halfedge* jcv_get_edge_above_x(jcv_context_internal* internal, const jcv_point* p)\n{\n    // Gets the arc on the beach line at the x coordinate (i.e. right above the new site event)\n\n    // A good guess it's close by (Can be optimized)\n    jcv_halfedge* he = internal->last_inserted;\n    if( !he )\n    {\n        if( p->x < (internal->rect.max.x - internal->rect.min.x) / 2 )\n            he = internal->beachline_start;\n        else\n            he = internal->beachline_end;\n    }\n\n    //\n    if( he == internal->beachline_start || (he != internal->beachline_end && jcv_halfedge_rightof(he, p)) )\n    {\n        do {\n            he = he->right;\n        }\n        while( he != internal->beachline_end && jcv_halfedge_rightof(he, p) );\n\n        he = he->left;\n    }\n    else\n    {\n        do {\n            he = he->left;\n        }\n        while( he != internal->beachline_start && !jcv_halfedge_rightof(he, p) );\n    }\n\n    return he;\n}\n\nstatic int jcv_check_circle_event(const jcv_halfedge* he1, const jcv_halfedge* he2, jcv_point* vertex)\n{\n    jcv_edge* e1 = he1->edge;\n    jcv_edge* e2 = he2->edge;\n    if( e1 == 0 || e2 == 0 || e1->sites[1] == e2->sites[1] )\n    {\n        return 0;\n    }\n\n    return jcv_halfedge_intersect(he1, he2, vertex);\n}\n\nstatic void jcv_site_event(jcv_context_internal* internal, jcv_site* site)\n{\n    jcv_halfedge* left   = jcv_get_edge_above_x(internal, &site->p);\n    jcv_halfedge* right  = left->right;\n    jcv_site*     bottom = jcv_halfedge_rightsite(left);\n    if( !bottom )\n        bottom = internal->bottomsite;\n\n    jcv_edge* edge = jcv_edge_new(internal, bottom, site);\n    edge->next = internal->edges;\n    internal->edges = edge;\n\n    jcv_halfedge* edge1 = jcv_halfedge_new(internal, edge, JCV_DIRECTION_LEFT);\n    jcv_halfedge* edge2 = jcv_halfedge_new(internal, edge, JCV_DIRECTION_RIGHT);\n\n    jcv_halfedge_link(left, edge1);\n    jcv_halfedge_link(edge1, edge2);\n\n    internal->last_inserted = right;\n\n    jcv_point p;\n    if( jcv_check_circle_event( left, edge1, &p ) )\n    {\n        jcv_pq_remove(internal->eventqueue, left);\n        left->vertex    = p;\n        left->y         = p.y + jcv_point_dist(&site->p, &p);\n        jcv_pq_push(internal->eventqueue, left);\n    }\n    if( jcv_check_circle_event( edge2, right, &p ) )\n    {\n        edge2->vertex   = p;\n        edge2->y        = p.y + jcv_point_dist(&site->p, &p);\n        jcv_pq_push(internal->eventqueue, edge2);\n    }\n}\n\n// https://cp-algorithms.com/geometry/oriented-triangle-area.html\nstatic inline jcv_real jcv_determinant(const jcv_point* a, const jcv_point* b, const jcv_point* c)\n{\n    return (b->x - a->x)*(c->y - a->y) - (b->y - a->y)*(c->x - a->x);\n}\n\nstatic inline jcv_real jcv_calc_sort_metric(const jcv_site* site, const jcv_graphedge* edge)\n{\n    // We take the average of the two points, since we can better distinguish between very small edges\n    jcv_real half = 1/(jcv_real)2;\n    jcv_real x = (edge->pos[0].x + edge->pos[1].x) * half;\n    jcv_real y = (edge->pos[0].y + edge->pos[1].y) * half;\n    jcv_real diffy = y - site->p.y;\n    jcv_real angle = JCV_ATAN2( diffy, x - site->p.x );\n    if( diffy < 0 )\n        angle = angle + 2 * JCV_PI;\n    return (jcv_real)angle;\n}\n\nstatic void jcv_sortedges_insert(jcv_site* site, jcv_graphedge* edge)\n{\n    // Special case for the head end\n    if (site->edges == 0 || site->edges->angle >= edge->angle)\n    {\n        edge->next = site->edges;\n        site->edges = edge;\n    }\n    else\n    {\n        // Locate the node before the point of insertion\n        jcv_graphedge* current = site->edges;\n        while(current->next != 0 && current->next->angle < edge->angle)\n        {\n            current = current->next;\n        }\n        edge->next = current->next;\n        current->next = edge;\n    }\n}\n\nstatic void jcv_finishline(jcv_context_internal* internal, jcv_edge* e)\n{\n    if( !jcv_edge_clipline(internal, e) ) {\n        return;\n    }\n\n    // Make sure the graph edges are CCW\n    int flip = jcv_determinant(&e->sites[0]->p, &e->pos[0], &e->pos[1]) > (jcv_real)0 ? 0 : 1;\n\n    for( int i = 0; i < 2; ++i )\n    {\n        jcv_graphedge* ge = jcv_alloc_graphedge(internal);\n\n        ge->edge = e;\n        ge->next = 0;\n        ge->neighbor = e->sites[1-i];\n        ge->pos[flip] = e->pos[i];\n        ge->pos[1-flip] = e->pos[1-i];\n        ge->angle = jcv_calc_sort_metric(e->sites[i], ge);\n\n        jcv_sortedges_insert( e->sites[i], ge );\n\n        // check that we didn't accidentally add a duplicate (rare), then remove it\n        if( ge->next && ge->angle == ge->next->angle )\n        {\n            if( jcv_point_eq( &ge->pos[0], &ge->next->pos[0] ) && jcv_point_eq( &ge->pos[1], &ge->next->pos[1] ) )\n            {\n                ge->next = ge->next->next; // Throw it away, they're so few anyways\n            }\n        }\n    }\n}\n\n\nstatic void jcv_endpos(jcv_context_internal* internal, jcv_edge* e, const jcv_point* p, int direction)\n{\n    e->pos[direction] = *p;\n\n    if( !jcv_is_valid(&e->pos[1 - direction]) )\n        return;\n\n    jcv_finishline(internal, e);\n}\n\nstatic inline void jcv_create_corner_edge(jcv_context_internal* internal, const jcv_site* site, jcv_graphedge* current, jcv_graphedge* gap)\n{\n    gap->neighbor   = 0;\n    gap->pos[0]     = current->pos[1];\n\n    if( current->pos[1].x < internal->rect.max.x && current->pos[1].y == internal->rect.min.y )\n    {\n        gap->pos[1].x = internal->rect.max.x;\n        gap->pos[1].y = internal->rect.min.y;\n    }\n    else if( current->pos[1].x > internal->rect.min.x && current->pos[1].y == internal->rect.max.y )\n    {\n        gap->pos[1].x = internal->rect.min.x;\n        gap->pos[1].y = internal->rect.max.y;\n    }\n    else if( current->pos[1].y > internal->rect.min.y && current->pos[1].x == internal->rect.min.x )\n    {\n        gap->pos[1].x = internal->rect.min.x;\n        gap->pos[1].y = internal->rect.min.y;\n    }\n    else if( current->pos[1].y < internal->rect.max.y && current->pos[1].x == internal->rect.max.x )\n    {\n        gap->pos[1].x = internal->rect.max.x;\n        gap->pos[1].y = internal->rect.max.y;\n    }\n\n    gap->angle = jcv_calc_sort_metric(site, gap);\n}\n\nstatic jcv_edge* jcv_create_gap_edge(jcv_context_internal* internal, jcv_site* site, jcv_graphedge* ge)\n{\n    jcv_edge* edge  = jcv_alloc_edge(internal);\n    edge->pos[0]    = ge->pos[0];\n    edge->pos[1]    = ge->pos[1];\n    edge->sites[0]  = site;\n    edge->sites[1]  = 0;\n    edge->a = edge->b = edge->c = 0;\n    edge->next      = internal->edges;\n    internal->edges = edge;\n    return edge;\n}\n\nvoid jcv_boxshape_fillgaps(const jcv_clipper* clipper, jcv_context_internal* allocator, jcv_site* site)\n{\n    // They're sorted CCW, so if the current->pos[1] != next->pos[0], then we have a gap\n    jcv_graphedge* current = site->edges;\n    if( !current )\n    {\n        // No edges, then it should be a single cell\n        assert( allocator->numsites == 1 );\n\n        jcv_graphedge* gap = jcv_alloc_graphedge(allocator);\n        gap->neighbor   = 0;\n        gap->pos[0]     = clipper->min;\n        gap->pos[1].x   = clipper->max.x;\n        gap->pos[1].y   = clipper->min.y;\n        gap->angle      = jcv_calc_sort_metric(site, gap);\n        gap->next       = 0;\n        gap->edge       = jcv_create_gap_edge(allocator, site, gap);\n\n        current = gap;\n        site->edges = gap;\n    }\n\n    jcv_graphedge* next = current->next;\n    if( !next )\n    {\n        // Only one edge, then we assume it's a corner gap\n        jcv_graphedge* gap = jcv_alloc_graphedge(allocator);\n        jcv_create_corner_edge(allocator, site, current, gap);\n        gap->edge = jcv_create_gap_edge(allocator, site, gap);\n\n        gap->next = current->next;\n        current->next = gap;\n        current = gap;\n        next = site->edges;\n    }\n\n    while( current && next )\n    {\n        if( jcv_point_on_box_edge(&current->pos[1], &clipper->min, &clipper->max) && !jcv_point_eq(&current->pos[1], &next->pos[0]) )\n        {\n            // Border gap\n            if( current->pos[1].x == next->pos[0].x || current->pos[1].y == next->pos[0].y)\n            {\n                jcv_graphedge* gap = jcv_alloc_graphedge(allocator);\n                gap->neighbor   = 0;\n                gap->pos[0]     = current->pos[1];\n                gap->pos[1]     = next->pos[0];\n                gap->angle      = jcv_calc_sort_metric(site, gap);\n                gap->edge       = jcv_create_gap_edge(allocator, site, gap);\n\n                gap->next = current->next;\n                current->next = gap;\n            }\n            else if( jcv_point_on_box_edge(&current->pos[1], &clipper->min, &clipper->max) &&\n                     jcv_point_on_box_edge(&next->pos[0], &clipper->min, &clipper->max) )\n            {\n                jcv_graphedge* gap = jcv_alloc_graphedge(allocator);\n                jcv_create_corner_edge(allocator, site, current, gap);\n                gap->edge = jcv_create_gap_edge(allocator, site, gap);\n                gap->next = current->next;\n                current->next = gap;\n            }\n            else\n            {\n                // something went wrong, abort instead of looping indefinitely\n                break;\n            }\n        }\n\n        current = current->next;\n        if( current )\n        {\n            next = current->next;\n            if( !next )\n                next = site->edges;\n        }\n    }\n}\n\n// Since the algorithm leaves gaps at the borders/corner, we want to fill them\nstatic void jcv_fillgaps(jcv_diagram* diagram)\n{\n    jcv_context_internal* internal = diagram->internal;\n    if (!internal->clipper.fill_fn)\n        return;\n\n    for( int i = 0; i < internal->numsites; ++i )\n    {\n        jcv_site* site = &internal->sites[i];\n        internal->clipper.fill_fn(&internal->clipper, internal, site);\n    }\n}\n\n\nstatic void jcv_circle_event(jcv_context_internal* internal)\n{\n    jcv_halfedge* left      = (jcv_halfedge*)jcv_pq_pop(internal->eventqueue);\n\n    jcv_halfedge* leftleft  = left->left;\n    jcv_halfedge* right     = left->right;\n    jcv_halfedge* rightright= right->right;\n    jcv_site* bottom = jcv_halfedge_leftsite(left);\n    jcv_site* top    = jcv_halfedge_rightsite(right);\n\n    jcv_point vertex = left->vertex;\n    jcv_endpos(internal, left->edge, &vertex, left->direction);\n    jcv_endpos(internal, right->edge, &vertex, right->direction);\n\n    internal->last_inserted = rightright;\n\n    jcv_pq_remove(internal->eventqueue, right);\n    jcv_halfedge_unlink(left);\n    jcv_halfedge_unlink(right);\n    jcv_halfedge_delete(internal, left);\n    jcv_halfedge_delete(internal, right);\n\n    int direction = JCV_DIRECTION_LEFT;\n    if( bottom->p.y > top->p.y )\n    {\n        jcv_site* temp = bottom;\n        bottom = top;\n        top = temp;\n        direction = JCV_DIRECTION_RIGHT;\n    }\n\n    jcv_edge* edge = jcv_edge_new(internal, bottom, top);\n    edge->next = internal->edges;\n    internal->edges = edge;\n\n    jcv_halfedge* he = jcv_halfedge_new(internal, edge, direction);\n    jcv_halfedge_link(leftleft, he);\n    jcv_endpos(internal, edge, &vertex, JCV_DIRECTION_RIGHT - direction);\n\n    jcv_point p;\n    if( jcv_check_circle_event( leftleft, he, &p ) )\n    {\n        jcv_pq_remove(internal->eventqueue, leftleft);\n        leftleft->vertex    = p;\n        leftleft->y         = p.y + jcv_point_dist(&bottom->p, &p);\n        jcv_pq_push(internal->eventqueue, leftleft);\n    }\n    if( jcv_check_circle_event( he, rightright, &p ) )\n    {\n        he->vertex      = p;\n        he->y           = p.y + jcv_point_dist(&bottom->p, &p);\n        jcv_pq_push(internal->eventqueue, he);\n    }\n}\n\nstatic inline jcv_real jcv_floor(jcv_real v) {\n    jcv_real i = (jcv_real)(int)v;\n    return (v < i) ? i - 1 : i;\n}\n\nstatic inline jcv_real jcv_ceil(jcv_real v) {\n    jcv_real i = (jcv_real)(int)v;\n    return (v > i) ? i + 1 : i;\n}\n\nstatic inline jcv_real jcv_min(jcv_real a, jcv_real b) {\n    return a < b ? a : b;\n}\n\nstatic inline jcv_real jcv_max(jcv_real a, jcv_real b) {\n    return a > b ? a : b;\n}\n\nvoid jcv_diagram_generate( int num_points, const jcv_point* points, const jcv_rect* rect, const jcv_clipper* clipper, jcv_diagram* d )\n{\n    jcv_diagram_generate_useralloc(num_points, points, rect, clipper, 0, jcv_alloc_fn, jcv_free_fn, d);\n}\n\ntypedef union _jcv_cast_align_struct\n{\n    char*   charp;\n    void**  voidpp;\n} jcv_cast_align_struct;\n\nstatic inline void jcv_rect_union(jcv_rect* rect, const jcv_point* p)\n{\n    rect->min.x = jcv_min(rect->min.x, p->x);\n    rect->min.y = jcv_min(rect->min.y, p->y);\n    rect->max.x = jcv_max(rect->max.x, p->x);\n    rect->max.y = jcv_max(rect->max.y, p->y);\n}\n\nstatic inline void jcv_rect_round(jcv_rect* rect)\n{\n    rect->min.x = jcv_floor(rect->min.x);\n    rect->min.y = jcv_floor(rect->min.y);\n    rect->max.x = jcv_ceil(rect->max.x);\n    rect->max.y = jcv_ceil(rect->max.y);\n}\n\nstatic inline void jcv_rect_inflate(jcv_rect* rect, jcv_real amount)\n{\n    rect->min.x -= amount;\n    rect->min.y -= amount;\n    rect->max.x += amount;\n    rect->max.y += amount;\n}\n\nstatic int jcv_prune_duplicates(jcv_context_internal* internal, jcv_rect* rect)\n{\n    int num_sites = internal->numsites;\n    jcv_site* sites = internal->sites;\n\n    jcv_rect r;\n    r.min.x = r.min.y = JCV_FLT_MAX;\n    r.max.x = r.max.y = -JCV_FLT_MAX;\n\n    int offset = 0;\n    // Prune duplicates first\n    for (int i = 0; i < num_sites; i++)\n    {\n        const jcv_site* s = &sites[i];\n        // Remove duplicates, to avoid anomalies\n        if( i > 0 && jcv_point_eq(&s->p, &sites[i - 1].p) )\n        {\n            offset++;\n            continue;\n        }\n\n        sites[i - offset] = sites[i];\n\n        jcv_rect_union(&r, &s->p);\n    }\n    internal->numsites -= offset;\n    if (rect) {\n        *rect = r;\n    }\n    return offset;\n}\n\nstatic int jcv_prune_not_in_shape(jcv_context_internal* internal, jcv_rect* rect)\n{\n    int num_sites = internal->numsites;\n    jcv_site* sites = internal->sites;\n\n    jcv_rect r;\n    r.min.x = r.min.y = JCV_FLT_MAX;\n    r.max.x = r.max.y = -JCV_FLT_MAX;\n\n    int offset = 0;\n    for (int i = 0; i < num_sites; i++)\n    {\n        const jcv_site* s = &sites[i];\n\n        if (!internal->clipper.test_fn(&internal->clipper, s->p))\n        {\n            offset++;\n            continue;\n        }\n\n        sites[i - offset] = sites[i];\n\n        jcv_rect_union(&r, &s->p);\n    }\n    internal->numsites -= offset;\n    if (rect) {\n        *rect = r;\n    }\n    return offset;\n}\n\nstatic jcv_context_internal* jcv_alloc_internal(int num_points, void* userallocctx, FJCVAllocFn allocfn, FJCVFreeFn freefn)\n{\n    // Interesting limits from Euler's equation\n    // Slide 81: https://courses.cs.washington.edu/courses/csep521/01au/lectures/lecture10slides.pdf\n    // Page 3: https://sites.cs.ucsb.edu/~suri/cs235/Voronoi.pdf\n    int max_num_events = num_points*2; // beachline can have max 2*n-5 parabolas\n    size_t sitessize = (size_t)num_points * sizeof(jcv_site);\n    size_t memsize = 8u + (size_t)max_num_events * sizeof(void*) + sizeof(jcv_priorityqueue) + sitessize + sizeof(jcv_context_internal);\n\n    char* originalmem = (char*)allocfn(userallocctx, memsize);\n    memset(originalmem, 0, memsize);\n\n    // align memory\n    char* mem = originalmem + 8 - ( (size_t)(originalmem) & 0x7);\n\n    jcv_context_internal* internal = (jcv_context_internal*)mem;\n    mem += sizeof(jcv_context_internal);\n\n    internal->mem    = originalmem;\n    internal->memctx = userallocctx;\n    internal->alloc  = allocfn;\n    internal->free   = freefn;\n\n    internal->sites = (jcv_site*) mem;\n    mem += sitessize;\n\n    internal->eventqueue = (jcv_priorityqueue*)mem;\n    mem += sizeof(jcv_priorityqueue);\n\n    jcv_cast_align_struct tmp;\n    tmp.charp = mem;\n    internal->eventmem = tmp.voidpp;\n\n    return internal;\n}\n\nvoid jcv_diagram_generate_useralloc(int num_points, const jcv_point* points, const jcv_rect* rect, const jcv_clipper* clipper, void* userallocctx, FJCVAllocFn allocfn, FJCVFreeFn freefn, jcv_diagram* d)\n{\n    if( d->internal )\n        jcv_diagram_free( d );\n\n    jcv_context_internal* internal = jcv_alloc_internal(num_points, userallocctx, allocfn, freefn);\n\n    internal->beachline_start = jcv_halfedge_new(internal, 0, 0);\n    internal->beachline_end = jcv_halfedge_new(internal, 0, 0);\n\n    internal->beachline_start->left     = 0;\n    internal->beachline_start->right    = internal->beachline_end;\n    internal->beachline_end->left       = internal->beachline_start;\n    internal->beachline_end->right      = 0;\n\n    internal->last_inserted = 0;\n\n    int max_num_events = num_points*2; // beachline can have max 2*n-5 parabolas\n    jcv_pq_create(internal->eventqueue, max_num_events, (void**)internal->eventmem);\n\n    internal->numsites = num_points;\n    jcv_site* sites = internal->sites;\n\n    for( int i = 0; i < num_points; ++i )\n    {\n        sites[i].p        = points[i];\n        sites[i].edges    = 0;\n        sites[i].index    = i;\n    }\n\n    qsort(sites, (size_t)num_points, sizeof(jcv_site), jcv_point_cmp);\n\n    jcv_clipper box_clipper;\n    if (clipper == 0) {\n        box_clipper.test_fn = jcv_boxshape_test;\n        box_clipper.clip_fn = jcv_boxshape_clip;\n        box_clipper.fill_fn = jcv_boxshape_fillgaps;\n        clipper = &box_clipper;\n    }\n    internal->clipper = *clipper;\n\n    jcv_rect tmp_rect;\n    tmp_rect.min.x = tmp_rect.min.y = JCV_FLT_MAX;\n    tmp_rect.max.x = tmp_rect.max.y = -JCV_FLT_MAX;\n    jcv_prune_duplicates(internal, &tmp_rect);\n\n    // Prune using the test second\n    if (internal->clipper.test_fn)\n    {\n        // e.g. used by the box clipper in the test_fn\n        internal->clipper.min = rect ? rect->min : tmp_rect.min;\n        internal->clipper.max = rect ? rect->max : tmp_rect.max;\n\n        jcv_prune_not_in_shape(internal, &tmp_rect);\n\n        // The pruning might have made the bounding box smaller\n        if (!rect) {\n            // In the case of all sites being all on a horizontal or vertical line, the\n            // rect area will be zero, and the diagram generation will most likely fail\n            jcv_rect_round(&tmp_rect);\n            jcv_rect_inflate(&tmp_rect, 10);\n\n            internal->clipper.min = tmp_rect.min;\n            internal->clipper.max = tmp_rect.max;\n        }\n    }\n\n    internal->rect = rect ? *rect : tmp_rect;\n\n    d->min      = internal->rect.min;\n    d->max      = internal->rect.max;\n    d->numsites = internal->numsites;\n    d->internal = internal;\n\n    internal->bottomsite = jcv_nextsite(internal);\n\n    jcv_priorityqueue* pq = internal->eventqueue;\n    jcv_site* site = jcv_nextsite(internal);\n\n    int finished = 0;\n    while( !finished )\n    {\n        jcv_point lowest_pq_point;\n        if( !jcv_pq_empty(pq) )\n        {\n            jcv_halfedge* he = (jcv_halfedge*)jcv_pq_top(pq);\n            lowest_pq_point.x = he->vertex.x;\n            lowest_pq_point.y = he->y;\n        }\n\n        if( site != 0 && (jcv_pq_empty(pq) || jcv_point_less(&site->p, &lowest_pq_point) ) )\n        {\n            jcv_site_event(internal, site);\n            site = jcv_nextsite(internal);\n        }\n        else if( !jcv_pq_empty(pq) )\n        {\n            jcv_circle_event(internal);\n        }\n        else\n        {\n            finished = 1;\n        }\n    }\n\n    for( jcv_halfedge* he = internal->beachline_start->right; he != internal->beachline_end; he = he->right )\n    {\n        jcv_finishline(internal, he->edge);\n    }\n\n    jcv_fillgaps(d);\n}\n\n#endif // JC_VORONOI_IMPLEMENTATION\n\n/*\n\nABOUT:\n\n    A fast single file 2D voronoi diagram generator\n\nHISTORY:\n\n    0.7     2019-10-25  - Added support for clipping against convex polygons\n                        - Added JCV_EDGE_INTERSECT_THRESHOLD for edge intersections\n                        - Fixed issue where the bounds calculation wasn’t considering all points\n    0.6     2018-10-21  - Removed JCV_CEIL/JCV_FLOOR/JCV_FABS\n                        - Optimizations: Fewer indirections, better beach head approximation\n    0.5     2018-10-14  - Fixed issue where the graph edge had the wrong edge assigned (issue #28)\n                        - Fixed issue where a point was falsely passing the jcv_is_valid() test (issue #22)\n                        - Fixed jcv_diagram_get_edges() so it now returns _all_ edges (issue #28)\n                        - Added jcv_diagram_get_next_edge() to skip zero length edges (issue #10)\n                        - Added defines JCV_CEIL/JCV_FLOOR/JCV_FLT_MAX for easier configuration\n    0.4     2017-06-03  - Increased the max number of events that are preallocated\n    0.3     2017-04-16  - Added clipping box as input argument (Automatically calculated if needed)\n                        - Input points are pruned based on bounding box\n    0.2     2016-12-30  - Fixed issue of edges not being closed properly\n                        - Fixed issue when having many events\n                        - Fixed edge sorting\n                        - Code cleanup\n    0.1                 Initial version\n\nLICENSE:\n\n    The MIT License (MIT)\n\n    Copyright (c) 2015-2019 Mathias Westerdahl\n\n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE.\n\n\nDISCLAIMER:\n\n    This software is supplied \"AS IS\" without any warranties and support\n\nUSAGE:\n\n    The input points are pruned if\n\n        * There are duplicates points\n        * The input points are outside of the bounding box (i.e. fail the clipping test function)\n        * The input points are rejected by the clipper's test function\n\n    The input bounding box is optional (calculated automatically)\n\n    The input domain is (-FLT_MAX, FLT_MAX] (for floats)\n\n    The api consists of these functions:\n\n    void jcv_diagram_generate( int num_points, const jcv_point* points, const jcv_rect* rect, const jcv_clipper* clipper, jcv_diagram* diagram );\n    void jcv_diagram_generate_useralloc( int num_points, const jcv_point* points, const jcv_rect* rect, const jcv_clipper* clipper, const jcv_clipper* clipper, void* userallocctx, FJCVAllocFn allocfn, FJCVFreeFn freefn, jcv_diagram* diagram );\n    void jcv_diagram_free( jcv_diagram* diagram );\n\n    const jcv_site* jcv_diagram_get_sites( const jcv_diagram* diagram );\n    const jcv_edge* jcv_diagram_get_edges( const jcv_diagram* diagram );\n    const jcv_edge* jcv_diagram_get_next_edge( const jcv_edge* edge );\n\n    An example usage:\n\n    #define JC_VORONOI_IMPLEMENTATION\n    // If you wish to use doubles\n    //#define JCV_REAL_TYPE double\n    //#define JCV_ATAN2 atan2\n    //#define JCV_FLT_MAX 1.7976931348623157E+308\n    #include \"jc_voronoi.h\"\n\n    void draw_edges(const jcv_diagram* diagram);\n    void draw_cells(const jcv_diagram* diagram);\n\n    void generate_and_draw(int numpoints, const jcv_point* points)\n    {\n        jcv_diagram diagram;\n        memset(&diagram, 0, sizeof(jcv_diagram));\n        jcv_diagram_generate(count, points, 0, 0, &diagram);\n\n        draw_edges(diagram);\n        draw_cells(diagram);\n\n        jcv_diagram_free( &diagram );\n    }\n\n    void draw_edges(const jcv_diagram* diagram)\n    {\n        // If all you need are the edges\n        const jcv_edge* edge = jcv_diagram_get_edges( diagram );\n        while( edge )\n        {\n            draw_line(edge->pos[0], edge->pos[1]);\n            edge = jcv_diagram_get_next_edge(edge);\n        }\n    }\n\n    void draw_cells(const jcv_diagram* diagram)\n    {\n        // If you want to draw triangles, or relax the diagram,\n        // you can iterate over the sites and get all edges easily\n        const jcv_site* sites = jcv_diagram_get_sites( diagram );\n        for( int i = 0; i < diagram->numsites; ++i )\n        {\n            const jcv_site* site = &sites[i];\n\n            const jcv_graphedge* e = site->edges;\n            while( e )\n            {\n                draw_triangle( site->p, e->pos[0], e->pos[1]);\n                e = e->next;\n            }\n        }\n    }\n\n    // Here is a simple example of how to do the relaxations of the cells\n    void relax_points(const jcv_diagram* diagram, jcv_point* points)\n    {\n        const jcv_site* sites = jcv_diagram_get_sites(diagram);\n        for( int i = 0; i < diagram->numsites; ++i )\n        {\n            const jcv_site* site = &sites[i];\n            jcv_point sum = site->p;\n            int count = 1;\n\n            const jcv_graphedge* edge = site->edges;\n\n            while( edge )\n            {\n                sum.x += edge->pos[0].x;\n                sum.y += edge->pos[0].y;\n                ++count;\n                edge = edge->next;\n            }\n\n            points[site->index].x = sum.x / count;\n            points[site->index].y = sum.y / count;\n        }\n    }\n\n */\n"
  },
  {
    "path": "deps/jc_voronoi/include/jc_voronoi/jc_voronoi_clip.h",
    "content": "// Copyright (c) 2019 Mathias Westerdahl\n// For full LICENSE (MIT) or USAGE, see bottom of file\n\n#ifndef JC_VORONOI_CLIP_H\n#define JC_VORONOI_CLIP_H\n\n#include \"jc_voronoi.h\"\n\n#pragma pack(push, 1)\n\ntypedef struct _jcv_clipping_polygon\n{\n    jcv_point* points;\n    int num_points;\n} jcv_clipping_polygon;\n\n#pragma pack(pop)\n\n\n// Convex polygon clip functions\nint jcv_clip_polygon_test_point(const jcv_clipper* clipper, const jcv_point p);\nint jcv_clip_polygon_clip_edge(const jcv_clipper* clipper, jcv_edge* e);\nvoid jcv_clip_polygon_fill_gaps(const jcv_clipper* clipper, jcv_context_internal* allocator, jcv_site* site);\n\n\n#endif // JC_VORONOI_CLIP_H\n\n#ifdef JC_VORONOI_CLIP_IMPLEMENTATION\n#undef JC_VORONOI_CLIP_IMPLEMENTATION\n\n// These helpers will probably end up in the main library\n\nstatic inline jcv_real jcv_cross(const jcv_point a, const jcv_point b) {\n    return a.x * b.y - a.y * b.x;\n}\n\nstatic inline jcv_point jcv_add(jcv_point a, jcv_point b) {\n    jcv_point r;\n    r.x = a.x + b.x;\n    r.y = a.y + b.y;\n    return r;\n}\n\nstatic inline jcv_point jcv_sub(jcv_point a, jcv_point b) {\n    jcv_point r;\n    r.x = a.x - b.x;\n    r.y = a.y - b.y;\n    return r;\n}\n\nstatic inline jcv_point jcv_mul(jcv_point v, jcv_real s) {\n    jcv_point r;\n    r.x = v.x * s;\n    r.y = v.y * s;\n    return r;\n}\n\nstatic inline jcv_point jcv_mix(jcv_point a, jcv_point b, jcv_real t) {\n    jcv_point r;\n    r.x = a.x + (b.x - a.x) * t;\n    r.y = a.y + (b.y - a.y) * t;\n    return r;\n}\n\nstatic inline jcv_real jcv_dot(jcv_point a, jcv_point b) {\n    return a.x * b.x + a.y * b.y;\n}\n\n\nstatic inline jcv_real jcv_length(jcv_point v) {\n    return JCV_SQRT(v.x*v.x + v.y*v.y);\n}\n\nstatic inline jcv_real jcv_length_sq(jcv_point v) {\n    return v.x*v.x + v.y*v.y;\n}\n\nstatic inline jcv_real jcv_fabs(jcv_real a) {\n    return a < 0 ? -a : a;\n}\n\n// if it returns [0.0, 1.0] it's on the line segment\nstatic inline jcv_real jcv_point_to_line_segment_t(jcv_point p, jcv_point p0, jcv_point p1) {\n    jcv_point vpoint = jcv_sub(p, p0);\n    jcv_point vsegment = jcv_sub(p1, p0);\n    return jcv_dot(vsegment, vpoint) / jcv_dot(vsegment, vsegment);\n}\n\nint jcv_clip_polygon_test_point(const jcv_clipper* clipper, const jcv_point p)\n{\n    jcv_clipping_polygon* polygon = (jcv_clipping_polygon*)clipper->ctx;\n    int num_points = polygon->num_points;\n\n    // convex polygon\n    // winding CCW\n    // all polygon normals point outward\n    // if the point is in front of the plane, it is outside\n\n    int result = 1;\n    for (int i = 0; i < num_points; ++i)\n    {\n        jcv_point p0 = polygon->points[i];\n        jcv_point p1 = polygon->points[(i+1)%num_points];\n        jcv_point n;\n        n.x = p1.y - p0.y;\n        n.y = p0.x - p1.x;\n        jcv_point diff;\n        diff.x = p.x - p0.x;\n        diff.y = p.y - p0.y;\n\n        if (jcv_dot(n, diff) > 0) {\n            result = 0;\n            break;\n        }\n    }\n    return result;\n}\n\nstatic int jcv_ray_intersect_polygon(const jcv_clipper* clipper, jcv_point p0, jcv_point p1, jcv_real* out_t0, jcv_real* out_t1)\n{\n    jcv_clipping_polygon* polygon = (jcv_clipping_polygon*)clipper->ctx;\n    int num_points = polygon->num_points;\n\n    jcv_real t0 = (jcv_real)0;\n    jcv_real t1 = (jcv_real)1;\n    jcv_point dir = jcv_sub(p1, p0);\n\n    for (int i = 0; i < num_points; ++i)\n    {\n        jcv_point v0 = polygon->points[i];\n        jcv_point v1 = polygon->points[(i+1)%num_points];\n        jcv_point n;\n        n.x = v1.y - v0.y;\n        n.y = -(v1.x - v0.x);\n\n        jcv_point v0p0 = jcv_sub(p0, v0);\n\n        jcv_real N = -jcv_dot(v0p0, n);\n        jcv_real D = jcv_dot(dir, n);\n        if (jcv_fabs(D) < 0.0001f) // parallel to the line\n        {\n            if (N < 0)\n                return 0;\n            continue;\n        }\n\n        jcv_real t = N / D;\n        if (D < 0) // -> entering\n        {\n            t0 = t > t0 ? t : t0;\n            if (t0 > t1)\n                return 0;\n        }\n        else // D > 0 -> exiting\n        {\n            t1 = t < t1 ? t : t1;\n            if (t1 < t0)\n                return 0;\n        }\n    }\n\n    *out_t0 = t0;\n    *out_t1 = t1;\n    return 1;\n}\n\nint jcv_clip_polygon_clip_edge(const jcv_clipper* clipper, jcv_edge* e)\n{\n    // Using the box clipper to get a finite line segment\n    int result = jcv_boxshape_clip(clipper, e);\n    if (!result)\n        return 0;\n\n    jcv_point p0 = e->pos[0];\n    jcv_point p1 = e->pos[1];\n\n    jcv_real t0;\n    jcv_real t1;\n    result = jcv_ray_intersect_polygon(clipper, p0, p1, &t0, &t1);\n\n    if (!result) {\n        e->pos[0] = e->pos[1];\n        return 0;\n    }\n\n    e->pos[0] = jcv_mix(p0, p1, t0);\n    e->pos[1] = jcv_mix(p0, p1, t1);\n    return 1;\n}\n\n// Find the edge which the point sits on\nstatic int jcv_find_polygon_edge(const jcv_clipper* clipper, jcv_point p)\n{\n    jcv_clipping_polygon* polygon = (jcv_clipping_polygon*)clipper->ctx;\n\n    int min_edge = -1;\n    jcv_real min_dist = (jcv_real)1000000;\n    int num_points = polygon->num_points;\n    for (int i = 0; i < num_points; ++i)\n    {\n        jcv_point p0 = polygon->points[i];\n        if (jcv_point_eq(&p, &p0))\n            return i;\n\n        jcv_point p1 = polygon->points[(i+1)%num_points];\n        jcv_point vsegment = jcv_sub(p1, p0);\n        jcv_point vpoint = jcv_sub(p, p0);\n\n        jcv_real t = jcv_dot(vsegment, vpoint) / jcv_dot(vsegment,vsegment);\n\n        if (t < (jcv_real)0.0f || t > (jcv_real)1.0f)\n            continue;\n\n        jcv_point projected = jcv_add(p0, jcv_mul(vsegment, t));\n        jcv_real distsq = jcv_length_sq(jcv_sub(p, projected));\n\n        if (distsq < min_dist) {\n            min_dist = distsq;\n            min_edge = i;\n        }\n    }\n    assert(min_edge >= 0);\n    return min_edge;\n}\n\nvoid jcv_clip_polygon_fill_gaps(const jcv_clipper* clipper, jcv_context_internal* allocator, jcv_site* site)\n{\n    // They're sorted CCW, so if the current->pos[1] != next->pos[0], then we have a gap\n    jcv_clipping_polygon* polygon = (jcv_clipping_polygon*)clipper->ctx;\n    int num_points = polygon->num_points;\n\n    jcv_graphedge* current = site->edges;\n    if( !current )\n    {\n        jcv_graphedge* gap = jcv_alloc_graphedge(allocator);\n        gap->neighbor = 0;\n        // Pick the first edge of the polygon (which is also CCW)\n        gap->pos[0] = polygon->points[0];\n        gap->pos[1] = polygon->points[1];\n        gap->angle  = jcv_calc_sort_metric(site, gap);\n        gap->next   = 0;\n        gap->edge   = jcv_create_gap_edge(allocator, site, gap);\n\n        current = gap;\n        site->edges = gap;\n    }\n\n    jcv_graphedge* next = current->next;\n    if( !next )\n    {\n        jcv_graphedge* gap = jcv_alloc_graphedge(allocator);\n\n        int polygon_edge = jcv_find_polygon_edge(clipper, current->pos[1]);\n        if (!jcv_point_eq(&current->pos[1], &polygon->points[(polygon_edge+1)%num_points])) {\n            gap->pos[0] = current->pos[1];\n            gap->pos[1] = polygon->points[(polygon_edge+1)%num_points];\n        } else {\n            gap->pos[0] = polygon->points[(polygon_edge+1)%num_points];\n            gap->pos[1] = polygon->points[(polygon_edge+2)%num_points];\n        }\n\n        gap->neighbor   = 0;\n        gap->angle      = jcv_calc_sort_metric(site, gap);\n        gap->next       = 0;\n        gap->edge       = jcv_create_gap_edge(allocator, site, gap);\n\n        gap->next = current->next;\n        current->next = gap;\n        current = gap;\n        next = site->edges;\n    }\n\n    while (current && next)\n    {\n        if (!jcv_point_eq(&current->pos[1], &next->pos[0]))\n        {\n            int polygon_edge1 = jcv_find_polygon_edge(clipper, current->pos[1]);\n            int polygon_edge2 = jcv_find_polygon_edge(clipper, next->pos[0]);\n\n            jcv_graphedge* gap = jcv_alloc_graphedge(allocator);\n            gap->pos[0] = current->pos[1];\n\n            if (polygon_edge1 != polygon_edge2) {\n                gap->pos[1] = polygon->points[(polygon_edge1+1)%num_points];\n            } else {\n                gap->pos[1] = next->pos[0];\n            }\n\n            gap->neighbor   = 0;\n            gap->angle      = jcv_calc_sort_metric(site, gap);\n            gap->edge       = jcv_create_gap_edge(allocator, site, gap);\n            gap->next       = current->next;\n            current->next   = gap;\n        }\n\n        current = current->next;\n        if( current )\n        {\n            next = current->next;\n            if( !next ) {\n                next = site->edges;\n            }\n        }\n    }\n}\n\n#endif // JC_VORONOI_CLIP_IMPLEMENTATION\n\n\n/*\n\nABOUT:\n\n    Helper functions for clipping a vosonoi diagram against a convex polygon\n\n\nLICENSE:\n\n    The MIT License (MIT)\n\n    Copyright (c) 2019 Mathias Westerdahl\n\n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE.\n\n\nDISCLAIMER:\n\n    This software is supplied \"AS IS\" without any warranties and support\n\nUSAGE:\n\nUSAGE:\n\n    The function `jcv_clipper` struct allows for supplying a set of custom clipper functions to interact with the generating of the resulting diagram.\n\n    #define JC_VORONOI_CLIP_IMPLEMENTATION\n    #include \"jc_voronoi_clip.h\"\n\n    jcv_clipping_polygon polygon;\n    // Triangle\n    polygon.num_points = 3;\n    polygon.points = (jcv_point*)malloc(sizeof(jcv_point)*(size_t)polygon.num_points);\n\n    polygon.points[0].x = width/2;\n    polygon.points[1].x = width - width/5;\n    polygon.points[2].x = width/5;\n    polygon.points[0].y = height/5;\n    polygon.points[1].y = height - height/5;\n    polygon.points[2].y = height - height/5;\n\n    jcv_clipper polygonclipper;\n    polygonclipper.test_fn = jcv_clip_polygon_test_point;\n    polygonclipper.clip_fn = jcv_clip_polygon_clip_edge;\n    polygonclipper.fill_fn = jcv_clip_polygon_fill_gaps;\n    polygonclipper.ctx = &polygon;\n\n    jcv_diagram diagram;\n    memset(&diagram, 0, sizeof(jcv_diagram));\n    jcv_diagram_generate(count, (const jcv_point*)points, 0, clipper, &diagram);\n*/\n\n"
  },
  {
    "path": "misc/conditional_deploy_build.py",
    "content": "import sys, os, subprocess\n\nif os.getenv(\"APPVEYOR_REPO_TAG\") == \"true\":\n    proc = subprocess.Popen(['python','-m', 'twine', 'upload','--skip-existing', 'wheelhouse/*'])\n    proc.communicate() # wait for it to terminate\n\n    # forward the return code\n    code = proc.returncode\n    sys.exit(code)\n\nelse:\n    print(\"not deploying, no appveyor repo tag present\")\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"robust_laplacian\"\nversion = \"1.1.0\"\ndescription = \"Robust Laplace matrices for meshes and point clouds\"\nreadme = \"README.md\"\nlicense.file = \"LICENSE\"\nauthors = [\n  { name = \"Nicholas Sharp\", email = \"nmwsharp@gmail.com\" },\n]\nmaintainers = [\n  {  name = \"Nicholas Sharp\", email = \"nmwsharp@gmail.com\" },\n]\nclassifiers = [\n  \"Development Status :: 5 - Production/Stable\",\n  \"License :: OSI Approved :: MIT License\",\n  \"Programming Language :: Python :: 3\",\n]\nrequires-python = \">=3.9\"\n\ndependencies = [\n  \"numpy\",\n]\n\n[project.urls]\nHomepage = \"https://github.com/nmwsharp/robust-laplacians-py\"\n\n[build-system]\nrequires = [\"scikit-build-core\"]\nbuild-backend = \"scikit_build_core.build\"\n\n[tool.scikit-build]\nbuild.verbose = true\nlogging.level = \"INFO\""
  },
  {
    "path": "src/cpp/.clang-format",
    "content": "﻿---\nAlignAfterOpenBracket: Align\nAlignOperands: 'true'\nAllowShortBlocksOnASingleLine: 'false'\nAllowShortIfStatementsOnASingleLine: 'true'\nAllowShortLoopsOnASingleLine: 'true'\nAlwaysBreakTemplateDeclarations: 'true'\nBinPackParameters: 'true'\nBreakBeforeBraces: Attach\nColumnLimit: '120'\nIndentWidth: '2'\nKeepEmptyLinesAtTheStartOfBlocks: 'true'\nMaxEmptyLinesToKeep: '2'\nPointerAlignment: Left\nReflowComments: 'true'\nSpacesInAngles: 'false'\nSpacesInParentheses: 'false'\nSpacesInSquareBrackets: 'false'\nStandard: Cpp11\nUseTab: Never\n\n...\n"
  },
  {
    "path": "src/cpp/core.cpp",
    "content": "#include \"point_cloud_utilities.h\"\n\n#include \"geometrycentral/numerical/linear_algebra_utilities.h\"\n#include \"geometrycentral/surface/edge_length_geometry.h\"\n#include \"geometrycentral/surface/intrinsic_mollification.h\"\n#include \"geometrycentral/surface/manifold_surface_mesh.h\"\n#include \"geometrycentral/surface/meshio.h\"\n#include \"geometrycentral/surface/simple_polygon_mesh.h\"\n#include \"geometrycentral/surface/surface_mesh.h\"\n#include \"geometrycentral/surface/surface_mesh_factories.h\"\n#include \"geometrycentral/surface/tufted_laplacian.h\"\n#include \"geometrycentral/surface/vertex_position_geometry.h\"\n\n#include <pybind11/eigen.h>\n#include <pybind11/numpy.h>\n#include <pybind11/pybind11.h>\n\n#include \"Eigen/Dense\"\n\nnamespace py = pybind11;\n\nusing namespace geometrycentral;\nusing namespace geometrycentral::surface;\n\n\n// For overloaded functions, with C++11 compiler only\ntemplate <typename... Args>\nusing overload_cast_ = pybind11::detail::overload_cast_impl<Args...>;\n\n// Parameters related to unused elements. Maybe expose these as parameters?\ndouble laplacianReplaceVal = 1.0;\ndouble massReplaceVal = -1e-3;\n\n\nstd::tuple<SparseMatrix<double>, SparseMatrix<double>>\nbuildMeshLaplacian(const DenseMatrix<double>& vMat, const DenseMatrix<size_t>& fMat, double mollifyFactor) {\n\n  // First, load a simple polygon mesh\n  SimplePolygonMesh simpleMesh;\n\n  // Copy to std vector representation\n  simpleMesh.vertexCoordinates.resize(vMat.rows());\n  for (size_t iP = 0; iP < simpleMesh.vertexCoordinates.size(); iP++) {\n    simpleMesh.vertexCoordinates[iP] = Vector3{vMat(iP, 0), vMat(iP, 1), vMat(iP, 2)};\n  }\n  simpleMesh.polygons.resize(fMat.rows());\n  for (size_t iF = 0; iF < simpleMesh.polygons.size(); iF++) {\n    simpleMesh.polygons[iF] = std::vector<size_t>{fMat(iF, 0), fMat(iF, 1), fMat(iF, 2)};\n  }\n\n  // Remove any unused vertices\n  std::vector<size_t> oldToNewMap = simpleMesh.stripUnusedVertices();\n\n\n  // Build the rich mesh data structure\n  std::unique_ptr<SurfaceMesh> mesh;\n  std::unique_ptr<VertexPositionGeometry> geometry;\n  std::tie(mesh, geometry) = makeSurfaceMeshAndGeometry(simpleMesh.polygons, simpleMesh.vertexCoordinates);\n\n  // Do the hard work, calling the geometry-central function\n  SparseMatrix<double> L, M;\n  std::tie(L, M) = buildTuftedLaplacian(*mesh, *geometry, mollifyFactor);\n\n  // If necessary, re-index matrices to account for any unreferenced vertices which were skipped.\n  // For any unreferenced verts, creates an identity row/col in the Laplacian and\n  bool anyUnreferenced = false;\n  for (const size_t& ind : oldToNewMap) {\n    if (ind == INVALID_IND) anyUnreferenced = true;\n  }\n  if (anyUnreferenced) {\n\n\n    // Invert the map\n    std::vector<size_t> newToOldMap(simpleMesh.nVertices());\n    for (size_t iOld = 0; iOld < oldToNewMap.size(); iOld++) {\n      if (oldToNewMap[iOld] != INVALID_IND) {\n        newToOldMap[oldToNewMap[iOld]] = iOld;\n      }\n    }\n    size_t N = oldToNewMap.size();\n\n    { // Update the Laplacian\n\n      std::vector<Eigen::Triplet<double>> triplets;\n\n      // Copy entries\n      for (int k = 0; k < L.outerSize(); k++) {\n        for (typename SparseMatrix<double>::InnerIterator it(L, k); it; ++it) {\n          double thisVal = it.value();\n          int i = it.row();\n          int j = it.col();\n          triplets.emplace_back(newToOldMap[i], newToOldMap[j], thisVal);\n        }\n      }\n\n      // Add diagonal entries for unreferenced\n      for (size_t iOld = 0; iOld < oldToNewMap.size(); iOld++) {\n        if (oldToNewMap[iOld] == INVALID_IND) {\n          triplets.emplace_back(iOld, iOld, laplacianReplaceVal);\n        }\n      }\n\n      // Update the matrix\n      L = SparseMatrix<double>(N, N);\n      L.setFromTriplets(triplets.begin(), triplets.end());\n    }\n\n    { // Update the mass matrix\n      std::vector<Eigen::Triplet<double>> triplets;\n\n      // Copy entries\n      double smallestVal = std::numeric_limits<double>::infinity();\n      for (int k = 0; k < M.outerSize(); k++) {\n        for (typename SparseMatrix<double>::InnerIterator it(M, k); it; ++it) {\n          double thisVal = it.value();\n          int i = it.row();\n          int j = it.col();\n          triplets.emplace_back(newToOldMap[i], newToOldMap[j], thisVal);\n          smallestVal = std::fmin(smallestVal, std::abs(thisVal));\n        }\n      }\n\n      // Add diagonal entries for unreferenced\n      double newMassVal = massReplaceVal < 0 ? -massReplaceVal * smallestVal : massReplaceVal;\n      for (size_t iOld = 0; iOld < oldToNewMap.size(); iOld++) {\n        if (oldToNewMap[iOld] == INVALID_IND) {\n          triplets.emplace_back(iOld, iOld, newMassVal);\n        }\n      }\n\n      // Update the matrix\n      M = SparseMatrix<double>(N, N);\n      M.setFromTriplets(triplets.begin(), triplets.end());\n    }\n  }\n\n\n  return std::make_tuple(L, M);\n}\n\nstd::tuple<SparseMatrix<double>, SparseMatrix<double>> buildPointCloudLaplacian(const DenseMatrix<double>& vMat,\n                                                                                double mollifyFactor, size_t nNeigh) {\n\n  SimplePolygonMesh cloudMesh;\n\n  // Copy to std vector representation\n  cloudMesh.vertexCoordinates.resize(vMat.rows());\n  for (size_t iP = 0; iP < cloudMesh.vertexCoordinates.size(); iP++) {\n    cloudMesh.vertexCoordinates[iP] = Vector3{vMat(iP, 0), vMat(iP, 1), vMat(iP, 2)};\n  }\n\n  // Generate the local triangulations for the point cloud\n  Neighbors_t neigh = generate_knn(cloudMesh.vertexCoordinates, nNeigh);\n  std::vector<Vector3> normals = generate_normals(cloudMesh.vertexCoordinates, neigh);\n  std::vector<std::vector<Vector2>> coords = generate_coords_projection(cloudMesh.vertexCoordinates, normals, neigh);\n  LocalTriangulationResult localTri = build_delaunay_triangulations(coords, neigh);\n\n  // Take the union of all triangles in all the neighborhoods\n  for (size_t iPt = 0; iPt < cloudMesh.vertexCoordinates.size(); iPt++) {\n    const std::vector<size_t>& thisNeigh = neigh[iPt];\n    size_t nNeigh = thisNeigh.size();\n\n    // Accumulate over triangles\n    for (const auto& tri : localTri.pointTriangles[iPt]) {\n      std::array<size_t, 3> triGlobal = {iPt, thisNeigh[tri[1]], thisNeigh[tri[2]]};\n      cloudMesh.polygons.push_back({triGlobal[0], triGlobal[1], triGlobal[2]});\n    }\n  }\n\n\n  // strip unreferenced vertices (can we argue this should never happen? good regardless for robustness.)\n  std::vector<size_t> oldToNewMap = cloudMesh.stripUnusedVertices();\n\n  std::unique_ptr<SurfaceMesh> mesh;\n  std::unique_ptr<VertexPositionGeometry> geometry;\n  std::tie(mesh, geometry) = makeSurfaceMeshAndGeometry(cloudMesh.polygons, cloudMesh.vertexCoordinates);\n\n  SparseMatrix<double> L, M;\n  std::tie(L, M) = buildTuftedLaplacian(*mesh, *geometry, mollifyFactor);\n\n  L = L / 3.;\n  M = M / 3.;\n\n  // If necessary, re-index matrices to account for any unreferenced vertices which were skipped.\n  // For any unreferenced verts, creates an identity row/col in the Laplacian and\n  bool anyUnreferenced = false;\n  for (const size_t& ind : oldToNewMap) {\n    if (ind == INVALID_IND) anyUnreferenced = true;\n  }\n  if (anyUnreferenced) {\n\n\n    // Invert the map\n    std::vector<size_t> newToOldMap(cloudMesh.nVertices());\n    for (size_t iOld = 0; iOld < oldToNewMap.size(); iOld++) {\n      if (oldToNewMap[iOld] != INVALID_IND) {\n        newToOldMap[oldToNewMap[iOld]] = iOld;\n      }\n    }\n    size_t N = oldToNewMap.size();\n\n    { // Update the Laplacian\n\n      std::vector<Eigen::Triplet<double>> triplets;\n\n      // Copy entries\n      for (int k = 0; k < L.outerSize(); k++) {\n        for (typename SparseMatrix<double>::InnerIterator it(L, k); it; ++it) {\n          double thisVal = it.value();\n          int i = it.row();\n          int j = it.col();\n          triplets.emplace_back(newToOldMap[i], newToOldMap[j], thisVal);\n        }\n      }\n\n      // Add diagonal entries for unreferenced\n      for (size_t iOld = 0; iOld < oldToNewMap.size(); iOld++) {\n        if (oldToNewMap[iOld] == INVALID_IND) {\n          triplets.emplace_back(iOld, iOld, laplacianReplaceVal);\n        }\n      }\n\n      // Update the matrix\n      L = SparseMatrix<double>(N, N);\n      L.setFromTriplets(triplets.begin(), triplets.end());\n    }\n\n    { // Update the mass matrix\n      std::vector<Eigen::Triplet<double>> triplets;\n\n      // Copy entries\n      double smallestVal = std::numeric_limits<double>::infinity();\n      for (int k = 0; k < M.outerSize(); k++) {\n        for (typename SparseMatrix<double>::InnerIterator it(M, k); it; ++it) {\n          double thisVal = it.value();\n          int i = it.row();\n          int j = it.col();\n          triplets.emplace_back(newToOldMap[i], newToOldMap[j], thisVal);\n          smallestVal = std::fmin(smallestVal, std::abs(thisVal));\n        }\n      }\n\n      // Add diagonal entries for unreferenced\n      double newMassVal = massReplaceVal < 0 ? -massReplaceVal * smallestVal : massReplaceVal;\n      for (size_t iOld = 0; iOld < oldToNewMap.size(); iOld++) {\n        if (oldToNewMap[iOld] == INVALID_IND) {\n          triplets.emplace_back(iOld, iOld, newMassVal);\n        }\n      }\n\n      // Update the matrix\n      M = SparseMatrix<double>(N, N);\n      M.setFromTriplets(triplets.begin(), triplets.end());\n    }\n  }\n\n\n  return std::make_tuple(L, M);\n}\n\n// Actual binding code\n// clang-format off\nPYBIND11_MODULE(robust_laplacian_bindings, m) {\n  m.doc() = \"Robust laplacian low-level bindings\";\n  \n\n  m.def(\"buildMeshLaplacian\", &buildMeshLaplacian, \"build the mesh Laplacian\", \n      py::arg(\"vMat\"), py::arg(\"fMat\"), py::arg(\"mollifyFactor\"));\n  \n  m.def(\"buildPointCloudLaplacian\", &buildPointCloudLaplacian, \"build the point cloud Laplacian\", \n      py::arg(\"vMat\"), py::arg(\"mollifyFactor\"), py::arg(\"nNeigh\"));\n}\n\n// clang-format on\n"
  },
  {
    "path": "src/cpp/point_cloud_utilities.cpp",
    "content": "#include \"point_cloud_utilities.h\"\n\n#include \"geometrycentral/utilities/elementary_geometry.h\"\n#include \"geometrycentral/utilities/knn.h\"\n\n#include \"Eigen/Dense\"\n\n#include <cfloat>\n#include <numeric>\n\nstd::vector<std::vector<size_t>> generate_knn(const std::vector<Vector3>& points, size_t k) {\n\n  geometrycentral::NearestNeighborFinder finder(points);\n\n  std::vector<std::vector<size_t>> result;\n  for (size_t i = 0; i < points.size(); i++) {\n    result.emplace_back(finder.kNearestNeighbors(i, k));\n  }\n\n  return result;\n}\n\n\nstd::vector<Vector3> generate_normals(const std::vector<Vector3>& points, const Neighbors_t& neigh) {\n\n  std::vector<Vector3> normals(points.size());\n\n  for (size_t iPt = 0; iPt < points.size(); iPt++) {\n    size_t nNeigh = neigh[iPt].size();\n\n    // Compute centroid\n    Vector3 center{0., 0., 0.};\n    for (size_t iN = 0; iN < nNeigh; iN++) {\n      center += points[neigh[iPt][iN]];\n    }\n    center /= nNeigh + 1;\n\n    // Assemble matrix os vectors from centroid\n    Eigen::MatrixXd localMat(3, neigh[iPt].size());\n    for (size_t iN = 0; iN < nNeigh; iN++) {\n      Vector3 neighPos = points[neigh[iPt][iN]] - center;\n      localMat(0, iN) = neighPos.x;\n      localMat(1, iN) = neighPos.y;\n      localMat(2, iN) = neighPos.z;\n    }\n\n    // Smallest singular vector is best normal\n    Eigen::JacobiSVD<Eigen::MatrixXd> svd(localMat, Eigen::ComputeThinU);\n    Eigen::Vector3d bestNormal = svd.matrixU().col(2);\n\n    Vector3 N{bestNormal(0), bestNormal(1), bestNormal(2)};\n    N = unit(N);\n    normals[iPt] = N;\n  }\n\n  return normals;\n}\n\n\nstd::vector<std::vector<Vector2>> generate_coords_projection(const std::vector<Vector3>& points,\n                                                             const std::vector<Vector3> normals,\n                                                             const Neighbors_t& neigh) {\n  std::vector<std::vector<Vector2>> coords(points.size());\n\n  for (size_t iPt = 0; iPt < points.size(); iPt++) {\n    size_t nNeigh = neigh[iPt].size();\n    coords[iPt].resize(nNeigh);\n    Vector3 center = points[iPt];\n    Vector3 normal = normals[iPt];\n\n    // build an arbitrary tangent basis\n    Vector3 basisX, basisY;\n    auto r = normal.buildTangentBasis();\n    basisX = r[0];\n    basisY = r[1];\n\n    for (size_t iN = 0; iN < nNeigh; iN++) {\n      Vector3 vec = points[neigh[iPt][iN]] - center;\n      vec = vec.removeComponent(normal);\n\n      Vector2 coord{dot(basisX, vec), dot(basisY, vec)};\n      coords[iPt][iN] = coord;\n    }\n  }\n\n  return coords;\n}\n\n// For each planar-projected neighborhood, generate the triangles in the Delaunay triangulation which are incident on\n// the center vertex.\n//\n// This could be done robustly via e.g. Shewchuk's triangle.c. However, instead we use a simple self-contained strategy\n// which leverages the needs of this particular situation. In particular, we don't really care about getting exactly the\n// Delaunay triangulation; we're just looking for any sane triangulation to use as input the the subsequent step. We\n// just use Delaunay because we like the property that (in the limit of sampling), it's a triple-cover of the domain;\n// with other strategies it's hard to quantify how many times our triangles cover the domain. This makes the problem\n// easier, because for degenerate/underdetermined cases, we're happy to output any triangulation, even if it's not the\n// Delaunay triangulation in exact arithmetic.\n//\n// This strategy works by angularly sorting points relative to the neighborhood center, then walking around circle\n// identifying pairs of edges which form Delaunay triangles (more details inline). In particular, using a sorting of the\n// points helps to distinguish indeterminate cases and always output some triangles. Additionally, a few heuristics are\n// included for handling of degenerate and collinear points. This routine has O(n*k^2) complexity, where k is the\n// neighborhood size).\nLocalTriangulationResult build_delaunay_triangulations(const std::vector<std::vector<Vector2>>& coords,\n                                                       const Neighbors_t& neigh) {\n\n  // A few innocent numerical parameters\n  const double PERTURB_THRESH = 1e-7;         // in units of relative length\n  const double ANGLE_COLLINEAR_THRESH = 1e-5; // in units of radians\n  const double OUTSIDE_EPS = 1e-4;            // in units of relative length\n\n  // NOTE: This is not robust if the entire neighbohood is coincident (or very nearly coincident) with the centerpoint.\n  // Though in that case, the generate_normals() routine will probably also have issues.\n\n  size_t nPts = coords.size();\n  LocalTriangulationResult result;\n  result.pointTriangles.resize(nPts);\n\n  for (size_t iPt = 0; iPt < nPts; iPt++) {\n    size_t nNeigh = neigh[iPt].size();\n    double lenScale = norm(coords[iPt].back());\n\n    // Something is hopelessly degenerate, don't even bother trying. No triangles for this point.\n    if (!std::isfinite(lenScale) || lenScale <= 0) {\n      continue;\n    }\n\n    // Local copies of points\n    std::vector<Vector2> perturbPoints = coords[iPt];\n    std::vector<size_t> perturbInds = neigh[iPt];\n\n    { // Perturb points which are extremely close to the source\n      for (size_t iNeigh = 0; iNeigh < nNeigh; iNeigh++) {\n        Vector2& neighPt = perturbPoints[iNeigh];\n        double dist = norm(neighPt);\n        if (dist < lenScale * PERTURB_THRESH) { // need to perturb\n          Vector2 dir = normalize(neighPt);\n          if (!isfinite(dir)) { // even direction is degenerate :(\n            // pick a direction from index\n            double thetaDir = (2. * M_PI * iNeigh) / nNeigh;\n            dir = Vector2::fromAngle(thetaDir);\n          }\n\n          // Set the distance from the origin for the pertubed point. Including the index avoids creating many\n          // co-circular points; no need to stress the Delaunay triangulation unnessecarily.\n          double len = (1. + static_cast<double>(iNeigh) / nNeigh) * lenScale * PERTURB_THRESH * 10;\n\n          neighPt = len * dir; // update the point\n        }\n      }\n    }\n\n\n    size_t closestPointInd = 0;\n    double closestPointDist = std::numeric_limits<double>::infinity();\n    bool hasBoundary = false;\n    { // Find the starting point for the angular search.\n      // If there is boundary, it's the beginning of the interior region; otherwise its the closest point.\n      // (either way, this point is guaranteed to appear in the triangulation)\n      // NOTE: boundary check is actually done after inline sort below, since its cheaper there\n\n      for (size_t iNeigh = 0; iNeigh < nNeigh; iNeigh++) {\n        Vector2 neighPt = perturbPoints[iNeigh];\n        double thisPointDist = norm(neighPt);\n        if (thisPointDist < closestPointDist) {\n          closestPointDist = thisPointDist;\n          closestPointInd = iNeigh;\n        }\n      }\n    }\n\n\n    std::vector<size_t> sortInds(nNeigh);\n    { // = Angularly sort the points CCW, such that the closest point comes first\n\n      // Angular sort\n      std::vector<double> pointAngles(nNeigh);\n      for (size_t i = 0; i < nNeigh; i++) {\n        pointAngles[i] = arg(perturbPoints[i]);\n      }\n      std::iota(std::begin(sortInds), std::end(sortInds), 0);\n      std::sort(sortInds.begin(), sortInds.end(),\n                [&](const size_t& a, const size_t& b) -> bool { return pointAngles[a] < pointAngles[b]; });\n\n      // Check if theres a gap of >= PI between any two consecutive points. If so it's a boundary.\n      double largestGap = -1;\n      size_t largestGapEndInd = 0;\n      for (size_t i = 0; i < nNeigh; i++) {\n        size_t j = (i + 1) % nNeigh;\n        double angleI = pointAngles[sortInds[i]];\n        double angleJ = pointAngles[sortInds[j]];\n        double gap;\n        if (i + 1 == nNeigh) {\n          gap = angleJ - (angleI + 2 * M_PI);\n        } else {\n          gap = angleJ - angleI;\n        }\n\n        if (gap > largestGap) {\n          largestGap = gap;\n          largestGapEndInd = j;\n        }\n      }\n\n      // The start of the cyclic ordering is either\n      size_t firstInd;\n      if (largestGap > (M_PI - ANGLE_COLLINEAR_THRESH)) {\n        firstInd = largestGapEndInd;\n        hasBoundary = true;\n      } else {\n        firstInd = std::distance(sortInds.begin(), std::find(sortInds.begin(), sortInds.end(), closestPointInd));\n        hasBoundary = false;\n      }\n\n      // Cyclically permute to ensure starting point comes first\n      std::rotate(sortInds.begin(), sortInds.begin() + firstInd, sortInds.end());\n    }\n\n    size_t edgeStartInd = 0;\n    std::vector<std::array<size_t, 3>>& thisPointTriangles = result.pointTriangles[iPt]; // accumulate result\n\n    // end point should wrap around the check the first point only if there is no boundary\n    size_t searchEnd = nNeigh + (hasBoundary ? 0 : 1);\n\n    // Walk around the angularly-sorted points, forming triangles spanning angular regions. To construct each triangle,\n    // we start with leg at edgeStartInd, then search over edgeEndInd to find the first other end which has an empty\n    // circumcircle. Once it is found, we form a triangle and being searching again from edgeEndInd.\n    //\n    // At first, this might sound like it has n^3 complexity, since there are n^2 triangles to consider, and testing\n    // each costs n. However, since we march around the angular direction in increasing order, we will only test at most\n    // O(n) triangles, leading to n^2 complexity.\n    while (edgeStartInd < nNeigh) {\n      size_t iStart = sortInds[edgeStartInd];\n      Vector2 startPos = perturbPoints[iStart];\n\n      // lookahead and find the first triangle we can form with an empty (or nearly empty) circumcircle\n      bool foundTri = false;\n      for (size_t edgeEndInd = edgeStartInd + 1; edgeEndInd < searchEnd; edgeEndInd++) {\n        size_t iEnd = sortInds[edgeEndInd % nNeigh];\n        Vector2 endPos = perturbPoints[iEnd];\n\n        // If the start and end points are too close to being colinear, don't bother\n        Vector2 startPosDir = unit(startPos);\n        Vector2 endPosDir = unit(endPos);\n        if (std::fabs(cross(startPosDir, endPosDir)) < ANGLE_COLLINEAR_THRESH) {\n          continue;\n        }\n\n        // Find the circumcenter and circumradius\n        geometrycentral::RayRayIntersectionResult2D isect =\n            rayRayIntersection(0.5 * startPos, startPosDir.rotate90(), 0.5 * endPos, -endPosDir.rotate90());\n        Vector2 circumcenter = 0.5 * startPos + isect.tRay1 * startPosDir.rotate90();\n        double circumradius = norm(circumcenter);\n\n        // Find the minimum distance to the circumcenter\n        double nearestDistSq = std::numeric_limits<double>::infinity();\n        double circumradSqConservative = (circumradius - lenScale * OUTSIDE_EPS);\n        circumradSqConservative *= circumradSqConservative;\n        for (size_t iTest = 0; iTest < nNeigh; iTest++) {\n          if (iTest == iStart || iTest == iEnd) continue; // skip the points forming the triangle\n          double thisDistSq = norm2(circumcenter - perturbPoints[iTest]);\n          nearestDistSq = std::fmin(nearestDistSq, thisDistSq);\n\n          // if it's already strictly inside, no need to keep searching\n          if (nearestDistSq < circumradSqConservative) break;\n        }\n        double nearestDist = std::sqrt(nearestDistSq);\n\n        // Accept the triangle if its circumcircle is sufficiently empty\n        // NOTE: The choice of signs in this expression is important: we preferential DO accept triangles whos\n        // circumcircle is barely empty. This makes sense here because our circular loop already avoids any risk of\n        // accepting overlapping triangles; the risk is in not accepting any, so we should preferrentially accept.\n        if (nearestDist + lenScale * OUTSIDE_EPS > circumradius) {\n          std::array<size_t, 3> triInds = {std::numeric_limits<size_t>::max(), iStart, iEnd};\n          thisPointTriangles.push_back(triInds);\n\n          // advance the circular search to find a triangle starting at this edge\n          edgeStartInd = edgeEndInd;\n          foundTri = true;\n          break;\n        }\n      }\n\n      // if we couldn't find any triangles, increment the start index\n      if (!foundTri) {\n        edgeStartInd++;\n      }\n    }\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "src/cpp/point_cloud_utilities.h",
    "content": "#pragma once\n\n#include \"geometrycentral/utilities/vector2.h\"\n#include \"geometrycentral/utilities/vector3.h\"\n\n#include \"geometrycentral/numerical/linear_algebra_utilities.h\"\n\nusing geometrycentral::SparseMatrix;\nusing geometrycentral::Vector;\nusing geometrycentral::Vector2;\nusing geometrycentral::Vector3;\n\n\n// === Basic utility methods\n\nusing Neighbors_t = std::vector<std::vector<size_t>>;\n\n// Generate the k-nearest-neighbors for the points.\n// The list will _not_ include the center point.\nNeighbors_t generate_knn(const std::vector<Vector3>& points, size_t k);\n\n// Estimate normals from a neighborhood (arbitrarily oriented)\nstd::vector<Vector3> generate_normals(const std::vector<Vector3>& points, const Neighbors_t& neigh);\n\n// Project a neighborhood to 2D tangent plane\n// The output is in correspondence with `neigh`, with the center point implicitly at (0,0)\nstd::vector<std::vector<Vector2>> generate_coords_projection(const std::vector<Vector3>& points,\n                                                             const std::vector<Vector3> normals,\n                                                             const Neighbors_t& neigh);\n\n\nstruct LocalTriangulationResult {\n  // all triangle indices are in to the neighbors list\n  std::vector<std::vector<std::array<size_t, 3>>> pointTriangles; // the triangles which touch the center vertex, always\n                                                                  // numbered such that the center vertex comes first\n};\n\nLocalTriangulationResult build_delaunay_triangulations(const std::vector<std::vector<Vector2>>& coords,\n                                                       const Neighbors_t& neigh);\n"
  },
  {
    "path": "src/robust_laplacian/__init__.py",
    "content": "from robust_laplacian.core import *\n"
  },
  {
    "path": "src/robust_laplacian/core.py",
    "content": "import numpy as np\n\nimport robust_laplacian_bindings as rlb\n\ndef mesh_laplacian(verts, faces, mollify_factor=1e-5):\n\n    ## Validate input\n    if type(verts) is not np.ndarray:\n        raise ValueError(\"`verts` should be a numpy array\")\n    if (len(verts.shape) != 2) or (verts.shape[1] != 3):\n        raise ValueError(\"`verts` should have shape (V,3), shape is \" + str(verts.shape))\n    \n    if type(faces) is not np.ndarray:\n        raise ValueError(\"`faces` should be a numpy array\")\n    if (len(faces.shape) != 2) or (faces.shape[1] != 3):\n        raise ValueError(\"`faces` should have shape (F,3), shape is \" + str(faces.shape))\n\n    ## Call the main algorithm from the bindings\n    L, M = rlb.buildMeshLaplacian(verts, faces, mollify_factor)\n\n    ## Return the result\n    return L, M\n\ndef point_cloud_laplacian(points, mollify_factor=1e-5, n_neighbors=30):\n\n    ## Validate input\n    if type(points) is not np.ndarray:\n        raise ValueError(\"`points` should be a numpy array\")\n    if (len(points.shape) != 2) or (points.shape[1] != 3):\n        raise ValueError(\"`points` should have shape (V,3), shape is \" + str(points.shape))\n    \n    ## Call the main algorithm from the bindings\n    L, M = rlb.buildPointCloudLaplacian(points, mollify_factor, n_neighbors)\n\n    ## Return the result\n    return L, M\n"
  },
  {
    "path": "test/robust_laplacian_test.py",
    "content": "import unittest\nimport os\nimport sys\nimport os.path as path\nimport numpy as np\nimport scipy\n\n# Path to where the bindings live\n\nsys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\", \"src\")))\nif os.name == 'nt': # if Windows\n    # handle default location where VS puts binary\n    sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\", \"build\", \"Debug\")))\nelse:\n    # normal / unix case\n    sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\", \"build\")))\n\n\nimport robust_laplacian as rl\n\n\ndef generate_verts(n_pts=999):\n    np.random.seed(777)        \n    return np.random.rand(n_pts, 3)\n\ndef generate_faces(n_pts=999):\n    # n_pts should be a multiple of 3 for indexing to work out\n    np.random.seed(777)        \n    rand_faces = np.random.randint(0, n_pts, size=(2*n_pts,3))\n    coverage_faces = np.arange(n_pts).reshape(-1, 3)\n    faces = np.vstack((rand_faces, coverage_faces))\n    return faces\n\ndef is_symmetric(A, eps=1e-6):\n    resid = A - A.T\n    return np.all(np.abs(resid.data) < eps)\n\ndef is_nonnegative(A, eps=1e-6):\n    return np.all(A.data > -eps)\n\nclass TestCore(unittest.TestCase):\n\n    def test_mesh_laplacian(self):\n\n        V = generate_verts()\n        F = generate_faces()\n\n        L, M = rl.mesh_laplacian(V, F)\n\n        # Validate mass matrix    \n        self.assertTrue(is_nonnegative(M))\n        self.assertTrue(is_symmetric(M))\n        self.assertEqual(M.sum(), M.diagonal().sum())\n        \n        # Validate Laplacian \n        self.assertTrue(is_symmetric(L))\n        off_L = scipy.sparse.diags(L.diagonal()) - L\n        self.assertTrue(is_nonnegative(off_L)) # positive edge weights\n        self.assertGreater(L.sum(), -1e-5)\n\n\n        # Trigger validation errors\n        # rl.mesh_laplacian(\"cat\", F)  \n        # rl.mesh_laplacian(V, \"cat\")  \n        # rl.mesh_laplacian(V.flatten(), F)  \n        # rl.mesh_laplacian(V, F.flatten())  \n    \n    def test_mesh_laplacian_unused_verts(self):\n\n        V = generate_verts()\n        F = generate_faces()\n        \n        # add an unused vertex at the beginning and end\n        V = np.vstack((\n                    np.array([[0., 0., 0.,]]),\n                    V, \n                    np.array([[0., 0., 0.,]])\n                )) \n        F = F + 1\n\n        L, M = rl.mesh_laplacian(V, F)\n\n        # Validate mass matrix    \n        self.assertTrue(is_nonnegative(M))\n        self.assertTrue(is_symmetric(M))\n        self.assertEqual(M.sum(), M.diagonal().sum())\n        \n        # Validate Laplacian \n        self.assertTrue(is_symmetric(L))\n        off_L = scipy.sparse.diags(L.diagonal()) - L\n        self.assertTrue(is_nonnegative(off_L)) # positive edge weights\n        self.assertGreater(L.sum(), -1e-5)\n\n   \n    def test_point_cloud_laplacian(self):\n\n        V = generate_verts()\n\n        L, M = rl.point_cloud_laplacian(V)\n\n        # Validate mass matrix    \n        self.assertTrue(is_nonnegative(M))\n        self.assertTrue(is_symmetric(M))\n        self.assertEqual(M.sum(), M.diagonal().sum())\n        \n        # Validate Laplacian \n        self.assertTrue(is_symmetric(L))\n        off_L = scipy.sparse.diags(L.diagonal()) - L\n        self.assertTrue(is_nonnegative(off_L)) # positive edge weights\n        self.assertGreater(L.sum(), -1e-5)\n\n        # Trigger validation errors\n        # rl.point_cloud_laplacian(\"cat\")  \n        # rl.point_cloud_laplacian(V.flatten())  \n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "test/sample.py",
    "content": "import os, sys\n\nimport polyscope as ps\nimport numpy as np\nimport scipy.sparse.linalg as sla\nfrom plyfile import PlyData\n\n# Path to where the bindings live\nsys.path.append(os.path.join(os.path.dirname(__file__), \"../build/\"))\nsys.path.append(os.path.join(os.path.dirname(__file__), \"../src/\"))\n\nimport robust_laplacian\n\n# Read input\nplydata = PlyData.read(\"/path/to/cloud.ply\")\npoints = np.vstack((\n    plydata['vertex']['x'],\n    plydata['vertex']['y'],\n    plydata['vertex']['z']\n)).T\n\n# for meshes\n# tri_data = plydata['face'].data['vertex_indices']\n# faces = np.vstack(tri_data)\n\n# Build Laplacian\nL, M = robust_laplacian.point_cloud_laplacian(points, mollify_factor=1e-5)\n\n# for meshes\n# L, M = robust_laplacian.mesh_laplacian(points, faces, mollify_factor=1e-5)\n\n# Compute some eigenvectors\nn_eig = 10\nevals, evecs = sla.eigsh(L, n_eig, M, sigma=1e-8)\n\n# Visualize\nps.init()\nps_cloud = ps.register_point_cloud(\"my cloud\", points)\nfor i in range(n_eig):\n    ps_cloud.add_scalar_quantity(\"eigenvector_\"+str(i), evecs[:,i], enabled=True)\n\n# for meshes\n# ps_surf = ps.register_surface_mesh(\"my surf\", points, faces)\n# for i in range(n_eig):\n    # ps_surf.add_scalar_quantity(\"eigenvector_\"+str(i), evecs[:,i], enabled=True)\n\nps.show()\n"
  }
]