Fast/robust point-in-mesh test: c_pointsinside(method=) - #6
Merged
Merged
Conversation
Robust generalised (solid-angle) winding number implemented in C++ and parallelised over query points with RcppThread. Thresholding abs(w) > 0.5 classifies points as inside a closed triangle mesh. Unlike a closest-point signed-distance test (e.g. Rvcg::vcgClostKD) it does not depend on surface normals and has no ray-casting tie-breaking, so it does not produce the spurious "outside point classified as inside" results that normal-based tests give near thin protrusions or sharp features. Intended as the accelerated back end for nat::pointsinside(). The exported c_pointsinside() returns a logical vector; the underlying c_mesh_winding_number() (raw winding numbers) is kept internal, useful for correctness checks. Default threads = 4 matches the rest of the package.
Analytic tetrahedron (interior w ~ +/-1, exterior ~ 0), orientation independence, thread-count invariance and input validation, plus a real visual-CA1 mesh (bundled in testdata) where the four points a normal-based test wrongly called inside are confirmed outside and 2000 bbox-sampled points match an independent oracle (CGAL Side_of_triangle_mesh) baked into the rds. All parallel calls capped at threads = 2 to respect CRAN's check-farm limit; suite passes with _R_CHECK_LIMIT_CORES_=TRUE.
Add the transitive include closure of igl::fast_winding_number (10 files, MPL-2.0) under src/vendor/igl, plus LICENSE.MPL2 and a provenance README (libigl commit 7100764). No CGAL/Boost/GMP/MPFR/TBB is compiled; the only new dependency is Eigen, added via RcppEigen in LinkingTo. libigl contributors credited as a copyright holder in Authors@R.
c_fast_mesh_winding_number() builds a bounding-volume hierarchy once over the mesh (Barill et al. 2018) and evaluates each query point in O(log F), scaling to millions of points on large meshes. Query points are distributed with RcppThread (natcpp's threads convention) rather than libigl's own std::thread pool, so the core count stays controllable for CRAN's check-farm limit. Each worker calls the underlying UT_SolidAngle::computeSolidAngle primitive directly -- the same call libigl's batch overload makes internally. Do NOT loop libigl's templated fast_winding_number(bvh, accuracy, p) wrapper per point instead: that convenience overload is only explicitly instantiated for dynamic float matrices and carries heavy per-call overhead, making it ~450x slower than the primitive (1.82M points on the CA1 mesh: 89s vs 1.6s single-threaded). We also avoid libigl's batch API, whose internal parallel_for reads a process-wide thread-count singleton that cannot honour a per-call `threads` argument. Kept internal (unexported) alongside the internal c_fast_pointsinside() R wrapper: this accelerated path is intended as the back end for nat::pointsinside, while the brute-force winding number remains as an O(P*F) correctness reference.
Fast path agrees with the brute-force winding number on the analytic tetrahedron, calls the four known CA1 false positives outside, and matches the independent CGAL oracle on the 2000-point bbox sample. Threads capped at 2.
FastWindingNumberForSoups.h is an upstream Side Effects HDK amalgamation (vendored via libigl). Under GCC/Clang it emits -Wpedantic (anonymous structs) and -Wclass-memaccess warnings that CRAN's win-builder r-devel flags as significant, tripping an install-time "R CMD check" WARNING. Add a single `#pragma GCC system_header` at the top so both compilers treat its diagnostics as toolchain-header diagnostics and stay silent, without altering any semantics and without non-portable -Wno-* flags in Makevars. The file is otherwise byte-identical upstream.
Collapse the separate brute-force export and internal libigl wrapper into
one exported c_pointsinside() with method = c("auto", "bvh", "bruteforce").
"auto" builds the libigl BVH only for large meshes (>=1000 faces with a
non-trivial query workload) and uses the brute-force O(P*F) test otherwise,
so a simple mesh such as a cuboid always takes the cheaper path; the choice
is asymmetric-safe since a mistaken "bvh" only costs the bounded octree
build while a mistaken "bruteforce" on a large mesh costs seconds.
Adopt the package threads=NULL policy (natcpp_threads()) in place of the
hard-coded threads=4L, and drop the redundant internal c_fast_pointsinside()
wrapper. The two C++ back ends (c_mesh_winding_number,
c_fast_mesh_winding_number) remain internal .Call targets behind the wrapper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
c_pointsinside(), a robust point-in-mesh classifier based on the generalised (solid-angle) winding number — normal-free, so it avoids the spurious "outside classified as inside" results normal-based tests give near thin protrusions.API
A single exported function with two back ends behind it:
"bruteforce"— self-contained O(P*F) winding number, parallelised over points with RcppThread. No setup cost."bvh"— libigl "Fast Winding Numbers for Soups and Clouds" (Barill et al. 2018); builds a bounding-volume hierarchy once, then each query is O(log F). libigl (MPL-2.0) vendored undersrc/vendor/igl, requiresRcppEigen. The bundled Houdini HDK amalgamation is marked a system header (one-line#pragma) so its third-party warnings don't surface as install-timeR CMD checkWARNINGs (confirmed clean on win-builder r-devel + rhub)."auto"(default) — picks bvh once a mesh is non-trivial (>=1000 faces with a real query workload), else brute force; a cuboid always takes the cheap path.Auto threshold
Tuned from a benchmark rather than guessed. The pick is asymmetric-safe (a wrong bvh choice only costs the bounded octree build; a wrong brute-force on a large mesh costs seconds):
Other
threads = NULLpolicy (natcpp_threads()), not a hard-coded 4..Calltargets behind the R wrapper.🤖 Generated with Claude Code