From b71a922169a052a2060c61c42e2e30e0efa83de3 Mon Sep 17 00:00:00 2001 From: ZigaSajovic Date: Sat, 28 Feb 2026 01:37:28 +0100 Subject: [PATCH 1/3] TypeScript SDK: full module coverage, documentation, and updated live examples - Complete TypeScript bindings: clean, geometry, topology, reindex, remesh, spatial, cut, intersect, io - PointCloud form type, IndexMap, NDArray improvements (isNaN, multi-axis take, arange) - Full TypeScript documentation site with getting-started, module reference, examples - Live examples ported to pure TypeScript SDK - Moved wasm-examples into docs/ - C++ fixes: boolean classification for near-coplanar faces, signed distance query, split_earcut fix, on-vertex scalar field intersections Closes #7 Closes #15 --- .github/workflows/generate-docs.yml | 23 +- .gitignore | 2 + CMakeLists.txt | 2 +- README.md | 18 +- docs/app/components/LibPicker.vue | 12 +- docs/app/components/content/TsFlowDiagram.vue | 310 ++ .../app/components/content/TsQueryDiagram.vue | 304 ++ docs/app/composables/useLibraryCollection.ts | 6 +- docs/app/composables/useTrueform.ts | 19 + docs/app/examples/AlignmentExample.ts | 690 +++- docs/app/examples/BooleanExample.ts | 741 +++- docs/app/examples/ClosestPointsExample.ts | 492 +++ docs/app/examples/CollisionExample.ts | 307 +- docs/app/examples/CrossSectionExample.ts | 418 +- docs/app/examples/IsobandsExample.ts | 431 +- docs/app/examples/PositioningExample.ts | 299 -- docs/app/pages/[...slug].vue | 2 +- docs/app/pages/live-examples/alignment.vue | 59 +- docs/app/pages/live-examples/boolean.vue | 79 +- .../pages/live-examples/closest-points.vue | 63 +- docs/app/pages/live-examples/collision.vue | 87 +- .../app/pages/live-examples/cross-section.vue | 56 +- docs/app/pages/live-examples/slicing.vue | 56 +- docs/app/utils/utils.ts | 136 + docs/content.config.ts | 14 + .../cpp/2.modules/{0.index.md => 00.index.md} | 0 .../cpp/2.modules/{1.core.md => 01.core.md} | 0 .../2.modules/{2.spatial.md => 02.spatial.md} | 0 .../{3.topology.md => 03.topology.md} | 0 .../{4.geometry.md => 04.geometry.md} | 0 .../2.modules/{5.remesh.md => 05.remesh.md} | 0 .../{6.intersect.md => 06.intersect.md} | 0 .../cpp/2.modules/{7.cut.md => 07.cut.md} | 0 .../cpp/2.modules/{8.clean.md => 08.clean.md} | 0 .../cpp/2.modules/{9.reidx.md => 09.reidx.md} | 0 docs/content/index.md | 49 +- .../ts/1.getting-started/.navigation.yml | 2 + docs/content/ts/1.getting-started/1.index.md | 204 + .../ts/1.getting-started/2.installation.md | 37 + .../ts/1.getting-started/3.live-examples.md | 22 + docs/content/ts/2.modules/.navigation.yml | 2 + docs/content/ts/2.modules/00.index.md | 116 + docs/content/ts/2.modules/01.core.md | 758 ++++ docs/content/ts/2.modules/02.spatial.md | 271 ++ docs/content/ts/2.modules/03.topology.md | 202 + docs/content/ts/2.modules/04.geometry.md | 340 ++ docs/content/ts/2.modules/05.remesh.md | 127 + docs/content/ts/2.modules/06.intersect.md | 123 + docs/content/ts/2.modules/07.cut.md | 186 + docs/content/ts/2.modules/08.clean.md | 138 + docs/content/ts/2.modules/09.reidx.md | 182 + docs/content/ts/2.modules/10.io.md | 118 + docs/content/ts/3.benchmarks/.navigation.yml | 2 + docs/content/ts/3.benchmarks/1.index.md | 92 + docs/content/ts/4.examples/.navigation.yml | 2 + docs/content/ts/4.examples/0.index.md | 55 + .../ts/4.examples/2.core-functionality.md | 228 ++ docs/content/ts/4.examples/3.alignment.md | 181 + .../content/ts/4.examples/4.raycast-render.md | 162 + docs/content/ts/5.about/.navigation.yml | 2 + docs/content/ts/5.about/1.research.md | 50 + docs/content/ts/5.about/2.contributing.md | 212 + docs/content/ts/5.about/3.license.md | 17 + docs/modules/copy-files.ts | 13 +- docs/nuxt.config.ts | 13 +- docs/package.json | 22 +- docs/pnpm-lock.yaml | 3463 ++++++++--------- .../wasm-examples}/.gitignore | 0 .../wasm-examples}/CMakeLists.txt | 2 +- .../wasm-examples}/Makefile | 0 .../wasm-examples}/README.md | 0 .../cmake/EmscriptenUtils.cmake | 0 .../wasm-examples}/extras/Dockerfile | 0 .../wasm-examples}/extras/README.md | 0 .../wasm-examples}/extras/build_web.sh | 15 + .../wasm-examples}/extras/docker-compose.yml | 6 +- .../wasm-examples}/package.json | 0 .../src/laplacian_smoothing_web.h | 0 .../wasm-examples}/src/main.cpp | 147 +- .../wasm-examples}/src/main.h | 0 .../wasm-examples}/src/shape_histogram_web.h | 0 .../wasm-examples}/src/utils/bridge_web.h | 0 .../src/utils/cursor_interactor_interface.cpp | 0 .../src/utils/cursor_interactor_interface.h | 0 .../wasm-examples}/src/utils/utils.h | 0 include/trueform/clean/soup/polygons.hpp | 165 + include/trueform/core.hpp | 1 + include/trueform/core/form.hpp | 1 - include/trueform/core/is_form.hpp | 33 + include/trueform/core/points.hpp | 1 + include/trueform/core/policy/normals.hpp | 34 +- include/trueform/core/ray_cast.hpp | 7 + include/trueform/core/ray_triangle_check.hpp | 74 + include/trueform/cut/boolean_config.hpp | 27 + .../cut/classify/classify_on_shared_edge.hpp | 56 +- include/trueform/cut/classify/tagged.hpp | 134 +- .../trueform/cut/debug/verify_cut_faces.hpp | 112 + include/trueform/cut/impl/make_boolean.hpp | 14 +- .../trueform/cut/impl/make_boolean_pair.hpp | 4 +- .../cut/impl/make_mesh_arrangements.hpp | 6 +- include/trueform/cut/impl/scalars.hpp | 42 +- include/trueform/cut/loop/loop_extractor.hpp | 2 +- include/trueform/cut/make_boolean.hpp | 29 +- include/trueform/cut/make_boolean_pair.hpp | 15 +- .../trueform/cut/make_mesh_arrangements.hpp | 8 +- .../trueform/geometry/fit_obb_alignment.hpp | 3 +- include/trueform/geometry/impl/ear_cutter.hpp | 30 +- .../intersect/scalar_field_intersections.hpp | 62 +- include/trueform/io/write_obj.hpp | 77 +- include/trueform/io/write_stl.hpp | 295 +- include/trueform/reindex.hpp | 1 + include/trueform/reindex/make_dynamic.hpp | 139 + include/trueform/spatial/gather_ids.hpp | 7 +- include/trueform/spatial/signed_distance.hpp | 155 + .../topology/compute_unique_faces_mask.hpp | 13 +- tests/clean/test_clean_polygons.cpp | 296 ++ tests/common/canonicalize_mesh.hpp | 32 + tests/intersect/test_isocontours.cpp | 272 ++ typescript/CMakeLists.txt | 20 +- typescript/README.md | 136 + typescript/build.mjs | 30 +- .../trueform/ts/clean/result_types.hpp | 33 + .../include/trueform/ts/core/elementwise.hpp | 37 + .../trueform/ts/core/to_wasm_matrix.hpp | 46 + .../include/trueform/ts/core/wasm_curves.hpp | 10 + .../trueform/ts/core/wasm_index_map.hpp | 38 + .../include/trueform/ts/core/wasm_mesh.hpp | 68 + .../trueform/ts/core/wasm_point_cloud.hpp | 170 + .../trueform/ts/reindex/result_types.hpp | 28 + .../trueform/ts/spatial/result_types.hpp | 13 + .../trueform/ts/topology/result_types.hpp | 26 + typescript/cpp/src/clean/clean.cpp | 186 + typescript/cpp/src/core/elementwise.cpp | 104 + typescript/cpp/src/core/wasm_curves.cpp | 1 + typescript/cpp/src/core/wasm_index_map.cpp | 21 + typescript/cpp/src/core/wasm_mesh.cpp | 66 + typescript/cpp/src/core/wasm_ndarray.cpp | 108 + typescript/cpp/src/core/wasm_point_cloud.cpp | 51 + typescript/cpp/src/cut/boolean.cpp | 18 +- typescript/cpp/src/cut/embedded.cpp | 27 +- typescript/cpp/src/cut/isobands.cpp | 83 + typescript/cpp/src/geometry/curvature.cpp | 137 + typescript/cpp/src/geometry/measurements.cpp | 113 + .../cpp/src/geometry/mesh_primitives.cpp | 116 + typescript/cpp/src/geometry/orientation.cpp | 63 + typescript/cpp/src/geometry/registration.cpp | 298 ++ typescript/cpp/src/geometry/smoothing.cpp | 80 + typescript/cpp/src/geometry/triangulate.cpp | 140 + typescript/cpp/src/intersect/intersect.cpp | 14 +- typescript/cpp/src/io/io.cpp | 55 + typescript/cpp/src/reindex/reindex.cpp | 386 ++ typescript/cpp/src/remesh/remesh.cpp | 124 + typescript/cpp/src/spatial/distance.cpp | 418 +- typescript/cpp/src/spatial/intersects.cpp | 136 +- .../cpp/src/spatial/neighbor_search.cpp | 340 +- typescript/cpp/src/spatial/ray_cast.cpp | 199 +- typescript/cpp/src/topology/topology.cpp | 321 ++ typescript/examples/alignment.mjs | 316 ++ typescript/examples/core.mjs | 223 ++ typescript/examples/raycast_render.mjs | 164 + typescript/package.json | 42 +- typescript/src/async/index.ts | 27 +- typescript/src/clean/async.ts | 97 + typescript/src/clean/sync.ts | 88 + typescript/src/core/IndexMap.ts | 44 + typescript/src/cut/async.ts | 18 + typescript/src/cut/sync.ts | 52 +- typescript/src/form/Mesh.ts | 54 + typescript/src/form/MeshLike.ts | 23 + typescript/src/form/PointCloud.ts | 163 + typescript/src/form/factories.ts | 16 + typescript/src/geometry/async.ts | 244 ++ typescript/src/geometry/sync.ts | 218 ++ typescript/src/index.ts | 56 +- typescript/src/io/async.ts | 17 + typescript/src/io/sync.ts | 11 + typescript/src/ndarray/NDArray.ts | 175 +- typescript/src/ndarray/OffsetBlockedBuffer.ts | 32 +- typescript/src/ndarray/factories.ts | 25 +- typescript/src/ndarray/math.ts | 70 +- typescript/src/ndarray/reductions.ts | 9 + typescript/src/primitive/Primitive.ts | 10 + typescript/src/primitive/factories.ts | 16 +- typescript/src/primitive/index.ts | 2 +- typescript/src/primitive/transformations.ts | 57 +- typescript/src/reindex/async.ts | 239 ++ typescript/src/reindex/sync.ts | 208 + typescript/src/remesh/async.ts | 50 + typescript/src/remesh/sync.ts | 72 + typescript/src/spatial/async.ts | 209 +- typescript/src/spatial/index.ts | 2 +- typescript/src/spatial/sync.ts | 249 +- typescript/src/topology/async.ts | 128 + typescript/src/topology/sync.ts | 122 + typescript/tests/runner.mjs | 12 + typescript/tests/test_clean.mjs | 131 + typescript/tests/test_cut.mjs | 210 + typescript/tests/test_geometry.mjs | 507 +++ typescript/tests/test_intersect.mjs | 66 + typescript/tests/test_io_roundtrip.mjs | 64 + typescript/tests/test_mesh.mjs | 2 +- typescript/tests/test_mesh_setters.mjs | 167 + typescript/tests/test_ndarray.mjs | 513 ++- typescript/tests/test_point_cloud.mjs | 167 + typescript/tests/test_primitives.mjs | 317 ++ typescript/tests/test_registration.mjs | 650 ++++ typescript/tests/test_reindex.mjs | 336 ++ typescript/tests/test_remesh.mjs | 184 + typescript/tests/test_spatial.mjs | 342 ++ typescript/tests/test_topology.mjs | 286 ++ typescript/tests/test_transformations.mjs | 121 + vtk/examples/isobands.cpp | 8 + wasm-examples/src/alignment_web.h | 239 -- wasm-examples/src/boolean_web.h | 197 - wasm-examples/src/collision_web.h | 196 - wasm-examples/src/cross_section_web.h | 143 - wasm-examples/src/isobands_web.h | 143 - wasm-examples/src/positioning_web.h | 355 -- 218 files changed, 22267 insertions(+), 5231 deletions(-) create mode 100644 docs/app/components/content/TsFlowDiagram.vue create mode 100644 docs/app/components/content/TsQueryDiagram.vue create mode 100644 docs/app/composables/useTrueform.ts create mode 100644 docs/app/examples/ClosestPointsExample.ts delete mode 100644 docs/app/examples/PositioningExample.ts rename docs/content/cpp/2.modules/{0.index.md => 00.index.md} (100%) rename docs/content/cpp/2.modules/{1.core.md => 01.core.md} (100%) rename docs/content/cpp/2.modules/{2.spatial.md => 02.spatial.md} (100%) rename docs/content/cpp/2.modules/{3.topology.md => 03.topology.md} (100%) rename docs/content/cpp/2.modules/{4.geometry.md => 04.geometry.md} (100%) rename docs/content/cpp/2.modules/{5.remesh.md => 05.remesh.md} (100%) rename docs/content/cpp/2.modules/{6.intersect.md => 06.intersect.md} (100%) rename docs/content/cpp/2.modules/{7.cut.md => 07.cut.md} (100%) rename docs/content/cpp/2.modules/{8.clean.md => 08.clean.md} (100%) rename docs/content/cpp/2.modules/{9.reidx.md => 09.reidx.md} (100%) create mode 100644 docs/content/ts/1.getting-started/.navigation.yml create mode 100644 docs/content/ts/1.getting-started/1.index.md create mode 100644 docs/content/ts/1.getting-started/2.installation.md create mode 100644 docs/content/ts/1.getting-started/3.live-examples.md create mode 100644 docs/content/ts/2.modules/.navigation.yml create mode 100644 docs/content/ts/2.modules/00.index.md create mode 100644 docs/content/ts/2.modules/01.core.md create mode 100644 docs/content/ts/2.modules/02.spatial.md create mode 100644 docs/content/ts/2.modules/03.topology.md create mode 100644 docs/content/ts/2.modules/04.geometry.md create mode 100644 docs/content/ts/2.modules/05.remesh.md create mode 100644 docs/content/ts/2.modules/06.intersect.md create mode 100644 docs/content/ts/2.modules/07.cut.md create mode 100644 docs/content/ts/2.modules/08.clean.md create mode 100644 docs/content/ts/2.modules/09.reidx.md create mode 100644 docs/content/ts/2.modules/10.io.md create mode 100644 docs/content/ts/3.benchmarks/.navigation.yml create mode 100644 docs/content/ts/3.benchmarks/1.index.md create mode 100644 docs/content/ts/4.examples/.navigation.yml create mode 100644 docs/content/ts/4.examples/0.index.md create mode 100644 docs/content/ts/4.examples/2.core-functionality.md create mode 100644 docs/content/ts/4.examples/3.alignment.md create mode 100644 docs/content/ts/4.examples/4.raycast-render.md create mode 100644 docs/content/ts/5.about/.navigation.yml create mode 100644 docs/content/ts/5.about/1.research.md create mode 100644 docs/content/ts/5.about/2.contributing.md create mode 100644 docs/content/ts/5.about/3.license.md rename {wasm-examples => docs/wasm-examples}/.gitignore (100%) rename {wasm-examples => docs/wasm-examples}/CMakeLists.txt (97%) rename {wasm-examples => docs/wasm-examples}/Makefile (100%) rename {wasm-examples => docs/wasm-examples}/README.md (100%) rename {wasm-examples => docs/wasm-examples}/cmake/EmscriptenUtils.cmake (100%) rename {wasm-examples => docs/wasm-examples}/extras/Dockerfile (100%) rename {wasm-examples => docs/wasm-examples}/extras/README.md (100%) rename {wasm-examples => docs/wasm-examples}/extras/build_web.sh (77%) rename {wasm-examples => docs/wasm-examples}/extras/docker-compose.yml (51%) rename {wasm-examples => docs/wasm-examples}/package.json (100%) rename {wasm-examples => docs/wasm-examples}/src/laplacian_smoothing_web.h (100%) rename {wasm-examples => docs/wasm-examples}/src/main.cpp (65%) rename {wasm-examples => docs/wasm-examples}/src/main.h (100%) rename {wasm-examples => docs/wasm-examples}/src/shape_histogram_web.h (100%) rename {wasm-examples => docs/wasm-examples}/src/utils/bridge_web.h (100%) rename {wasm-examples => docs/wasm-examples}/src/utils/cursor_interactor_interface.cpp (100%) rename {wasm-examples => docs/wasm-examples}/src/utils/cursor_interactor_interface.h (100%) rename {wasm-examples => docs/wasm-examples}/src/utils/utils.h (100%) create mode 100644 include/trueform/core/is_form.hpp create mode 100644 include/trueform/core/ray_triangle_check.hpp create mode 100644 include/trueform/cut/boolean_config.hpp create mode 100644 include/trueform/cut/debug/verify_cut_faces.hpp create mode 100644 include/trueform/reindex/make_dynamic.hpp create mode 100644 include/trueform/spatial/signed_distance.hpp create mode 100644 typescript/README.md create mode 100644 typescript/cpp/include/trueform/ts/clean/result_types.hpp create mode 100644 typescript/cpp/include/trueform/ts/core/to_wasm_matrix.hpp create mode 100644 typescript/cpp/include/trueform/ts/core/wasm_index_map.hpp create mode 100644 typescript/cpp/include/trueform/ts/core/wasm_point_cloud.hpp create mode 100644 typescript/cpp/include/trueform/ts/reindex/result_types.hpp create mode 100644 typescript/cpp/include/trueform/ts/topology/result_types.hpp create mode 100644 typescript/cpp/src/clean/clean.cpp create mode 100644 typescript/cpp/src/core/wasm_index_map.cpp create mode 100644 typescript/cpp/src/core/wasm_point_cloud.cpp create mode 100644 typescript/cpp/src/geometry/curvature.cpp create mode 100644 typescript/cpp/src/geometry/measurements.cpp create mode 100644 typescript/cpp/src/geometry/mesh_primitives.cpp create mode 100644 typescript/cpp/src/geometry/orientation.cpp create mode 100644 typescript/cpp/src/geometry/registration.cpp create mode 100644 typescript/cpp/src/geometry/smoothing.cpp create mode 100644 typescript/cpp/src/geometry/triangulate.cpp create mode 100644 typescript/cpp/src/reindex/reindex.cpp create mode 100644 typescript/cpp/src/remesh/remesh.cpp create mode 100644 typescript/cpp/src/topology/topology.cpp create mode 100644 typescript/examples/alignment.mjs create mode 100644 typescript/examples/core.mjs create mode 100644 typescript/examples/raycast_render.mjs create mode 100644 typescript/src/clean/async.ts create mode 100644 typescript/src/clean/sync.ts create mode 100644 typescript/src/core/IndexMap.ts create mode 100644 typescript/src/form/MeshLike.ts create mode 100644 typescript/src/form/PointCloud.ts create mode 100644 typescript/src/geometry/async.ts create mode 100644 typescript/src/geometry/sync.ts create mode 100644 typescript/src/reindex/async.ts create mode 100644 typescript/src/reindex/sync.ts create mode 100644 typescript/src/remesh/async.ts create mode 100644 typescript/src/remesh/sync.ts create mode 100644 typescript/src/topology/async.ts create mode 100644 typescript/src/topology/sync.ts create mode 100644 typescript/tests/test_clean.mjs create mode 100644 typescript/tests/test_cut.mjs create mode 100644 typescript/tests/test_geometry.mjs create mode 100644 typescript/tests/test_intersect.mjs create mode 100644 typescript/tests/test_io_roundtrip.mjs create mode 100644 typescript/tests/test_mesh_setters.mjs create mode 100644 typescript/tests/test_point_cloud.mjs create mode 100644 typescript/tests/test_registration.mjs create mode 100644 typescript/tests/test_reindex.mjs create mode 100644 typescript/tests/test_remesh.mjs create mode 100644 typescript/tests/test_topology.mjs create mode 100644 typescript/tests/test_transformations.mjs delete mode 100644 wasm-examples/src/alignment_web.h delete mode 100644 wasm-examples/src/boolean_web.h delete mode 100644 wasm-examples/src/collision_web.h delete mode 100644 wasm-examples/src/cross_section_web.h delete mode 100644 wasm-examples/src/isobands_web.h delete mode 100644 wasm-examples/src/positioning_web.h diff --git a/.github/workflows/generate-docs.yml b/.github/workflows/generate-docs.yml index 47bd3e2..b02ec0f 100644 --- a/.github/workflows/generate-docs.yml +++ b/.github/workflows/generate-docs.yml @@ -15,20 +15,27 @@ jobs: - name: Build WebAssembly assets run: | - docker compose -f wasm-examples/extras/docker-compose.yml \ + docker compose -f docs/wasm-examples/extras/docker-compose.yml \ up --build --abort-on-container-exit --exit-code-from web-example - name: Tear down docker compose if: always() run: | - docker compose -f wasm-examples/extras/docker-compose.yml down \ + docker compose -f docs/wasm-examples/extras/docker-compose.yml down \ --volumes --remove-orphans - - name: Upload artifacts + - name: Upload WASM artifacts uses: actions/upload-artifact@v4 with: name: wasm-build - path: wasm-examples/build/dist + path: docs/wasm-examples/build/dist + retention-days: 7 + + - name: Upload TypeScript SDK artifacts + uses: actions/upload-artifact@v4 + with: + name: ts-sdk-build + path: typescript/dist retention-days: 7 build: @@ -43,7 +50,13 @@ jobs: uses: actions/download-artifact@v4 with: name: wasm-build - path: wasm-examples/build/dist + path: docs/wasm-examples/build/dist + + - name: Download TypeScript SDK artifacts + uses: actions/download-artifact@v4 + with: + name: ts-sdk-build + path: typescript/dist - name: Install pnpm uses: pnpm/action-setup@v4 diff --git a/.gitignore b/.gitignore index 2b6f26e..9546639 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ __pycache__/ # Generated include/trueform/version.hpp +typescript/LICENSE +typescript/LICENSE.noncommercial diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a3cb06..65667b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ endif() # ============================================================================== # Project Configuration # ============================================================================== -project(trueform VERSION 0.6.0 LANGUAGES CXX) +project(trueform VERSION 0.7.0 LANGUAGES CXX) configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.hpp.in diff --git a/README.md b/README.md index a556042..d3b1fe1 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ [![Docs](https://github.com/polydera/trueform/actions/workflows/generate-docs.yml/badge.svg)](https://github.com/polydera/trueform/actions/workflows/generate-docs.yml) [![Build](https://github.com/polydera/trueform/actions/workflows/build-python.yml/badge.svg)](https://github.com/polydera/trueform/actions/workflows/build-python.yml) [![PyPI](https://img.shields.io/pypi/v/trueform)](https://pypi.org/project/trueform/) +[![npm](https://img.shields.io/npm/v/@polydera/trueform)](https://www.npmjs.com/package/@polydera/trueform) Real-time geometric processing. Easy to use, robust on real-world data. -Mesh booleans, registration, remeshing and queries — at interactive speed on million-polygon meshes. Robust to non-manifold flaps, inconsistent winding, and pipeline artifacts. Header-only C++17; works directly on your data with zero-copy views. +Mesh booleans, registration, remeshing and queries — at interactive speed on million-polygon meshes. Robust to non-manifold flaps, inconsistent winding, and pipeline artifacts. One engine across C++, Python, and TypeScript. **[▶ Try it live](https://trueform.polydera.com/live-examples/boolean)** — Interactive mesh booleans, collisions, isobands and more. No install needed. @@ -40,10 +41,22 @@ cmake -B build -Dtrueform_ROOT=$(python -m trueform.cmake) For manual installation without pip (FetchContent, system install, conan from repo), see the [full installation guide](https://trueform.polydera.com/cpp/getting-started/installation). +**Python** — the same pip package includes Python bindings: +```python +import trueform as tf +mesh = tf.read_stl("model.stl") +``` + +**TypeScript** — browser and Node.js: +```bash +npm install @polydera/trueform +``` + ## Integrations -- **[VTK](https://trueform.polydera.com/cpp/vtk)** — Filters and functions that integrate with VTK pipelines - **[Python](https://trueform.polydera.com/py/getting-started)** — NumPy in, NumPy out +- **[TypeScript](https://trueform.polydera.com/ts/getting-started)** — NDArrays in, NDArrays out. Browser and Node.js. +- **[VTK](https://trueform.polydera.com/cpp/vtk)** — Filters and functions that integrate with VTK pipelines - **[Blender](https://trueform.polydera.com/py/blender)** — Cached meshes with automatic updates for live preview ## Quick Tour @@ -205,6 +218,7 @@ Apple M4 Max, 16 threads, Clang `-O3 -march=native`. Full methodology, interacti - [Benchmarks](https://trueform.polydera.com/cpp/benchmarks) — Performance comparisons - [Examples](https://trueform.polydera.com/cpp/examples) — Workflows and library comparisons - [Python Bindings](https://trueform.polydera.com/py/getting-started) — Full API for Python +- [TypeScript SDK](https://trueform.polydera.com/ts/getting-started) — WASM-powered, browser and Node.js - [Research](https://trueform.polydera.com/cpp/about/research) — Theory, publications, and citation ## License diff --git a/docs/app/components/LibPicker.vue b/docs/app/components/LibPicker.vue index 11f675f..2d9e67c 100644 --- a/docs/app/components/LibPicker.vue +++ b/docs/app/components/LibPicker.vue @@ -4,27 +4,29 @@ const route = useRoute(); const items = [ { - label: "C++", icon: "i-vscode-icons:file-type-cpp", value: "cpp", }, { - label: "Python", icon: "i-vscode-icons:file-type-python", value: "py", }, + { + icon: "i-vscode-icons:file-type-typescript", + value: "ts", + }, ]; const router = useRouter(); const handleChange = async (value: string | number) => { - const newLibrary = value as "cpp" | "py"; - const newCollection = newLibrary === "cpp" ? "docsCpp" : "docsPy"; + const newLibrary = value as "cpp" | "py" | "ts"; + const newCollection = newLibrary === "cpp" ? "docsCpp" : newLibrary === "py" ? "docsPy" : "docsTs"; // If we're on a library-specific path, try to find the equivalent page const currentPath = route.path; const pathParts = currentPath.split("/"); - if (pathParts[1] === "cpp" || pathParts[1] === "py") { + if (pathParts[1] === "cpp" || pathParts[1] === "py" || pathParts[1] === "ts") { // Replace the library prefix const newPath = `/${newLibrary}${currentPath.slice(pathParts[1].length + 1)}`; diff --git a/docs/app/components/content/TsFlowDiagram.vue b/docs/app/components/content/TsFlowDiagram.vue new file mode 100644 index 0000000..fc317c3 --- /dev/null +++ b/docs/app/components/content/TsFlowDiagram.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/docs/app/components/content/TsQueryDiagram.vue b/docs/app/components/content/TsQueryDiagram.vue new file mode 100644 index 0000000..82a5446 --- /dev/null +++ b/docs/app/components/content/TsQueryDiagram.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/docs/app/composables/useLibraryCollection.ts b/docs/app/composables/useLibraryCollection.ts index fd4997e..1a9179a 100644 --- a/docs/app/composables/useLibraryCollection.ts +++ b/docs/app/composables/useLibraryCollection.ts @@ -4,13 +4,13 @@ export const useLibraryCollection = () => { // Determine library from route path, fallback to storage const library = computed(() => { const pathLibrary = route.path.split("/")[1]; - if (pathLibrary === "cpp" || pathLibrary === "py") { - return pathLibrary as "cpp" | "py"; + if (pathLibrary === "cpp" || pathLibrary === "py" || pathLibrary === "ts") { + return pathLibrary as "cpp" | "py" | "ts"; } return "cpp"; }); - const collection = computed(() => (library.value === "cpp" ? "docsCpp" : "docsPy")); + const collection = computed(() => (library.value === "cpp" ? "docsCpp" : library.value === "py" ? "docsPy" : "docsTs")); // Fetch navigation data const { data: navigation } = useAsyncData( diff --git a/docs/app/composables/useTrueform.ts b/docs/app/composables/useTrueform.ts new file mode 100644 index 0000000..47396ea --- /dev/null +++ b/docs/app/composables/useTrueform.ts @@ -0,0 +1,19 @@ +let _tf: any = null; +let _promise: Promise | null = null; + +export function useTrueform() { + const load = async () => { + if (_tf) return _tf; + if (!_promise) { + _promise = import("@/examples/trueform/index.js"); + } + try { + _tf = await _promise; + return _tf; + } catch (error) { + _promise = null; + throw error; + } + }; + return { load }; +} diff --git a/docs/app/examples/AlignmentExample.ts b/docs/app/examples/AlignmentExample.ts index cc372af..71ecbbf 100644 --- a/docs/app/examples/AlignmentExample.ts +++ b/docs/app/examples/AlignmentExample.ts @@ -1,67 +1,292 @@ -import type { MainModule } from "@/examples/native"; -import { ThreejsBase } from "@/examples/ThreejsBase"; -import { fitCameraToAllMeshesFromZPlane } from "@/utils/sceneUtils"; +import { createScene, type SceneBundle } from "@/utils/sceneUtils"; +import { centerAndScale, pickMesh } from "@/utils/utils"; import * as THREE from "three"; +import { ArcballControls } from "three/addons/controls/ArcballControls.js"; + +type TF = typeof import("@/examples/trueform/index.js"); export type InteractionMode = "move" | "rotate"; -export class AlignmentExample extends ThreejsBase { - private alignmentTime = 0; +// ════════════════════════════════════════════════════════════════════════════ +// Matrix convention helpers: TF row-major ↔ Three.js column-major +// ════════════════════════════════════════════════════════════════════════════ + +function tfToThree(tfObj: any): THREE.Matrix4 { + const mat = tfObj.transformation; + if (!mat) return new THREE.Matrix4(); + const m = new THREE.Matrix4().fromArray(mat.data).transpose(); + mat.delete(); + return m; +} + +function threeToTf(tf: TF, m: THREE.Matrix4): any { + const rm = m.clone().transpose(); + const mat = tf.ndarray(new Float32Array(rm.toArray())).reshape([4, 4]); + return mat; // caller must set .transformation then .delete() +} + +export class AlignmentExample { + private tf: TF; + private smoothedTarget: any; + private source: any; + private targetPC: any; + private sourcePC: any; + private aabbDiagonal: number; + + private container: HTMLElement; + private renderer: THREE.WebGLRenderer; + private sceneBundle: SceneBundle; + + private targetThree: THREE.Mesh; + private sourceThree: THREE.Mesh; + private materials: THREE.MeshMatcapMaterial[] = []; + + private running = true; + private cleanups: (() => void)[] = []; + private interactionMode: InteractionMode = "move"; - // Rotation state + // Move state + private selectedId: number | null = null; + private dragging = false; + private movingPlane = new THREE.Plane(); + private lastPoint = new THREE.Vector3(); + + // Rotate state private isRotating = false; private lastMouseX = 0; private lastMouseY = 0; + private raycaster = new THREE.Raycaster(); + private ndc = new THREE.Vector2(); + constructor( - wasmInstance: MainModule, - paths: string[], + tf: TF, + sourceBuffer: ArrayBuffer, + sourceFilename: string, + targetBuffer: ArrayBuffer, container: HTMLElement, isDarkMode = true, ) { - // skipUpdate = true so we can position meshes before fitting camera - super(wasmInstance, paths, container, undefined, true, false, isDarkMode); - this.updateMeshes(); - this.positionMeshesForScreen(container); - this.setupOrthographicCamera(container); - - // Warmup alignment (run once, then reposition) - this.wasmInstance.alignment_run_align(); - this.positionMeshesForScreen(container); - this.updateMeshes(); + this.tf = tf; + this.container = container; + + // ── Load and prepare TARGET mesh ──────────────────────────────────── + const target = tf.readStl(targetBuffer); + centerAndScale(tf, target); + // Taubin smooth BEFORE creating point cloud (lambda=0.9, kpb=0.1) + this.smoothedTarget = tf.taubinSmoothed(target, 50, 0.9, 0.1); + target.delete(); + + // ── Create target PointCloud (from smoothed mesh) ────────────────── + const PointCloud = (tf as any).PointCloud; + this.targetPC = PointCloud.fromMesh(this.smoothedTarget); + this.targetPC.buildTree(); + + // ── Load and prepare SOURCE mesh ─────────────────────────────────── + const ext = sourceFilename.split(".").pop()?.toLowerCase(); + this.source = ext === "stl" ? tf.readStl(sourceBuffer) : tf.readObj(sourceBuffer); + centerAndScale(tf, this.source); + + // ── Create source PointCloud ─────────────────────────────────────── + this.sourcePC = PointCloud.fromMesh(this.source); + + // ── Compute AABB diagonal (from source, after centerAndScale) ────── + { + const pts = this.source.points; + const pMin = tf.min(pts, 0); + const pMax = tf.max(pts, 0); + const diff = pMax.sub(pMin); + this.aabbDiagonal = tf.norm(diff) as number; + pMin.delete(); pMax.delete(); diff.delete(); pts.delete(); + } + + // ── Set identity transformation on target ────────────────────────── + const identityMat = tf.ndarray(new Float32Array([ + 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, + ])).reshape([4, 4]); + this.smoothedTarget.transformation = identityMat; + this.targetPC.transformation = identityMat; + identityMat.delete(); + + // ── Renderer setup ───────────────────────────────────────────────── + const bgColor = isDarkMode ? 0x1e1e1e : 0xfafafa; + this.renderer = new THREE.WebGLRenderer({ antialias: true }); + this.renderer.outputColorSpace = THREE.SRGBColorSpace; + this.renderer.toneMapping = THREE.ACESFilmicToneMapping; + this.renderer.toneMappingExposure = 1.0; + container.appendChild(this.renderer.domElement); + + // ── Scene + camera ───────────────────────────────────────────────── + this.sceneBundle = createScene(this.renderer, { backgroundColor: bgColor, enableFog: false }); + this.switchToOrthographicCamera(); + + // ── Target Three.js mesh (teal, semi-transparent) ────────────────── + const targetGeometry = new THREE.BufferGeometry(); + { + const pts = this.smoothedTarget.points; + const fcs = this.smoothedTarget.faces; + targetGeometry.setAttribute("position", new THREE.BufferAttribute(pts.data, 3)); + targetGeometry.setIndex(new THREE.BufferAttribute( + new Uint32Array(fcs.data.buffer, fcs.data.byteOffset, fcs.data.length), 1)); + targetGeometry.computeBoundingSphere(); + pts.delete(); fcs.delete(); + } + const targetColor = new THREE.Color(); + targetColor.setRGB(0, 0.835, 0.745, THREE.SRGBColorSpace); + const targetMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + color: targetColor, + transparent: true, + opacity: 0.5, + }); + this.materials.push(targetMaterial); + this.targetThree = new THREE.Mesh(targetGeometry, targetMaterial); + this.targetThree.matrixAutoUpdate = false; + this.syncThreeMatrix(this.targetThree, this.smoothedTarget); + this.sceneBundle.scene.add(this.targetThree); + + // ── Source Three.js mesh (white, opaque) ─────────────────────────── + const sourceGeometry = new THREE.BufferGeometry(); + { + const pts = this.source.points; + const fcs = this.source.faces; + sourceGeometry.setAttribute("position", new THREE.BufferAttribute(pts.data, 3)); + sourceGeometry.setIndex(new THREE.BufferAttribute( + new Uint32Array(fcs.data.buffer, fcs.data.byteOffset, fcs.data.length), 1)); + sourceGeometry.computeBoundingSphere(); + pts.delete(); fcs.delete(); + } + const sourceMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + }); + this.materials.push(sourceMaterial); + this.sourceThree = new THREE.Mesh(sourceGeometry, sourceMaterial); + this.sourceThree.matrixAutoUpdate = false; + this.sceneBundle.scene.add(this.sourceThree); + + // ── Matcap textures ──────────────────────────────────────────────── + const matcapUrl = isDarkMode + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + new THREE.TextureLoader().load(matcapUrl, (tex) => { + for (let i = 0; i < this.materials.length; i++) { + this.materials[i]!.matcap = i === 0 ? tex : tex.clone(); + this.materials[i]!.needsUpdate = true; + } + }); + + // ── Position meshes + fit camera ─────────────────────────────────── + this.positionMeshesForScreen(); + this.fitOrthographicCamera(); + + // ── Warmup alignment (run once, then reposition) ─────────────────── + this.align(); + this.positionMeshesForScreen(); + this.fitOrthographicCamera(); + + // ── Pointer events ───────────────────────────────────────────────── + const onPointerMove = (e: PointerEvent) => { + if (!container.contains(e.target as Node)) return; + this.updateNDC(e); + if (this.interactionMode === "rotate" && this.isRotating) { + this.handleRotation(e); + } else if (this.dragging && this.selectedId !== null) { + this.handleDrag(); + } else if (!this.dragging && !this.isRotating) { + this.handleHover(); + } + }; + const onPointerDown = (e: PointerEvent) => { + if (!container.contains(e.target as Node)) return; + this.updateNDC(e); + this.handleHover(); + + if (this.selectedId !== null) { + if (this.interactionMode === "rotate") { + this.isRotating = true; + this.lastMouseX = e.clientX; + this.lastMouseY = e.clientY; + } else { + this.dragging = true; + } + this.sceneBundle.controls.enabled = false; + } + }; + const onPointerUp = () => { + this.dragging = false; + this.isRotating = false; + this.selectedId = null; + this.sceneBundle.controls.enabled = true; + }; + + container.addEventListener("pointermove", onPointerMove); + container.addEventListener("pointerdown", onPointerDown); + window.addEventListener("pointerup", onPointerUp); + this.cleanups.push(() => { + container.removeEventListener("pointermove", onPointerMove); + container.removeEventListener("pointerdown", onPointerDown); + window.removeEventListener("pointerup", onPointerUp); + }); + + // ── Resize observer ──────────────────────────────────────────────── + const resizeObs = new ResizeObserver(() => this.resize()); + resizeObs.observe(container); + this.cleanups.push(() => resizeObs.disconnect()); + this.resize(); + + // ── Animation loop ───────────────────────────────────────────────── + this.animate(); } - private setupOrthographicCamera(container: HTMLElement): void { + // ════════════════════════════════════════════════════════════════════════ + // Orthographic camera + // ════════════════════════════════════════════════════════════════════════ + + private switchToOrthographicCamera() { + const rect = this.container.getBoundingClientRect(); + const aspect = rect.width / rect.height; + const frustumSize = 50; + const orthoCamera = new THREE.OrthographicCamera( - -1, 1, 1, -1, 0.1, 1000 + -frustumSize * aspect / 2, frustumSize * aspect / 2, + frustumSize / 2, -frustumSize / 2, + 0.1, 1000, ); + orthoCamera.position.copy(this.sceneBundle.camera.position); + orthoCamera.quaternion.copy(this.sceneBundle.camera.quaternion); + + this.sceneBundle.scene.remove(this.sceneBundle.camera); + this.sceneBundle.scene.add(orthoCamera); - // Replace camera in scene bundle - (this.sceneBundle1 as any).camera = orthoCamera; - this.sceneBundle1.controls.setCamera(orthoCamera); + const oldTarget = ((this.sceneBundle.controls as any).target as THREE.Vector3).clone(); + this.sceneBundle.controls.dispose(); + const newControls = new ArcballControls( + orthoCamera, this.container.querySelector("canvas")!, this.sceneBundle.scene, + ); + newControls.rotateSpeed = 1.2; + newControls.setGizmosVisible(false); + (newControls as any).target.copy(oldTarget); + newControls.update(); - // Now fit the orthographic camera to all meshes - this.fitOrthographicCamera(container); + (this.sceneBundle as any).camera = orthoCamera; + (this.sceneBundle as any).controls = newControls; } - private fitOrthographicCamera(container: HTMLElement): void { - const rect = container.getBoundingClientRect(); + private fitOrthographicCamera() { + const rect = this.container.getBoundingClientRect(); const aspect = rect.width / rect.height; const isLandscape = rect.width > rect.height; - const camera = this.sceneBundle1.camera as unknown as THREE.OrthographicCamera; + const camera = this.sceneBundle.camera as unknown as THREE.OrthographicCamera; + const diag = this.aabbDiagonal; - // Get positions from both instances to compute bounding box + // Get positions from both meshes const positions: THREE.Vector3[] = []; - const diag = this.wasmInstance.alignment_get_aabb_diagonal() ?? 1; - - for (let i = 0; i < 2; i++) { - const inst = this.wasmInstance.get_instance_on_idx(i); - if (!inst) continue; - const matrix = new Float32Array(inst.get_matrix()); - const m = new THREE.Matrix4().fromArray(matrix).transpose(); - const pos = new THREE.Vector3(); - pos.setFromMatrixPosition(m); + for (const tfMesh of [this.smoothedTarget, this.source]) { + const m = tfToThree(tfMesh); + const pos = new THREE.Vector3().setFromMatrixPosition(m); positions.push(pos); } @@ -70,9 +295,7 @@ export class AlignmentExample extends ThreejsBase { if (positions.length >= 2) { center.addVectors(positions[0]!, positions[1]!).multiplyScalar(0.5); } - const separation = positions.length >= 2 ? positions[0]!.distanceTo(positions[1]!) : 0; - // Different zoom for landscape vs portrait const zoomFactor = isLandscape ? 0.5 : 0.7; const extent = (separation + diag) * zoomFactor; @@ -87,183 +310,264 @@ export class AlignmentExample extends ThreejsBase { camera.position.set(center.x, center.y, center.z + diag * 3); camera.lookAt(center); - this.sceneBundle1.controls.target.copy(center); - this.sceneBundle1.controls.update(); + (this.sceneBundle.controls as any).target.copy(center); + this.sceneBundle.controls.update(); } - private positionMeshesForScreen(container: HTMLElement): void { - const rect = container.getBoundingClientRect(); + // ════════════════════════════════════════════════════════════════════════ + // Mesh positioning + // ════════════════════════════════════════════════════════════════════════ + + private positionMeshesForScreen() { + const rect = this.container.getBoundingClientRect(); const isLandscape = rect.width > rect.height; - const diag = this.wasmInstance.alignment_get_aabb_diagonal() ?? 1; + const diag = this.aabbDiagonal; - // More spacing for the axis we're spreading along - // Landscape: spread in X, Portrait: spread in Z const spacing = isLandscape ? diag * 1.2 : diag * 1.0; - - // Target stays at origin, source gets offset - // Camera on Z axis looking at XY plane: X = screen horizontal, Y = screen vertical - // Landscape: target left, source right (positive X) - // Portrait: target below, source above (positive Y) - const offset = isLandscape - ? [spacing, 0, 0] - : [0, spacing, 0]; - - // Build translation matrix for source - const m = new THREE.Matrix4().makeTranslation(offset[0], offset[1], offset[2]); - m.transpose(); - - const arr = m.toArray() as [ - number, number, number, number, - number, number, number, number, - number, number, number, number, - number, number, number, number - ]; - this.wasmInstance.alignment_set_source_matrix(arr); - this.updateMeshes(); + const offset = isLandscape ? [spacing, 0, 0] : [0, spacing, 0]; + + // Target: identity + const identityMat = this.tf.ndarray(new Float32Array([ + 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, + ])).reshape([4, 4]); + this.smoothedTarget.transformation = identityMat; + this.targetPC.transformation = identityMat; + identityMat.delete(); + this.syncThreeMatrix(this.targetThree, this.smoothedTarget); + + // Source: offset translation + const m = new THREE.Matrix4().makeTranslation(offset[0]!, offset[1]!, offset[2]!); + const mat = threeToTf(this.tf, m); + this.source.transformation = mat; + this.sourcePC.transformation = mat; + mat.delete(); + this.syncThreeMatrix(this.sourceThree, this.source); } - public setMode(mode: InteractionMode): void { - this.interactionMode = mode; - } + // ════════════════════════════════════════════════════════════════════════ + // Three.js matrix sync + // ════════════════════════════════════════════════════════════════════════ - public getMode(): InteractionMode { - return this.interactionMode; + private syncThreeMatrix(threeMesh: THREE.Mesh, tfMesh: any) { + const mat = tfMesh.transformation; + if (!mat) return; + const m = new THREE.Matrix4().fromArray(mat.data).transpose(); + threeMesh.matrix.copy(m); + mat.delete(); } - // Override pointer handlers to support rotate mode - public override onPointerDown(event: PointerEvent): void { - if (this.interactionMode === "rotate" && event.buttons === 1) { - // First do a mouse move to update selection state in WASM - const rect = this.renderer.domElement.getBoundingClientRect(); - const ndc = new THREE.Vector2( - ((event.clientX - rect.left) / rect.width) * 2 - 1, - -((event.clientY - rect.top) / rect.height) * 2 + 1 - ); - - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(ndc, this.sceneBundle1.camera); - const ray = raycaster.ray; - const cameraPos = this.sceneBundle1.camera.position; - const dir = new THREE.Vector3(); - this.sceneBundle1.camera.getWorldDirection(dir); - const focalPoint = cameraPos.clone().add(dir.multiplyScalar(100)); - - this.wasmInstance.OnMouseMove( - [ray.origin.x, ray.origin.y, ray.origin.z], - [ray.direction.x, ray.direction.y, ray.direction.z], - [cameraPos.x, cameraPos.y, cameraPos.z], - [focalPoint.x, focalPoint.y, focalPoint.z] - ); - - // Check if we hit a selectable mesh - const hitMesh = this.wasmInstance.OnLeftButtonDown(); - if (hitMesh) { - // Cancel WASM's drag mode, we handle rotation ourselves - this.wasmInstance.OnLeftButtonUp(); - this.isRotating = true; - this.lastMouseX = event.clientX; - this.lastMouseY = event.clientY; - this.sceneBundle1.controls.enabled = false; - event.stopPropagation(); - } - } else { - super.onPointerDown(event); - } + // ════════════════════════════════════════════════════════════════════════ + // Interaction: hover, drag, rotate + // ════════════════════════════════════════════════════════════════════════ + + private updateNDC(e: PointerEvent) { + const rect = this.container.getBoundingClientRect(); + this.ndc.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + this.ndc.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; } - public override onPointerMove(event: PointerEvent, touchHover = false): boolean { - if (this.interactionMode === "rotate" && this.isRotating) { - const dx = event.clientX - this.lastMouseX; - const dy = event.clientY - this.lastMouseY; - this.lastMouseX = event.clientX; - this.lastMouseY = event.clientY; - - // Convert mouse movement to rotation (similar to C++ example) - const angleX = dy * 0.5; // degrees - const angleY = dx * 0.5; - - // Get current source matrix (source is instance 1) - const sourceInst = this.wasmInstance.get_instance_on_idx(1); - if (!sourceInst) return true; - - const matrix = new Float32Array(sourceInst.get_matrix()); - const m = new THREE.Matrix4().fromArray(matrix).transpose(); - - // Get rotation center (translation part of current matrix) - const center = new THREE.Vector3(); - center.setFromMatrixPosition(m); - - // Create rotation matrices around world X and Y axes - const rotX = new THREE.Matrix4().makeRotationAxis( - new THREE.Vector3(1, 0, 0), - THREE.MathUtils.degToRad(angleX) - ); - const rotY = new THREE.Matrix4().makeRotationAxis( - new THREE.Vector3(0, 1, 0), - THREE.MathUtils.degToRad(angleY) - ); - - // Apply rotations centered at the mesh position - const toOrigin = new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z); - const fromOrigin = new THREE.Matrix4().makeTranslation(center.x, center.y, center.z); - - const newMatrix = new THREE.Matrix4() - .multiply(fromOrigin) - .multiply(rotY) - .multiply(rotX) - .multiply(toOrigin) - .multiply(m); - - // Send to WASM - newMatrix.transpose(); - const arr = newMatrix.toArray() as [ - number, number, number, number, - number, number, number, number, - number, number, number, number, - number, number, number, number - ]; - this.wasmInstance.alignment_set_source_matrix(arr); - this.updateMeshes(); - - event.stopPropagation(); - return true; + private handleHover() { + this.raycaster.setFromCamera(this.ndc, this.sceneBundle.camera); + const o = this.raycaster.ray.origin; + const d = this.raycaster.ray.direction; + const tfRay = this.tf.ray([o.x, o.y, o.z, d.x, d.y, d.z]); + + // Only source is pickable + const hit = pickMesh(this.tf, tfRay, [this.source]); + tfRay.delete(); + + if (hit) { + this.selectedId = 0; // index in [source] array + const hitPoint = this.raycaster.ray.at(hit.t, new THREE.Vector3()); + const camDir = new THREE.Vector3(); + this.sceneBundle.camera.getWorldDirection(camDir); + this.movingPlane.setFromNormalAndCoplanarPoint(camDir, hitPoint); + this.lastPoint.copy(hitPoint); } else { - return super.onPointerMove(event, touchHover); + this.selectedId = null; } } - public override onPointerUp(event: PointerEvent): void { - if (this.isRotating) { - this.isRotating = false; - this.sceneBundle1.controls.enabled = true; - event.stopPropagation(); - } else { - super.onPointerUp(event); - } + private handleDrag() { + this.raycaster.setFromCamera(this.ndc, this.sceneBundle.camera); + const nextPoint = new THREE.Vector3(); + this.raycaster.ray.intersectPlane(this.movingPlane, nextPoint); + if (!nextPoint) return; + + const dx = nextPoint.x - this.lastPoint.x; + const dy = nextPoint.y - this.lastPoint.y; + const dz = nextPoint.z - this.lastPoint.z; + this.lastPoint.copy(nextPoint); + + // Update TF transformation: read, modify translation, write back + const mat = this.source.transformation; + const dd = mat.data; + dd[3] += dx; dd[7] += dy; dd[11] += dz; + this.source.transformation = mat; + this.sourcePC.transformation = mat; + mat.delete(); + + this.syncThreeMatrix(this.sourceThree, this.source); } - public runMain() { - const v = new this.wasmInstance.VectorString(); - for (const path of this.paths) { - v.push_back(path); - } - this.wasmInstance.run_main_alignment(v); - for (const path of this.paths) { - this.wasmInstance.FS.unlink(path); - } + private handleRotation(event: PointerEvent) { + const dx = event.clientX - this.lastMouseX; + const dy = event.clientY - this.lastMouseY; + this.lastMouseX = event.clientX; + this.lastMouseY = event.clientY; + + const angleX = dy * 0.5; // degrees per pixel + const angleY = dx * 0.5; + + // Read current transform + const m = tfToThree(this.source); + const center = new THREE.Vector3().setFromMatrixPosition(m); + + // Create rotation matrices around world X and Y axes + const rotX = new THREE.Matrix4().makeRotationAxis( + new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(angleX), + ); + const rotY = new THREE.Matrix4().makeRotationAxis( + new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(angleY), + ); + + // Apply rotations centered at the mesh position + const toOrigin = new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z); + const fromOrigin = new THREE.Matrix4().makeTranslation(center.x, center.y, center.z); + + const newMatrix = new THREE.Matrix4() + .multiply(fromOrigin) + .multiply(rotY) + .multiply(rotX) + .multiply(toOrigin) + .multiply(m); + + // Write back + const newMat = threeToTf(this.tf, newMatrix); + this.source.transformation = newMat; + this.sourcePC.transformation = newMat; + newMat.delete(); + + this.syncThreeMatrix(this.sourceThree, this.source); } + // ════════════════════════════════════════════════════════════════════════ + // Alignment: OBB + ICP + // ════════════════════════════════════════════════════════════════════════ + public align(): number { - this.alignmentTime = this.wasmInstance.alignment_run_align(); - this.updateMeshes(); - return this.alignmentTime; + const t0 = performance.now(); + + // Read current source transform + const mCurrent = tfToThree(this.source); + + // Stage 1: OBB coarse alignment + const T_obb = this.tf.fitObbAlignment(this.sourcePC, this.targetPC); + const mObb = new THREE.Matrix4().fromArray(T_obb.data).transpose(); + T_obb.delete(); + + // Compose: T_after_obb = delta_obb * T_current + const mAfterObb = mObb.multiply(mCurrent); + + // Apply OBB result so ICP sees aligned position + let mat = threeToTf(this.tf, mAfterObb); + this.sourcePC.transformation = mat; + mat.delete(); + + // Stage 2: ICP refinement (point-to-plane) + const T_icp = this.tf.fitIcpAlignment(this.sourcePC, this.targetPC, { + maxIterations: 50, + nSamples: 1000, + k: 1, + }); + const mIcp = new THREE.Matrix4().fromArray(T_icp.data).transpose(); + T_icp.delete(); + + // Compose: T_final = delta_icp * T_after_obb + const mFinal = mIcp.multiply(mAfterObb); + + // Apply final transform to both source mesh and PC + mat = threeToTf(this.tf, mFinal); + this.source.transformation = mat; + this.sourcePC.transformation = mat; + mat.delete(); + + this.syncThreeMatrix(this.sourceThree, this.source); + return performance.now() - t0; } - public isAligned(): boolean { - return this.wasmInstance.alignment_is_aligned(); + // ════════════════════════════════════════════════════════════════════════ + // Public API + // ════════════════════════════════════════════════════════════════════════ + + public setMode(mode: InteractionMode) { + this.interactionMode = mode; + } + + public getMode(): InteractionMode { + return this.interactionMode; + } + + public applyTheme(isDark: boolean) { + this.sceneBundle.scene.background = new THREE.Color(isDark ? 0x1e1e1e : 0xfafafa); + + const matcapUrl = isDark + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + new THREE.TextureLoader().load(matcapUrl, (tex) => { + for (let i = 0; i < this.materials.length; i++) { + const mat = this.materials[i]!; + if (mat.matcap) mat.matcap.dispose(); + mat.matcap = i === 0 ? tex : tex.clone(); + mat.needsUpdate = true; + } + }); } - public getAlignmentTime(): number { - return this.alignmentTime; + // ════════════════════════════════════════════════════════════════════════ + // Resize + animate + // ════════════════════════════════════════════════════════════════════════ + + private resize() { + const w = this.container.clientWidth; + const h = this.container.clientHeight; + this.renderer.setSize(w, h); + this.renderer.setPixelRatio(window.devicePixelRatio); + const cam = this.sceneBundle.camera as unknown as THREE.OrthographicCamera; + const frustumH = cam.top - cam.bottom; + const aspect = w / h; + cam.left = -frustumH * aspect / 2; + cam.right = frustumH * aspect / 2; + cam.updateProjectionMatrix(); + } + + private animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + this.sceneBundle.controls.update(); + this.renderer.render(this.sceneBundle.scene, this.sceneBundle.camera); + } + + // ════════════════════════════════════════════════════════════════════════ + // Dispose + // ════════════════════════════════════════════════════════════════════════ + + public dispose() { + this.running = false; + for (const fn of this.cleanups) fn(); + this.cleanups = []; + + this.sceneBundle.controls.dispose(); + this.targetThree.geometry.dispose(); + this.sourceThree.geometry.dispose(); + for (const mat of this.materials) mat.dispose(); + + this.renderer.dispose(); + this.renderer.domElement.remove(); + + this.sourcePC.delete(); + this.targetPC.delete(); + this.source.delete(); + this.smoothedTarget.delete(); } } diff --git a/docs/app/examples/BooleanExample.ts b/docs/app/examples/BooleanExample.ts index e2a2573..64ef73b 100644 --- a/docs/app/examples/BooleanExample.ts +++ b/docs/app/examples/BooleanExample.ts @@ -1,265 +1,638 @@ -import type { MainModule } from "@/examples/native"; -import { syncOrbitControls, type SceneBundle } from "@/utils/sceneUtils"; -import { - buffersToCurves, - createMesh, - updateResultMesh, - CurveRenderer, -} from "@/utils/utils"; -import { ThreejsBase } from "@/examples/ThreejsBase"; +import { syncOrbitControls, createScene, type SceneBundle } from "@/utils/sceneUtils"; +import { centerAndScale, pickMesh, randomTransformation, RollingAverage, CurveRenderer } from "@/utils/utils"; import * as THREE from "three"; import { ArcballControls } from "three/addons/controls/ArcballControls.js"; -export class BooleanExample extends ThreejsBase { +type TF = typeof import("@/examples/trueform/index.js"); + +export class BooleanExample { + private tf: TF; + private dragon: any; + private sphere: any; + private tfMeshes: any[] = []; // [0] = dragon, [1] = sphere + private resultMesh: any = null; + + private container1: HTMLElement; + private container2: HTMLElement; + private renderer1: THREE.WebGLRenderer; + private renderer2: THREE.WebGLRenderer; + private sceneBundle1: SceneBundle; + private sceneBundle2: SceneBundle; + + private dragonThree: THREE.Mesh; + private sphereThree: THREE.Mesh; + private resultThree: THREE.Mesh; + private resultGeometry: THREE.BufferGeometry; private curveRenderer: CurveRenderer; + + private materials: THREE.MeshMatcapMaterial[] = []; + private running = true; + private cleanups: (() => void)[] = []; + + private selectedId: number | null = null; + private dragging = false; + private movingPlane = new THREE.Plane(); + private lastPoint = new THREE.Vector3(); private keyPressed = false; - public onSphereSizeDelta?: (deltaSteps: number) => void; + private lastActiveScene: 1 | 2 = 1; + private isSyncing = false; - // private pointDebug = createPoints(); - public randomize() { - this.wasmInstance.OnKeyPress("n"); - this.updateMeshes(); - } + private sphereScale = 2.0; + private booleanTiming = new RollingAverage(); + private raycaster = new THREE.Raycaster(); + private ndc = new THREE.Vector2(); - public resyncCamera() { - this.syncSceneControls = true; - if (this.sceneBundle2) { - syncOrbitControls(this.sceneBundle1.controls, this.sceneBundle2.controls); - } - } + public refreshTimeValue: (() => number) | null = null; + public onSphereSizeDelta?: (deltaSteps: number) => void; - public adjustSphereSize(deltaSteps: number) { - const steps = Math.trunc(deltaSteps); - if (steps === 0) return false; - const direction = Math.sign(steps); - let handled = false; - for (let i = 0; i < Math.abs(steps); i++) { - handled = this.wasmInstance.OnMouseWheel(direction, true) || handled; + constructor( + tf: TF, + fileBuffer: ArrayBuffer, + fileName: string, + container: HTMLElement, + container2: HTMLElement, + isDarkMode = true, + ) { + this.tf = tf; + this.container1 = container; + this.container2 = container2; + + // ── Load dragon ────────────────────────────────────────────────────── + const ext = fileName.split(".").pop()?.toLowerCase(); + this.dragon = ext === "stl" ? tf.readStl(fileBuffer) : tf.readObj(fileBuffer); + centerAndScale(tf, this.dragon); + + // Set identity transformation (required for booleanDifference to tag it) + const dragonMat = tf.ndarray(new Float32Array([ + 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, + ])).reshape([4, 4]); + this.dragon.transformation = dragonMat; + dragonMat.delete(); + + // ── Sphere position (C++ uses obb.axes[1] * 4.0; OBB not exposed in TS) ─ + // After centerAndScale the dragon is deterministic: OBB 2nd axis ≈ Y. + const spherePos = [2.5, 1.5, -1.2]; + + // ── Create sphere ──────────────────────────────────────────────────── + this.sphere = tf.sphereMesh(1.0, 32, 32); + + const s = this.sphereScale; + const sphereMat = tf.ndarray(new Float32Array([ + s, 0, 0, spherePos[0]!, 0, s, 0, spherePos[1]!, 0, 0, s, spherePos[2]!, 0, 0, 0, 1, + ])).reshape([4, 4]); + this.sphere.transformation = sphereMat; + sphereMat.delete(); + + this.tfMeshes = [this.dragon, this.sphere]; + + // ── Two renderers + scenes ─────────────────────────────────────────── + const bgColor1 = isDarkMode ? 0x1e1e1e : 0xfafafa; + const bgColor2 = isDarkMode ? 0x262626 : 0xf5f5f5; + + this.renderer1 = new THREE.WebGLRenderer({ antialias: true }); + this.renderer1.outputColorSpace = THREE.SRGBColorSpace; + this.renderer1.toneMapping = THREE.ACESFilmicToneMapping; + this.renderer1.toneMappingExposure = 1.0; + container.appendChild(this.renderer1.domElement); + this.sceneBundle1 = createScene(this.renderer1, { backgroundColor: bgColor1, enableFog: false }); + + this.renderer2 = new THREE.WebGLRenderer({ antialias: true }); + this.renderer2.outputColorSpace = THREE.SRGBColorSpace; + this.renderer2.toneMapping = THREE.ACESFilmicToneMapping; + this.renderer2.toneMappingExposure = 1.0; + container2.appendChild(this.renderer2.domElement); + this.sceneBundle2 = createScene(this.renderer2, { backgroundColor: bgColor2, enableFog: false }); + + // Switch both to orthographic cameras + this.switchToOrthographicCamera(this.sceneBundle1, container); + this.switchToOrthographicCamera(this.sceneBundle2, container2); + + // ── Matcap texture ─────────────────────────────────────────────────── + const matcapUrl = isDarkMode + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + + // ── Dragon Three.js mesh (flat shading, white) ─────────────────────── + const dragonGeometry = new THREE.BufferGeometry(); + { + const pts = this.dragon.points; + const fcs = this.dragon.faces; + dragonGeometry.setAttribute("position", new THREE.BufferAttribute(pts.data, 3)); + dragonGeometry.setIndex(new THREE.BufferAttribute( + new Uint32Array(fcs.data.buffer, fcs.data.byteOffset, fcs.data.length), 1)); + dragonGeometry.computeBoundingSphere(); + pts.delete(); fcs.delete(); } - this.updateMeshes(); - return handled; + const dragonMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + }); + this.materials.push(dragonMaterial); + this.dragonThree = new THREE.Mesh(dragonGeometry, dragonMaterial); + this.dragonThree.matrixAutoUpdate = false; + this.syncThreeMatrix(0, this.dragonThree); + this.sceneBundle1.scene.add(this.dragonThree); + + // ── Sphere Three.js mesh (smooth shading, light blue) ──────────────── + const sphereGeometry = new THREE.BufferGeometry(); + { + const pts = this.sphere.points; + const fcs = this.sphere.faces; + sphereGeometry.setAttribute("position", new THREE.BufferAttribute(pts.data, 3)); + sphereGeometry.setIndex(new THREE.BufferAttribute( + new Uint32Array(fcs.data.buffer, fcs.data.byteOffset, fcs.data.length), 1)); + sphereGeometry.computeVertexNormals(); + sphereGeometry.computeBoundingSphere(); + pts.delete(); fcs.delete(); + } + const sphereColor = new THREE.Color(); + sphereColor.setRGB(0.7, 0.85, 1.0, THREE.SRGBColorSpace); + const sphereMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: false, + color: sphereColor, + }); + this.materials.push(sphereMaterial); + this.sphereThree = new THREE.Mesh(sphereGeometry, sphereMaterial); + this.sphereThree.matrixAutoUpdate = false; + this.syncThreeMatrix(1, this.sphereThree); + this.sceneBundle1.scene.add(this.sphereThree); + + // ── CurveRenderer (teal, instanced tubes) ──────────────────────────── + this.curveRenderer = new CurveRenderer({ + color: 0x00d5be, + radius: 0.075, + maxSegments: 20000, + }); + this.sceneBundle1.scene.add(this.curveRenderer.object); + + // ── Result Three.js mesh (flat shading, right scene) ───────────────── + this.resultGeometry = new THREE.BufferGeometry(); + const resultMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + }); + this.materials.push(resultMaterial); + this.resultThree = new THREE.Mesh(this.resultGeometry, resultMaterial); + this.resultThree.matrixAutoUpdate = false; + this.sceneBundle2.scene.add(this.resultThree); + + // Load matcap textures for all materials + new THREE.TextureLoader().load(matcapUrl, (tex) => { + for (let i = 0; i < this.materials.length; i++) { + this.materials[i]!.matcap = i === 0 ? tex : tex.clone(); + this.materials[i]!.needsUpdate = true; + } + }); + + // ── Initial boolean computation (warm-up, discard timing) ─────────── + this.computeBoolean(); + this.booleanTiming.clear(); + + // ── Fit cameras ────────────────────────────────────────────────────── + this.fitOrthoCameraToMeshes(this.sceneBundle1, 1.8); + + // Adjust camera angle slightly right and up for better dragon view + const camera = this.sceneBundle1.camera; + const target = (this.sceneBundle1.controls as any).target as THREE.Vector3; + const offset = camera.position.clone().sub(target); + const dist = offset.length(); + const angleH = 0.15; + const angleV = 0.1; + camera.position.set( + target.x + dist * Math.sin(angleH), + target.y + dist * Math.sin(angleV), + target.z + dist * Math.cos(angleH) * Math.cos(angleV), + ); + camera.lookAt(target); + this.sceneBundle1.controls.update(); + + this.fitOrthoCameraToMeshes(this.sceneBundle2, 1.8); + syncOrbitControls(this.sceneBundle1.controls, this.sceneBundle2.controls); + + // ── Bidirectional camera sync ───────────────────────────────────────── + const syncFrom = (source: 1 | 2) => { + if (this.isSyncing) return; + this.isSyncing = true; + if (source === 1) { + syncOrbitControls(this.sceneBundle1.controls, this.sceneBundle2.controls); + } else { + syncOrbitControls(this.sceneBundle2.controls, this.sceneBundle1.controls); + } + this.isSyncing = false; + }; + + this.sceneBundle1.controls.addEventListener("change", () => { + if (this.lastActiveScene === 1) syncFrom(1); + }); + this.sceneBundle2.controls.addEventListener("change", () => { + if (this.lastActiveScene === 2) syncFrom(2); + }); + + const onEnterContainer1 = () => { this.lastActiveScene = 1; }; + const onEnterContainer2 = () => { this.lastActiveScene = 2; }; + container.addEventListener("pointerenter", onEnterContainer1); + container2.addEventListener("pointerenter", onEnterContainer2); + this.cleanups.push(() => { + container.removeEventListener("pointerenter", onEnterContainer1); + container2.removeEventListener("pointerenter", onEnterContainer2); + }); + + // ── Pointer events (on left container) ─────────────────────────────── + const onPointerMove = (e: PointerEvent) => { + if (!container.contains(e.target as Node)) return; + this.updateNDC(e); + if (this.dragging && this.selectedId !== null) { + this.handleDrag(); + } else if (!this.dragging) { + this.handleHover(); + } + }; + const onPointerDown = (e: PointerEvent) => { + if (!container.contains(e.target as Node)) return; + this.updateNDC(e); + this.handleHover(); + if (this.selectedId !== null) { + this.dragging = true; + this.sceneBundle1.controls.enabled = false; + } + }; + const onPointerUp = () => { + this.dragging = false; + this.selectedId = null; + this.sceneBundle1.controls.enabled = true; + }; + + container.addEventListener("pointermove", onPointerMove); + container.addEventListener("pointerdown", onPointerDown); + window.addEventListener("pointerup", onPointerUp); + this.cleanups.push(() => { + container.removeEventListener("pointermove", onPointerMove); + container.removeEventListener("pointerdown", onPointerDown); + window.removeEventListener("pointerup", onPointerUp); + }); + + // ── Keyboard events ────────────────────────────────────────────────── + const onKeyDown = (event: KeyboardEvent) => { + if (this.keyPressed) return; + this.keyPressed = true; + if (event.key === "r") { this.resyncCamera(); return; } + if (event.key === "n") { this.randomize(); return; } + }; + const onKeyUp = () => { this.keyPressed = false; }; + window.addEventListener("keydown", onKeyDown); + window.addEventListener("keyup", onKeyUp); + this.cleanups.push(() => { + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("keyup", onKeyUp); + }); + + // ── Ctrl+scroll for sphere size ────────────────────────────────────── + const onWheel = (event: WheelEvent) => { + if (!event.ctrlKey) return; + event.preventDefault(); + const delta = event.deltaY !== 0 ? event.deltaY : event.deltaX; + if (delta === 0) return; + const normalizedDelta = delta / Math.abs(delta); + const handled = this.adjustSphereSize(-normalizedDelta); + if (handled) { + this.onSphereSizeDelta?.(-normalizedDelta); + event.stopImmediatePropagation(); + } + }; + const wheelOpts = { passive: false, capture: true } as const; + window.addEventListener("wheel", onWheel, wheelOpts); + this.cleanups.push(() => { + window.removeEventListener("wheel", onWheel, wheelOpts); + }); + + // ── Resize observers ───────────────────────────────────────────────── + const resizeObs1 = new ResizeObserver(() => this.resize1()); + resizeObs1.observe(container); + const resizeObs2 = new ResizeObserver(() => this.resize2()); + resizeObs2.observe(container2); + this.cleanups.push(() => { resizeObs1.disconnect(); resizeObs2.disconnect(); }); + this.resize1(); + this.resize2(); + + // ── Animation loop ─────────────────────────────────────────────────── + this.animate(); } + // ════════════════════════════════════════════════════════════════════════ + // Orthographic camera helpers + // ════════════════════════════════════════════════════════════════════════ + private switchToOrthographicCamera(sceneBundle: SceneBundle, container: HTMLElement) { const rect = container.getBoundingClientRect(); const aspect = rect.width / rect.height; const frustumSize = 50; const orthoCamera = new THREE.OrthographicCamera( - -frustumSize * aspect / 2, - frustumSize * aspect / 2, - frustumSize / 2, - -frustumSize / 2, - 0.1, - 1000 + -frustumSize * aspect / 2, frustumSize * aspect / 2, + frustumSize / 2, -frustumSize / 2, + 0.1, 1000, ); - - // Copy position and orientation from perspective camera orthoCamera.position.copy(sceneBundle.camera.position); orthoCamera.quaternion.copy(sceneBundle.camera.quaternion); - // Remove old camera and add new one sceneBundle.scene.remove(sceneBundle.camera); sceneBundle.scene.add(orthoCamera); - // Dispose old controls and create new ones with ortho camera - const oldTarget = sceneBundle.controls.target.clone(); + const oldTarget = ((sceneBundle.controls as any).target as THREE.Vector3).clone(); sceneBundle.controls.dispose(); - const newControls = new ArcballControls(orthoCamera, container.querySelector('canvas')!, sceneBundle.scene); + const newControls = new ArcballControls(orthoCamera, container.querySelector("canvas")!, sceneBundle.scene); newControls.rotateSpeed = 1.2; newControls.setGizmosVisible(false); - newControls.target.copy(oldTarget); + (newControls as any).target.copy(oldTarget); newControls.update(); - // Update bundle references (cast to any to bypass readonly) (sceneBundle as any).camera = orthoCamera; (sceneBundle as any).controls = newControls; } - private fitOrthoCameraToMeshes(sceneBundle: SceneBundle, offset: number = 1.25) { + private fitOrthoCameraToMeshes(sceneBundle: SceneBundle, offset = 1.25) { const { scene, controls } = sceneBundle; const camera = sceneBundle.camera as unknown as THREE.OrthographicCamera; - // Find all meshes in the scene const meshes: THREE.Mesh[] = []; scene.traverse((child) => { - if (child.type === 'Mesh' || child.type === 'InstancedMesh') { + if (child.type === "Mesh" || child.type === "InstancedMesh") { meshes.push(child as THREE.Mesh); } }); - if (meshes.length === 0) return; - // Calculate bounding box of all meshes const combinedBox = new THREE.Box3(); - meshes.forEach(mesh => { - const meshBox = new THREE.Box3().setFromObject(mesh); - combinedBox.union(meshBox); - }); + meshes.forEach((mesh) => { combinedBox.union(new THREE.Box3().setFromObject(mesh)); }); const center = combinedBox.getCenter(new THREE.Vector3()); const size = combinedBox.getSize(new THREE.Vector3()); const maxDimension = Math.max(size.x, size.y, size.z) * offset; - // Update orthographic frustum to fit the scene const aspect = (camera.right - camera.left) / (camera.top - camera.bottom); camera.left = -maxDimension * aspect / 2; camera.right = maxDimension * aspect / 2; camera.top = maxDimension / 2; camera.bottom = -maxDimension / 2; - // Position camera along Z axis looking at center const distance = maxDimension * 2; camera.position.set(center.x, center.y, center.z + distance); camera.lookAt(center); camera.updateProjectionMatrix(); - controls.target.copy(center); + (controls as any).target.copy(center); controls.update(); } - constructor( - wasmInstance: MainModule, - path: string[], - container: HTMLElement, - container2: HTMLElement, - isDarkMode = true, - ) { - super(wasmInstance, path, container, container2, true, false, isDarkMode); + // ════════════════════════════════════════════════════════════════════════ + // Boolean computation + // ════════════════════════════════════════════════════════════════════════ + + private computeBoolean() { + + if (this.resultMesh) { this.resultMesh.delete(); this.resultMesh = null; } + + const t0 = performance.now(); + const result = this.tf.booleanDifference(this.dragon, this.sphere, { returnCurves: true }); + this.booleanTiming.add(performance.now() - t0); + + // Update result geometry — views, zero copies + const rPts = result.mesh.points; + const rFaces = result.mesh.faces; + this.resultGeometry.setAttribute("position", new THREE.BufferAttribute(rPts.data, 3)); + this.resultGeometry.setIndex(new THREE.BufferAttribute( + new Uint32Array(rFaces.data.buffer, rFaces.data.byteOffset, rFaces.data.length), 1)); + this.resultGeometry.computeBoundingSphere(); + this.resultGeometry.computeBoundingBox(); + rPts.delete(); rFaces.delete(); + + // Update curves — zero-copy via updateFromBuffers + const pts = result.curves.points; + const pathsBuf = result.curves.paths; + const pathsData = pathsBuf.data; + const pathsOffsets = pathsBuf.offsets; + this.curveRenderer.updateFromBuffers( + pts.data as Float32Array, + pathsData.data as Int32Array, + pathsOffsets.data as Int32Array, + ); + pathsOffsets.delete(); pathsData.delete(); pathsBuf.delete(); pts.delete(); - // Switch to orthographic camera for scene 1 - this.switchToOrthographicCamera(this.sceneBundle1, container); - if (this.sceneBundle2) { - this.switchToOrthographicCamera(this.sceneBundle2, container2); - } + this.resultMesh = result.mesh; + result.labels.delete(); + result.curves.delete(); - // Enable smooth shading for the sphere (meshDataId = 1) - const sphereGeometry = this.geometries.get(1); - const sphereInstancedMesh = this.instancedMeshes.get(1); - if (sphereGeometry && sphereInstancedMesh) { - sphereGeometry.computeVertexNormals(); - const oldMaterial = sphereInstancedMesh.material as THREE.MeshMatcapMaterial; - const smoothMaterial = new THREE.MeshMatcapMaterial({ - matcap: oldMaterial.matcap, - side: THREE.DoubleSide, - flatShading: false, - }); - sphereInstancedMesh.material = smoothMaterial; - } + if (this.refreshTimeValue) this.refreshTimeValue(); + } - const interceptKeyDownEvent = (event: KeyboardEvent) => { - if (this.keyPressed) return; - this.keyPressed = true; - if (event.key === "r") { - this.resyncCamera(); - return; - } - if (event.key === "n") { - this.randomize(); - return; - } - this.wasmInstance.OnKeyPress(event.key); - this.updateMeshes(); - }; - const interceptKeyUpEvent = (_event: KeyboardEvent) => { - this.keyPressed = false; - }; - window.addEventListener("keydown", interceptKeyDownEvent); - window.addEventListener("keyup", interceptKeyUpEvent); - this.addCleanup(() => { - window.removeEventListener("keydown", interceptKeyDownEvent); - window.removeEventListener("keyup", interceptKeyUpEvent); - }); + // ════════════════════════════════════════════════════════════════════════ + // Three.js matrix sync + // ════════════════════════════════════════════════════════════════════════ + + private syncThreeMatrix(index: number, mesh?: THREE.Mesh) { + const target = mesh ?? (index === 0 ? this.dragonThree : this.sphereThree); + const mat = this.tfMeshes[index]!.transformation; + if (!mat) return; + const m = new THREE.Matrix4(); + m.fromArray(mat.data); + m.transpose(); + target.matrix.copy(m); + mat.delete(); + } - // Ctrl+scroll to change sphere radius - const interceptWheelEvent = (event: WheelEvent) => { - if (!event.ctrlKey) return; - event.preventDefault(); - const delta = event.deltaY !== 0 ? event.deltaY : event.deltaX; - if (delta === 0) return; - const normalizedDelta = delta / Math.abs(delta); - const handled = this.adjustSphereSize(-normalizedDelta); - if (handled) { - this.onSphereSizeDelta?.(-normalizedDelta); - event.stopImmediatePropagation(); - } - }; - const wheelListenerOptions = { - passive: false, - capture: true, - }; - window.addEventListener("wheel", interceptWheelEvent, wheelListenerOptions); - this.addCleanup(() => { - window.removeEventListener("wheel", interceptWheelEvent, wheelListenerOptions); - }); + // ════════════════════════════════════════════════════════════════════════ + // Interaction: drag + // ════════════════════════════════════════════════════════════════════════ - this.curveRenderer = new CurveRenderer({ - color: 0x00d5be, - radius: 0.075, - maxSegments: 20000, - }); - this.sceneBundle1.scene.add(this.curveRenderer.object); + private updateNDC(e: PointerEvent) { + const rect = this.container1.getBoundingClientRect(); + this.ndc.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + this.ndc.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + } - if (this.sceneBundle2 && this.renderer2) { - const mesh = createMesh(this.isDarkMode); - this.meshes2.set(0, mesh); - this.sceneBundle2.scene.add(mesh); + private handleHover() { + this.raycaster.setFromCamera(this.ndc, this.sceneBundle1.camera); + const o = this.raycaster.ray.origin; + const d = this.raycaster.ray.direction; + const tfRay = this.tf.ray([o.x, o.y, o.z, d.x, d.y, d.z]); + + const hit = pickMesh(this.tf, tfRay, this.tfMeshes); + tfRay.delete(); + + if (hit) { + this.selectedId = hit.index; + const hitPoint = this.raycaster.ray.at(hit.t, new THREE.Vector3()); + const camDir = new THREE.Vector3(); + this.sceneBundle1.camera.getWorldDirection(camDir); + this.movingPlane.setFromNormalAndCoplanarPoint(camDir, hitPoint); + this.lastPoint.copy(hitPoint); + } else { + this.selectedId = null; } + } - this.updateMeshes(); - this.fitOrthoCameraToMeshes(this.sceneBundle1, 1.8); + private handleDrag() { + const id = this.selectedId!; - // Adjust camera angle slightly right and up for better dragon view - const camera = this.sceneBundle1.camera; - const target = this.sceneBundle1.controls.target; - const offset = camera.position.clone().sub(target); - const distance = offset.length(); - const angleH = 0.15; // horizontal angle, positive = right - const angleV = 0.1; // vertical angle, positive = up - camera.position.set( - target.x + distance * Math.sin(angleH), - target.y + distance * Math.sin(angleV), - target.z + distance * Math.cos(angleH) * Math.cos(angleV) - ); - camera.lookAt(target); - this.sceneBundle1.controls.update(); + this.raycaster.setFromCamera(this.ndc, this.sceneBundle1.camera); + const nextPoint = new THREE.Vector3(); + this.raycaster.ray.intersectPlane(this.movingPlane, nextPoint); + if (!nextPoint) return; - if (this.sceneBundle2) { - this.fitOrthoCameraToMeshes(this.sceneBundle2, 1.8); - syncOrbitControls(this.sceneBundle1.controls, this.sceneBundle2.controls); - } + const dx = nextPoint.x - this.lastPoint.x; + const dy = nextPoint.y - this.lastPoint.y; + const dz = nextPoint.z - this.lastPoint.z; + this.lastPoint.copy(nextPoint); + + // Update TF transformation: read, modify translation, write back + const mat = this.tfMeshes[id]!.transformation; + const d = mat.data; + d[3] += dx; d[7] += dy; d[11] += dz; + this.tfMeshes[id]!.transformation = mat; + mat.delete(); + + this.syncThreeMatrix(id); + this.computeBoolean(); } - public runMain() { - const v = new this.wasmInstance.VectorString(); - for (let i = 0; i < this.paths.length; i++) { - v.push_back(this.paths[i]!); - } - this.wasmInstance.run_main(v); - for (let i = 0; i < this.paths.length; i++) { - this.wasmInstance.FS.unlink(this.paths[i]); - } + // ════════════════════════════════════════════════════════════════════════ + // Public API + // ════════════════════════════════════════════════════════════════════════ + + public getAverageTime(): number { + return this.booleanTiming.average; } - public override updateMeshes() { - super.updateMeshes(); - - // Update curve mesh (intersection curves) - const cO = this.wasmInstance.get_curve_mesh(); - if (cO && cO.updated) { - const points = cO.get_curve_points(); - const ids = cO.get_curve_ids(); - const offsets = cO.get_curve_offsets(); - const curves = buffersToCurves(points, ids, offsets); - this.curveRenderer.update(curves); - } + public resyncCamera() { + syncOrbitControls(this.sceneBundle1.controls, this.sceneBundle2.controls); + } + + public randomize() { + const tf = this.tf; + + // Dragon: random rotation, preserve translation + const dMat = this.dragon.transformation; + const dtx = dMat.data[3], dty = dMat.data[7], dtz = dMat.data[11]; + dMat.delete(); + const dNew = randomTransformation(tf, dtx, dty, dtz); + this.dragon.transformation = dNew; + dNew.delete(); + + // Sphere: random rotation with scale, preserve translation + const sMat = this.sphere.transformation; + const stx = sMat.data[3], sty = sMat.data[7], stz = sMat.data[11]; + sMat.delete(); + const sNew = randomTransformation(tf, stx, sty, stz); + const sd = sNew.data; + const sc = this.sphereScale; + sd[0] *= sc; sd[1] *= sc; sd[2] *= sc; + sd[4] *= sc; sd[5] *= sc; sd[6] *= sc; + sd[8] *= sc; sd[9] *= sc; sd[10] *= sc; + this.sphere.transformation = sNew; + sNew.delete(); + + this.syncThreeMatrix(0); + this.syncThreeMatrix(1); + this.computeBoolean(); + } + + public adjustSphereSize(deltaSteps: number): boolean { + const steps = Math.trunc(deltaSteps); + if (steps === 0) return false; + const newScale = Math.max(0.1, Math.min(5.0, this.sphereScale + steps * 0.05)); + if (newScale === this.sphereScale) return false; + this.sphereScale = newScale; + + // Set only diagonal — matches old WASM behavior (matrix[0/5/10] = scale) + const mat = this.sphere.transformation; + const d = mat.data; + d[0] = newScale; d[5] = newScale; d[10] = newScale; + this.sphere.transformation = mat; + mat.delete(); + + this.syncThreeMatrix(1); + this.computeBoolean(); + return true; + } - if (this.renderer2 && this.sceneBundle2) { - const resultMesh = this.wasmInstance.get_result_mesh(); - const mesh = this.meshes2.get(0); - if (resultMesh && mesh) { - updateResultMesh(resultMesh, mesh); + public applyTheme(isDark: boolean) { + this.sceneBundle1.scene.background = new THREE.Color(isDark ? 0x1e1e1e : 0xfafafa); + this.sceneBundle2.scene.background = new THREE.Color(isDark ? 0x262626 : 0xf5f5f5); + + const matcapUrl = isDark + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + new THREE.TextureLoader().load(matcapUrl, (tex) => { + for (let i = 0; i < this.materials.length; i++) { + const mat = this.materials[i]!; + if (mat.matcap) mat.matcap.dispose(); + mat.matcap = i === 0 ? tex : tex.clone(); + mat.needsUpdate = true; } - } + }); + } + + // ════════════════════════════════════════════════════════════════════════ + // Resize + animate + // ════════════════════════════════════════════════════════════════════════ + + private resize1() { + const w = this.container1.clientWidth; + const h = this.container1.clientHeight; + this.renderer1.setSize(w, h); + this.renderer1.setPixelRatio(window.devicePixelRatio); + const cam = this.sceneBundle1.camera as unknown as THREE.OrthographicCamera; + const frustumH = cam.top - cam.bottom; + const aspect = w / h; + cam.left = -frustumH * aspect / 2; + cam.right = frustumH * aspect / 2; + cam.updateProjectionMatrix(); + } + + private resize2() { + const w = this.container2.clientWidth; + const h = this.container2.clientHeight; + this.renderer2.setSize(w, h); + this.renderer2.setPixelRatio(window.devicePixelRatio); + const cam = this.sceneBundle2.camera as unknown as THREE.OrthographicCamera; + const frustumH = cam.top - cam.bottom; + const aspect = w / h; + cam.left = -frustumH * aspect / 2; + cam.right = frustumH * aspect / 2; + cam.updateProjectionMatrix(); + } + + private animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + this.sceneBundle1.controls.update(); + this.sceneBundle2.controls.update(); + this.renderer1.render(this.sceneBundle1.scene, this.sceneBundle1.camera); + this.renderer2.render(this.sceneBundle2.scene, this.sceneBundle2.camera); + } + + public dispose() { + this.running = false; + for (const fn of this.cleanups) fn(); + this.cleanups = []; + + this.sceneBundle1.controls.dispose(); + this.sceneBundle2.controls.dispose(); + this.curveRenderer.dispose(); + + this.dragonThree.geometry.dispose(); + this.sphereThree.geometry.dispose(); + this.resultGeometry.dispose(); + for (const mat of this.materials) mat.dispose(); + + this.renderer1.dispose(); + this.renderer1.domElement.remove(); + this.renderer2.dispose(); + this.renderer2.domElement.remove(); + + if (this.resultMesh) { this.resultMesh.delete(); this.resultMesh = null; } + this.sphere.delete(); + this.dragon.delete(); } } diff --git a/docs/app/examples/ClosestPointsExample.ts b/docs/app/examples/ClosestPointsExample.ts new file mode 100644 index 0000000..a48ef4a --- /dev/null +++ b/docs/app/examples/ClosestPointsExample.ts @@ -0,0 +1,492 @@ +import { fitCameraToAllMeshesFromZPlane, createScene, type SceneBundle } from "@/utils/sceneUtils"; +import { centerAndScale, pickMesh, randomTransformation, RollingAverage } from "@/utils/utils"; +import * as THREE from "three"; + +type TF = typeof import("@/examples/trueform/index.js"); + +export class ClosestPointsExample { + private tf: TF; + private baseMesh: any; + private tfMeshes: any[] = []; + private container: HTMLElement; + private renderer: THREE.WebGLRenderer; + private sceneBundle: SceneBundle; + private threeMeshes: THREE.Mesh[] = []; + private materials: THREE.MeshMatcapMaterial[] = []; + private running = true; + private cleanups: (() => void)[] = []; + + // Interaction + private selectedId: number | null = null; + private dragging = false; + private movingPlane = new THREE.Plane(); + private lastPoint = new THREE.Vector3(); + private raycaster = new THREE.Raycaster(); + private ndc = new THREE.Vector2(); + + // Closest points visualization + private closestGroup!: THREE.Group; + private sphere1!: THREE.Mesh; + private sphere2!: THREE.Mesh; + private connector!: THREE.Mesh; + private hasClosestPoints = false; + private closestPtSelected = new THREE.Vector3(); // on the dragged mesh + private closestPtOther = new THREE.Vector3(); // on the other mesh + private aabbDiag = 10; + + // Animation (pointer-up slide) + private animating = false; + private animSelectedId = 0; + private animRay: { origin: THREE.Vector3; dir: THREE.Vector3 } | null = null; + private animFocalRay: { origin: THREE.Vector3; dir: THREE.Vector3 } | null = null; + private animPrev = new THREE.Vector3(); + private animStart = 0; + + // Timing + private timing = new RollingAverage(); + + public refreshTimeValue: (() => number) | null = null; + + constructor(tf: TF, fileBuffer: ArrayBuffer, fileName: string, container: HTMLElement, isDarkMode = true) { + this.tf = tf; + this.container = container; + + // Load mesh + const ext = fileName.split(".").pop()?.toLowerCase(); + this.baseMesh = ext === "stl" ? tf.readStl(fileBuffer) : tf.readObj(fileBuffer); + centerAndScale(tf, this.baseMesh); + + // Compute AABB diagonal for sizing + const points = this.baseMesh.points; + const pMin = tf.min(points, 0); + const pMax = tf.max(points, 0); + const diag = pMax.sub(pMin); + this.aabbDiag = tf.norm(diag) as number; + pMin.delete(); pMax.delete(); diag.delete(); + + // Create 2 tf meshes, positioned based on screen orientation + const rect = container.getBoundingClientRect(); + const isLandscape = rect.width > rect.height; + const spacing = isLandscape ? this.aabbDiag * 1.2 : this.aabbDiag * 1.0; + const offsets: [number, number, number][] = isLandscape + ? [[-spacing / 2, 0, 0], [spacing / 2, 0, 0]] + : [[0, -spacing / 2, 0], [0, spacing / 2, 0]]; + + const mesh0 = this.baseMesh; + const mesh1 = this.baseMesh.sharedView(); + // No random rotation on first frame — just translate into position + const mat0 = tf.makeTranslation(...offsets[0]!); + mesh0.transformation = mat0; + mat0.delete(); + const mat1 = tf.makeTranslation(...offsets[1]!); + mesh1.transformation = mat1; + mat1.delete(); + this.tfMeshes = [mesh0, mesh1]; + + // Three.js setup + this.renderer = new THREE.WebGLRenderer({ antialias: true }); + container.appendChild(this.renderer.domElement); + + this.sceneBundle = createScene(this.renderer, { + backgroundColor: isDarkMode ? 0x1e1e1e : 0xfafafa, + enableFog: false, + }); + + // Shared geometry from base mesh + const faces = this.baseMesh.faces; + const sharedGeometry = new THREE.BufferGeometry(); + sharedGeometry.setAttribute("position", new THREE.BufferAttribute(points.data, 3)); + sharedGeometry.setIndex(new THREE.BufferAttribute( + new Uint32Array(faces.data.buffer, faces.data.byteOffset, faces.data.length), 1, + )); + sharedGeometry.computeVertexNormals(); + sharedGeometry.computeBoundingSphere(); + + // Matcap URL + const matcapUrl = isDarkMode + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + + // Create 2 Three.js meshes + for (let idx = 0; idx < 2; idx++) { + const material = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + color: new THREE.Color(0.8, 0.8, 0.8), + }); + this.materials.push(material); + const mesh = new THREE.Mesh(sharedGeometry, material); + mesh.matrixAutoUpdate = false; + this.syncThreeMatrix(idx, mesh); + this.sceneBundle.scene.add(mesh); + this.threeMeshes.push(mesh); + } + + // Load matcap texture + new THREE.TextureLoader().load(matcapUrl, (tex) => { + for (let i = 0; i < this.materials.length; i++) { + this.materials[i].matcap = i === 0 ? tex : tex.clone(); + this.materials[i].needsUpdate = true; + } + }); + + // Closest points visuals + this.setupClosestPointsVisuals(); + + // Compute initial closest points + this.computeClosestPoints(); + + // Fit camera + fitCameraToAllMeshesFromZPlane(this.sceneBundle, 1.5); + + // Pointer events + const onPointerMove = (e: PointerEvent) => { + if (!container.contains(e.target as Node)) return; + this.updateNDC(e); + if (this.dragging && this.selectedId !== null) { + this.handleDrag(); + } else if (!this.dragging && !this.animating) { + this.handleHover(); + } + }; + const onPointerDown = (e: PointerEvent) => { + if (!container.contains(e.target as Node)) return; + if (this.animating) return; + this.updateNDC(e); + this.handleHover(); + if (this.selectedId !== null) { + this.dragging = true; + this.sceneBundle.controls.enabled = false; + } + }; + const onPointerUp = () => { + if (this.animating) return; + if (this.dragging && this.selectedId !== null && this.hasClosestPoints) { + this.startSnapAnimation(); + } else { + this.dragging = false; + this.selectedId = null; + this.sceneBundle.controls.enabled = true; + } + }; + + container.addEventListener("pointermove", onPointerMove); + container.addEventListener("pointerdown", onPointerDown); + window.addEventListener("pointerup", onPointerUp); + this.cleanups.push(() => { + container.removeEventListener("pointermove", onPointerMove); + container.removeEventListener("pointerdown", onPointerDown); + window.removeEventListener("pointerup", onPointerUp); + }); + + // Resize + const resizeObs = new ResizeObserver(() => this.resize()); + resizeObs.observe(container); + this.cleanups.push(() => resizeObs.disconnect()); + this.resize(); + + // Animate + this.animate(); + } + + // ========== Closest points visuals ========== + + private setupClosestPointsVisuals() { + this.closestGroup = new THREE.Group(); + + const sphereGeom = new THREE.SphereGeometry(1, 16, 12); + const sphereMat = new THREE.MeshStandardMaterial({ + color: 0x00d5be, + roughness: 0.3, + metalness: 0.1, + }); + const cylGeom = new THREE.CylinderGeometry(1, 1, 1, 8); + const cylMat = new THREE.MeshStandardMaterial({ + color: 0x00a89a, + roughness: 0.3, + metalness: 0.1, + }); + + this.sphere1 = new THREE.Mesh(sphereGeom, sphereMat); + this.sphere2 = new THREE.Mesh(sphereGeom.clone(), sphereMat.clone()); + this.connector = new THREE.Mesh(cylGeom, cylMat); + + this.closestGroup.add(this.sphere1); + this.closestGroup.add(this.sphere2); + this.closestGroup.add(this.connector); + this.closestGroup.visible = false; + this.sceneBundle.scene.add(this.closestGroup); + } + + private updateClosestVisuals() { + if (!this.hasClosestPoints) { + this.closestGroup.visible = false; + return; + } + + const sphereRadius = this.aabbDiag * 0.015; + const cylRadius = this.aabbDiag * 0.0075; + const dist = this.closestPtSelected.distanceTo(this.closestPtOther); + + this.sphere1.position.copy(this.closestPtSelected); + this.sphere1.scale.setScalar(sphereRadius); + + this.sphere2.position.copy(this.closestPtOther); + this.sphere2.scale.setScalar(sphereRadius); + + if (dist > 0.001) { + const mid = new THREE.Vector3().addVectors(this.closestPtSelected, this.closestPtOther).multiplyScalar(0.5); + this.connector.position.copy(mid); + const direction = new THREE.Vector3().subVectors(this.closestPtOther, this.closestPtSelected).normalize(); + const quaternion = new THREE.Quaternion(); + quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction); + this.connector.quaternion.copy(quaternion); + this.connector.scale.set(cylRadius, dist, cylRadius); + this.connector.visible = true; + } else { + this.connector.visible = false; + } + + this.closestGroup.visible = true; + } + + // ========== Closest points computation ========== + + private computeClosestPoints(draggedId = 0) { + const tf = this.tf; + const otherId = draggedId === 0 ? 1 : 0; + const t0 = performance.now(); + if (tf.intersects(this.tfMeshes[draggedId], this.tfMeshes[otherId])) { + this.hasClosestPoints = false; + } else { + // neighborSearch(dragged, other) → point0 on dragged, point1 on other + const result = tf.neighborSearch(this.tfMeshes[draggedId], this.tfMeshes[otherId]); + this.closestPtSelected.fromArray(result.point0.data as Float32Array); + this.closestPtOther.fromArray(result.point1.data as Float32Array); + result.point0.delete(); + result.point1.delete(); + this.hasClosestPoints = true; + } + this.timing.add(performance.now() - t0); + this.updateClosestVisuals(); + if (this.refreshTimeValue) this.refreshTimeValue(); + } + + // ========== Pointer interaction ========== + + private updateNDC(e: PointerEvent) { + const rect = this.container.getBoundingClientRect(); + this.ndc.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + this.ndc.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + } + + private handleHover() { + const tf = this.tf; + this.raycaster.setFromCamera(this.ndc, this.sceneBundle.camera); + const o = this.raycaster.ray.origin; + const d = this.raycaster.ray.direction; + const tfRay = tf.ray([o.x, o.y, o.z, d.x, d.y, d.z]); + + const hit = pickMesh(tf, tfRay, this.tfMeshes); + tfRay.delete(); + + if (hit) { + this.selectedId = hit.index; + const hitPoint = this.raycaster.ray.at(hit.t, new THREE.Vector3()); + const camDir = new THREE.Vector3(); + this.sceneBundle.camera.getWorldDirection(camDir); + this.movingPlane.setFromNormalAndCoplanarPoint(camDir, hitPoint); + this.lastPoint.copy(hitPoint); + } else { + this.selectedId = null; + } + } + + private handleDrag() { + const id = this.selectedId!; + + this.raycaster.setFromCamera(this.ndc, this.sceneBundle.camera); + const nextPoint = new THREE.Vector3(); + this.raycaster.ray.intersectPlane(this.movingPlane, nextPoint); + if (!nextPoint) return; + + const dx = nextPoint.x - this.lastPoint.x; + const dy = nextPoint.y - this.lastPoint.y; + const dz = nextPoint.z - this.lastPoint.z; + this.lastPoint.copy(nextPoint); + + // Update tf mesh transformation + const mat = this.tfMeshes[id].transformation; + const d = mat.data; + d[3] += dx; d[7] += dy; d[11] += dz; + this.tfMeshes[id].transformation = mat; + mat.delete(); + + this.syncThreeMatrix(id); + this.computeClosestPoints(id); + } + + // ========== Snap animation ========== + + private startSnapAnimation() { + this.animSelectedId = this.selectedId!; + const origin = this.closestPtSelected.clone(); + const dir = new THREE.Vector3().subVectors(this.closestPtOther, this.closestPtSelected); + this.animRay = { origin, dir }; + + // Animate camera focal point toward the other mesh's closest point + const camTarget = (this.sceneBundle.controls as any).target.clone(); + this.animFocalRay = { + origin: camTarget, + dir: new THREE.Vector3().subVectors(this.closestPtOther, camTarget), + }; + + this.animPrev.copy(origin); + this.animStart = performance.now(); + this.animating = true; + this.dragging = false; + this.selectedId = null; + this.sceneBundle.controls.enabled = false; + } + + private stepAnimation() { + if (!this.animRay) return; + + const elapsed = (performance.now() - this.animStart) / 1000; + const t = Math.min(1, 1.75 * elapsed); + const s = 3 * t * t - 2 * t * t * t; // smooth-step + + const pt = this.animRay.origin.clone().addScaledVector(this.animRay.dir, s); + const delta = pt.clone().sub(this.animPrev); + + // Move tf mesh + const mat = this.tfMeshes[this.animSelectedId].transformation; + const d = mat.data; + d[3] += delta.x; d[7] += delta.y; d[11] += delta.z; + this.tfMeshes[this.animSelectedId].transformation = mat; + mat.delete(); + this.syncThreeMatrix(this.animSelectedId); + this.animPrev.copy(pt); + + // Update closest point visualization (shrinking line) + this.closestPtSelected.copy(pt); + this.updateClosestVisuals(); + + // Animate focal point + if (this.animFocalRay) { + const focal = this.animFocalRay.origin.clone().addScaledVector(this.animFocalRay.dir, s); + (this.sceneBundle.controls as any).target.copy(focal); + } + + if (t >= 1) { + this.animating = false; + this.animRay = null; + this.animFocalRay = null; + this.hasClosestPoints = false; + this.updateClosestVisuals(); + this.sceneBundle.controls.enabled = true; + } + } + + // ========== Helpers ========== + + private syncThreeMatrix(index: number, mesh?: THREE.Mesh) { + const target = mesh ?? this.threeMeshes[index]; + const mat = this.tfMeshes[index].transformation; + if (!mat) return; + const m = new THREE.Matrix4(); + m.fromArray(mat.data); + m.transpose(); + target.matrix.copy(m); + mat.delete(); + } + + // ========== Public API ========== + + public getAverageTime(): number { + return this.timing.average; + } + + public randomize() { + const tf = this.tf; + for (let i = 0; i < this.tfMeshes.length; i++) { + // Keep current translation, re-roll rotation + const oldMat = this.tfMeshes[i].transformation; + const tx = oldMat.data[3]; + const ty = oldMat.data[7]; + const tz = oldMat.data[11]; + oldMat.delete(); + + const newMat = randomTransformation(tf, tx, ty, tz); + this.tfMeshes[i].transformation = newMat; + newMat.delete(); + this.syncThreeMatrix(i); + } + this.computeClosestPoints(); + } + + public applyTheme(isDark: boolean) { + this.sceneBundle.scene.background = new THREE.Color(isDark ? 0x1e1e1e : 0xfafafa); + const matcapUrl = isDark + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + new THREE.TextureLoader().load(matcapUrl, (tex) => { + for (let i = 0; i < this.materials.length; i++) { + if (this.materials[i].matcap) this.materials[i].matcap!.dispose(); + this.materials[i].matcap = i === 0 ? tex : tex.clone(); + this.materials[i].needsUpdate = true; + } + }); + } + + private resize() { + const w = this.container.clientWidth; + const h = this.container.clientHeight; + this.renderer.setSize(w, h); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.sceneBundle.camera.aspect = w / h; + this.sceneBundle.camera.updateProjectionMatrix(); + } + + private animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + if (this.animating) { + this.stepAnimation(); + } + this.sceneBundle.controls.update(); + this.renderer.render(this.sceneBundle.scene, this.sceneBundle.camera); + } + + public dispose() { + this.running = false; + for (const fn of this.cleanups) fn(); + this.cleanups = []; + this.sceneBundle.controls.dispose(); + + // Dispose shared geometry (only once) + if (this.threeMeshes.length > 0) { + this.threeMeshes[0].geometry.dispose(); + } + for (const mesh of this.threeMeshes) { + (mesh.material as THREE.Material).dispose(); + } + + // Dispose closest points visuals + this.sphere1.geometry.dispose(); + (this.sphere1.material as THREE.Material).dispose(); + this.sphere2.geometry.dispose(); + (this.sphere2.material as THREE.Material).dispose(); + this.connector.geometry.dispose(); + (this.connector.material as THREE.Material).dispose(); + + this.renderer.dispose(); + this.renderer.domElement.remove(); + + // Delete tf meshes (sharedView first, then baseMesh) + for (let i = this.tfMeshes.length - 1; i >= 0; i--) { + this.tfMeshes[i].delete(); + } + this.tfMeshes = []; + } +} diff --git a/docs/app/examples/CollisionExample.ts b/docs/app/examples/CollisionExample.ts index 686a037..6c8d791 100644 --- a/docs/app/examples/CollisionExample.ts +++ b/docs/app/examples/CollisionExample.ts @@ -1,19 +1,302 @@ -import type { MainModule } from "@/examples/native"; -import { ThreejsBase } from "@/examples/ThreejsBase"; +import { fitCameraToAllMeshesFromZPlane, createScene, type SceneBundle } from "@/utils/sceneUtils"; +import { centerAndScale, pickMesh, randomTransformation, RollingAverage } from "@/utils/utils"; +import * as THREE from "three"; -export class CollisionExample extends ThreejsBase { - constructor(wasmInstance: MainModule, paths: string[], container: HTMLElement, isDarkMode = true) { - super(wasmInstance, paths, container, undefined, false, false, isDarkMode); +type TF = typeof import("@/examples/trueform/index.js"); + +export class CollisionExample { + private tf: TF; + private baseMesh: any; + private tfMeshes: any[] = []; + private container: HTMLElement; + private renderer: THREE.WebGLRenderer; + private sceneBundle: SceneBundle; + private threeMeshes: THREE.Mesh[] = []; + private running = true; + private cleanups: (() => void)[] = []; + + private selectedId: number | null = null; + private dragging = false; + private movingPlane = new THREE.Plane(); + private lastPoint = new THREE.Vector3(); + private colliding = new Set(); + private pickTiming = new RollingAverage(); + private collideTiming = new RollingAverage(); + + private raycaster = new THREE.Raycaster(); + private ndc = new THREE.Vector2(); + + private normalColor = new THREE.Color(0.8, 0.8, 0.8); + private collidingColor = new THREE.Color(0.7, 1, 1); + + public refreshTimeValue: (() => number) | null = null; + + constructor(tf: TF, fileBuffer: ArrayBuffer, fileName: string, container: HTMLElement, isDarkMode = true) { + this.tf = tf; + this.container = container; + + // Load mesh + const ext = fileName.split(".").pop()?.toLowerCase(); + this.baseMesh = ext === "stl" ? tf.readStl(fileBuffer) : tf.readObj(fileBuffer); + centerAndScale(tf, this.baseMesh); + + // Create 5x5 grid of meshes: baseMesh for first slot, sharedView() for the rest + const gridSize = 5; + const spacing = 15; + for (let j = 0; j < gridSize; j++) { + for (let i = 0; i < gridSize; i++) { + const tfMesh = (i === 0 && j === 0) ? this.baseMesh : this.baseMesh.sharedView(); + const mat = randomTransformation(tf, i * spacing, j * spacing, 0); + tfMesh.transformation = mat; + mat.delete(); + this.tfMeshes.push(tfMesh); + } + } + + // Three.js setup + this.renderer = new THREE.WebGLRenderer({ antialias: true }); + container.appendChild(this.renderer.domElement); + + this.sceneBundle = createScene(this.renderer, { + backgroundColor: isDarkMode ? 0x1e1e1e : 0xfafafa, + enableFog: false, + }); + + // Shared geometry from base mesh points/faces + const points = this.baseMesh.points; + const faces = this.baseMesh.faces; + const sharedGeometry = new THREE.BufferGeometry(); + sharedGeometry.setAttribute("position", new THREE.BufferAttribute(points.data, 3)); + sharedGeometry.setIndex(new THREE.BufferAttribute( + new Uint32Array(faces.data.buffer, faces.data.byteOffset, faces.data.length), 1, + )); + sharedGeometry.computeVertexNormals(); + sharedGeometry.computeBoundingSphere(); + + // Matcap URL + const matcapUrl = isDarkMode + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + + // Create 25 Three.js meshes, each with own material (for independent color), sharing geometry + const materials: THREE.MeshMatcapMaterial[] = []; + for (let idx = 0; idx < this.tfMeshes.length; idx++) { + const material = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + color: this.normalColor.clone(), + }); + materials.push(material); + const mesh = new THREE.Mesh(sharedGeometry, material); + mesh.matrixAutoUpdate = false; + this.syncThreeMatrix(idx, mesh); + this.sceneBundle.scene.add(mesh); + this.threeMeshes.push(mesh); + } + + // Load matcap texture and assign to all materials + new THREE.TextureLoader().load(matcapUrl, (tex) => { + for (let i = 0; i < materials.length; i++) { + materials[i].matcap = i === 0 ? tex : tex.clone(); + materials[i].needsUpdate = true; + } + }); + + // Fit camera + fitCameraToAllMeshesFromZPlane(this.sceneBundle, 1.5); + + // Pointer events + const onPointerMove = (e: PointerEvent) => { + if (!container.contains(e.target as Node)) return; + this.updateNDC(e); + if (this.dragging && this.selectedId !== null) { + this.handleDrag(); + } else if (!this.dragging) { + this.handleHover(); + } + }; + const onPointerDown = (e: PointerEvent) => { + if (!container.contains(e.target as Node)) return; + this.updateNDC(e); + this.handleHover(); // pick on click + if (this.selectedId !== null) { + this.dragging = true; + this.sceneBundle.controls.enabled = false; + } + }; + const onPointerUp = () => { + this.dragging = false; + this.selectedId = null; + this.sceneBundle.controls.enabled = true; + this.colliding.clear(); + this.updateColors(); + }; + + container.addEventListener("pointermove", onPointerMove); + container.addEventListener("pointerdown", onPointerDown); + window.addEventListener("pointerup", onPointerUp); + this.cleanups.push(() => { + container.removeEventListener("pointermove", onPointerMove); + container.removeEventListener("pointerdown", onPointerDown); + window.removeEventListener("pointerup", onPointerUp); + }); + + // Resize + const resizeObs = new ResizeObserver(() => this.resize()); + resizeObs.observe(container); + this.cleanups.push(() => resizeObs.disconnect()); + this.resize(); + + // Animate + this.animate(); + } + + private updateNDC(e: PointerEvent) { + const rect = this.container.getBoundingClientRect(); + this.ndc.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + this.ndc.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + } + + private handleHover() { + const tf = this.tf; + this.raycaster.setFromCamera(this.ndc, this.sceneBundle.camera); + const o = this.raycaster.ray.origin; + const d = this.raycaster.ray.direction; + const tfRay = tf.ray([o.x, o.y, o.z, d.x, d.y, d.z]); + + const t0 = performance.now(); + const hit = pickMesh(tf, tfRay, this.tfMeshes); + this.pickTiming.add(performance.now() - t0); + tfRay.delete(); + + if (hit) { + this.selectedId = hit.index; + const hitPoint = this.raycaster.ray.at(hit.t, new THREE.Vector3()); + const camDir = new THREE.Vector3(); + this.sceneBundle.camera.getWorldDirection(camDir); + this.movingPlane.setFromNormalAndCoplanarPoint(camDir, hitPoint); + this.lastPoint.copy(hitPoint); + } else { + this.selectedId = null; + } + + if (this.refreshTimeValue) this.refreshTimeValue(); + } + + private handleDrag() { + const tf = this.tf; + const id = this.selectedId!; + + this.raycaster.setFromCamera(this.ndc, this.sceneBundle.camera); + const nextPoint = new THREE.Vector3(); + this.raycaster.ray.intersectPlane(this.movingPlane, nextPoint); + if (!nextPoint) return; + + const dx = nextPoint.x - this.lastPoint.x; + const dy = nextPoint.y - this.lastPoint.y; + const dz = nextPoint.z - this.lastPoint.z; + this.lastPoint.copy(nextPoint); + + // Update tf mesh transformation: read, modify translation in-place, write back + const mat = this.tfMeshes[id].transformation; + const d = mat.data; // mutable WASM heap view + d[3] += dx; d[7] += dy; d[11] += dz; + this.tfMeshes[id].transformation = mat; + mat.delete(); + + // Sync Three.js matrix + this.syncThreeMatrix(id); + + // Collision detection + const t0 = performance.now(); + this.colliding.clear(); + for (let i = 0; i < this.tfMeshes.length; i++) { + if (i === id) continue; + if (tf.intersects(this.tfMeshes[id], this.tfMeshes[i])) { + this.colliding.add(i); + } + } + this.collideTiming.add(performance.now() - t0); + + this.updateColors(); + if (this.refreshTimeValue) this.refreshTimeValue(); } - runMain() { - const v = new this.wasmInstance.VectorString(); - for (const path of this.paths) { - v.push_back(path); + private syncThreeMatrix(index: number, mesh?: THREE.Mesh) { + const target = mesh ?? this.threeMeshes[index]; + const mat = this.tfMeshes[index].transformation; + if (!mat) return; + const m = new THREE.Matrix4(); + m.fromArray(mat.data); + m.transpose(); // row-major → column-major + target.matrix.copy(m); + mat.delete(); + } + + private updateColors() { + for (let i = 0; i < this.threeMeshes.length; i++) { + const mat = this.threeMeshes[i].material as THREE.MeshMatcapMaterial; + mat.color.copy(this.colliding.has(i) ? this.collidingColor : this.normalColor); + } + } + + public getAverageTime(): number { + return this.collideTiming.average; + } + + public getAveragePickTime(): number { + return this.pickTiming.average; + } + + public applyTheme(isDark: boolean) { + this.sceneBundle.scene.background = new THREE.Color(isDark ? 0x1e1e1e : 0xfafafa); + const matcapUrl = isDark + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + new THREE.TextureLoader().load(matcapUrl, (tex) => { + for (let i = 0; i < this.threeMeshes.length; i++) { + const mat = this.threeMeshes[i].material as THREE.MeshMatcapMaterial; + if (mat.matcap) mat.matcap.dispose(); + mat.matcap = i === 0 ? tex : tex.clone(); + mat.needsUpdate = true; + } + }); + } + + private resize() { + const w = this.container.clientWidth; + const h = this.container.clientHeight; + this.renderer.setSize(w, h); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.sceneBundle.camera.aspect = w / h; + this.sceneBundle.camera.updateProjectionMatrix(); + } + + private animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + this.sceneBundle.controls.update(); + this.renderer.render(this.sceneBundle.scene, this.sceneBundle.camera); + } + + public dispose() { + this.running = false; + for (const fn of this.cleanups) fn(); + this.cleanups = []; + this.sceneBundle.controls.dispose(); + // Dispose shared geometry (only once) + if (this.threeMeshes.length > 0) { + this.threeMeshes[0].geometry.dispose(); + } + for (const mesh of this.threeMeshes) { + (mesh.material as THREE.Material).dispose(); } - this.wasmInstance.run_main_collisions(v); - for (const path of this.paths) { - this.wasmInstance.FS.unlink(path); + this.renderer.dispose(); + this.renderer.domElement.remove(); + // Delete tf meshes (sharedViews first, then baseMesh) + for (let i = this.tfMeshes.length - 1; i >= 0; i--) { + this.tfMeshes[i].delete(); } + this.tfMeshes = []; } } diff --git a/docs/app/examples/CrossSectionExample.ts b/docs/app/examples/CrossSectionExample.ts index 447ffe5..e0fc191 100644 --- a/docs/app/examples/CrossSectionExample.ts +++ b/docs/app/examples/CrossSectionExample.ts @@ -1,193 +1,289 @@ -import type { MainModule } from "@/examples/native"; -import { fitCameraToAllMeshesFromZPlane } from "@/utils/sceneUtils"; -import { - buffersToCurves, - createMesh, - updateResultMesh, - CurveRenderer, -} from "@/utils/utils"; -import { ThreejsBase } from "@/examples/ThreejsBase"; +import { fitCameraToAllMeshesFromZPlane, createScene, type SceneBundle } from "@/utils/sceneUtils"; +import { centerAndScale, CurveRenderer, RollingAverage } from "@/utils/utils"; import * as THREE from "three"; -export class CrossSectionExample extends ThreejsBase { +type TF = typeof import("@/examples/trueform/index.js"); + +export class CrossSectionExample { + private tf: TF; + private mesh: any; + private container: HTMLElement; + private renderer: THREE.WebGLRenderer; + private sceneBundle: SceneBundle; private curveRenderer: CurveRenderer; + private baseMesh: THREE.Mesh; private crossSectionMesh: THREE.Mesh; - private keyPressed = false; + private running = true; + private cleanups: (() => void)[] = []; - public randomize() { - this.wasmInstance.OnKeyPress("n"); - this.updateMeshes(); - } + // Scalar field (NDArray [N], persists across scroll events) + private scalarsND: any = null; + private scalarMin = 0; + private scalarMax = 1; + private planeOffset = 0; + private normalVec: any = null; + private timing = new RollingAverage(); + + public refreshTimeValue: (() => number) | null = null; + + constructor(tf: TF, fileBuffer: ArrayBuffer, fileName: string, container: HTMLElement, isDarkMode = true) { + this.tf = tf; + this.container = container; + + // Load mesh + const ext = fileName.split(".").pop()?.toLowerCase(); + this.mesh = ext === "stl" ? tf.readStl(fileBuffer) : tf.readObj(fileBuffer); + + // Center and scale mesh (AABB center at origin, diagonal/2 = 10) + centerAndScale(this.tf, this.mesh); - constructor( - wasmInstance: MainModule, - paths: string[], - container: HTMLElement, - isDarkMode = true, - ) { - // Single viewport - no second container - super(wasmInstance, paths, container, undefined, true, false, isDarkMode); - this.sceneBundle1.controls.enableZoom = false; - - // Make the original mesh semi-transparent - this.instancedMeshes.forEach((instancedMesh) => { - const material = instancedMesh.material as THREE.MeshMatcapMaterial; - material.transparent = true; - material.opacity = 0.25; - material.depthWrite = false; + // Initial normal [1, 2, 1] normalized + this.normalVec = tf.normalize(tf.vector(1, 2, 1)); + + // Compute initial scalar field + this.computeScalars(); + + // Three.js setup + this.renderer = new THREE.WebGLRenderer({ antialias: true }); + container.appendChild(this.renderer.domElement); + + this.sceneBundle = createScene(this.renderer, { + backgroundColor: isDarkMode ? 0x1e1e1e : 0xfafafa, + enableFog: false, }); - const interceptKeyDownEvent = (event: KeyboardEvent) => { - if (this.keyPressed) return; - this.keyPressed = true; - if (event.key === "n") { - this.randomize(); - return; - } - this.wasmInstance.OnKeyPress(event.key); - this.updateMeshes(); - }; - const interceptKeyUpEvent = (_event: KeyboardEvent) => { - this.keyPressed = false; - }; - window.addEventListener("keydown", interceptKeyDownEvent); - window.addEventListener("keyup", interceptKeyUpEvent); - - const isEventFromCanvas = (eventTarget: EventTarget | null) => { - if (!eventTarget) return false; - const target = eventTarget as Node; - const container1 = this.renderer.domElement.parentElement; - return !!container1 && container1.contains(target); + // Base mesh (semi-transparent) + const points = this.mesh.points; + const faces = this.mesh.faces; + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(points.data, 3)); + geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(faces.data.buffer, faces.data.byteOffset, faces.data.length), 1)); + geometry.computeVertexNormals(); + geometry.computeBoundingSphere(); + + const matcapUrl = isDarkMode + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + const baseMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + transparent: true, + opacity: 0.25, + depthWrite: false, + }); + this.baseMesh = new THREE.Mesh(geometry, baseMaterial); + this.baseMesh.matrixAutoUpdate = false; + this.sceneBundle.scene.add(this.baseMesh); + + // Cross-section cap mesh + const capMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + color: new THREE.Color(0x00a89a), + }); + this.crossSectionMesh = new THREE.Mesh(new THREE.BufferGeometry(), capMaterial); + this.crossSectionMesh.matrixAutoUpdate = false; + this.sceneBundle.scene.add(this.crossSectionMesh); + + // Load matcap texture + new THREE.TextureLoader().load(matcapUrl, (tex) => { + baseMaterial.matcap = tex; + baseMaterial.needsUpdate = true; + capMaterial.matcap = tex.clone(); + capMaterial.needsUpdate = true; + }); + + // Curve renderer + this.curveRenderer = new CurveRenderer({ color: 0x00d5be, radius: 0.075, maxSegments: 20000 }); + this.sceneBundle.scene.add(this.curveRenderer.object); + + // Compute initial cross-section + this.recomputeCrossSection(); + + // Fit camera + fitCameraToAllMeshesFromZPlane(this.sceneBundle, 1.5); + this.sceneBundle.controls.enableZoom = false; + + // Scroll interaction (matches WASM cross_section_web.h:104) + const range = () => this.scalarMax - this.scalarMin; + const margin = () => range() * 0.01; + const scrollStep = (delta: number) => { + this.planeOffset += delta * 0.003 * range(); + this.planeOffset = Math.max( + this.scalarMin + margin(), + Math.min(this.scalarMax - margin(), this.planeOffset), + ); + this.recomputeCrossSection(); }; - const interceptWheelEvent = (event: WheelEvent) => { - if (!isEventFromCanvas(event.target)) return; + const onWheel = (event: WheelEvent) => { + if (!container.contains(event.target as Node)) return; + event.preventDefault(); const delta = event.deltaY !== 0 ? event.deltaY : event.deltaX; if (delta === 0) return; - event.preventDefault(); - const normalizedDelta = delta / Math.abs(delta); - const handled = this.wasmInstance.OnMouseWheel(normalizedDelta, true); - this.updateMeshes(); - if (handled) event.stopImmediatePropagation(); - }; - const wheelListenerOptions = { - passive: false, - capture: true, + scrollStep(Math.sign(delta)); }; - window.addEventListener("wheel", interceptWheelEvent, wheelListenerOptions); + window.addEventListener("wheel", onWheel, { passive: false, capture: true }); + this.cleanups.push(() => window.removeEventListener("wheel", onWheel, { passive: false, capture: true } as any)); - let touchScrollActive = false; + // Touch scroll + let touchActive = false; let lastTouchY = 0; - const setTouchScrollMode = (active: boolean) => { - touchScrollActive = active; - this.sceneBundle1.controls.enabled = !active; + const onTouchStart = (e: TouchEvent) => { + if (e.touches.length !== 1 || !container.contains(e.target as Node)) return; + e.preventDefault(); + touchActive = true; + this.sceneBundle.controls.enabled = false; + lastTouchY = e.touches[0]!.clientY; }; - const touchScrollThresholdPx = 10; - const getAverageTouchY = (touches: TouchList) => { - let sum = 0; - for (let i = 0; i < touches.length; i++) { - sum += touches[i]!.clientY; - } - return sum / touches.length; - }; - - const interceptTouchStart = (event: TouchEvent) => { - if (event.touches.length !== 1) return; - if (!isEventFromCanvas(event.target)) return; - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - setTouchScrollMode(true); - lastTouchY = getAverageTouchY(event.touches); + const onTouchMove = (e: TouchEvent) => { + if (!touchActive || e.touches.length !== 1) { touchActive = false; this.sceneBundle.controls.enabled = true; return; } + e.preventDefault(); + const dy = e.touches[0]!.clientY - lastTouchY; + if (Math.abs(dy) < 10) return; + scrollStep(Math.sign(dy)); + lastTouchY = e.touches[0]!.clientY; }; + const onTouchEnd = () => { touchActive = false; this.sceneBundle.controls.enabled = true; }; + const touchOpts = { passive: false, capture: true }; + window.addEventListener("touchstart", onTouchStart, touchOpts); + window.addEventListener("touchmove", onTouchMove, touchOpts); + window.addEventListener("touchend", onTouchEnd, touchOpts); + window.addEventListener("touchcancel", onTouchEnd, touchOpts); + this.cleanups.push(() => { + window.removeEventListener("touchstart", onTouchStart, touchOpts as any); + window.removeEventListener("touchmove", onTouchMove, touchOpts as any); + window.removeEventListener("touchend", onTouchEnd, touchOpts as any); + window.removeEventListener("touchcancel", onTouchEnd, touchOpts as any); + }); - const interceptTouchMove = (event: TouchEvent) => { - if (!touchScrollActive) return; - if (event.touches.length !== 1) { - setTouchScrollMode(false); - return; - } - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - const currentY = getAverageTouchY(event.touches); - const deltaY = currentY - lastTouchY; - if (Math.abs(deltaY) < touchScrollThresholdPx) return; - const normalizedDelta = deltaY / Math.abs(deltaY); - const handled = this.wasmInstance.OnMouseWheel(normalizedDelta, true); - this.updateMeshes(); - if (handled) { - event.stopImmediatePropagation(); - } - lastTouchY = currentY; - }; + // Resize + const resizeObs = new ResizeObserver(() => this.resize()); + resizeObs.observe(container); + this.cleanups.push(() => resizeObs.disconnect()); + this.resize(); - const interceptTouchEnd = (event: TouchEvent) => { - if (touchScrollActive) { - setTouchScrollMode(false); - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - } - }; + // Animate + this.animate(); + } - const touchListenerOptions = { - passive: false, - capture: true, - }; - window.addEventListener("touchstart", interceptTouchStart, touchListenerOptions); - window.addEventListener("touchmove", interceptTouchMove, touchListenerOptions); - window.addEventListener("touchend", interceptTouchEnd, touchListenerOptions); - window.addEventListener("touchcancel", interceptTouchEnd, touchListenerOptions); - - this.addCleanup(() => { - window.removeEventListener("keydown", interceptKeyDownEvent); - window.removeEventListener("keyup", interceptKeyUpEvent); - window.removeEventListener("wheel", interceptWheelEvent, wheelListenerOptions); - window.removeEventListener("touchstart", interceptTouchStart, touchListenerOptions); - window.removeEventListener("touchmove", interceptTouchMove, touchListenerOptions); - window.removeEventListener("touchend", interceptTouchEnd, touchListenerOptions); - window.removeEventListener("touchcancel", interceptTouchEnd, touchListenerOptions); - }); + private computeScalars() { + const tf = this.tf; + if (this.scalarsND) { this.scalarsND.delete(); this.scalarsND = null; } - this.curveRenderer = new CurveRenderer({ - color: 0x00d5be, - radius: 0.075, - maxSegments: 20000, - }); - this.sceneBundle1.scene.add(this.curveRenderer.object); + const points = this.mesh.points; + const centroid = tf.mean(points, 0) as any; + const d = -(tf.dot(this.normalVec, centroid) as number); + const p = tf.plane(this.normalVec, d); + const pts = tf.point(points); - this.crossSectionMesh = createMesh(this.isDarkMode); - const crossSectionMaterial = this.crossSectionMesh.material as THREE.MeshMatcapMaterial; - crossSectionMaterial.color = new THREE.Color(0x00a89a); - this.sceneBundle1.scene.add(this.crossSectionMesh); + this.scalarsND = tf.distance(pts, p); + this.scalarMin = this.scalarsND.min() as number; + this.scalarMax = this.scalarsND.max() as number; + this.planeOffset = (this.scalarMin + this.scalarMax) * 0.5; - this.updateMeshes(); - fitCameraToAllMeshesFromZPlane(this.sceneBundle1, 1.5); + centroid.delete(); } - public override updateMeshes() { - super.updateMeshes(); - - // Update curves - const curveOutput = this.wasmInstance.get_curve_mesh(); - if (curveOutput && curveOutput.updated) { - const points = curveOutput.get_curve_points(); - const ids = curveOutput.get_curve_ids(); - const offsets = curveOutput.get_curve_offsets(); - const curves = buffersToCurves(points, ids, offsets); - this.curveRenderer.update(curves); + private recomputeCrossSection() { + const t0 = performance.now(); + const tf = this.tf; + + // 1. Isocontours at the cut value → boundary curves + const curves = tf.isocontours(this.mesh, this.scalarsND, this.planeOffset); + + // 2. Triangulate curve paths as polygons → filled cap mesh + const cap = tf.triangulate({ faces: curves.paths, points: curves.points }); + + this.timing.add(performance.now() - t0); + + // 3. Update cap mesh geometry + const capPoints = cap.points; + const capFaces = cap.faces; + const geom = this.crossSectionMesh.geometry; + geom.setAttribute("position", new THREE.BufferAttribute(capPoints.data, 3)); + geom.setIndex(new THREE.BufferAttribute( + new Uint32Array(capFaces.data.buffer, capFaces.data.byteOffset, capFaces.data.length), 1, + )); + if (capPoints.data.length >= 3) { + geom.computeBoundingSphere(); + geom.computeBoundingBox(); } - // Update filled cross-section mesh - const resultMesh = this.wasmInstance.get_result_mesh(); - if (resultMesh) { - updateResultMesh(resultMesh, this.crossSectionMesh); + // 4. Update curves for tube rendering + const curvePts = curves.points.data as Float32Array; + const paths: number[][] = []; + for (const p of curves.paths) { + paths.push(Array.from(p.data)); + p.delete(); } + this.curveRenderer.update({ points: curvePts, paths }); + + // Cleanup + cap.delete(); + curves.delete(); + if (this.refreshTimeValue) this.refreshTimeValue(); + } + + public randomize() { + const tf = this.tf; + this.normalVec.delete(); + this.normalVec = tf.normalize(tf.vector(tf.random("float32", [3], -1, 1))); + this.computeScalars(); + this.recomputeCrossSection(); + } + + public getAverageTime(): number { + return this.timing.average; + } + + public applyTheme(isDark: boolean) { + this.sceneBundle.scene.background = new THREE.Color(isDark ? 0x1e1e1e : 0xfafafa); + + const matcapUrl = isDark + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + const newTex = new THREE.TextureLoader().load(matcapUrl); + + const baseMat = this.baseMesh.material as THREE.MeshMatcapMaterial; + if (baseMat.matcap) baseMat.matcap.dispose(); + baseMat.matcap = newTex; + + const capMat = this.crossSectionMesh.material as THREE.MeshMatcapMaterial; + if (capMat.matcap) capMat.matcap.dispose(); + capMat.matcap = newTex.clone(); + } + + private resize() { + const w = this.container.clientWidth; + const h = this.container.clientHeight; + this.renderer.setSize(w, h); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.sceneBundle.camera.aspect = w / h; + this.sceneBundle.camera.updateProjectionMatrix(); + } + + private animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + this.sceneBundle.controls.update(); + this.renderer.render(this.sceneBundle.scene, this.sceneBundle.camera); } - public runMain() { - this.wasmInstance.run_main_cross_section(this.paths[0]!); - this.wasmInstance.FS.unlink(this.paths[0]); + public dispose() { + this.running = false; + for (const fn of this.cleanups) fn(); + this.cleanups = []; + this.sceneBundle.controls.dispose(); + this.baseMesh.geometry.dispose(); + (this.baseMesh.material as THREE.Material).dispose(); + this.crossSectionMesh.geometry.dispose(); + (this.crossSectionMesh.material as THREE.Material).dispose(); + this.curveRenderer.dispose(); + this.renderer.dispose(); + this.renderer.domElement.remove(); + if (this.scalarsND) { this.scalarsND.delete(); this.scalarsND = null; } + if (this.normalVec) { this.normalVec.delete(); this.normalVec = null; } + this.mesh.delete(); } } diff --git a/docs/app/examples/IsobandsExample.ts b/docs/app/examples/IsobandsExample.ts index c5ec6f9..6c3f66d 100644 --- a/docs/app/examples/IsobandsExample.ts +++ b/docs/app/examples/IsobandsExample.ts @@ -1,189 +1,310 @@ -import type { MainModule } from "@/examples/native"; -import { fitCameraToAllMeshesFromZPlane } from "@/utils/sceneUtils"; -import { - buffersToCurves, - createMesh, - updateResultMesh, - CurveRenderer, -} from "@/utils/utils"; -import { ThreejsBase } from "@/examples/ThreejsBase"; +import { fitCameraToAllMeshesFromZPlane, createScene, type SceneBundle } from "@/utils/sceneUtils"; +import { centerAndScale, CurveRenderer, RollingAverage } from "@/utils/utils"; import * as THREE from "three"; -export class IsobandsExample extends ThreejsBase { +type TF = typeof import("@/examples/trueform/index.js"); + +export class IsobandsExample { + private tf: TF; + private mesh: any; + private container: HTMLElement; + private renderer: THREE.WebGLRenderer; + private sceneBundle: SceneBundle; private curveRenderer: CurveRenderer; + private baseMesh: THREE.Mesh; private isobandsMesh: THREE.Mesh; - private keyPressed = false; + private running = true; + private cleanups: (() => void)[] = []; - public randomize() { - this.wasmInstance.OnKeyPress("n"); - this.updateMeshes(); - } + // Scalar field (NDArray [N], persists across scroll events) + private scalarsND: any = null; + private scalarMin = 0; + private scalarMax = 1; + private planeOffset = 0; + private normalVec: any = null; // Vector primitive [3] + private timing = new RollingAverage(); + + public refreshTimeValue: (() => number) | null = null; + + constructor(tf: TF, fileBuffer: ArrayBuffer, fileName: string, container: HTMLElement, isDarkMode = true) { + this.tf = tf; + this.container = container; + + // Load mesh + const ext = fileName.split(".").pop()?.toLowerCase(); + this.mesh = ext === "stl" ? tf.readStl(fileBuffer) : tf.readObj(fileBuffer); + + // Center and scale mesh (match old pipeline: AABB center at origin, diagonal/2 = 10) + centerAndScale(this.tf, this.mesh); + + // Initial normal [1, 2, 1] normalized + this.normalVec = tf.normalize(tf.vector(1, 2, 1)); - constructor( - wasmInstance: MainModule, - paths: string[], - container: HTMLElement, - isDarkMode = true, - ) { - super(wasmInstance, paths, container, undefined, true, false, isDarkMode); - this.sceneBundle1.controls.enableZoom = false; - - this.instancedMeshes.forEach((instancedMesh) => { - const material = instancedMesh.material as THREE.MeshMatcapMaterial; - material.transparent = true; - material.opacity = 0.25; - material.depthWrite = false; + // Compute initial scalar field + this.computeScalars(); + + // Three.js setup + this.renderer = new THREE.WebGLRenderer({ antialias: true }); + container.appendChild(this.renderer.domElement); + + this.sceneBundle = createScene(this.renderer, { + backgroundColor: isDarkMode ? 0x1e1e1e : 0xfafafa, + enableFog: false, }); - const interceptKeyDownEvent = (event: KeyboardEvent) => { - if (this.keyPressed) return; - this.keyPressed = true; - if (event.key === "n") { - this.randomize(); - return; - } - this.wasmInstance.OnKeyPress(event.key); - this.updateMeshes(); - }; - const interceptKeyUpEvent = (_event: KeyboardEvent) => { - this.keyPressed = false; - }; - window.addEventListener("keydown", interceptKeyDownEvent); - window.addEventListener("keyup", interceptKeyUpEvent); - - const isEventFromCanvas = (eventTarget: EventTarget | null) => { - if (!eventTarget) return false; - const target = eventTarget as Node; - const container1 = this.renderer.domElement.parentElement; - return !!container1 && container1.contains(target); + // Base mesh (semi-transparent) + const points = this.mesh.points; + const faces = this.mesh.faces; + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute("position", new THREE.BufferAttribute(points.data, 3)); + geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(faces.data.buffer, faces.data.byteOffset, faces.data.length), 1)); + geometry.computeVertexNormals(); + geometry.computeBoundingSphere(); + + const matcapUrl = isDarkMode + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + const baseMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + transparent: true, + opacity: 0.25, + depthWrite: false, + }); + this.baseMesh = new THREE.Mesh(geometry, baseMaterial); + this.baseMesh.matrixAutoUpdate = false; + this.sceneBundle.scene.add(this.baseMesh); + + // Isobands mesh + const isoMaterial = new THREE.MeshMatcapMaterial({ + side: THREE.DoubleSide, + flatShading: true, + color: new THREE.Color(0x00a89a), + }); + this.isobandsMesh = new THREE.Mesh(new THREE.BufferGeometry(), isoMaterial); + this.isobandsMesh.matrixAutoUpdate = false; + this.sceneBundle.scene.add(this.isobandsMesh); + + // Load matcap texture, assign to both materials once ready + new THREE.TextureLoader().load(matcapUrl, (tex) => { + baseMaterial.matcap = tex; + baseMaterial.needsUpdate = true; + isoMaterial.matcap = tex.clone(); + isoMaterial.needsUpdate = true; + }); + + // Curve renderer + this.curveRenderer = new CurveRenderer({ color: 0x00d5be, radius: 0.075, maxSegments: 20000 }); + this.sceneBundle.scene.add(this.curveRenderer.object); + + // Compute initial isobands + this.recomputeIsobands(); + + // Fit camera + fitCameraToAllMeshesFromZPlane(this.sceneBundle, 1.5); + this.sceneBundle.controls.enableZoom = false; + + // Scroll interaction (wraps around like old pipeline) + const range = () => this.scalarMax - this.scalarMin; + const scrollStep = (delta: number) => { + this.planeOffset += delta * 0.003 * range(); + // Wrap with fmod + let offset = (this.planeOffset - this.scalarMin) % range(); + if (offset < 0) offset += range(); + this.planeOffset = this.scalarMin + offset; + this.recomputeIsobands(); }; - const interceptWheelEvent = (event: WheelEvent) => { - if (!isEventFromCanvas(event.target)) return; + const onWheel = (event: WheelEvent) => { + if (!container.contains(event.target as Node)) return; + event.preventDefault(); const delta = event.deltaY !== 0 ? event.deltaY : event.deltaX; if (delta === 0) return; - event.preventDefault(); - const normalizedDelta = delta / Math.abs(delta); - const handled = this.wasmInstance.OnMouseWheel(normalizedDelta, true); - this.updateMeshes(); - if (handled) event.stopImmediatePropagation(); + scrollStep(Math.sign(delta)); }; - const wheelListenerOptions = { - passive: false, - capture: true, - }; - window.addEventListener("wheel", interceptWheelEvent, wheelListenerOptions); + window.addEventListener("wheel", onWheel, { passive: false, capture: true }); + this.cleanups.push(() => window.removeEventListener("wheel", onWheel, { passive: false, capture: true } as any)); - let touchScrollActive = false; + // Touch scroll + let touchActive = false; let lastTouchY = 0; - const setTouchScrollMode = (active: boolean) => { - touchScrollActive = active; - this.sceneBundle1.controls.enabled = !active; + const onTouchStart = (e: TouchEvent) => { + if (e.touches.length !== 1 || !container.contains(e.target as Node)) return; + e.preventDefault(); + touchActive = true; + this.sceneBundle.controls.enabled = false; + lastTouchY = e.touches[0]!.clientY; }; - const touchScrollThresholdPx = 10; - const getAverageTouchY = (touches: TouchList) => { - let sum = 0; - for (let i = 0; i < touches.length; i++) { - sum += touches[i]!.clientY; - } - return sum / touches.length; + const onTouchMove = (e: TouchEvent) => { + if (!touchActive || e.touches.length !== 1) { touchActive = false; this.sceneBundle.controls.enabled = true; return; } + e.preventDefault(); + const dy = e.touches[0]!.clientY - lastTouchY; + if (Math.abs(dy) < 10) return; + scrollStep(Math.sign(dy)); + lastTouchY = e.touches[0]!.clientY; }; + const onTouchEnd = () => { touchActive = false; this.sceneBundle.controls.enabled = true; }; + const touchOpts = { passive: false, capture: true }; + window.addEventListener("touchstart", onTouchStart, touchOpts); + window.addEventListener("touchmove", onTouchMove, touchOpts); + window.addEventListener("touchend", onTouchEnd, touchOpts); + window.addEventListener("touchcancel", onTouchEnd, touchOpts); + this.cleanups.push(() => { + window.removeEventListener("touchstart", onTouchStart, touchOpts as any); + window.removeEventListener("touchmove", onTouchMove, touchOpts as any); + window.removeEventListener("touchend", onTouchEnd, touchOpts as any); + window.removeEventListener("touchcancel", onTouchEnd, touchOpts as any); + }); - const interceptTouchStart = (event: TouchEvent) => { - if (event.touches.length !== 1) return; - if (!isEventFromCanvas(event.target)) return; - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - setTouchScrollMode(true); - lastTouchY = getAverageTouchY(event.touches); - }; + // Resize + const resizeObs = new ResizeObserver(() => this.resize()); + resizeObs.observe(container); + this.cleanups.push(() => resizeObs.disconnect()); + this.resize(); - const interceptTouchMove = (event: TouchEvent) => { - if (!touchScrollActive) return; - if (event.touches.length !== 1) { - setTouchScrollMode(false); - return; - } - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - const currentY = getAverageTouchY(event.touches); - const deltaY = currentY - lastTouchY; - if (Math.abs(deltaY) < touchScrollThresholdPx) return; - const normalizedDelta = deltaY / Math.abs(deltaY); - const handled = this.wasmInstance.OnMouseWheel(normalizedDelta, true); - this.updateMeshes(); - if (handled) { - event.stopImmediatePropagation(); - } - lastTouchY = currentY; - }; + // Animate + this.animate(); + } - const interceptTouchEnd = (event: TouchEvent) => { - if (touchScrollActive) { - setTouchScrollMode(false); - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - } - }; + private computeScalars() { + const tf = this.tf; + if (this.scalarsND) { this.scalarsND.delete(); this.scalarsND = null; } - const touchListenerOptions = { - passive: false, - capture: true, - }; - window.addEventListener("touchstart", interceptTouchStart, touchListenerOptions); - window.addEventListener("touchmove", interceptTouchMove, touchListenerOptions); - window.addEventListener("touchend", interceptTouchEnd, touchListenerOptions); - window.addEventListener("touchcancel", interceptTouchEnd, touchListenerOptions); - - this.addCleanup(() => { - window.removeEventListener("keydown", interceptKeyDownEvent); - window.removeEventListener("keyup", interceptKeyUpEvent); - window.removeEventListener("wheel", interceptWheelEvent, wheelListenerOptions); - window.removeEventListener("touchstart", interceptTouchStart, touchListenerOptions); - window.removeEventListener("touchmove", interceptTouchMove, touchListenerOptions); - window.removeEventListener("touchend", interceptTouchEnd, touchListenerOptions); - window.removeEventListener("touchcancel", interceptTouchEnd, touchListenerOptions); - }); + const points = this.mesh.points; + const centroid = tf.mean(points, 0) as any; + const d = -(tf.dot(this.normalVec, centroid) as number); + const p = tf.plane(this.normalVec, d); + const pts = tf.point(points); - this.curveRenderer = new CurveRenderer({ - color: 0x00d5be, - radius: 0.075, - maxSegments: 20000, - }); - this.sceneBundle1.scene.add(this.curveRenderer.object); + this.scalarsND = tf.distance(pts, p); + this.scalarMin = this.scalarsND.min() as number; + this.scalarMax = this.scalarsND.max() as number; - this.isobandsMesh = createMesh(this.isDarkMode); - const isobandsMaterial = this.isobandsMesh.material as THREE.MeshMatcapMaterial; - isobandsMaterial.color = new THREE.Color(0x00a89a); - this.sceneBundle1.scene.add(this.isobandsMesh); + const neg = tf.sum(this.scalarsND.lt(0)) as number; + const pos = this.scalarsND.length - neg; + console.log("[computeScalars] min:", this.scalarMin, "max:", this.scalarMax, "neg:", neg, "pos:", pos); - this.updateMeshes(); - fitCameraToAllMeshesFromZPlane(this.sceneBundle1, 1.5); + centroid.delete(); } - public override updateMeshes() { - super.updateMeshes(); + private recomputeIsobands() { + const t0 = performance.now(); + const tf = this.tf; + const n = 10; + const range = this.scalarMax - this.scalarMin; + const s = range / n; + + // Which band does the current offset fall in? + const a = (this.planeOffset - this.scalarMin) / s; + const k = Math.max(0, Math.min(n - 1, Math.floor(a))); - const curveOutput = this.wasmInstance.get_curve_mesh(); - if (curveOutput && curveOutput.updated) { - const points = curveOutput.get_curve_points(); - const ids = curveOutput.get_curve_ids(); - const offsets = curveOutput.get_curve_offsets(); - const curves = buffersToCurves(points, ids, offsets); - this.curveRenderer.update(curves); + // Cut values centered on planeOffset (same logic as old C++ pipeline) + const cutValues = new Float32Array(n); + for (let i = 0; i < n; i++) { + cutValues[i] = this.planeOffset + (i - k) * s; } - const resultMesh = this.wasmInstance.get_result_mesh(); - if (resultMesh) { - updateResultMesh(resultMesh, this.isobandsMesh); + // Select alternating bands based on parity of k + const parity = k & 1; + const selectedBands: number[] = []; + for (let i = 0; i < n; i++) { + if ((i & 1) === parity) selectedBands.push(i); } + + const result = tf.isobands(this.mesh, this.scalarsND, cutValues, { selectedBands, returnCurves: true }); + this.timing.add(performance.now() - t0); + + // Update isobands geometry + const isoPoints = result.mesh.points; + const isoFaces = result.mesh.faces; + const geom = this.isobandsMesh.geometry; + geom.setAttribute("position", new THREE.BufferAttribute(isoPoints.data, 3)); + geom.setIndex(new THREE.BufferAttribute( + new Uint32Array(isoFaces.data.buffer, isoFaces.data.byteOffset, isoFaces.data.length), 1, + )); + if (isoPoints.data.length >= 3) { + geom.computeBoundingSphere(); + geom.computeBoundingBox(); + } + + // Update curves + const curvePts = result.curves.points.data as Float32Array; + const paths: number[][] = []; + for (const p of result.curves.paths) { + paths.push(Array.from(p.data)); + p.delete(); + } + this.curveRenderer.update({ points: curvePts, paths }); + + // Cleanup + result.mesh.delete(); + result.labels.delete(); + result.curves.delete(); + + if (this.refreshTimeValue) this.refreshTimeValue(); + } + + public randomize() { + const tf = this.tf; + this.normalVec.delete(); + this.normalVec = tf.normalize(tf.vector(tf.random("float32", [3], -1, 1))); + this.planeOffset = 0; + this.computeScalars(); + this.recomputeIsobands(); + } + + public getAverageTime(): number { + return this.timing.average; + } + + public applyTheme(isDark: boolean) { + this.sceneBundle.scene.background = new THREE.Color(isDark ? 0x1e1e1e : 0xfafafa); + + // Swap matcap textures + const matcapUrl = isDark + ? "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/635D52_A9BCC0_B1AEA0_819598.png" + : "https://raw.githubusercontent.com/nidorx/matcaps/master/1024/2D2D2F_C6C2C5_727176_94949B.png"; + const newTex = new THREE.TextureLoader().load(matcapUrl); + + const baseMat = this.baseMesh.material as THREE.MeshMatcapMaterial; + if (baseMat.matcap) baseMat.matcap.dispose(); + baseMat.matcap = newTex; + + const isoMat = this.isobandsMesh.material as THREE.MeshMatcapMaterial; + if (isoMat.matcap) isoMat.matcap.dispose(); + isoMat.matcap = newTex.clone(); + } + + private resize() { + const w = this.container.clientWidth; + const h = this.container.clientHeight; + this.renderer.setSize(w, h); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.sceneBundle.camera.aspect = w / h; + this.sceneBundle.camera.updateProjectionMatrix(); + } + + private animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + this.sceneBundle.controls.update(); + this.renderer.render(this.sceneBundle.scene, this.sceneBundle.camera); } - public runMain() { - this.wasmInstance.run_main_isobands(this.paths[0]!); - this.wasmInstance.FS.unlink(this.paths[0]); + public dispose() { + this.running = false; + for (const fn of this.cleanups) fn(); + this.cleanups = []; + this.sceneBundle.controls.dispose(); + this.baseMesh.geometry.dispose(); + (this.baseMesh.material as THREE.Material).dispose(); + this.isobandsMesh.geometry.dispose(); + (this.isobandsMesh.material as THREE.Material).dispose(); + this.curveRenderer.dispose(); + this.renderer.dispose(); + this.renderer.domElement.remove(); + if (this.scalarsND) { this.scalarsND.delete(); this.scalarsND = null; } + if (this.normalVec) { this.normalVec.delete(); this.normalVec = null; } + this.mesh.delete(); } } diff --git a/docs/app/examples/PositioningExample.ts b/docs/app/examples/PositioningExample.ts deleted file mode 100644 index 30767fb..0000000 --- a/docs/app/examples/PositioningExample.ts +++ /dev/null @@ -1,299 +0,0 @@ -import type { MainModule } from "@/examples/native"; -import { ThreejsBase } from "@/examples/ThreejsBase"; -import * as THREE from "three"; - -export class PositioningExample extends ThreejsBase { - private keyPressed = false; - - // Closest points visualization - private closestPointsGroup!: THREE.Group; - private sphere1!: THREE.Mesh; - private sphere2!: THREE.Mesh; - private connector!: THREE.Mesh; - - constructor( - wasmInstance: MainModule, - paths: string[], - container: HTMLElement, - isDarkMode = true, - ) { - // skipUpdate = true so we can set up camera before fitting - super(wasmInstance, paths, container, undefined, true, false, isDarkMode); - this.updateMeshes(); - - const interceptKeyDownEvent = (event: KeyboardEvent) => { - if (this.keyPressed) return; - this.keyPressed = true; - this.wasmInstance.OnKeyPress(event.key); - this.updateMeshes(); - }; - const interceptKeyUpEvent = (_event: KeyboardEvent) => { - this.keyPressed = false; - }; - window.addEventListener("keydown", interceptKeyDownEvent); - window.addEventListener("keyup", interceptKeyUpEvent); - - this.setupClosestPointsVisuals(); - this.positionMeshesForScreen(container); - this.setupOrthographicCamera(container); - - // Recompute closest points after repositioning and show - this.wasmInstance.positioning_compute_closest_points?.(); - this.updateMeshes(); - } - - private setupOrthographicCamera(container: HTMLElement): void { - const orthoCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000); - - // Replace camera in scene bundle - (this.sceneBundle1 as unknown as { camera: THREE.Camera }).camera = orthoCamera; - this.sceneBundle1.controls.setCamera(orthoCamera); - - this.fitOrthographicCamera(container); - } - - private positionMeshesForScreen(container: HTMLElement): void { - const rect = container.getBoundingClientRect(); - const isLandscape = rect.width > rect.height; - const aabbDiag = this.wasmInstance.positioning_get_aabb_diagonal?.() ?? 10; - - // Spacing between meshes - const spacing = isLandscape ? aabbDiag * 1.2 : aabbDiag * 1.0; - - // Camera on Z axis looking at XY plane: X = screen horizontal, Y = screen vertical - // Landscape: side by side (X axis) - // Portrait: stacked (Y axis) - const offsets: [number, number, number][] = isLandscape - ? [[-spacing / 2, 0, 0], [spacing / 2, 0, 0]] - : [[0, -spacing / 2, 0], [0, spacing / 2, 0]]; - - for (let i = 0; i < 2; i++) { - const [ox, oy, oz] = offsets[i]!; - const m = new THREE.Matrix4().makeTranslation(ox, oy, oz); - m.transpose(); - - const arr = m.toArray() as [ - number, number, number, number, - number, number, number, number, - number, number, number, number, - number, number, number, number - ]; - this.wasmInstance.positioning_set_instance_matrix?.(i, arr); - } - this.updateMeshes(); - } - - private fitOrthographicCamera(container: HTMLElement): void { - const rect = container.getBoundingClientRect(); - const aspect = rect.width / rect.height; - const isLandscape = rect.width > rect.height; - const camera = this.sceneBundle1.camera as unknown as THREE.OrthographicCamera; - - // Get bounding box of all instances - const positions: THREE.Vector3[] = []; - const aabbDiag = this.wasmInstance.positioning_get_aabb_diagonal?.() ?? 10; - - for (let i = 0; i < this.wasmInstance.get_number_of_instances(); i++) { - const inst = this.wasmInstance.get_instance_on_idx(i); - if (!inst) continue; - const matrix = new Float32Array(inst.get_matrix()); - const m = new THREE.Matrix4().fromArray(matrix).transpose(); - const pos = new THREE.Vector3(); - pos.setFromMatrixPosition(m); - positions.push(pos); - } - - // Compute center and extent - const center = new THREE.Vector3(); - if (positions.length >= 2) { - center.addVectors(positions[0]!, positions[1]!).multiplyScalar(0.5); - } - - const separation = positions.length >= 2 ? positions[0]!.distanceTo(positions[1]!) : 0; - // Different zoom for landscape vs portrait - const zoomFactor = isLandscape ? 0.5 : 0.7; - const extent = (separation + aabbDiag) * zoomFactor; - - // Set orthographic frustum - camera.left = -extent * aspect; - camera.right = extent * aspect; - camera.top = extent; - camera.bottom = -extent; - camera.updateProjectionMatrix(); - - // Position camera - camera.position.set(center.x, center.y, center.z + aabbDiag * 3); - camera.lookAt(center); - - this.sceneBundle1.controls.target.copy(center); - this.sceneBundle1.controls.update(); - } - - private setupClosestPointsVisuals(): void { - this.closestPointsGroup = new THREE.Group(); - this.closestPointsGroup.name = "closest_points"; - - // Sphere geometry and materials - const sphereGeom = new THREE.SphereGeometry(1, 16, 12); - const sphereMat = new THREE.MeshStandardMaterial({ - color: 0x00d5be, // Bright teal (like cross-section boundary) - roughness: 0.3, - metalness: 0.1, - }); - - // Cylinder geometry and material - const cylGeom = new THREE.CylinderGeometry(1, 1, 1, 8); - const cylMat = new THREE.MeshStandardMaterial({ - color: 0x00a89a, // Darker teal (like cross-section fill) - roughness: 0.3, - metalness: 0.1, - }); - - this.sphere1 = new THREE.Mesh(sphereGeom, sphereMat); - this.sphere2 = new THREE.Mesh(sphereGeom.clone(), sphereMat.clone()); - this.connector = new THREE.Mesh(cylGeom, cylMat); - - this.closestPointsGroup.add(this.sphere1); - this.closestPointsGroup.add(this.sphere2); - this.closestPointsGroup.add(this.connector); - - this.closestPointsGroup.visible = false; - this.sceneBundle1.scene.add(this.closestPointsGroup); - } - - private updateClosestPointsVisuals(): void { - if (!this.closestPointsGroup) return; - - const hasPoints = this.wasmInstance.positioning_has_closest_points?.(); - if (!hasPoints) { - this.closestPointsGroup.visible = false; - return; - } - - const pts = this.wasmInstance.positioning_get_closest_points?.() as number[] | undefined; - if (!pts || pts.length < 6) { - this.closestPointsGroup.visible = false; - return; - } - - const p0 = new THREE.Vector3(pts[0], pts[1], pts[2]); - const p1 = new THREE.Vector3(pts[3], pts[4], pts[5]); - const dist = p0.distanceTo(p1); - - // Fixed size based on AABB diagonal (1.5% for spheres, 0.75% for cylinder) - const aabbDiag = this.wasmInstance.positioning_get_aabb_diagonal?.() ?? 10; - const sphereRadius = aabbDiag * 0.015; - const cylRadius = aabbDiag * 0.0075; - - // Position and scale spheres - this.sphere1.position.copy(p0); - this.sphere1.scale.setScalar(sphereRadius); - - this.sphere2.position.copy(p1); - this.sphere2.scale.setScalar(sphereRadius); - - // Position and orient cylinder - if (dist > 0.001) { - const mid = new THREE.Vector3().addVectors(p0, p1).multiplyScalar(0.5); - this.connector.position.copy(mid); - - // Orient cylinder to point from p0 to p1 - const direction = new THREE.Vector3().subVectors(p1, p0).normalize(); - const quaternion = new THREE.Quaternion(); - quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction); - this.connector.quaternion.copy(quaternion); - - // Scale: radius for X/Z, length for Y - this.connector.scale.set(cylRadius, dist, cylRadius); - this.connector.visible = true; - } else { - this.connector.visible = false; - } - - this.closestPointsGroup.visible = true; - } - - public override updateMeshes(): void { - super.updateMeshes(); - this.updateClosestPointsVisuals(); - this.refreshTimeValue?.(); - } - - public override getAverageTime(): number { - return this.wasmInstance.get_average_time(); - } - - public randomize() { - this.wasmInstance.OnKeyPress("n"); - this.updateMeshes(); - } - - public override onPointerUp(event: PointerEvent) { - const cameraPosition = this.sceneBundle1.camera.position.clone(); - const dir = new THREE.Vector3(); - this.sceneBundle1.camera.getWorldDirection(dir); - const cameraFocalPoint = cameraPosition.clone().add(dir.multiplyScalar(100)); - - const updateFocalPoint = (x: number, y: number, z: number) => { - this.sceneBundle1.controls.target.set(x, y, z); - this.sceneBundle1.controls.update(); - - // Update instanced mesh matrices - const tempMatrix = new THREE.Matrix4(); - const needsUpdate = new Set(); - - for (let i = 0; i < this.wasmInstance.get_number_of_instances(); i++) { - const inst = this.wasmInstance.get_instance_on_idx(i); - const indices = this.instanceIndices.get(i); - if (!inst || !indices) continue; - - const { meshDataId, indexInBatch } = indices; - const instancedMesh = this.instancedMeshes.get(meshDataId); - if (!instancedMesh) continue; - - const matrix = new Float32Array(inst.get_matrix()); - tempMatrix.fromArray(matrix); - tempMatrix.transpose(); - instancedMesh.setMatrixAt(indexInBatch, tempMatrix); - needsUpdate.add(meshDataId); - } - - for (const meshDataId of needsUpdate) { - const mesh = this.instancedMeshes.get(meshDataId); - if (mesh) mesh.instanceMatrix.needsUpdate = true; - } - - this.sceneBundle1.scene.updateMatrixWorld(true); - this.renderer.render(this.sceneBundle1.scene, this.sceneBundle1.camera); - }; - - let t = 0; - const stepPositioning = () => { - if (this.isDisposed()) return; - t = this.wasmInstance.OnLeftButtonUpCustom( - [cameraFocalPoint.x, cameraFocalPoint.y, cameraFocalPoint.z], - updateFocalPoint, - t, - ); - if (t < 1.0) { - requestAnimationFrame(stepPositioning); - } - this.updateMeshes(); - if (t < 1) { - event.stopPropagation(); - } - }; - requestAnimationFrame(stepPositioning); - } - - public runMain() { - const v = new this.wasmInstance.VectorString(); - for (const path of this.paths) { - v.push_back(path); - } - this.wasmInstance.run_main_positioning(v); - for (const path of this.paths) { - this.wasmInstance.FS.unlink(path); - } - } -} diff --git a/docs/app/pages/[...slug].vue b/docs/app/pages/[...slug].vue index 4c5fe32..b6b4859 100644 --- a/docs/app/pages/[...slug].vue +++ b/docs/app/pages/[...slug].vue @@ -31,7 +31,7 @@ const { data: surround } = await useAsyncData( const title = page.value.seo?.title || page.value.title; const description = page.value.seo?.description || page.value.description; -const headline = computed(() => `${findPageHeadline(navigation?.value, page.value?.path)} | ${library.value === "cpp" ? "C++" : "PY"}`); +const headline = computed(() => `${findPageHeadline(navigation?.value, page.value?.path)} | ${library.value === "cpp" ? "C++" : library.value === "py" ? "PY" : "TS"}`); // Generate OG image using nuxt-og-image with headline defineOgImageComponent("Docs", { diff --git a/docs/app/pages/live-examples/alignment.vue b/docs/app/pages/live-examples/alignment.vue index 7fd2206..3b05ed9 100644 --- a/docs/app/pages/live-examples/alignment.vue +++ b/docs/app/pages/live-examples/alignment.vue @@ -1,5 +1,5 @@ + + + + diff --git a/docs/content/py/1.getting-started/1.index.md b/docs/content/py/1.getting-started/1.index.md index 28f57ff..c5025ef 100644 --- a/docs/content/py/1.getting-started/1.index.md +++ b/docs/content/py/1.getting-started/1.index.md @@ -5,12 +5,17 @@ navigation: icon: i-lucide-house --- -`trueform` is a Python library for real-time geometric processing. Mesh booleans, registration, remeshing and queries — at interactive speed on million-polygon meshes. Robust to non-manifold flaps, inconsistent winding, and pipeline artifacts. NumPy in, NumPy out. +`trueform` is a Python library for real-time geometric processing. Enriched NumPy arrays with support for vectorized spatial queries. Mesh booleans, registration, remeshing — at interactive speed on million-polygon meshes. Robust to non-manifold flaps, inconsistent winding, and pipeline artifacts. NumPy in, NumPy out. ::try-it-banner :: -Simple code just works. Forms cache structures on demand. Algorithms process forms and return NumPy arrays. +Arrays become typed primitives — single or batched. Primitives compose into forms with cached geometric and topological structures. + +::python-query-diagram +:: + +Same functions for primitives, batches, and forms. Algorithms transform forms into new geometry. ::python-flow-diagram :: @@ -73,13 +78,24 @@ Simple code just works. Forms cache structures on demand. Algorithms process for ## Quick Tour -Here's how trueform enables complex geometric workflows — NumPy arrays in, NumPy arrays out: +**Primitives** — typed NumPy arrays, single or batched: ```python import numpy as np import trueform as tf -# Start with numpy arrays +triangle = tf.Triangle(a=[0, 0, 0], b=[1, 0, 0], c=[0, 1, 0]) +segment = tf.Segment([[0, 0, 0], [1, 1, 1]]) +ray = tf.Ray(origin=[0.2, 0.2, -1], direction=[0, 0, 1]) + +# Add a leading dimension for batches +pts = tf.Point(np.random.rand(1000, 3).astype(np.float32)) +segs = tf.Segment(start=np.random.rand(500, 3), end=np.random.rand(500, 3)) +``` + +**Meshes** are created from NumPy arrays or read from files: + +```python points = np.array([ [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1] ], dtype=np.float32) @@ -87,27 +103,24 @@ faces = np.array([ [0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2] ], dtype=np.int32) -# Create a mesh mesh = tf.Mesh(faces, points) -# Or with variable-sized faces (n-gons) -offsets = np.array([0, 3, 7], dtype=np.int64) -data = np.array([0, 1, 2, 0, 2, 3, 1], dtype=np.int32) -ngon_faces = tf.OffsetBlockedArray(offsets, data) -ngon_mesh = tf.Mesh(ngon_faces, points) - # Or read from file -mesh = tf.read_stl("model.stl") +mesh = tf.Mesh(*tf.read_stl("model.stl")) ``` -**Primitive queries** work directly on geometry: +**Queries** — same functions for any combination. Batches broadcast: ```python -triangle = tf.Polygon([[0, 0, 0], [1, 0, 0], [0, 1, 0]]) -segment = tf.Segment([[0, 0, 0], [1, 1, 1]]) -ray = tf.Ray(origin=[0.2, 0.2, -1], direction=[0, 0, 1]) +# Batch × Primitive — distance field to a plane +plane = tf.Plane(normal=[0, 0, 1], offset=0.0) +scalars = tf.distance(pts, plane) # shape (1000,) -dist2, pt_on_tri, pt_on_seg = tf.closest_metric_point_pair(triangle, segment) +# Batch × Form — closest point on mesh for each segment +ids, dist2s, closest = tf.neighbor_search(mesh, segs) # 3 arrays, shape (500,) + +# Single × Single +dist2, pt_a, pt_b = tf.closest_metric_point_pair(triangle, segment) if (t := tf.ray_cast(ray, triangle)) is not None: hit_point = ray.origin + t * np.array(ray.direction) @@ -167,16 +180,24 @@ paths, curve_points = tf.intersection_curves(static_mesh, dynamic_mesh) **Scalar fields and isocontours**: ```python -# Compute distance field from a plane -plane = tf.Plane(normal=[0, 0, 1], offset=0.0) -scalars = tf.distance_field(mesh.points, plane) - # Extract isobands with boundary curves (band_faces, band_points), labels, (paths, curve_points) = tf.isobands( mesh, scalars, [-1.0, 0.0, 1.0], return_curves=True ) ``` +**Remeshing** — decimate and remesh: + +```python +# Decimate to 50% +dec_faces, dec_points = tf.decimated(mesh, 0.5) + +# Isotropic remesh to uniform edge lengths +dec_mesh = tf.Mesh(dec_faces, dec_points) +mel = tf.mean_edge_length(dec_mesh) +rem_faces, rem_points = tf.isotropic_remeshed(dec_mesh, mel) +``` + **Mesh cleanup** prepares geometry for processing: ```python diff --git a/docs/content/py/2.modules/0.index.md b/docs/content/py/2.modules/00.index.md similarity index 93% rename from docs/content/py/2.modules/0.index.md rename to docs/content/py/2.modules/00.index.md index f9b3f0c..bce4028 100644 --- a/docs/content/py/2.modules/0.index.md +++ b/docs/content/py/2.modules/00.index.md @@ -30,7 +30,7 @@ Each module page documents its API with working code snippets. title: Core to: /py/modules/core --- - Primitives, forms, transformations, and basic geometric queries. + Primitives, batch support, transformations, and data structures. ::: :::card @@ -39,7 +39,7 @@ Each module page documents its API with working code snippets. title: Spatial to: /py/modules/spatial --- - Spatial queries and acceleration structures. + Forms, spatial queries, and acceleration structures. ::: :::card diff --git a/docs/content/py/2.modules/01.core.md b/docs/content/py/2.modules/01.core.md new file mode 100644 index 0000000..4b4d4d9 --- /dev/null +++ b/docs/content/py/2.modules/01.core.md @@ -0,0 +1,361 @@ +--- +title: Core +description: Primitives, batch support, transformations, and data structures. +path: /py/modules/core +navigation: + icon: i-lucide-atom +--- + +Primitives are numpy arrays with geometric meaning. A `Point` is a `(3,)` array that knows it's a point. A `Segment` is a `(2, 3)` array that knows it has a start and an end. This lets operations like `tf.distance`, `tf.intersects`, and `tf.ray_cast` work uniformly across all primitive types. + +Batch support is built in. Adding a leading `N` dimension turns any primitive into a batch: `(N, 3)` becomes N points, `(N, 2, 3)` becomes N segments. All operations broadcast over batches — `tf.distance(batch_of_points, plane)` returns an `(N,)` array, no loops needed. + +## Primitives + +All eight primitives — `Point`, `Line`, `Ray`, `Segment`, `Triangle`, `Polygon`, `Plane`, `AABB` — inherit from `Primitive` and share these common properties: + +| **Property** | **Returns** | **Description** | +|--------------|----------------|----------------------------------------------| +| `.data` | `ndarray` | Underlying numpy array | +| `.dims` | `int` | Dimensionality (2 or 3) | +| `.dtype` | `np.dtype` | Data type (`float32` or `float64`) | +| `.is_batch` | `bool` | Whether this holds a batch of primitives | +| `.count` | `int` | Number of primitives (1 for single, N for batch) | +| `len()` | `int` | Same as `.count` | + +```python +import trueform as tf + +point = tf.Point([1.0, 2.0, 3.0]) +print(point.dims) # 3 +print(point.dtype) # float32 +print(point.is_batch) # False +print(point.count) # 1 + +# Type checking +isinstance(point, tf.Primitive) # True +isinstance(point, tf.Point) # True +``` + +::tip{icon="i-lucide-info"} +**Dtype handling**: Non-float inputs are automatically cast to `float32`. To use `float64`, pass data with that dtype explicitly. +:: + +### Point + +A point in 2D or 3D space. Shape `(D,)` for single, `(N, D)` for batch. + +```python +import numpy as np + +# Single point +point = tf.Point([1.0, 2.0, 3.0]) +print(point.coords) # [1. 2. 3.] +print(point.x, point.y, point.z) # 1.0 2.0 3.0 + +# Batch of points +points = tf.Point(np.random.rand(100, 3).astype(np.float32)) +print(points.count) # 100 +``` + +| **Property** | **Single** | **Batch** | +|---|---|---| +| `.coords` | `(D,)` | `(N, D)` | +| `.x`, `.y`, `.z` | `float` | `ndarray(N,)` | + +### Line + +An infinite line defined by an origin point and direction vector. Shape `(2, D)` for single, `(N, 2, D)` for batch. + +```python +line = tf.Line(origin=[0, 0, 0], direction=[1, 0, 0]) +print(line.origin) # [0. 0. 0.] +print(line.direction) # [1. 0. 0.] + +# Factory method +line = tf.Line.from_points([0, 0], [1, 1]) + +# Batch of lines +lines = tf.Line(np.random.rand(50, 2, 3).astype(np.float32)) +``` + +| **Property** | **Single** | **Batch** | +|---|---|---| +| `.origin` | `(D,)` | `(N, D)` | +| `.direction` | `(D,)` | `(N, D)` | +| `.normalized_direction` | `(D,)` | `(N, D)` | + +### Ray + +A semi-infinite line starting at an origin and extending in a direction. Shape `(2, D)` for single, `(N, 2, D)` for batch. + +```python +ray = tf.Ray(origin=[0, 0, 0], direction=[1, 0, 0]) + +# Direction is NOT normalized by default +print(ray.normalized_direction) # Get unit vector + +# Factory method: ray through another point +ray = tf.Ray.from_points(start=[0, 0, 0], through_point=[1, 1, 1]) + +# Batch of rays +rays = tf.Ray(np.random.rand(50, 2, 3).astype(np.float32)) +``` + +| **Property** | **Single** | **Batch** | +|---|---|---| +| `.origin` | `(D,)` | `(N, D)` | +| `.direction` | `(D,)` | `(N, D)` | +| `.normalized_direction` | `(D,)` | `(N, D)` | + +### Segment + +A line segment defined by two endpoints. Shape `(2, D)` for single, `(N, 2, D)` for batch. + +```python +segment = tf.Segment([[0, 0, 0], [1, 1, 1]]) +print(segment.start) # [0. 0. 0.] +print(segment.end) # [1. 1. 1.] +print(segment.length) # 1.732... +print(segment.midpoint) # [0.5 0.5 0.5] + +# Batch of segments +segs = tf.Segment(np.random.rand(50, 2, 3).astype(np.float32)) +print(segs.count) # 50 +``` + +| **Property** | **Single** | **Batch** | +|---|---|---| +| `.start`, `.end` | `(D,)` | `(N, D)` | +| `.endpoints` | `(2, D)` | `(N, 2, D)` | +| `.vector` | `(D,)` | `(N, D)` | +| `.midpoint` | `(D,)` | `(N, D)` | +| `.length` | `float` | `ndarray(N,)` | + +### Triangle + +A triangle defined by three vertices. Shape `(3, D)` for single, `(N, 3, D)` for batch. + +```python +# From keyword arguments +tri = tf.Triangle(a=[0, 0, 0], b=[1, 0, 0], c=[0, 1, 0]) +print(tri.a) # [0. 0. 0.] + +# From data array +tri = tf.Triangle([[0, 0, 0], [1, 0, 0], [0, 1, 0]]) + +# Batch of triangles +tris = tf.Triangle(np.random.rand(50, 3, 3).astype(np.float32)) +print(tris.count) # 50 + +# Geometry functions +print(tf.area(tri)) # 0.5 +print(tf.normals(tri)) # [0. 0. 1.] +``` + +| **Property** | **Single** | **Batch** | +|---|---|---| +| `.a`, `.b`, `.c` | `(D,)` | `(N, D)` | +| `.vertices` | `(3, D)` | `(N, 3, D)` | + +::tip{icon="i-lucide-info"} +Use `tf.area()` and `tf.normals()` for computed geometry — these work on both single and batch triangles. See the [Geometry](/py/modules/geometry) module. +:: + +### Polygon + +A polygon defined by ordered vertices. Shape `(V, D)` for single, `(N, V, D)` for batch (all polygons in a batch have the same vertex count). + +```python +# Create a quad +quad = tf.Polygon([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]) +print(quad.num_vertices) # 4 +print(tf.area(quad)) # 1.0 + +# Batch of pentagons +polys = tf.Polygon(np.random.rand(50, 5, 3).astype(np.float32)) +print(polys.count) # 50 +``` + +| **Property** | **Single** | **Batch** | +|---|---|---| +| `.vertices` | `(V, D)` | `(N, V, D)` | +| `.num_vertices` | `int` | `int` | + +### Plane + +A plane defined by a unit normal and signed offset: `dot(normal, x) + offset = 0`. Shape `(D+1,)` for single, `(N, D+1)` for batch. 3D only. + +```python +# Using normal and offset +plane = tf.Plane(normal=[0, 0, 1], offset=-5.0) +print(plane.normal) # [0. 0. 1.] +print(plane.offset) # -5.0 + +# Using a point on the plane (offset computed automatically) +plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 5]) + +# From raw coefficients +plane = tf.Plane([0, 0, 1, -5]) + +# Factory methods +plane = tf.Plane.from_point_normal(origin=[0, 0, 5], normal=[0, 0, 1]) +plane = tf.Plane.from_points([0, 0, 0], [1, 0, 0], [0, 1, 0]) # XY plane + +# Batch of planes +planes = tf.Plane(np.random.rand(50, 4).astype(np.float32)) +``` + +| **Property** | **Single** | **Batch** | +|---|---|---| +| `.normal` | `(D,)` | `(N, D)` | +| `.offset` | `float` | `ndarray(N,)` | +| `.coeffs` | `(D+1,)` | `(N, D+1)` | + +### AABB + +An axis-aligned bounding box defined by min and max corners. Shape `(2, D)` for single, `(N, 2, D)` for batch. + +```python +aabb = tf.AABB(min=[0, 0, 0], max=[1, 1, 1]) +print(aabb.min) # [0. 0. 0.] +print(aabb.max) # [1. 1. 1.] +print(aabb.center) # [0.5 0.5 0.5] +print(aabb.size) # [1. 1. 1.] +print(aabb.volume) # 1.0 + +# From data array +aabb = tf.AABB([[0, 0, 0], [1, 1, 1]]) + +# Factory methods +aabb = tf.AABB.from_center_size(center=[5, 5], size=[2, 2]) +aabb = tf.AABB.from_points([[0, 0], [1, 0], [1, 1]]) + +# Batch of AABBs +mins = np.zeros((50, 3), dtype=np.float32) +maxs = np.ones((50, 3), dtype=np.float32) +boxes = tf.AABB(min=mins, max=maxs) +print(boxes.count) # 50 +``` + +| **Property** | **Single** | **Batch** | +|---|---|---| +| `.min`, `.max` | `(D,)` | `(N, D)` | +| `.bounds` | `(2, D)` | `(N, 2, D)` | +| `.center` | `(D,)` | `(N, D)` | +| `.size` | `(D,)` | `(N, D)` | +| `.volume` | `float` | `ndarray(N,)` | + +### Summary + +| **Primitive** | **Single Shape** | **Batch Shape** | **Dims** | **Key Properties** | +|---|---|---|---|---| +| `Point` | `(D,)` | `(N, D)` | 2D, 3D | `.coords`, `.x`, `.y`, `.z` | +| `Line` | `(2, D)` | `(N, 2, D)` | 2D, 3D | `.origin`, `.direction` | +| `Ray` | `(2, D)` | `(N, 2, D)` | 2D, 3D | `.origin`, `.direction` | +| `Segment` | `(2, D)` | `(N, 2, D)` | 2D, 3D | `.start`, `.end`, `.length`, `.midpoint` | +| `Triangle` | `(3, D)` | `(N, 3, D)` | 2D, 3D | `.a`, `.b`, `.c`, `.vertices` | +| `Polygon` | `(V, D)` | `(N, V, D)` | 2D, 3D | `.vertices`, `.num_vertices` | +| `Plane` | `(D+1,)` | `(N, D+1)` | 3D | `.normal`, `.offset` | +| `AABB` | `(2, D)` | `(N, 2, D)` | 2D, 3D | `.min`, `.max`, `.center`, `.volume` | + +## Broadcasting + +Query functions broadcast across single and batch primitives: + +| **A** | **B** | **Result** | +|---|---|---| +| single | single | scalar | +| batch(N) | single | `ndarray(N,)` | +| single | batch(N) | `ndarray(N,)` | +| batch(N) | batch(N) | `ndarray(N,)` | + +```python +# single x single → scalar +d = tf.distance(tf.Point([0, 0, 0]), tf.Point([1, 0, 0])) # 1.0 + +# batch x single → array +pts = tf.Point(np.random.rand(100, 3).astype(np.float32)) +plane = tf.Plane(normal=[0, 0, 1], offset=0.0) +distances = tf.distance(pts, plane) # shape (100,) + +# batch x batch → array (must have same count) +pts_a = tf.Point(np.random.rand(50, 3).astype(np.float32)) +pts_b = tf.Point(np.random.rand(50, 3).astype(np.float32)) +distances = tf.distance(pts_a, pts_b) # shape (50,) +``` + +Batch properties also broadcast: + +```python +segs = tf.Segment(np.random.rand(50, 2, 3).astype(np.float32)) +print(segs.start.shape) # (50, 3) +print(segs.length.shape) # (50,) +print(segs.midpoint.shape) # (50, 3) +``` + +::tip{icon="i-lucide-info"} +**Batch x batch requires matching counts.** Both operands must have the same `.count`. Mismatched counts raise an error. +:: + +## Transforming Primitives + +Any primitive can be transformed using `tf.transformed(primitive, transformation)`: + +```python +# 4x4 homogeneous transformation (3D) +translation = np.eye(4, dtype=np.float32) +translation[:3, 3] = [10, 0, 0] + +point = tf.Point([1, 2, 3]) +transformed_point = tf.transformed(point, translation) +print(transformed_point.coords) # [11. 2. 3.] + +segment = tf.Segment([[0, 0, 0], [1, 0, 0]]) +transformed_segment = tf.transformed(segment, translation) +print(transformed_segment.start) # [10. 0. 0.] +``` + +Batch primitives are also supported — the same transformation applies to every element in the batch. + +## Data Structures + +### OffsetBlockedArray + +`OffsetBlockedArray` stores variable-length blocks of data using two arrays: offsets (block boundaries) and data (packed elements). It is used for curves, paths, and dynamic mesh faces. + +```python +offsets = [0, 3, 7, 9] # Block boundaries +data = [0,1,2, 3,4,5,6, 7,8] # Packed data +# Block 0: data[0:3] = [0,1,2] +# Block 1: data[3:7] = [3,4,5,6] +# Block 2: data[7:9] = [7,8] +``` + +```python +# Create from offsets and data +offsets = np.array([0, 3, 7, 10], dtype=np.int32) +data = np.array([0,1,2, 3,4,5,6, 7,8,9], dtype=np.int32) +blocks = tf.OffsetBlockedArray(offsets, data) + +print(len(blocks)) # 3 +print(blocks[0]) # [0 1 2] +print(blocks[1]) # [3 4 5 6] + +for block in blocks: + print(block) + +# From uniform arrays (e.g., quad faces) +quads = np.array([[0,1,2,3], [4,5,6,7]], dtype=np.int32) +faces = tf.as_offset_blocked(quads) +``` + +| **Property** | **Returns** | **Description** | +|---|---|---| +| `.offsets` | `ndarray` | Block boundary indices | +| `.data` | `ndarray` | Packed data array | +| `.dtype` | `np.dtype` | Data type of the data array | + +::tip{icon="i-lucide-info"} +For queries on forms (Mesh, EdgeMesh, PointCloud), see [Spatial](/py/modules/spatial). For geometry functions (area, normals, volume), see [Geometry](/py/modules/geometry). For file I/O, see [I/O](/py/modules/io). +:: diff --git a/docs/content/py/2.modules/02.spatial.md b/docs/content/py/2.modules/02.spatial.md new file mode 100644 index 0000000..2fc6744 --- /dev/null +++ b/docs/content/py/2.modules/02.spatial.md @@ -0,0 +1,347 @@ +--- +title: Spatial +description: Forms, spatial queries, and acceleration structures. +path: /py/modules/spatial +navigation: + icon: i-lucide-scale-3d +--- + +The Spatial module provides all query operations — distance, intersection, ray casting, neighbor search, and element gathering. Queries work uniformly across primitives and forms, and all support batch broadcasting. + +Forms are geometric collections backed by spatial acceleration structures: `Mesh`, `EdgeMesh`, and `PointCloud`. The tree is built automatically on first query, enabling efficient search over large datasets. + +## Forms + +| **Form** | **Description** | **Elements** | **Dimensions** | +|----------------|----------------------------------------------|-----------------------------|----------------| +| `PointCloud` | Collection of points | Points | 2D, 3D | +| `EdgeMesh` | Collection of line segments (edges) | Edges (2 vertices) | 2D, 3D | +| `Mesh` | Polygonal mesh (triangles or dynamic n-gons) | Faces (triangles or n-gons) | 2D, 3D | + +### PointCloud + +```python +import trueform as tf +import numpy as np + +points = np.random.rand(100, 3).astype(np.float32) +cloud = tf.PointCloud(points) + +print(cloud.size) # 100 +print(cloud.dims) # 3 +print(cloud.dtype) # float32 +``` + +| **Property** | **Returns** | **Description** | +|------------------|----------------------|-------------------------------------| +| `points` | `ndarray` | Vertex coordinates | +| `size` | `int` | Number of points | +| `dims` | `int` | Dimensionality (2 or 3) | +| `dtype` | `np.dtype` | Data type (`float32` or `float64`) | +| `transformation` | `ndarray` or `None` | Transformation matrix | + +### EdgeMesh + +```python +edges = np.array([[0, 1], [1, 2], [2, 3]], dtype=np.int32) +points = np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32) +edge_mesh = tf.EdgeMesh(edges, points) +``` + +| **Property** | **Returns** | **Description** | +|--------------------|----------------------|-------------------------------------| +| `edges` | `ndarray` | Edge connectivity `(N, 2)` | +| `points` | `ndarray` | Vertex coordinates | +| `number_of_edges` | `int` | Number of edges | +| `number_of_points` | `int` | Number of vertices | +| `dims` | `int` | Dimensionality (2 or 3) | +| `dtype` | `np.dtype` | Data type (`float32` or `float64`) | +| `transformation` | `ndarray` or `None` | Transformation matrix | + +EdgeMesh also provides topology properties. See [Topology](/py/modules/topology) for details: + +| **Property** | **Returns** | **Description** | +|--------------------|----------------------|----------------------------------| +| `edge_membership` | `OffsetBlockedArray` | Vertex → edges containing it | +| `vertex_link` | `OffsetBlockedArray` | Vertex → connected vertices | + +### Mesh + +Supports both fixed-size triangles and dynamic n-gons via `OffsetBlockedArray`: + +```python +# Triangle mesh +faces = np.array([[0, 1, 2], [1, 2, 3]], dtype=np.int32) +points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]], dtype=np.float32) +mesh = tf.Mesh(faces, points) + +print(mesh.is_dynamic) # False +print(mesh.ngon) # 3 (triangles) + +# Dynamic mesh (variable-sized faces) +quads = np.array([[0, 1, 2, 3], [4, 5, 6, 7]], dtype=np.int32) +faces = tf.as_offset_blocked(quads) +mesh = tf.Mesh(faces, points) + +print(mesh.is_dynamic) # True +print(mesh.ngon) # None +``` + +| **Property** | **Returns** | **Description** | +|--------------------|-----------------------------------|------------------------------------------------------| +| `faces` | `ndarray` or `OffsetBlockedArray` | Face connectivity | +| `points` | `ndarray` | Vertex coordinates | +| `number_of_faces` | `int` | Number of faces | +| `number_of_points` | `int` | Number of vertices | +| `dims` | `int` | Dimensionality (2 or 3) | +| `is_dynamic` | `bool` | True if mesh uses variable-sized faces | +| `ngon` | `int` or `None` | Vertices per face (3 for triangles, None if dynamic) | +| `dtype` | `np.dtype` | Data type (`float32` or `float64`) | +| `transformation` | `ndarray` or `None` | Transformation matrix | + +Mesh also provides topology and geometry properties. See [Topology](/py/modules/topology) and [Geometry](/py/modules/geometry) for details: + +| **Property** | **Returns** | **Description** | +|----------------------|-----------------------------------|----------------------------------| +| `face_membership` | `OffsetBlockedArray` | Vertex → faces containing it | +| `manifold_edge_link` | `ndarray` or `OffsetBlockedArray` | Face edge → adjacent face | +| `face_link` | `OffsetBlockedArray` | Face → adjacent faces | +| `vertex_link` | `OffsetBlockedArray` | Vertex → connected vertices | +| `normals` | `ndarray` | Face normals (3D only) | +| `point_normals` | `ndarray` | Vertex normals (3D only) | + +::tip{icon="i-lucide-zap"} +The spatial tree is built automatically on first query. Call `form.build_tree()` to prebuild it if you want to control when the cost is paid. +:: + +## Transformations on Forms + +Forms can carry a transformation matrix. Queries use the transformation on the fly — the underlying data and acceleration structures stay unchanged. + +```python +# 4x4 homogeneous transformation (3D) +translation = np.eye(4, dtype=np.float32) +translation[:3, 3] = [5, 0, 0] + +# Attach at construction +mesh = tf.Mesh(faces, points, transformation=translation) + +# Or set later +mesh.transformation = translation + +# Queries use the transformation automatically +d = tf.distance(mesh, other_mesh) +``` + +All forms support transformations: `PointCloud`, `EdgeMesh`, and `Mesh`. The original data remains unchanged. + +## Shared Views + +`shared_view()` creates a new instance sharing the same underlying data (points, faces, spatial tree) but with its own transformation. Use this to query the same geometry at multiple poses without duplicating anything. + +```python +# Build once +mesh = tf.Mesh(faces, points) +mesh.build_tree() + +# Create views with different poses +mesh_a = mesh.shared_view() +mesh_a.transformation = transform_A + +mesh_b = mesh.shared_view() +mesh_b.transformation = transform_B + +# Both share the same data and tree +d = tf.distance(mesh_a, mesh_b) +``` + +::tip{icon="i-lucide-zap"} +Build the tree and topology structures once on the original form, then create shared views. All views benefit from the cached structures. +:: + +## Queries + +All query functions accept any combination of primitives and forms. Both operands must have the same `dtype` and `dims`. + +Queries broadcast like numpy operations. A single primitive paired with a batch of N primitives produces N results — the single operand is reused for each element in the batch. Two batches of the same size are paired element-wise. A form paired with a batch queries each element against the spatial tree independently. + +### Distance + +`tf.distance(a, b)` and `tf.distance2(a, b)` compute the distance (or squared distance) between any two geometric objects. + +```python +# Primitive × Primitive +d = tf.distance(tf.Point([0, 0, 0]), tf.Segment([[1, 0, 0], [1, 1, 0]])) + +# Form × Primitive +d = tf.distance(mesh, tf.Point([0, 0, 0])) + +# Form × Form +d = tf.distance(mesh_a, mesh_b) + +# Batch → array of distances +pts = tf.Point(np.random.rand(1000, 3).astype(np.float32)) +distances = tf.distance(pts, mesh) # shape (1000,) +``` + +| **A** | **B** | **Returns** | +|---|---|---| +| primitive | primitive | `float` | +| batch(N) | primitive or form | `ndarray(N,)` | +| batch(N) | batch(N) | `ndarray(N,)` | +| form | primitive | `float` | +| form | form | `float` | + +### Closest Point + +`tf.closest_metric_point(a, b)` returns the squared distance and closest point on `a` to `b`. `tf.closest_metric_point_pair(a, b)` returns closest points on both sides. + +These work on primitives only. For forms, use `tf.neighbor_search` which returns element IDs alongside closest points. + +```python +# Closest point on a polygon to a point +dist2, pt = tf.closest_metric_point(polygon, point) + +# Closest points between two segments +dist2, pt_on_a, pt_on_b = tf.closest_metric_point_pair(segment_a, segment_b) + +# Batch → arrays +pts = tf.Point(np.random.rand(100, 3).astype(np.float32)) +dist2s, closest_pts = tf.closest_metric_point(polygon, pts) +# dist2s.shape = (100,), closest_pts.shape = (100, 3) +``` + +| **A** | **B** | `closest_metric_point` | `closest_metric_point_pair` | +|---|---|---|---| +| primitive | primitive | `(float, ndarray[D])` | `(float, ndarray[D], ndarray[D])` | +| batch(N) | primitive | `(ndarray[N], ndarray[N,D])` | `(ndarray[N], ndarray[N,D], ndarray[N,D])` | +| primitive | batch(N) | `(ndarray[N], ndarray[N,D])` | `(ndarray[N], ndarray[N,D], ndarray[N,D])` | +| batch(N) | batch(N) | `(ndarray[N], ndarray[N,D])` | `(ndarray[N], ndarray[N,D], ndarray[N,D])` | + +### Intersection Testing + +`tf.intersects(a, b)` tests whether two geometric objects intersect. + +```python +# Primitive × Primitive +hit = tf.intersects(tf.AABB(min=[0,0,0], max=[1,1,1]), tf.Segment([[0,0,0], [2,2,2]])) + +# Form × Primitive +hit = tf.intersects(mesh, polygon) + +# Form × Form +hit = tf.intersects(mesh_a, mesh_b) + +# Batch → array +segs = tf.Segment(np.random.rand(100, 2, 3).astype(np.float32)) +hits = tf.intersects(mesh, segs) # shape (100,) +``` + +| **A** | **B** | **Returns** | +|---|---|---| +| primitive | primitive | `bool` | +| batch(N) | primitive or form | `ndarray(N,)` | +| batch(N) | batch(N) | `ndarray(N,)` | +| form | primitive | `bool` | +| form | form | `bool` | + +### Ray Casting + +`tf.ray_cast(ray, target, config)` casts a ray and returns the parametric distance `t` where `hit_point = ray.origin + t * ray.direction`. The `config` tuple `(min_t, max_t)` constrains the valid range. For batch rays, `config` can be per-ray arrays. + +```python +config = (0.0, 100.0) + +# Ray × Primitive — returns t or None +t = tf.ray_cast(ray, triangle, config) + +# Ray × Form — returns (element_id, t) or None +result = tf.ray_cast(ray, mesh, config) +if result is not None: + face_id, t = result + +# Batch rays × Primitive — returns ndarray with NaN for misses +rays = tf.Ray(np.random.rand(100, 2, 3).astype(np.float32)) +ts = tf.ray_cast(rays, triangle, config) # shape (100,) + +# Batch rays × Form — returns (element_ids, ts) with -1/NaN for misses +ids, ts = tf.ray_cast(rays, mesh, config) # both shape (100,) + +# Per-ray config — each ray gets its own (min_t, max_t) +min_ts = np.zeros(100, dtype=np.float32) +max_ts = np.full(100, 50.0, dtype=np.float32) +ids, ts = tf.ray_cast(rays, mesh, config=(min_ts, max_ts)) +``` + +| **Ray** | **Target** | **Returns (hit)** | **Returns (miss)** | +|---|---|---|---| +| single | primitive | `float` | `None` | +| single | form | `(element_id, t)` | `None` | +| batch(N) | primitive | `ndarray(N,)` | NaN entries | +| batch(N) | form | `(ndarray[N] ids, ndarray[N] ts)` | -1/NaN entries | + +### Neighbor Search + +`tf.neighbor_search(form, query)` finds the nearest element in a form. This is the form equivalent of `closest_metric_point` — it returns element IDs alongside closest points. + +```python +# Single query — returns (element_id, dist², closest_point) +idx, dist2, pt = tf.neighbor_search(mesh, point) + +# With search radius — returns None if nothing within radius +result = tf.neighbor_search(mesh, point, radius=1.0) +if result is not None: + idx, dist2, pt = result + +# Batch query — returns arrays +pts = tf.Point(np.random.rand(1000, 3).astype(np.float32)) +ids, dist2s, closest_pts = tf.neighbor_search(mesh, pts) +# ids.shape = (1000,), dist2s.shape = (1000,), closest_pts.shape = (1000, 3) +# ids[i] = -1 and dist2s[i] = inf when nothing found within radius + +# Form × Form — closest pair between two forms +(id0, id1), (dist2, pt0, pt1) = tf.neighbor_search(mesh_a, mesh_b) +``` + +### k-Nearest Neighbors (kNN) + +Pass `k` to `tf.neighbor_search` for k-nearest neighbor queries: + +```python +# Single query — list of (id, dist², point), sorted by distance +neighbors = tf.neighbor_search(mesh, point, k=10) +for idx, dist2, pt in neighbors: + pass + +# With radius limit — may return fewer than k +neighbors = tf.neighbor_search(mesh, point, k=10, radius=5.0) + +# Batch query — adds a K dimension and a counts array +ids, dist2s, pts, counts = tf.neighbor_search(mesh, pts, k=10) +# ids.shape = (N, K), dist2s.shape = (N, K), pts.shape = (N, K, 3) +# counts[i] = number of valid neighbors for query i +``` + +::note{icon="i-lucide-info"} +kNN is not supported for form vs form queries. +:: + +### Gathering Element IDs + +`tf.gather_intersecting_ids` and `tf.gather_ids_within_distance` collect all element IDs matching a spatial predicate. + +```python +# Elements that intersect a primitive +ids = tf.gather_intersecting_ids(mesh, polygon) # 1D array of face IDs + +# Elements within a distance of a primitive +ids = tf.gather_ids_within_distance(mesh, point, distance=0.5) # 1D array + +# Form × Form — returns (M, 2) array of ID pairs +pairs = tf.gather_intersecting_ids(mesh_a, mesh_b) +pairs = tf.gather_ids_within_distance(mesh_a, mesh_b, distance=0.5) +``` + +::tip{icon="i-lucide-info"} +For implementation details and custom search patterns, see the [C++ Spatial](/cpp/modules/spatial) documentation. +:: diff --git a/docs/content/py/2.modules/3.topology.md b/docs/content/py/2.modules/03.topology.md similarity index 100% rename from docs/content/py/2.modules/3.topology.md rename to docs/content/py/2.modules/03.topology.md diff --git a/docs/content/py/2.modules/4.geometry.md b/docs/content/py/2.modules/04.geometry.md similarity index 88% rename from docs/content/py/2.modules/4.geometry.md rename to docs/content/py/2.modules/04.geometry.md index 4f34433..9041999 100644 --- a/docs/content/py/2.modules/4.geometry.md +++ b/docs/content/py/2.modules/04.geometry.md @@ -74,35 +74,38 @@ All generated meshes have consistent outward-facing normals (positive orientatio ## Measurements -Compute geometric properties of polygons and meshes. - ### Area -Compute the area of a single polygon or the total surface area of a mesh: +Compute the area of a triangle, polygon, or total surface area of a mesh. Supports single and batch primitives. ```python import trueform as tf import numpy as np -# Single polygon (as ndarray) -polygon = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], dtype=np.float32) -area = tf.area(polygon) # 1.0 +# Single triangle +tri = tf.Triangle(a=[0, 0, 0], b=[1, 0, 0], c=[0, 1, 0]) +tf.area(tri) # 0.5 -# Single polygon (as Polygon object) -poly = tf.Polygon([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]) -area = tf.area(poly) # 1.0 +# Batch of triangles → array of areas +tris = tf.Triangle(np.random.rand(50, 3, 3).astype(np.float32)) +areas = tf.area(tris) # shape (50,) -# 2D polygon -polygon_2d = np.array([[0, 0], [2, 0], [2, 3], [0, 3]], dtype=np.float32) -area = tf.area(polygon_2d) # 6.0 +# Single polygon +quad = tf.Polygon([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]) +tf.area(quad) # 1.0 -# Mesh surface area (from tuple) -faces, points = tf.make_box_mesh(2.0, 3.0, 4.0) -area = tf.area((faces, points)) # 2*(2*3 + 3*4 + 2*4) = 52.0 +# Batch of polygons +polys = tf.Polygon(np.random.rand(50, 4, 3).astype(np.float32)) +areas = tf.area(polys) # shape (50,) -# Mesh surface area (from Mesh object) -mesh = tf.Mesh(faces, points) -area = tf.area(mesh) +# Raw ndarray polygon +polygon = np.array([[0, 0], [2, 0], [2, 3], [0, 3]], dtype=np.float32) +tf.area(polygon) # 6.0 + +# Mesh total surface area +faces, points = tf.make_box_mesh(2.0, 3.0, 4.0) +tf.area((faces, points)) # 52.0 +tf.area(tf.Mesh(faces, points)) # 52.0 ``` ### Volume @@ -110,44 +113,36 @@ area = tf.area(mesh) Compute the volume of a closed 3D mesh: ```python -# From tuple faces, points = tf.make_box_mesh(2.0, 3.0, 4.0) -vol = tf.volume((faces, points)) # 24.0 - -# From Mesh object -mesh = tf.Mesh(faces, points) -vol = tf.volume(mesh) +tf.volume((faces, points)) # 24.0 # Signed volume (positive = outward normals, negative = inward) -sv = tf.signed_volume((faces, points)) - -# Flipped faces give negative volume -sv_flipped = tf.signed_volume((faces[:, ::-1], points)) # -24.0 +tf.signed_volume((faces, points)) # 24.0 +tf.signed_volume((faces[:, ::-1], points)) # -24.0 ``` ### Mean Edge Length -Compute the mean edge length of a polygon mesh: +Compute the mean edge length of a triangle, polygon, or mesh: ```python -# From Mesh object -mel = tf.mean_edge_length(mesh) +# Primitives (single or batch) +tf.mean_edge_length(tri) +tf.mean_edge_length(tris) # mean over all edges in the batch -# From (faces, points) tuple -mel = tf.mean_edge_length((faces, points)) - -# Dynamic polygon mesh (OffsetBlockedArray) -mel = tf.mean_edge_length((dyn_faces, points)) +# Mesh +tf.mean_edge_length(mesh) +tf.mean_edge_length((faces, points)) ``` -| **Function** | **Input** | **Description** | -|--------------|-----------|-----------------| -| `area` | `ndarray`, `Polygon`, `Mesh`, or `(faces, points)` tuple | Area of polygon or total surface area | -| `volume` | `Mesh` or `(faces, points)` tuple (3D only) | Absolute volume of closed mesh | -| `signed_volume` | `Mesh` or `(faces, points)` tuple (3D only) | Signed volume (orientation-dependent) | -| `mean_edge_length` | `Mesh` or `(faces, points)` tuple | Mean edge length across all faces | +### Summary -Tuple inputs accept both `ndarray` and `OffsetBlockedArray` for faces. +| **Function** | **Input** | **Returns** | +|---|---|---| +| `area` | `Triangle`, `Polygon`, `ndarray`, `Mesh`, or tuple | `float` (single/mesh) or `ndarray(N,)` (batch) | +| `volume` | `Mesh` or tuple (3D only) | `float` | +| `signed_volume` | `Mesh` or tuple (3D only) | `float` | +| `mean_edge_length` | `Triangle`, `Polygon`, `Mesh`, or tuple | `float` | ::note{icon="i-lucide-info"} Volume functions require 3D meshes. Signed volume is positive when face normals point outward (CCW winding). @@ -157,47 +152,49 @@ Volume functions require 3D meshes. Signed volume is positive when face normals `area`, `volume`, `signed_volume`, and `mean_edge_length` respect the `Mesh` [transformation](/py/modules/spatial#transformations-on-forms). When a `Mesh` has a transformation, measurements are computed in the transformed coordinate space. :: -## Normal Computation - -Compute face normals and vertex normals for 3D meshes. Normals are lazily computed on first access and cached for subsequent use. +## Normals -### Face Normals +### Primitive Normals -Access face normals via the `normals` property: +`tf.normals()` computes unit normals for 3D Triangle and Polygon primitives, with batch support: ```python import trueform as tf import numpy as np -faces = np.array([[0, 1, 2], [1, 3, 2]], dtype=np.int32) -points = np.array([ - [0, 0, 0], [1, 0, 0], [0.5, 1, 0], [1.5, 1, 0] -], dtype=np.float32) -mesh = tf.Mesh(faces, points) +# Single triangle +tri = tf.Triangle(a=[0, 0, 0], b=[1, 0, 0], c=[0, 1, 0]) +tf.normals(tri) # [0. 0. 1.] -# Get face normals (one unit normal per face) -normals = mesh.normals # shape: (num_faces, 3) - -# Prebuild for performance (optional) -mesh.build_normals() +# Batch of triangles → array of normals +tris = tf.Triangle(np.random.rand(50, 3, 3).astype(np.float32)) +norms = tf.normals(tris) # shape (50, 3), unit vectors -# Set custom normals -mesh.normals = custom_normals +# Polygons work the same way +quad = tf.Polygon([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]) +tf.normals(quad) # [0. 0. 1.] ``` -### Vertex Normals +::warning{icon="i-lucide-alert-triangle"} +Normals require 3D primitives. Passing a 2D triangle or polygon raises `ValueError`. +:: + +### Mesh Normals -Vertex normals are computed by averaging adjacent face normals, weighted by face area: +Face and vertex normals for meshes. Lazily computed on first access and cached. ```python -# Get vertex normals (one unit normal per vertex) +mesh = tf.Mesh(faces, points) + +# Face normals (one unit normal per face) +normals = mesh.normals # shape: (num_faces, 3) + +# Vertex normals (area-weighted average of adjacent face normals) point_normals = mesh.point_normals # shape: (num_points, 3) # Prebuild for performance (optional) +mesh.build_normals() mesh.build_point_normals() - -# Set custom vertex normals -mesh.point_normals = custom_point_normals ``` | **Property** | **Returns** | **Description** | @@ -205,27 +202,13 @@ mesh.point_normals = custom_point_normals | `normals` | `ndarray` | Unit face normals, shape `(num_faces, 3)` | | `point_normals` | `ndarray` | Unit vertex normals, shape `(num_points, 3)` | -::warning{icon="i-lucide-alert-triangle"} -Normals are only supported for 3D meshes. Accessing `normals` or `point_normals` on a 2D mesh raises `ValueError`. -:: - -### Standalone Functions - -For convenience, standalone functions accept either a `Mesh` or a `(faces, points)` tuple: +Standalone functions accept a `Mesh`, `(faces, points)` tuple, or primitives: ```python -import trueform as tf - -# From Mesh +# From Mesh or tuple normals = tf.normals(mesh) -point_normals = tf.point_normals(mesh) - -# From tuple normals = tf.normals((faces, points)) -point_normals = tf.point_normals((faces, points)) - -# Dynamic mesh (OffsetBlockedArray) -normals = tf.normals((dyn_faces, points)) +point_normals = tf.point_normals(mesh) ``` ## Curvature Analysis diff --git a/docs/content/py/2.modules/5.remesh.md b/docs/content/py/2.modules/05.remesh.md similarity index 100% rename from docs/content/py/2.modules/5.remesh.md rename to docs/content/py/2.modules/05.remesh.md diff --git a/docs/content/py/2.modules/6.intersect.md b/docs/content/py/2.modules/06.intersect.md similarity index 98% rename from docs/content/py/2.modules/6.intersect.md rename to docs/content/py/2.modules/06.intersect.md index 8d055df..29cfd96 100644 --- a/docs/content/py/2.modules/6.intersect.md +++ b/docs/content/py/2.modules/06.intersect.md @@ -97,7 +97,7 @@ mesh = tf.Mesh(faces, points) # Create scalar field using distance to a plane (one value per vertex) plane = tf.Plane(normal=[0, 0, 1], offset=0.0) -scalar_field = tf.distance_field(mesh.points, plane) +scalar_field = tf.distance(tf.Point(mesh.points), plane) # Extract isocontours at specific threshold values paths, contour_points = tf.isocontours(mesh, scalar_field, 0.0) diff --git a/docs/content/py/2.modules/7.cut.md b/docs/content/py/2.modules/07.cut.md similarity index 99% rename from docs/content/py/2.modules/7.cut.md rename to docs/content/py/2.modules/07.cut.md index 428d0ae..34fed32 100644 --- a/docs/content/py/2.modules/7.cut.md +++ b/docs/content/py/2.modules/07.cut.md @@ -31,7 +31,7 @@ import numpy as np faces, points = tf.read_stl("terrain.stl") mesh = tf.Mesh(faces, points) plane = tf.Plane(normal=[0, 0, 1], offset=0.0) -scalar_field = tf.distance_field(mesh.points, plane) +scalar_field = tf.distance(tf.Point(mesh.points), plane) # Extract isobands (band_faces, band_points), labels = tf.isobands( diff --git a/docs/content/py/2.modules/8.clean.md b/docs/content/py/2.modules/08.clean.md similarity index 100% rename from docs/content/py/2.modules/8.clean.md rename to docs/content/py/2.modules/08.clean.md diff --git a/docs/content/py/2.modules/9.reidx.md b/docs/content/py/2.modules/09.reidx.md similarity index 100% rename from docs/content/py/2.modules/9.reidx.md rename to docs/content/py/2.modules/09.reidx.md diff --git a/docs/content/py/2.modules/1.core.md b/docs/content/py/2.modules/1.core.md deleted file mode 100644 index 485f162..0000000 --- a/docs/content/py/2.modules/1.core.md +++ /dev/null @@ -1,365 +0,0 @@ ---- -title: Core -description: Core data structures, primitives, and basic geometric queries. -path: /py/modules/core -navigation: - icon: i-lucide-atom ---- - -The Core module provides fundamental geometric primitives and basic queries for geometric processing. - -## Primitives - -Primitives are lightweight wrappers around numpy arrays for type safety and dispatch. - -| **Primitive** | **Description** | **Dimensions** | -|---------------|------------------------------------------|----------------| -| `Point` | Point in 2D or 3D space | 2D, 3D | -| `Ray` | Semi-infinite line (origin + direction) | 2D, 3D | -| `Line` | Infinite line (origin + direction) | 2D, 3D | -| `Plane` | Plane defined by normal and offset | 3D only | -| `Segment` | Line segment between two points | 2D, 3D | -| `Polygon` | Ordered vertices forming a polygon | 2D, 3D | -| `AABB` | Axis-aligned bounding box | 2D, 3D | - -### Points - -A point in 2D or 3D space. - -```python -import trueform as tf -import numpy as np - -# Create a 3D point -point = tf.Point([1.0, 2.0, 3.0]) -print(point.dims) # 3 -print(point.coords) # [1. 2. 3.] - -# Access coordinates -print(point.x, point.y, point.z) # 1.0 2.0 3.0 - -# Factory methods -point2d = tf.Point.from_xy(1.0, 2.0) -point3d = tf.Point.from_xyz(1.0, 2.0, 3.0) -``` - -### Line and Ray - -Lines and rays are composites of an origin point and a direction vector. - -```python -# Ray (semi-infinite line with origin and direction) -ray = tf.Ray(origin=[0, 0, 0], direction=[1, 0, 0]) -print(ray.origin) # [0. 0. 0.] -print(ray.direction) # [1. 0. 0.] - -# Direction is NOT normalized by default -print(ray.normalized_direction) # Get unit vector -print(ray.direction_norm) # Get magnitude - -# Factory method: ray from start point through another point -ray = tf.Ray.from_points(start=[0, 0, 0], through_point=[1, 1, 1]) - -# Line (infinite line through origin with direction) -line = tf.Line(origin=[0, 0, 0], direction=[1, 0, 0]) -# Or from two points -line = tf.Line.from_points([0, 0], [1, 1]) -``` - -### Plane - -A plane is a composite of a normal vector and offset `d`, where the equation is `normal · x + d = 0`. - -```python -# Using normal and a point on the plane -plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 5]) # z = 5 plane -print(plane.normal) # [0. 0. 1.] (normalized) -print(plane.offset) # -5.0 - -# Using plane coefficients directly -plane = tf.Plane(coeffs=[0, 0, 1, -5]) # ax + by + cz + d = 0 - -# Factory methods -plane = tf.Plane.from_point_normal(origin=[0, 0, 5], normal=[0, 0, 1]) -plane = tf.Plane.from_points([0, 0, 0], [1, 0, 0], [0, 1, 0]) # XY plane - -# Note: Plane is 3D only -``` - -### Segment - -A line segment defined by two endpoints. - -```python -# Create segment from endpoints -segment = tf.Segment([[0, 0, 0], [1, 1, 1]]) -print(segment.start) # [0. 0. 0.] -print(segment.end) # [1. 1. 1.] - -# Properties -print(segment.length) # Length of segment -print(segment.midpoint) # Midpoint -print(segment.vector) # Direction vector - -# Factory method -seg = tf.Segment.from_points([0, 0], [1, 1]) -``` - -### Polygon - -A polygon defined by ordered vertices. - -```python -# Create a triangle -triangle = tf.Polygon([[0, 0], [1, 0], [0.5, 1]]) -print(triangle.num_vertices) # 3 -print(triangle.dims) # 2 - -# Access vertices -vertices = triangle.vertices # (3, 2) array -``` - -### AABB - -An axis-aligned bounding box is a composite of a min and max point, representing the minimal and maximal corners. - -```python -# Using min/max -aabb = tf.AABB(min=[0, 0, 0], max=[1, 1, 1]) -print(aabb.min) # [0. 0. 0.] -print(aabb.max) # [1. 1. 1.] -print(aabb.center) # [0.5 0.5 0.5] -print(aabb.size) # [1. 1. 1.] -print(aabb.volume) # 1.0 - -# Using bounds array -aabb = tf.AABB(bounds=[[0, 0, 0], [1, 1, 1]]) - -# Factory methods -aabb = tf.AABB.from_center_size(center=[5, 5], size=[2, 2]) -aabb = tf.AABB.from_points([[0, 0], [1, 0], [1, 1], [0, 1]]) # Bounds all points -``` - -### Transforming Primitives - -Any primitive can be transformed using `tf.transformed(primitive, transformation)`: - -```python -import numpy as np - -# Create a 3D transformation matrix (4x4 homogeneous) -# Translation by (10, 0, 0) -translation = np.eye(4, dtype=np.float32) -translation[:3, 3] = [10, 0, 0] - -# Transform a point -point = tf.Point([1, 2, 3]) -transformed_point = tf.transformed(point, translation) -print(transformed_point.coords) # [11. 2. 3.] - -# Transform a segment -segment = tf.Segment([[0, 0, 0], [1, 0, 0]]) -transformed_segment = tf.transformed(segment, translation) -print(transformed_segment.start) # [10. 0. 0.] -print(transformed_segment.end) # [11. 0. 0.] - -# 90-degree rotation around Z-axis -rotation = np.array([ - [0, -1, 0, 0], - [1, 0, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1] -], dtype=np.float32) - -# Transform a polygon -polygon = tf.Polygon([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) -transformed_polygon = tf.transformed(polygon, rotation) - -# Combined transformation (rotation + translation) -combined = translation @ rotation -transformed = tf.transformed(polygon, combined) -``` - -## Queries on Primitives - -Primitives support distance, intersection, and ray casting queries. - -### Distance and Proximity - -All pairs of primitives support the following queries: - -| **Query** | **Returns** | -|---------------------------|-------------------------------------| -|`distance2` |Squared distance between primitives | -|`distance` |Distance between primitives | -|`closest_metric_point` |`(dist2, point)` on first argument | -|`closest_metric_point_pair`|`(dist2, point0, point1)` | -|`intersects` |`bool` - do the primitives intersect | - -```python -import trueform as tf - -# Squared distance (faster, avoids sqrt) -d2 = tf.distance2(point, polygon) - -# Distance -d = tf.distance(segment0, segment1) - -# Closest point on polygon to a point -dist2, closest_pt = tf.closest_metric_point(polygon, point) - -# Closest points between two primitives -dist2, pt_on_poly0, pt_on_poly1 = tf.closest_metric_point_pair(polygon0, polygon1) - -# Intersection test -do_intersect = tf.intersects(aabb0, aabb1) -``` - -### Ray Casting - -Ray casting tests whether a ray intersects a primitive, returning the parametric distance `t` along the ray (or `None` if no intersection). - -| **Query** | **Returns** | -|-------------|------------------------------------------| -|`ray_cast` |`t` (parametric distance) or `None` | - -**Supported primitives:** `Point`, `Line`, `Ray`, `Segment`, `Plane`, `Polygon`, `AABB` - -```python -ray = tf.Ray(origin=[0.5, 0.3, 2.0], direction=[0, 0, -1]) -polygon = tf.Polygon([[0, 0, 0], [2, 0, 0], [1, 2, 0]]) - -# Basic ray cast -t = tf.ray_cast(ray, polygon) -if t is not None: - hit_point = ray.origin + t * ray.direction - print(f"Hit at {hit_point}, t={t}") - -# With parametric bounds [min_t, max_t] -aabb = tf.AABB(min=[0, 0, 0], max=[1, 1, 1]) -t = tf.ray_cast(ray, aabb, config=(0.0, 100.0)) -``` - -## Data Structures - -### OffsetBlockedArray - -`OffsetBlockedArray` is a data structure for representing variable-length blocks of data, commonly used for curves and paths returned by various trueform functions. It efficiently stores multiple sequences of different lengths using two arrays. - -#### Structure - -The data structure consists of: -- **offsets**: Array of block boundaries (starting at 0, ending at `len(data)`) -- **data**: Packed array containing all block elements - -For example, to represent 3 curves with 3, 4, and 2 points respectively: -```python -offsets = [0, 3, 7, 9] # Block boundaries -data = [0,1,2, 3,4,5,6, 7,8] # Packed data -# Block 0: data[0:3] = [0,1,2] -# Block 1: data[3:7] = [3,4,5,6] -# Block 2: data[7:9] = [7,8] -``` - -#### Usage - -Functions like `intersection_curves` and `isocontours` return paths as `OffsetBlockedArray`: - -```python -import trueform as tf -import numpy as np - -# Example: intersection_curves returns (paths, points) -paths, curve_points = tf.intersection_curves(mesh1, mesh2) - -# paths is an OffsetBlockedArray -# curve_points is a numpy array of all curve vertices - -# Get number of curves -print(f"Number of curves: {len(paths)}") - -# Iterate over each curve -for i, path_indices in enumerate(paths): - # path_indices is a numpy array of indices into curve_points - pts = curve_points[path_indices] - print(f"Curve {i}: {len(pts)} points") - -# Access individual curves by index -first_curve_indices = paths[0] -first_curve_points = curve_points[first_curve_indices] - -# Access underlying arrays -print(f"Offsets: {paths.offsets}") -print(f"Data: {paths.data}") -``` - -#### Creating OffsetBlockedArray - -You can create an `OffsetBlockedArray` directly from offsets and data arrays: - -```python -# Create from offsets and data arrays -offsets = np.array([0, 3, 7, 10], dtype=np.int32) -data = np.array([0,1,2, 3,4,5,6, 7,8,9], dtype=np.int32) - -blocks = tf.OffsetBlockedArray(offsets, data) - -# Iterate over blocks -for block in blocks: - print(block) # Each block is a view into the data array - -# Access by index -print(blocks[0]) # [0 1 2] -print(blocks[1]) # [3 4 5 6] -print(blocks[2]) # [7 8 9] -``` - -#### Creating from Uniform Arrays - -For uniform arrays (where all blocks have the same size), use `from_uniform()` or the convenience function `tf.as_offset_blocked()`: - -```python -import trueform as tf -import numpy as np - -# Quad faces as a 2D array (4 vertices per face) -quads = np.array([ - [0, 1, 2, 3], - [4, 5, 6, 7], - [8, 9, 10, 11] -], dtype=np.int32) - -# Method 1: Class method -faces = tf.OffsetBlockedArray.from_uniform(quads) - -# Method 2: Factory function (equivalent) -faces = tf.as_offset_blocked(quads) - -# Now create a mesh with dynamic face support -mesh = tf.Mesh(faces, points) -print(mesh.is_dynamic) # True -print(mesh.ngon) # 4 (uniform quad mesh) -``` - -This is particularly useful for creating meshes with quad or higher-order polygon faces, enabling the full dynamic mesh pipeline while maintaining the familiar 2D array input format. - -#### Properties - -| **Property** | **Returns** | **Description** | -|--------------|-------------|-----------------| -| `offsets` | `np.ndarray` | Block boundary indices | -| `data` | `np.ndarray` | Packed data array | -| `dtype` | `np.dtype` | Data type of the data array | - -#### Requirements - -- Both `offsets` and `data` must have the same dtype (int32 or int64) -- `offsets[0]` must be 0 -- `offsets[-1]` must equal `len(data)` -- `offsets` must be non-decreasing - -This data structure is memory-efficient and enables zero-copy iteration over variable-length sequences. - -::tip{icon="i-lucide-info"} -For queries on forms (Mesh, EdgeMesh, PointCloud) with spatial acceleration, see the [Spatial](/py/modules/spatial) module. For file I/O operations, see the [I/O](/py/modules/io) module. For detailed implementation, see the [C++ Core](/cpp/modules/core) documentation. -:: diff --git a/docs/content/py/2.modules/2.spatial.md b/docs/content/py/2.modules/2.spatial.md deleted file mode 100644 index c246f82..0000000 --- a/docs/content/py/2.modules/2.spatial.md +++ /dev/null @@ -1,335 +0,0 @@ ---- -title: Spatial -description: Spatial queries and acceleration structures for efficient geometric processing. -path: /py/modules/spatial -navigation: - icon: i-lucide-scale-3d ---- - -The Spatial module extends core queries to forms with spatial acceleration structures. - -## Forms - -In trueform, `Mesh`, `EdgeMesh`, and `PointCloud` represent **forms** - geometric objects equipped with spatial acceleration structures. These structures enable efficient queries on large datasets by leveraging the underlying `tf::tree` spatial hierarchy. - -| **Form** | **Description** | **Elements** | **Dimensions** | -|----------------|----------------------------------------------------|------------------------------|----------------| -| `PointCloud` | Collection of points | Points | 2D, 3D | -| `EdgeMesh` | Collection of line segments (edges) | Edges (2 verts) | 2D, 3D | -| `Mesh` | Polygonal mesh (triangles or dynamic n-gons) | Faces (triangles or n-gons) | 2D, 3D | - -::tip{icon="i-lucide-zap"} -**Spatial Tree Building**: The spatial tree is built automatically on first use of any spatial query. For performance-critical applications, you can prebuild the tree using `form.build_tree()` to avoid the cost during the first query. -:: - -### PointCloud - -Represents a collection of points: - -```python -import trueform as tf -import numpy as np - -# Create point cloud -points = np.random.rand(100, 3).astype(np.float32) -cloud = tf.PointCloud(points) - -# Access properties -print(f"Point cloud has {cloud.size} points in {cloud.dims}D") - -# Prebuild spatial tree for performance (optional) -cloud.build_tree() # Tree is built automatically on first query if not called -``` - -| **Property** | **Returns** | **Description** | -|------------------|-------------------|------------------------------------------| -| `points` | `ndarray` | Vertex coordinates | -| `size` | `int` | Number of points | -| `dims` | `int` | Dimensionality (2 or 3) | -| `dtype` | `np.dtype` | Data type (`float32` or `float64`) | -| `transformation` | `ndarray` or `None` | Transformation matrix | - -### EdgeMesh - -Represents a mesh of line segments: - -```python -# Create edge mesh (polyline or collection of segments) -edges = np.array([[0, 1], [1, 2], [2, 3]], dtype=np.int32) -points = np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32) -edge_mesh = tf.EdgeMesh(edges, points) -``` - -| **Property** | **Returns** | **Description** | -|--------------------|---------------------|------------------------------------------| -| `edges` | `ndarray` | Edge connectivity (N, 2) | -| `points` | `ndarray` | Vertex coordinates | -| `number_of_edges` | `int` | Number of edges | -| `number_of_points` | `int` | Number of vertices | -| `dims` | `int` | Dimensionality (2 or 3) | -| `dtype` | `np.dtype` | Data type (`float32` or `float64`) | -| `transformation` | `ndarray` or `None` | Transformation matrix | - -EdgeMesh also provides topology properties. See [Topology](/py/modules/topology) for details: - -| **Property** | **Returns** | **Description** | -|--------------------|-----------------------|----------------------------------------| -| `edge_membership` | `OffsetBlockedArray` | Vertex → edges containing it | -| `vertex_link` | `OffsetBlockedArray` | Vertex → connected vertices | - -### Mesh - -Represents a polygonal mesh. Supports both fixed-size triangles and dynamic n-gons via `OffsetBlockedArray`: - -```python -import trueform as tf -import numpy as np - -# Triangle mesh (fixed-size, 3 vertices per face) -faces = np.array([[0, 1, 2], [1, 2, 3]], dtype=np.int32) -points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]], dtype=np.float32) -mesh = tf.Mesh(faces, points) - -print(mesh.is_dynamic) # False -print(mesh.ngon) # 3 (triangles) - -# Dynamic mesh (variable-sized faces using OffsetBlockedArray) -quads = np.array([[0, 1, 2, 3], [4, 5, 6, 7]], dtype=np.int32) -faces = tf.as_offset_blocked(quads) # Convert to OffsetBlockedArray -mesh = tf.Mesh(faces, points) - -print(mesh.is_dynamic) # True -print(mesh.ngon) # None (variable-sized) -``` - -| **Property** | **Returns** | **Description** | -|--------------------|-----------------------------------|------------------------------------------------------| -| `faces` | `ndarray` or `OffsetBlockedArray` | Face connectivity | -| `points` | `ndarray` | Vertex coordinates | -| `number_of_faces` | `int` | Number of faces | -| `number_of_points` | `int` | Number of vertices | -| `dims` | `int` | Dimensionality (2 or 3) | -| `is_dynamic` | `bool` | True if mesh uses variable-sized faces | -| `ngon` | `int` or `None` | Vertices per face (3 for triangles, None if dynamic) | -| `dtype` | `np.dtype` | Data type (`float32` or `float64`) | -| `transformation` | `ndarray` or `None` | Transformation matrix | - -Mesh also provides topology and geometry properties. See [Topology](/py/modules/topology) and [Geometry](/py/modules/geometry) for details: - -| **Property** | **Returns** | **Description** | -|----------------------|-----------------------------------|----------------------------------------| -| `face_membership` | `OffsetBlockedArray` | Vertex → faces containing it | -| `manifold_edge_link` | `ndarray` or `OffsetBlockedArray` | Face edge → adjacent face | -| `face_link` | `OffsetBlockedArray` | Face → adjacent faces | -| `vertex_link` | `OffsetBlockedArray` | Vertex → connected vertices | -| `normals` | `ndarray` | Face normals (3D only) | -| `point_normals` | `ndarray` | Vertex normals (3D only) | - -## Transformations on Forms - -Forms can be equipped with a transformation matrix, enabling queries on moving or transformed geometry without modifying the underlying data or rebuilding acceleration structures. - -```python -import numpy as np - -# Create a 3D transformation matrix (4x4 homogeneous) -# Translation by (5, 0, 0) -translation = np.eye(4, dtype=np.float32) -translation[:3, 3] = [5, 0, 0] - -# Attach transformation when creating the form -mesh = tf.Mesh(faces, points, transformation=translation) - -# Or set transformation later -mesh.transformation = translation - -# Queries automatically use the transformation -d = tf.distance(mesh, other_mesh) # Queries mesh in transformed pose - -# 2D rotation + translation (3x3 homogeneous) -angle = np.radians(45) -transform_2d = np.array([ - [np.cos(angle), -np.sin(angle), 10], - [np.sin(angle), np.cos(angle), 20], - [0, 0, 1] -], dtype=np.float32) - -edge_mesh = tf.EdgeMesh(edges, points_2d, transformation=transform_2d) -``` - -All forms support transformations: -- **PointCloud**: Transforms points during spatial queries -- **EdgeMesh**: Transforms edges during spatial queries -- **Mesh**: Transforms faces during spatial queries - -The transformation is applied internally during queries—the original data remains unchanged. - -## Shared Views - -All forms support `shared_view()` to create a new instance that shares the same underlying data (points, faces/edges, and cached structures like the spatial tree) but has its own transformation. This is useful for efficiently querying the same geometry at multiple poses. - -```python -# Load mesh and build acceleration structures -mesh = tf.Mesh(faces, points) -mesh.build_tree() -mesh.build_face_membership() -mesh.build_manifold_edge_link() - -# Create shared views with different transformations -mesh_a = mesh.shared_view() -mesh_a.transformation = transform_A - -mesh_b = mesh.shared_view() -mesh_b.transformation = transform_B - -# Both share the same data and tree - only transformation differs -# Queries use the respective transformations -d = tf.distance(mesh_a, mesh_b) -curves = tf.intersection_curves(mesh_a, mesh_b) -``` - -| **Method** | **Returns** | **Description** | -|----------------|--------------------|----------------------------------------------------| -| `shared_view()`| Same form type | New instance sharing data, with no transformation | - -::tip{icon="i-lucide-zap"} -**Performance**: Shared views avoid duplicating geometry and acceleration structures. Build the tree and topology structures once on the original form, then create shared views for different poses. All views benefit from the cached structures. -:: - -## Spatial Queries - -Queries on primitives extend to forms via tree-accelerated search. All queries support both **form vs primitive** and **form vs form**. - -### Distance and Intersection - -| **Query** | **Returns** | -|--------------|-------------| -| `distance2` | Squared distance | -| `distance` | Distance | -| `intersects` | `bool` | - -**Supported primitives:** `Point`, `Segment`, `Line`, `Ray`, `Polygon`, `Plane` - -```python -import trueform as tf - -# Form vs primitive -d2 = tf.distance2(mesh, point) -d = tf.distance(mesh, segment) -hit = tf.intersects(mesh, polygon) - -# Form vs form -d2 = tf.distance2(mesh0, mesh1) -collide = tf.intersects(mesh0, mesh1) -``` - -### Neighbor Search - -Generalizes `closest_metric_point` from Core with tree acceleration, optional search radius, and kNN support. - -**Form vs primitive** returns `(element_id, distance_squared, closest_point)` or `None` if radius specified and nothing found. - -**Form vs form** returns `((id0, id1), (distance_squared, pt0, pt1))` or `None` if radius specified and nothing found. - -**Supported primitives:** `Point`, `Segment`, `Line`, `Ray`, `Polygon`, `Plane` - -```python -# Nearest neighbor (always finds one) -idx, dist2, pt = tf.neighbor_search(mesh, point) - -# Nearest within radius (may not find any) -result = tf.neighbor_search(mesh, point, radius=1.0) -if result is not None: - idx, dist2, pt = result - -# Form vs form -(id0, id1), (dist2, pt0, pt1) = tf.neighbor_search(mesh0, mesh1) - -# Form vs form within radius -result = tf.neighbor_search(mesh0, mesh1, radius=1.0) -if result is not None: - (id0, id1), (dist2, pt0, pt1) = result -``` - -#### kNN Queries - -For k nearest neighbors, pass `k` parameter: - -```python -# Find 10 nearest neighbors -neighbors = tf.neighbor_search(mesh, point, k=10) -for idx, dist2, pt in neighbors: - # process neighbor (sorted by distance) - pass - -# With radius limit (may find fewer than k) -neighbors = tf.neighbor_search(mesh, point, k=10, radius=5.0) -``` - -### Ray Casting - -Extends ray casting to forms. Returns `(element_id, t)` or `None`. - -```python -config = (0.0, 100.0) # (min_t, max_t) - -result = tf.ray_cast(ray, mesh, config) -if result is not None: - face_id, t = result - hit_point = ray.origin + t * ray.direction -``` - -## Gathering Primitive IDs - -`gather_ids` collects primitive IDs matching spatial criteria. - -### Gather Intersecting IDs - -**Supported primitives:** `Point`, `Segment`, `Line`, `Ray`, `Polygon`, `Plane` - -```python -# Form vs primitive - returns array of element IDs -ids = tf.gather_intersecting_ids(mesh, polygon) - -# Form vs form - returns Nx2 array of ID pairs -pairs = tf.gather_intersecting_ids(mesh0, mesh1) -for id0, id1 in pairs: - # process intersecting pair - pass -``` - -### Gather IDs Within Distance - -**Supported primitives:** `Point`, `Segment`, `Line`, `Ray`, `Polygon`, `Plane` - -```python -# Form vs primitive - returns array of element IDs within distance -ids = tf.gather_ids_within_distance(mesh, point, distance=0.5) - -# Form vs form - returns Nx2 array of ID pairs -pairs = tf.gather_ids_within_distance(mesh0, mesh1, distance=0.5) -``` - -## Distance Fields - -Compute vectorized distances from many points to a primitive: - -```python -# Distance from points to a polygon -query_points = np.random.rand(1000, 3).astype(np.float32) -polygon = tf.Polygon([[0, 0, 0], [1, 0, 0], [0.5, 1, 0]]) -distances = tf.distance_field(query_points, polygon) -# Returns array of shape (1000,) with unsigned distances - -# Signed distance to a plane (negative inside, positive outside) -plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 5]) -signed_distances = tf.distance_field(query_points, plane) - -# Works with PointCloud input too -cloud = tf.PointCloud(query_points) -distances = tf.distance_field(cloud, plane) -``` - -::tip{icon="i-lucide-info"} -All spatial queries leverage spatial acceleration structures for efficient computation on large datasets. For implementation details, see the [C++ Spatial](/cpp/modules/spatial) documentation. -:: diff --git a/docs/content/py/5.examples/0.index.md b/docs/content/py/5.examples/0.index.md index 071abf2..3d18c98 100644 --- a/docs/content/py/5.examples/0.index.md +++ b/docs/content/py/5.examples/0.index.md @@ -18,6 +18,7 @@ cd trueform/python/examples | Script | Description | |--------|-------------| | [alignment.py](https://github.com/polydera/trueform/blob/main/python/examples/alignment.py) | Point cloud alignment with rigid, OBB, and ICP | +| [raycast_render.py](https://github.com/polydera/trueform/blob/main/python/examples/raycast_render.py) | Batch ray cast rendering with Lambertian shading | ## Interactive VTK Examples @@ -52,6 +53,15 @@ Step-by-step walkthroughs in the documentation: Primitives, spatial queries, topology, and boolean operations. ::: + :::card + --- + icon: i-lucide-scan-line + title: Raycast Rendering + to: /py/examples/raycast-rendering + --- + Batch ray casting, face normal shading, and matplotlib rendering. + ::: + :::card --- icon: i-lucide-grid-2x2 diff --git a/docs/content/py/5.examples/2.core-functionality.md b/docs/content/py/5.examples/2.core-functionality.md index b056e52..d7598c7 100644 --- a/docs/content/py/5.examples/2.core-functionality.md +++ b/docs/content/py/5.examples/2.core-functionality.md @@ -217,7 +217,7 @@ Demonstrates extracting isocontours from scalar fields on meshes. ### Features Showcased -- Creating scalar fields with `tf.distance_field` +- Creating scalar fields with `tf.distance` - Extracting isocontours at threshold values - Iterating over resulting curves @@ -239,7 +239,7 @@ mesh = tf.Mesh(faces, points) # Create scalar field (signed distance from plane) plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 0.5]) -scalar_field = tf.distance_field(mesh.points, plane) +scalar_field = tf.distance(tf.Point(mesh.points), plane) # Extract isocontour at threshold paths, curve_points = tf.isocontours(mesh, scalar_field, 0.0) diff --git a/docs/content/py/5.examples/3.vtk-integration.md b/docs/content/py/5.examples/3.vtk-integration.md index 4d4bdef..a928876 100644 --- a/docs/content/py/5.examples/3.vtk-integration.md +++ b/docs/content/py/5.examples/3.vtk-integration.md @@ -266,7 +266,7 @@ python isobands.py mesh.stl ### Features Showcased -- Scalar field from plane distance with `tf.distance_field` +- Scalar field from plane distance with `tf.distance` - Isoband extraction with `tf.isobands` - Boundary curve extraction @@ -281,7 +281,7 @@ normal = np.array([1.0, 2.0, 1.0], dtype=mesh.dtype) normal /= np.linalg.norm(normal) plane = tf.Plane.from_point_normal(center, normal) -scalars = tf.distance_field(mesh.points, plane) +scalars = tf.distance(tf.Point(mesh.points), plane) cut_values = np.array([-1.0, -0.5, 0.0, 0.5, 1.0], dtype=np.float32) selected_bands = np.array([0, 2, 4], dtype=np.int32) @@ -312,7 +312,7 @@ import numpy as np # Define scalar field plane = tf.Plane(normal=[0, 0, 1], origin=[0, 0, 0]) -scalars = tf.distance_field(mesh.points, plane) +scalars = tf.distance(tf.Point(mesh.points), plane) # Single threshold paths, curve_points = tf.isocontours(mesh, scalars, 0.0) @@ -345,7 +345,7 @@ python cross_section.py mesh.stl ### Features Showcased -- Scalar field from plane distance with `tf.distance_field` +- Scalar field from plane distance with `tf.distance` - Isocontour extraction with `tf.isocontours` - Curve triangulation with `tf.triangulated` @@ -360,7 +360,7 @@ normal = np.array([1.0, 2.0, 1.0], dtype=mesh.dtype) normal /= np.linalg.norm(normal) plane = tf.Plane.from_point_normal(center, normal) -scalars = tf.distance_field(mesh.points, plane) +scalars = tf.distance(tf.Point(mesh.points), plane) paths, curve_points = tf.isocontours(mesh, scalars, cut_value) diff --git a/docs/content/py/5.examples/4.raycast-rendering.md b/docs/content/py/5.examples/4.raycast-rendering.md new file mode 100644 index 0000000..db79a45 --- /dev/null +++ b/docs/content/py/5.examples/4.raycast-rendering.md @@ -0,0 +1,142 @@ +--- +title: Raycast Rendering +description: Render a mesh image using batch ray casting, face normals, and matplotlib. +navigation: + icon: i-lucide-scan-line +--- + +**Source:** [raycast_render.py](https://github.com/polydera/trueform/blob/main/python/examples/raycast_render.py) + +Ray-traced rendering of a mesh using an orthographic camera. Generates a grid of rays (one per pixel), batch ray_casts them against the mesh in a single call, and renders Lambertian shading from face normals. + +```bash +python raycast_render.py [mesh.stl] [--resolution 512] +``` + +### Features Showcased + +- Batch `tf.Ray` construction from numpy arrays +- Batch `tf.ray_cast` returning `(element_ids, t_values)` per ray +- Looking up `mesh.normals` by hit face ID +- Two-light Lambertian shading with rim lighting + +## Loading the Mesh + +```python [load.py] +import numpy as np +import trueform as tf + +faces, points = tf.read_stl("dragon.stl") +mesh = tf.Mesh(faces, points) +mesh.build_tree() +``` + +## Setting Up the Camera + +An orthographic camera looking down the -Z axis. The ray grid spans the mesh bounding box with 10% padding. + +```python [camera.py] +aabb_min = points.min(axis=0) +aabb_max = points.max(axis=0) +size = aabb_max - aabb_min +pad = size.max() * 0.1 +res = 512 + +x = np.linspace(aabb_min[0] - pad, aabb_max[0] + pad, res).astype(points.dtype) +y = np.linspace(aabb_max[1] + pad, aabb_min[1] - pad, res).astype(points.dtype) +gx, gy = np.meshgrid(x, y) +n_rays = res * res + +origins = np.zeros((n_rays, 3), dtype=points.dtype) +origins[:, 0] = gx.ravel() +origins[:, 1] = gy.ravel() +origins[:, 2] = aabb_max[2] + pad + +directions = np.zeros((n_rays, 3), dtype=points.dtype) +directions[:, 2] = -1.0 +``` + +## Batch Ray Casting + +Construct a batch of rays and cast them all against the mesh in a single call. The result is a tuple of `(element_ids, t_values)` — one entry per ray. Misses have `-1` for the element ID and `NaN` for t. + +```python [raycast.py] +rays = tf.Ray(origin=origins, direction=directions) + +ids, ts = tf.ray_cast(rays, mesh) + +ids = ids.reshape(res, res) +ts = ts.reshape(res, res) +hit = ~np.isnan(ts) +``` + +::tip{icon="i-lucide-zap"} +All 262,144 rays (512x512) are cast in a single `tf.ray_cast` call. The C++ backend parallelizes automatically for batches above 1,000 rays. +:: + +## Shading + +Look up face normals by hit element ID. Two directional lights (key + fill) provide Lambertian diffuse shading, and a Fresnel-like rim term adds bright edges where the surface is nearly perpendicular to the camera. + +```python [shading.py] +normals = mesh.normals +hit_ids = ids[hit] +hit_normals = normals[hit_ids].copy() + +# Flip back-facing normals +hit_normals[hit_normals[:, 2] > 0] *= -1 + +# Key light + fill light +key_dir = np.array([0.4, 0.6, -0.7], dtype=np.float32) +key_dir /= np.linalg.norm(key_dir) +fill_dir = np.array([-0.5, -0.3, -0.6], dtype=np.float32) +fill_dir /= np.linalg.norm(fill_dir) + +key = np.clip(hit_normals @ key_dir, 0.0, 1.0) +fill = np.clip(hit_normals @ fill_dir, 0.0, 1.0) +diffuse = 0.08 + 0.72 * key + 0.20 * fill + +# Rim lighting +view_dir = np.array([0.0, 0.0, -1.0], dtype=np.float32) +facing = np.abs(hit_normals @ view_dir) +rim = (1.0 - facing) ** 3 +intensity = diffuse + 0.35 * rim +``` + +## Compositing the Image + +Map intensity to a teal color ramp on a dark background and display with matplotlib. + +```python [display.py] +import matplotlib.pyplot as plt + +BG = np.array([27, 43, 52], dtype=np.float32) / 255 +TEAL = np.array([0.0, 0.659, 0.604]) +TEAL_BRIGHT = np.array([0.0, 0.835, 0.745]) + +image = np.empty((res, res, 3), dtype=np.float32) +image[:] = BG + +color = TEAL + np.clip(intensity, 0, 1)[:, np.newaxis] * (TEAL_BRIGHT - TEAL) +image[hit] = color * intensity[:, np.newaxis] +image = np.clip(image, 0, 1) + +fig, ax = plt.subplots(1, 1, figsize=(8, 8), facecolor=BG) +ax.imshow(image) +ax.set_axis_off() +fig.patch.set_facecolor(BG) +plt.show() +``` + +## Batch Ray Cast Return Values + +| Rays | Target | Returns | Misses | +|------|--------|---------|--------| +| single | primitive | `float` (t) or `None` | `None` | +| single | form | `(element_id, t)` or `None` | `None` | +| batch(N) | primitive | `ndarray(N,)` | NaN entries | +| batch(N) | form | `(ndarray[N] ids, ndarray[N] ts)` | -1 / NaN entries | + +::tip{icon="i-lucide-info"} +Single ray cast against a form returns `(element_id, t)`. The batch version returns the same fields as parallel arrays, with `-1` and `NaN` for misses. The `config` parameter accepts per-ray arrays `(min_ts, max_ts)` for variable-depth queries — useful for progressive nearest-hit across multiple meshes. +:: diff --git a/docs/package.json b/docs/package.json index 3e59379..96c9c8d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "typecheck": "nuxt typecheck" }, "dependencies": { - "@iconify-json/lucide": "^1.2.94", + "@iconify-json/lucide": "^1.2.95", "@iconify-json/simple-icons": "^1.2.71", "@iconify-json/vscode-icons": "^1.2.44", "@nuxt/content": "^3.12.0", @@ -54,7 +54,8 @@ "tar@<=7.5.3": ">=7.5.4", "lodash-es@>=4.0.0 <=4.17.22": ">=4.17.23", "lodash@>=4.0.0 <=4.17.22": ">=4.17.23", - "tar@<7.5.7": ">=7.5.7" + "tar@<7.5.7": ">=7.5.7", + "serialize-javascript@<=7.0.2": ">=7.0.3" }, "ignoredBuiltDependencies": [ "@parcel/watcher", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index f4de899..dd311ea 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -14,14 +14,15 @@ overrides: lodash-es@>=4.0.0 <=4.17.22: '>=4.17.23' lodash@>=4.0.0 <=4.17.22: '>=4.17.23' tar@<7.5.7: '>=7.5.7' + serialize-javascript@<=7.0.2: '>=7.0.3' importers: .: dependencies: '@iconify-json/lucide': - specifier: ^1.2.94 - version: 1.2.94 + specifier: ^1.2.95 + version: 1.2.95 '@iconify-json/simple-icons': specifier: ^1.2.71 version: 1.2.71 @@ -603,8 +604,8 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@iconify-json/lucide@1.2.94': - resolution: {integrity: sha512-P54R7gqZs3t1GZQmzW2WkXIkGb9RPCq77iCb7IDU94IIsVegzuSQg52hSTjhvDKRXGUDc1HdwOBIuqg3iLIluw==} + '@iconify-json/lucide@1.2.95': + resolution: {integrity: sha512-SxDM/NEJtcGGAiLAnaZ7rcmVxkJI8esswTZLCm5BfNcoPf/yawIImb4nNfsu4dtFzP/Cl6KRg+vZq521zOUOnQ==} '@iconify-json/simple-icons@1.2.71': resolution: {integrity: sha512-rNoDFbq1fAYiEexBvrw613/xiUOPEu5MKVV/X8lI64AgdTzLQUUemr9f9fplxUMPoxCBP2rWzlhOEeTHk/Sf0Q==} @@ -612,8 +613,8 @@ packages: '@iconify-json/vscode-icons@1.2.44': resolution: {integrity: sha512-3fLOIRRtsm6HD6UPJ3Y6/UztqxNTYgKA8VxrWeg1C+042MD3A/06CkWSsii1pz/f1zl0+YxvCHIVM3tXUlho+A==} - '@iconify/collections@1.0.654': - resolution: {integrity: sha512-xpxyDlrndFo7z6tRyybLA8U/fzX5b+EZThqqudjbfDRknLWpjQykefbCZLFvp/XMRJCWk75JN0jFtG1Cw+Dbsw==} + '@iconify/collections@1.0.655': + resolution: {integrity: sha512-XSSKhI0AwBcFhDnJkWFMDKj9KlH+A67cc4AV6vTM6O3ake6YMPNpSeN1nwe0KHoQMqAnbn2IRz+YaW0J0X6uTQ==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -5700,9 +5701,6 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -5928,8 +5926,9 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serialize-javascript@7.0.3: + resolution: {integrity: sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww==} + engines: {node: '>=20.0.0'} seroval@1.5.0: resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} @@ -6141,8 +6140,8 @@ packages: striptags@3.2.0: resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} - strnum@2.1.2: - resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + strnum@2.2.0: + resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} structured-clone-es@1.0.0: resolution: {integrity: sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==} @@ -7418,7 +7417,7 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@iconify-json/lucide@1.2.94': + '@iconify-json/lucide@1.2.95': dependencies: '@iconify/types': 2.0.0 @@ -7430,7 +7429,7 @@ snapshots: dependencies: '@iconify/types': 2.0.0 - '@iconify/collections@1.0.654': + '@iconify/collections@1.0.655': dependencies: '@iconify/types': 2.0.0 @@ -7936,7 +7935,7 @@ snapshots: '@nuxt/icon@2.2.1(magicast@0.5.2)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': dependencies: - '@iconify/collections': 1.0.654 + '@iconify/collections': 1.0.655 '@iconify/types': 2.0.0 '@iconify/utils': 3.1.0 '@iconify/vue': 5.0.0(vue@3.5.29(typescript@5.9.3)) @@ -8783,7 +8782,7 @@ snapshots: '@rollup/plugin-terser@0.4.4(rollup@4.59.0)': dependencies: - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.3 smob: 1.6.1 terser: 5.46.0 optionalDependencies: @@ -11242,7 +11241,7 @@ snapshots: fast-xml-parser@5.4.1: dependencies: fast-xml-builder: 1.0.0 - strnum: 2.1.2 + strnum: 2.2.0 fastq@1.20.1: dependencies: @@ -13597,10 +13596,6 @@ snapshots: radix3@1.1.2: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - range-parser@1.2.1: {} rc9@2.1.2: @@ -13943,9 +13938,7 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 + serialize-javascript@7.0.3: {} seroval@1.5.0: {} @@ -14197,7 +14190,7 @@ snapshots: striptags@3.2.0: {} - strnum@2.1.2: {} + strnum@2.2.0: {} structured-clone-es@1.0.0: {} diff --git a/include/trueform/core/closest_metric_point_pair.hpp b/include/trueform/core/closest_metric_point_pair.hpp index 71dec6a..c07c698 100644 --- a/include/trueform/core/closest_metric_point_pair.hpp +++ b/include/trueform/core/closest_metric_point_pair.hpp @@ -784,42 +784,65 @@ template auto closest_metric_point_pair(const tf::aabb_like &aabb, const tf::polygon &poly_in) { using T = tf::coordinate_type; - const auto &poly = tf::tag_plane(poly_in); - // Face case: support vertex projected onto polygon plane - auto d_center = - tf::dot(poly.plane().normal, aabb.center()) + poly.plane().d; - tf::point support; - for (std::size_t i = 0; i < Dims; ++i) { - bool pick_min = (poly.plane().normal[i] >= T(0)) == (d_center > T(0)); - support[i] = pick_min ? aabb.min[i] : aabb.max[i]; - } - auto d = tf::dot(poly.plane().normal, support) + poly.plane().d; + tf::point dummy = tf::zero; auto best = tf::make_metric_point_pair(std::numeric_limits::max(), - support, support); - if (d * d_center <= T(0)) { - // Polygon plane intersects the AABB. Project center onto plane, - // clamp to AABB, and check if that point is inside the polygon face. - auto center = aabb.center(); - auto pt = center - d_center * poly.plane().normal; - for (std::size_t i = 0; i < Dims; ++i) - pt[i] = std::min(std::max(pt[i], T(aabb.min[i])), T(aabb.max[i])); - if (tf::contains_coplanar_point(poly, pt)) - return tf::make_metric_point_pair(T(0), pt, pt); + dummy, dummy); + if constexpr (Dims >= 3) { + const auto &poly = tf::tag_plane(poly_in); + // Face case: support vertex projected onto polygon plane + auto d_center = + tf::dot(poly.plane().normal, aabb.center()) + poly.plane().d; + tf::point support; + for (std::size_t i = 0; i < Dims; ++i) { + bool pick_min = (poly.plane().normal[i] >= T(0)) == (d_center > T(0)); + support[i] = pick_min ? aabb.min[i] : aabb.max[i]; + } + auto d = tf::dot(poly.plane().normal, support) + poly.plane().d; + best = tf::make_metric_point_pair(std::numeric_limits::max(), + support, support); + if (d * d_center <= T(0)) { + // Polygon plane intersects the AABB. Project center onto plane, + // clamp to AABB, and check if that point is inside the polygon face. + auto center = aabb.center(); + auto pt = center - d_center * poly.plane().normal; + for (std::size_t i = 0; i < Dims; ++i) + pt[i] = std::min(std::max(pt[i], T(aabb.min[i])), T(aabb.max[i])); + if (tf::contains_coplanar_point(poly, pt)) + return tf::make_metric_point_pair(T(0), pt, pt); + } else { + auto proj = support - d * poly.plane().normal; + if (tf::contains_coplanar_point(poly, proj)) + best = tf::make_metric_point_pair(d * d, support, proj); + } + // Edge cases + std::size_t size = poly.size(); + std::size_t prev = size - 1; + for (std::size_t i = 0; i < size; prev = i++) { + auto seg = tf::make_segment_between_points(poly[prev], poly[i]); + auto tmp = tf::closest_metric_point_pair(aabb, seg); + if (tmp.metric < best.metric) + best = tmp; + if (best.metric < tf::epsilon2) + return best; + } } else { - auto proj = support - d * poly.plane().normal; - if (tf::contains_coplanar_point(poly, proj)) - best = tf::make_metric_point_pair(d * d, support, proj); - } - // Edge cases - std::size_t size = poly.size(); - std::size_t prev = size - 1; - for (std::size_t i = 0; i < size; prev = i++) { - auto seg = tf::make_segment_between_points(poly[prev], poly[i]); - auto tmp = tf::closest_metric_point_pair(aabb, seg); - if (tmp.metric < best.metric) - best = tmp; - if (best.metric < tf::epsilon2) - return best; + // 2D: no plane available. Check containment + edge loop. + tf::point center; + for (std::size_t i = 0; i < Dims; ++i) + center[i] = (T(aabb.min[i]) + T(aabb.max[i])) / T(2); + if (tf::contains_coplanar_point(poly_in, center)) + return tf::make_metric_point_pair(T(0), center, center); + // Edge cases + std::size_t size = poly_in.size(); + std::size_t prev = size - 1; + for (std::size_t i = 0; i < size; prev = i++) { + auto seg = tf::make_segment_between_points(poly_in[prev], poly_in[i]); + auto tmp = tf::closest_metric_point_pair(aabb, seg); + if (tmp.metric < best.metric) + best = tmp; + if (best.metric < tf::epsilon2) + return best; + } } return best; } diff --git a/include/trueform/core/distance.hpp b/include/trueform/core/distance.hpp index f8f7a4a..232d034 100644 --- a/include/trueform/core/distance.hpp +++ b/include/trueform/core/distance.hpp @@ -889,6 +889,30 @@ auto distance2(const segment &seg, const sphere_like &s) { return distance2(seg, s); } +template +auto distance(const sphere_like &s, const plane_like &p) { + auto d2 = distance2(p, s.origin); + if (d2 + tf::epsilon2 < s.r * s.r) + return decltype(d2)(0); + return tf::sqrt(d2) - s.r; +} + +template +auto distance(const plane_like &p, const sphere_like &s) { + return distance(s, p); +} + +template +auto distance2(const sphere_like &s, const plane_like &p) { + auto d = distance(s, p); + return d * d; +} + +template +auto distance2(const plane_like &p, const sphere_like &s) { + return distance2(s, p); +} + template auto distance(const tf::rss_like &rss0, const tf::rss_like &rss1) { diff --git a/include/trueform/intersect/scalar_field_intersections.hpp b/include/trueform/intersect/scalar_field_intersections.hpp index 0d352dc..8f2d151 100644 --- a/include/trueform/intersect/scalar_field_intersections.hpp +++ b/include/trueform/intersect/scalar_field_intersections.hpp @@ -12,7 +12,6 @@ */ #pragma once #include "../core/algorithm/block_reduce.hpp" -#include "../core/epsilon.hpp" #include "../core/polygons.hpp" #include "../core/views/enumerate.hpp" #include "./types/simple_intersections.hpp" @@ -92,29 +91,16 @@ class scalar_field_intersections auto t = (cut_value - scalar_field[id0]) / (scalar_field[id1] - scalar_field[id0]); - if (t < tf::epsilon) { - // Vertex intersection: snap to original vertex position - Index pt_id = points.size(); - points.push_back(polygon[v0]); - edge_point_ids.push_back( - {Index(id0), Index(id0), Index(pt_id)}); - intersections.push_back( - {Index(polygon_id), - tf::intersect::intersection_target{ - Index(v0), tf::topo_type::vertex}, - pt_id}); - } else { - auto created_point = polygon[v0] + t * edge; - Index pt_id = points.size(); - points.push_back(created_point); - edge_point_ids.push_back( - {Index(id0), Index(id1), Index(pt_id)}); - intersections.push_back( - {Index(polygon_id), - tf::intersect::intersection_target{ - Index(prev), tf::topo_type::edge}, - pt_id}); - } + auto created_point = polygon[v0] + t * edge; + Index pt_id = points.size(); + points.push_back(created_point); + edge_point_ids.push_back( + {Index(id0), Index(id1), Index(pt_id)}); + intersections.push_back( + {Index(polygon_id), + tf::intersect::intersection_target{ + Index(prev), tf::topo_type::edge}, + pt_id}); } } }, diff --git a/include/trueform/intersect/types/simple_edge_point_id.hpp b/include/trueform/intersect/types/simple_edge_point_id.hpp index 1a00001..30e3dbf 100644 --- a/include/trueform/intersect/types/simple_edge_point_id.hpp +++ b/include/trueform/intersect/types/simple_edge_point_id.hpp @@ -12,6 +12,7 @@ */ #pragma once #include +#include #include namespace tf::intersect { @@ -21,10 +22,8 @@ template struct simple_edge_point_id { Index point_id; simple_edge_point_id() = default; simple_edge_point_id(Index pt0, Index pt1, Index point_id) - : vertex_id0{pt0}, vertex_id1{pt1}, point_id{point_id} { - if (pt1 < pt0) - std::swap(pt0, pt1); - } + : vertex_id0{std::min(pt0, pt1)}, vertex_id1{std::max(pt0, pt1)}, + point_id{point_id} {} friend auto operator<(const simple_edge_point_id &e0, const simple_edge_point_id &e1) -> bool { diff --git a/pyproject.toml b/pyproject.toml index 255a4c9..0ea6295 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "scikit_build_core.build" name = "trueform" dynamic = ["version"] description = "Real-time geometric processing on NumPy arrays. Easy to use, robust on real-world data." -readme = "README_python.md" +readme = "python/README.md" requires-python = ">=3.9" license = { file = "LICENSE.noncommercial" } authors = [ @@ -75,7 +75,7 @@ sdist.include = [ "/LICENSE", "/LICENSE.noncommercial", "/README.md", - "/README_python.md", + "/python/README.md", "/examples/**", "/trueformConfig.cmake.in", "/cmake/**", diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 798b214..09be156 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -52,7 +52,8 @@ target_include_directories(_trueform PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/includ if(MSVC) target_compile_options(_trueform PRIVATE /W3 /wd4244 /wd4267 /wd4458 /wd4701) else() - target_compile_options(_trueform PRIVATE -Wall -Wextra) + # -Wno-nested-anon-types and -Wno-zero-length-array suppress nanobind warnings + target_compile_options(_trueform PRIVATE -Wall -Wextra -Wpedantic -Wno-nested-anon-types -Wno-zero-length-array) endif() # 7. Output Directory Layout diff --git a/README_python.md b/python/README.md similarity index 65% rename from README_python.md rename to python/README.md index 070622f..bc0e659 100644 --- a/README_python.md +++ b/python/README.md @@ -1,8 +1,6 @@ # trueform -Real-time geometric processing on NumPy arrays. Easy to use, robust on real-world data. - -Mesh booleans, registration, remeshing and queries — at interactive speed on million-polygon meshes. Robust to non-manifold flaps, inconsistent winding, and pipeline artifacts. NumPy in, NumPy out. +Real-time geometric processing on NumPy arrays. Enriched NumPy arrays with support for vectorized spatial queries. Mesh booleans, registration, remeshing — at interactive speed on million-polygon meshes. Robust to non-manifold flaps, inconsistent winding, and pipeline artifacts. NumPy in, NumPy out. **[Documentation](https://trueform.polydera.com/py/getting-started)** | **[Live Examples](https://trueform.polydera.com/live-examples/boolean)** @@ -14,11 +12,24 @@ pip install trueform ## Quick Tour +**Primitives** — typed NumPy arrays, single or batched: + ```python import numpy as np import trueform as tf -# NumPy arrays in +triangle = tf.Triangle(a=[0, 0, 0], b=[1, 0, 0], c=[0, 1, 0]) +segment = tf.Segment([[0, 0, 0], [1, 1, 1]]) +ray = tf.Ray(origin=[0.2, 0.2, -1], direction=[0, 0, 1]) + +# Add a leading dimension for batches +pts = tf.Point(np.random.rand(1000, 3).astype(np.float32)) +segs = tf.Segment(start=np.random.rand(500, 3), end=np.random.rand(500, 3)) +``` + +**Meshes** are created from NumPy arrays or read from files: + +```python points = np.array([ [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1] ], dtype=np.float32) @@ -29,7 +40,24 @@ faces = np.array([ mesh = tf.Mesh(faces, points) # Or read from file -mesh = tf.read_stl("model.stl") +mesh = tf.Mesh(*tf.read_stl("model.stl")) +``` + +**Queries** — same functions for any combination. Batches broadcast: + +```python +# Batch × Primitive — distance field to a plane +plane = tf.Plane(normal=[0, 0, 1], offset=0.0) +scalars = tf.distance(pts, plane) # shape (1000,) + +# Batch × Form — closest point on mesh for each segment +ids, dist2s, closest = tf.neighbor_search(mesh, segs) # 3 arrays, shape (500,) + +# Single × Single +dist2, pt_a, pt_b = tf.closest_metric_point_pair(triangle, segment) + +if (t := tf.ray_cast(ray, triangle)) is not None: + hit_point = ray.origin + t * np.array(ray.direction) ``` **Boolean operations:** @@ -43,19 +71,16 @@ mesh = tf.read_stl("model.stl") ) ``` -**Spatial queries:** +**Remeshing:** ```python -static_mesh = tf.Mesh(faces0, points0) -dynamic_mesh = tf.Mesh(faces1, points1) -dynamic_mesh.transformation = rotation_matrix - -does_intersect = tf.intersects(static_mesh, dynamic_mesh) -distance = tf.distance(static_mesh, dynamic_mesh) -(id0, id1), (dist2, pt0, pt1) = tf.neighbor_search(static_mesh, dynamic_mesh) -neighbors = tf.neighbor_search(dynamic_mesh, static_mesh.points[0], k=10) -for idx, dist2, pt in neighbors: - pass +# Decimate to 50% +dec_faces, dec_points = tf.decimated(mesh, 0.5) + +# Isotropic remesh to uniform edge lengths +dec_mesh = tf.Mesh(dec_faces, dec_points) +mel = tf.mean_edge_length(dec_mesh) +rem_faces, rem_points = tf.isotropic_remeshed(dec_mesh, mel) ``` → [Full documentation](https://trueform.polydera.com/py/modules) covers mesh analysis, topology, isocontours, curvature, and more. diff --git a/python/examples/raycast_render.py b/python/examples/raycast_render.py new file mode 100644 index 0000000..60c40ee --- /dev/null +++ b/python/examples/raycast_render.py @@ -0,0 +1,131 @@ +""" +Ray-traced depth rendering using batch ray casting + +Loads a mesh, generates a grid of rays (one per pixel), batch ray_casts +them against the mesh, and renders Lambertian shading from face normals. + +Usage: + python raycast_render.py [mesh.stl] [--resolution 512] + +Default mesh: dragon-250k.stl +""" + +import os +import time +import argparse +import numpy as np +import trueform as tf + +# TrueForm color scheme (matches VTK examples) +BG_COLOR = np.array([27, 43, 52], dtype=np.float32) / 255 # dark blue-gray +TEAL = np.array([0.0, 0.659, 0.604]) # base teal +TEAL_BRIGHT = np.array([0.0, 0.835, 0.745]) # highlight teal + + +def main(): + parser = argparse.ArgumentParser(description="Ray-traced mesh rendering") + parser.add_argument("mesh", nargs="?", default=None, + help="Path to .stl mesh") + parser.add_argument("--resolution", type=int, + default=512, help="Image resolution") + args = parser.parse_args() + + data_dir = os.path.join(os.path.dirname( + __file__), "../../benchmarks/data/") + mesh_path = args.mesh or os.path.join(data_dir, "dragon-125k.stl") + res = args.resolution + + # Load mesh + print(f"Loading {mesh_path}") + faces, points = tf.read_stl(mesh_path) + mesh = tf.Mesh(faces, points) + print(f" {mesh.number_of_faces} faces, {mesh.number_of_points} vertices") + mesh.build_tree() + mesh.normals + + # Bounding box + aabb_min = points.min(axis=0) + aabb_max = points.max(axis=0) + size = aabb_max - aabb_min + pad = size.max() * 0.1 + + # Orthographic camera looking down -Z + x = np.linspace(aabb_min[0] - pad, aabb_max[0] + + pad, res).astype(points.dtype) + y = np.linspace(aabb_max[1] + pad, aabb_min[1] - + pad, res).astype(points.dtype) + gx, gy = np.meshgrid(x, y) + n_rays = res * res + + origins = np.zeros((n_rays, 3), dtype=points.dtype) + origins[:, 0] = gx.ravel() + origins[:, 1] = gy.ravel() + origins[:, 2] = aabb_max[2] + pad + + directions = np.zeros((n_rays, 3), dtype=points.dtype) + directions[:, 2] = -1.0 + + rays = tf.Ray(origin=origins, direction=directions) + + # Batch ray cast + print(f"Casting {n_rays:,} rays ({res}x{res})...") + t0 = time.perf_counter() + ids, ts = tf.ray_cast(rays, mesh) + elapsed = (time.perf_counter() - t0) * 1000 + print(f" {elapsed:.1f} ms") + + # Reshape + ids = ids.reshape(res, res) + ts = ts.reshape(res, res) + hit = ~np.isnan(ts) + print(f" {hit.sum():,} hits ({100 * hit.sum() / n_rays:.1f}%)") + + # Two-light Lambertian shading + normals = mesh.normals + hit_ids = ids[hit] + hit_normals = normals[hit_ids].copy() + + # Flip back-facing normals + hit_normals[hit_normals[:, 2] > 0] *= -1 + + key_dir = np.array([0.4, 0.6, -0.7], dtype=np.float32) + key_dir /= np.linalg.norm(key_dir) + fill_dir = np.array([-0.5, -0.3, -0.6], dtype=np.float32) + fill_dir /= np.linalg.norm(fill_dir) + + key = np.clip(hit_normals @ key_dir, 0.0, 1.0) + fill = np.clip(hit_normals @ fill_dir, 0.0, 1.0) + diffuse = 0.08 + 0.72 * key + 0.20 * fill # ambient + key + fill + + # Rim lighting — bright edge glow where surface is near-perpendicular to view + view_dir = np.array([0.0, 0.0, -1.0], dtype=np.float32) + facing = np.abs(hit_normals @ view_dir) + rim = (1.0 - facing) ** 3 + intensity = diffuse + 0.35 * rim + + # Teal-shaded image on dark background + image = np.empty((res, res, 3), dtype=np.float32) + image[:] = BG_COLOR + + # Shade from dark teal to bright teal by intensity + color = TEAL + np.clip(intensity, 0, 1)[:, np.newaxis] * (TEAL_BRIGHT - TEAL) + image[hit] = color * intensity[:, np.newaxis] + + image = np.clip(image, 0, 1) + + # Display + import matplotlib.pyplot as plt + fig, ax = plt.subplots(1, 1, figsize=(8, 8), facecolor=BG_COLOR) + ax.imshow(image) + ax.set_axis_off() + ax.set_title( + f"{os.path.basename(mesh_path)} \u2014 {res}\u00d7{res}, {elapsed:.0f} ms", + color="white", fontsize=13, pad=12, + ) + fig.patch.set_facecolor(BG_COLOR) + plt.tight_layout() + plt.show() + + +if __name__ == "__main__": + main() diff --git a/python/examples/vtk/cross_section.py b/python/examples/vtk/cross_section.py index 1c3d4e2..c474475 100644 --- a/python/examples/vtk/cross_section.py +++ b/python/examples/vtk/cross_section.py @@ -61,7 +61,7 @@ def reset_plane(self): plane = tf.Plane.from_point_normal(center, normal) - self.scalars = tf.distance_field(self.mesh.points, plane) + self.scalars = tf.distance(tf.Point(self.mesh.points), plane) self.min_d = float(np.min(self.scalars)) self.max_d = float(np.max(self.scalars)) self.cut_value = (self.min_d + self.max_d) * 0.5 @@ -74,7 +74,7 @@ def randomize_plane(self): plane = tf.Plane.from_point_normal(random_point, random_normal) - self.scalars = tf.distance_field(self.mesh.points, plane) + self.scalars = tf.distance(tf.Point(self.mesh.points), plane) self.min_d = float(np.min(self.scalars)) self.max_d = float(np.max(self.scalars)) self.cut_value = (self.min_d + self.max_d) * 0.5 diff --git a/python/examples/vtk/isobands.py b/python/examples/vtk/isobands.py index a5fb9fa..068dda0 100644 --- a/python/examples/vtk/isobands.py +++ b/python/examples/vtk/isobands.py @@ -63,7 +63,7 @@ def reset_plane(self): plane = tf.Plane.from_point_normal(center, normal) - self.scalars = tf.distance_field(self.mesh.points, plane) + self.scalars = tf.distance(tf.Point(self.mesh.points), plane) self.distance = 0.0 self.min_d = float(np.min(self.scalars)) @@ -78,7 +78,7 @@ def randomize_plane(self): plane = tf.Plane.from_point_normal(random_point, random_normal) - self.scalars = tf.distance_field(self.mesh.points, plane) + self.scalars = tf.distance(tf.Point(self.mesh.points), plane) self.distance = 0.0 self.min_d = float(np.min(self.scalars)) diff --git a/python/examples/vtk/isocontours.py b/python/examples/vtk/isocontours.py index 04a3869..b1bff70 100644 --- a/python/examples/vtk/isocontours.py +++ b/python/examples/vtk/isocontours.py @@ -69,7 +69,7 @@ def reset_plane(self): plane = tf.Plane.from_point_normal(random_point, random_normal) # Compute scalar field (signed distance to plane) - self.scalars = tf.distance_field(self.mesh.points, plane) + self.scalars = tf.distance(tf.Point(self.mesh.points), plane) self.distance = 0.0 # Cache min/max for adaptive level spacing diff --git a/python/include/trueform/python/core.hpp b/python/include/trueform/python/core.hpp index 10185dc..1832a7d 100644 --- a/python/include/trueform/python/core.hpp +++ b/python/include/trueform/python/core.hpp @@ -17,17 +17,9 @@ namespace tf::py { // Forward declarations for core module registration functions -auto register_offset_blocked_array(nanobind::module_ &m) -> void; - -auto register_core_closest_metric_point_pair(nanobind::module_ &m) -> void; - -auto register_core_ray_cast(nanobind::module_ &m) -> void; +auto register_primitive_wrappers(nanobind::module_ &m) -> void; -auto register_core_intersects(nanobind::module_ &m) -> void; - -auto register_core_distance(nanobind::module_ &m) -> void; - -auto register_core_distance_field(nanobind::module_ &m) -> void; +auto register_offset_blocked_array(nanobind::module_ &m) -> void; auto register_core(nanobind::module_ &m) -> void; diff --git a/python/include/trueform/python/core/closest_metric_point_pair.hpp b/python/include/trueform/python/core/closest_metric_point_pair.hpp deleted file mode 100644 index bb7c8eb..0000000 --- a/python/include/trueform/python/core/closest_metric_point_pair.hpp +++ /dev/null @@ -1,22 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#pragma once - -#include - -namespace tf::py { - -// Forward declaration of registration function -auto register_core_closest_metric_point_pair(nanobind::module_ &m) -> void; - -} // namespace tf::py diff --git a/python/include/trueform/python/core/distance.hpp b/python/include/trueform/python/core/distance.hpp deleted file mode 100644 index 616b1cc..0000000 --- a/python/include/trueform/python/core/distance.hpp +++ /dev/null @@ -1,21 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#pragma once - -#include - -namespace tf::py { - -auto register_core_distance(nanobind::module_ &m) -> void; - -} // namespace tf::py diff --git a/python/include/trueform/python/core/distance_field.hpp b/python/include/trueform/python/core/distance_field.hpp deleted file mode 100644 index 29fd0c1..0000000 --- a/python/include/trueform/python/core/distance_field.hpp +++ /dev/null @@ -1,59 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -/// @brief Compute distance field from points to a primitive (vectorized, parallel) -/// @tparam Dims Dimensionality (2 or 3) -/// @tparam RealT Floating point type (float or double) -/// @tparam Primitive Type of geometric primitive -/// @param points_array Numpy array of points with shape (N, Dims) -/// @param primitive The target primitive -/// @return Numpy array of distances with shape (N,) -template -auto distance_field_impl( - nanobind::ndarray> - points_array, - const Primitive &primitive) { - - // Create tf::points view from numpy array - std::size_t num_points = points_array.shape(0); - auto points = tf::make_points( - tf::make_range(points_array.data(), num_points * Dims)); - - // Allocate result buffer - tf::buffer scalars; - scalars.allocate(points.size()); - - // Parallel distance computation using tf::distance_f - tf::parallel_transform(points, scalars, tf::distance_f(primitive)); - - // Release ownership from buffer and wrap as numpy array - RealT *data_ptr = scalars.release(); - return make_numpy_array>( - data_ptr, {static_cast(points.size())}); -} - -auto register_core_distance_field(nanobind::module_ &m) -> void; - -} // namespace tf::py diff --git a/python/include/trueform/python/core/intersects.hpp b/python/include/trueform/python/core/intersects.hpp deleted file mode 100644 index 601264b..0000000 --- a/python/include/trueform/python/core/intersects.hpp +++ /dev/null @@ -1,22 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#pragma once - -#include - -namespace tf::py { - -// Forward declaration of registration function -auto register_core_intersects(nanobind::module_ &m) -> void; - -} // namespace tf::py diff --git a/python/include/trueform/python/core/make_primitives.hpp b/python/include/trueform/python/core/make_primitives.hpp deleted file mode 100644 index 21f0659..0000000 --- a/python/include/trueform/python/core/make_primitives.hpp +++ /dev/null @@ -1,112 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -// Helper to create views from primitive data -template -auto make_point_from_array( - nanobind::ndarray> - data) { - return tf::make_point_view(data.data()); -} - -template -auto make_segment_from_array( - nanobind::ndarray> - data) { - auto pts = tf::make_points(tf::make_range(data.data(), 2 * Dims)); - return tf::make_segment(pts); -} - -template -auto make_polygon_from_array( - nanobind::ndarray> - data) { - std::size_t num_vertices = data.shape(0); - auto pts = - tf::make_points(tf::make_range(data.data(), num_vertices * Dims)); - return tf::make_polygon(pts); -} - -template -auto make_polygon_from_array( - nanobind::ndarray> - data) { - std::size_t num_vertices = data.shape(0); - auto pts = - tf::make_points(tf::make_range(data.data(), num_vertices * Dims)); - return tf::make_polygon(pts); -} - -template -auto make_polygon_from_array( - nanobind::ndarray data) { - std::size_t num_vertices = data.shape(0); - auto pts = - tf::make_points(tf::make_range(data.data(), num_vertices * Dims)); - return tf::make_polygon(pts); -} - -template -auto make_ray_from_array( - nanobind::ndarray> - data) { - auto origin = tf::make_point_view(data.data()); - auto direction = tf::make_vector_view(data.data() + Dims); - return tf::make_ray_like(origin, direction); -} - -template -auto make_line_from_array( - nanobind::ndarray> - data) { - auto origin = tf::make_point_view(data.data()); - auto direction = tf::make_vector_view(data.data() + Dims); - return tf::make_line_like(origin, direction); -} - -template -auto make_aabb_from_array( - nanobind::ndarray> - data) { - auto min = tf::make_point_view(data.data()); - auto max = tf::make_point_view(data.data() + Dims); - return tf::make_aabb_like(min, max); -} - -template -auto make_plane_from_array( - nanobind::ndarray> - data) { - auto normal = tf::make_unit_vector_view( - tf::unsafe, tf::make_vector_view(data.data())); - auto offset = data.data()[Dims]; - return tf::make_plane(normal, offset); -} -} // namespace tf::py diff --git a/python/include/trueform/python/core/prim_dispatch.hpp b/python/include/trueform/python/core/prim_dispatch.hpp new file mode 100644 index 0000000..c272464 --- /dev/null +++ b/python/include/trueform/python/core/prim_dispatch.hpp @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#pragma once + +#include "primitive_wrapper.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tf::py { + +// ============================================================================ +// Single-element factories: primitive_wrapper → tf primitive view +// ============================================================================ + +template +auto make_point(const primitive_wrapper &pw) + -> decltype(tf::make_point_view(pw.data_ptr())) { + return tf::make_point_view(pw.data_ptr()); +} + +template +auto make_segment(const primitive_wrapper &pw) + -> decltype(tf::make_segment( + tf::make_points(tf::make_range(pw.data_ptr(), 2 * Dims)))) { + auto pts = tf::make_points(tf::make_range(pw.data_ptr(), 2 * Dims)); + return tf::make_segment(pts); +} + +template +auto make_triangle(const primitive_wrapper &pw) + -> decltype(tf::make_polygon<3>( + tf::make_points(tf::make_range(pw.data_ptr(), 3 * Dims)))) { + auto pts = tf::make_points(tf::make_range(pw.data_ptr(), 3 * Dims)); + return tf::make_polygon<3>(pts); +} + +template +auto make_ray(const primitive_wrapper &pw) + -> decltype(tf::make_ray_like(tf::make_point_view(pw.data_ptr()), + tf::make_vector_view(pw.data_ptr()))) { + auto pts = tf::make_points(tf::make_range(pw.data_ptr(), 2 * Dims)); + return tf::make_ray_like(pts[0], pts[1].as_vector_view()); +} + +template +auto make_line(const primitive_wrapper &pw) + -> decltype(tf::make_line_like(tf::make_point_view(pw.data_ptr()), + tf::make_vector_view(pw.data_ptr()))) { + auto pts = tf::make_points(tf::make_range(pw.data_ptr(), 2 * Dims)); + return tf::make_line_like(pts[0], pts[1].as_vector_view()); +} + +template +auto make_plane(const primitive_wrapper &pw) + -> decltype(tf::make_plane( + tf::make_unit_vector_view(tf::unsafe, + tf::make_vector_view(pw.data_ptr())), + pw.data_ptr()[Dims])) { + auto ptr = pw.data_ptr(); + return tf::make_plane( + tf::make_unit_vector_view(tf::unsafe, tf::make_vector_view(ptr)), + ptr[Dims]); +} + +template +auto make_aabb(const primitive_wrapper &pw) + -> decltype(tf::make_aabb_like(tf::make_point_view(pw.data_ptr()), + tf::make_point_view(pw.data_ptr()))) { + auto pts = tf::make_points(tf::make_range(pw.data_ptr(), 2 * Dims)); + return tf::make_aabb_like(pts[0], pts[1]); +} + +template +auto make_polygon(const primitive_wrapper &pw) + -> decltype(tf::make_polygon(tf::make_points(tf::make_range( + pw.data_ptr(), + static_cast(pw.poly_verts()) * Dims)))) { + auto pts = tf::make_points(tf::make_range( + pw.data_ptr(), static_cast(pw.poly_verts()) * Dims)); + return tf::make_polygon(pts); +} + +// ============================================================================ +// Batch factories: primitive_wrapper → tf batch type / mapped range +// ============================================================================ + +template +auto make_points(const primitive_wrapper &pw) -> decltype(auto) { + return tf::make_points(tf::make_range( + pw.data_ptr(), static_cast(pw.count()) * Dims)); +} + +template +auto make_segments(const primitive_wrapper &pw) + -> decltype(auto) { + auto pts = tf::make_points(tf::make_range( + pw.data_ptr(), static_cast(pw.count()) * 2 * Dims)); + auto blocks = tf::make_blocked_range<2>(pts); + return tf::make_segments(tf::make_mapped_range( + blocks, [](auto block) { return tf::make_segment(block); })); +} + +template +auto make_triangles(const primitive_wrapper &pw) + -> decltype(auto) { + auto pts = tf::make_points(tf::make_range( + pw.data_ptr(), static_cast(pw.count()) * 3 * Dims)); + auto blocks = tf::make_blocked_range<3>(pts); + return tf::make_polygons(tf::make_mapped_range( + blocks, [](auto block) { return tf::make_polygon<3>(block); })); +} + +template +auto make_rays(const primitive_wrapper &pw) -> decltype(auto) { + auto pts = tf::make_points(tf::make_range( + pw.data_ptr(), static_cast(pw.count()) * 2 * Dims)); + auto blocks = tf::make_blocked_range<2>(pts); + return tf::make_mapped_range(blocks, [](auto block) { + return tf::make_ray_like(block[0], block[1].as_vector_view()); + }); +} + +template +auto make_lines(const primitive_wrapper &pw) -> decltype(auto) { + auto pts = tf::make_points(tf::make_range( + pw.data_ptr(), static_cast(pw.count()) * 2 * Dims)); + auto blocks = tf::make_blocked_range<2>(pts); + return tf::make_mapped_range(blocks, [](auto block) { + return tf::make_line_like(block[0], block[1].as_vector_view()); + }); +} + +template +auto make_planes(const primitive_wrapper &pw) -> decltype(auto) { + auto flat = tf::make_range(pw.data_ptr(), + static_cast(pw.count()) * (Dims + 1)); + auto blocks = tf::make_blocked_range(flat); + return tf::make_mapped_range(blocks, [](auto block) { + return tf::make_plane( + tf::make_unit_vector_view(tf::unsafe, + tf::make_vector_view(&block[0])), + block[Dims]); + }); +} + +template +auto make_aabbs(const primitive_wrapper &pw) -> decltype(auto) { + auto pts = tf::make_points(tf::make_range( + pw.data_ptr(), static_cast(pw.count()) * 2 * Dims)); + auto blocks = tf::make_blocked_range<2>(pts); + return tf::make_mapped_range(blocks, [](auto block) { + return tf::make_aabb_like(block[0], block[1]); + }); +} + +template +auto make_polygons(const primitive_wrapper &pw) + -> decltype(auto) { + auto nv = pw.poly_verts(); + auto pts = tf::make_points(tf::make_range( + pw.data_ptr(), + static_cast(pw.count()) * static_cast(nv) * + Dims)); + auto blocks = tf::make_blocked_range(pts, static_cast(nv)); + return tf::make_polygons(tf::make_mapped_range( + blocks, [](auto block) { return tf::make_polygon(block); })); +} + +// ============================================================================ +// dispatch_single: non-batch primitive_wrapper → fn(tf_view) +// ============================================================================ + +template +auto dispatch_single(Fn &&fn, const primitive_wrapper &pw) + -> decltype(fn(make_point(pw))) { + switch (pw.type()) { + case prim_type::point: + return fn(make_point(pw)); + case prim_type::segment: + return fn(make_segment(pw)); + case prim_type::triangle: + return fn(make_triangle(pw)); + case prim_type::ray: + return fn(make_ray(pw)); + case prim_type::line: + return fn(make_line(pw)); + case prim_type::plane: + if constexpr (Dims >= 3) { + return fn(make_plane(pw)); + } else { + throw std::runtime_error("plane primitive requires 3D"); + } + case prim_type::aabb: + return fn(make_aabb(pw)); + case prim_type::polygon: + return fn(make_polygon(pw)); + default: // silence MSVC C4715: not all control paths return a value + throw std::runtime_error("unknown primitive type"); + } +} + +// ============================================================================ +// dispatch_at: per-element dispatch for batch loops +// ============================================================================ + +template +auto dispatch_at(Fn &&fn, const primitive_wrapper &pw, int i) + -> decltype(fn(tf::make_point_view(pw.element_ptr(i)))) { + auto ptr = pw.element_ptr(i); + auto nv = pw.poly_verts(); + switch (pw.type()) { + case prim_type::point: + return fn(tf::make_point_view(ptr)); + case prim_type::segment: { + auto pts = tf::make_points(tf::make_range(ptr, 2 * Dims)); + return fn(tf::make_segment(pts)); + } + case prim_type::triangle: { + auto pts = tf::make_points(tf::make_range(ptr, 3 * Dims)); + return fn(tf::make_polygon<3>(pts)); + } + case prim_type::ray: { + auto pts = tf::make_points(tf::make_range(ptr, 2 * Dims)); + return fn(tf::make_ray_like(pts[0], pts[1].as_vector_view())); + } + case prim_type::line: { + auto pts = tf::make_points(tf::make_range(ptr, 2 * Dims)); + return fn(tf::make_line_like(pts[0], pts[1].as_vector_view())); + } + case prim_type::plane: + if constexpr (Dims >= 3) { + return fn(tf::make_plane( + tf::make_unit_vector_view(tf::unsafe, + tf::make_vector_view(ptr)), + ptr[Dims])); + } else { + throw std::runtime_error("plane primitive requires 3D"); + } + case prim_type::aabb: { + auto pts = tf::make_points(tf::make_range(ptr, 2 * Dims)); + return fn(tf::make_aabb_like(pts[0], pts[1])); + } + case prim_type::polygon: { + auto pts = tf::make_points( + tf::make_range(ptr, static_cast(nv) * Dims)); + return fn(tf::make_polygon(pts)); + } + default: // silence MSVC C4715: not all control paths return a value + throw std::runtime_error("unknown primitive type"); + } +} + +// ============================================================================ +// dispatch_pair: non-batch pair dispatch via nested dispatch_single +// ============================================================================ + +template +auto dispatch_pair(Fn &&fn, const primitive_wrapper &a, + const primitive_wrapper &b) + -> decltype(fn(make_point(a), make_point(b))) { + return dispatch_single( + [&](auto &&pa) { + return dispatch_single( + [&](auto &&pb) { return fn(pa, pb); }, b); + }, + a); +} + +// ============================================================================ +// dispatch_pair_at: per-element pair dispatch via nested dispatch_at +// ============================================================================ + +template +auto dispatch_pair_at(Fn &&fn, const primitive_wrapper &a, int ia, + const primitive_wrapper &b, int ib) + -> decltype(fn(tf::make_point_view(a.data_ptr()), + tf::make_point_view(b.data_ptr()))) { + return dispatch_at( + [&](auto &&pa) { + return dispatch_at([&](auto &&pb) { return fn(pa, pb); }, b, ib); + }, + a, ia); +} + +} // namespace tf::py diff --git a/python/include/trueform/python/core/primitive_wrapper.hpp b/python/include/trueform/python/core/primitive_wrapper.hpp new file mode 100644 index 0000000..f50ec63 --- /dev/null +++ b/python/include/trueform/python/core/primitive_wrapper.hpp @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#pragma once + +#include +#include +#include + +namespace tf::py { + +// ============================================================================ +// Primitive type enum (mirrors TS PrimitiveType) +// ============================================================================ + +enum class prim_type : int { + point = 0, + segment = 1, + triangle = 2, + ray = 3, + line = 4, + plane = 5, + aabb = 6, + polygon = 7 +}; + +// ============================================================================ +// Helpers +// ============================================================================ + +/// Expected ndim for a single (non-batched) element. +inline auto single_ndim(prim_type t) -> int { + switch (t) { + case prim_type::point: + case prim_type::plane: + return 1; + default: + return 2; + } +} + +/// Number of RealT elements per single element for a given primitive type. +template +inline auto prim_stride(prim_type t, int n_poly_verts = 0) -> int { + switch (t) { + case prim_type::point: + return static_cast(Dims); + case prim_type::segment: + return static_cast(2 * Dims); + case prim_type::triangle: + return static_cast(3 * Dims); + case prim_type::ray: + return static_cast(2 * Dims); + case prim_type::line: + return static_cast(2 * Dims); + case prim_type::plane: + return static_cast(Dims + 1); + case prim_type::aabb: + return static_cast(2 * Dims); + case prim_type::polygon: + return static_cast(n_poly_verts * Dims); + } + return 0; +} + +// ============================================================================ +// primitive_wrapper +// ============================================================================ + +template class primitive_wrapper { + prim_type _type; + nanobind::ndarray _data; + +public: + primitive_wrapper( + int type_int, + nanobind::ndarray data) + : _type(static_cast(type_int)), _data(std::move(data)) {} + + auto type() const -> prim_type { return _type; } + + auto type_int() const -> int { return static_cast(_type); } + + auto data() const + -> const nanobind::ndarray & { + return _data; + } + + auto data_ptr() const -> const RealT * { return _data.data(); } + + auto ndim() const -> std::size_t { return _data.ndim(); } + + auto shape(std::size_t axis) const -> std::size_t { + return _data.shape(axis); + } + + auto is_batch() const -> bool { + return static_cast(ndim()) > single_ndim(_type); + } + + auto count() const -> int { + return is_batch() ? static_cast(shape(0)) : 1; + } + + static constexpr auto dims() -> std::size_t { return Dims; } + + auto poly_verts() const -> int { + if (_type == prim_type::polygon) + return static_cast(shape(ndim() - 2)); + if (_type == prim_type::triangle) + return 3; + return 0; + } + + auto stride() const -> int { return prim_stride(_type, poly_verts()); } + + auto element_ptr(int i) const -> const RealT * { + return is_batch() ? data_ptr() + i * stride() : data_ptr(); + } +}; + +// ============================================================================ +// Registration helper +// ============================================================================ + +template +auto register_primitive_wrapper(nanobind::module_ &m, const char *name) + -> void { + namespace nb = nanobind; + using PW = primitive_wrapper; + + nb::class_(m, name) + .def(nb::init>(), + nb::arg("type"), nb::arg("data")) + .def("type_int", &PW::type_int) + .def("is_batch", &PW::is_batch) + .def("count", &PW::count) + .def("dims", &PW::dims) + .def("stride", &PW::stride) + .def("poly_verts", &PW::poly_verts) + .def("ndim", &PW::ndim) + .def_prop_ro("data_array", + [](const PW &self) { return self.data(); }); +} + +} // namespace tf::py diff --git a/python/include/trueform/python/core/ray_cast.hpp b/python/include/trueform/python/core/ray_cast.hpp deleted file mode 100644 index 9741307..0000000 --- a/python/include/trueform/python/core/ray_cast.hpp +++ /dev/null @@ -1,22 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#pragma once - -#include - -namespace tf::py { - -// Forward declaration of registration function -auto register_core_ray_cast(nanobind::module_ &m) -> void; - -} // namespace tf::py diff --git a/python/include/trueform/python/spatial.hpp b/python/include/trueform/python/spatial.hpp index 0496766..6a9bb8d 100644 --- a/python/include/trueform/python/spatial.hpp +++ b/python/include/trueform/python/spatial.hpp @@ -23,12 +23,6 @@ void register_mesh(nanobind::module_ &m); void register_edge_mesh(nanobind::module_ &m); -void register_point_cloud_neighbor_search(nanobind::module_ &m); - -void register_mesh_neighbor_search(nanobind::module_ &m); - -void register_edge_mesh_neighbor_search(nanobind::module_ &m); - void register_point_cloud_neighbor_search_point_cloud(nanobind::module_ &m); void register_edge_mesh_neighbor_search_edge_mesh(nanobind::module_ &m); @@ -41,24 +35,6 @@ void register_mesh_neighbor_search_edge_mesh(nanobind::module_ &m); void register_mesh_neighbor_search_mesh(nanobind::module_ &m); -void register_point_cloud_ray_cast(nanobind::module_ &m); - -void register_mesh_ray_cast(nanobind::module_ &m); - -void register_edge_mesh_ray_cast(nanobind::module_ &m); - -void register_mesh_intersects_primitive(nanobind::module_ &m); - -void register_edge_mesh_intersects_primitive(nanobind::module_ &m); - -void register_point_cloud_intersects_primitive(nanobind::module_ &m); - -void register_mesh_gather_ids_primitive(nanobind::module_ &m); - -void register_edge_mesh_gather_ids_primitive(nanobind::module_ &m); - -void register_point_cloud_gather_ids_primitive(nanobind::module_ &m); - void register_point_cloud_gather_ids_point_cloud(nanobind::module_ &m); void register_edge_mesh_gather_ids_edge_mesh(nanobind::module_ &m); @@ -83,6 +59,13 @@ void register_mesh_intersects_edge_mesh(nanobind::module_ &m); void register_mesh_intersects_mesh(nanobind::module_ &m); +auto register_prim_prim_ops(nanobind::module_ &m) -> void; +auto register_prim_geometry_ops(nanobind::module_ &m) -> void; + +auto register_mesh_fp(nanobind::module_ &m) -> void; +auto register_edge_mesh_fp(nanobind::module_ &m) -> void; +auto register_point_cloud_fp(nanobind::module_ &m) -> void; + void register_spatial_module(nanobind::module_ &m); } // namespace tf::py diff --git a/python/include/trueform/python/spatial/form_prim_dispatch.hpp b/python/include/trueform/python/spatial/form_prim_dispatch.hpp new file mode 100644 index 0000000..ab8eecd --- /dev/null +++ b/python/include/trueform/python/spatial/form_prim_dispatch.hpp @@ -0,0 +1,605 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#pragma once + +#include "trueform/python/core/prim_dispatch.hpp" +#include "trueform/python/spatial/form_intersects_primitive.hpp" +#include "trueform/python/spatial/gather_ids.hpp" +#include "trueform/python/spatial/neighbor_search.hpp" +#include "trueform/python/spatial/ray_cast.hpp" +#include "trueform/python/util/make_numpy_array.hpp" +#include "trueform/python/util/ray_config_helper.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tf::py { + +// ============================================================================ +// SFINAE trait: detect bounded primitives (those supporting tf::aabb_from) +// Bounded: point, segment, triangle, aabb, polygon +// Unbounded: ray, line, plane +// ============================================================================ + +namespace detail { + +template +struct has_aabb_from : std::false_type {}; + +template +struct has_aabb_from< + T, std::void_t()))>> + : std::true_type {}; + +template +inline constexpr bool has_aabb_from_v = has_aabb_from::value; + +} // namespace detail + +// ============================================================================ +// register_form_prim_ops: registration template shared by all per-type .cpp +// Registers 7 nanobind functions per form type combination: +// Batch-capable (6): intersects, distance, distance2, ray_cast, +// neighbor_search, neighbor_search_knn +// Single-only (1): gather_ids +// ============================================================================ + +template +auto register_form_prim_ops(nanobind::module_ &m, const char *form_name, + const char *suffix) -> void { + namespace nb = nanobind; + using PW = primitive_wrapper; + + auto name = [form_name, suffix](const char *op) -> std::string { + return std::string(op) + form_name + "_fp_" + suffix; + }; + + // ======================================================================== + // BATCH-CAPABLE OPS (single → scalar, batch → ndarray) + // ======================================================================== + + // ==== intersects: single → bool, batch → ndarray[int8] ==== + m.def( + name("intersects_").c_str(), + [](FormWrapper &fw, const PW &pw) -> nb::object { + if (!pw.is_batch()) { + auto hit = dispatch_single( + [&](const auto &prim) -> bool { + return form_intersects_primitive(fw, prim); + }, + pw); + return nb::cast(hit); + } + + fw.tree(); + int n = pw.count(); + tf::buffer out; + out.allocate(n); + auto *dst = out.data(); + + auto run = [&](const auto &form) { + auto compute = [&](int i) { + dst[i] = dispatch_at( + [&](const auto &prim) -> bool { + return tf::intersects(form, prim); + }, + pw, i) + ? std::int8_t{1} + : std::int8_t{0}; + }; + if (n >= 1000) + tf::parallel_for_each(tf::make_sequence_range(n), + compute); + else + for (int i = 0; i < n; ++i) + compute(i); + }; + + if (fw.has_transformation()) + run(fw.make_primitive_range() | tf::tag(fw.tree()) | + tf::tag( + tf::make_frame(fw.transformation_view()))); + else + run(fw.make_primitive_range() | tf::tag(fw.tree())); + + return nb::cast(make_numpy_array>( + std::move(out), {static_cast(n)})); + }, + nb::arg("form"), nb::arg("primitive")); + + // ==== distance: single → float, batch → ndarray[float] ==== + m.def( + name("distance_").c_str(), + [](FormWrapper &fw, const PW &pw) -> nb::object { + if (!pw.is_batch()) { + auto run = [&](const auto &form) { + return dispatch_single( + [&](const auto &prim) -> RealT { + return tf::distance(form, prim); + }, + pw); + }; + RealT d; + if (fw.has_transformation()) + d = run(fw.make_primitive_range() | tf::tag(fw.tree()) | + tf::tag( + tf::make_frame(fw.transformation_view()))); + else + d = run(fw.make_primitive_range() | tf::tag(fw.tree())); + return nb::cast(d); + } + + fw.tree(); + int n = pw.count(); + tf::buffer out; + out.allocate(n); + auto *dst = out.data(); + + auto run = [&](const auto &form) { + auto compute = [&](int i) { + dispatch_at( + [&](const auto &prim) { + dst[i] = tf::distance(form, prim); + }, + pw, i); + }; + if (n >= 1000) + tf::parallel_for_each(tf::make_sequence_range(n), + compute); + else + for (int i = 0; i < n; ++i) + compute(i); + }; + + if (fw.has_transformation()) + run(fw.make_primitive_range() | tf::tag(fw.tree()) | + tf::tag( + tf::make_frame(fw.transformation_view()))); + else + run(fw.make_primitive_range() | tf::tag(fw.tree())); + + return nb::cast(make_numpy_array>( + std::move(out), {static_cast(n)})); + }, + nb::arg("form"), nb::arg("primitive")); + + // ==== distance2: single → float, batch → ndarray[float] ==== + m.def( + name("distance2_").c_str(), + [](FormWrapper &fw, const PW &pw) -> nb::object { + if (!pw.is_batch()) { + auto run = [&](const auto &form) { + return dispatch_single( + [&](const auto &prim) -> RealT { + return tf::distance2(form, prim); + }, + pw); + }; + RealT d2; + if (fw.has_transformation()) + d2 = run(fw.make_primitive_range() | tf::tag(fw.tree()) | + tf::tag( + tf::make_frame(fw.transformation_view()))); + else + d2 = run(fw.make_primitive_range() | tf::tag(fw.tree())); + return nb::cast(d2); + } + + fw.tree(); + int n = pw.count(); + tf::buffer out; + out.allocate(n); + auto *dst = out.data(); + + auto run = [&](const auto &form) { + auto compute = [&](int i) { + dispatch_at( + [&](const auto &prim) { + dst[i] = tf::distance2(form, prim); + }, + pw, i); + }; + if (n >= 1000) + tf::parallel_for_each(tf::make_sequence_range(n), + compute); + else + for (int i = 0; i < n; ++i) + compute(i); + }; + + if (fw.has_transformation()) + run(fw.make_primitive_range() | tf::tag(fw.tree()) | + tf::tag( + tf::make_frame(fw.transformation_view()))); + else + run(fw.make_primitive_range() | tf::tag(fw.tree())); + + return nb::cast(make_numpy_array>( + std::move(out), {static_cast(n)})); + }, + nb::arg("form"), nb::arg("primitive")); + + // ==== ray_cast: single → tuple(index, t)|None, batch → (ids[N], ts[N]) ==== + // config: optional pair of ndarrays (min_ts, max_ts). Python expands + // scalar tuples and None to arrays before calling. + using RayConfigArrays = std::pair< + nb::ndarray, nb::c_contig>, + nb::ndarray, nb::c_contig>>; + m.def( + name("ray_cast_").c_str(), + [](const PW &ray_pw, FormWrapper &fw, + std::optional opt_config) -> nb::object { + if (!ray_pw.is_batch()) { + // Single ray: config arrays have length 1 + std::optional> scalar_config; + if (opt_config) + scalar_config = std::make_tuple( + opt_config->first.data()[0], opt_config->second.data()[0]); + auto ray = make_ray(ray_pw); + auto result = ray_cast(ray, fw, scalar_config); + if (result) + return nb::cast( + nb::make_tuple(result->first, result->second)); + return nb::none(); + } + + // Batch: Python always provides config arrays + fw.tree(); + const RealT *min_ts = opt_config->first.data(); + const RealT *max_ts = opt_config->second.data(); + + int n = ray_pw.count(); + using Index = + typename std::decay_t::index_type; + tf::buffer ids; + ids.allocate(n); + tf::buffer ts; + ts.allocate(n); + auto *id_dst = ids.data(); + auto *t_dst = ts.data(); + constexpr auto nan = std::numeric_limits::quiet_NaN(); + + auto run = [&](const auto &form) { + auto compute = [&](int i) { + auto ray_ptr = ray_pw.element_ptr(i); + auto pts = tf::make_points( + tf::make_range(ray_ptr, 2 * Dims)); + auto ray = + tf::make_ray_like(pts[0], pts[1].as_vector_view()); + auto res = tf::ray_cast( + ray, form, + tf::ray_config{min_ts[i], max_ts[i]}); + if (res) { + id_dst[i] = res.element; + t_dst[i] = static_cast(res.info.t); + } else { + id_dst[i] = static_cast(-1); + t_dst[i] = nan; + } + }; + if (n >= 1000) + tf::parallel_for_each(tf::make_sequence_range(n), + compute); + else + for (int i = 0; i < n; ++i) + compute(i); + }; + + if (fw.has_transformation()) + run(fw.make_primitive_range() | tf::tag(fw.tree()) | + tf::tag( + tf::make_frame(fw.transformation_view()))); + else + run(fw.make_primitive_range() | tf::tag(fw.tree())); + + return nb::cast(nb::make_tuple( + make_numpy_array>( + std::move(ids), {static_cast(n)}), + make_numpy_array>( + std::move(ts), {static_cast(n)}))); + }, + nb::arg("ray"), nb::arg("form"), + nb::arg("config").none() = nb::none()); + + // ======================================================================== + // BATCH-CAPABLE NEIGHBOR SEARCH + // (single → optional/list, batch → tuple of ndarrays) + // ======================================================================== + + // ==== neighbor_search (1-nn): single → tuple|None, batch → + // (ids[N], dists[N], pts[N,D]) ==== + m.def( + name("neighbor_search_").c_str(), + [](FormWrapper &fw, const PW &pw, + std::optional radius) -> nb::object { + if (!pw.is_batch()) { + auto result = dispatch_single( + [&](const auto &prim) { + return neighbor_search(fw, prim, radius); + }, + pw); + if (!result) + return nb::none(); + return nb::cast(*result); + } + + fw.tree(); + using Index = + typename std::decay_t::index_type; + int n = pw.count(); + RealT r = + radius ? *radius : std::numeric_limits::max(); + + tf::buffer ids; + ids.allocate(n); + tf::buffer dists; + dists.allocate(n); + tf::buffer pts; + pts.allocate(static_cast(n) * Dims); + auto *id_dst = ids.data(); + auto *dist_dst = dists.data(); + auto *pts_dst = pts.data(); + + auto run = [&](const auto &form) { + auto compute = [&](int i) { + dispatch_at( + [&](const auto &prim) { + auto res = tf::neighbor_search(form, prim, r); + if (res) { + id_dst[i] = res.element; + dist_dst[i] = + static_cast(res.info.metric); + const auto &pt = res.info.point; + for (std::size_t d = 0; d < Dims; ++d) + pts_dst[i * Dims + d] = + static_cast(pt[d]); + } else { + id_dst[i] = static_cast(-1); + dist_dst[i] = + std::numeric_limits::infinity(); + for (std::size_t d = 0; d < Dims; ++d) + pts_dst[i * Dims + d] = RealT{0}; + } + }, + pw, i); + }; + if (n >= 100) + tf::parallel_for_each(tf::make_sequence_range(n), + compute); + else + for (int i = 0; i < n; ++i) + compute(i); + }; + + if (fw.has_transformation()) + run(fw.make_primitive_range() | tf::tag(fw.tree()) | + tf::tag( + tf::make_frame(fw.transformation_view()))); + else + run(fw.make_primitive_range() | tf::tag(fw.tree())); + + return nb::cast(nb::make_tuple( + make_numpy_array>( + std::move(ids), {static_cast(n)}), + make_numpy_array>( + std::move(dists), {static_cast(n)}), + make_numpy_array>( + std::move(pts), + {static_cast(n), Dims}))); + }, + nb::arg("form"), nb::arg("primitive"), + nb::arg("radius").none() = nb::none()); + + // ==== neighbor_search (k-nn): single → list, batch → + // (ids[N,K], dists[N,K], pts[N,K,D], counts[N]) ==== + m.def( + name("neighbor_search_knn_").c_str(), + [](FormWrapper &fw, const PW &pw, int k, + std::optional radius) -> nb::object { + if (!pw.is_batch()) { + return nb::cast(dispatch_single( + [&](const auto &prim) { + return neighbor_search(fw, prim, k, + radius); + }, + pw)); + } + + fw.tree(); + using Index = + typename std::decay_t::index_type; + using nn_t = tf::nearest_neighbor; + int n = pw.count(); + RealT r = + radius ? *radius : std::numeric_limits::max(); + + std::size_t nk = static_cast(n) * k; + tf::buffer ids; + ids.allocate(nk); + tf::buffer dists; + dists.allocate(nk); + tf::buffer pts; + pts.allocate(nk * Dims); + tf::buffer counts; + counts.allocate(n); + + auto *id_dst = ids.data(); + auto *dist_dst = dists.data(); + auto *pts_dst = pts.data(); + auto *count_dst = counts.data(); + + tf::parallel_fill(ids, static_cast(-1)); + + auto run = [&](const auto &form) { + std::vector state_buf; + auto compute = [&](int i, std::vector &buf) { + buf.resize(k); + auto knn = + tf::make_nearest_neighbors(buf.data(), k, r); + dispatch_at( + [&](const auto &prim) { + tf::neighbor_search(form, prim, knn); + }, + pw, i); + int cnt = static_cast(knn.size()); + count_dst[i] = cnt; + int base = i * k; + for (int j = 0; j < cnt; ++j) { + id_dst[base + j] = buf[j].element; + dist_dst[base + j] = + static_cast(buf[j].info.metric); + for (std::size_t d = 0; d < Dims; ++d) + pts_dst[(base + j) * Dims + d] = + static_cast(buf[j].info.point[d]); + } + }; + if (n >= 100) + tf::parallel_for_each(tf::make_sequence_range(n), + compute, + std::move(state_buf)); + else { + state_buf.resize(k); + for (int i = 0; i < n; ++i) + compute(i, state_buf); + } + }; + + if (fw.has_transformation()) + run(fw.make_primitive_range() | tf::tag(fw.tree()) | + tf::tag( + tf::make_frame(fw.transformation_view()))); + else + run(fw.make_primitive_range() | tf::tag(fw.tree())); + + return nb::cast(nb::make_tuple( + make_numpy_array>( + std::move(ids), + {static_cast(n), + static_cast(k)}), + make_numpy_array>( + std::move(dists), + {static_cast(n), + static_cast(k)}), + make_numpy_array>( + std::move(pts), + {static_cast(n), + static_cast(k), Dims}), + make_numpy_array>( + std::move(counts), + {static_cast(n)}))); + }, + nb::arg("form"), nb::arg("primitive"), nb::arg("k"), + nb::arg("radius").none() = nb::none()); + + // ======================================================================== + // SINGLE-ONLY OPS (no broadcasting) + // ======================================================================== + + // ==== gather_ids ==== + m.def( + name("gather_ids_").c_str(), + [](FormWrapper &fw, const PW &pw, const std::string &predicate_type, + std::optional threshold) { + if (predicate_type != "intersects" && + predicate_type != "within_distance") + throw std::runtime_error("Unknown predicate: " + + predicate_type); + if (predicate_type == "within_distance" && !threshold) + throw std::runtime_error( + "threshold required for within_distance"); + + bool within = predicate_type == "within_distance"; + RealT threshold2 = + within ? (*threshold) * (*threshold) : RealT{0}; + + return dispatch_single( + [&](const auto &query) { + using Q = std::decay_t; + + if (!within) { + if constexpr (detail::has_aabb_from_v) { + auto query_aabb = tf::aabb_from(query); + auto aabb_pred = [query_aabb](const auto &aabb) { + return tf::intersects(aabb, query_aabb); + }; + auto prim_pred = [&query](const auto &prim) { + return tf::intersects(prim, query); + }; + return gather_ids(fw, aabb_pred, + prim_pred); + } else { + auto aabb_pred = [&query](const auto &aabb) { + return tf::intersects(query, aabb); + }; + auto prim_pred = [&query](const auto &prim) { + return tf::intersects(prim, query); + }; + return gather_ids(fw, aabb_pred, + prim_pred); + } + } else { + if constexpr (detail::has_aabb_from_v) { + auto query_aabb = tf::aabb_from(query); + auto aabb_pred = [query_aabb, + threshold2](const auto &aabb) { + return tf::distance2(aabb, query_aabb) <= threshold2; + }; + auto prim_pred = [&query, + threshold2](const auto &prim) { + return tf::distance2(prim, query) <= threshold2; + }; + return gather_ids(fw, aabb_pred, + prim_pred); + } else { + auto aabb_pred = [&query, + threshold2](const auto &aabb) { + return tf::distance2( + tf::make_sphere( + aabb.center(), + aabb.diagonal().length() / 2), + query) <= threshold2; + }; + auto prim_pred = [&query, + threshold2](const auto &prim) { + return tf::distance2(prim, query) <= threshold2; + }; + return gather_ids(fw, aabb_pred, + prim_pred); + } + } + }, + pw); + }, + nb::arg("form"), nb::arg("primitive"), nb::arg("predicate_type"), + nb::arg("threshold").none() = nb::none()); +} + +} // namespace tf::py diff --git a/python/src/core.cpp b/python/src/core.cpp index 4a7e158..6df04bd 100644 --- a/python/src/core.cpp +++ b/python/src/core.cpp @@ -20,12 +20,8 @@ auto register_core(nanobind::module_ &m) -> void { auto core_module = m.def_submodule("core", "Core operations"); // Register core components to submodule + register_primitive_wrappers(core_module); register_offset_blocked_array(core_module); - register_core_closest_metric_point_pair(core_module); - register_core_ray_cast(core_module); - register_core_intersects(core_module); - register_core_distance(core_module); - register_core_distance_field(core_module); } } // namespace tf::py diff --git a/python/src/core/closest_metric_point_pair.cpp b/python/src/core/closest_metric_point_pair.cpp deleted file mode 100644 index 58a342d..0000000 --- a/python/src/core/closest_metric_point_pair.cpp +++ /dev/null @@ -1,1083 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include "trueform/python/core/closest_metric_point_pair.hpp" -#include "trueform/python/core/make_primitives.hpp" -#include -#include - -namespace tf::py { - -namespace { - -// Convert metric_point_pair result to Python tuple -template -auto metric_point_pair_to_tuple(const ResultT &result) { - auto [dist2, pt0, pt1] = result; - - // Create numpy arrays for the two points - RealT *pt0_data = new RealT[Dims]; - auto pt0_array = - nanobind::ndarray>( - pt0_data, {Dims}, nanobind::capsule(pt0_data, [](void *p) noexcept { - delete[] static_cast(p); - })); - - RealT *pt1_data = new RealT[Dims]; - auto pt1_array = - nanobind::ndarray>( - pt1_data, {Dims}, nanobind::capsule(pt1_data, [](void *p) noexcept { - delete[] static_cast(p); - })); - - // Copy data - for (std::size_t i = 0; i < Dims; ++i) { - pt0_array.data()[i] = pt0[i]; - pt1_array.data()[i] = pt1[i]; - } - - return nanobind::make_tuple(dist2, pt0_array, pt1_array); -} - -} // anonymous namespace - -auto register_core_closest_metric_point_pair(nanobind::module_ &m) -> void { - // Point to Point (float, 2D) - m.def("closest_metric_point_pair_point_point_float2d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<2, float>(pt0_data); - auto pt1 = make_point_from_array<2, float>(pt1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt0, pt1)); - }); - - // Point to Point (float, 3D) - m.def("closest_metric_point_pair_point_point_float3d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<3, float>(pt0_data); - auto pt1 = make_point_from_array<3, float>(pt1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt0, pt1)); - }); - - // Point to Point (double, 2D) - m.def("closest_metric_point_pair_point_point_double2d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<2, double>(pt0_data); - auto pt1 = make_point_from_array<2, double>(pt1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt0, pt1)); - }); - - // Point to Point (double, 3D) - m.def("closest_metric_point_pair_point_point_double3d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<3, double>(pt0_data); - auto pt1 = make_point_from_array<3, double>(pt1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt0, pt1)); - }); - - // Point to Segment (float, 3D) - m.def( - "closest_metric_point_pair_point_segment_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, seg)); - }); - - // Point to Polygon (float, 3D) - m.def("closest_metric_point_pair_point_polygon_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray poly_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, poly)); - }); - - // Point to Ray (float, 3D) - m.def( - "closest_metric_point_pair_point_ray_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, ray)); - }); - - // Point to Segment (float, 2D) - m.def( - "closest_metric_point_pair_point_segment_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, seg)); - }); - - // Point to Segment (double, 2D) - m.def( - "closest_metric_point_pair_point_segment_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, seg)); - }); - - // Point to Segment (double, 3D) - m.def( - "closest_metric_point_pair_point_segment_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, seg)); - }); - - // Point to Polygon (float, 2D) - m.def("closest_metric_point_pair_point_polygon_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray poly_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, poly)); - }); - - // Point to Polygon (double, 2D) - m.def("closest_metric_point_pair_point_polygon_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray poly_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, poly)); - }); - - // Point to Polygon (double, 3D) - m.def("closest_metric_point_pair_point_polygon_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray poly_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, poly)); - }); - - // Segment to Segment (float, 2D) - m.def( - "closest_metric_point_pair_segment_segment_float2d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<2, float>(seg0_data); - auto seg1 = make_segment_from_array<2, float>(seg1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg0, seg1)); - }); - - // Segment to Segment (float, 3D) - m.def( - "closest_metric_point_pair_segment_segment_float3d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<3, float>(seg0_data); - auto seg1 = make_segment_from_array<3, float>(seg1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg0, seg1)); - }); - - // Segment to Segment (double, 2D) - m.def( - "closest_metric_point_pair_segment_segment_double2d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<2, double>(seg0_data); - auto seg1 = make_segment_from_array<2, double>(seg1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg0, seg1)); - }); - - // Segment to Segment (double, 3D) - m.def( - "closest_metric_point_pair_segment_segment_double3d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<3, double>(seg0_data); - auto seg1 = make_segment_from_array<3, double>(seg1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg0, seg1)); - }); - - // Segment to Polygon (float, 2D) - m.def( - "closest_metric_point_pair_segment_polygon_float2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray poly_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, poly)); - }); - - // Segment to Polygon (float, 3D) - m.def( - "closest_metric_point_pair_segment_polygon_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray poly_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, poly)); - }); - - // Segment to Polygon (double, 2D) - m.def( - "closest_metric_point_pair_segment_polygon_double2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray poly_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, poly)); - }); - - // Segment to Polygon (double, 3D) - m.def( - "closest_metric_point_pair_segment_polygon_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray poly_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, poly)); - }); - - // Polygon to Polygon (float, 2D) - m.def("closest_metric_point_pair_polygon_polygon_float2d", - [](nanobind::ndarray poly0_data, - nanobind::ndarray poly1_data) { - auto poly0 = make_polygon_from_array<2, float>(poly0_data); - auto poly1 = make_polygon_from_array<2, float>(poly1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly0, poly1)); - }); - - // Polygon to Polygon (float, 3D) - m.def("closest_metric_point_pair_polygon_polygon_float3d", - [](nanobind::ndarray poly0_data, - nanobind::ndarray poly1_data) { - auto poly0 = make_polygon_from_array<3, float>(poly0_data); - auto poly1 = make_polygon_from_array<3, float>(poly1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly0, poly1)); - }); - - // Polygon to Polygon (double, 2D) - m.def("closest_metric_point_pair_polygon_polygon_double2d", - [](nanobind::ndarray poly0_data, - nanobind::ndarray poly1_data) { - auto poly0 = make_polygon_from_array<2, double>(poly0_data); - auto poly1 = make_polygon_from_array<2, double>(poly1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly0, poly1)); - }); - - // Polygon to Polygon (double, 3D) - m.def("closest_metric_point_pair_polygon_polygon_double3d", - [](nanobind::ndarray poly0_data, - nanobind::ndarray poly1_data) { - auto poly0 = make_polygon_from_array<3, double>(poly0_data); - auto poly1 = make_polygon_from_array<3, double>(poly1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly0, poly1)); - }); - - // ==== Point to Ray ==== - // Point to Ray (float, 2D) - m.def( - "closest_metric_point_pair_point_ray_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, ray)); - }); - - // Point to Ray (float, 3D) - m.def( - "closest_metric_point_pair_point_ray_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, ray)); - }); - - // Point to Ray (double, 2D) - m.def( - "closest_metric_point_pair_point_ray_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, ray)); - }); - - // Point to Ray (double, 3D) - m.def( - "closest_metric_point_pair_point_ray_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, ray)); - }); - - // ==== Point to Line ==== - // Point to Line (float, 2D) - m.def( - "closest_metric_point_pair_point_line_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto line = make_line_from_array<2, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, line)); - }); - - // Point to Line (float, 3D) - m.def( - "closest_metric_point_pair_point_line_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto line = make_line_from_array<3, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, line)); - }); - - // Point to Line (double, 2D) - m.def( - "closest_metric_point_pair_point_line_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto line = make_line_from_array<2, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, line)); - }); - - // Point to Line (double, 3D) - m.def( - "closest_metric_point_pair_point_line_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto line = make_line_from_array<3, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, line)); - }); - - // ==== Segment to Ray ==== - // Segment to Ray (float, 2D) - m.def( - "closest_metric_point_pair_segment_ray_float2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - ray_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, ray)); - }); - - // Segment to Ray (float, 3D) - m.def( - "closest_metric_point_pair_segment_ray_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - ray_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, ray)); - }); - - // Segment to Ray (double, 2D) - m.def( - "closest_metric_point_pair_segment_ray_double2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - ray_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, ray)); - }); - - // Segment to Ray (double, 3D) - m.def( - "closest_metric_point_pair_segment_ray_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - ray_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, ray)); - }); - - // ==== Segment to Line ==== - // Segment to Line (float, 2D) - m.def( - "closest_metric_point_pair_segment_line_float2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - line_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - auto line = make_line_from_array<2, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, line)); - }); - - // Segment to Line (float, 3D) - m.def( - "closest_metric_point_pair_segment_line_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - line_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto line = make_line_from_array<3, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, line)); - }); - - // Segment to Line (double, 2D) - m.def( - "closest_metric_point_pair_segment_line_double2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - line_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - auto line = make_line_from_array<2, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, line)); - }); - - // Segment to Line (double, 3D) - m.def( - "closest_metric_point_pair_segment_line_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - line_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto line = make_line_from_array<3, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, line)); - }); - - // ==== Polygon to Ray ==== - // Polygon to Ray (float, 2D) - m.def( - "closest_metric_point_pair_polygon_ray_float2d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - ray_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, ray)); - }); - - // Polygon to Ray (float, 3D) - m.def( - "closest_metric_point_pair_polygon_ray_float3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - ray_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, ray)); - }); - - // Polygon to Ray (double, 2D) - m.def( - "closest_metric_point_pair_polygon_ray_double2d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - ray_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, ray)); - }); - - // Polygon to Ray (double, 3D) - m.def( - "closest_metric_point_pair_polygon_ray_double3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - ray_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, ray)); - }); - - // ==== Polygon to Line ==== - // Polygon to Line (float, 2D) - m.def( - "closest_metric_point_pair_polygon_line_float2d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - line_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - auto line = make_line_from_array<2, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, line)); - }); - - // Polygon to Line (float, 3D) - m.def( - "closest_metric_point_pair_polygon_line_float3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - line_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - auto line = make_line_from_array<3, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, line)); - }); - - // Polygon to Line (double, 2D) - m.def( - "closest_metric_point_pair_polygon_line_double2d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - line_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - auto line = make_line_from_array<2, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, line)); - }); - - // Polygon to Line (double, 3D) - m.def( - "closest_metric_point_pair_polygon_line_double3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - line_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - auto line = make_line_from_array<3, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, line)); - }); - - // ==== Ray to Ray ==== - // Ray to Ray (float, 2D) - m.def( - "closest_metric_point_pair_ray_ray_float2d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<2, float>(ray0_data); - auto ray1 = make_ray_from_array<2, float>(ray1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray0, ray1)); - }); - - // Ray to Ray (float, 3D) - m.def( - "closest_metric_point_pair_ray_ray_float3d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<3, float>(ray0_data); - auto ray1 = make_ray_from_array<3, float>(ray1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray0, ray1)); - }); - - // Ray to Ray (double, 2D) - m.def( - "closest_metric_point_pair_ray_ray_double2d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<2, double>(ray0_data); - auto ray1 = make_ray_from_array<2, double>(ray1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray0, ray1)); - }); - - // Ray to Ray (double, 3D) - m.def( - "closest_metric_point_pair_ray_ray_double3d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<3, double>(ray0_data); - auto ray1 = make_ray_from_array<3, double>(ray1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray0, ray1)); - }); - - // ==== Line to Line ==== - // Line to Line (float, 2D) - m.def( - "closest_metric_point_pair_line_line_float2d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<2, float>(line0_data); - auto line1 = make_line_from_array<2, float>(line1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(line0, line1)); - }); - - // Line to Line (float, 3D) - m.def( - "closest_metric_point_pair_line_line_float3d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<3, float>(line0_data); - auto line1 = make_line_from_array<3, float>(line1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(line0, line1)); - }); - - // Line to Line (double, 2D) - m.def( - "closest_metric_point_pair_line_line_double2d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<2, double>(line0_data); - auto line1 = make_line_from_array<2, double>(line1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(line0, line1)); - }); - - // Line to Line (double, 3D) - m.def( - "closest_metric_point_pair_line_line_double3d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<3, double>(line0_data); - auto line1 = make_line_from_array<3, double>(line1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(line0, line1)); - }); - - // ==== Ray to Line ==== - // Ray to Line (float, 2D) - m.def( - "closest_metric_point_pair_ray_line_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - line_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto line = make_line_from_array<2, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray, line)); - }); - - // Ray to Line (float, 3D) - m.def( - "closest_metric_point_pair_ray_line_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - line_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto line = make_line_from_array<3, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray, line)); - }); - - // Ray to Line (double, 2D) - m.def( - "closest_metric_point_pair_ray_line_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - line_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto line = make_line_from_array<2, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray, line)); - }); - - // Ray to Line (double, 3D) - m.def( - "closest_metric_point_pair_ray_line_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - line_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto line = make_line_from_array<3, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray, line)); - }); - - // ==== Point to Plane (3D only) ==== - m.def("closest_metric_point_pair_point_plane_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - plane_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, plane)); - }); - - m.def("closest_metric_point_pair_point_plane_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - plane_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(pt, plane)); - }); - - // ==== Plane to Point (3D only) ==== - m.def("closest_metric_point_pair_plane_point_float3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray> - pt_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - auto pt = make_point_from_array<3, float>(pt_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, pt)); - }); - - m.def("closest_metric_point_pair_plane_point_double3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray> - pt_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - auto pt = make_point_from_array<3, double>(pt_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, pt)); - }); - - // ==== Segment to Plane (3D only) ==== - m.def( - "closest_metric_point_pair_segment_plane_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - plane_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, plane)); - }); - - m.def( - "closest_metric_point_pair_segment_plane_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - plane_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(seg, plane)); - }); - - // ==== Plane to Segment (3D only) ==== - m.def( - "closest_metric_point_pair_plane_segment_float3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray> - seg_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, seg)); - }); - - m.def( - "closest_metric_point_pair_plane_segment_double3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray> - seg_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, seg)); - }); - - // ==== Ray to Plane (3D only) ==== - m.def( - "closest_metric_point_pair_ray_plane_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray, plane)); - }); - - m.def( - "closest_metric_point_pair_ray_plane_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(ray, plane)); - }); - - // ==== Plane to Ray (3D only) ==== - m.def( - "closest_metric_point_pair_plane_ray_float3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray> - ray_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, ray)); - }); - - m.def( - "closest_metric_point_pair_plane_ray_double3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray> - ray_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, ray)); - }); - - // ==== Line to Plane (3D only) ==== - m.def( - "closest_metric_point_pair_line_plane_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - plane_data) { - auto line = make_line_from_array<3, float>(line_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(line, plane)); - }); - - m.def( - "closest_metric_point_pair_line_plane_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - plane_data) { - auto line = make_line_from_array<3, double>(line_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(line, plane)); - }); - - // ==== Plane to Line (3D only) ==== - m.def( - "closest_metric_point_pair_plane_line_float3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray> - line_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - auto line = make_line_from_array<3, float>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, line)); - }); - - m.def( - "closest_metric_point_pair_plane_line_double3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray> - line_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - auto line = make_line_from_array<3, double>(line_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, line)); - }); - - // ==== Polygon to Plane (3D only) ==== - m.def("closest_metric_point_pair_polygon_plane_float3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - plane_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, plane)); - }); - - m.def("closest_metric_point_pair_polygon_plane_double3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - plane_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(poly, plane)); - }); - - // ==== Plane to Polygon (3D only) ==== - m.def("closest_metric_point_pair_plane_polygon_float3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray poly_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, poly)); - }); - - m.def("closest_metric_point_pair_plane_polygon_double3d", - [](nanobind::ndarray> - plane_data, - nanobind::ndarray poly_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane, poly)); - }); - - // ==== Plane to Plane (3D only) ==== - m.def("closest_metric_point_pair_plane_plane_float3d", - [](nanobind::ndarray> - plane0_data, - nanobind::ndarray> - plane1_data) { - auto plane0 = make_plane_from_array<3, float>(plane0_data); - auto plane1 = make_plane_from_array<3, float>(plane1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane0, plane1)); - }); - - m.def("closest_metric_point_pair_plane_plane_double3d", - [](nanobind::ndarray> - plane0_data, - nanobind::ndarray> - plane1_data) { - auto plane0 = make_plane_from_array<3, double>(plane0_data); - auto plane1 = make_plane_from_array<3, double>(plane1_data); - return metric_point_pair_to_tuple( - tf::closest_metric_point_pair(plane0, plane1)); - }); -} - -} // namespace tf::py diff --git a/python/src/core/distance.cpp b/python/src/core/distance.cpp deleted file mode 100644 index 25281e0..0000000 --- a/python/src/core/distance.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include - -namespace tf::py { - -// Forward declarations for distance bindings split across multiple files -auto register_core_distance_float2d(nanobind::module_ &m) -> void; -auto register_core_distance_float3d(nanobind::module_ &m) -> void; -auto register_core_distance_double2d(nanobind::module_ &m) -> void; -auto register_core_distance_double3d(nanobind::module_ &m) -> void; - -auto register_core_distance(nanobind::module_ &m) -> void { - // Register all distance bindings - // Split across multiple files for parallel compilation - register_core_distance_float2d(m); - register_core_distance_float3d(m); - register_core_distance_double2d(m); - register_core_distance_double3d(m); -} - -} // namespace tf::py diff --git a/python/src/core/distance_double2d.cpp b/python/src/core/distance_double2d.cpp deleted file mode 100644 index 4c05ae1..0000000 --- a/python/src/core/distance_double2d.cpp +++ /dev/null @@ -1,407 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include "trueform/python/core/distance.hpp" -#include "trueform/python/core/make_primitives.hpp" -#include -#include -#include - -namespace tf::py { - -auto register_core_distance_double2d(nanobind::module_ &m) -> void { - // ==== Point to Point ==== - - m.def("distance_point_point_double2d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<2, double>(pt0_data); - auto pt1 = make_point_from_array<2, double>(pt1_data); - return tf::distance(pt0, pt1); - }); - - m.def("distance2_point_point_double2d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<2, double>(pt0_data); - auto pt1 = make_point_from_array<2, double>(pt1_data); - return tf::distance2(pt0, pt1); - }); - - m.def("distance_point_aabb_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return tf::distance(pt, aabb); - }); - - m.def("distance2_point_aabb_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return tf::distance2(pt, aabb); - }); - - m.def("distance_point_line_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto line = make_line_from_array<2, double>(line_data); - return tf::distance(pt, line); - }); - - m.def("distance2_point_line_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto line = make_line_from_array<2, double>(line_data); - return tf::distance2(pt, line); - }); - - m.def("distance_point_ray_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return tf::distance(pt, ray); - }); - - m.def("distance2_point_ray_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return tf::distance2(pt, ray); - }); - - m.def("distance_point_segment_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::distance(pt, seg); - }); - - m.def("distance2_point_segment_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::distance2(pt, seg); - }); - - m.def("distance_point_polygon_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - poly_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::distance(pt, poly); - }); - - m.def("distance2_point_polygon_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - poly_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::distance2(pt, poly); - }); - - m.def("distance_aabb_aabb_double2d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<2, double>(aabb0_data); - auto aabb1 = make_aabb_from_array<2, double>(aabb1_data); - return tf::distance(aabb0, aabb1); - }); - - m.def("distance2_aabb_aabb_double2d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<2, double>(aabb0_data); - auto aabb1 = make_aabb_from_array<2, double>(aabb1_data); - return tf::distance2(aabb0, aabb1); - }); - - m.def("distance_line_line_double2d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<2, double>(line0_data); - auto line1 = make_line_from_array<2, double>(line1_data); - return tf::distance(line0, line1); - }); - - m.def("distance2_line_line_double2d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<2, double>(line0_data); - auto line1 = make_line_from_array<2, double>(line1_data); - return tf::distance2(line0, line1); - }); - - m.def("distance_line_ray_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<2, double>(line_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return tf::distance(line, ray); - }); - - m.def("distance2_line_ray_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<2, double>(line_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return tf::distance2(line, ray); - }); - - m.def("distance_line_segment_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<2, double>(line_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::distance(line, seg); - }); - - m.def("distance2_line_segment_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<2, double>(line_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::distance2(line, seg); - }); - - m.def("distance_line_polygon_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - poly_data) { - auto line = make_line_from_array<2, double>(line_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::distance(line, poly); - }); - - m.def("distance2_line_polygon_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - poly_data) { - auto line = make_line_from_array<2, double>(line_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::distance2(line, poly); - }); - - m.def("distance_ray_ray_double2d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<2, double>(ray0_data); - auto ray1 = make_ray_from_array<2, double>(ray1_data); - return tf::distance(ray0, ray1); - }); - - m.def("distance2_ray_ray_double2d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<2, double>(ray0_data); - auto ray1 = make_ray_from_array<2, double>(ray1_data); - return tf::distance2(ray0, ray1); - }); - - m.def("distance_ray_segment_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::distance(ray, seg); - }); - - m.def("distance2_ray_segment_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::distance2(ray, seg); - }); - - m.def("distance_ray_polygon_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - poly_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::distance(ray, poly); - }); - - m.def("distance2_ray_polygon_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - poly_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::distance2(ray, poly); - }); - - m.def("distance_segment_segment_double2d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<2, double>(seg0_data); - auto seg1 = make_segment_from_array<2, double>(seg1_data); - return tf::distance(seg0, seg1); - }); - - m.def("distance2_segment_segment_double2d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<2, double>(seg0_data); - auto seg1 = make_segment_from_array<2, double>(seg1_data); - return tf::distance2(seg0, seg1); - }); - - m.def("distance_segment_polygon_double2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - poly_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::distance(seg, poly); - }); - - m.def("distance2_segment_polygon_double2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - poly_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::distance2(seg, poly); - }); - - m.def("distance_polygon_polygon_double2d", - [](nanobind::ndarray> - poly0_data, - nanobind::ndarray> - poly1_data) { - auto poly0 = make_polygon_from_array<2, double>(poly0_data); - auto poly1 = make_polygon_from_array<2, double>(poly1_data); - return tf::distance(poly0, poly1); - }); - - m.def("distance2_polygon_polygon_double2d", - [](nanobind::ndarray> - poly0_data, - nanobind::ndarray> - poly1_data) { - auto poly0 = make_polygon_from_array<2, double>(poly0_data); - auto poly1 = make_polygon_from_array<2, double>(poly1_data); - return tf::distance2(poly0, poly1); - }); - -} - -} // namespace tf::py diff --git a/python/src/core/distance_double3d.cpp b/python/src/core/distance_double3d.cpp deleted file mode 100644 index aa5c3f1..0000000 --- a/python/src/core/distance_double3d.cpp +++ /dev/null @@ -1,575 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include "trueform/python/core/distance.hpp" -#include "trueform/python/core/make_primitives.hpp" -#include -#include -#include - -namespace tf::py { - -auto register_core_distance_double3d(nanobind::module_ &m) -> void { - // ==== Point to Point ==== - - m.def("distance_point_point_double3d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<3, double>(pt0_data); - auto pt1 = make_point_from_array<3, double>(pt1_data); - return tf::distance(pt0, pt1); - }); - - m.def("distance2_point_point_double3d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<3, double>(pt0_data); - auto pt1 = make_point_from_array<3, double>(pt1_data); - return tf::distance2(pt0, pt1); - }); - - // ==== Point to AABB ==== - - m.def("distance_point_aabb_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return tf::distance(pt, aabb); - }); - - m.def("distance2_point_aabb_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return tf::distance2(pt, aabb); - }); - - // ==== Point to Plane (3D only) ==== - - m.def("distance_point_plane_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - plane_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance(pt, plane); - }); - - m.def("distance2_point_plane_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - plane_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance2(pt, plane); - }); - - // ==== Point to Line ==== - - m.def("distance_point_line_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto line = make_line_from_array<3, double>(line_data); - return tf::distance(pt, line); - }); - - m.def("distance2_point_line_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto line = make_line_from_array<3, double>(line_data); - return tf::distance2(pt, line); - }); - - // ==== Point to Ray ==== - - m.def("distance_point_ray_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return tf::distance(pt, ray); - }); - - m.def("distance2_point_ray_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return tf::distance2(pt, ray); - }); - - // ==== Point to Segment ==== - - m.def("distance_point_segment_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::distance(pt, seg); - }); - - m.def("distance2_point_segment_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::distance2(pt, seg); - }); - - // ==== Point to Polygon ==== - - m.def("distance_point_polygon_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - poly_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::distance(pt, poly); - }); - - m.def("distance2_point_polygon_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - poly_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::distance2(pt, poly); - }); - - // ==== AABB to AABB ==== - - m.def("distance_aabb_aabb_double3d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<3, double>(aabb0_data); - auto aabb1 = make_aabb_from_array<3, double>(aabb1_data); - return tf::distance(aabb0, aabb1); - }); - - m.def("distance2_aabb_aabb_double3d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<3, double>(aabb0_data); - auto aabb1 = make_aabb_from_array<3, double>(aabb1_data); - return tf::distance2(aabb0, aabb1); - }); - - // ==== Line to Line ==== - - m.def("distance_line_line_double3d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<3, double>(line0_data); - auto line1 = make_line_from_array<3, double>(line1_data); - return tf::distance(line0, line1); - }); - - m.def("distance2_line_line_double3d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<3, double>(line0_data); - auto line1 = make_line_from_array<3, double>(line1_data); - return tf::distance2(line0, line1); - }); - - // ==== Line to Ray ==== - - m.def("distance_line_ray_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<3, double>(line_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return tf::distance(line, ray); - }); - - m.def("distance2_line_ray_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<3, double>(line_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return tf::distance2(line, ray); - }); - - // ==== Line to Segment ==== - - m.def("distance_line_segment_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<3, double>(line_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::distance(line, seg); - }); - - m.def("distance2_line_segment_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<3, double>(line_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::distance2(line, seg); - }); - - // ==== Line to Polygon ==== - - m.def("distance_line_polygon_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - poly_data) { - auto line = make_line_from_array<3, double>(line_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::distance(line, poly); - }); - - m.def("distance2_line_polygon_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - poly_data) { - auto line = make_line_from_array<3, double>(line_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::distance2(line, poly); - }); - - // ==== Ray to Ray ==== - - m.def("distance_ray_ray_double3d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<3, double>(ray0_data); - auto ray1 = make_ray_from_array<3, double>(ray1_data); - return tf::distance(ray0, ray1); - }); - - m.def("distance2_ray_ray_double3d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<3, double>(ray0_data); - auto ray1 = make_ray_from_array<3, double>(ray1_data); - return tf::distance2(ray0, ray1); - }); - - // ==== Ray to Segment ==== - - m.def("distance_ray_segment_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::distance(ray, seg); - }); - - m.def("distance2_ray_segment_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::distance2(ray, seg); - }); - - // ==== Ray to Polygon ==== - - m.def("distance_ray_polygon_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - poly_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::distance(ray, poly); - }); - - m.def("distance2_ray_polygon_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - poly_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::distance2(ray, poly); - }); - - // ==== Segment to Segment ==== - - m.def("distance_segment_segment_double3d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<3, double>(seg0_data); - auto seg1 = make_segment_from_array<3, double>(seg1_data); - return tf::distance(seg0, seg1); - }); - - m.def("distance2_segment_segment_double3d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<3, double>(seg0_data); - auto seg1 = make_segment_from_array<3, double>(seg1_data); - return tf::distance2(seg0, seg1); - }); - - // ==== Segment to Polygon ==== - - m.def("distance_segment_polygon_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - poly_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::distance(seg, poly); - }); - - m.def("distance2_segment_polygon_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - poly_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::distance2(seg, poly); - }); - - // ==== Polygon to Polygon ==== - - m.def("distance_polygon_polygon_double3d", - [](nanobind::ndarray> - poly0_data, - nanobind::ndarray> - poly1_data) { - auto poly0 = make_polygon_from_array<3, double>(poly0_data); - auto poly1 = make_polygon_from_array<3, double>(poly1_data); - return tf::distance(poly0, poly1); - }); - - m.def("distance2_polygon_polygon_double3d", - [](nanobind::ndarray> - poly0_data, - nanobind::ndarray> - poly1_data) { - auto poly0 = make_polygon_from_array<3, double>(poly0_data); - auto poly1 = make_polygon_from_array<3, double>(poly1_data); - return tf::distance2(poly0, poly1); - }); - - // ==== Segment to Plane (3D only) ==== - - m.def("distance_segment_plane_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - plane_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance(seg, plane); - }); - - m.def("distance2_segment_plane_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - plane_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance2(seg, plane); - }); - - // ==== Ray to Plane (3D only) ==== - - m.def("distance_ray_plane_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance(ray, plane); - }); - - m.def("distance2_ray_plane_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance2(ray, plane); - }); - - // ==== Line to Plane (3D only) ==== - - m.def("distance_line_plane_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - plane_data) { - auto line = make_line_from_array<3, double>(line_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance(line, plane); - }); - - m.def("distance2_line_plane_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - plane_data) { - auto line = make_line_from_array<3, double>(line_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance2(line, plane); - }); - - // ==== Polygon to Plane (3D only) ==== - - m.def("distance_polygon_plane_double3d", - [](nanobind::ndarray> - poly_data, - nanobind::ndarray> - plane_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance(poly, plane); - }); - - m.def("distance2_polygon_plane_double3d", - [](nanobind::ndarray> - poly_data, - nanobind::ndarray> - plane_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::distance2(poly, plane); - }); - - // ==== Plane to Plane (3D only) ==== - - m.def("distance_plane_plane_double3d", - [](nanobind::ndarray> - plane0_data, - nanobind::ndarray> - plane1_data) { - auto plane0 = make_plane_from_array<3, double>(plane0_data); - auto plane1 = make_plane_from_array<3, double>(plane1_data); - return tf::distance(plane0, plane1); - }); - - m.def("distance2_plane_plane_double3d", - [](nanobind::ndarray> - plane0_data, - nanobind::ndarray> - plane1_data) { - auto plane0 = make_plane_from_array<3, double>(plane0_data); - auto plane1 = make_plane_from_array<3, double>(plane1_data); - return tf::distance2(plane0, plane1); - }); - -} - -} // namespace tf::py diff --git a/python/src/core/distance_field.cpp b/python/src/core/distance_field.cpp deleted file mode 100644 index a07ffae..0000000 --- a/python/src/core/distance_field.cpp +++ /dev/null @@ -1,245 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include "trueform/python/core/distance_field.hpp" -#include "trueform/python/core/make_primitives.hpp" -#include -#include - -namespace tf::py { - -auto register_core_distance_field(nanobind::module_ &m) -> void { - // ==== Distance field to Plane (3D only) ==== - - // float, 3D - m.def( - "distance_field_plane_float3d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - return distance_field_impl<3, float>(points, plane); - }, - nanobind::arg("points"), nanobind::arg("plane")); - - // double, 3D - m.def( - "distance_field_plane_double3d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - return distance_field_impl<3, double>(points, plane); - }, - nanobind::arg("points"), nanobind::arg("plane")); - - // ==== Distance field to Segment ==== - - // float, 2D - m.def( - "distance_field_segment_float2d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - segment_data) { - auto segment = make_segment_from_array<2, float>(segment_data); - return distance_field_impl<2, float>(points, segment); - }, - nanobind::arg("points"), nanobind::arg("segment")); - - // float, 3D - m.def( - "distance_field_segment_float3d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - segment_data) { - auto segment = make_segment_from_array<3, float>(segment_data); - return distance_field_impl<3, float>(points, segment); - }, - nanobind::arg("points"), nanobind::arg("segment")); - - // double, 2D - m.def( - "distance_field_segment_double2d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - segment_data) { - auto segment = make_segment_from_array<2, double>(segment_data); - return distance_field_impl<2, double>(points, segment); - }, - nanobind::arg("points"), nanobind::arg("segment")); - - // double, 3D - m.def( - "distance_field_segment_double3d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - segment_data) { - auto segment = make_segment_from_array<3, double>(segment_data); - return distance_field_impl<3, double>(points, segment); - }, - nanobind::arg("points"), nanobind::arg("segment")); - - // ==== Distance field to Polygon ==== - - // float, 2D - m.def( - "distance_field_polygon_float2d", - [](nanobind::ndarray> - points, - nanobind::ndarray poly_data) { - auto polygon = make_polygon_from_array<2, float>(poly_data); - return distance_field_impl<2, float>(points, polygon); - }, - nanobind::arg("points"), nanobind::arg("polygon")); - - // float, 3D - m.def( - "distance_field_polygon_float3d", - [](nanobind::ndarray> - points, - nanobind::ndarray poly_data) { - auto polygon = make_polygon_from_array<3, float>(poly_data); - return distance_field_impl<3, float>(points, polygon); - }, - nanobind::arg("points"), nanobind::arg("polygon")); - - // double, 2D - m.def( - "distance_field_polygon_double2d", - [](nanobind::ndarray> - points, - nanobind::ndarray poly_data) { - auto polygon = make_polygon_from_array<2, double>(poly_data); - return distance_field_impl<2, double>(points, polygon); - }, - nanobind::arg("points"), nanobind::arg("polygon")); - - // double, 3D - m.def( - "distance_field_polygon_double3d", - [](nanobind::ndarray> - points, - nanobind::ndarray poly_data) { - auto polygon = make_polygon_from_array<3, double>(poly_data); - return distance_field_impl<3, double>(points, polygon); - }, - nanobind::arg("points"), nanobind::arg("polygon")); - - // ==== Distance field to Line ==== - - // float, 2D - m.def( - "distance_field_line_float2d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, float>(line_data); - return distance_field_impl<2, float>(points, line); - }, - nanobind::arg("points"), nanobind::arg("line")); - - // float, 3D - m.def( - "distance_field_line_float3d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, float>(line_data); - return distance_field_impl<3, float>(points, line); - }, - nanobind::arg("points"), nanobind::arg("line")); - - // double, 2D - m.def( - "distance_field_line_double2d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, double>(line_data); - return distance_field_impl<2, double>(points, line); - }, - nanobind::arg("points"), nanobind::arg("line")); - - // double, 3D - m.def( - "distance_field_line_double3d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, double>(line_data); - return distance_field_impl<3, double>(points, line); - }, - nanobind::arg("points"), nanobind::arg("line")); - - // ==== Distance field to AABB ==== - - // float, 2D - m.def( - "distance_field_aabb_float2d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - aabb_data) { - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return distance_field_impl<2, float>(points, aabb); - }, - nanobind::arg("points"), nanobind::arg("aabb")); - - // float, 3D - m.def( - "distance_field_aabb_float3d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - aabb_data) { - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return distance_field_impl<3, float>(points, aabb); - }, - nanobind::arg("points"), nanobind::arg("aabb")); - - // double, 2D - m.def( - "distance_field_aabb_double2d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - aabb_data) { - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return distance_field_impl<2, double>(points, aabb); - }, - nanobind::arg("points"), nanobind::arg("aabb")); - - // double, 3D - m.def( - "distance_field_aabb_double3d", - [](nanobind::ndarray> - points, - nanobind::ndarray> - aabb_data) { - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return distance_field_impl<3, double>(points, aabb); - }, - nanobind::arg("points"), nanobind::arg("aabb")); -} - -} // namespace tf::py diff --git a/python/src/core/distance_float2d.cpp b/python/src/core/distance_float2d.cpp deleted file mode 100644 index 96e7589..0000000 --- a/python/src/core/distance_float2d.cpp +++ /dev/null @@ -1,379 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include "trueform/python/core/distance.hpp" -#include "trueform/python/core/make_primitives.hpp" -#include -#include -#include - -namespace tf::py { - -auto register_core_distance_float2d(nanobind::module_ &m) -> void { - // ==== Point to Point ==== - - m.def("distance_point_point_float2d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<2, float>(pt0_data); - auto pt1 = make_point_from_array<2, float>(pt1_data); - return tf::distance(pt0, pt1); - }); - - m.def("distance2_point_point_float2d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<2, float>(pt0_data); - auto pt1 = make_point_from_array<2, float>(pt1_data); - return tf::distance2(pt0, pt1); - }); - - m.def("distance_point_aabb_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return tf::distance(pt, aabb); - }); - - m.def("distance2_point_aabb_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return tf::distance2(pt, aabb); - }); - - m.def("distance_point_line_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto line = make_line_from_array<2, float>(line_data); - return tf::distance(pt, line); - }); - - m.def("distance2_point_line_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto line = make_line_from_array<2, float>(line_data); - return tf::distance2(pt, line); - }); - - m.def("distance_point_ray_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return tf::distance(pt, ray); - }); - - m.def("distance2_point_ray_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return tf::distance2(pt, ray); - }); - - m.def("distance_point_segment_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::distance(pt, seg); - }); - - m.def("distance2_point_segment_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::distance2(pt, seg); - }); - - m.def("distance_point_polygon_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - poly_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::distance(pt, poly); - }); - - m.def("distance2_point_polygon_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - poly_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::distance2(pt, poly); - }); - - m.def("distance_aabb_aabb_float2d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<2, float>(aabb0_data); - auto aabb1 = make_aabb_from_array<2, float>(aabb1_data); - return tf::distance(aabb0, aabb1); - }); - - m.def("distance2_aabb_aabb_float2d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<2, float>(aabb0_data); - auto aabb1 = make_aabb_from_array<2, float>(aabb1_data); - return tf::distance2(aabb0, aabb1); - }); - - m.def("distance_line_line_float2d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<2, float>(line0_data); - auto line1 = make_line_from_array<2, float>(line1_data); - return tf::distance(line0, line1); - }); - - m.def("distance2_line_line_float2d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<2, float>(line0_data); - auto line1 = make_line_from_array<2, float>(line1_data); - return tf::distance2(line0, line1); - }); - - m.def("distance_line_ray_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<2, float>(line_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return tf::distance(line, ray); - }); - - m.def("distance2_line_ray_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<2, float>(line_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return tf::distance2(line, ray); - }); - - m.def("distance_line_segment_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<2, float>(line_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::distance(line, seg); - }); - - m.def("distance2_line_segment_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<2, float>(line_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::distance2(line, seg); - }); - - m.def("distance_line_polygon_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - poly_data) { - auto line = make_line_from_array<2, float>(line_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::distance(line, poly); - }); - - m.def("distance2_line_polygon_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - poly_data) { - auto line = make_line_from_array<2, float>(line_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::distance2(line, poly); - }); - - m.def("distance_ray_ray_float2d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<2, float>(ray0_data); - auto ray1 = make_ray_from_array<2, float>(ray1_data); - return tf::distance(ray0, ray1); - }); - - m.def("distance2_ray_ray_float2d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<2, float>(ray0_data); - auto ray1 = make_ray_from_array<2, float>(ray1_data); - return tf::distance2(ray0, ray1); - }); - - m.def("distance_ray_segment_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::distance(ray, seg); - }); - - m.def("distance2_ray_segment_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::distance2(ray, seg); - }); - - m.def("distance_ray_polygon_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - poly_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::distance(ray, poly); - }); - - m.def("distance2_ray_polygon_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - poly_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::distance2(ray, poly); - }); - - m.def("distance_segment_segment_float2d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<2, float>(seg0_data); - auto seg1 = make_segment_from_array<2, float>(seg1_data); - return tf::distance(seg0, seg1); - }); - - m.def("distance2_segment_segment_float2d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<2, float>(seg0_data); - auto seg1 = make_segment_from_array<2, float>(seg1_data); - return tf::distance2(seg0, seg1); - }); - - m.def("distance_segment_polygon_float2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - poly_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::distance(seg, poly); - }); - - m.def("distance2_segment_polygon_float2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - poly_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::distance2(seg, poly); - }); - - m.def("distance_polygon_polygon_float2d", - [](nanobind::ndarray> - poly0_data, - nanobind::ndarray> - poly1_data) { - auto poly0 = make_polygon_from_array<2, float>(poly0_data); - auto poly1 = make_polygon_from_array<2, float>(poly1_data); - return tf::distance(poly0, poly1); - }); - - m.def("distance2_polygon_polygon_float2d", - [](nanobind::ndarray> - poly0_data, - nanobind::ndarray> - poly1_data) { - auto poly0 = make_polygon_from_array<2, float>(poly0_data); - auto poly1 = make_polygon_from_array<2, float>(poly1_data); - return tf::distance2(poly0, poly1); - }); - -} - -} // namespace tf::py diff --git a/python/src/core/distance_float3d.cpp b/python/src/core/distance_float3d.cpp deleted file mode 100644 index daf88bc..0000000 --- a/python/src/core/distance_float3d.cpp +++ /dev/null @@ -1,509 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include "trueform/python/core/distance.hpp" -#include "trueform/python/core/make_primitives.hpp" -#include -#include -#include - -namespace tf::py { - -auto register_core_distance_float3d(nanobind::module_ &m) -> void { - // ==== Point to Point ==== - - m.def("distance_point_point_float3d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<3, float>(pt0_data); - auto pt1 = make_point_from_array<3, float>(pt1_data); - return tf::distance(pt0, pt1); - }); - - m.def("distance2_point_point_float3d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<3, float>(pt0_data); - auto pt1 = make_point_from_array<3, float>(pt1_data); - return tf::distance2(pt0, pt1); - }); - - m.def("distance_point_aabb_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return tf::distance(pt, aabb); - }); - - m.def("distance2_point_aabb_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return tf::distance2(pt, aabb); - }); - - m.def("distance_point_plane_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - plane_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance(pt, plane); - }); - - m.def("distance2_point_plane_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - plane_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance2(pt, plane); - }); - - m.def("distance_point_line_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto line = make_line_from_array<3, float>(line_data); - return tf::distance(pt, line); - }); - - m.def("distance2_point_line_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto line = make_line_from_array<3, float>(line_data); - return tf::distance2(pt, line); - }); - - m.def("distance_point_ray_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return tf::distance(pt, ray); - }); - - m.def("distance2_point_ray_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return tf::distance2(pt, ray); - }); - - m.def("distance_point_segment_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::distance(pt, seg); - }); - - m.def("distance2_point_segment_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::distance2(pt, seg); - }); - - m.def("distance_point_polygon_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - poly_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::distance(pt, poly); - }); - - m.def("distance2_point_polygon_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - poly_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::distance2(pt, poly); - }); - - m.def("distance_aabb_aabb_float3d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<3, float>(aabb0_data); - auto aabb1 = make_aabb_from_array<3, float>(aabb1_data); - return tf::distance(aabb0, aabb1); - }); - - m.def("distance2_aabb_aabb_float3d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<3, float>(aabb0_data); - auto aabb1 = make_aabb_from_array<3, float>(aabb1_data); - return tf::distance2(aabb0, aabb1); - }); - - m.def("distance_line_line_float3d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<3, float>(line0_data); - auto line1 = make_line_from_array<3, float>(line1_data); - return tf::distance(line0, line1); - }); - - m.def("distance2_line_line_float3d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<3, float>(line0_data); - auto line1 = make_line_from_array<3, float>(line1_data); - return tf::distance2(line0, line1); - }); - - m.def("distance_line_ray_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<3, float>(line_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return tf::distance(line, ray); - }); - - m.def("distance2_line_ray_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<3, float>(line_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return tf::distance2(line, ray); - }); - - m.def("distance_line_segment_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<3, float>(line_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::distance(line, seg); - }); - - m.def("distance2_line_segment_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<3, float>(line_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::distance2(line, seg); - }); - - m.def("distance_line_polygon_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - poly_data) { - auto line = make_line_from_array<3, float>(line_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::distance(line, poly); - }); - - m.def("distance2_line_polygon_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - poly_data) { - auto line = make_line_from_array<3, float>(line_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::distance2(line, poly); - }); - - m.def("distance_ray_ray_float3d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<3, float>(ray0_data); - auto ray1 = make_ray_from_array<3, float>(ray1_data); - return tf::distance(ray0, ray1); - }); - - m.def("distance2_ray_ray_float3d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<3, float>(ray0_data); - auto ray1 = make_ray_from_array<3, float>(ray1_data); - return tf::distance2(ray0, ray1); - }); - - m.def("distance_ray_segment_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::distance(ray, seg); - }); - - m.def("distance2_ray_segment_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::distance2(ray, seg); - }); - - m.def("distance_ray_polygon_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - poly_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::distance(ray, poly); - }); - - m.def("distance2_ray_polygon_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - poly_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::distance2(ray, poly); - }); - - m.def("distance_segment_segment_float3d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<3, float>(seg0_data); - auto seg1 = make_segment_from_array<3, float>(seg1_data); - return tf::distance(seg0, seg1); - }); - - m.def("distance2_segment_segment_float3d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<3, float>(seg0_data); - auto seg1 = make_segment_from_array<3, float>(seg1_data); - return tf::distance2(seg0, seg1); - }); - - m.def("distance_segment_polygon_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - poly_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::distance(seg, poly); - }); - - m.def("distance2_segment_polygon_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - poly_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::distance2(seg, poly); - }); - - m.def("distance_polygon_polygon_float3d", - [](nanobind::ndarray> - poly0_data, - nanobind::ndarray> - poly1_data) { - auto poly0 = make_polygon_from_array<3, float>(poly0_data); - auto poly1 = make_polygon_from_array<3, float>(poly1_data); - return tf::distance(poly0, poly1); - }); - - m.def("distance2_polygon_polygon_float3d", - [](nanobind::ndarray> - poly0_data, - nanobind::ndarray> - poly1_data) { - auto poly0 = make_polygon_from_array<3, float>(poly0_data); - auto poly1 = make_polygon_from_array<3, float>(poly1_data); - return tf::distance2(poly0, poly1); - }); - - // ==== Segment to Plane (3D only) ==== - - m.def("distance_segment_plane_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - plane_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance(seg, plane); - }); - - m.def("distance2_segment_plane_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - plane_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance2(seg, plane); - }); - - // ==== Ray to Plane (3D only) ==== - - m.def("distance_ray_plane_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance(ray, plane); - }); - - m.def("distance2_ray_plane_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance2(ray, plane); - }); - - // ==== Line to Plane (3D only) ==== - - m.def("distance_line_plane_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - plane_data) { - auto line = make_line_from_array<3, float>(line_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance(line, plane); - }); - - m.def("distance2_line_plane_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - plane_data) { - auto line = make_line_from_array<3, float>(line_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance2(line, plane); - }); - - // ==== Polygon to Plane (3D only) ==== - - m.def("distance_polygon_plane_float3d", - [](nanobind::ndarray> - poly_data, - nanobind::ndarray> - plane_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance(poly, plane); - }); - - m.def("distance2_polygon_plane_float3d", - [](nanobind::ndarray> - poly_data, - nanobind::ndarray> - plane_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::distance2(poly, plane); - }); - - // ==== Plane to Plane (3D only) ==== - - m.def("distance_plane_plane_float3d", - [](nanobind::ndarray> - plane0_data, - nanobind::ndarray> - plane1_data) { - auto plane0 = make_plane_from_array<3, float>(plane0_data); - auto plane1 = make_plane_from_array<3, float>(plane1_data); - return tf::distance(plane0, plane1); - }); - - m.def("distance2_plane_plane_float3d", - [](nanobind::ndarray> - plane0_data, - nanobind::ndarray> - plane1_data) { - auto plane0 = make_plane_from_array<3, float>(plane0_data); - auto plane1 = make_plane_from_array<3, float>(plane1_data); - return tf::distance2(plane0, plane1); - }); - -} - -} // namespace tf::py diff --git a/python/src/core/intersects.cpp b/python/src/core/intersects.cpp deleted file mode 100644 index a12c37e..0000000 --- a/python/src/core/intersects.cpp +++ /dev/null @@ -1,1002 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include "trueform/python/core/intersects.hpp" -#include "trueform/python/core/make_primitives.hpp" -#include -#include -#include - -namespace tf::py { - -auto register_core_intersects(nanobind::module_ &m) -> void { - // ==== Point to Point ==== - m.def("intersects_point_point_float2d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<2, float>(pt0_data); - auto pt1 = make_point_from_array<2, float>(pt1_data); - return tf::intersects(pt0, pt1); - }); - - m.def("intersects_point_point_float3d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<3, float>(pt0_data); - auto pt1 = make_point_from_array<3, float>(pt1_data); - return tf::intersects(pt0, pt1); - }); - - m.def("intersects_point_point_double2d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<2, double>(pt0_data); - auto pt1 = make_point_from_array<2, double>(pt1_data); - return tf::intersects(pt0, pt1); - }); - - m.def("intersects_point_point_double3d", - [](nanobind::ndarray> - pt0_data, - nanobind::ndarray> - pt1_data) { - auto pt0 = make_point_from_array<3, double>(pt0_data); - auto pt1 = make_point_from_array<3, double>(pt1_data); - return tf::intersects(pt0, pt1); - }); - - // ==== Point to AABB ==== - m.def("intersects_point_aabb_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return tf::intersects(pt, aabb); - }); - - m.def("intersects_point_aabb_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return tf::intersects(pt, aabb); - }); - - m.def("intersects_point_aabb_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return tf::intersects(pt, aabb); - }); - - m.def("intersects_point_aabb_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - aabb_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return tf::intersects(pt, aabb); - }); - - // ==== Point to Line ==== - m.def("intersects_point_line_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto line = make_line_from_array<2, float>(line_data); - return tf::intersects(pt, line); - }); - - m.def("intersects_point_line_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto line = make_line_from_array<3, float>(line_data); - return tf::intersects(pt, line); - }); - - m.def("intersects_point_line_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto line = make_line_from_array<2, double>(line_data); - return tf::intersects(pt, line); - }); - - m.def("intersects_point_line_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - line_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto line = make_line_from_array<3, double>(line_data); - return tf::intersects(pt, line); - }); - - // ==== Point to Ray ==== - m.def("intersects_point_ray_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return tf::intersects(pt, ray); - }); - - m.def("intersects_point_ray_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return tf::intersects(pt, ray); - }); - - m.def("intersects_point_ray_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return tf::intersects(pt, ray); - }); - - m.def("intersects_point_ray_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - ray_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return tf::intersects(pt, ray); - }); - - // ==== Point to Segment ==== - m.def("intersects_point_segment_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::intersects(pt, seg); - }); - - m.def("intersects_point_segment_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::intersects(pt, seg); - }); - - m.def("intersects_point_segment_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::intersects(pt, seg); - }); - - m.def("intersects_point_segment_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - seg_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::intersects(pt, seg); - }); - - // ==== Point to Polygon ==== - m.def("intersects_point_polygon_float2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray poly_data) { - auto pt = make_point_from_array<2, float>(pt_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::intersects(pt, poly); - }); - - m.def("intersects_point_polygon_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray poly_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::intersects(pt, poly); - }); - - m.def("intersects_point_polygon_double2d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray poly_data) { - auto pt = make_point_from_array<2, double>(pt_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::intersects(pt, poly); - }); - - m.def("intersects_point_polygon_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray poly_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::intersects(pt, poly); - }); - - // ==== Point to Plane (3D only) ==== - m.def("intersects_point_plane_float3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - plane_data) { - auto pt = make_point_from_array<3, float>(pt_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::intersects(pt, plane); - }); - - m.def("intersects_point_plane_double3d", - [](nanobind::ndarray> - pt_data, - nanobind::ndarray> - plane_data) { - auto pt = make_point_from_array<3, double>(pt_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::intersects(pt, plane); - }); - - // ==== AABB to AABB ==== - m.def("intersects_aabb_aabb_float2d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<2, float>(aabb0_data); - auto aabb1 = make_aabb_from_array<2, float>(aabb1_data); - return tf::intersects(aabb0, aabb1); - }); - - m.def("intersects_aabb_aabb_float3d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<3, float>(aabb0_data); - auto aabb1 = make_aabb_from_array<3, float>(aabb1_data); - return tf::intersects(aabb0, aabb1); - }); - - m.def("intersects_aabb_aabb_double2d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<2, double>(aabb0_data); - auto aabb1 = make_aabb_from_array<2, double>(aabb1_data); - return tf::intersects(aabb0, aabb1); - }); - - m.def("intersects_aabb_aabb_double3d", - [](nanobind::ndarray> - aabb0_data, - nanobind::ndarray> - aabb1_data) { - auto aabb0 = make_aabb_from_array<3, double>(aabb0_data); - auto aabb1 = make_aabb_from_array<3, double>(aabb1_data); - return tf::intersects(aabb0, aabb1); - }); - - // ==== AABB to Plane (3D only) ==== - m.def("intersects_aabb_plane_float3d", - [](nanobind::ndarray> - aabb_data, - nanobind::ndarray> - plane_data) { - auto aabb = make_aabb_from_array<3, float>(aabb_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::intersects(aabb, plane); - }); - - m.def("intersects_aabb_plane_double3d", - [](nanobind::ndarray> - aabb_data, - nanobind::ndarray> - plane_data) { - auto aabb = make_aabb_from_array<3, double>(aabb_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::intersects(aabb, plane); - }); - - // ==== Line to Line ==== - m.def("intersects_line_line_float2d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<2, float>(line0_data); - auto line1 = make_line_from_array<2, float>(line1_data); - return tf::intersects(line0, line1); - }); - - m.def("intersects_line_line_float3d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<3, float>(line0_data); - auto line1 = make_line_from_array<3, float>(line1_data); - return tf::intersects(line0, line1); - }); - - m.def("intersects_line_line_double2d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<2, double>(line0_data); - auto line1 = make_line_from_array<2, double>(line1_data); - return tf::intersects(line0, line1); - }); - - m.def("intersects_line_line_double3d", - [](nanobind::ndarray> - line0_data, - nanobind::ndarray> - line1_data) { - auto line0 = make_line_from_array<3, double>(line0_data); - auto line1 = make_line_from_array<3, double>(line1_data); - return tf::intersects(line0, line1); - }); - - // ==== Line to Ray ==== - m.def("intersects_line_ray_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<2, float>(line_data); - auto ray = make_ray_from_array<2, float>(ray_data); - return tf::intersects(line, ray); - }); - - m.def("intersects_line_ray_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<3, float>(line_data); - auto ray = make_ray_from_array<3, float>(ray_data); - return tf::intersects(line, ray); - }); - - m.def("intersects_line_ray_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<2, double>(line_data); - auto ray = make_ray_from_array<2, double>(ray_data); - return tf::intersects(line, ray); - }); - - m.def("intersects_line_ray_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - ray_data) { - auto line = make_line_from_array<3, double>(line_data); - auto ray = make_ray_from_array<3, double>(ray_data); - return tf::intersects(line, ray); - }); - - // ==== Line to Segment ==== - m.def("intersects_line_segment_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<2, float>(line_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::intersects(line, seg); - }); - - m.def("intersects_line_segment_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<3, float>(line_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::intersects(line, seg); - }); - - m.def("intersects_line_segment_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<2, double>(line_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::intersects(line, seg); - }); - - m.def("intersects_line_segment_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - seg_data) { - auto line = make_line_from_array<3, double>(line_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::intersects(line, seg); - }); - - // ==== Line to Polygon ==== - m.def("intersects_line_polygon_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray poly_data) { - auto line = make_line_from_array<2, float>(line_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::intersects(line, poly); - }); - - m.def("intersects_line_polygon_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray poly_data) { - auto line = make_line_from_array<3, float>(line_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::intersects(line, poly); - }); - - m.def("intersects_line_polygon_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray poly_data) { - auto line = make_line_from_array<2, double>(line_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::intersects(line, poly); - }); - - m.def("intersects_line_polygon_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray poly_data) { - auto line = make_line_from_array<3, double>(line_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::intersects(line, poly); - }); - - // ==== Line to Plane (3D only) ==== - m.def("intersects_line_plane_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - plane_data) { - auto line = make_line_from_array<3, float>(line_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::intersects(line, plane); - }); - - m.def("intersects_line_plane_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - plane_data) { - auto line = make_line_from_array<3, double>(line_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::intersects(line, plane); - }); - - // ==== Ray to Ray ==== - m.def("intersects_ray_ray_float2d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<2, float>(ray0_data); - auto ray1 = make_ray_from_array<2, float>(ray1_data); - return tf::intersects(ray0, ray1); - }); - - m.def("intersects_ray_ray_float3d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<3, float>(ray0_data); - auto ray1 = make_ray_from_array<3, float>(ray1_data); - return tf::intersects(ray0, ray1); - }); - - m.def("intersects_ray_ray_double2d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<2, double>(ray0_data); - auto ray1 = make_ray_from_array<2, double>(ray1_data); - return tf::intersects(ray0, ray1); - }); - - m.def("intersects_ray_ray_double3d", - [](nanobind::ndarray> - ray0_data, - nanobind::ndarray> - ray1_data) { - auto ray0 = make_ray_from_array<3, double>(ray0_data); - auto ray1 = make_ray_from_array<3, double>(ray1_data); - return tf::intersects(ray0, ray1); - }); - - // ==== Ray to Segment ==== - m.def("intersects_ray_segment_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return tf::intersects(ray, seg); - }); - - m.def("intersects_ray_segment_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return tf::intersects(ray, seg); - }); - - m.def("intersects_ray_segment_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return tf::intersects(ray, seg); - }); - - m.def("intersects_ray_segment_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return tf::intersects(ray, seg); - }); - - // ==== Ray to Polygon ==== - m.def("intersects_ray_polygon_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray poly_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::intersects(ray, poly); - }); - - m.def("intersects_ray_polygon_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray poly_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::intersects(ray, poly); - }); - - m.def("intersects_ray_polygon_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray poly_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::intersects(ray, poly); - }); - - m.def("intersects_ray_polygon_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray poly_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::intersects(ray, poly); - }); - - // ==== Ray to Plane (3D only) ==== - m.def("intersects_ray_plane_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::intersects(ray, plane); - }); - - m.def("intersects_ray_plane_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::intersects(ray, plane); - }); - - // ==== Segment to Segment ==== - m.def("intersects_segment_segment_float2d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<2, float>(seg0_data); - auto seg1 = make_segment_from_array<2, float>(seg1_data); - return tf::intersects(seg0, seg1); - }); - - m.def("intersects_segment_segment_float3d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<3, float>(seg0_data); - auto seg1 = make_segment_from_array<3, float>(seg1_data); - return tf::intersects(seg0, seg1); - }); - - m.def("intersects_segment_segment_double2d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<2, double>(seg0_data); - auto seg1 = make_segment_from_array<2, double>(seg1_data); - return tf::intersects(seg0, seg1); - }); - - m.def("intersects_segment_segment_double3d", - [](nanobind::ndarray> - seg0_data, - nanobind::ndarray> - seg1_data) { - auto seg0 = make_segment_from_array<3, double>(seg0_data); - auto seg1 = make_segment_from_array<3, double>(seg1_data); - return tf::intersects(seg0, seg1); - }); - - // ==== Segment to Polygon ==== - m.def("intersects_segment_polygon_float2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray poly_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return tf::intersects(seg, poly); - }); - - m.def("intersects_segment_polygon_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray poly_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return tf::intersects(seg, poly); - }); - - m.def("intersects_segment_polygon_double2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray poly_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return tf::intersects(seg, poly); - }); - - m.def("intersects_segment_polygon_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray poly_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return tf::intersects(seg, poly); - }); - - // ==== Segment to Plane (3D only) ==== - m.def("intersects_segment_plane_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - plane_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::intersects(seg, plane); - }); - - m.def("intersects_segment_plane_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - plane_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::intersects(seg, plane); - }); - - // ==== Polygon to Polygon ==== - m.def("intersects_polygon_polygon_float2d", - [](nanobind::ndarray poly0_data, - nanobind::ndarray poly1_data) { - auto poly0 = make_polygon_from_array<2, float>(poly0_data); - auto poly1 = make_polygon_from_array<2, float>(poly1_data); - return tf::intersects(poly0, poly1); - }); - - m.def("intersects_polygon_polygon_float3d", - [](nanobind::ndarray poly0_data, - nanobind::ndarray poly1_data) { - auto poly0 = make_polygon_from_array<3, float>(poly0_data); - auto poly1 = make_polygon_from_array<3, float>(poly1_data); - return tf::intersects(poly0, poly1); - }); - - m.def("intersects_polygon_polygon_double2d", - [](nanobind::ndarray poly0_data, - nanobind::ndarray poly1_data) { - auto poly0 = make_polygon_from_array<2, double>(poly0_data); - auto poly1 = make_polygon_from_array<2, double>(poly1_data); - return tf::intersects(poly0, poly1); - }); - - m.def("intersects_polygon_polygon_double3d", - [](nanobind::ndarray poly0_data, - nanobind::ndarray poly1_data) { - auto poly0 = make_polygon_from_array<3, double>(poly0_data); - auto poly1 = make_polygon_from_array<3, double>(poly1_data); - return tf::intersects(poly0, poly1); - }); - - // ==== Polygon to Plane (3D only) ==== - m.def("intersects_polygon_plane_float3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - plane_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return tf::intersects(poly, plane); - }); - - m.def("intersects_polygon_plane_double3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - plane_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return tf::intersects(poly, plane); - }); - - // ==== Segment to AABB ==== - m.def("intersects_segment_aabb_float2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - aabb_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return tf::intersects(seg, aabb); - }); - - m.def("intersects_segment_aabb_float3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - aabb_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return tf::intersects(seg, aabb); - }); - - m.def("intersects_segment_aabb_double2d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - aabb_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return tf::intersects(seg, aabb); - }); - - m.def("intersects_segment_aabb_double3d", - [](nanobind::ndarray> - seg_data, - nanobind::ndarray> - aabb_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return tf::intersects(seg, aabb); - }); - - // ==== Ray to AABB ==== - m.def("intersects_ray_aabb_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - aabb_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return tf::intersects(ray, aabb); - }); - - m.def("intersects_ray_aabb_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - aabb_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return tf::intersects(ray, aabb); - }); - - m.def("intersects_ray_aabb_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - aabb_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return tf::intersects(ray, aabb); - }); - - m.def("intersects_ray_aabb_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - aabb_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return tf::intersects(ray, aabb); - }); - - // ==== Line to AABB ==== - m.def("intersects_line_aabb_float2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - aabb_data) { - auto line = make_line_from_array<2, float>(line_data); - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return tf::intersects(line, aabb); - }); - - m.def("intersects_line_aabb_float3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - aabb_data) { - auto line = make_line_from_array<3, float>(line_data); - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return tf::intersects(line, aabb); - }); - - m.def("intersects_line_aabb_double2d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - aabb_data) { - auto line = make_line_from_array<2, double>(line_data); - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return tf::intersects(line, aabb); - }); - - m.def("intersects_line_aabb_double3d", - [](nanobind::ndarray> - line_data, - nanobind::ndarray> - aabb_data) { - auto line = make_line_from_array<3, double>(line_data); - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return tf::intersects(line, aabb); - }); - - // ==== Polygon to AABB ==== - m.def("intersects_polygon_aabb_float2d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - aabb_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return tf::intersects(poly, aabb); - }); - - m.def("intersects_polygon_aabb_float3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - aabb_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return tf::intersects(poly, aabb); - }); - - m.def("intersects_polygon_aabb_double2d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - aabb_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return tf::intersects(poly, aabb); - }); - - m.def("intersects_polygon_aabb_double3d", - [](nanobind::ndarray poly_data, - nanobind::ndarray> - aabb_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return tf::intersects(poly, aabb); - }); - - // ==== Plane to Plane (3D only) ==== - m.def("intersects_plane_plane_float3d", - [](nanobind::ndarray> - plane0_data, - nanobind::ndarray> - plane1_data) { - auto plane0 = make_plane_from_array<3, float>(plane0_data); - auto plane1 = make_plane_from_array<3, float>(plane1_data); - return tf::intersects(plane0, plane1); - }); - - m.def("intersects_plane_plane_double3d", - [](nanobind::ndarray> - plane0_data, - nanobind::ndarray> - plane1_data) { - auto plane0 = make_plane_from_array<3, double>(plane0_data); - auto plane1 = make_plane_from_array<3, double>(plane1_data); - return tf::intersects(plane0, plane1); - }); -} - -} // namespace tf::py diff --git a/python/src/core/primitive_wrapper.cpp b/python/src/core/primitive_wrapper.cpp new file mode 100644 index 0000000..4754878 --- /dev/null +++ b/python/src/core/primitive_wrapper.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/core/primitive_wrapper.hpp" + +namespace tf::py { + +auto register_primitive_wrappers(nanobind::module_ &m) -> void { + register_primitive_wrapper<2, float>(m, "PrimitiveWrapperFloat2D"); + register_primitive_wrapper<3, float>(m, "PrimitiveWrapperFloat3D"); + register_primitive_wrapper<2, double>(m, "PrimitiveWrapperDouble2D"); + register_primitive_wrapper<3, double>(m, "PrimitiveWrapperDouble3D"); +} + +// Explicit template instantiations +template class primitive_wrapper<2, float>; +template class primitive_wrapper<3, float>; +template class primitive_wrapper<2, double>; +template class primitive_wrapper<3, double>; + +} // namespace tf::py diff --git a/python/src/core/ray_cast.cpp b/python/src/core/ray_cast.cpp deleted file mode 100644 index 7b4e9a9..0000000 --- a/python/src/core/ray_cast.cpp +++ /dev/null @@ -1,335 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include "trueform/python/core/ray_cast.hpp" -#include "trueform/python/core/make_primitives.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -namespace { - -// Convert ray_cast_info result to Python Optional[float] -template -auto ray_cast_info_to_optional(const ResultT &result) { - if (result) { - // Intersection occurred, return t - return nanobind::cast(result.t); - } else { - // No intersection, return None - return nanobind::none(); - } -} - -} // anonymous namespace - -auto register_core_ray_cast(nanobind::module_ &m) -> void { - // ==== Ray to Plane (3D only) ==== - // Ray to Plane (float, 3D) - m.def( - "ray_cast_ray_plane_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, float>(ray_data); - auto plane = make_plane_from_array<3, float>(plane_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, plane, config)); - }, - nanobind::arg("ray"), nanobind::arg("plane"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Plane (double, 3D) - m.def( - "ray_cast_ray_plane_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - plane_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, double>(ray_data); - auto plane = make_plane_from_array<3, double>(plane_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, plane, config)); - }, - nanobind::arg("ray"), nanobind::arg("plane"), - nanobind::arg("config").none() = nanobind::none()); - - // ==== Ray to Polygon ==== - // Ray to Polygon (float, 2D) - m.def( - "ray_cast_ray_polygon_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray poly_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<2, float>(ray_data); - auto poly = make_polygon_from_array<2, float>(poly_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, poly, config)); - }, - nanobind::arg("ray"), nanobind::arg("polygon"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Polygon (float, 3D) - m.def( - "ray_cast_ray_polygon_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray poly_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, float>(ray_data); - auto poly = make_polygon_from_array<3, float>(poly_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, poly, config)); - }, - nanobind::arg("ray"), nanobind::arg("polygon"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Polygon (double, 2D) - m.def( - "ray_cast_ray_polygon_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray poly_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<2, double>(ray_data); - auto poly = make_polygon_from_array<2, double>(poly_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, poly, config)); - }, - nanobind::arg("ray"), nanobind::arg("polygon"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Polygon (double, 3D) - m.def( - "ray_cast_ray_polygon_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray poly_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, double>(ray_data); - auto poly = make_polygon_from_array<3, double>(poly_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, poly, config)); - }, - nanobind::arg("ray"), nanobind::arg("polygon"), - nanobind::arg("config").none() = nanobind::none()); - - // ==== Ray to Segment ==== - // Ray to Segment (float, 2D) - m.def( - "ray_cast_ray_segment_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<2, float>(ray_data); - auto seg = make_segment_from_array<2, float>(seg_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, seg, config)); - }, - nanobind::arg("ray"), nanobind::arg("segment"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Segment (float, 3D) - m.def( - "ray_cast_ray_segment_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, float>(ray_data); - auto seg = make_segment_from_array<3, float>(seg_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, seg, config)); - }, - nanobind::arg("ray"), nanobind::arg("segment"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Segment (double, 2D) - m.def( - "ray_cast_ray_segment_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<2, double>(ray_data); - auto seg = make_segment_from_array<2, double>(seg_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, seg, config)); - }, - nanobind::arg("ray"), nanobind::arg("segment"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Segment (double, 3D) - m.def( - "ray_cast_ray_segment_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - seg_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, double>(ray_data); - auto seg = make_segment_from_array<3, double>(seg_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, seg, config)); - }, - nanobind::arg("ray"), nanobind::arg("segment"), - nanobind::arg("config").none() = nanobind::none()); - - // ==== Ray to Line ==== - // Ray to Line (float, 2D) - m.def( - "ray_cast_ray_line_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - line_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<2, float>(ray_data); - auto line = make_line_from_array<2, float>(line_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, line, config)); - }, - nanobind::arg("ray"), nanobind::arg("line"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Line (float, 3D) - m.def( - "ray_cast_ray_line_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - line_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, float>(ray_data); - auto line = make_line_from_array<3, float>(line_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, line, config)); - }, - nanobind::arg("ray"), nanobind::arg("line"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Line (double, 2D) - m.def( - "ray_cast_ray_line_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - line_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<2, double>(ray_data); - auto line = make_line_from_array<2, double>(line_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, line, config)); - }, - nanobind::arg("ray"), nanobind::arg("line"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to Line (double, 3D) - m.def( - "ray_cast_ray_line_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - line_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, double>(ray_data); - auto line = make_line_from_array<3, double>(line_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, line, config)); - }, - nanobind::arg("ray"), nanobind::arg("line"), - nanobind::arg("config").none() = nanobind::none()); - - // ==== Ray to AABB ==== - // Ray to AABB (float, 2D) - m.def( - "ray_cast_ray_aabb_float2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - aabb_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<2, float>(ray_data); - auto aabb = make_aabb_from_array<2, float>(aabb_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, aabb, config)); - }, - nanobind::arg("ray"), nanobind::arg("aabb"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to AABB (float, 3D) - m.def( - "ray_cast_ray_aabb_float3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - aabb_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, float>(ray_data); - auto aabb = make_aabb_from_array<3, float>(aabb_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, aabb, config)); - }, - nanobind::arg("ray"), nanobind::arg("aabb"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to AABB (double, 2D) - m.def( - "ray_cast_ray_aabb_double2d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - aabb_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<2, double>(ray_data); - auto aabb = make_aabb_from_array<2, double>(aabb_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, aabb, config)); - }, - nanobind::arg("ray"), nanobind::arg("aabb"), - nanobind::arg("config").none() = nanobind::none()); - - // Ray to AABB (double, 3D) - m.def( - "ray_cast_ray_aabb_double3d", - [](nanobind::ndarray> - ray_data, - nanobind::ndarray> - aabb_data, - std::optional> opt_config) { - auto config = tf::py::make_ray_config_from_optional(opt_config); - auto ray = make_ray_from_array<3, double>(ray_data); - auto aabb = make_aabb_from_array<3, double>(aabb_data); - return ray_cast_info_to_optional(tf::ray_cast(ray, aabb, config)); - }, - nanobind::arg("ray"), nanobind::arg("aabb"), - nanobind::arg("config").none() = nanobind::none()); -} - -} // namespace tf::py diff --git a/python/src/spatial.cpp b/python/src/spatial.cpp index feaae44..845677a 100644 --- a/python/src/spatial.cpp +++ b/python/src/spatial.cpp @@ -24,25 +24,13 @@ auto register_spatial_module(nanobind::module_ &m) -> void { register_mesh(spatial_module); register_edge_mesh(spatial_module); - // Register spatial operations - register_point_cloud_neighbor_search(spatial_module); - register_mesh_neighbor_search(spatial_module); - register_edge_mesh_neighbor_search(spatial_module); + // Register spatial operations (form x form) register_point_cloud_neighbor_search_point_cloud(spatial_module); register_edge_mesh_neighbor_search_edge_mesh(spatial_module); register_edge_mesh_neighbor_search_point_cloud(spatial_module); register_mesh_neighbor_search_point_cloud(spatial_module); register_mesh_neighbor_search_edge_mesh(spatial_module); register_mesh_neighbor_search_mesh(spatial_module); - register_point_cloud_ray_cast(spatial_module); - register_mesh_ray_cast(spatial_module); - register_edge_mesh_ray_cast(spatial_module); - register_mesh_intersects_primitive(spatial_module); - register_edge_mesh_intersects_primitive(spatial_module); - register_point_cloud_intersects_primitive(spatial_module); - register_mesh_gather_ids_primitive(spatial_module); - register_edge_mesh_gather_ids_primitive(spatial_module); - register_point_cloud_gather_ids_primitive(spatial_module); register_point_cloud_gather_ids_point_cloud(spatial_module); register_edge_mesh_gather_ids_edge_mesh(spatial_module); register_edge_mesh_gather_ids_point_cloud(spatial_module); @@ -55,6 +43,17 @@ auto register_spatial_module(nanobind::module_ &m) -> void { register_mesh_intersects_point_cloud(spatial_module); register_mesh_intersects_edge_mesh(spatial_module); register_mesh_intersects_mesh(spatial_module); + + // Unified prim × prim operations (dispatch-based) + register_prim_prim_ops(spatial_module); + + // Primitive geometry operations (area, normals) + register_prim_geometry_ops(spatial_module); + + // Unified form × prim operations (dispatch-based) + register_mesh_fp(spatial_module); + register_edge_mesh_fp(spatial_module); + register_point_cloud_fp(spatial_module); } } // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp.cpp b/python/src/spatial/edge_mesh_fp.cpp new file mode 100644 index 0000000..902e22f --- /dev/null +++ b/python/src/spatial/edge_mesh_fp.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include + +namespace tf::py { + +auto register_edge_mesh_fp_intfloat2d(nanobind::module_ &m) -> void; +auto register_edge_mesh_fp_intfloat3d(nanobind::module_ &m) -> void; +auto register_edge_mesh_fp_intdouble2d(nanobind::module_ &m) -> void; +auto register_edge_mesh_fp_intdouble3d(nanobind::module_ &m) -> void; +auto register_edge_mesh_fp_int64float2d(nanobind::module_ &m) -> void; +auto register_edge_mesh_fp_int64float3d(nanobind::module_ &m) -> void; +auto register_edge_mesh_fp_int64double2d(nanobind::module_ &m) -> void; +auto register_edge_mesh_fp_int64double3d(nanobind::module_ &m) -> void; + +auto register_edge_mesh_fp(nanobind::module_ &m) -> void { + register_edge_mesh_fp_intfloat2d(m); + register_edge_mesh_fp_intfloat3d(m); + register_edge_mesh_fp_intdouble2d(m); + register_edge_mesh_fp_intdouble3d(m); + register_edge_mesh_fp_int64float2d(m); + register_edge_mesh_fp_int64float3d(m); + register_edge_mesh_fp_int64double2d(m); + register_edge_mesh_fp_int64double3d(m); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp_int64double2d.cpp b/python/src/spatial/edge_mesh_fp_int64double2d.cpp new file mode 100644 index 0000000..f07eb4d --- /dev/null +++ b/python/src/spatial/edge_mesh_fp_int64double2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_edge_mesh_fp_int64double2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, double>( + m, "edge_mesh", "int64double2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp_int64double3d.cpp b/python/src/spatial/edge_mesh_fp_int64double3d.cpp new file mode 100644 index 0000000..29fa643 --- /dev/null +++ b/python/src/spatial/edge_mesh_fp_int64double3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_edge_mesh_fp_int64double3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, double>( + m, "edge_mesh", "int64double3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp_int64float2d.cpp b/python/src/spatial/edge_mesh_fp_int64float2d.cpp new file mode 100644 index 0000000..23bb116 --- /dev/null +++ b/python/src/spatial/edge_mesh_fp_int64float2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_edge_mesh_fp_int64float2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, float>( + m, "edge_mesh", "int64float2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp_int64float3d.cpp b/python/src/spatial/edge_mesh_fp_int64float3d.cpp new file mode 100644 index 0000000..bb8ea19 --- /dev/null +++ b/python/src/spatial/edge_mesh_fp_int64float3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_edge_mesh_fp_int64float3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, float>( + m, "edge_mesh", "int64float3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp_intdouble2d.cpp b/python/src/spatial/edge_mesh_fp_intdouble2d.cpp new file mode 100644 index 0000000..b6fabd1 --- /dev/null +++ b/python/src/spatial/edge_mesh_fp_intdouble2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_edge_mesh_fp_intdouble2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, double>( + m, "edge_mesh", "intdouble2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp_intdouble3d.cpp b/python/src/spatial/edge_mesh_fp_intdouble3d.cpp new file mode 100644 index 0000000..3fbe514 --- /dev/null +++ b/python/src/spatial/edge_mesh_fp_intdouble3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_edge_mesh_fp_intdouble3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, double>( + m, "edge_mesh", "intdouble3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp_intfloat2d.cpp b/python/src/spatial/edge_mesh_fp_intfloat2d.cpp new file mode 100644 index 0000000..895ae10 --- /dev/null +++ b/python/src/spatial/edge_mesh_fp_intfloat2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_edge_mesh_fp_intfloat2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, float>( + m, "edge_mesh", "intfloat2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_fp_intfloat3d.cpp b/python/src/spatial/edge_mesh_fp_intfloat3d.cpp new file mode 100644 index 0000000..0f28c96 --- /dev/null +++ b/python/src/spatial/edge_mesh_fp_intfloat3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_edge_mesh_fp_intfloat3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, float>( + m, "edge_mesh", "intfloat3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_gather_ids_primitive.cpp b/python/src/spatial/edge_mesh_gather_ids_primitive.cpp deleted file mode 100644 index 69b1db4..0000000 --- a/python/src/spatial/edge_mesh_gather_ids_primitive.cpp +++ /dev/null @@ -1,1449 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_edge_mesh_gather_ids_primitive(nanobind::module_ &m) -> void { - - namespace nb = nanobind; - - // ============================================================================ - // gather_ids - int, float, 2D - // ============================================================================ - - // Point - intfloat2d - m.def("gather_ids_point_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - intfloat2d - m.def("gather_ids_segment_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - intfloat2d - m.def("gather_ids_polygon_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - intfloat2d - m.def("gather_ids_ray_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - intfloat2d - m.def("gather_ids_line_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int, float, 3D - // ============================================================================ - - // Point - intfloat3d - m.def("gather_ids_point_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - intfloat3d - m.def("gather_ids_segment_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - intfloat3d - m.def("gather_ids_polygon_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - intfloat3d - m.def("gather_ids_ray_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - intfloat3d - m.def("gather_ids_line_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int, double, 2D - // ============================================================================ - - // Point - intdouble2d - m.def("gather_ids_point_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - intdouble2d - m.def("gather_ids_segment_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - intdouble2d - m.def("gather_ids_polygon_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - intdouble2d - m.def("gather_ids_ray_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - intdouble2d - m.def("gather_ids_line_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int, double, 3D - // ============================================================================ - - // Point - intdouble3d - m.def("gather_ids_point_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - intdouble3d - m.def("gather_ids_segment_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - intdouble3d - m.def("gather_ids_polygon_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - intdouble3d - m.def("gather_ids_ray_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - intdouble3d - m.def("gather_ids_line_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, float, 2D - // ============================================================================ - - // Point - int64float2d - m.def("gather_ids_point_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int64float2d - m.def("gather_ids_segment_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int64float2d - m.def("gather_ids_polygon_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int64float2d - m.def("gather_ids_ray_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int64float2d - m.def("gather_ids_line_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, float, 3D - // ============================================================================ - - // Point - int64float3d - m.def("gather_ids_point_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int64float3d - m.def("gather_ids_segment_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int64float3d - m.def("gather_ids_polygon_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int64float3d - m.def("gather_ids_ray_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int64float3d - m.def("gather_ids_line_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, double, 2D - // ============================================================================ - - // Point - int64double2d - m.def("gather_ids_point_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int64double2d - m.def("gather_ids_segment_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int64double2d - m.def("gather_ids_polygon_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int64double2d - m.def("gather_ids_ray_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int64double2d - m.def("gather_ids_line_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, double, 3D - // ============================================================================ - - // Point - int64double3d - m.def("gather_ids_point_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int64double3d - m.def("gather_ids_segment_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int64double3d - m.def("gather_ids_polygon_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int64double3d - m.def("gather_ids_ray_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int64double3d - m.def("gather_ids_line_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(edge_mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("edge_mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_intersects_primitive.cpp b/python/src/spatial/edge_mesh_intersects_primitive.cpp deleted file mode 100644 index d96ee5c..0000000 --- a/python/src/spatial/edge_mesh_intersects_primitive.cpp +++ /dev/null @@ -1,446 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include - -namespace tf::py { - -auto register_edge_mesh_intersects_primitive(nanobind::module_ &m) -> void { - - // ============================================================================ - // EdgeMesh intersects primitives - // Index types: int, int64 - // Real types: float, double - // Dims: 2D, 3D - // Total: 2 × 2 × 2 = 8 edge mesh types - // Primitives: Point, Segment, Polygon, Ray, Line = 5 primitives - // Total: 8 × 5 = 40 functions - // ============================================================================ - - // int32, float, 2D - m.def("intersects_edge_mesh_point_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, float>(pt_data); - return form_intersects_primitive(edge_mesh, pt); - }, - nanobind::arg("edge_mesh"), nanobind::arg("point")); - - m.def("intersects_edge_mesh_segment_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - return form_intersects_primitive(edge_mesh, seg); - }, - nanobind::arg("edge_mesh"), nanobind::arg("segment")); - - m.def("intersects_edge_mesh_polygon_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - return form_intersects_primitive(edge_mesh, poly); - }, - nanobind::arg("edge_mesh"), nanobind::arg("polygon")); - - m.def("intersects_edge_mesh_ray_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - return form_intersects_primitive(edge_mesh, ray); - }, - nanobind::arg("edge_mesh"), nanobind::arg("ray")); - - m.def("intersects_edge_mesh_line_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, float>(line_data); - return form_intersects_primitive(edge_mesh, line); - }, - nanobind::arg("edge_mesh"), nanobind::arg("line")); - - // int32, float, 3D - m.def("intersects_edge_mesh_point_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, float>(pt_data); - return form_intersects_primitive(edge_mesh, pt); - }, - nanobind::arg("edge_mesh"), nanobind::arg("point")); - - m.def("intersects_edge_mesh_segment_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - return form_intersects_primitive(edge_mesh, seg); - }, - nanobind::arg("edge_mesh"), nanobind::arg("segment")); - - m.def("intersects_edge_mesh_polygon_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - return form_intersects_primitive(edge_mesh, poly); - }, - nanobind::arg("edge_mesh"), nanobind::arg("polygon")); - - m.def("intersects_edge_mesh_ray_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - return form_intersects_primitive(edge_mesh, ray); - }, - nanobind::arg("edge_mesh"), nanobind::arg("ray")); - - m.def("intersects_edge_mesh_line_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, float>(line_data); - return form_intersects_primitive(edge_mesh, line); - }, - nanobind::arg("edge_mesh"), nanobind::arg("line")); - - // int32, double, 2D - m.def("intersects_edge_mesh_point_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, double>(pt_data); - return form_intersects_primitive(edge_mesh, pt); - }, - nanobind::arg("edge_mesh"), nanobind::arg("point")); - - m.def("intersects_edge_mesh_segment_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - return form_intersects_primitive(edge_mesh, seg); - }, - nanobind::arg("edge_mesh"), nanobind::arg("segment")); - - m.def("intersects_edge_mesh_polygon_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - return form_intersects_primitive(edge_mesh, poly); - }, - nanobind::arg("edge_mesh"), nanobind::arg("polygon")); - - m.def("intersects_edge_mesh_ray_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - return form_intersects_primitive(edge_mesh, ray); - }, - nanobind::arg("edge_mesh"), nanobind::arg("ray")); - - m.def("intersects_edge_mesh_line_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, double>(line_data); - return form_intersects_primitive(edge_mesh, line); - }, - nanobind::arg("edge_mesh"), nanobind::arg("line")); - - // int32, double, 3D - m.def("intersects_edge_mesh_point_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, double>(pt_data); - return form_intersects_primitive(edge_mesh, pt); - }, - nanobind::arg("edge_mesh"), nanobind::arg("point")); - - m.def("intersects_edge_mesh_segment_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - return form_intersects_primitive(edge_mesh, seg); - }, - nanobind::arg("edge_mesh"), nanobind::arg("segment")); - - m.def("intersects_edge_mesh_polygon_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - return form_intersects_primitive(edge_mesh, poly); - }, - nanobind::arg("edge_mesh"), nanobind::arg("polygon")); - - m.def("intersects_edge_mesh_ray_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - return form_intersects_primitive(edge_mesh, ray); - }, - nanobind::arg("edge_mesh"), nanobind::arg("ray")); - - m.def("intersects_edge_mesh_line_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, double>(line_data); - return form_intersects_primitive(edge_mesh, line); - }, - nanobind::arg("edge_mesh"), nanobind::arg("line")); - - // int64, float, 2D - m.def("intersects_edge_mesh_point_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, float>(pt_data); - return form_intersects_primitive(edge_mesh, pt); - }, - nanobind::arg("edge_mesh"), nanobind::arg("point")); - - m.def("intersects_edge_mesh_segment_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - return form_intersects_primitive(edge_mesh, seg); - }, - nanobind::arg("edge_mesh"), nanobind::arg("segment")); - - m.def("intersects_edge_mesh_polygon_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - return form_intersects_primitive(edge_mesh, poly); - }, - nanobind::arg("edge_mesh"), nanobind::arg("polygon")); - - m.def("intersects_edge_mesh_ray_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - return form_intersects_primitive(edge_mesh, ray); - }, - nanobind::arg("edge_mesh"), nanobind::arg("ray")); - - m.def("intersects_edge_mesh_line_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, float>(line_data); - return form_intersects_primitive(edge_mesh, line); - }, - nanobind::arg("edge_mesh"), nanobind::arg("line")); - - // int64, float, 3D - m.def("intersects_edge_mesh_point_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, float>(pt_data); - return form_intersects_primitive(edge_mesh, pt); - }, - nanobind::arg("edge_mesh"), nanobind::arg("point")); - - m.def("intersects_edge_mesh_segment_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - return form_intersects_primitive(edge_mesh, seg); - }, - nanobind::arg("edge_mesh"), nanobind::arg("segment")); - - m.def("intersects_edge_mesh_polygon_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - return form_intersects_primitive(edge_mesh, poly); - }, - nanobind::arg("edge_mesh"), nanobind::arg("polygon")); - - m.def("intersects_edge_mesh_ray_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - return form_intersects_primitive(edge_mesh, ray); - }, - nanobind::arg("edge_mesh"), nanobind::arg("ray")); - - m.def("intersects_edge_mesh_line_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, float>(line_data); - return form_intersects_primitive(edge_mesh, line); - }, - nanobind::arg("edge_mesh"), nanobind::arg("line")); - - // int64, double, 2D - m.def("intersects_edge_mesh_point_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, double>(pt_data); - return form_intersects_primitive(edge_mesh, pt); - }, - nanobind::arg("edge_mesh"), nanobind::arg("point")); - - m.def("intersects_edge_mesh_segment_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - return form_intersects_primitive(edge_mesh, seg); - }, - nanobind::arg("edge_mesh"), nanobind::arg("segment")); - - m.def("intersects_edge_mesh_polygon_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - return form_intersects_primitive(edge_mesh, poly); - }, - nanobind::arg("edge_mesh"), nanobind::arg("polygon")); - - m.def("intersects_edge_mesh_ray_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - return form_intersects_primitive(edge_mesh, ray); - }, - nanobind::arg("edge_mesh"), nanobind::arg("ray")); - - m.def("intersects_edge_mesh_line_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, double>(line_data); - return form_intersects_primitive(edge_mesh, line); - }, - nanobind::arg("edge_mesh"), nanobind::arg("line")); - - // int64, double, 3D - m.def("intersects_edge_mesh_point_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, double>(pt_data); - return form_intersects_primitive(edge_mesh, pt); - }, - nanobind::arg("edge_mesh"), nanobind::arg("point")); - - m.def("intersects_edge_mesh_segment_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - return form_intersects_primitive(edge_mesh, seg); - }, - nanobind::arg("edge_mesh"), nanobind::arg("segment")); - - m.def("intersects_edge_mesh_polygon_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - return form_intersects_primitive(edge_mesh, poly); - }, - nanobind::arg("edge_mesh"), nanobind::arg("polygon")); - - m.def("intersects_edge_mesh_ray_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - return form_intersects_primitive(edge_mesh, ray); - }, - nanobind::arg("edge_mesh"), nanobind::arg("ray")); - - m.def("intersects_edge_mesh_line_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, double>(line_data); - return form_intersects_primitive(edge_mesh, line); - }, - nanobind::arg("edge_mesh"), nanobind::arg("line")); - - // ==== Plane (3D only) ==== - // int32, float, 3D - m.def("intersects_edge_mesh_plane_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - return form_intersects_primitive(edge_mesh, plane); - }, - nanobind::arg("edge_mesh"), nanobind::arg("plane")); - - // int32, double, 3D - m.def("intersects_edge_mesh_plane_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - return form_intersects_primitive(edge_mesh, plane); - }, - nanobind::arg("edge_mesh"), nanobind::arg("plane")); - - // int64, float, 3D - m.def("intersects_edge_mesh_plane_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - return form_intersects_primitive(edge_mesh, plane); - }, - nanobind::arg("edge_mesh"), nanobind::arg("plane")); - - // int64, double, 3D - m.def("intersects_edge_mesh_plane_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - return form_intersects_primitive(edge_mesh, plane); - }, - nanobind::arg("edge_mesh"), nanobind::arg("plane")); -} - -} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_neighbor_search.cpp b/python/src/spatial/edge_mesh_neighbor_search.cpp deleted file mode 100644 index f8fb38d..0000000 --- a/python/src/spatial/edge_mesh_neighbor_search.cpp +++ /dev/null @@ -1,668 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_edge_mesh_neighbor_search(nanobind::module_ &m) -> void { - - // ============================================================================ - // Non-KNN neighbor search (single nearest neighbor) - // EdgeMeshWrapperIntFloat2D (int, float, 2D) - // ============================================================================ - - // Point queries - m.def("neighbor_search_edge_mesh_point_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_point_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_edge_mesh_segment_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_segment_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_edge_mesh_polygon_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_polygon_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_edge_mesh_ray_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_ray_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_edge_mesh_line_intfloat2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_line_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // ============================================================================ - // EdgeMeshWrapperIntFloat3D (int, float, 3D) - // ============================================================================ - - // Point queries - m.def("neighbor_search_edge_mesh_point_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_point_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_edge_mesh_segment_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_segment_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_edge_mesh_polygon_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_polygon_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_edge_mesh_ray_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_ray_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_edge_mesh_line_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_line_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_edge_mesh_plane_intfloat3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_plane_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // ============================================================================ - // EdgeMeshWrapperIntDouble2D (int, double, 2D) - // ============================================================================ - - // Point queries - m.def("neighbor_search_edge_mesh_point_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_point_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_edge_mesh_segment_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_segment_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_edge_mesh_polygon_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_polygon_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_edge_mesh_ray_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_ray_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_edge_mesh_line_intdouble2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_line_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // ============================================================================ - // EdgeMeshWrapperIntDouble3D (int, double, 3D) - // ============================================================================ - - // Point queries - m.def("neighbor_search_edge_mesh_point_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_point_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_edge_mesh_segment_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_segment_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_edge_mesh_polygon_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_polygon_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_edge_mesh_ray_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_ray_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_edge_mesh_line_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_line_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_edge_mesh_plane_intdouble3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_plane_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // ============================================================================ - // EdgeMeshWrapperInt64Float2D (int64, float, 2D) - // ============================================================================ - - // Point queries - m.def("neighbor_search_edge_mesh_point_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_point_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_edge_mesh_segment_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_segment_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_edge_mesh_polygon_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_polygon_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_edge_mesh_ray_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_ray_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_edge_mesh_line_int64float2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_line_from_array<2, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // ============================================================================ - // EdgeMeshWrapperInt64Float3D (int64, float, 3D) - // ============================================================================ - - // Point queries - m.def("neighbor_search_edge_mesh_point_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_point_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_edge_mesh_segment_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_segment_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_edge_mesh_polygon_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_polygon_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_edge_mesh_ray_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_ray_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_edge_mesh_line_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_line_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_edge_mesh_plane_int64float3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_plane_from_array<3, float>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // ============================================================================ - // EdgeMeshWrapperInt64Double2D (int64, double, 2D) - // ============================================================================ - - // Point queries - m.def("neighbor_search_edge_mesh_point_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_point_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_edge_mesh_segment_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_segment_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_edge_mesh_polygon_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_polygon_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_edge_mesh_ray_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_ray_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_edge_mesh_line_int64double2d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_line_from_array<2, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // ============================================================================ - // EdgeMeshWrapperInt64Double3D (int64, double, 3D) - // ============================================================================ - - // Point queries - m.def("neighbor_search_edge_mesh_point_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_point_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_edge_mesh_segment_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_segment_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_edge_mesh_polygon_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_polygon_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_edge_mesh_ray_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_ray_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_edge_mesh_line_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_line_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_edge_mesh_plane_int64double3d", - [](edge_mesh_wrapper &edge_mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - edge_mesh, make_plane_from_array<3, double>(query), radius); - }, - nanobind::arg("edge_mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); -} - -} // namespace tf::py diff --git a/python/src/spatial/edge_mesh_ray_cast.cpp b/python/src/spatial/edge_mesh_ray_cast.cpp deleted file mode 100644 index 26f650c..0000000 --- a/python/src/spatial/edge_mesh_ray_cast.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_edge_mesh_ray_cast(nanobind::module_ &m) -> void { - - // ============================================================================ - // Ray cast on edge meshes - all type combinations - // Index types: int, int64 - // Real types: float, double - // Dims: 2D, 3D - // Total: 8 combinations - // ============================================================================ - - // int32, float, 2D - m.def( - "ray_cast_edge_mesh_intfloat2d", - [](nanobind::ndarray> - ray_data, - edge_mesh_wrapper &edge_mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto result = ray_cast(ray, edge_mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("edge_mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, float, 3D - m.def( - "ray_cast_edge_mesh_intfloat3d", - [](nanobind::ndarray> - ray_data, - edge_mesh_wrapper &edge_mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto result = ray_cast(ray, edge_mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("edge_mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, double, 2D - m.def( - "ray_cast_edge_mesh_intdouble2d", - [](nanobind::ndarray> - ray_data, - edge_mesh_wrapper &edge_mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto result = ray_cast(ray, edge_mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("edge_mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, double, 3D - m.def( - "ray_cast_edge_mesh_intdouble3d", - [](nanobind::ndarray> - ray_data, - edge_mesh_wrapper &edge_mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto result = ray_cast(ray, edge_mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("edge_mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, float, 2D - m.def( - "ray_cast_edge_mesh_int64float2d", - [](nanobind::ndarray> - ray_data, - edge_mesh_wrapper &edge_mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto result = ray_cast(ray, edge_mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("edge_mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, float, 3D - m.def( - "ray_cast_edge_mesh_int64float3d", - [](nanobind::ndarray> - ray_data, - edge_mesh_wrapper &edge_mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto result = ray_cast(ray, edge_mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("edge_mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, double, 2D - m.def( - "ray_cast_edge_mesh_int64double2d", - [](nanobind::ndarray> - ray_data, - edge_mesh_wrapper &edge_mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto result = ray_cast(ray, edge_mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("edge_mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, double, 3D - m.def( - "ray_cast_edge_mesh_int64double3d", - [](nanobind::ndarray> - ray_data, - edge_mesh_wrapper &edge_mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto result = ray_cast(ray, edge_mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("edge_mesh"), - nanobind::arg("config").none() = nanobind::none()); -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_fp.cpp b/python/src/spatial/mesh_fp.cpp new file mode 100644 index 0000000..7e9e071 --- /dev/null +++ b/python/src/spatial/mesh_fp.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include + +namespace tf::py { + +auto register_mesh_fp_int3float2d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int3float3d(nanobind::module_ &m) -> void; +auto register_mesh_fp_intdynfloat2d(nanobind::module_ &m) -> void; +auto register_mesh_fp_intdynfloat3d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int3double2d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int3double3d(nanobind::module_ &m) -> void; +auto register_mesh_fp_intdyndouble2d(nanobind::module_ &m) -> void; +auto register_mesh_fp_intdyndouble3d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int643float2d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int643float3d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int64dynfloat2d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int64dynfloat3d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int643double2d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int643double3d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int64dyndouble2d(nanobind::module_ &m) -> void; +auto register_mesh_fp_int64dyndouble3d(nanobind::module_ &m) -> void; + +auto register_mesh_fp(nanobind::module_ &m) -> void { + register_mesh_fp_int3float2d(m); + register_mesh_fp_int3float3d(m); + register_mesh_fp_intdynfloat2d(m); + register_mesh_fp_intdynfloat3d(m); + register_mesh_fp_int3double2d(m); + register_mesh_fp_int3double3d(m); + register_mesh_fp_intdyndouble2d(m); + register_mesh_fp_intdyndouble3d(m); + register_mesh_fp_int643float2d(m); + register_mesh_fp_int643float3d(m); + register_mesh_fp_int64dynfloat2d(m); + register_mesh_fp_int64dynfloat3d(m); + register_mesh_fp_int643double2d(m); + register_mesh_fp_int643double3d(m); + register_mesh_fp_int64dyndouble2d(m); + register_mesh_fp_int64dyndouble3d(m); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int3double2d.cpp b/python/src/spatial/mesh_fp_int3double2d.cpp new file mode 100644 index 0000000..c6f3552 --- /dev/null +++ b/python/src/spatial/mesh_fp_int3double2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int3double2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, double>( + m, "mesh", "int3double2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int3double3d.cpp b/python/src/spatial/mesh_fp_int3double3d.cpp new file mode 100644 index 0000000..f606698 --- /dev/null +++ b/python/src/spatial/mesh_fp_int3double3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int3double3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, double>( + m, "mesh", "int3double3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int3float2d.cpp b/python/src/spatial/mesh_fp_int3float2d.cpp new file mode 100644 index 0000000..0a5f97b --- /dev/null +++ b/python/src/spatial/mesh_fp_int3float2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int3float2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, float>( + m, "mesh", "int3float2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int3float3d.cpp b/python/src/spatial/mesh_fp_int3float3d.cpp new file mode 100644 index 0000000..eae4d31 --- /dev/null +++ b/python/src/spatial/mesh_fp_int3float3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int3float3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, float>( + m, "mesh", "int3float3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int643double2d.cpp b/python/src/spatial/mesh_fp_int643double2d.cpp new file mode 100644 index 0000000..90e248d --- /dev/null +++ b/python/src/spatial/mesh_fp_int643double2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int643double2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, double>( + m, "mesh", "int643double2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int643double3d.cpp b/python/src/spatial/mesh_fp_int643double3d.cpp new file mode 100644 index 0000000..3833ab8 --- /dev/null +++ b/python/src/spatial/mesh_fp_int643double3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int643double3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, double>( + m, "mesh", "int643double3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int643float2d.cpp b/python/src/spatial/mesh_fp_int643float2d.cpp new file mode 100644 index 0000000..14bbbd7 --- /dev/null +++ b/python/src/spatial/mesh_fp_int643float2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int643float2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, float>( + m, "mesh", "int643float2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int643float3d.cpp b/python/src/spatial/mesh_fp_int643float3d.cpp new file mode 100644 index 0000000..d82ccc2 --- /dev/null +++ b/python/src/spatial/mesh_fp_int643float3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int643float3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, float>( + m, "mesh", "int643float3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int64dyndouble2d.cpp b/python/src/spatial/mesh_fp_int64dyndouble2d.cpp new file mode 100644 index 0000000..a7a69f3 --- /dev/null +++ b/python/src/spatial/mesh_fp_int64dyndouble2d.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int64dyndouble2d(nanobind::module_ &m) -> void { + register_form_prim_ops< + mesh_wrapper, 2, double>( + m, "mesh", "int64dyndouble2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int64dyndouble3d.cpp b/python/src/spatial/mesh_fp_int64dyndouble3d.cpp new file mode 100644 index 0000000..9544dcb --- /dev/null +++ b/python/src/spatial/mesh_fp_int64dyndouble3d.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int64dyndouble3d(nanobind::module_ &m) -> void { + register_form_prim_ops< + mesh_wrapper, 3, double>( + m, "mesh", "int64dyndouble3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int64dynfloat2d.cpp b/python/src/spatial/mesh_fp_int64dynfloat2d.cpp new file mode 100644 index 0000000..c6a1ae8 --- /dev/null +++ b/python/src/spatial/mesh_fp_int64dynfloat2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int64dynfloat2d(nanobind::module_ &m) -> void { + register_form_prim_ops, + 2, float>(m, "mesh", "int64dynfloat2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_int64dynfloat3d.cpp b/python/src/spatial/mesh_fp_int64dynfloat3d.cpp new file mode 100644 index 0000000..fd6f3c0 --- /dev/null +++ b/python/src/spatial/mesh_fp_int64dynfloat3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_int64dynfloat3d(nanobind::module_ &m) -> void { + register_form_prim_ops, + 3, float>(m, "mesh", "int64dynfloat3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_intdyndouble2d.cpp b/python/src/spatial/mesh_fp_intdyndouble2d.cpp new file mode 100644 index 0000000..ddf9e6d --- /dev/null +++ b/python/src/spatial/mesh_fp_intdyndouble2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_intdyndouble2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, + double>(m, "mesh", "intdyndouble2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_intdyndouble3d.cpp b/python/src/spatial/mesh_fp_intdyndouble3d.cpp new file mode 100644 index 0000000..b8386b0 --- /dev/null +++ b/python/src/spatial/mesh_fp_intdyndouble3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_intdyndouble3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, + double>(m, "mesh", "intdyndouble3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_intdynfloat2d.cpp b/python/src/spatial/mesh_fp_intdynfloat2d.cpp new file mode 100644 index 0000000..e511db6 --- /dev/null +++ b/python/src/spatial/mesh_fp_intdynfloat2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_intdynfloat2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, + float>(m, "mesh", "intdynfloat2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_fp_intdynfloat3d.cpp b/python/src/spatial/mesh_fp_intdynfloat3d.cpp new file mode 100644 index 0000000..cedb388 --- /dev/null +++ b/python/src/spatial/mesh_fp_intdynfloat3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_mesh_fp_intdynfloat3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, + float>(m, "mesh", "intdynfloat3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/mesh_gather_ids_primitive.cpp b/python/src/spatial/mesh_gather_ids_primitive.cpp deleted file mode 100644 index 8937546..0000000 --- a/python/src/spatial/mesh_gather_ids_primitive.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include - -namespace tf::py { - -// Forward declarations for mesh_gather_ids_primitive bindings split across multiple files -auto register_mesh_gather_ids_primitive_float2d(nanobind::module_ &m) -> void; -auto register_mesh_gather_ids_primitive_float3d(nanobind::module_ &m) -> void; -auto register_mesh_gather_ids_primitive_double2d(nanobind::module_ &m) -> void; -auto register_mesh_gather_ids_primitive_double3d(nanobind::module_ &m) -> void; - -auto register_mesh_gather_ids_primitive(nanobind::module_ &m) -> void { - // Register all mesh_gather_ids_primitive bindings - // Split across multiple files for parallel compilation - register_mesh_gather_ids_primitive_float2d(m); - register_mesh_gather_ids_primitive_float3d(m); - register_mesh_gather_ids_primitive_double2d(m); - register_mesh_gather_ids_primitive_double3d(m); -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_gather_ids_primitive_double2d.cpp b/python/src/spatial/mesh_gather_ids_primitive_double2d.cpp deleted file mode 100644 index ce5704f..0000000 --- a/python/src/spatial/mesh_gather_ids_primitive_double2d.cpp +++ /dev/null @@ -1,741 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_gather_ids_primitive_double2d(nanobind::module_ &m) -> void { - - namespace nb = nanobind; - - // ============================================================================ - // gather_ids - int, double, ngon=3, 2D - // ============================================================================ - - // Point - int3double2d - m.def("gather_ids_point_int3double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int3double2d - m.def("gather_ids_segment_int3double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int3double2d - m.def("gather_ids_polygon_int3double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int3double2d - m.def("gather_ids_ray_int3double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int3double2d - m.def("gather_ids_line_int3double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int, double, dynamic, 2D - // ============================================================================ - - // Point - intdyndouble2d - m.def("gather_ids_point_intdyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - intdyndouble2d - m.def("gather_ids_segment_intdyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - intdyndouble2d - m.def("gather_ids_polygon_intdyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - intdyndouble2d - m.def("gather_ids_ray_intdyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - intdyndouble2d - m.def("gather_ids_line_intdyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, double, ngon=3, 2D - // ============================================================================ - - // Point - int643double2d - m.def("gather_ids_point_int643double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int643double2d - m.def("gather_ids_segment_int643double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int643double2d - m.def("gather_ids_polygon_int643double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int643double2d - m.def("gather_ids_ray_int643double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int643double2d - m.def("gather_ids_line_int643double2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, double, dynamic, 2D - // ============================================================================ - - // Point - int64dyndouble2d - m.def("gather_ids_point_int64dyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int64dyndouble2d - m.def("gather_ids_segment_int64dyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int64dyndouble2d - m.def("gather_ids_polygon_int64dyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int64dyndouble2d - m.def("gather_ids_ray_int64dyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int64dyndouble2d - m.def("gather_ids_line_int64dyndouble2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_gather_ids_primitive_double3d.cpp b/python/src/spatial/mesh_gather_ids_primitive_double3d.cpp deleted file mode 100644 index 21d6d5b..0000000 --- a/python/src/spatial/mesh_gather_ids_primitive_double3d.cpp +++ /dev/null @@ -1,741 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_gather_ids_primitive_double3d(nanobind::module_ &m) -> void { - - namespace nb = nanobind; - - // ============================================================================ - // gather_ids - int, double, ngon=3, 3D - // ============================================================================ - - // Point - int3double3d - m.def("gather_ids_point_int3double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int3double3d - m.def("gather_ids_segment_int3double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int3double3d - m.def("gather_ids_polygon_int3double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int3double3d - m.def("gather_ids_ray_int3double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int3double3d - m.def("gather_ids_line_int3double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int, double, dynamic, 3D - // ============================================================================ - - // Point - intdyndouble3d - m.def("gather_ids_point_intdyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - intdyndouble3d - m.def("gather_ids_segment_intdyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - intdyndouble3d - m.def("gather_ids_polygon_intdyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - intdyndouble3d - m.def("gather_ids_ray_intdyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - intdyndouble3d - m.def("gather_ids_line_intdyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, double, ngon=3, 3D - // ============================================================================ - - // Point - int643double3d - m.def("gather_ids_point_int643double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int643double3d - m.def("gather_ids_segment_int643double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int643double3d - m.def("gather_ids_polygon_int643double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int643double3d - m.def("gather_ids_ray_int643double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int643double3d - m.def("gather_ids_line_int643double3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, double, dynamic, 3D - // ============================================================================ - - // Point - int64dyndouble3d - m.def("gather_ids_point_int64dyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int64dyndouble3d - m.def("gather_ids_segment_int64dyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int64dyndouble3d - m.def("gather_ids_polygon_int64dyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int64dyndouble3d - m.def("gather_ids_ray_int64dyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int64dyndouble3d - m.def("gather_ids_line_int64dyndouble3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_gather_ids_primitive_float2d.cpp b/python/src/spatial/mesh_gather_ids_primitive_float2d.cpp deleted file mode 100644 index 46e3c68..0000000 --- a/python/src/spatial/mesh_gather_ids_primitive_float2d.cpp +++ /dev/null @@ -1,741 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_gather_ids_primitive_float2d(nanobind::module_ &m) -> void { - - namespace nb = nanobind; - - // ============================================================================ - // gather_ids - int, float, ngon=3, 2D - // ============================================================================ - - // Point - int3float2d - m.def("gather_ids_point_int3float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int3float2d - m.def("gather_ids_segment_int3float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int3float2d - m.def("gather_ids_polygon_int3float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int3float2d - m.def("gather_ids_ray_int3float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int3float2d - m.def("gather_ids_line_int3float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int, float, dynamic, 2D - // ============================================================================ - - // Point - intdynfloat2d - m.def("gather_ids_point_intdynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - intdynfloat2d - m.def("gather_ids_segment_intdynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - intdynfloat2d - m.def("gather_ids_polygon_intdynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - intdynfloat2d - m.def("gather_ids_ray_intdynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - intdynfloat2d - m.def("gather_ids_line_intdynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, float, ngon=3, 2D - // ============================================================================ - - // Point - int643float2d - m.def("gather_ids_point_int643float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int643float2d - m.def("gather_ids_segment_int643float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int643float2d - m.def("gather_ids_polygon_int643float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int643float2d - m.def("gather_ids_ray_int643float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int643float2d - m.def("gather_ids_line_int643float2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, float, dynamic, 2D - // ============================================================================ - - // Point - int64dynfloat2d - m.def("gather_ids_point_int64dynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int64dynfloat2d - m.def("gather_ids_segment_int64dynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int64dynfloat2d - m.def("gather_ids_polygon_int64dynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int64dynfloat2d - m.def("gather_ids_ray_int64dynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int64dynfloat2d - m.def("gather_ids_line_int64dynfloat2d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_gather_ids_primitive_float3d.cpp b/python/src/spatial/mesh_gather_ids_primitive_float3d.cpp deleted file mode 100644 index 69b671a..0000000 --- a/python/src/spatial/mesh_gather_ids_primitive_float3d.cpp +++ /dev/null @@ -1,741 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_gather_ids_primitive_float3d(nanobind::module_ &m) -> void { - - namespace nb = nanobind; - - // ============================================================================ - // gather_ids - int, float, ngon=3, 3D - // ============================================================================ - - // Point - int3float3d - m.def("gather_ids_point_int3float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int3float3d - m.def("gather_ids_segment_int3float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int3float3d - m.def("gather_ids_polygon_int3float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int3float3d - m.def("gather_ids_ray_int3float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int3float3d - m.def("gather_ids_line_int3float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int, float, dynamic, 3D - // ============================================================================ - - // Point - intdynfloat3d - m.def("gather_ids_point_intdynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - intdynfloat3d - m.def("gather_ids_segment_intdynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - intdynfloat3d - m.def("gather_ids_polygon_intdynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - intdynfloat3d - m.def("gather_ids_ray_intdynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - intdynfloat3d - m.def("gather_ids_line_intdynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, float, ngon=3, 3D - // ============================================================================ - - // Point - int643float3d - m.def("gather_ids_point_int643float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int643float3d - m.def("gather_ids_segment_int643float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int643float3d - m.def("gather_ids_polygon_int643float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int643float3d - m.def("gather_ids_ray_int643float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int643float3d - m.def("gather_ids_line_int643float3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - int64, float, dynamic, 3D - // ============================================================================ - - // Point - int64dynfloat3d - m.def("gather_ids_point_int64dynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Segment - int64dynfloat3d - m.def("gather_ids_segment_int64dynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Polygon - int64dynfloat3d - m.def("gather_ids_polygon_int64dynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Ray - int64dynfloat3d - m.def("gather_ids_ray_int64dynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // Line - int64dynfloat3d - m.def("gather_ids_line_int64dynfloat3d", - [](mesh_wrapper &mesh, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(mesh, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("mesh"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_intersects_primitive.cpp b/python/src/spatial/mesh_intersects_primitive.cpp deleted file mode 100644 index 6298570..0000000 --- a/python/src/spatial/mesh_intersects_primitive.cpp +++ /dev/null @@ -1,859 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_intersects_primitive(nanobind::module_ &m) -> void { - - // ============================================================================ - // Mesh intersects primitives - // Index types: int, int64 - // Real types: float, double - // Ngon: 3 (triangles), dynamic - // Dims: 2D, 3D - // Total: 2 × 2 × 2 × 2 = 16 mesh types - // Primitives: Point, Segment, Polygon, Ray, Line = 5 primitives - // Total: 16 × 5 = 80 functions - // ============================================================================ - - // int32, float, triangle, 2D - m.def("intersects_mesh_point_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, float>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, float>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int32, float, triangle, 3D - m.def("intersects_mesh_point_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, float>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, float>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int32, float, dynamic, 2D - m.def("intersects_mesh_point_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, float>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, float>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int32, float, dynamic, 3D - m.def("intersects_mesh_point_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, float>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, float>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int32, double, triangle, 2D - m.def("intersects_mesh_point_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, double>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, double>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int32, double, triangle, 3D - m.def("intersects_mesh_point_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, double>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, double>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int32, double, dynamic, 2D - m.def("intersects_mesh_point_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, double>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, double>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int32, double, dynamic, 3D - m.def("intersects_mesh_point_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, double>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, double>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int64, float, triangle, 2D - m.def("intersects_mesh_point_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, float>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, float>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int64, float, triangle, 3D - m.def("intersects_mesh_point_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, float>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, float>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int64, float, dynamic, 2D - m.def("intersects_mesh_point_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, float>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, float>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int64, float, dynamic, 3D - m.def("intersects_mesh_point_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, float>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, float>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int64, double, triangle, 2D - m.def("intersects_mesh_point_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, double>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, double>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int64, double, triangle, 3D - m.def("intersects_mesh_point_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, double>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, double>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int64, double, dynamic, 2D - m.def("intersects_mesh_point_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, double>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, double>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // int64, double, dynamic, 3D - m.def("intersects_mesh_point_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, double>(pt_data); - return form_intersects_primitive(mesh, pt); - }, - nanobind::arg("mesh"), nanobind::arg("point")); - - m.def("intersects_mesh_segment_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - return form_intersects_primitive(mesh, seg); - }, - nanobind::arg("mesh"), nanobind::arg("segment")); - - m.def("intersects_mesh_polygon_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - return form_intersects_primitive(mesh, poly); - }, - nanobind::arg("mesh"), nanobind::arg("polygon")); - - m.def("intersects_mesh_ray_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - return form_intersects_primitive(mesh, ray); - }, - nanobind::arg("mesh"), nanobind::arg("ray")); - - m.def("intersects_mesh_line_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, double>(line_data); - return form_intersects_primitive(mesh, line); - }, - nanobind::arg("mesh"), nanobind::arg("line")); - - // ==== Plane (3D only) ==== - // int32, float, triangle, 3D - m.def("intersects_mesh_plane_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - return form_intersects_primitive(mesh, plane); - }, - nanobind::arg("mesh"), nanobind::arg("plane")); - - // int32, float, dynamic, 3D - m.def("intersects_mesh_plane_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - return form_intersects_primitive(mesh, plane); - }, - nanobind::arg("mesh"), nanobind::arg("plane")); - - // int32, double, triangle, 3D - m.def("intersects_mesh_plane_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - return form_intersects_primitive(mesh, plane); - }, - nanobind::arg("mesh"), nanobind::arg("plane")); - - // int32, double, dynamic, 3D - m.def("intersects_mesh_plane_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - return form_intersects_primitive(mesh, plane); - }, - nanobind::arg("mesh"), nanobind::arg("plane")); - - // int64, float, triangle, 3D - m.def("intersects_mesh_plane_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - return form_intersects_primitive(mesh, plane); - }, - nanobind::arg("mesh"), nanobind::arg("plane")); - - // int64, float, dynamic, 3D - m.def("intersects_mesh_plane_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - return form_intersects_primitive(mesh, plane); - }, - nanobind::arg("mesh"), nanobind::arg("plane")); - - // int64, double, triangle, 3D - m.def("intersects_mesh_plane_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - return form_intersects_primitive(mesh, plane); - }, - nanobind::arg("mesh"), nanobind::arg("plane")); - - // int64, double, dynamic, 3D - m.def("intersects_mesh_plane_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - return form_intersects_primitive(mesh, plane); - }, - nanobind::arg("mesh"), nanobind::arg("plane")); -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search.cpp b/python/src/spatial/mesh_neighbor_search.cpp deleted file mode 100644 index e1bdfd8..0000000 --- a/python/src/spatial/mesh_neighbor_search.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ - -#include - -namespace tf::py { - -// Forward declarations for mesh neighbor search bindings split across multiple -// files -auto register_mesh_neighbor_search_int3float2d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_int3float3d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_intdynfloat2d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_intdynfloat3d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_int3double2d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_int3double3d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_intdyndouble2d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_intdyndouble3d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_int643float2d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_int643float3d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_int64dynfloat2d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_int64dynfloat3d(nanobind::module_ &m) -> void; -auto register_mesh_neighbor_search_int643double2d(nanobind::module_ &m) - -> void; -auto register_mesh_neighbor_search_int643double3d(nanobind::module_ &m) - -> void; -auto register_mesh_neighbor_search_int64dyndouble2d(nanobind::module_ &m) - -> void; -auto register_mesh_neighbor_search_int64dyndouble3d(nanobind::module_ &m) - -> void; - -auto register_mesh_neighbor_search(nanobind::module_ &m) -> void { - // Register all mesh neighbor search bindings - // Split across multiple files for parallel compilation - register_mesh_neighbor_search_int3float2d(m); - register_mesh_neighbor_search_int3float3d(m); - register_mesh_neighbor_search_intdynfloat2d(m); - register_mesh_neighbor_search_intdynfloat3d(m); - register_mesh_neighbor_search_int3double2d(m); - register_mesh_neighbor_search_int3double3d(m); - register_mesh_neighbor_search_intdyndouble2d(m); - register_mesh_neighbor_search_intdyndouble3d(m); - register_mesh_neighbor_search_int643float2d(m); - register_mesh_neighbor_search_int643float3d(m); - register_mesh_neighbor_search_int64dynfloat2d(m); - register_mesh_neighbor_search_int64dynfloat3d(m); - register_mesh_neighbor_search_int643double2d(m); - register_mesh_neighbor_search_int643double3d(m); - register_mesh_neighbor_search_int64dyndouble2d(m); - register_mesh_neighbor_search_int64dyndouble3d(m); -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_int64double32d.cpp b/python/src/spatial/mesh_neighbor_search_int64double32d.cpp deleted file mode 100644 index 5ee29d0..0000000 --- a/python/src/spatial/mesh_neighbor_search_int64double32d.cpp +++ /dev/null @@ -1,168 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int643double2d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def("neighbor_search_mesh_polygon_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - - m.def("neighbor_search_mesh_knn_point_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int643double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_int64double33d.cpp b/python/src/spatial/mesh_neighbor_search_int64double33d.cpp deleted file mode 100644 index 58d1d83..0000000 --- a/python/src/spatial/mesh_neighbor_search_int64double33d.cpp +++ /dev/null @@ -1,195 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int643double3d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def("neighbor_search_mesh_polygon_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_mesh_plane_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def("neighbor_search_mesh_knn_point_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_plane_int643double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_int64doubledyn2d.cpp b/python/src/spatial/mesh_neighbor_search_int64doubledyn2d.cpp deleted file mode 100644 index f0669e6..0000000 --- a/python/src/spatial/mesh_neighbor_search_int64doubledyn2d.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int64dyndouble2d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - - m.def("neighbor_search_mesh_knn_point_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int64dyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_int64doubledyn3d.cpp b/python/src/spatial/mesh_neighbor_search_int64doubledyn3d.cpp deleted file mode 100644 index 1f6b030..0000000 --- a/python/src/spatial/mesh_neighbor_search_int64doubledyn3d.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int64dyndouble3d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_mesh_plane_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def("neighbor_search_mesh_knn_point_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_plane_int64dyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_int64float32d.cpp b/python/src/spatial/mesh_neighbor_search_int64float32d.cpp deleted file mode 100644 index f21f874..0000000 --- a/python/src/spatial/mesh_neighbor_search_int64float32d.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int643float2d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - - m.def("neighbor_search_mesh_knn_point_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int643float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_int64float33d.cpp b/python/src/spatial/mesh_neighbor_search_int64float33d.cpp deleted file mode 100644 index ef2ca68..0000000 --- a/python/src/spatial/mesh_neighbor_search_int64float33d.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int643float3d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_mesh_plane_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def("neighbor_search_mesh_knn_point_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_plane_int643float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_int64floatdyn2d.cpp b/python/src/spatial/mesh_neighbor_search_int64floatdyn2d.cpp deleted file mode 100644 index 4d29261..0000000 --- a/python/src/spatial/mesh_neighbor_search_int64floatdyn2d.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int64dynfloat2d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - - m.def("neighbor_search_mesh_knn_point_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int64dynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_int64floatdyn3d.cpp b/python/src/spatial/mesh_neighbor_search_int64floatdyn3d.cpp deleted file mode 100644 index 115dac0..0000000 --- a/python/src/spatial/mesh_neighbor_search_int64floatdyn3d.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int64dynfloat3d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_mesh_plane_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def("neighbor_search_mesh_knn_point_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_plane_int64dynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_intdouble32d.cpp b/python/src/spatial/mesh_neighbor_search_intdouble32d.cpp deleted file mode 100644 index 9347fbd..0000000 --- a/python/src/spatial/mesh_neighbor_search_intdouble32d.cpp +++ /dev/null @@ -1,168 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int3double2d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def("neighbor_search_mesh_polygon_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - - m.def("neighbor_search_mesh_knn_point_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int3double2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_intdouble33d.cpp b/python/src/spatial/mesh_neighbor_search_intdouble33d.cpp deleted file mode 100644 index 71a6ad6..0000000 --- a/python/src/spatial/mesh_neighbor_search_intdouble33d.cpp +++ /dev/null @@ -1,195 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int3double3d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def("neighbor_search_mesh_polygon_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_mesh_plane_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def("neighbor_search_mesh_knn_point_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_plane_int3double3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_intdoubledyn2d.cpp b/python/src/spatial/mesh_neighbor_search_intdoubledyn2d.cpp deleted file mode 100644 index 5983c3f..0000000 --- a/python/src/spatial/mesh_neighbor_search_intdoubledyn2d.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_intdyndouble2d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - - m.def("neighbor_search_mesh_knn_point_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_intdyndouble2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_intdoubledyn3d.cpp b/python/src/spatial/mesh_neighbor_search_intdoubledyn3d.cpp deleted file mode 100644 index 7c18fcf..0000000 --- a/python/src/spatial/mesh_neighbor_search_intdoubledyn3d.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_intdyndouble3d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_mesh_plane_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, double>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def("neighbor_search_mesh_knn_point_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_plane_intdyndouble3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, double>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_intfloat32d.cpp b/python/src/spatial/mesh_neighbor_search_intfloat32d.cpp deleted file mode 100644 index ca7f7a7..0000000 --- a/python/src/spatial/mesh_neighbor_search_intfloat32d.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int3float2d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - - m.def("neighbor_search_mesh_knn_point_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int3float2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_intfloat33d.cpp b/python/src/spatial/mesh_neighbor_search_intfloat33d.cpp deleted file mode 100644 index 5ef7b87..0000000 --- a/python/src/spatial/mesh_neighbor_search_intfloat33d.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_int3float3d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_mesh_plane_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def("neighbor_search_mesh_knn_point_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_plane_int3float3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_intfloatdyn2d.cpp b/python/src/spatial/mesh_neighbor_search_intfloatdyn2d.cpp deleted file mode 100644 index 4138911..0000000 --- a/python/src/spatial/mesh_neighbor_search_intfloatdyn2d.cpp +++ /dev/null @@ -1,167 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_intdynfloat2d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - - m.def("neighbor_search_mesh_knn_point_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_intdynfloat2d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<2, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_neighbor_search_intfloatdyn3d.cpp b/python/src/spatial/mesh_neighbor_search_intfloatdyn3d.cpp deleted file mode 100644 index 70184ff..0000000 --- a/python/src/spatial/mesh_neighbor_search_intfloatdyn3d.cpp +++ /dev/null @@ -1,194 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_neighbor_search_intdynfloat3d(nanobind::module_ &m) -> void { - - // Point queries - m.def("neighbor_search_mesh_point_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - m.def( - "neighbor_search_mesh_segment_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - m.def( - "neighbor_search_mesh_polygon_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - m.def( - "neighbor_search_mesh_ray_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - m.def( - "neighbor_search_mesh_line_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - m.def( - "neighbor_search_mesh_plane_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, float>(query), radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def("neighbor_search_mesh_knn_point_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_point_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_segment_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_segment_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_polygon_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_polygon_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_ray_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_ray_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_line_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_line_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - m.def( - "neighbor_search_mesh_knn_plane_intdynfloat3d", - [](mesh_wrapper &mesh, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - mesh, make_plane_from_array<3, float>(query), k, radius); - }, - nanobind::arg("mesh"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/mesh_ray_cast.cpp b/python/src/spatial/mesh_ray_cast.cpp deleted file mode 100644 index 1d9a0bd..0000000 --- a/python/src/spatial/mesh_ray_cast.cpp +++ /dev/null @@ -1,340 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_mesh_ray_cast(nanobind::module_ &m) -> void { - - // ============================================================================ - // Ray cast on meshes - all type combinations - // Index types: int, int64 - // Real types: float, double - // Ngon: 3 (triangles), dynamic (variable-size) - // Dims: 2D, 3D - // Total: 16 combinations - // ============================================================================ - - // int32, float, triangle, 2D - m.def( - "ray_cast_mesh_int3float2d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, float, triangle, 3D - m.def( - "ray_cast_mesh_int3float3d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, float, dynamic, 2D - m.def( - "ray_cast_mesh_intdynfloat2d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, float, dynamic, 3D - m.def( - "ray_cast_mesh_intdynfloat3d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, double, triangle, 2D - m.def( - "ray_cast_mesh_int3double2d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, double, triangle, 3D - m.def( - "ray_cast_mesh_int3double3d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, double, dynamic, 2D - m.def( - "ray_cast_mesh_intdyndouble2d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int32, double, dynamic, 3D - m.def( - "ray_cast_mesh_intdyndouble3d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, float, triangle, 2D - m.def( - "ray_cast_mesh_int643float2d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, float, triangle, 3D - m.def( - "ray_cast_mesh_int643float3d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, float, dynamic, 2D - m.def( - "ray_cast_mesh_int64dynfloat2d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, float, dynamic, 3D - m.def( - "ray_cast_mesh_int64dynfloat3d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, double, triangle, 2D - m.def( - "ray_cast_mesh_int643double2d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, double, triangle, 3D - m.def( - "ray_cast_mesh_int643double3d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, double, dynamic, 2D - m.def( - "ray_cast_mesh_int64dyndouble2d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); - - // int64, double, dynamic, 3D - m.def( - "ray_cast_mesh_int64dyndouble3d", - [](nanobind::ndarray> - ray_data, - mesh_wrapper &mesh, - std::optional> config) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto result = ray_cast(ray, mesh, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("mesh"), - nanobind::arg("config").none() = nanobind::none()); -} - -} // namespace tf::py diff --git a/python/src/spatial/point_cloud_fp.cpp b/python/src/spatial/point_cloud_fp.cpp new file mode 100644 index 0000000..1ef6b8c --- /dev/null +++ b/python/src/spatial/point_cloud_fp.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include + +namespace tf::py { + +auto register_point_cloud_fp_float2d(nanobind::module_ &m) -> void; +auto register_point_cloud_fp_float3d(nanobind::module_ &m) -> void; +auto register_point_cloud_fp_double2d(nanobind::module_ &m) -> void; +auto register_point_cloud_fp_double3d(nanobind::module_ &m) -> void; + +auto register_point_cloud_fp(nanobind::module_ &m) -> void { + register_point_cloud_fp_float2d(m); + register_point_cloud_fp_float3d(m); + register_point_cloud_fp_double2d(m); + register_point_cloud_fp_double3d(m); +} + +} // namespace tf::py diff --git a/python/src/spatial/point_cloud_fp_double2d.cpp b/python/src/spatial/point_cloud_fp_double2d.cpp new file mode 100644 index 0000000..623e3e4 --- /dev/null +++ b/python/src/spatial/point_cloud_fp_double2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_point_cloud_fp_double2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, double>( + m, "point_cloud", "double2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/point_cloud_fp_double3d.cpp b/python/src/spatial/point_cloud_fp_double3d.cpp new file mode 100644 index 0000000..f9aec29 --- /dev/null +++ b/python/src/spatial/point_cloud_fp_double3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_point_cloud_fp_double3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, double>( + m, "point_cloud", "double3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/point_cloud_fp_float2d.cpp b/python/src/spatial/point_cloud_fp_float2d.cpp new file mode 100644 index 0000000..f68ac1d --- /dev/null +++ b/python/src/spatial/point_cloud_fp_float2d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_point_cloud_fp_float2d(nanobind::module_ &m) -> void { + register_form_prim_ops, 2, float>( + m, "point_cloud", "float2d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/point_cloud_fp_float3d.cpp b/python/src/spatial/point_cloud_fp_float3d.cpp new file mode 100644 index 0000000..e9711e8 --- /dev/null +++ b/python/src/spatial/point_cloud_fp_float3d.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/spatial/form_prim_dispatch.hpp" +#include + +namespace tf::py { + +auto register_point_cloud_fp_float3d(nanobind::module_ &m) -> void { + register_form_prim_ops, 3, float>( + m, "point_cloud", "float3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/point_cloud_gather_ids_primitive.cpp b/python/src/spatial/point_cloud_gather_ids_primitive.cpp deleted file mode 100644 index 16799aa..0000000 --- a/python/src/spatial/point_cloud_gather_ids_primitive.cpp +++ /dev/null @@ -1,745 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_point_cloud_gather_ids_primitive(nanobind::module_ &m) -> void { - - namespace nb = nanobind; - - // ============================================================================ - // gather_ids - Point queries - // ============================================================================ - - // 2D float - m.def("gather_ids_point_float2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 2D double - m.def("gather_ids_point_double2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D float - m.def("gather_ids_point_float3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D double - m.def("gather_ids_point_double3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_point_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - Segment queries - // ============================================================================ - - // 2D float - m.def("gather_ids_segment_float2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 2D double - m.def("gather_ids_segment_double2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D float - m.def("gather_ids_segment_float3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D double - m.def("gather_ids_segment_double3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_segment_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - Polygon queries - // ============================================================================ - - // 2D float - m.def("gather_ids_polygon_float2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 2D double - m.def("gather_ids_polygon_double2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D float - m.def("gather_ids_polygon_float3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D double - m.def("gather_ids_polygon_double3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_polygon_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb](const auto &aabb) { - return tf::intersects(aabb, query_aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto query_aabb = tf::aabb_from(query); - auto aabb_pred = [query_aabb, threshold2](const auto &aabb) { - return tf::distance2(aabb, query_aabb) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - Ray queries - // ============================================================================ - - // 2D float - m.def("gather_ids_ray_float2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 2D double - m.def("gather_ids_ray_double2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D float - m.def("gather_ids_ray_float3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D double - m.def("gather_ids_ray_double3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_ray_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // ============================================================================ - // gather_ids - Line queries - // ============================================================================ - - // 2D float - m.def("gather_ids_line_float2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 2D double - m.def("gather_ids_line_double2d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<2, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D float - m.def("gather_ids_line_float3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, float>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - float threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - - // 3D double - m.def("gather_ids_line_double3d", - [](point_cloud_wrapper &cloud, - nb::ndarray> query_array, - const std::string &predicate_type, std::optional threshold) { - auto query = make_line_from_array<3, double>(query_array); - - if (predicate_type == "intersects") { - auto aabb_pred = [&query](const auto &aabb) { - return tf::intersects(query, aabb); - }; - auto prim_pred = [&query](const auto &prim) { - return tf::intersects(prim, query); - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else if (predicate_type == "within_distance") { - if (!threshold) - throw std::runtime_error("threshold required for within_distance"); - double threshold2 = (*threshold) * (*threshold); - auto aabb_pred = [&query, threshold2](const auto &aabb) { - return tf::distance2( - tf::make_sphere(aabb.center(), aabb.diagonal().length() / 2), query) <= threshold2; - }; - auto prim_pred = [&query, threshold2](const auto &prim) { - return tf::distance2(prim, query) <= threshold2; - }; - return gather_ids(cloud, aabb_pred, prim_pred); - } else { - throw std::runtime_error("Unknown predicate: " + predicate_type); - } - }, - nb::arg("cloud"), nb::arg("query"), nb::arg("predicate_type"), - nb::arg("threshold").none() = nb::none()); - -} - -} // namespace tf::py diff --git a/python/src/spatial/point_cloud_intersects_primitive.cpp b/python/src/spatial/point_cloud_intersects_primitive.cpp deleted file mode 100644 index b9736a6..0000000 --- a/python/src/spatial/point_cloud_intersects_primitive.cpp +++ /dev/null @@ -1,239 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include - -namespace tf::py { - -auto register_point_cloud_intersects_primitive(nanobind::module_ &m) -> void { - - // ============================================================================ - // PointCloud intersects primitives - // Real types: float, double - // Dims: 2D, 3D - // Total: 2 × 2 = 4 point cloud types - // Primitives: Point, Segment, Polygon, Ray, Line = 5 primitives - // Total: 4 × 5 = 20 functions - // ============================================================================ - - // float, 2D - m.def("intersects_point_cloud_point_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, float>(pt_data); - return form_intersects_primitive(cloud, pt); - }, - nanobind::arg("point_cloud"), nanobind::arg("point")); - - m.def("intersects_point_cloud_segment_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, float>(seg_data); - return form_intersects_primitive(cloud, seg); - }, - nanobind::arg("point_cloud"), nanobind::arg("segment")); - - m.def("intersects_point_cloud_polygon_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, float>(poly_data); - return form_intersects_primitive(cloud, poly); - }, - nanobind::arg("point_cloud"), nanobind::arg("polygon")); - - m.def("intersects_point_cloud_ray_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, float>(ray_data); - return form_intersects_primitive(cloud, ray); - }, - nanobind::arg("point_cloud"), nanobind::arg("ray")); - - m.def("intersects_point_cloud_line_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, float>(line_data); - return form_intersects_primitive(cloud, line); - }, - nanobind::arg("point_cloud"), nanobind::arg("line")); - - // float, 3D - m.def("intersects_point_cloud_point_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, float>(pt_data); - return form_intersects_primitive(cloud, pt); - }, - nanobind::arg("point_cloud"), nanobind::arg("point")); - - m.def("intersects_point_cloud_segment_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, float>(seg_data); - return form_intersects_primitive(cloud, seg); - }, - nanobind::arg("point_cloud"), nanobind::arg("segment")); - - m.def("intersects_point_cloud_polygon_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, float>(poly_data); - return form_intersects_primitive(cloud, poly); - }, - nanobind::arg("point_cloud"), nanobind::arg("polygon")); - - m.def("intersects_point_cloud_ray_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, float>(ray_data); - return form_intersects_primitive(cloud, ray); - }, - nanobind::arg("point_cloud"), nanobind::arg("ray")); - - m.def("intersects_point_cloud_line_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, float>(line_data); - return form_intersects_primitive(cloud, line); - }, - nanobind::arg("point_cloud"), nanobind::arg("line")); - - // double, 2D - m.def("intersects_point_cloud_point_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<2, double>(pt_data); - return form_intersects_primitive(cloud, pt); - }, - nanobind::arg("point_cloud"), nanobind::arg("point")); - - m.def("intersects_point_cloud_segment_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<2, double>(seg_data); - return form_intersects_primitive(cloud, seg); - }, - nanobind::arg("point_cloud"), nanobind::arg("segment")); - - m.def("intersects_point_cloud_polygon_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<2, double>(poly_data); - return form_intersects_primitive(cloud, poly); - }, - nanobind::arg("point_cloud"), nanobind::arg("polygon")); - - m.def("intersects_point_cloud_ray_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<2, double>(ray_data); - return form_intersects_primitive(cloud, ray); - }, - nanobind::arg("point_cloud"), nanobind::arg("ray")); - - m.def("intersects_point_cloud_line_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<2, double>(line_data); - return form_intersects_primitive(cloud, line); - }, - nanobind::arg("point_cloud"), nanobind::arg("line")); - - // double, 3D - m.def("intersects_point_cloud_point_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - pt_data) { - auto pt = make_point_from_array<3, double>(pt_data); - return form_intersects_primitive(cloud, pt); - }, - nanobind::arg("point_cloud"), nanobind::arg("point")); - - m.def("intersects_point_cloud_segment_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - seg_data) { - auto seg = make_segment_from_array<3, double>(seg_data); - return form_intersects_primitive(cloud, seg); - }, - nanobind::arg("point_cloud"), nanobind::arg("segment")); - - m.def("intersects_point_cloud_polygon_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray poly_data) { - auto poly = make_polygon_from_array<3, double>(poly_data); - return form_intersects_primitive(cloud, poly); - }, - nanobind::arg("point_cloud"), nanobind::arg("polygon")); - - m.def("intersects_point_cloud_ray_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - ray_data) { - auto ray = make_ray_from_array<3, double>(ray_data); - return form_intersects_primitive(cloud, ray); - }, - nanobind::arg("point_cloud"), nanobind::arg("ray")); - - m.def("intersects_point_cloud_line_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - line_data) { - auto line = make_line_from_array<3, double>(line_data); - return form_intersects_primitive(cloud, line); - }, - nanobind::arg("point_cloud"), nanobind::arg("line")); - - // ==== Plane (3D only) ==== - // float, 3D - m.def("intersects_point_cloud_plane_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, float>(plane_data); - return form_intersects_primitive(cloud, plane); - }, - nanobind::arg("point_cloud"), nanobind::arg("plane")); - - // double, 3D - m.def("intersects_point_cloud_plane_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - plane_data) { - auto plane = make_plane_from_array<3, double>(plane_data); - return form_intersects_primitive(cloud, plane); - }, - nanobind::arg("point_cloud"), nanobind::arg("plane")); -} - -} // namespace tf::py diff --git a/python/src/spatial/point_cloud_neighbor_search.cpp b/python/src/spatial/point_cloud_neighbor_search.cpp deleted file mode 100644 index 6c47861..0000000 --- a/python/src/spatial/point_cloud_neighbor_search.cpp +++ /dev/null @@ -1,665 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_point_cloud_neighbor_search(nanobind::module_ &m) -> void { - - // ============================================================================ - // Non-KNN neighbor search (single nearest neighbor) - // ============================================================================ - - // Point queries - 2D float - m.def("neighbor_search_point_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_point_from_array<2, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Point queries - 2D double - m.def("neighbor_search_point_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_point_from_array<2, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Point queries - 3D float - m.def("neighbor_search_point_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_point_from_array<3, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Point queries - 3D double - m.def("neighbor_search_point_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_point_from_array<3, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - 2D float - m.def( - "neighbor_search_segment_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_segment_from_array<2, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - 2D double - m.def( - "neighbor_search_segment_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_segment_from_array<2, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - 3D float - m.def( - "neighbor_search_segment_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_segment_from_array<3, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - 3D double - m.def( - "neighbor_search_segment_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_segment_from_array<3, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - 2D float - m.def( - "neighbor_search_polygon_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_polygon_from_array<2, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - 2D double - m.def("neighbor_search_polygon_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_polygon_from_array<2, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - 3D float - m.def( - "neighbor_search_polygon_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_polygon_from_array<3, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - 3D double - m.def("neighbor_search_polygon_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_polygon_from_array<3, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - 2D float - m.def( - "neighbor_search_ray_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_ray_from_array<2, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - 2D double - m.def( - "neighbor_search_ray_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_ray_from_array<2, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - 3D float - m.def( - "neighbor_search_ray_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_ray_from_array<3, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - 3D double - m.def( - "neighbor_search_ray_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_ray_from_array<3, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - 2D float - m.def( - "neighbor_search_line_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_line_from_array<2, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - 2D double - m.def( - "neighbor_search_line_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_line_from_array<2, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - 3D float - m.def( - "neighbor_search_line_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_line_from_array<3, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - 3D double - m.def( - "neighbor_search_line_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_line_from_array<3, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - 3D float - m.def( - "neighbor_search_plane_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_plane_from_array<3, float>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - 3D double - m.def( - "neighbor_search_plane_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - std::optional radius) { - return neighbor_search( - cloud, make_plane_from_array<3, double>(query), radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("radius").none() = nanobind::none()); - - // ============================================================================ - // KNN neighbor search (k nearest neighbors) - // ============================================================================ - - // Point queries - 2D float - m.def("neighbor_search_knn_point_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_point_from_array<2, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Point queries - 2D double - m.def("neighbor_search_knn_point_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_point_from_array<2, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Point queries - 3D float - m.def("neighbor_search_knn_point_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_point_from_array<3, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Point queries - 3D double - m.def("neighbor_search_knn_point_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_point_from_array<3, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - 2D float - m.def( - "neighbor_search_knn_segment_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_segment_from_array<2, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - 2D double - m.def( - "neighbor_search_knn_segment_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_segment_from_array<2, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - 3D float - m.def( - "neighbor_search_knn_segment_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_segment_from_array<3, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Segment queries - 3D double - m.def( - "neighbor_search_knn_segment_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_segment_from_array<3, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - 2D float - m.def( - "neighbor_search_knn_polygon_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_polygon_from_array<2, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - 2D double - m.def("neighbor_search_knn_polygon_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_polygon_from_array<2, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - 3D float - m.def( - "neighbor_search_knn_polygon_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_polygon_from_array<3, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Polygon queries - 3D double - m.def("neighbor_search_knn_polygon_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_polygon_from_array<3, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - 2D float - m.def( - "neighbor_search_knn_ray_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_ray_from_array<2, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - 2D double - m.def( - "neighbor_search_knn_ray_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_ray_from_array<2, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - 3D float - m.def( - "neighbor_search_knn_ray_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_ray_from_array<3, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Ray queries - 3D double - m.def( - "neighbor_search_knn_ray_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_ray_from_array<3, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - 2D float - m.def( - "neighbor_search_knn_line_float2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_line_from_array<2, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - 2D double - m.def( - "neighbor_search_knn_line_double2d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_line_from_array<2, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - 3D float - m.def( - "neighbor_search_knn_line_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_line_from_array<3, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Line queries - 3D double - m.def( - "neighbor_search_knn_line_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_line_from_array<3, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - 3D float - m.def( - "neighbor_search_knn_plane_float3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_plane_from_array<3, float>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); - - // Plane queries - 3D double - m.def( - "neighbor_search_knn_plane_double3d", - [](point_cloud_wrapper &cloud, - nanobind::ndarray> - query, - int k, std::optional radius) { - return neighbor_search( - cloud, make_plane_from_array<3, double>(query), k, radius); - }, - nanobind::arg("cloud"), - nanobind::arg("query"), - nanobind::arg("k"), - nanobind::arg("radius").none() = nanobind::none()); -} - -} // namespace tf::py diff --git a/python/src/spatial/point_cloud_ray_cast.cpp b/python/src/spatial/point_cloud_ray_cast.cpp deleted file mode 100644 index b814d60..0000000 --- a/python/src/spatial/point_cloud_ray_cast.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* -* Copyright (c) 2025 XLAB -* All rights reserved. -* -* This file is part of trueform (trueform.polydera.com) -* -* Licensed for noncommercial use under the PolyForm Noncommercial -* License 1.0.0. -* Commercial licensing available via info@polydera.com. -* -* Author: Žiga Sajovic -*/ -#include -#include -#include -#include -#include -#include -#include -#include - -namespace tf::py { - -auto register_point_cloud_ray_cast(nanobind::module_ &m) -> void { - - // ============================================================================ - // Ray cast on point clouds - // ============================================================================ - - // 2D float - m.def( - "ray_cast_point_cloud_float2d", - [](nanobind::ndarray> - ray_data, - point_cloud_wrapper &cloud, - std::optional> config) { - auto ray = make_ray_from_array<2, float>(ray_data); - auto result = ray_cast(ray, cloud, config); - - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("cloud"), - nanobind::arg("config").none() = nanobind::none()); - - // 2D double - m.def( - "ray_cast_point_cloud_double2d", - [](nanobind::ndarray> - ray_data, - point_cloud_wrapper &cloud, - std::optional> config) { - auto ray = make_ray_from_array<2, double>(ray_data); - auto result = ray_cast(ray, cloud, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::none(); - } - }, - nanobind::arg("ray"), nanobind::arg("cloud"), - nanobind::arg("config").none() = nanobind::none()); - - // 3D float - m.def( - "ray_cast_point_cloud_float3d", - [](nanobind::ndarray> - ray_data, - point_cloud_wrapper &cloud, - std::optional> config) { - auto ray = make_ray_from_array<3, float>(ray_data); - auto result = ray_cast(ray, cloud, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::object(nanobind::none()); - } - }, - nanobind::arg("ray"), nanobind::arg("cloud"), - nanobind::arg("config").none() = nanobind::none()); - - // 3D double - m.def( - "ray_cast_point_cloud_double3d", - [](nanobind::ndarray> - ray_data, - point_cloud_wrapper &cloud, - std::optional> config) { - auto ray = make_ray_from_array<3, double>(ray_data); - auto result = ray_cast(ray, cloud, config); - if (result) { - return nanobind::cast( - nanobind::make_tuple(result->first, result->second)); - } else { - return nanobind::object(nanobind::none()); - } - }, - nanobind::arg("ray"), nanobind::arg("cloud"), - nanobind::arg("config").none() = nanobind::none()); -} - -} // namespace tf::py diff --git a/python/src/spatial/prim_geometry_ops.cpp b/python/src/spatial/prim_geometry_ops.cpp new file mode 100644 index 0000000..54ceed5 --- /dev/null +++ b/python/src/spatial/prim_geometry_ops.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/core/prim_dispatch.hpp" +#include "trueform/python/util/make_numpy_array.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tf::py { + +namespace { + +template +auto register_prim_geometry_ops_impl(nanobind::module_ &m, const char *suffix) + -> void { + namespace nb = nanobind; + using PW = primitive_wrapper; + + auto name = [suffix](const char *base) -> std::string { + return std::string(base) + suffix; + }; + + // ==== area ==== + m.def( + name("area_prim_").c_str(), + [](const PW &a) -> nb::object { + auto type = a.type(); + if (type != prim_type::triangle && type != prim_type::polygon) + throw nanobind::type_error( + "area() only supports Triangle and Polygon primitives"); + + if (!a.is_batch()) { + RealT result; + if (type == prim_type::triangle) { + result = static_cast(tf::area(make_triangle(a))); + } else { + result = static_cast(tf::area(make_polygon(a))); + } + return nb::cast(result); + } + + // Batch + int n = a.count(); + tf::buffer out; + out.allocate(n); + auto *dst = out.data(); + + auto compute = [&](int i) { + auto ptr = a.element_ptr(i); + if (type == prim_type::triangle) { + auto pts = + tf::make_points(tf::make_range(ptr, 3 * Dims)); + dst[i] = static_cast(tf::area(tf::make_polygon<3>(pts))); + } else { + auto nv = a.poly_verts(); + auto pts = tf::make_points(tf::make_range( + ptr, static_cast(nv) * Dims)); + dst[i] = static_cast(tf::area(tf::make_polygon(pts))); + } + }; + + if (n >= 5000) + tf::parallel_for_each(tf::make_sequence_range(n), compute); + else + for (int i = 0; i < n; ++i) + compute(i); + + return nb::cast(make_numpy_array>( + std::move(out), {static_cast(n)})); + }, + nb::arg("a")); + + // ==== normals (3D only) ==== + if constexpr (Dims == 3) { + m.def( + name("normals_prim_").c_str(), + [](const PW &a) -> nb::object { + auto type = a.type(); + if (type != prim_type::triangle && type != prim_type::polygon) + throw nanobind::type_error( + "normals() only supports 3D Triangle and Polygon primitives"); + + if (!a.is_batch()) { + // Single: extract first 3 vertices for normal + if (type == prim_type::triangle) { + auto poly = make_triangle(a); + auto n = tf::make_normal(poly[0], poly[1], poly[2]); + auto *data = new RealT[3]; + for (std::size_t j = 0; j < 3; ++j) + data[j] = n[j]; + return nb::cast(make_numpy_array>(data, {3})); + } else { + auto poly = make_polygon(a); + auto n = tf::make_normal(poly[0], poly[1], poly[2]); + auto *data = new RealT[3]; + for (std::size_t j = 0; j < 3; ++j) + data[j] = n[j]; + return nb::cast(make_numpy_array>(data, {3})); + } + } + + // Batch: use compute_normals on polygons range + if (type == prim_type::triangle) { + auto polys = make_triangles(a); + auto normals_buf = tf::compute_normals(polys); + return nb::cast(make_numpy_array(std::move(normals_buf))); + } else { + auto polys = make_polygons(a); + auto normals_buf = tf::compute_normals(polys); + return nb::cast(make_numpy_array(std::move(normals_buf))); + } + }, + nb::arg("a")); + } +} + +} // anonymous namespace + +auto register_prim_geometry_ops(nanobind::module_ &m) -> void { + register_prim_geometry_ops_impl<2, float>(m, "float2d"); + register_prim_geometry_ops_impl<3, float>(m, "float3d"); + register_prim_geometry_ops_impl<2, double>(m, "double2d"); + register_prim_geometry_ops_impl<3, double>(m, "double3d"); +} + +} // namespace tf::py diff --git a/python/src/spatial/prim_prim_ops.cpp b/python/src/spatial/prim_prim_ops.cpp new file mode 100644 index 0000000..e8cbda8 --- /dev/null +++ b/python/src/spatial/prim_prim_ops.cpp @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2025 XLAB + * All rights reserved. + * + * This file is part of trueform (trueform.polydera.com) + * + * Licensed for noncommercial use under the PolyForm Noncommercial + * License 1.0.0. + * Commercial licensing available via info@polydera.com. + * + * Author: Ziga Sajovic + */ + +#include "trueform/python/core/prim_dispatch.hpp" +#include "trueform/python/util/make_numpy_array.hpp" +#include "trueform/python/util/ray_config_helper.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tf::py { + +namespace { + +// ============================================================================ +// Name helper +// ============================================================================ + +auto fn_name(const char *base, const char *suffix) -> std::string { + return std::string(base) + suffix; +} + +// ============================================================================ +// Batch size helper +// ============================================================================ + +template +auto batch_count_pair(const primitive_wrapper &a, + const primitive_wrapper &b) -> int { + bool ba = a.is_batch(); + bool bb = b.is_batch(); + int n = ba ? a.count() : b.count(); + if (ba && bb && a.count() != b.count()) + throw std::runtime_error("batch size mismatch"); + return n; +} + +// ============================================================================ +// Registration template +// ============================================================================ + +template +auto register_prim_prim_ops_impl(nanobind::module_ &m, const char *suffix) + -> void { + namespace nb = nanobind; + using PW = primitive_wrapper; + + auto name = [suffix](const char *base) -> std::string { + return fn_name(base, suffix); + }; + + // ==== distance ==== + m.def( + name("distance_pp_").c_str(), + [](const PW &a, const PW &b) -> nb::object { + if (!a.is_batch() && !b.is_batch()) { + auto d = dispatch_pair( + [](const auto &pa, const auto &pb) -> RealT { + return static_cast(tf::distance(pa, pb)); + }, + a, b); + return nb::cast(d); + } + + int n = batch_count_pair(a, b); + tf::buffer out; + out.allocate(n); + auto *dst = out.data(); + + auto compute = [&](int i) { + dst[i] = static_cast(dispatch_pair_at( + [](const auto &pa, const auto &pb) -> RealT { + return static_cast(tf::distance(pa, pb)); + }, + a, i, b, i)); + }; + + if (n >= 5000) + tf::parallel_for_each(tf::make_sequence_range(n), compute); + else + for (int i = 0; i < n; ++i) + compute(i); + + return nb::cast(make_numpy_array>( + std::move(out), {static_cast(n)})); + }, + nb::arg("a"), nb::arg("b")); + + // ==== distance2 ==== + m.def( + name("distance2_pp_").c_str(), + [](const PW &a, const PW &b) -> nb::object { + if (!a.is_batch() && !b.is_batch()) { + auto d = dispatch_pair( + [](const auto &pa, const auto &pb) -> RealT { + return static_cast(tf::distance2(pa, pb)); + }, + a, b); + return nb::cast(d); + } + + int n = batch_count_pair(a, b); + tf::buffer out; + out.allocate(n); + auto *dst = out.data(); + + auto compute = [&](int i) { + dst[i] = static_cast(dispatch_pair_at( + [](const auto &pa, const auto &pb) -> RealT { + return static_cast(tf::distance2(pa, pb)); + }, + a, i, b, i)); + }; + + if (n >= 5000) + tf::parallel_for_each(tf::make_sequence_range(n), compute); + else + for (int i = 0; i < n; ++i) + compute(i); + + return nb::cast(make_numpy_array>( + std::move(out), {static_cast(n)})); + }, + nb::arg("a"), nb::arg("b")); + + // ==== intersects ==== + m.def( + name("intersects_pp_").c_str(), + [](const PW &a, const PW &b) -> nb::object { + if (!a.is_batch() && !b.is_batch()) { + auto hit = dispatch_pair( + [](const auto &pa, const auto &pb) -> bool { + return tf::intersects(pa, pb); + }, + a, b); + return nb::cast(hit); + } + + int n = batch_count_pair(a, b); + tf::buffer out; + out.allocate(n); + auto *dst = out.data(); + + auto compute = [&](int i) { + dst[i] = dispatch_pair_at( + [](const auto &pa, const auto &pb) -> bool { + return tf::intersects(pa, pb); + }, + a, i, b, i) + ? std::int8_t{1} + : std::int8_t{0}; + }; + + if (n >= 1000) + tf::parallel_for_each(tf::make_sequence_range(n), compute); + else + for (int i = 0; i < n; ++i) + compute(i); + + return nb::cast(make_numpy_array>( + std::move(out), {static_cast(n)})); + }, + nb::arg("a"), nb::arg("b")); + + // ==== ray_cast ==== + // config: optional pair of ndarrays (min_ts, max_ts). Python expands + // scalar tuples and None to arrays before calling. + using RayConfigArrays = std::pair< + nb::ndarray, nb::c_contig>, + nb::ndarray, nb::c_contig>>; + m.def( + name("ray_cast_pp_").c_str(), + [](const PW &ray_pw, const PW &target, + std::optional opt_config) -> nb::object { + if (!ray_pw.is_batch() && !target.is_batch()) { + // Single ray: config arrays have length 1 or absent + auto config = tf::ray_config{}; + if (opt_config) + config = {opt_config->first.data()[0], + opt_config->second.data()[0]}; + auto ray = make_ray(ray_pw); + auto result = dispatch_single( + [&](const auto &t) { return tf::ray_cast(ray, t, config); }, + target); + if (result) + return nb::cast(static_cast(result.t)); + return nb::none(); + } + + // Batch: Python always provides config arrays + const RealT *min_ts = opt_config->first.data(); + const RealT *max_ts = opt_config->second.data(); + + bool br = ray_pw.is_batch(); + bool bt = target.is_batch(); + int n = br ? ray_pw.count() : target.count(); + if (br && bt && ray_pw.count() != target.count()) + throw std::runtime_error("batch size mismatch"); + + tf::buffer ts; + ts.allocate(n); + auto *t_dst = ts.data(); + constexpr auto nan = std::numeric_limits::quiet_NaN(); + + auto compute = [&](int i) { + auto ray_ptr = ray_pw.element_ptr(i); + auto pts = + tf::make_points(tf::make_range(ray_ptr, 2 * Dims)); + auto ray = tf::make_ray_like(pts[0], pts[1].as_vector_view()); + + dispatch_at( + [&](const auto &t_view) { + auto r = tf::ray_cast( + ray, t_view, + tf::ray_config{min_ts[i], max_ts[i]}); + t_dst[i] = r ? static_cast(r.t) : nan; + }, + target, i); + }; + + if (n >= 1000) + tf::parallel_for_each(tf::make_sequence_range(n), compute); + else + for (int i = 0; i < n; ++i) + compute(i); + + return nb::cast(make_numpy_array>( + std::move(ts), {static_cast(n)})); + }, + nb::arg("ray"), nb::arg("target"), + nb::arg("config").none() = nb::none()); + + // ==== closest_point_pair ==== + m.def( + name("closest_point_pair_pp_").c_str(), + [](const PW &a, const PW &b) -> nb::object { + if (!a.is_batch() && !b.is_batch()) { + auto r = dispatch_pair( + [](const auto &pa, + const auto &pb) -> tf::metric_point_pair { + return tf::closest_metric_point_pair(pa, pb); + }, + a, b); + + auto *pt0_data = new RealT[Dims]; + auto *pt1_data = new RealT[Dims]; + for (std::size_t j = 0; j < Dims; ++j) { + pt0_data[j] = r.first[j]; + pt1_data[j] = r.second[j]; + } + auto pt0_arr = + make_numpy_array>(pt0_data, {Dims}); + auto pt1_arr = + make_numpy_array>(pt1_data, {Dims}); + return nb::make_tuple(static_cast(r.metric), pt0_arr, + pt1_arr); + } + + int n = batch_count_pair(a, b); + tf::buffer dists; + tf::buffer pts0; + tf::buffer pts1; + dists.allocate(n); + pts0.allocate(static_cast(n) * Dims); + pts1.allocate(static_cast(n) * Dims); + auto *d_dst = dists.data(); + auto *p0_dst = pts0.data(); + auto *p1_dst = pts1.data(); + + auto compute = [&](int i) { + auto r = dispatch_pair_at( + [](const auto &pa, + const auto &pb) -> tf::metric_point_pair { + return tf::closest_metric_point_pair(pa, pb); + }, + a, i, b, i); + d_dst[i] = static_cast(r.metric); + auto *p0 = p0_dst + static_cast(i) * Dims; + auto *p1 = p1_dst + static_cast(i) * Dims; + for (std::size_t j = 0; j < Dims; ++j) { + p0[j] = r.first[j]; + p1[j] = r.second[j]; + } + }; + + if (n >= 1000) + tf::parallel_for_each(tf::make_sequence_range(n), compute); + else + for (int i = 0; i < n; ++i) + compute(i); + + auto d_arr = make_numpy_array>( + std::move(dists), {static_cast(n)}); + auto p0_arr = make_numpy_array>( + std::move(pts0), {static_cast(n), Dims}); + auto p1_arr = make_numpy_array>( + std::move(pts1), {static_cast(n), Dims}); + return nb::make_tuple(d_arr, p0_arr, p1_arr); + }, + nb::arg("a"), nb::arg("b")); + + // ==== distance_field ==== + m.def( + name("distance_field_pp_").c_str(), + [](const PW &points, const PW &target) -> nb::object { + int n = points.count(); + tf::buffer out; + out.allocate(n); + auto *dst = out.data(); + + dispatch_single( + [&](const auto &t_view) { + auto compute = [&](int i) { + auto pt = + tf::make_point_view(points.element_ptr(i)); + dst[i] = static_cast(tf::distance(pt, t_view)); + }; + if (n >= 1000) + tf::parallel_for_each(tf::make_sequence_range(n), compute); + else + for (int i = 0; i < n; ++i) + compute(i); + }, + target); + + return nb::cast(make_numpy_array>( + std::move(out), {static_cast(n)})); + }, + nb::arg("points"), nb::arg("target")); +} + +} // anonymous namespace + +auto register_prim_prim_ops(nanobind::module_ &m) -> void { + register_prim_prim_ops_impl<2, float>(m, "float2d"); + register_prim_prim_ops_impl<3, float>(m, "float3d"); + register_prim_prim_ops_impl<2, double>(m, "double2d"); + register_prim_prim_ops_impl<3, double>(m, "double3d"); +} + +} // namespace tf::py diff --git a/python/src/trueform/__init__.py b/python/src/trueform/__init__.py index 195b321..f34ac59 100644 --- a/python/src/trueform/__init__.py +++ b/python/src/trueform/__init__.py @@ -14,13 +14,12 @@ # Core data structures from ._spatial import PointCloud, Mesh, EdgeMesh -from ._core import closest_metric_point_pair, closest_metric_point from ._core import OffsetBlockedArray, as_offset_blocked # Top-level functions -from .ray_cast import ray_cast -from .intersects import intersects -from .distance import distance, distance2 -from .distance_field import distance_field +from ._spatial.ray_cast import ray_cast +from ._spatial.intersects import intersects +from ._spatial.distance import distance, distance2 +from ._spatial.closest_point import closest_metric_point_pair, closest_metric_point, closest_point_pair from ._intersect import isocontours, intersection_curves, self_intersection_curves from ._cut import isobands, boolean_union, boolean_intersection, boolean_difference, embedded_self_intersection_curves, embedded_intersection_curves from ._clean import cleaned @@ -38,7 +37,7 @@ from ._io import read_stl, write_stl, read_obj, write_obj # Primitives -from ._primitives import Point, Segment, Polygon, AABB, Ray, Line, Plane +from ._primitives import Primitive, PrimitiveType, Point, Segment, Triangle, Polygon, AABB, Ray, Line, Plane __all__ = [ # Core @@ -49,11 +48,11 @@ 'as_offset_blocked', 'closest_metric_point_pair', 'closest_metric_point', + 'closest_point_pair', 'ray_cast', 'intersects', 'distance', 'distance2', - 'distance_field', 'isocontours', 'isobands', 'intersection_curves', @@ -123,6 +122,7 @@ 'read_obj', 'write_obj', # Primitives - 'Point', 'Segment', 'Polygon', 'AABB', 'Ray', 'Line', 'Plane', + 'Primitive', 'PrimitiveType', + 'Point', 'Segment', 'Triangle', 'Polygon', 'AABB', 'Ray', 'Line', 'Plane', '__version__', ] diff --git a/python/src/trueform/_core/__init__.py b/python/src/trueform/_core/__init__.py index 69c1b27..566c3ba 100644 --- a/python/src/trueform/_core/__init__.py +++ b/python/src/trueform/_core/__init__.py @@ -7,14 +7,11 @@ https://github.com/polydera/trueform """ -from .closest_metric_point_pair import closest_metric_point_pair, closest_metric_point from .offset_blocked_array import OffsetBlockedArray from .as_offset_blocked import as_offset_blocked __all__ = [ - 'closest_metric_point_pair', - 'closest_metric_point', 'OffsetBlockedArray', 'as_offset_blocked', ] diff --git a/python/src/trueform/_core/_dispatch.py b/python/src/trueform/_core/_dispatch.py deleted file mode 100644 index d2a4118..0000000 --- a/python/src/trueform/_core/_dispatch.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -Dispatch tables for tf::core operations (primitive x primitive). - -All share the same suffix pattern: {real}{dims}d - -Copyright (c) 2025 Ziga Sajovic, XLAB -Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. -Commercial licensing available via info@polydera.com. -https://github.com/polydera/trueform -""" -from .._primitives import Point, Segment, Polygon, Line, AABB, Ray, Plane - - -# ============================================================================= -# INTERSECTS: (type0, type1) -> (func_template, needs_swap) -# ============================================================================= -INTERSECTS = { - # Point combinations - (Point, Point): ("intersects_point_point_{}", False), - (Point, AABB): ("intersects_point_aabb_{}", False), - (AABB, Point): ("intersects_point_aabb_{}", True), - (Point, Line): ("intersects_point_line_{}", False), - (Line, Point): ("intersects_point_line_{}", True), - (Point, Ray): ("intersects_point_ray_{}", False), - (Ray, Point): ("intersects_point_ray_{}", True), - (Point, Segment): ("intersects_point_segment_{}", False), - (Segment, Point): ("intersects_point_segment_{}", True), - (Point, Polygon): ("intersects_point_polygon_{}", False), - (Polygon, Point): ("intersects_point_polygon_{}", True), - (Point, Plane): ("intersects_point_plane_{}", False), - (Plane, Point): ("intersects_point_plane_{}", True), - - # AABB combinations - (AABB, AABB): ("intersects_aabb_aabb_{}", False), - (AABB, Plane): ("intersects_aabb_plane_{}", False), - (Plane, AABB): ("intersects_aabb_plane_{}", True), - (Segment, AABB): ("intersects_segment_aabb_{}", False), - (AABB, Segment): ("intersects_segment_aabb_{}", True), - (Ray, AABB): ("intersects_ray_aabb_{}", False), - (AABB, Ray): ("intersects_ray_aabb_{}", True), - (Line, AABB): ("intersects_line_aabb_{}", False), - (AABB, Line): ("intersects_line_aabb_{}", True), - (Polygon, AABB): ("intersects_polygon_aabb_{}", False), - (AABB, Polygon): ("intersects_polygon_aabb_{}", True), - - # Line combinations - (Line, Line): ("intersects_line_line_{}", False), - (Line, Ray): ("intersects_line_ray_{}", False), - (Ray, Line): ("intersects_line_ray_{}", True), - (Line, Segment): ("intersects_line_segment_{}", False), - (Segment, Line): ("intersects_line_segment_{}", True), - (Line, Polygon): ("intersects_line_polygon_{}", False), - (Polygon, Line): ("intersects_line_polygon_{}", True), - (Line, Plane): ("intersects_line_plane_{}", False), - (Plane, Line): ("intersects_line_plane_{}", True), - - # Ray combinations - (Ray, Ray): ("intersects_ray_ray_{}", False), - (Ray, Segment): ("intersects_ray_segment_{}", False), - (Segment, Ray): ("intersects_ray_segment_{}", True), - (Ray, Polygon): ("intersects_ray_polygon_{}", False), - (Polygon, Ray): ("intersects_ray_polygon_{}", True), - (Ray, Plane): ("intersects_ray_plane_{}", False), - (Plane, Ray): ("intersects_ray_plane_{}", True), - - # Segment combinations - (Segment, Segment): ("intersects_segment_segment_{}", False), - (Segment, Polygon): ("intersects_segment_polygon_{}", False), - (Polygon, Segment): ("intersects_segment_polygon_{}", True), - (Segment, Plane): ("intersects_segment_plane_{}", False), - (Plane, Segment): ("intersects_segment_plane_{}", True), - - # Polygon combinations - (Polygon, Polygon): ("intersects_polygon_polygon_{}", False), - (Polygon, Plane): ("intersects_polygon_plane_{}", False), - (Plane, Polygon): ("intersects_polygon_plane_{}", True), - - # Plane combinations - (Plane, Plane): ("intersects_plane_plane_{}", False), -} - - -# ============================================================================= -# DISTANCE: (type0, type1) -> (func_base_template, needs_swap) -# func_base_template has {} for "distance" or "distance2", then {} for suffix -# ============================================================================= -DISTANCE = { - # Point combinations - (Point, Point): ("{}_point_point_{}", False), - (Point, AABB): ("{}_point_aabb_{}", False), - (AABB, Point): ("{}_point_aabb_{}", True), - (Point, Plane): ("{}_point_plane_{}", False), - (Plane, Point): ("{}_point_plane_{}", True), - (Point, Line): ("{}_point_line_{}", False), - (Line, Point): ("{}_point_line_{}", True), - (Point, Ray): ("{}_point_ray_{}", False), - (Ray, Point): ("{}_point_ray_{}", True), - (Point, Segment): ("{}_point_segment_{}", False), - (Segment, Point): ("{}_point_segment_{}", True), - (Point, Polygon): ("{}_point_polygon_{}", False), - (Polygon, Point): ("{}_point_polygon_{}", True), - - # AABB combinations - (AABB, AABB): ("{}_aabb_aabb_{}", False), - - # Line combinations - (Line, Line): ("{}_line_line_{}", False), - (Line, Ray): ("{}_line_ray_{}", False), - (Ray, Line): ("{}_line_ray_{}", True), - (Line, Segment): ("{}_line_segment_{}", False), - (Segment, Line): ("{}_line_segment_{}", True), - (Line, Polygon): ("{}_line_polygon_{}", False), - (Polygon, Line): ("{}_line_polygon_{}", True), - - # Ray combinations - (Ray, Ray): ("{}_ray_ray_{}", False), - (Ray, Segment): ("{}_ray_segment_{}", False), - (Segment, Ray): ("{}_ray_segment_{}", True), - (Ray, Polygon): ("{}_ray_polygon_{}", False), - (Polygon, Ray): ("{}_ray_polygon_{}", True), - - # Segment combinations - (Segment, Segment): ("{}_segment_segment_{}", False), - (Segment, Polygon): ("{}_segment_polygon_{}", False), - (Polygon, Segment): ("{}_segment_polygon_{}", True), - - # Polygon combinations - (Polygon, Polygon): ("{}_polygon_polygon_{}", False), - - # Plane combinations (3D only) - (Segment, Plane): ("{}_segment_plane_{}", False), - (Plane, Segment): ("{}_segment_plane_{}", True), - (Ray, Plane): ("{}_ray_plane_{}", False), - (Plane, Ray): ("{}_ray_plane_{}", True), - (Line, Plane): ("{}_line_plane_{}", False), - (Plane, Line): ("{}_line_plane_{}", True), - (Polygon, Plane): ("{}_polygon_plane_{}", False), - (Plane, Polygon): ("{}_polygon_plane_{}", True), - (Plane, Plane): ("{}_plane_plane_{}", False), -} - - -# ============================================================================= -# RAY_CAST: target_type -> func_template -# Ray is always the source, no needs_swap -# ============================================================================= -RAY_CAST = { - Plane: "ray_cast_ray_plane_{}", - Polygon: "ray_cast_ray_polygon_{}", - Segment: "ray_cast_ray_segment_{}", - Line: "ray_cast_ray_line_{}", - AABB: "ray_cast_ray_aabb_{}", -} diff --git a/python/src/trueform/_core/closest_metric_point_pair.py b/python/src/trueform/_core/closest_metric_point_pair.py deleted file mode 100644 index d1e6729..0000000 --- a/python/src/trueform/_core/closest_metric_point_pair.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Closest metric point pair functions - -Copyright (c) 2025 Žiga Sajovic, XLAB -Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. -Commercial licensing available via info@polydera.com. -https://github.com/polydera/trueform -""" - -import numpy as np -from typing import Any, Tuple -from .. import _trueform -from .._primitives import Point, Segment, Polygon, Ray, Line, Plane -from .._dispatch import InputMeta, build_suffix - - -# Dispatch table for closest_metric_point_pair -# Maps (type0, type1) -> (function_name_template, needs_swap) -# needs_swap=True means we need to swap arguments and results -_CLOSEST_PAIR_DISPATCH = { - (Point, Point): ("closest_metric_point_pair_point_point_{}", False), - (Point, Segment): ("closest_metric_point_pair_point_segment_{}", False), - (Segment, Point): ("closest_metric_point_pair_point_segment_{}", True), - (Point, Polygon): ("closest_metric_point_pair_point_polygon_{}", False), - (Polygon, Point): ("closest_metric_point_pair_point_polygon_{}", True), - (Segment, Segment): ("closest_metric_point_pair_segment_segment_{}", False), - (Segment, Polygon): ("closest_metric_point_pair_segment_polygon_{}", False), - (Polygon, Segment): ("closest_metric_point_pair_segment_polygon_{}", True), - (Polygon, Polygon): ("closest_metric_point_pair_polygon_polygon_{}", False), - (Point, Ray): ("closest_metric_point_pair_point_ray_{}", False), - (Ray, Point): ("closest_metric_point_pair_point_ray_{}", True), - (Point, Line): ("closest_metric_point_pair_point_line_{}", False), - (Line, Point): ("closest_metric_point_pair_point_line_{}", True), - (Segment, Ray): ("closest_metric_point_pair_segment_ray_{}", False), - (Ray, Segment): ("closest_metric_point_pair_segment_ray_{}", True), - (Segment, Line): ("closest_metric_point_pair_segment_line_{}", False), - (Line, Segment): ("closest_metric_point_pair_segment_line_{}", True), - (Polygon, Ray): ("closest_metric_point_pair_polygon_ray_{}", False), - (Ray, Polygon): ("closest_metric_point_pair_polygon_ray_{}", True), - (Polygon, Line): ("closest_metric_point_pair_polygon_line_{}", False), - (Line, Polygon): ("closest_metric_point_pair_polygon_line_{}", True), - (Ray, Ray): ("closest_metric_point_pair_ray_ray_{}", False), - (Line, Line): ("closest_metric_point_pair_line_line_{}", False), - (Ray, Line): ("closest_metric_point_pair_ray_line_{}", False), - (Line, Ray): ("closest_metric_point_pair_ray_line_{}", True), - # Plane combinations (3D only) - (Point, Plane): ("closest_metric_point_pair_point_plane_{}", False), - (Plane, Point): ("closest_metric_point_pair_plane_point_{}", False), - (Segment, Plane): ("closest_metric_point_pair_segment_plane_{}", False), - (Plane, Segment): ("closest_metric_point_pair_plane_segment_{}", False), - (Ray, Plane): ("closest_metric_point_pair_ray_plane_{}", False), - (Plane, Ray): ("closest_metric_point_pair_plane_ray_{}", False), - (Line, Plane): ("closest_metric_point_pair_line_plane_{}", False), - (Plane, Line): ("closest_metric_point_pair_plane_line_{}", False), - (Polygon, Plane): ("closest_metric_point_pair_polygon_plane_{}", False), - (Plane, Polygon): ("closest_metric_point_pair_plane_polygon_{}", False), - (Plane, Plane): ("closest_metric_point_pair_plane_plane_{}", False), -} - - -def closest_metric_point(obj0: Any, obj1: Any) -> Tuple[float, np.ndarray]: - """ - Compute closest point on obj0 to obj1. - - Returns the squared distance and the closest point on obj0. - - Parameters - ---------- - obj0, obj1 - Geometric objects (Point, Segment, Polygon, Ray, Line, Plane, or numpy arrays) - - Returns - ------- - distance_squared : float - Squared distance between obj0 and obj1 - closest_point : np.ndarray - Closest point on obj0 (first argument) - - Examples - -------- - >>> import trueform as tf - >>> import numpy as np - >>> seg = tf.Segment([[1, 0, 0], [1, 1, 0]]) - >>> pt = tf.Point([0, 0, 0]) - >>> dist2, closest = tf.closest_metric_point(seg, pt) - >>> dist2 - 1.0 - >>> closest - array([1., 0., 0.]) - """ - dist2, closest_pt, _ = closest_metric_point_pair(obj0, obj1) - return dist2, closest_pt - - -def closest_metric_point_pair(obj0: Any, obj1: Any) -> Tuple[float, np.ndarray, np.ndarray]: - """ - Compute closest point pair between two geometric objects. - - Returns the squared distance and the two closest points, one on each object. - - Parameters - ---------- - obj0, obj1 - Geometric objects (Point, Segment, Polygon, Ray, Line, or numpy arrays) - - Returns - ------- - distance_squared : float - Squared distance between the objects - point0 : np.ndarray - Closest point on obj0 - point1 : np.ndarray - Closest point on obj1 - - Examples - -------- - >>> import trueform as tf - >>> import numpy as np - >>> pt0 = tf.Point([0, 0, 0]) - >>> pt1 = tf.Point([1, 1, 1]) - >>> dist2, p0, p1 = tf.closest_metric_point_pair(pt0, pt1) - >>> dist2 - 3.0 - """ - - # Helper to get dimensionality - def get_dims(obj): - if hasattr(obj, 'dims'): - return obj.dims - elif isinstance(obj, np.ndarray): - if obj.ndim == 1: - return obj.shape[0] - elif obj.ndim == 2: - return obj.shape[1] - raise TypeError(f"Cannot determine dimensions for type {type(obj)}") - - # Helper to get variant suffix - def get_suffix(obj): - if hasattr(obj, 'dtype') and hasattr(obj, 'dims'): - meta = InputMeta(None, obj.dtype, None, obj.dims) - return build_suffix(meta) - elif isinstance(obj, np.ndarray): - # For raw arrays, infer dims from shape - dims = obj.shape[0] if obj.ndim == 1 else obj.shape[1] - meta = InputMeta(None, obj.dtype, None, dims) - return build_suffix(meta) - raise TypeError(f"Cannot determine variant for type {type(obj)}") - - # Helper to extract data from object (handles Point wrappers and numpy arrays) - def get_data(obj): - if hasattr(obj, 'data'): - return obj.data - elif isinstance(obj, np.ndarray): - return obj - raise TypeError(f"Cannot extract data from type {type(obj)}") - - # Validate dimensions match - dims0 = get_dims(obj0) - dims1 = get_dims(obj1) - if dims0 != dims1: - raise ValueError( - f"Dimension mismatch: obj0 has {dims0}D, obj1 has {dims1}D. " - f"Both objects must have the same dimensionality (2D or 3D)." - ) - - # Normalize types (treat numpy arrays as Point for dispatch) - type0 = Point if isinstance( - obj0, np.ndarray) and obj0.ndim == 1 else type(obj0) - type1 = Point if isinstance( - obj1, np.ndarray) and obj1.ndim == 1 else type(obj1) - - # Look up dispatch info - type_pair = (type0, type1) - if type_pair not in _CLOSEST_PAIR_DISPATCH: - raise TypeError( - f"closest_metric_point_pair not implemented for types: " - f"{type0.__name__}, {type1.__name__}" - ) - - func_template, needs_swap = _CLOSEST_PAIR_DISPATCH[type_pair] - - # Special case: Plane is 3D only - if (type0 is Plane or type1 is Plane) and dims0 != 3: - raise ValueError("closest_metric_point_pair with Plane is only supported in 3D") - - # Get suffix and function name - suffix = get_suffix(obj0 if not needs_swap else obj1) - func_name = func_template.format(suffix) - - # Get data and call C++ function - data0 = get_data(obj0) - data1 = get_data(obj1) - - if needs_swap: - # Swap arguments and results - dist2, pt1, pt0 = getattr(_trueform.core, func_name)(data1, data0) - return dist2, pt0, pt1 - else: - return getattr(_trueform.core, func_name)(data0, data1) diff --git a/python/src/trueform/_core/transformed.py b/python/src/trueform/_core/transformed.py index f11427a..50197fe 100644 --- a/python/src/trueform/_core/transformed.py +++ b/python/src/trueform/_core/transformed.py @@ -69,115 +69,102 @@ def transformed( # Extract rotation part (translation handled via homogeneous coordinates) R = transformation[:dims, :dims] # Rotation matrix - - # Helper: Transform a point (affine transformation) - def transform_point(pt): - """Apply affine transformation: R*pt + t""" - homogeneous = np.append(pt, 1) - return transformation[:dims] @ homogeneous - - # Helper: Transform a vector/direction (rotation only) - def transform_vector(vec): - """Apply rotation only: R*vec""" - return R @ vec + dtype = primitive.data.dtype + + # Vectorized helpers that handle both single and batch shapes. + # Any shape (..., D) is flattened to (M, D), transformed, and reshaped back. + + def transform_points(pts): + """Apply affine transformation to point(s). Shape (..., D) -> (..., D).""" + shape = pts.shape + flat = pts.reshape(-1, dims) + ones = np.ones((flat.shape[0], 1), dtype=dtype) + homo = np.hstack([flat, ones]) + result = homo @ transformation[:dims].T + return result.reshape(shape).astype(dtype) + + def transform_vectors(vecs): + """Apply rotation to vector(s). Shape (..., D) -> (..., D).""" + shape = vecs.shape + flat = vecs.reshape(-1, dims) + result = flat @ R.T + return result.reshape(shape).astype(dtype) # Dispatch based on primitive type if prim_type is Point: - new_data = transform_point(primitive.data) - return Point(new_data) + # Single (D,) or batch (N, D) - all entries are points + return Point(transform_points(primitive.data)) elif prim_type is Segment: - # Transform both endpoints - pt0 = primitive.data[0] - pt1 = primitive.data[1] - new_data = np.array([ - transform_point(pt0), - transform_point(pt1) - ], dtype=primitive.data.dtype) - return Segment(new_data) + # Single (2, D) or batch (N, 2, D) - all entries are point coords + return Segment(transform_points(primitive.data)) elif prim_type is Polygon: - # Transform all vertices - new_data = np.array([ - transform_point(pt) for pt in primitive.data - ], dtype=primitive.data.dtype) - return Polygon(new_data) + # Single (V, D) or batch (N, V, D) - all entries are point coords + return Polygon(transform_points(primitive.data)) elif prim_type is AABB: - # Efficient AABB transformation without transforming all 8 corners - # Method: Transform center and half-extents separately - center = (primitive.data[0] + primitive.data[1]) / 2 - half_extent = (primitive.data[1] - primitive.data[0]) / 2 - - # Transform center as a point - new_center = transform_point(center) + # Single (2, D) or batch (N, 2, D) + min_pts = primitive.data[..., 0, :] # (..., D) + max_pts = primitive.data[..., 1, :] # (..., D) + center = (min_pts + max_pts) / 2 + half_extent = (max_pts - min_pts) / 2 + + new_center = transform_points(center) + # |R|^T @ half_extent, vectorized for any leading dims + he_shape = half_extent.shape + new_half_extent = half_extent.reshape(-1, dims) @ np.abs(R).T + new_half_extent = new_half_extent.reshape(he_shape).astype(dtype) - # Transform half-extents using absolute rotation values - # This accounts for potential axis flipping - new_half_extent = np.abs(R) @ half_extent - - # Reconstruct AABB new_min = new_center - new_half_extent new_max = new_center + new_half_extent - new_data = np.array([new_min, new_max], dtype=primitive.data.dtype) return AABB(min=new_min, max=new_max) elif prim_type is Ray: - # Transform origin (point) and direction (vector) - origin = primitive.data[0] - direction = primitive.data[1] - - new_origin = transform_point(origin) - new_direction = transform_vector(direction) + # Single (2, D) or batch (N, 2, D) + origin = primitive.data[..., 0, :] # (..., D) + direction = primitive.data[..., 1, :] # (..., D) - # Renormalize direction (transformation might scale it) - new_direction = new_direction / np.linalg.norm(new_direction) + new_origin = transform_points(origin) + new_direction = transform_vectors(direction) + new_direction = new_direction / np.linalg.norm( + new_direction, axis=-1, keepdims=True) - new_data = np.array([new_origin, new_direction], dtype=primitive.data.dtype) + new_data = np.stack([new_origin, new_direction], axis=-2).astype(dtype) return Ray(data=new_data) elif prim_type is Line: - # Transform origin (point) and direction (vector) - origin = primitive.data[0] - direction = primitive.data[1] + # Single (2, D) or batch (N, 2, D) + origin = primitive.data[..., 0, :] # (..., D) + direction = primitive.data[..., 1, :] # (..., D) - new_origin = transform_point(origin) - new_direction = transform_vector(direction) + new_origin = transform_points(origin) + new_direction = transform_vectors(direction) + new_direction = new_direction / np.linalg.norm( + new_direction, axis=-1, keepdims=True) - # Renormalize direction (transformation might scale it) - new_direction = new_direction / np.linalg.norm(new_direction) - - new_data = np.array([new_origin, new_direction], dtype=primitive.data.dtype) + new_data = np.stack([new_origin, new_direction], axis=-2).astype(dtype) return Line(data=new_data) elif prim_type is Plane: - # For planes, we need to transform the normal and recompute d - # Plane equation: n·x + d = 0 - # Normal transforms as: n' = (R^-T) * n (inverse transpose for normals) - # But for orthogonal transformations (rotation), R^-T = R - - # Get original normal and d - normal = primitive.data[:dims] - d = primitive.data[dims] - - # Transform normal using inverse transpose of rotation - # For rotation matrices: R^-T = R, so we can use R directly - # For general transformations, we'd need: (R^-1)^T - R_inv_T = np.linalg.inv(R).T - new_normal = R_inv_T @ normal - - # Renormalize - new_normal = new_normal / np.linalg.norm(new_normal) + # Single (D+1,) or batch (N, D+1) + normal = primitive.data[..., :dims] # (..., D) + d = primitive.data[..., dims] # scalar or (N,) - # Recompute d from transformed normal and a point on the plane - # Original point on plane: any point where n·p + d = 0 - # Use origin projection: p = -d * n - point_on_plane = -d * normal - transformed_point = transform_point(point_on_plane) - new_d = -np.dot(new_normal, transformed_point) - - # Reconstruct plane data: [nx, ny, nz, d] - new_data = np.append(new_normal, new_d).astype(primitive.data.dtype) + R_inv_T = np.linalg.inv(R).T + n_shape = normal.shape + new_normal = normal.reshape(-1, dims) @ R_inv_T.T + new_normal = new_normal.reshape(n_shape) + new_normal = new_normal / np.linalg.norm( + new_normal, axis=-1, keepdims=True) + + # Reconstruct d from a point on the original plane: p = -d * n + point_on_plane = -np.expand_dims(d, -1) * normal + transformed_pt = transform_points(point_on_plane) + new_d = -np.einsum('...i,...i->...', new_normal, transformed_pt) + + new_data = np.concatenate( + [new_normal, np.expand_dims(new_d, -1)], axis=-1).astype(dtype) return Plane(new_data) else: diff --git a/python/src/trueform/_cut/isobands.py b/python/src/trueform/_cut/isobands.py index 3d291a7..0e00085 100644 --- a/python/src/trueform/_cut/isobands.py +++ b/python/src/trueform/_cut/isobands.py @@ -76,7 +76,7 @@ def isobands( >>> faces, points = tf.read_stl("mesh.stl") >>> mesh = tf.Mesh(faces, points) >>> plane = tf.Plane(normal=[0.0, 0.0, 1.0], offset=0.0) - >>> distances = tf.distance_field(mesh.points, plane) + >>> distances = tf.distance(tf.Point(mesh.points), plane) >>> >>> # Extract isobands at different height levels using Mesh >>> (band_faces, band_points), labels = tf.isobands(mesh, distances, [-1.0, 0.0, 1.0]) diff --git a/python/src/trueform/_dispatch/meta.py b/python/src/trueform/_dispatch/meta.py index ecbb379..6356de2 100644 --- a/python/src/trueform/_dispatch/meta.py +++ b/python/src/trueform/_dispatch/meta.py @@ -16,6 +16,7 @@ class InputMeta(NamedTuple): real_dtype: np.dtype # np.float32 or np.float64 ngon: Optional[str] # None for PointCloud/EdgeMesh, '2'/'3'/'dyn' for Mesh/tuples dims: int # 2 or 3 + form_name: Optional[str] = None # "mesh", "edge_mesh", "point_cloud", or None def extract_meta(data) -> InputMeta: @@ -28,20 +29,23 @@ def extract_meta(data) -> InputMeta: real_dtype=data.dtype, ngon='dyn' if data.is_dynamic else str(data.ngon), dims=data.dims, + form_name="mesh", ) elif isinstance(data, EdgeMesh): return InputMeta( index_dtype=data.edges.dtype, real_dtype=data.dtype, - ngon=None, # EdgeMesh has no ngon + ngon=None, dims=data.dims, + form_name="edge_mesh", ) elif isinstance(data, PointCloud): return InputMeta( - index_dtype=None, # PointCloud has no index + index_dtype=None, real_dtype=data.dtype, ngon=None, dims=data.dims, + form_name="point_cloud", ) elif isinstance(data, tuple): return _extract_meta_from_tuple(data) diff --git a/python/src/trueform/_geometry/measurements.py b/python/src/trueform/_geometry/measurements.py index a923c02..6ed99a4 100644 --- a/python/src/trueform/_geometry/measurements.py +++ b/python/src/trueform/_geometry/measurements.py @@ -11,9 +11,10 @@ import numpy as np from .. import _trueform from .._spatial import Mesh -from .._primitives import Polygon +from .._primitives import Polygon, Triangle from .._core import OffsetBlockedArray -from .._dispatch import ensure_mesh, extract_meta, build_suffix +from .._primitives.primitive import Primitive +from .._dispatch import ensure_mesh, extract_meta, build_suffix, InputMeta def signed_volume( @@ -85,37 +86,42 @@ def volume( def area( - data: Union[np.ndarray, Polygon, Mesh, Tuple[np.ndarray, np.ndarray], + data: Union[np.ndarray, Triangle, Polygon, Mesh, Tuple[np.ndarray, np.ndarray], Tuple[OffsetBlockedArray, np.ndarray]] -) -> float: +): """ - Compute area of a polygon or total surface area of a mesh. + Compute area of a triangle, polygon, or total surface area of a mesh. Parameters ---------- - data : np.ndarray, Polygon, Mesh, or tuple - - np.ndarray shape (N, D): Single polygon with N vertices in D dimensions - - Polygon: tf.Polygon object + data : Triangle, Polygon, np.ndarray, Mesh, or tuple + - Triangle: Single or batch of triangles. Returns scalar or (N,) array. + - Polygon: Single or batch of polygons. Returns scalar or (N,) array. + - np.ndarray shape (V, D): Single polygon with V vertices in D dimensions - Mesh: Triangle mesh (returns total surface area) - (faces, points): Tuple with face indices and point coordinates - (OffsetBlockedArray, points): Dynamic polygon mesh Returns ------- - float - Area of the polygon, or total surface area of all faces. + float or np.ndarray + Scalar for single primitive or mesh, array for batch primitives. Examples -------- >>> import trueform as tf >>> import numpy as np >>> - >>> # Single polygon as array - >>> polygon = np.array([[0,0,0],[1,0,0],[1,1,0],[0,1,0]], dtype=np.float32) - >>> tf.area(polygon) - 1.0 + >>> # Single triangle + >>> tri = tf.Triangle(a=[0,0,0], b=[1,0,0], c=[0,1,0]) + >>> tf.area(tri) + 0.5 >>> - >>> # Single polygon as Polygon object + >>> # Batch of triangles + >>> tris = tf.Triangle(np.random.rand(50, 3, 3).astype(np.float32)) + >>> areas = tf.area(tris) # shape (50,) + >>> + >>> # Single polygon >>> poly = tf.Polygon([[0,0,0],[1,0,0],[1,1,0],[0,1,0]]) >>> tf.area(poly) 1.0 @@ -125,17 +131,19 @@ def area( >>> tf.area((faces, points)) 6.0 """ - # Handle Polygon object - extract vertices - if isinstance(data, Polygon): - data = data.vertices - - meta = extract_meta(data) - - # Single polygon (points only, no index dtype means it's just points) - if meta.index_dtype is None: - suffix = build_suffix(meta) - func = getattr(_trueform.geometry, f"area_{suffix}") - return func(data) + # Triangle or Polygon primitive → dispatch to C++ primitive binding + if isinstance(data, (Triangle, Polygon)): + suffix = build_suffix(InputMeta(None, data.dtype, None, data.dims)) + fn = getattr(_trueform.spatial, f"area_prim_{suffix}") + return fn(data._wrapper) + + # Handle raw ndarray polygon (no wrapper) + if isinstance(data, np.ndarray): + meta = extract_meta(data) + if meta.index_dtype is None: + suffix = build_suffix(meta) + func = getattr(_trueform.geometry, f"area_{suffix}") + return func(data) # Mesh or tuple mesh = ensure_mesh(data) @@ -145,15 +153,17 @@ def area( def mean_edge_length( - data: Union[Mesh, Tuple[np.ndarray, np.ndarray], + data: Union[Triangle, Polygon, Mesh, Tuple[np.ndarray, np.ndarray], Tuple[OffsetBlockedArray, np.ndarray]] ) -> float: """ - Compute mean edge length of a polygon mesh. + Compute mean edge length of a triangle, polygon, or mesh. Parameters ---------- - data : Mesh or tuple + data : Triangle, Polygon, Mesh, or tuple + - Triangle: Single or batch. Returns mean over all edges. + - Polygon: Single or batch. Returns mean over all edges. - Mesh: tf.Mesh object - (faces, points): Tuple with face indices and point coordinates - (OffsetBlockedArray, points): Dynamic polygon mesh @@ -166,10 +176,22 @@ def mean_edge_length( Examples -------- >>> import trueform as tf + >>> tri = tf.Triangle(a=[0,0,0], b=[1,0,0], c=[0,1,0]) + >>> tf.mean_edge_length(tri) + 1.1380... + >>> >>> mesh = tf.Mesh(*tf.read_stl("model.stl")) >>> tf.mean_edge_length(mesh) 0.042 """ + # Triangle or Polygon primitive — pure numpy + if isinstance(data, (Triangle, Polygon)): + verts = data.data + v_next = np.roll(verts, -1, axis=-2) + edges = (v_next - verts).reshape(-1, verts.shape[-1]) + lengths = np.linalg.norm(edges, axis=1) + return float(lengths.mean()) + transform = None if isinstance(data, Mesh): faces, points = data.faces, data.points @@ -178,7 +200,7 @@ def mean_edge_length( faces, points = data else: raise TypeError( - f"data must be a Mesh or (faces, points) tuple, " + f"data must be a Triangle, Polygon, Mesh, or (faces, points) tuple, " f"got {type(data).__name__}" ) diff --git a/python/src/trueform/_geometry/normals.py b/python/src/trueform/_geometry/normals.py index c148ff1..4bf5704 100644 --- a/python/src/trueform/_geometry/normals.py +++ b/python/src/trueform/_geometry/normals.py @@ -9,43 +9,58 @@ from typing import Union, Tuple import numpy as np +from .. import _trueform from .._spatial import Mesh +from .._primitives import Triangle, Polygon +from .._primitives.primitive import Primitive from .._core import OffsetBlockedArray -from .._dispatch import ensure_mesh +from .._dispatch import ensure_mesh, InputMeta, build_suffix def normals( - data: Union[Mesh, Tuple[np.ndarray, np.ndarray], Tuple[OffsetBlockedArray, np.ndarray]] + data: Union[Triangle, Polygon, Mesh, Tuple[np.ndarray, np.ndarray], + Tuple[OffsetBlockedArray, np.ndarray]] ) -> np.ndarray: """ - Compute face normals for a mesh. + Compute face normals for a triangle, polygon, or mesh. Parameters ---------- - data : Mesh or tuple - - Mesh: Mesh object + data : Triangle, Polygon, Mesh, or tuple + - Triangle: Single 3D triangle returns (3,) unit normal; + batch of N triangles returns (N, 3) unit normals. + - Polygon: Single 3D polygon returns (3,) unit normal; + batch of N polygons returns (N, 3) unit normals. + - Mesh: Mesh object (must be 3D) - (faces, points): Tuple with face indices and point coordinates - (OffsetBlockedArray, points): Dynamic polygon mesh Returns ------- - normals : np.ndarray of shape (num_faces, 3) - Unit face normals + normals : np.ndarray + Unit face normals. Shape (3,) for single primitive, (N, 3) for batch, + or (num_faces, 3) for mesh. Raises ------ TypeError - If data is not a Mesh or valid tuple. + If data is not a Triangle, Polygon, Mesh, or valid tuple. ValueError - If mesh is not 3D. + If data is not 3D. Examples -------- >>> import trueform as tf >>> import numpy as np >>> - >>> faces = np.array([[0,1,2], [1,3,2]], dtype=np.int32) - >>> points = np.array([[0,0,0],[1,0,0],[0.5,1,0],[1.5,1,0]], dtype=np.float32) + >>> # Single triangle + >>> tri = tf.Triangle(a=[0,0,0], b=[1,0,0], c=[0,1,0]) + >>> tf.normals(tri) + array([0., 0., 1.], dtype=float32) + >>> + >>> # Batch of triangles + >>> tris = tf.Triangle(np.random.rand(50, 3, 3).astype(np.float32)) + >>> n = tf.normals(tris) # shape (50, 3) >>> >>> # From Mesh >>> mesh = tf.Mesh(faces, points) @@ -54,5 +69,14 @@ def normals( >>> # From tuple >>> n = tf.normals((faces, points)) """ + # Triangle or Polygon primitive → dispatch to C++ primitive binding + if isinstance(data, (Triangle, Polygon)): + if data.dims != 3: + raise ValueError( + f"normals() requires 3D primitives, got {data.dims}D") + suffix = build_suffix(InputMeta(None, data.dtype, None, data.dims)) + fn = getattr(_trueform.spatial, f"normals_prim_{suffix}") + return fn(data._wrapper) + mesh = ensure_mesh(data, dims=3) return mesh.normals diff --git a/python/src/trueform/_intersect/isocontours.py b/python/src/trueform/_intersect/isocontours.py index 36041e2..f38f49d 100644 --- a/python/src/trueform/_intersect/isocontours.py +++ b/python/src/trueform/_intersect/isocontours.py @@ -58,7 +58,7 @@ def isocontours( >>> faces, points = tf.read_stl("mesh.stl") >>> mesh = tf.Mesh(faces, points) >>> plane = tf.Plane(normal=[0.0, 0.0, 1.0], offset=0.0) - >>> distances = tf.distance_field(mesh.points, plane) + >>> distances = tf.distance(tf.Point(mesh.points), plane) >>> >>> # Extract single isocontour at z=0 using Mesh >>> paths, points = tf.isocontours(mesh, distances, 0.0) diff --git a/python/src/trueform/_io/__init__.py b/python/src/trueform/_io/__init__.py new file mode 100644 index 0000000..67b2fb1 --- /dev/null +++ b/python/src/trueform/_io/__init__.py @@ -0,0 +1,13 @@ +""" +IO utilities for reading and writing mesh files + +Copyright (c) 2025 Ziga Sajovic, XLAB +Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. +Commercial licensing available via info@polydera.com. +https://github.com/polydera/trueform +""" + +from .stl import read_stl, write_stl +from .obj import read_obj, write_obj + +__all__ = ['read_stl', 'write_stl', 'read_obj', 'write_obj'] diff --git a/python/src/trueform/_io.py b/python/src/trueform/_io/obj.py similarity index 50% rename from python/src/trueform/_io.py rename to python/src/trueform/_io/obj.py index c4d648b..aa5ca98 100644 --- a/python/src/trueform/_io.py +++ b/python/src/trueform/_io/obj.py @@ -1,7 +1,7 @@ """ -IO utilities for reading and writing mesh files +OBJ file reading and writing -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform @@ -9,221 +9,9 @@ import numpy as np from typing import Tuple, Optional, Union -from . import _trueform -from ._spatial.mesh import Mesh -from ._dispatch import InputMeta, build_suffix - - -def read_stl(filename: str, index_dtype: Union[type, np.dtype] = np.int32) -> Tuple[np.ndarray, np.ndarray]: - """ - Read an STL file and return mesh data as numpy arrays. - - Parameters - ---------- - filename : str - Path to the STL file to read - index_dtype : dtype, optional - Data type for face indices. Must be np.int32 or np.int64. Default is np.int32. - - Returns - ------- - faces : ndarray of shape (num_faces, 3) with dtype specified by index_dtype - Face indices into the points array. Each row contains three indices - that reference vertices in the points array, forming a triangle. - points : ndarray of shape (num_points, 3) and dtype float32 - 3D coordinates of mesh vertices. Each row is a (x, y, z) coordinate. - - Examples - -------- - >>> import trueform as tf - >>> # Read with default int32 indices - >>> faces, points = tf.read_stl("model.stl") - >>> print(f"Faces dtype: {faces.dtype}") # int32 - >>> - >>> # Read with int64 indices for large meshes - >>> faces, points = tf.read_stl("model.stl", index_dtype=np.int64) - >>> print(f"Faces dtype: {faces.dtype}") # int64 - - Notes - ----- - The STL file format stores triangular mesh data. This function: - - Reads both ASCII and binary STL files - - Cleans duplicate vertices (merges points that are identical) - - Returns zero-copy numpy arrays (memory is managed by numpy) - - Face indices are 0-based - """ - # Normalize dtype - if isinstance(index_dtype, type): - index_dtype = np.dtype(index_dtype) - elif not isinstance(index_dtype, np.dtype): - index_dtype = np.dtype(index_dtype) - - # Validate and dispatch - if index_dtype == np.int32: - return _trueform.io.read_stl_int32(filename) - elif index_dtype == np.int64: - return _trueform.io.read_stl_int64(filename) - else: - raise ValueError( - f"index_dtype must be np.int32 or np.int64, got {index_dtype}" - ) - - -def write_stl( - data: Union[Tuple[np.ndarray, np.ndarray], Mesh], - filename: str, - transformation: Optional[np.ndarray] = None -) -> bool: - """ - Write a triangular mesh to an STL file. - - Supports indexed geometry as tuples or Mesh objects: - - Tuple: write_stl((faces, points), filename, transformation=None) - - Mesh: write_stl(mesh, filename, transformation=None) - - Parameters - ---------- - data : tuple or Mesh - Input geometric data: - - Tuple (faces, points) where: - * faces: shape (N, 3) with dtype int32 or int64 - * points: shape (M, 3) with dtype float32 - - Mesh object (must be 3D triangular mesh) - filename : str - Path to output STL file. - The .stl extension will be appended if not present. - transformation : ndarray of shape (4, 4) with dtype float32, optional - Homogeneous transformation matrix to apply before writing. - If provided, overrides any transformation set on the Mesh object. - - Returns - ------- - success : bool - True if the file was written successfully, False otherwise. - - Examples - -------- - >>> import trueform as tf - >>> import numpy as np - >>> - >>> # Tuple input - >>> faces = np.array([[0, 1, 2]], dtype=np.int32) - >>> points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]], dtype=np.float32) - >>> tf.write_stl((faces, points), "triangle.stl") - >>> - >>> # Tuple input with transformation - >>> transform = np.eye(4, dtype=np.float32) - >>> transform[2, 3] = 5.0 # Translate 5 units in Z - >>> tf.write_stl((faces, points), "triangle_translated.stl", transformation=transform) - >>> - >>> # Mesh object - >>> mesh = tf.Mesh(faces, points) - >>> tf.write_stl(mesh, "triangle.stl") - >>> - >>> # Mesh with transformation - >>> mesh.transformation = transform - >>> tf.write_stl(mesh, "triangle_transformed.stl") - >>> - >>> # Override mesh transformation - >>> tf.write_stl(mesh, "triangle_custom.stl", transformation=other_transform) - - Notes - ----- - - The STL format only supports triangular meshes. All faces must have exactly 3 vertices. - - For Mesh objects, only triangle meshes (ngon=3) are supported, not quads. - - Points must be 3D (shape (N, 3)) - - Binary STL format is used for writing - - Arrays must be C-contiguous - - When using a Mesh object, if the explicit transformation kwarg is provided, - it overrides the mesh's transformation property - """ - # Handle Mesh object - if isinstance(data, Mesh): - mesh = data - - # Validate Mesh requirements for STL - if mesh.dims != 3: - raise ValueError( - f"STL format only supports 3D meshes, but mesh has {mesh.dims}D points. " - f"Only 3D meshes can be written to STL." - ) - - if mesh.is_dynamic or mesh.ngon != 3: - raise ValueError( - f"STL format only supports triangular meshes, but mesh has " - f"{'dynamic' if mesh.is_dynamic else f'{mesh.ngon}-gon'} faces. " - f"Convert to triangles first." - ) - - # Extract data from mesh - faces = mesh.faces - points = mesh.points - - # Use mesh transformation if no explicit override - if transformation is None and mesh.transformation is not None: - transformation = mesh.transformation - - # Handle tuple input - elif isinstance(data, tuple): - if len(data) != 2: - raise ValueError( - f"Tuple input must have exactly 2 elements (faces, points), got {len(data)}" - ) - - faces, points = data - - else: - raise TypeError( - f"Expected tuple or Mesh object, got {type(data).__name__}" - ) - - # Validate faces shape and dtype - if faces.ndim != 2 or faces.shape[1] != 3: - raise ValueError( - f"faces must have shape (N, 3), got shape {faces.shape}" - ) - - faces_dtype = faces.dtype - if faces_dtype not in (np.int32, np.int64): - raise ValueError( - f"faces dtype must be np.int32 or np.int64, got {faces_dtype}" - ) - - # Validate points shape and dtype - if points.ndim != 2 or points.shape[1] != 3: - raise ValueError( - f"points must have shape (M, 3), got shape {points.shape}" - ) - - if points.dtype != np.float32: - raise ValueError( - f"points dtype must be np.float32, got {points.dtype}" - ) - - # Validate transformation if provided - if transformation is not None: - if transformation.shape != (4, 4): - raise ValueError( - f"transformation must have shape (4, 4), got {transformation.shape}" - ) - if transformation.dtype != np.float32: - raise ValueError( - f"transformation dtype must be np.float32, got {transformation.dtype}" - ) - if not transformation.flags['C_CONTIGUOUS']: - transformation = np.ascontiguousarray(transformation) - - # Ensure arrays are C-contiguous - if not faces.flags['C_CONTIGUOUS']: - faces = np.ascontiguousarray(faces) - if not points.flags['C_CONTIGUOUS']: - points = np.ascontiguousarray(points) - - # Dispatch based on faces dtype - if faces_dtype == np.int32: - return _trueform.io.write_stl_int32(faces, points, transformation, filename) - else: # int64 - return _trueform.io.write_stl_int64(faces, points, transformation, filename) +from .. import _trueform +from .._spatial.mesh import Mesh +from .._dispatch import InputMeta, build_suffix def read_obj(filename: str, ngon: int, index_dtype: Union[type, np.dtype] = np.int32) -> Tuple[np.ndarray, np.ndarray]: diff --git a/python/src/trueform/_io/stl.py b/python/src/trueform/_io/stl.py new file mode 100644 index 0000000..b649059 --- /dev/null +++ b/python/src/trueform/_io/stl.py @@ -0,0 +1,225 @@ +""" +STL file reading and writing + +Copyright (c) 2025 Ziga Sajovic, XLAB +Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. +Commercial licensing available via info@polydera.com. +https://github.com/polydera/trueform +""" + +import numpy as np +from typing import Tuple, Optional, Union +from .. import _trueform +from .._spatial.mesh import Mesh + + +def read_stl(filename: str, index_dtype: Union[type, np.dtype] = np.int32) -> Tuple[np.ndarray, np.ndarray]: + """ + Read an STL file and return mesh data as numpy arrays. + + Parameters + ---------- + filename : str + Path to the STL file to read + index_dtype : dtype, optional + Data type for face indices. Must be np.int32 or np.int64. Default is np.int32. + + Returns + ------- + faces : ndarray of shape (num_faces, 3) with dtype specified by index_dtype + Face indices into the points array. Each row contains three indices + that reference vertices in the points array, forming a triangle. + points : ndarray of shape (num_points, 3) and dtype float32 + 3D coordinates of mesh vertices. Each row is a (x, y, z) coordinate. + + Examples + -------- + >>> import trueform as tf + >>> # Read with default int32 indices + >>> faces, points = tf.read_stl("model.stl") + >>> print(f"Faces dtype: {faces.dtype}") # int32 + >>> + >>> # Read with int64 indices for large meshes + >>> faces, points = tf.read_stl("model.stl", index_dtype=np.int64) + >>> print(f"Faces dtype: {faces.dtype}") # int64 + + Notes + ----- + The STL file format stores triangular mesh data. This function: + - Reads both ASCII and binary STL files + - Cleans duplicate vertices (merges points that are identical) + - Returns zero-copy numpy arrays (memory is managed by numpy) + - Face indices are 0-based + """ + # Normalize dtype + if isinstance(index_dtype, type): + index_dtype = np.dtype(index_dtype) + elif not isinstance(index_dtype, np.dtype): + index_dtype = np.dtype(index_dtype) + + # Validate and dispatch + if index_dtype == np.int32: + return _trueform.io.read_stl_int32(filename) + elif index_dtype == np.int64: + return _trueform.io.read_stl_int64(filename) + else: + raise ValueError( + f"index_dtype must be np.int32 or np.int64, got {index_dtype}" + ) + + +def write_stl( + data: Union[Tuple[np.ndarray, np.ndarray], Mesh], + filename: str, + transformation: Optional[np.ndarray] = None +) -> bool: + """ + Write a triangular mesh to an STL file. + + Supports indexed geometry as tuples or Mesh objects: + - Tuple: write_stl((faces, points), filename, transformation=None) + - Mesh: write_stl(mesh, filename, transformation=None) + + Parameters + ---------- + data : tuple or Mesh + Input geometric data: + - Tuple (faces, points) where: + * faces: shape (N, 3) with dtype int32 or int64 + * points: shape (M, 3) with dtype float32 + - Mesh object (must be 3D triangular mesh) + filename : str + Path to output STL file. + The .stl extension will be appended if not present. + transformation : ndarray of shape (4, 4) with dtype float32, optional + Homogeneous transformation matrix to apply before writing. + If provided, overrides any transformation set on the Mesh object. + + Returns + ------- + success : bool + True if the file was written successfully, False otherwise. + + Examples + -------- + >>> import trueform as tf + >>> import numpy as np + >>> + >>> # Tuple input + >>> faces = np.array([[0, 1, 2]], dtype=np.int32) + >>> points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]], dtype=np.float32) + >>> tf.write_stl((faces, points), "triangle.stl") + >>> + >>> # Tuple input with transformation + >>> transform = np.eye(4, dtype=np.float32) + >>> transform[2, 3] = 5.0 # Translate 5 units in Z + >>> tf.write_stl((faces, points), "triangle_translated.stl", transformation=transform) + >>> + >>> # Mesh object + >>> mesh = tf.Mesh(faces, points) + >>> tf.write_stl(mesh, "triangle.stl") + >>> + >>> # Mesh with transformation + >>> mesh.transformation = transform + >>> tf.write_stl(mesh, "triangle_transformed.stl") + >>> + >>> # Override mesh transformation + >>> tf.write_stl(mesh, "triangle_custom.stl", transformation=other_transform) + + Notes + ----- + - The STL format only supports triangular meshes. All faces must have exactly 3 vertices. + - For Mesh objects, only triangle meshes (ngon=3) are supported, not quads. + - Points must be 3D (shape (N, 3)) + - Binary STL format is used for writing + - Arrays must be C-contiguous + - When using a Mesh object, if the explicit transformation kwarg is provided, + it overrides the mesh's transformation property + """ + # Handle Mesh object + if isinstance(data, Mesh): + mesh = data + + # Validate Mesh requirements for STL + if mesh.dims != 3: + raise ValueError( + f"STL format only supports 3D meshes, but mesh has {mesh.dims}D points. " + f"Only 3D meshes can be written to STL." + ) + + if mesh.is_dynamic or mesh.ngon != 3: + raise ValueError( + f"STL format only supports triangular meshes, but mesh has " + f"{'dynamic' if mesh.is_dynamic else f'{mesh.ngon}-gon'} faces. " + f"Convert to triangles first." + ) + + # Extract data from mesh + faces = mesh.faces + points = mesh.points + + # Use mesh transformation if no explicit override + if transformation is None and mesh.transformation is not None: + transformation = mesh.transformation + + # Handle tuple input + elif isinstance(data, tuple): + if len(data) != 2: + raise ValueError( + f"Tuple input must have exactly 2 elements (faces, points), got {len(data)}" + ) + + faces, points = data + + else: + raise TypeError( + f"Expected tuple or Mesh object, got {type(data).__name__}" + ) + + # Validate faces shape and dtype + if faces.ndim != 2 or faces.shape[1] != 3: + raise ValueError( + f"faces must have shape (N, 3), got shape {faces.shape}" + ) + + faces_dtype = faces.dtype + if faces_dtype not in (np.int32, np.int64): + raise ValueError( + f"faces dtype must be np.int32 or np.int64, got {faces_dtype}" + ) + + # Validate points shape and dtype + if points.ndim != 2 or points.shape[1] != 3: + raise ValueError( + f"points must have shape (M, 3), got shape {points.shape}" + ) + + if points.dtype != np.float32: + raise ValueError( + f"points dtype must be np.float32, got {points.dtype}" + ) + + # Validate transformation if provided + if transformation is not None: + if transformation.shape != (4, 4): + raise ValueError( + f"transformation must have shape (4, 4), got {transformation.shape}" + ) + if transformation.dtype != np.float32: + raise ValueError( + f"transformation dtype must be np.float32, got {transformation.dtype}" + ) + if not transformation.flags['C_CONTIGUOUS']: + transformation = np.ascontiguousarray(transformation) + + # Ensure arrays are C-contiguous + if not faces.flags['C_CONTIGUOUS']: + faces = np.ascontiguousarray(faces) + if not points.flags['C_CONTIGUOUS']: + points = np.ascontiguousarray(points) + + # Dispatch based on faces dtype + if faces_dtype == np.int32: + return _trueform.io.write_stl_int32(faces, points, transformation, filename) + else: # int64 + return _trueform.io.write_stl_int64(faces, points, transformation, filename) diff --git a/python/src/trueform/_primitives/__init__.py b/python/src/trueform/_primitives/__init__.py index c909ea7..b8f8771 100644 --- a/python/src/trueform/_primitives/__init__.py +++ b/python/src/trueform/_primitives/__init__.py @@ -7,12 +7,14 @@ https://github.com/polydera/trueform """ +from .primitive import Primitive, PrimitiveType from .point import Point from .segment import Segment +from .triangle import Triangle from .polygon import Polygon from .aabb import AABB from .ray import Ray from .line import Line from .plane import Plane -__all__ = ['Point', 'Segment', 'Polygon', 'AABB', 'Ray', 'Line', 'Plane'] +__all__ = ['Primitive', 'PrimitiveType', 'Point', 'Segment', 'Triangle', 'Polygon', 'AABB', 'Ray', 'Line', 'Plane'] diff --git a/python/src/trueform/_primitives/aabb.py b/python/src/trueform/_primitives/aabb.py index 2f4c943..1eacf4f 100644 --- a/python/src/trueform/_primitives/aabb.py +++ b/python/src/trueform/_primitives/aabb.py @@ -1,137 +1,100 @@ """ AABB (Axis-Aligned Bounding Box) primitive -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform """ import numpy as np -from typing import Optional +from .primitive import Primitive, PrimitiveType -class AABB: +class AABB(Primitive): """ An axis-aligned bounding box defined by min and max corners. Parameters ---------- - min : np.ndarray, optional - Minimum corner, shape (D,) where D is 2 or 3 - max : np.ndarray, optional - Maximum corner, shape (D,) - bounds : np.ndarray, optional - Alternative: shape (2, D) with [min, max] + data : array-like, optional + Bounds data. Shape (2, D) for a single AABB, (N, 2, D) for a batch, + where D is 2 or 3. Row 0 is min, row 1 is max. + min : array-like, optional + Minimum corner(s). Shape (D,) for single, (N, D) for batch. + max : array-like, optional + Maximum corner(s). Shape (D,) for single, (N, D) for batch. + + Either ``data`` or both ``min`` and ``max`` must be provided. Examples -------- - >>> import numpy as np >>> from trueform import AABB - >>> # Using min/max >>> box = AABB(min=[0, 0, 0], max=[1, 1, 1]) + >>> box.center + array([0.5, 0.5, 0.5], dtype=float32) + >>> box = AABB([[0, 0, 0], [1, 1, 1]]) >>> box.min array([0., 0., 0.], dtype=float32) - >>> # Using bounds array - >>> box = AABB(bounds=[[0, 0, 0], [1, 1, 1]]) - """ - def __init__(self, - min: Optional[np.ndarray] = None, - max: Optional[np.ndarray] = None, - bounds: Optional[np.ndarray] = None): - - if bounds is not None: - # Use bounds array directly - bounds = np.asarray(bounds) - if bounds.shape[0] != 2: - raise ValueError(f"AABB bounds must have shape (2, D), got {bounds.shape}") - data = bounds - elif min is not None and max is not None: - # Construct from min/max - min_arr = np.asarray(min) - max_arr = np.asarray(max) - - if min_arr.shape != max_arr.shape: - raise ValueError(f"Min and max must have same shape, got {min_arr.shape} and {max_arr.shape}") - - if min_arr.ndim != 1: - raise ValueError(f"Min and max must be 1D arrays, got shape {min_arr.shape}") - - data = np.stack([min_arr, max_arr], axis=0) - else: - raise ValueError("Must provide either 'bounds' or both 'min' and 'max'") - - # Validate shape - if data.ndim != 2 or data.shape[0] != 2: - raise ValueError(f"AABB data must have shape (2, D), got {data.shape}") + Batch construction: - # Validate dimensionality - dims = data.shape[1] - if dims not in [2, 3]: - raise ValueError(f"AABB must be 2D or 3D, got {dims} dimensions") + >>> import numpy as np + >>> boxes = AABB(np.random.rand(50, 2, 3)) + >>> boxes.count + 50 + """ - # Validate dtype - if data.dtype not in [np.float32, np.float64]: - # Try to convert + def __init__(self, data=None, *, min=None, max=None): + if data is None: + data = np.stack([np.asarray(min), np.asarray(max)], axis=-2) + else: + data = np.asarray(data) + if data.dtype not in (np.float32, np.float64): data = data.astype(np.float32) - - # Ensure C-contiguous if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data) - - # Validate min <= max - if np.any(data[0] > data[1]): - raise ValueError("AABB min must be <= max in all dimensions") - - self._data = data - self._dims = dims - self._dtype = data.dtype - - @property - def bounds(self) -> np.ndarray: - """Get bounds as (2, D) array with [min, max].""" - return self._data + dims = data.shape[-1] + if dims not in (2, 3): + raise ValueError(f"AABB must be 2D or 3D, got {dims} dimensions") + self._init_wrapper(data, PrimitiveType.aabb, dims) @property def min(self) -> np.ndarray: - """Get minimum corner.""" + """Get minimum corner(s).""" + if self.is_batch: + return self._data[:, 0] return self._data[0] @property def max(self) -> np.ndarray: - """Get maximum corner.""" + """Get maximum corner(s).""" + if self.is_batch: + return self._data[:, 1] return self._data[1] @property - def data(self) -> np.ndarray: - """Get underlying data array.""" + def bounds(self) -> np.ndarray: + """Get bounds as (2, D) or (N, 2, D) array.""" return self._data - @property - def dims(self) -> int: - """Get dimensionality (2 or 3).""" - return self._dims - - @property - def dtype(self) -> np.dtype: - """Get data type (float32 or float64).""" - return self._dtype - @property def center(self) -> np.ndarray: - """Get center point of the AABB.""" - return (self._data[0] + self._data[1]) / 2 + """Get center point(s).""" + return (self.min + self.max) / 2 @property def size(self) -> np.ndarray: """Get size in each dimension.""" - return self._data[1] - self._data[0] + return self.max - self.min @property - def volume(self) -> float: - """Get volume (area in 2D, volume in 3D).""" - return float(np.prod(self.size)) + def volume(self): + """Get volume (area in 2D). Returns scalar for single, array for batch.""" + s = self.size + if self.is_batch: + return np.prod(s, axis=-1) + return float(np.prod(s)) @classmethod def from_center_size(cls, center, size): @@ -141,60 +104,48 @@ def from_center_size(cls, center, size): Parameters ---------- center : array-like - Center point, shape (D,) where D is 2 or 3 + Center point, shape (D,) size : array-like Size in each dimension, shape (D,) Returns ------- AABB - AABB centered at center with given size Examples -------- >>> box = AABB.from_center_size([5, 5], [2, 2]) >>> box.min array([4., 4.], dtype=float32) - >>> box.max - array([6., 6.], dtype=float32) """ center = np.asarray(center) - size = np.asarray(size) - half_size = size / 2 - min_corner = center - half_size - max_corner = center + half_size - return cls(min=min_corner, max=max_corner) + half = np.asarray(size) / 2 + return cls(min=center - half, max=center + half) @classmethod def from_points(cls, points): """ - Create AABB that bounds the given points. + Create AABB bounding the given points. Parameters ---------- points : array-like - Points array, shape (N, D) where D is 2 or 3 + Points, shape (N, D) Returns ------- AABB - Axis-aligned bounding box containing all points Examples -------- - >>> points = [[0, 0], [1, 0], [1, 1], [0, 1]] - >>> box = AABB.from_points(points) - >>> box.min - array([0., 0.], dtype=float32) + >>> box = AABB.from_points([[0, 0], [1, 0], [1, 1]]) >>> box.max array([1., 1.], dtype=float32) """ points = np.asarray(points) - if points.ndim != 2: - raise ValueError(f"Points must be an array with shape (N, D) where D is 2 or 3, got shape {points.shape}") - min_corner = points.min(axis=0) - max_corner = points.max(axis=0) - return cls(min=min_corner, max=max_corner) + return cls(min=points.min(axis=0), max=points.max(axis=0)) def __repr__(self) -> str: + if self.is_batch: + return f"AABB(batch={self.count}, dims={self._dims}, dtype={self._dtype})" return f"AABB(min={self.min.tolist()}, max={self.max.tolist()}, dtype={self._dtype})" diff --git a/python/src/trueform/_primitives/line.py b/python/src/trueform/_primitives/line.py index 0ab7594..2c7177a 100644 --- a/python/src/trueform/_primitives/line.py +++ b/python/src/trueform/_primitives/line.py @@ -1,165 +1,117 @@ """ Line primitive -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform """ import numpy as np -from typing import Optional +from .primitive import Primitive, PrimitiveType -class Line: +class Line(Primitive): """ - A line defined by origin and direction. + An infinite line defined by an origin and direction. - ⚠️ **Important:** The direction vector is stored as-is, NOT normalized. - Use the `normalized_direction` property if you need a unit vector. + The direction vector is stored as-is, NOT normalized. Parameters ---------- - origin : np.ndarray, optional - Line origin, shape (D,) where D is 2 or 3 - direction : np.ndarray, optional - Line direction, shape (D,) - data : np.ndarray, optional - Alternative: shape (2, D) with [origin, direction] + data : array-like, optional + Line data. Shape (2, D) for a single line, (N, 2, D) for a batch, + where D is 2 or 3. Row 0 is origin, row 1 is direction. + origin : array-like, optional + Origin point(s). Shape (D,) for single, (N, D) for batch. + direction : array-like, optional + Direction vector(s). Shape (D,) for single, (N, D) for batch. + + Either ``data`` or both ``origin`` and ``direction`` must be provided. Examples -------- - >>> import numpy as np >>> from trueform import Line - >>> # Using origin/direction >>> line = Line(origin=[0, 0, 0], direction=[1, 2, 3]) >>> line.origin array([0., 0., 0.], dtype=float32) >>> line.direction array([1., 2., 3.], dtype=float32) - >>> # Using data array - >>> line = Line(data=[[0, 0, 0], [1, 1, 0]]) - """ - - def __init__(self, - origin: Optional[np.ndarray] = None, - direction: Optional[np.ndarray] = None, - data: Optional[np.ndarray] = None): - - if data is not None: - # Use data array directly - data = np.asarray(data) - if data.shape[0] != 2: - raise ValueError(f"Line data must have shape (2, D), got {data.shape}") - line_data = data.copy() - elif origin is not None and direction is not None: - # Construct from origin/direction - origin_arr = np.asarray(origin) - direction_arr = np.asarray(direction) + >>> line = Line([[0, 0, 0], [1, 1, 0]]) + >>> line.origin + array([0., 0., 0.], dtype=float32) - if origin_arr.shape != direction_arr.shape: - raise ValueError(f"Origin and direction must have same shape, got {origin_arr.shape} and {direction_arr.shape}") + Batch construction: - if origin_arr.ndim != 1: - raise ValueError(f"Origin and direction must be 1D arrays, got shape {origin_arr.shape}") + >>> import numpy as np + >>> lines = Line(np.random.rand(50, 2, 3)) + >>> lines.count + 50 + """ - line_data = np.stack([origin_arr, direction_arr], axis=0) + def __init__(self, data=None, *, origin=None, direction=None): + if data is None: + data = np.stack([np.asarray(origin), np.asarray(direction)], axis=-2) else: - raise ValueError("Must provide either 'data' or both 'origin' and 'direction'") - - # Validate shape - if line_data.ndim != 2 or line_data.shape[0] != 2: - raise ValueError(f"Line data must have shape (2, D), got {line_data.shape}") - - # Validate dimensionality - dims = line_data.shape[1] - if dims not in [2, 3]: + data = np.asarray(data) + if data.dtype not in (np.float32, np.float64): + data = data.astype(np.float32) + if not data.flags['C_CONTIGUOUS']: + data = np.ascontiguousarray(data) + dims = data.shape[-1] + if dims not in (2, 3): raise ValueError(f"Line must be 2D or 3D, got {dims} dimensions") - - # Validate dtype - if line_data.dtype not in [np.float32, np.float64]: - # Try to convert - line_data = line_data.astype(np.float32) - - # Validate direction is not zero - direction_norm = np.linalg.norm(line_data[1]) - if direction_norm < 1e-10: - raise ValueError("Line direction vector cannot be zero") - - # Ensure C-contiguous - if not line_data.flags['C_CONTIGUOUS']: - line_data = np.ascontiguousarray(line_data) - - self._data = line_data - self._dims = dims - self._dtype = line_data.dtype + self._init_wrapper(data, PrimitiveType.line, dims) @property def origin(self) -> np.ndarray: - """Get line origin.""" + """Get line origin(s).""" + if self.is_batch: + return self._data[:, 0] return self._data[0] @property def direction(self) -> np.ndarray: - """Get line direction.""" + """Get line direction(s).""" + if self.is_batch: + return self._data[:, 1] return self._data[1] - @property - def data(self) -> np.ndarray: - """Get underlying data array as (2, D) with [origin, direction].""" - return self._data - - @property - def dims(self) -> int: - """Get dimensionality (2 or 3).""" - return self._dims - - @property - def dtype(self) -> np.dtype: - """Get data type (float32 or float64).""" - return self._dtype - @property def normalized_direction(self) -> np.ndarray: - """Get unit direction vector (normalized to length 1).""" - return self._data[1] / np.linalg.norm(self._data[1]) - - @property - def direction_norm(self) -> float: - """Get magnitude (length) of the direction vector.""" - return float(np.linalg.norm(self._data[1])) + """Get unit direction vector(s).""" + d = self.direction + if self.is_batch: + return d / np.linalg.norm(d, axis=-1, keepdims=True) + return d / np.linalg.norm(d) @classmethod def from_points(cls, p1, p2): """ Create line passing through two points. - The direction is computed as (p2 - p1). - Parameters ---------- p1 : array-like - First point on the line, shape (D,) where D is 2 or 3 + First point, shape (D,) p2 : array-like - Second point on the line, shape (D,) + Second point, shape (D,) Returns ------- Line - Line passing through p1 and p2 Examples -------- >>> line = Line.from_points([0, 0], [1, 1]) - >>> line.origin - array([0., 0.], dtype=float32) >>> line.direction array([1., 1.], dtype=float32) """ p1 = np.asarray(p1) p2 = np.asarray(p2) - direction = p2 - p1 - return cls(origin=p1, direction=direction) + return cls(origin=p1, direction=p2 - p1) def __repr__(self) -> str: + if self.is_batch: + return f"Line(batch={self.count}, dims={self._dims}, dtype={self._dtype})" return f"Line(origin={self.origin.tolist()}, direction={self.direction.tolist()}, dtype={self._dtype})" diff --git a/python/src/trueform/_primitives/plane.py b/python/src/trueform/_primitives/plane.py index 7bdd235..33bb231 100644 --- a/python/src/trueform/_primitives/plane.py +++ b/python/src/trueform/_primitives/plane.py @@ -1,205 +1,137 @@ """ Plane primitive -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform """ import numpy as np -from typing import Optional +from .primitive import Primitive, PrimitiveType -class Plane: +class Plane(Primitive): """ - An infinite plane in 3D space. + A plane defined by a normal vector and offset: dot(normal, x) + offset = 0. - Equation: ax + by + cz + d = 0 + The data is stored as a flat array of (D+1) elements: [normal..., offset]. + For 2D planes shape is (3,) or (N, 3), for 3D planes shape is (4,) or (N, 4). Parameters ---------- - coeffs : np.ndarray, optional - Plane coefficients (a, b, c, d) for ax + by + cz + d = 0, shape (4,) - normal : np.ndarray, optional - Normal vector, shape (3,). Must provide with origin. - origin : np.ndarray, optional - Point on plane, shape (3,). Must provide with normal. + data : array-like, optional + Plane coefficients. Shape (D+1,) for a single plane, (N, D+1) for a + batch, where D is 2 or 3. + normal : array-like, optional + Normal vector(s). Shape (D,) for single, (N, D) for batch. + offset : float or array-like, optional + Offset scalar(s). Scalar for single, shape (N,) for batch. + + Either ``data`` or both ``normal`` and ``offset`` must be provided. Examples -------- - >>> import numpy as np >>> from trueform import Plane - >>> # Using coefficients - >>> plane = Plane(coeffs=[0, 0, 1, -5]) # z = 5 plane - >>> # Using normal and origin - >>> plane = Plane(normal=[0, 0, 1], origin=[0, 0, 5]) # z = 5 plane + >>> plane = Plane(normal=[0, 0, 1], offset=-5.0) >>> plane.normal array([0., 0., 1.], dtype=float32) >>> plane.offset -5.0 - """ - - def __init__(self, - coeffs: Optional[np.ndarray] = None, - normal: Optional[np.ndarray] = None, - origin: Optional[np.ndarray] = None): - - if coeffs is not None: - # Use coefficients directly and normalize - coeffs = np.asarray(coeffs) - - if coeffs.ndim != 1: - raise ValueError(f"Plane coefficients must be 1D array, got shape {coeffs.shape}") - - if coeffs.shape[0] != 4: - raise ValueError(f"Plane coefficients must have 4 elements (a, b, c, d), got {coeffs.shape[0]}") - - dims = 3 # Plane is 3D only - - # Normalize the normal vector and scale offset accordingly - normal = coeffs[:-1] - offset = coeffs[-1] - normal_norm = np.linalg.norm(normal) - - if normal_norm < 1e-10: - raise ValueError("Plane normal vector cannot be zero") - - # Normalize: divide both normal and offset by the norm - normalized_normal = normal / normal_norm - normalized_offset = offset / normal_norm - - data = np.concatenate([normalized_normal, [normalized_offset]]) - - elif normal is not None and origin is not None: - # Construct from normal and origin - normal_arr = np.asarray(normal) - origin_arr = np.asarray(origin) - - if normal_arr.shape != origin_arr.shape: - raise ValueError(f"Normal and origin must have same shape, got {normal_arr.shape} and {origin_arr.shape}") - - if normal_arr.ndim != 1: - raise ValueError(f"Normal and origin must be 1D arrays, got shape {normal_arr.shape}") - - dims = normal_arr.shape[0] - if dims != 3: - raise ValueError(f"Plane must be 3D, got {dims} dimensions. Normal and origin must have shape (3,)") - - # Normalize the normal vector - normal_norm = np.linalg.norm(normal_arr) - if normal_norm < 1e-10: - raise ValueError("Plane normal vector cannot be zero") - - normalized_normal = normal_arr / normal_norm + >>> plane = Plane([0, 0, 1, -5]) + >>> plane.normal + array([0., 0., 1.], dtype=float32) - # Compute offset: d = -dot(normalized_normal, origin) - offset = -np.dot(normalized_normal, origin_arr) + Batch construction: - # Store as [*normalized_normal, offset] - data = np.concatenate([normalized_normal, [offset]]) + >>> import numpy as np + >>> planes = Plane(np.random.rand(50, 4)) + >>> planes.count + 50 + """ + def __init__(self, data=None, *, normal=None, offset=None, origin=None): + if data is None: + normal = np.asarray(normal) + if origin is not None: + origin = np.asarray(origin) + offset = -np.dot(normal, origin) + offset = np.asarray(offset) + if normal.ndim == 1: + data = np.append(normal, offset) + else: + data = np.concatenate( + [normal, offset[..., np.newaxis]], axis=-1) else: - raise ValueError("Must provide either 'coeffs' or both 'normal' and 'origin'") - - # Validate dtype - if data.dtype not in [np.float32, np.float64]: - # Try to convert + data = np.asarray(data) + if data.dtype not in (np.float32, np.float64): data = data.astype(np.float32) - - # Ensure C-contiguous if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data) - - self._data = data - self._dims = dims - self._dtype = data.dtype - - @property - def coeffs(self) -> np.ndarray: - """Get plane coefficients (a, b, c[, d]).""" - return self._data + dims = data.shape[-1] - 1 + if dims != 3: + raise ValueError(f"Plane requires 3D, got {dims} dimensions") + self._init_wrapper(data, PrimitiveType.plane, dims) @property def normal(self) -> np.ndarray: - """Get normalized normal vector (unit length).""" - return self._data[:-1] + """Get normal vector(s).""" + return self._data[..., :-1] @property - def offset(self) -> float: - """Get offset coefficient d.""" + def offset(self): + """Get offset. Returns scalar for single, array for batch.""" + if self.is_batch: + return self._data[..., -1] return float(self._data[-1]) @property - def data(self) -> np.ndarray: - """Get underlying data array.""" + def coeffs(self) -> np.ndarray: + """Get full coefficient array [normal..., offset].""" return self._data - @property - def dims(self) -> int: - """Get dimensionality (always 3 for Plane).""" - return self._dims - - @property - def dtype(self) -> np.dtype: - """Get data type (float32 or float64).""" - return self._dtype - @classmethod def from_point_normal(cls, origin, normal): """ Create plane from a point and normal vector. - This is equivalent to Plane(normal=..., origin=...) but as a classmethod - for consistency with other primitives' from_* constructors. + Computes offset as -dot(normal, origin). Parameters ---------- origin : array-like - Point on the plane, shape (3,) + Point on the plane, shape (D,) normal : array-like - Normal vector to the plane, shape (3,) + Normal vector, shape (D,) Returns ------- Plane - Plane passing through origin with given normal Examples -------- >>> plane = Plane.from_point_normal([0, 0, 5], [0, 0, 1]) - >>> plane.normal - array([0., 0., 1.], dtype=float32) >>> plane.offset -5.0 """ - return cls(normal=normal, origin=origin) + origin = np.asarray(origin) + normal = np.asarray(normal) + offset = -np.dot(normal, origin) + return cls(normal=normal, offset=offset) @classmethod def from_points(cls, p1, p2, p3): """ - Create plane passing through three points. - - The normal vector is computed as (p2 - p1) × (p3 - p1). + Create plane passing through three points (3D only). Parameters ---------- - p1 : array-like - First point on the plane, shape (3,) - p2 : array-like - Second point on the plane, shape (3,) - p3 : array-like - Third point on the plane, shape (3,) + p1, p2, p3 : array-like + Points on the plane, each shape (3,) Returns ------- Plane - Plane passing through the three points - - Raises - ------ - ValueError - If points are collinear (don't define a unique plane) Examples -------- @@ -207,23 +139,15 @@ def from_points(cls, p1, p2, p3): >>> plane.normal array([0., 0., 1.], dtype=float32) """ - p1 = np.asarray(p1) - p2 = np.asarray(p2) - p3 = np.asarray(p3) - - if p1.shape[0] != 3 or p2.shape[0] != 3 or p3.shape[0] != 3: - raise ValueError(f"Plane.from_points requires 3D points, got shapes {p1.shape}, {p2.shape}, {p3.shape}") - - # Compute normal via cross product - v1 = p2 - p1 - v2 = p3 - p1 - normal = np.cross(v1, v2) - - # Check if points are collinear - if np.linalg.norm(normal) < 1e-10: - raise ValueError("Points are collinear and do not define a unique plane") - - return cls(normal=normal, origin=p1) + p1, p2, p3 = np.asarray(p1), np.asarray(p2), np.asarray(p3) + normal = np.cross(p2 - p1, p3 - p1) + norm = np.linalg.norm(normal) + if norm > 0: + normal = normal / norm + offset = -np.dot(normal, p1) + return cls(normal=normal, offset=offset) def __repr__(self) -> str: - return f"Plane(coeffs={self._data.tolist()}, {self._dims}D, dtype={self._dtype})" + if self.is_batch: + return f"Plane(batch={self.count}, dims={self._dims}, dtype={self._dtype})" + return f"Plane(normal={self.normal.tolist()}, offset={self.offset}, dtype={self._dtype})" diff --git a/python/src/trueform/_primitives/point.py b/python/src/trueform/_primitives/point.py index f755069..dfde050 100644 --- a/python/src/trueform/_primitives/point.py +++ b/python/src/trueform/_primitives/point.py @@ -1,23 +1,26 @@ """ Point primitive -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform """ import numpy as np +from .primitive import Primitive, PrimitiveType -class Point: +class Point(Primitive): """ A point in 2D or 3D space. Parameters ---------- - coords : np.ndarray - Coordinates, shape (D,) where D is 2 or 3, dtype float32 or float64 + data : array-like + Coordinates. Shape (D,) for a single point, (N, D) for a batch, + where D is 2 or 3. Dtype float32 or float64 (other dtypes are + cast to float32). Examples -------- @@ -28,32 +31,26 @@ class Point: 3 >>> p.coords array([1., 2., 3.], dtype=float32) - """ - - def __init__(self, coords: np.ndarray): - # Convert to numpy if needed - coords = np.asarray(coords) - - # Validate shape - if coords.ndim != 1: - raise ValueError(f"Point must be 1D array, got shape {coords.shape}") - - # Validate dimensionality - if coords.shape[0] not in [2, 3]: - raise ValueError(f"Point must be 2D or 3D, got {coords.shape[0]} dimensions") - # Validate dtype - if coords.dtype not in [np.float32, np.float64]: - # Try to convert - coords = coords.astype(np.float32) + Batch construction: - # Ensure C-contiguous - if not coords.flags['C_CONTIGUOUS']: - coords = np.ascontiguousarray(coords) + >>> pts = Point(np.random.rand(100, 3)) + >>> pts.is_batch + True + >>> pts.count + 100 + """ - self._data = coords - self._dims = coords.shape[0] - self._dtype = coords.dtype + def __init__(self, data): + data = np.asarray(data) + if data.dtype not in (np.float32, np.float64): + data = data.astype(np.float32) + if not data.flags['C_CONTIGUOUS']: + data = np.ascontiguousarray(data) + dims = data.shape[-1] + if dims not in (2, 3): + raise ValueError(f"Point must be 2D or 3D, got {dims} dimensions") + self._init_wrapper(data, PrimitiveType.point, dims) @property def coords(self) -> np.ndarray: @@ -61,101 +58,35 @@ def coords(self) -> np.ndarray: return self._data @property - def data(self) -> np.ndarray: - """Get underlying data array.""" - return self._data - - @property - def dims(self) -> int: - """Get dimensionality (2 or 3).""" - return self._dims - - @property - def dtype(self) -> np.dtype: - """Get data type (float32 or float64).""" - return self._dtype - - @property - def x(self) -> float: - """Get x coordinate.""" + def x(self): + """Get x coordinate. Returns scalar for single, array for batch.""" + if self.is_batch: + return self._data[..., 0] return float(self._data[0]) @property - def y(self) -> float: - """Get y coordinate.""" + def y(self): + """Get y coordinate. Returns scalar for single, array for batch.""" + if self.is_batch: + return self._data[..., 1] return float(self._data[1]) @property - def z(self) -> float: + def z(self): """ - Get z coordinate. - - For 2D points, returns 0.0. + Get z coordinate. Returns scalar for single, array for batch. - Returns - ------- - float - Z coordinate, or 0.0 if point is 2D + For 2D points, returns 0.0 (or array of zeros for batch). """ if self._dims < 3: + if self.is_batch: + return np.zeros(self.count, dtype=self._dtype) return 0.0 + if self.is_batch: + return self._data[..., 2] return float(self._data[2]) - @classmethod - def from_xy(cls, x: float, y: float, dtype=np.float32): - """ - Create a 2D point from x and y coordinates. - - Parameters - ---------- - x : float - X coordinate - y : float - Y coordinate - dtype : numpy dtype, optional - Data type for coordinates (float32 or float64). Default is float32. - - Returns - ------- - Point - 2D point at (x, y) - - Examples - -------- - >>> pt = Point.from_xy(1.0, 2.0) - >>> pt.coords - array([1., 2.], dtype=float32) - """ - return cls(np.array([x, y], dtype=dtype)) - - @classmethod - def from_xyz(cls, x: float, y: float, z: float, dtype=np.float32): - """ - Create a 3D point from x, y, and z coordinates. - - Parameters - ---------- - x : float - X coordinate - y : float - Y coordinate - z : float - Z coordinate - dtype : numpy dtype, optional - Data type for coordinates (float32 or float64). Default is float32. - - Returns - ------- - Point - 3D point at (x, y, z) - - Examples - -------- - >>> pt = Point.from_xyz(1.0, 2.0, 3.0) - >>> pt.coords - array([1., 2., 3.], dtype=float32) - """ - return cls(np.array([x, y, z], dtype=dtype)) - def __repr__(self) -> str: + if self.is_batch: + return f"Point(batch={self.count}, dims={self._dims}, dtype={self._dtype})" return f"Point({self._data.tolist()}, dtype={self._dtype})" diff --git a/python/src/trueform/_primitives/polygon.py b/python/src/trueform/_primitives/polygon.py index f132d09..19f595c 100644 --- a/python/src/trueform/_primitives/polygon.py +++ b/python/src/trueform/_primitives/polygon.py @@ -1,102 +1,68 @@ """ Polygon primitive -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform """ import numpy as np +from .primitive import Primitive, PrimitiveType -class Polygon: +class Polygon(Primitive): """ A polygon defined by ordered vertices. Parameters ---------- - vertices : np.ndarray - Ordered vertices, shape (N, D) where N >= 3, D is 2 or 3, dtype float32 or float64 + data : array-like + Vertex data. Shape (V, D) for a single polygon, (N, V, D) for a + batch, where V >= 3 and D is 2 or 3. Examples -------- - >>> import numpy as np >>> from trueform import Polygon - >>> tri = Polygon([[0, 0], [1, 0], [0.5, 1]]) - >>> tri.num_vertices - 3 - >>> tri.dims + >>> quad = Polygon([[0, 0], [1, 0], [1, 1], [0, 1]]) + >>> quad.num_vertices + 4 + >>> quad.dims 2 - """ - - def __init__(self, vertices: np.ndarray): - # Convert to numpy if needed - vertices = np.asarray(vertices) - - # Validate shape - if vertices.ndim != 2: - raise ValueError( - f"Polygon vertices must be 2D array, got shape {vertices.shape}") - num_vertices = vertices.shape[0] - if num_vertices < 3: - raise ValueError( - f"Polygon must have at least 3 vertices, got {num_vertices}") + Batch construction: - # Validate dimensionality - dims = vertices.shape[1] - if dims not in [2, 3]: - raise ValueError( - f"Polygon must be 2D or 3D, got {dims} dimensions") - - # Validate dtype - if vertices.dtype not in [np.float32, np.float64]: - # Try to convert - vertices = vertices.astype(np.float32) - - # Ensure C-contiguous - if not vertices.flags['C_CONTIGUOUS']: - vertices = np.ascontiguousarray(vertices) + >>> import numpy as np + >>> polys = Polygon(np.random.rand(50, 5, 3)) + >>> polys.count + 50 + """ - self._data = vertices - self._dims = dims - self._dtype = vertices.dtype + def __init__(self, data): + data = np.asarray(data) + if data.dtype not in (np.float32, np.float64): + data = data.astype(np.float32) + if not data.flags['C_CONTIGUOUS']: + data = np.ascontiguousarray(data) + dims = data.shape[-1] + if dims not in (2, 3): + raise ValueError(f"Polygon must be 2D or 3D, got {dims} dimensions") + self._init_wrapper(data, PrimitiveType.polygon, dims) @property def vertices(self) -> np.ndarray: - """Get all vertices as (N, D) array.""" - return self._data - - @property - def data(self) -> np.ndarray: - """Get underlying data array.""" + """Get vertices as (V, D) or (N, V, D) array.""" return self._data @property def num_vertices(self) -> int: - """Get number of vertices.""" - return self._data.shape[0] - - @property - def dims(self) -> int: - """Get dimensionality (2 or 3).""" - return self._dims - - @property - def dtype(self) -> np.dtype: - """Get data type (float32 or float64).""" - return self._dtype + """Get number of vertices per polygon.""" + return self._data.shape[-2] def __repr__(self) -> str: - n_verts = self.num_vertices - if n_verts <= 5: - # Show actual vertices for small polygons + nv = self.num_vertices + if self.is_batch: + return f"Polygon(batch={self.count}, vertices={nv}, dims={self._dims}, dtype={self._dtype})" + if nv <= 5: return f"Polygon({self._data.tolist()}, dtype={self._dtype})" - else: - # Show summary for large polygons - return f"Polygon({n_verts} vertices, {self._dims}D, dtype={self._dtype})" - - def __len__(self) -> int: - """Get number of vertices (for len(polygon) syntax).""" - return self.num_vertices + return f"Polygon({nv} vertices, dims={self._dims}, dtype={self._dtype})" diff --git a/python/src/trueform/_primitives/primitive.py b/python/src/trueform/_primitives/primitive.py new file mode 100644 index 0000000..be7c2a7 --- /dev/null +++ b/python/src/trueform/_primitives/primitive.py @@ -0,0 +1,112 @@ +""" +Primitive base class and PrimitiveType enum + +Copyright (c) 2025 Ziga Sajovic, XLAB +Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. +Commercial licensing available via info@polydera.com. +https://github.com/polydera/trueform +""" + +import enum +import numpy as np +from .. import _trueform + + +class PrimitiveType(enum.IntEnum): + point = 0 + segment = 1 + triangle = 2 + ray = 3 + line = 4 + plane = 5 + aabb = 6 + polygon = 7 + + +# Map (dtype, dims) -> C++ wrapper class +_WRAPPER_MAP = { + (np.dtype('float32'), 2): _trueform.core.PrimitiveWrapperFloat2D, + (np.dtype('float32'), 3): _trueform.core.PrimitiveWrapperFloat3D, + (np.dtype('float64'), 2): _trueform.core.PrimitiveWrapperDouble2D, + (np.dtype('float64'), 3): _trueform.core.PrimitiveWrapperDouble3D, +} + + +def _make_wrapper(data, prim_type, dims): + """Create the correct C++ primitive_wrapper variant.""" + key = (data.dtype, dims) + cls = _WRAPPER_MAP.get(key) + if cls is None: + raise ValueError(f"Unsupported dtype/dims combination: {dtype}, {dims}D") + return cls(int(prim_type), data) + + +class Primitive: + """ + Base class for all geometric primitives. + + Wraps a C++ primitive_wrapper that holds the type enum and numpy data together, + enabling efficient dispatch in C++ without per-type Python→C++ boundary crossings. + + Subclasses: Point, Segment, Triangle, Ray, Line, Plane, AABB, Polygon + """ + + __slots__ = ('_wrapper', '_data', '_dims', '_dtype') + + def _init_wrapper(self, data: np.ndarray, prim_type: PrimitiveType, dims: int): + """Initialize the C++ wrapper from validated data.""" + self._data = data + self._dims = dims + self._dtype = data.dtype + self._wrapper = _make_wrapper(data, prim_type, dims) + + @property + def type(self) -> PrimitiveType: + """Get the primitive type enum.""" + return PrimitiveType(self._wrapper.type_int()) + + @property + def data(self) -> np.ndarray: + """Get underlying numpy data array.""" + return self._data + + @property + def array(self) -> np.ndarray: + """Alias for data.""" + return self._data + + @property + def dims(self) -> int: + """Get dimensionality (2 or 3).""" + return self._dims + + @property + def dtype(self) -> np.dtype: + """Get data type (float32 or float64).""" + return self._dtype + + @property + def is_batch(self) -> bool: + """Whether this holds a batch of primitives.""" + return self._wrapper.is_batch() + + @property + def count(self) -> int: + """Number of primitives (1 for single, N for batch).""" + return self._wrapper.count() + + def __len__(self) -> int: + return self.count + + def __getitem__(self, key): + """ + NumPy-style indexing on the batch dimension. + + Returns the same primitive type. Integer indexing returns a single + primitive; slice/array indexing returns a batch. + + Only supported on batches. + """ + if not self.is_batch: + raise IndexError("Cannot index a single primitive; indexing is only supported on batches") + return type(self)(self._data[key]) diff --git a/python/src/trueform/_primitives/ray.py b/python/src/trueform/_primitives/ray.py index d800fd2..cf526c4 100644 --- a/python/src/trueform/_primitives/ray.py +++ b/python/src/trueform/_primitives/ray.py @@ -1,165 +1,119 @@ """ Ray primitive -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform """ import numpy as np -from typing import Optional +from .primitive import Primitive, PrimitiveType -class Ray: +class Ray(Primitive): """ - A ray defined by origin and direction. + A ray defined by an origin and direction. - ⚠️ **Important:** The direction vector is stored as-is, NOT normalized. - Use the `normalized_direction` property if you need a unit vector. + The direction vector is stored as-is, NOT normalized. Parameters ---------- - origin : np.ndarray, optional - Ray origin, shape (D,) where D is 2 or 3 - direction : np.ndarray, optional - Ray direction, shape (D,) - data : np.ndarray, optional - Alternative: shape (2, D) with [origin, direction] + data : array-like, optional + Ray data. Shape (2, D) for a single ray, (N, 2, D) for a batch, + where D is 2 or 3. Row 0 is origin, row 1 is direction. + origin : array-like, optional + Origin point(s). Shape (D,) for single, (N, D) for batch. + direction : array-like, optional + Direction vector(s). Shape (D,) for single, (N, D) for batch. + + Either ``data`` or both ``origin`` and ``direction`` must be provided. Examples -------- - >>> import numpy as np >>> from trueform import Ray - >>> # Using origin/direction >>> ray = Ray(origin=[0, 0, 0], direction=[1, 2, 3]) >>> ray.origin array([0., 0., 0.], dtype=float32) >>> ray.direction array([1., 2., 3.], dtype=float32) - >>> # Using data array - >>> ray = Ray(data=[[0, 0, 0], [1, 1, 0]]) - """ - - def __init__(self, - origin: Optional[np.ndarray] = None, - direction: Optional[np.ndarray] = None, - data: Optional[np.ndarray] = None): - - if data is not None: - # Use data array directly - data = np.asarray(data) - if data.shape[0] != 2: - raise ValueError(f"Ray data must have shape (2, D), got {data.shape}") - ray_data = data.copy() - elif origin is not None and direction is not None: - # Construct from origin/direction - origin_arr = np.asarray(origin) - direction_arr = np.asarray(direction) + >>> ray = Ray([[0, 0, 0], [1, 1, 0]]) + >>> ray.origin + array([0., 0., 0.], dtype=float32) - if origin_arr.shape != direction_arr.shape: - raise ValueError(f"Origin and direction must have same shape, got {origin_arr.shape} and {direction_arr.shape}") + Batch construction: - if origin_arr.ndim != 1: - raise ValueError(f"Origin and direction must be 1D arrays, got shape {origin_arr.shape}") + >>> import numpy as np + >>> rays = Ray(np.random.rand(50, 2, 3)) + >>> rays.count + 50 + """ - ray_data = np.stack([origin_arr, direction_arr], axis=0) + def __init__(self, data=None, *, origin=None, direction=None): + if data is None: + origin = np.asarray(origin) + direction = np.asarray(direction) + data = np.stack([origin, direction], axis=-2) else: - raise ValueError("Must provide either 'data' or both 'origin' and 'direction'") - - # Validate shape - if ray_data.ndim != 2 or ray_data.shape[0] != 2: - raise ValueError(f"Ray data must have shape (2, D), got {ray_data.shape}") - - # Validate dimensionality - dims = ray_data.shape[1] - if dims not in [2, 3]: + data = np.asarray(data) + if data.dtype not in (np.float32, np.float64): + data = data.astype(np.float32) + if not data.flags['C_CONTIGUOUS']: + data = np.ascontiguousarray(data) + dims = data.shape[-1] + if dims not in (2, 3): raise ValueError(f"Ray must be 2D or 3D, got {dims} dimensions") - - # Validate dtype - if ray_data.dtype not in [np.float32, np.float64]: - # Try to convert - ray_data = ray_data.astype(np.float32) - - # Validate direction is not zero - direction_norm = np.linalg.norm(ray_data[1]) - if direction_norm < 1e-10: - raise ValueError("Ray direction vector cannot be zero") - - # Ensure C-contiguous - if not ray_data.flags['C_CONTIGUOUS']: - ray_data = np.ascontiguousarray(ray_data) - - self._data = ray_data - self._dims = dims - self._dtype = ray_data.dtype + self._init_wrapper(data, PrimitiveType.ray, dims) @property def origin(self) -> np.ndarray: - """Get ray origin.""" + """Get ray origin(s).""" + if self.is_batch: + return self._data[:, 0] return self._data[0] @property def direction(self) -> np.ndarray: - """Get ray direction.""" + """Get ray direction(s).""" + if self.is_batch: + return self._data[:, 1] return self._data[1] - @property - def data(self) -> np.ndarray: - """Get underlying data array as (2, D) with [origin, direction].""" - return self._data - - @property - def dims(self) -> int: - """Get dimensionality (2 or 3).""" - return self._dims - - @property - def dtype(self) -> np.dtype: - """Get data type (float32 or float64).""" - return self._dtype - @property def normalized_direction(self) -> np.ndarray: - """Get unit direction vector (normalized to length 1).""" - return self._data[1] / np.linalg.norm(self._data[1]) - - @property - def direction_norm(self) -> float: - """Get magnitude (length) of the direction vector.""" - return float(np.linalg.norm(self._data[1])) + """Get unit direction vector(s).""" + d = self.direction + if self.is_batch: + return d / np.linalg.norm(d, axis=-1, keepdims=True) + return d / np.linalg.norm(d) @classmethod - def from_points(cls, start, through_point): + def from_points(cls, start, through): """ Create ray from start point through another point. - The direction is computed as (through_point - start). - Parameters ---------- start : array-like - Ray origin point, shape (D,) where D is 2 or 3 - through_point : array-like + Ray origin, shape (D,) + through : array-like Point the ray passes through, shape (D,) Returns ------- Ray - Ray starting at start and passing through through_point Examples -------- >>> ray = Ray.from_points([0, 0], [1, 1]) - >>> ray.origin - array([0., 0.], dtype=float32) >>> ray.direction array([1., 1.], dtype=float32) """ start = np.asarray(start) - through_point = np.asarray(through_point) - direction = through_point - start - return cls(origin=start, direction=direction) + through = np.asarray(through) + return cls(origin=start, direction=through - start) def __repr__(self) -> str: + if self.is_batch: + return f"Ray(batch={self.count}, dims={self._dims}, dtype={self._dtype})" return f"Ray(origin={self.origin.tolist()}, direction={self.direction.tolist()}, dtype={self._dtype})" diff --git a/python/src/trueform/_primitives/segment.py b/python/src/trueform/_primitives/segment.py index b82d02e..f11f1d1 100644 --- a/python/src/trueform/_primitives/segment.py +++ b/python/src/trueform/_primitives/segment.py @@ -1,137 +1,102 @@ """ Segment primitive -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform """ import numpy as np +from .primitive import Primitive, PrimitiveType -class Segment: +class Segment(Primitive): """ A line segment defined by two endpoints. Parameters ---------- - endpoints : np.ndarray - Two endpoints, shape (2, D) where D is 2 or 3, dtype float32 or float64 + data : array-like, optional + Endpoint data. Shape (2, D) for a single segment, (N, 2, D) for a + batch, where D is 2 or 3. + start : array-like, optional + Start point(s). Shape (D,) for single, (N, D) for batch. + end : array-like, optional + End point(s). Shape (D,) for single, (N, D) for batch. + + Either ``data`` or both ``start`` and ``end`` must be provided. Examples -------- - >>> import numpy as np >>> from trueform import Segment + >>> seg = Segment(start=[0, 0, 0], end=[1, 1, 1]) + >>> seg.length + 1.7320508... >>> seg = Segment([[0, 0, 0], [1, 1, 1]]) >>> seg.start array([0., 0., 0.], dtype=float32) - >>> seg.end - array([1., 1., 1.], dtype=float32) - """ - def __init__(self, endpoints: np.ndarray): - # Convert to numpy if needed - endpoints = np.asarray(endpoints) + Batch construction: - # Validate shape - if endpoints.ndim != 2: - raise ValueError(f"Segment endpoints must be 2D array, got shape {endpoints.shape}") - - if endpoints.shape[0] != 2: - raise ValueError(f"Segment must have exactly 2 endpoints, got {endpoints.shape[0]}") + >>> import numpy as np + >>> segs = Segment(np.random.rand(50, 2, 3)) + >>> segs.count + 50 + """ - # Validate dimensionality - dims = endpoints.shape[1] - if dims not in [2, 3]: + def __init__(self, data=None, *, start=None, end=None): + if data is None: + data = np.stack([np.asarray(start), np.asarray(end)], axis=-2) + else: + data = np.asarray(data) + if data.dtype not in (np.float32, np.float64): + data = data.astype(np.float32) + if not data.flags['C_CONTIGUOUS']: + data = np.ascontiguousarray(data) + dims = data.shape[-1] + if dims not in (2, 3): raise ValueError(f"Segment must be 2D or 3D, got {dims} dimensions") - - # Validate dtype - if endpoints.dtype not in [np.float32, np.float64]: - # Try to convert - endpoints = endpoints.astype(np.float32) - - # Ensure C-contiguous - if not endpoints.flags['C_CONTIGUOUS']: - endpoints = np.ascontiguousarray(endpoints) - - self._data = endpoints - self._dims = dims - self._dtype = endpoints.dtype - - @property - def endpoints(self) -> np.ndarray: - """Get both endpoints as (2, D) array.""" - return self._data + self._init_wrapper(data, PrimitiveType.segment, dims) @property def start(self) -> np.ndarray: - """Get start point.""" + """Get start point(s).""" + if self.is_batch: + return self._data[:, 0] return self._data[0] @property def end(self) -> np.ndarray: - """Get end point.""" + """Get end point(s).""" + if self.is_batch: + return self._data[:, 1] return self._data[1] @property - def data(self) -> np.ndarray: - """Get underlying data array.""" + def endpoints(self) -> np.ndarray: + """Get both endpoints as (2, D) or (N, 2, D) array.""" return self._data - @property - def dims(self) -> int: - """Get dimensionality (2 or 3).""" - return self._dims - - @property - def dtype(self) -> np.dtype: - """Get data type (float32 or float64).""" - return self._dtype - - @property - def length(self) -> float: - """Get length of the segment.""" - return float(np.linalg.norm(self._data[1] - self._data[0])) - @property def vector(self) -> np.ndarray: """Get direction vector from start to end.""" - return self._data[1] - self._data[0] + return self.end - self.start @property def midpoint(self) -> np.ndarray: """Get midpoint of the segment.""" - return (self._data[0] + self._data[1]) / 2 - - @classmethod - def from_points(cls, start, end): - """ - Create segment from two points. - - Parameters - ---------- - start : array-like - Start point, shape (D,) where D is 2 or 3 - end : array-like - End point, shape (D,) where D is 2 or 3 - - Returns - ------- - Segment - Segment from start to end - - Examples - -------- - >>> seg = Segment.from_points([0, 0], [1, 1]) - >>> seg.start - array([0., 0.], dtype=float32) - >>> seg.end - array([1., 1.], dtype=float32) - """ - start = np.asarray(start) - end = np.asarray(end) - return cls(np.stack([start, end], axis=0)) + return (self.start + self.end) / 2 + + @property + def length(self): + """Get length of the segment. Returns scalar for single, array for batch.""" + v = self.vector + if self.is_batch: + return np.linalg.norm(v, axis=-1) + return float(np.linalg.norm(v)) def __repr__(self) -> str: + if self.is_batch: + return f"Segment(batch={self.count}, dims={self._dims}, dtype={self._dtype})" return f"Segment(start={self.start.tolist()}, end={self.end.tolist()}, dtype={self._dtype})" diff --git a/python/src/trueform/_primitives/triangle.py b/python/src/trueform/_primitives/triangle.py new file mode 100644 index 0000000..88e7a15 --- /dev/null +++ b/python/src/trueform/_primitives/triangle.py @@ -0,0 +1,93 @@ +""" +Triangle primitive + +Copyright (c) 2025 Ziga Sajovic, XLAB +Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. +Commercial licensing available via info@polydera.com. +https://github.com/polydera/trueform +""" + +import numpy as np +from .primitive import Primitive, PrimitiveType + + +class Triangle(Primitive): + """ + A triangle defined by three vertices. + + Parameters + ---------- + data : array-like, optional + Vertex data. Shape (3, D) for a single triangle, (N, 3, D) for a + batch, where D is 2 or 3. + a : array-like, optional + First vertex. Shape (D,) for single, (N, D) for batch. + b : array-like, optional + Second vertex. Shape (D,) for single, (N, D) for batch. + c : array-like, optional + Third vertex. Shape (D,) for single, (N, D) for batch. + + Either ``data`` or all of ``a``, ``b``, and ``c`` must be provided. + + Examples + -------- + >>> from trueform import Triangle + >>> tri = Triangle(a=[0, 0, 0], b=[1, 0, 0], c=[0, 1, 0]) + >>> tri.a + array([0., 0., 0.], dtype=float32) + + Batch construction: + + >>> import numpy as np + >>> tris = Triangle(np.random.rand(50, 3, 3)) + >>> tris.count + 50 + """ + + def __init__(self, data=None, *, a=None, b=None, c=None): + if data is None: + data = np.stack([np.asarray(a), np.asarray(b), np.asarray(c)], axis=-2) + else: + data = np.asarray(data) + if data.dtype not in (np.float32, np.float64): + data = data.astype(np.float32) + if not data.flags['C_CONTIGUOUS']: + data = np.ascontiguousarray(data) + dims = data.shape[-1] + if dims not in (2, 3): + raise ValueError(f"Triangle must be 2D or 3D, got {dims} dimensions") + if data.shape[-2] != 3: + raise ValueError( + f"Triangle must have 3 vertices (shape[-2] == 3), got {data.shape[-2]}") + self._init_wrapper(data, PrimitiveType.triangle, dims) + + @property + def a(self) -> np.ndarray: + """Get first vertex.""" + if self.is_batch: + return self._data[:, 0] + return self._data[0] + + @property + def b(self) -> np.ndarray: + """Get second vertex.""" + if self.is_batch: + return self._data[:, 1] + return self._data[1] + + @property + def c(self) -> np.ndarray: + """Get third vertex.""" + if self.is_batch: + return self._data[:, 2] + return self._data[2] + + @property + def vertices(self) -> np.ndarray: + """Get all three vertices as (3, D) or (N, 3, D) array.""" + return self._data + + def __repr__(self) -> str: + if self.is_batch: + return f"Triangle(batch={self.count}, dims={self._dims}, dtype={self._dtype})" + return f"Triangle(a={self.a.tolist()}, b={self.b.tolist()}, c={self.c.tolist()}, dtype={self._dtype})" diff --git a/python/src/trueform/_spatial/_dispatch.py b/python/src/trueform/_spatial/_dispatch.py index 880a25b..964d1d8 100644 --- a/python/src/trueform/_spatial/_dispatch.py +++ b/python/src/trueform/_spatial/_dispatch.py @@ -1,5 +1,5 @@ """ -Dispatch tables for tf::spatial operations (forms and primitives). +Dispatch tables for tf::spatial form x form operations. Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. @@ -9,110 +9,32 @@ from .mesh import Mesh from .edge_mesh import EdgeMesh from .point_cloud import PointCloud -from .._primitives import Point, Segment, Polygon, Ray, Line, Plane # ============================================================================= -# NEIGHBOR_SEARCH: FormType -> {PrimitiveType: func_template} +# INTERSECTS_FORM_FORM: (FormType0, FormType1) -> (func_template, needs_swap) # ============================================================================= -NEIGHBOR_SEARCH = { - Mesh: { - Point: "neighbor_search_mesh_point_{}", - Segment: "neighbor_search_mesh_segment_{}", - Polygon: "neighbor_search_mesh_polygon_{}", - Ray: "neighbor_search_mesh_ray_{}", - Line: "neighbor_search_mesh_line_{}", - Plane: "neighbor_search_mesh_plane_{}", - }, - EdgeMesh: { - Point: "neighbor_search_edge_mesh_point_{}", - Segment: "neighbor_search_edge_mesh_segment_{}", - Polygon: "neighbor_search_edge_mesh_polygon_{}", - Ray: "neighbor_search_edge_mesh_ray_{}", - Line: "neighbor_search_edge_mesh_line_{}", - Plane: "neighbor_search_edge_mesh_plane_{}", - }, - PointCloud: { - Point: "neighbor_search_point_{}", - Segment: "neighbor_search_segment_{}", - Polygon: "neighbor_search_polygon_{}", - Ray: "neighbor_search_ray_{}", - Line: "neighbor_search_line_{}", - Plane: "neighbor_search_plane_{}", - }, -} +INTERSECTS_FORM_FORM = { + # PointCloud x PointCloud + (PointCloud, PointCloud): ("intersects_point_cloud_point_cloud_{}", False), + # EdgeMesh x EdgeMesh + (EdgeMesh, EdgeMesh): ("intersects_edge_mesh_edge_mesh_{}", False), -# ============================================================================= -# NEIGHBOR_SEARCH_KNN: FormType -> {PrimitiveType: func_template} -# ============================================================================= -NEIGHBOR_SEARCH_KNN = { - Mesh: { - Point: "neighbor_search_mesh_knn_point_{}", - Segment: "neighbor_search_mesh_knn_segment_{}", - Polygon: "neighbor_search_mesh_knn_polygon_{}", - Ray: "neighbor_search_mesh_knn_ray_{}", - Line: "neighbor_search_mesh_knn_line_{}", - Plane: "neighbor_search_mesh_knn_plane_{}", - }, - EdgeMesh: { - Point: "neighbor_search_edge_mesh_knn_point_{}", - Segment: "neighbor_search_edge_mesh_knn_segment_{}", - Polygon: "neighbor_search_edge_mesh_knn_polygon_{}", - Ray: "neighbor_search_edge_mesh_knn_ray_{}", - Line: "neighbor_search_edge_mesh_knn_line_{}", - Plane: "neighbor_search_edge_mesh_knn_plane_{}", - }, - PointCloud: { - Point: "neighbor_search_knn_point_{}", - Segment: "neighbor_search_knn_segment_{}", - Polygon: "neighbor_search_knn_polygon_{}", - Ray: "neighbor_search_knn_ray_{}", - Line: "neighbor_search_knn_line_{}", - Plane: "neighbor_search_knn_plane_{}", - }, -} + # EdgeMesh x PointCloud + (EdgeMesh, PointCloud): ("intersects_edge_mesh_point_cloud_{}", False), + (PointCloud, EdgeMesh): ("intersects_edge_mesh_point_cloud_{}", True), + # Mesh x PointCloud + (Mesh, PointCloud): ("intersects_mesh_point_cloud_{}", False), + (PointCloud, Mesh): ("intersects_mesh_point_cloud_{}", True), -# ============================================================================= -# GATHER_IDS_FORM_PRIM: (FormType, PrimitiveType) -> (func_template, needs_swap) -# ============================================================================= -GATHER_IDS_FORM_PRIM = { - # Mesh combinations - (Mesh, Point): ("gather_ids_point_{}", False), - (Point, Mesh): ("gather_ids_point_{}", True), - (Mesh, Segment): ("gather_ids_segment_{}", False), - (Segment, Mesh): ("gather_ids_segment_{}", True), - (Mesh, Polygon): ("gather_ids_polygon_{}", False), - (Polygon, Mesh): ("gather_ids_polygon_{}", True), - (Mesh, Ray): ("gather_ids_ray_{}", False), - (Ray, Mesh): ("gather_ids_ray_{}", True), - (Mesh, Line): ("gather_ids_line_{}", False), - (Line, Mesh): ("gather_ids_line_{}", True), - - # EdgeMesh combinations - (EdgeMesh, Point): ("gather_ids_point_{}", False), - (Point, EdgeMesh): ("gather_ids_point_{}", True), - (EdgeMesh, Segment): ("gather_ids_segment_{}", False), - (Segment, EdgeMesh): ("gather_ids_segment_{}", True), - (EdgeMesh, Polygon): ("gather_ids_polygon_{}", False), - (Polygon, EdgeMesh): ("gather_ids_polygon_{}", True), - (EdgeMesh, Ray): ("gather_ids_ray_{}", False), - (Ray, EdgeMesh): ("gather_ids_ray_{}", True), - (EdgeMesh, Line): ("gather_ids_line_{}", False), - (Line, EdgeMesh): ("gather_ids_line_{}", True), - - # PointCloud combinations - (PointCloud, Point): ("gather_ids_point_{}", False), - (Point, PointCloud): ("gather_ids_point_{}", True), - (PointCloud, Segment): ("gather_ids_segment_{}", False), - (Segment, PointCloud): ("gather_ids_segment_{}", True), - (PointCloud, Polygon): ("gather_ids_polygon_{}", False), - (Polygon, PointCloud): ("gather_ids_polygon_{}", True), - (PointCloud, Ray): ("gather_ids_ray_{}", False), - (Ray, PointCloud): ("gather_ids_ray_{}", True), - (PointCloud, Line): ("gather_ids_line_{}", False), - (Line, PointCloud): ("gather_ids_line_{}", True), + # Mesh x EdgeMesh + (Mesh, EdgeMesh): ("intersects_mesh_edge_mesh_{}", False), + (EdgeMesh, Mesh): ("intersects_mesh_edge_mesh_{}", True), + + # Mesh x Mesh + (Mesh, Mesh): ("intersects_mesh_mesh_{}", False), } @@ -143,91 +65,6 @@ } -# ============================================================================= -# INTERSECTS_FORM_PRIM: (FormType, PrimitiveType) -> (func_template, needs_swap) -# ============================================================================= -INTERSECTS_FORM_PRIM = { - # Mesh combinations - (Mesh, Point): ("intersects_mesh_point_{}", False), - (Point, Mesh): ("intersects_mesh_point_{}", True), - (Mesh, Segment): ("intersects_mesh_segment_{}", False), - (Segment, Mesh): ("intersects_mesh_segment_{}", True), - (Mesh, Polygon): ("intersects_mesh_polygon_{}", False), - (Polygon, Mesh): ("intersects_mesh_polygon_{}", True), - (Mesh, Ray): ("intersects_mesh_ray_{}", False), - (Ray, Mesh): ("intersects_mesh_ray_{}", True), - (Mesh, Line): ("intersects_mesh_line_{}", False), - (Line, Mesh): ("intersects_mesh_line_{}", True), - (Mesh, Plane): ("intersects_mesh_plane_{}", False), - (Plane, Mesh): ("intersects_mesh_plane_{}", True), - - # EdgeMesh combinations - (EdgeMesh, Point): ("intersects_edge_mesh_point_{}", False), - (Point, EdgeMesh): ("intersects_edge_mesh_point_{}", True), - (EdgeMesh, Segment): ("intersects_edge_mesh_segment_{}", False), - (Segment, EdgeMesh): ("intersects_edge_mesh_segment_{}", True), - (EdgeMesh, Polygon): ("intersects_edge_mesh_polygon_{}", False), - (Polygon, EdgeMesh): ("intersects_edge_mesh_polygon_{}", True), - (EdgeMesh, Ray): ("intersects_edge_mesh_ray_{}", False), - (Ray, EdgeMesh): ("intersects_edge_mesh_ray_{}", True), - (EdgeMesh, Line): ("intersects_edge_mesh_line_{}", False), - (Line, EdgeMesh): ("intersects_edge_mesh_line_{}", True), - (EdgeMesh, Plane): ("intersects_edge_mesh_plane_{}", False), - (Plane, EdgeMesh): ("intersects_edge_mesh_plane_{}", True), - - # PointCloud combinations - (PointCloud, Point): ("intersects_point_cloud_point_{}", False), - (Point, PointCloud): ("intersects_point_cloud_point_{}", True), - (PointCloud, Segment): ("intersects_point_cloud_segment_{}", False), - (Segment, PointCloud): ("intersects_point_cloud_segment_{}", True), - (PointCloud, Polygon): ("intersects_point_cloud_polygon_{}", False), - (Polygon, PointCloud): ("intersects_point_cloud_polygon_{}", True), - (PointCloud, Ray): ("intersects_point_cloud_ray_{}", False), - (Ray, PointCloud): ("intersects_point_cloud_ray_{}", True), - (PointCloud, Line): ("intersects_point_cloud_line_{}", False), - (Line, PointCloud): ("intersects_point_cloud_line_{}", True), - (PointCloud, Plane): ("intersects_point_cloud_plane_{}", False), - (Plane, PointCloud): ("intersects_point_cloud_plane_{}", True), -} - - -# ============================================================================= -# INTERSECTS_FORM_FORM: (FormType0, FormType1) -> (func_template, needs_swap) -# ============================================================================= -INTERSECTS_FORM_FORM = { - # PointCloud x PointCloud - (PointCloud, PointCloud): ("intersects_point_cloud_point_cloud_{}", False), - - # EdgeMesh x EdgeMesh - (EdgeMesh, EdgeMesh): ("intersects_edge_mesh_edge_mesh_{}", False), - - # EdgeMesh x PointCloud - (EdgeMesh, PointCloud): ("intersects_edge_mesh_point_cloud_{}", False), - (PointCloud, EdgeMesh): ("intersects_edge_mesh_point_cloud_{}", True), - - # Mesh x PointCloud - (Mesh, PointCloud): ("intersects_mesh_point_cloud_{}", False), - (PointCloud, Mesh): ("intersects_mesh_point_cloud_{}", True), - - # Mesh x EdgeMesh - (Mesh, EdgeMesh): ("intersects_mesh_edge_mesh_{}", False), - (EdgeMesh, Mesh): ("intersects_mesh_edge_mesh_{}", True), - - # Mesh x Mesh - (Mesh, Mesh): ("intersects_mesh_mesh_{}", False), -} - - -# ============================================================================= -# RAY_CAST: FormType -> func_template -# ============================================================================= -RAY_CAST = { - PointCloud: "ray_cast_point_cloud_{}", - Mesh: "ray_cast_mesh_{}", - EdgeMesh: "ray_cast_edge_mesh_{}", -} - - # ============================================================================= # NEIGHBOR_SEARCH_FORM_FORM: (FormType0, FormType1) -> (func_template, needs_swap) # ============================================================================= diff --git a/python/src/trueform/_spatial/closest_point.py b/python/src/trueform/_spatial/closest_point.py new file mode 100644 index 0000000..72332b6 --- /dev/null +++ b/python/src/trueform/_spatial/closest_point.py @@ -0,0 +1,97 @@ +""" +Closest metric point pair API + +Copyright (c) 2025 Ziga Sajovic, XLAB +Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. +Commercial licensing available via info@polydera.com. +https://github.com/polydera/trueform +""" + +from typing import Any, Tuple +import numpy as np +from .. import _trueform +from .._primitives.primitive import Primitive +from .._dispatch import InputMeta, build_suffix + + +def closest_metric_point_pair(obj0: Any, obj1: Any) -> Tuple[float, np.ndarray, np.ndarray]: + """ + Compute the closest point pair and squared distance between two primitives. + + Finds the pair of points (one on each object) that minimizes the distance, + and returns the squared distance metric along with both points. + Supports batch broadcasting. + + Parameters + ---------- + obj0, obj1 : Primitive + Any combination of Point, Segment, Triangle, Polygon, Ray, Line, Plane, AABB. + + Returns + ------- + metric : float or np.ndarray + Squared distance between the closest points. + Scalar for single x single, ndarray for batch. + point0 : np.ndarray + Closest point on obj0. Shape (D,) for single, (N, D) for batch. + point1 : np.ndarray + Closest point on obj1. Shape (D,) for single, (N, D) for batch. + + Examples + -------- + >>> import trueform as tf + >>> import numpy as np + >>> # Closest points between two segments + >>> seg1 = tf.Segment(start=[0, 0, 0], end=[1, 0, 0]) + >>> seg2 = tf.Segment(start=[0, 1, 0], end=[1, 1, 0]) + >>> metric, pt0, pt1 = tf.closest_metric_point_pair(seg1, seg2) + + >>> # Closest point from a point to a segment + >>> pt = tf.Point([0.5, 0.5, 0.0]) + >>> seg = tf.Segment(start=[0, 0, 0], end=[1, 0, 0]) + >>> metric, closest_on_pt, closest_on_seg = tf.closest_metric_point_pair(pt, seg) + """ + if not isinstance(obj0, Primitive) or not isinstance(obj1, Primitive): + raise TypeError( + f"closest_metric_point_pair requires two Primitives, got " + f"{type(obj0).__name__} and {type(obj1).__name__}") + + if obj0.dims != obj1.dims: + raise ValueError( + f"Dimension mismatch: obj0 has {obj0.dims}D, obj1 has {obj1.dims}D. " + f"Both objects must have the same dimensionality (2D or 3D).") + + if obj0.dtype != obj1.dtype: + raise TypeError( + f"Dtype mismatch: obj0 has {obj0.dtype}, obj1 has {obj1.dtype}. " + f"Both objects must have the same dtype.") + + suffix = build_suffix(InputMeta(None, obj0.dtype, None, obj0.dims)) + fn = getattr(_trueform.spatial, f"closest_point_pair_pp_{suffix}") + return fn(obj0._wrapper, obj1._wrapper) + + +def closest_metric_point(obj0: Any, obj1: Any) -> Tuple[float, np.ndarray]: + """ + Compute closest point on obj0 to obj1. + + Returns the squared distance and the closest point on obj0. + + Parameters + ---------- + obj0, obj1 : Primitive + Any combination of Point, Segment, Triangle, Polygon, Ray, Line, Plane, AABB. + + Returns + ------- + distance_squared : float + Squared distance between obj0 and obj1 + closest_point : np.ndarray + Closest point on obj0 (first argument) + """ + dist2, closest_pt, _ = closest_metric_point_pair(obj0, obj1) + return dist2, closest_pt + + +# Alias +closest_point_pair = closest_metric_point_pair diff --git a/python/src/trueform/_spatial/distance.py b/python/src/trueform/_spatial/distance.py new file mode 100644 index 0000000..30f66b0 --- /dev/null +++ b/python/src/trueform/_spatial/distance.py @@ -0,0 +1,141 @@ +""" +Unified distance and distance2 API + +Copyright (c) 2025 Ziga Sajovic, XLAB +Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. +Commercial licensing available via info@polydera.com. +https://github.com/polydera/trueform +""" + +import numpy as np +from typing import Any +from .. import _trueform +from .._primitives.primitive import Primitive +from .._dispatch import InputMeta, extract_meta, build_suffix + + +def distance(obj0: Any, obj1: Any): + """ + Compute the Euclidean distance between two geometric objects. + + This function works with both core primitives (Point, Segment, Triangle, Polygon, + Line, AABB, Ray, Plane) and spatial data structures (Mesh, EdgeMesh, PointCloud). + Supports batch broadcasting: single x single returns a scalar, batch x single, + single x batch, or batch x batch returns an ndarray. + + Parameters + ---------- + obj0, obj1 : geometric objects + Any combination of Point, Segment, Triangle, Polygon, Line, AABB, Ray, Plane, + Mesh, EdgeMesh, PointCloud. + + Returns + ------- + float or np.ndarray + Distance between the objects. Scalar for single x single, + ndarray for batch operations. + + Examples + -------- + >>> import trueform as tf + >>> import numpy as np + >>> # Distance from point to AABB + >>> pt = tf.Point([0.5, 0.5]) + >>> box = tf.AABB(min=[1.0, 1.0], max=[2.0, 2.0]) + >>> tf.distance(pt, box) + 0.7071067811865476 + + >>> # Distance between two segments + >>> seg1 = tf.Segment([[0, 0], [1, 0]]) + >>> seg2 = tf.Segment([[0, 2], [1, 2]]) + >>> tf.distance(seg1, seg2) + 2.0 + + >>> # Batch: distance from many points to a segment + >>> pts = tf.Point(np.random.rand(100, 3).astype(np.float32)) + >>> seg = tf.Segment(start=[0, 0, 0], end=[1, 1, 1]) + >>> dists = tf.distance(pts, seg) # shape (100,) + """ + return _distance_impl(obj0, obj1, "distance") + + +def distance2(obj0: Any, obj1: Any): + """ + Compute the squared Euclidean distance between two geometric objects. + + This is more efficient than distance() when you only need to compare distances, + as it avoids the square root computation. + + Parameters + ---------- + obj0, obj1 : geometric objects + Any combination of Point, Segment, Triangle, Polygon, Line, AABB, Ray, Plane, + Mesh, EdgeMesh, PointCloud. + + Returns + ------- + float or np.ndarray + Squared distance between the objects. + + Examples + -------- + >>> import trueform as tf + >>> import numpy as np + >>> # Squared distance from point to AABB + >>> pt = tf.Point([0.5, 0.5]) + >>> box = tf.AABB(min=[1.0, 1.0], max=[2.0, 2.0]) + >>> tf.distance2(pt, box) + 0.5 + + >>> # Squared distance between two points + >>> pt1 = tf.Point([0, 0, 0]) + >>> pt2 = tf.Point([1, 0, 0]) + >>> tf.distance2(pt1, pt2) + 1.0 + """ + return _distance_impl(obj0, obj1, "distance2") + + +def _distance_impl(obj0: Any, obj1: Any, op: str): + from . import Mesh, EdgeMesh, PointCloud + + is_prim0 = isinstance(obj0, Primitive) + is_prim1 = isinstance(obj1, Primitive) + is_form0 = isinstance(obj0, (Mesh, EdgeMesh, PointCloud)) + is_form1 = isinstance(obj1, (Mesh, EdgeMesh, PointCloud)) + + if obj0.dims != obj1.dims: + raise ValueError( + f"Dimension mismatch: obj0 has {obj0.dims}D, obj1 has {obj1.dims}D. " + f"Both objects must have the same dimensionality (2D or 3D).") + + if obj0.dtype != obj1.dtype: + raise TypeError( + f"Dtype mismatch: obj0 has {obj0.dtype}, obj1 has {obj1.dtype}. " + f"Both objects must have the same dtype.") + + if is_prim0 and is_prim1: + suffix = build_suffix(InputMeta(None, obj0.dtype, None, obj0.dims)) + fn = getattr(_trueform.spatial, f"{op}_pp_{suffix}") + return fn(obj0._wrapper, obj1._wrapper) + + if is_form0 and is_prim1: + meta = extract_meta(obj0) + suffix = build_suffix(meta) + fn = getattr(_trueform.spatial, f"{op}_{meta.form_name}_fp_{suffix}") + return fn(obj0._wrapper, obj1._wrapper) + + if is_prim0 and is_form1: + meta = extract_meta(obj1) + suffix = build_suffix(meta) + fn = getattr(_trueform.spatial, f"{op}_{meta.form_name}_fp_{suffix}") + return fn(obj1._wrapper, obj0._wrapper) + + if is_form0 and is_form1: + from .neighbor_search import neighbor_search as _neighbor_search + result = _neighbor_search(obj0, obj1, radius=None) + metric = result[1][0] + return np.sqrt(metric) if op == "distance" else metric + + raise TypeError( + f"{op} not supported for types: {type(obj0).__name__}, {type(obj1).__name__}") diff --git a/python/src/trueform/_spatial/gather_ids.py b/python/src/trueform/_spatial/gather_ids.py index 2e827ca..c487f55 100644 --- a/python/src/trueform/_spatial/gather_ids.py +++ b/python/src/trueform/_spatial/gather_ids.py @@ -1,7 +1,7 @@ """ Unified gather_ids API -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform @@ -10,15 +10,9 @@ import numpy as np from typing import Any, Optional from .. import _trueform - -# Dispatch infrastructure -from .._dispatch import ( - extract_meta, - build_suffix, - build_suffix_pair, - canonicalize_index_order, -) -from ._dispatch import GATHER_IDS_FORM_PRIM, GATHER_IDS_FORM_FORM +from .._primitives.primitive import Primitive +from .._dispatch import extract_meta, build_suffix, build_suffix_pair, canonicalize_index_order +from ._dispatch import GATHER_IDS_FORM_FORM def _gather_ids(form: Any, query: Any, predicate: str = "intersects", distance: Optional[float] = None) -> np.ndarray: @@ -32,8 +26,8 @@ def _gather_ids(form: Any, query: Any, predicate: str = "intersects", distance: ---------- form : Mesh, EdgeMesh, or PointCloud The spatial data structure to search - query : Point, Segment, Polygon, Ray, or Line - The query primitive + query : Point, Segment, Triangle, Polygon, Ray, Line, Mesh, EdgeMesh, or PointCloud + The query primitive or form predicate : str, optional The spatial predicate to evaluate. Options: - "intersects": Return primitives that intersect the query @@ -66,118 +60,58 @@ def _gather_ids(form: Any, query: Any, predicate: str = "intersects", distance: >>> >>> # Find faces intersecting a point >>> pt = tf.Point([0.3, 0.3]) - >>> ids = tf.gather_ids(mesh, pt, predicate="intersects") + >>> ids = tf.gather_intersecting_ids(mesh, pt) >>> print(ids) # e.g., [0] >>> >>> # Find faces within distance of a point >>> pt = tf.Point([2.0, 2.0]) - >>> ids = tf.gather_ids(mesh, pt, predicate="within_distance", distance=2.0) + >>> ids = tf.gather_ids_within_distance(mesh, pt, distance=2.0) >>> print(ids) # e.g., [1] """ + from .mesh import Mesh + from .edge_mesh import EdgeMesh + from .point_cloud import PointCloud - # Validate predicate if predicate not in ("intersects", "within_distance"): raise ValueError( f"Invalid predicate '{predicate}'. Must be 'intersects' or 'within_distance'") - # Validate distance for within_distance if predicate == "within_distance" and distance is None: raise ValueError( "distance is required when predicate='within_distance'") - # Validate dimensions match - if not hasattr(form, 'dims') or not hasattr(query, 'dims'): - raise TypeError("Both form and query must have 'dims' attribute") + # Handle swapped args: if form is a primitive and query is a form, swap them + is_form_form = isinstance(form, (Mesh, EdgeMesh, PointCloud)) + is_form_query = isinstance(query, (Mesh, EdgeMesh, PointCloud)) + if not is_form_form and is_form_query: + form, query = query, form + is_form_form = True + is_form_query = False if form.dims != query.dims: raise ValueError( f"Dimension mismatch: form has {form.dims}D, query has {query.dims}D. " - f"Both objects must have the same dimensionality (2D or 3D)." - ) - - # Get types - form_type = type(form) - query_type = type(query) - type_pair = (form_type, query_type) + f"Both objects must have the same dimensionality (2D or 3D).") - # Import spatial form types - from .mesh import Mesh - from .edge_mesh import EdgeMesh - from .point_cloud import PointCloud - - # Check if this is Form-Form or Form-Primitive operation - both_forms = (form_type in {Mesh, EdgeMesh, PointCloud} and - query_type in {Mesh, EdgeMesh, PointCloud}) + if form.dtype != query.dtype: + raise TypeError( + f"Dtype mismatch: form has {form.dtype}, query has {query.dtype}. " + f"Both objects must have the same dtype.") - if both_forms: + if is_form_query: + type_pair = (type(form), type(query)) return _form_form_gather_ids(form, query, type_pair, predicate, distance) - else: - return _form_prim_gather_ids(form, query, type_pair, predicate, distance) - -def _form_prim_gather_ids(form, query, type_pair, predicate, distance): - """Form x primitive gather_ids.""" - - if type_pair not in GATHER_IDS_FORM_PRIM: - supported = {t.__name__ for pair in GATHER_IDS_FORM_PRIM.keys() - for t in pair} + if not isinstance(query, Primitive): raise TypeError( - f"gather_ids not implemented for types: {type_pair[0].__name__}, {type_pair[1].__name__}. " - f"Supported types: {', '.join(sorted(supported))}" - ) - - func_template, needs_swap = GATHER_IDS_FORM_PRIM[type_pair] + f"query must be a Primitive, got {type(query).__name__}") - # Determine form and primitive based on swap - form_obj = query if needs_swap else form - prim_obj = form if needs_swap else query + assert not query.is_batch, "gather_ids does not support batch queries" - # Build suffix using dispatch utility - meta = extract_meta(form_obj) + meta = extract_meta(form) suffix = build_suffix(meta) - func_name = func_template.format(suffix) - cpp_func = getattr(_trueform.spatial, func_name) - - return cpp_func(form_obj._wrapper, prim_obj.data, predicate, distance) - - -def _form_form_gather_ids(form, query, type_pair, predicate, distance): - """Form x form gather_ids.""" - - if type_pair not in GATHER_IDS_FORM_FORM: - supported = {t.__name__ for pair in GATHER_IDS_FORM_FORM.keys() - for t in pair} - raise TypeError( - f"gather_ids not implemented for types: {type_pair[0].__name__}, {type_pair[1].__name__}. " - f"Supported form-form types: {', '.join(sorted(supported))}" - ) - - func_template, needs_swap = GATHER_IDS_FORM_FORM[type_pair] - - # Apply dispatch table swap - form0_obj = query if needs_swap else form - form1_obj = form if needs_swap else query - - # Apply index canonicalization (int32 before int64 for same types) - form0_obj, form1_obj, extra_swap = canonicalize_index_order( - form0_obj, form1_obj) - - # Build suffix using dispatch utility - meta0 = extract_meta(form0_obj) - meta1 = extract_meta(form1_obj) - suffix = build_suffix_pair(meta0, meta1) - - func_name = func_template.format(suffix) - cpp_func = getattr(_trueform.spatial, func_name) - result = cpp_func(form0_obj._wrapper, form1_obj._wrapper, - predicate, distance) - - # If forms were swapped, swap result columns back - # Result is numpy array of shape (N, 2) with columns [id0, id1] - if result is not None and result.shape[0] > 0 and (needs_swap or extra_swap): - result = result[:, [1, 0]] - - return result + fn = getattr(_trueform.spatial, f"gather_ids_{meta.form_name}_fp_{suffix}") + return fn(form._wrapper, query._wrapper, predicate, distance) def gather_intersecting_ids(form: Any, query: Any) -> np.ndarray: @@ -191,7 +125,7 @@ def gather_intersecting_ids(form: Any, query: Any) -> np.ndarray: ---------- form : Mesh, EdgeMesh, or PointCloud The spatial data structure to search - query : Point, Segment, Polygon, Ray, Line, Mesh, EdgeMesh, or PointCloud + query : Point, Segment, Triangle, Polygon, Ray, Line, Mesh, EdgeMesh, or PointCloud The query primitive or form Returns @@ -238,7 +172,7 @@ def gather_ids_within_distance(form: Any, query: Any, distance: float) -> np.nda ---------- form : Mesh, EdgeMesh, or PointCloud The spatial data structure to search - query : Point, Segment, Polygon, Ray, Line, Mesh, EdgeMesh, or PointCloud + query : Point, Segment, Triangle, Polygon, Ray, Line, Mesh, EdgeMesh, or PointCloud The query primitive or form distance : float Maximum distance @@ -274,3 +208,37 @@ def gather_ids_within_distance(form: Any, query: Any, distance: float) -> np.nda >>> print(ids) # e.g., [1] """ return _gather_ids(form, query, predicate="within_distance", distance=distance) + + +def _form_form_gather_ids(form, query, type_pair, predicate, distance): + """Form x form gather_ids.""" + + if type_pair not in GATHER_IDS_FORM_FORM: + supported = {t.__name__ for pair in GATHER_IDS_FORM_FORM.keys() + for t in pair} + raise TypeError( + f"gather_ids not implemented for types: {type_pair[0].__name__}, {type_pair[1].__name__}. " + f"Supported form-form types: {', '.join(sorted(supported))}" + ) + + func_template, needs_swap = GATHER_IDS_FORM_FORM[type_pair] + + form0_obj = query if needs_swap else form + form1_obj = form if needs_swap else query + + form0_obj, form1_obj, extra_swap = canonicalize_index_order( + form0_obj, form1_obj) + + meta0 = extract_meta(form0_obj) + meta1 = extract_meta(form1_obj) + suffix = build_suffix_pair(meta0, meta1) + + func_name = func_template.format(suffix) + cpp_func = getattr(_trueform.spatial, func_name) + result = cpp_func(form0_obj._wrapper, form1_obj._wrapper, + predicate, distance) + + if result is not None and result.shape[0] > 0 and (needs_swap or extra_swap): + result = result[:, [1, 0]] + + return result diff --git a/python/src/trueform/_spatial/intersects.py b/python/src/trueform/_spatial/intersects.py new file mode 100644 index 0000000..b0e10b8 --- /dev/null +++ b/python/src/trueform/_spatial/intersects.py @@ -0,0 +1,112 @@ +""" +Unified intersects API + +Copyright (c) 2025 Ziga Sajovic, XLAB +Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. +Commercial licensing available via info@polydera.com. +https://github.com/polydera/trueform +""" + +from typing import Any +from .. import _trueform +from .._primitives.primitive import Primitive +from .._dispatch import InputMeta, extract_meta, build_suffix, build_suffix_pair, canonicalize_index_order +from ._dispatch import INTERSECTS_FORM_FORM + + +def intersects(obj0: Any, obj1: Any): + """ + Check whether two geometric objects intersect. + + This function works with both core primitives (Point, Segment, Triangle, Polygon, + Line, AABB, Ray, Plane) and spatial data structures (Mesh, EdgeMesh, PointCloud). + Supports batch broadcasting: single x single returns a bool, batch operations + return an ndarray. + + Parameters + ---------- + obj0, obj1 : geometric objects + Any combination of Point, Segment, Triangle, Polygon, Line, AABB, Ray, Plane, + Mesh, EdgeMesh, PointCloud + + Returns + ------- + bool or np.ndarray + True if the objects intersect, False otherwise. + For batch operations, returns ndarray of int8 (0/1). + + Examples + -------- + >>> import trueform as tf + >>> import numpy as np + >>> # Check if point is inside AABB + >>> pt = tf.Point([0.5, 0.5]) + >>> box = tf.AABB(min=[0, 0], max=[1, 1]) + >>> tf.intersects(pt, box) + True + + >>> # Check if two segments intersect + >>> seg1 = tf.Segment([[0, 0], [1, 1]]) + >>> seg2 = tf.Segment([[0, 1], [1, 0]]) + >>> tf.intersects(seg1, seg2) + True + + >>> # Check if point intersects mesh + >>> faces = np.array([[0, 1, 2]], dtype=np.int32) + >>> points = np.array([[0, 0, 0], [1, 0, 0], [0.5, 1, 0]], dtype=np.float32) + >>> mesh = tf.Mesh(faces, points) + >>> pt = tf.Point([0.5, 0.3, 0.0]) + >>> tf.intersects(mesh, pt) + True + """ + from . import Mesh, EdgeMesh, PointCloud + + is_prim0 = isinstance(obj0, Primitive) + is_prim1 = isinstance(obj1, Primitive) + is_form0 = isinstance(obj0, (Mesh, EdgeMesh, PointCloud)) + is_form1 = isinstance(obj1, (Mesh, EdgeMesh, PointCloud)) + + if obj0.dims != obj1.dims: + raise ValueError( + f"Dimension mismatch: obj0 has {obj0.dims}D, obj1 has {obj1.dims}D. " + f"Both objects must have the same dimensionality (2D or 3D).") + + if obj0.dtype != obj1.dtype: + raise TypeError( + f"Dtype mismatch: obj0 has {obj0.dtype}, obj1 has {obj1.dtype}. " + f"Both objects must have the same dtype.") + + if is_prim0 and is_prim1: + suffix = build_suffix(InputMeta(None, obj0.dtype, None, obj0.dims)) + fn = getattr(_trueform.spatial, f"intersects_pp_{suffix}") + return fn(obj0._wrapper, obj1._wrapper) + + if is_form0 and is_prim1: + meta = extract_meta(obj0) + suffix = build_suffix(meta) + fn = getattr(_trueform.spatial, f"intersects_{meta.form_name}_fp_{suffix}") + return fn(obj0._wrapper, obj1._wrapper) + + if is_prim0 and is_form1: + meta = extract_meta(obj1) + suffix = build_suffix(meta) + fn = getattr(_trueform.spatial, f"intersects_{meta.form_name}_fp_{suffix}") + return fn(obj1._wrapper, obj0._wrapper) + + if is_form0 and is_form1: + type_pair = (type(obj0), type(obj1)) + if type_pair not in INTERSECTS_FORM_FORM: + raise TypeError( + f"intersects not supported for types: {type(obj0).__name__}, {type(obj1).__name__}") + func_template, needs_swap = INTERSECTS_FORM_FORM[type_pair] + form0 = obj1 if needs_swap else obj0 + form1 = obj0 if needs_swap else obj1 + form0, form1, _ = canonicalize_index_order(form0, form1) + meta0 = extract_meta(form0) + meta1 = extract_meta(form1) + suffix = build_suffix_pair(meta0, meta1) + fn = getattr(_trueform.spatial, func_template.format(suffix)) + return fn(form0._wrapper, form1._wrapper) + + raise TypeError( + f"intersects not supported for types: {type(obj0).__name__}, {type(obj1).__name__}") diff --git a/python/src/trueform/_spatial/neighbor_search.py b/python/src/trueform/_spatial/neighbor_search.py index 8560879..5a8d1d8 100644 --- a/python/src/trueform/_spatial/neighbor_search.py +++ b/python/src/trueform/_spatial/neighbor_search.py @@ -1,7 +1,7 @@ """ Neighbor search functions for spatial queries -Copyright (c) 2025 Žiga Sajovic, XLAB +Copyright (c) 2025 Ziga Sajovic, XLAB Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. Commercial licensing available via info@polydera.com. https://github.com/polydera/trueform @@ -10,22 +10,10 @@ import numpy as np from typing import Any, Optional, Union, List, Tuple from .. import _trueform +from .._primitives.primitive import Primitive from .._primitives import Point - -# Dispatch infrastructure -from .._dispatch import ( - extract_meta, - build_suffix, - build_suffix_pair, - canonicalize_index_order, -) -from ._dispatch import ( - NEIGHBOR_SEARCH, - NEIGHBOR_SEARCH_KNN, - NEIGHBOR_SEARCH_FORM_FORM, -) - -# Spatial forms (imported at function level to avoid circular imports) +from .._dispatch import extract_meta, build_suffix, build_suffix_pair, canonicalize_index_order +from ._dispatch import NEIGHBOR_SEARCH_FORM_FORM def neighbor_search( @@ -38,13 +26,13 @@ def neighbor_search( Search for nearest neighbor(s) in a spatial structure. Performs spatial queries to find the closest element(s) in a point cloud, mesh, or edge mesh to a given - geometric primitive (point, segment, polygon, ray, or line) or another spatial form. + geometric primitive (point, segment, triangle, polygon, ray, or line) or another spatial form. Parameters ---------- spatial_object : PointCloud, Mesh, or EdgeMesh The spatial structure to search in - query : Point, Segment, Polygon, Ray, Line, PointCloud, Mesh, EdgeMesh, or numpy array + query : Point, Segment, Triangle, Polygon, Ray, Line, PointCloud, Mesh, EdgeMesh, or numpy array The geometric primitive or spatial form to query with. Can be a wrapped primitive, another spatial form, or a numpy array (which will be treated as a Point) radius : float, optional @@ -58,17 +46,27 @@ def neighbor_search( Returns ------- - single_result : tuple[int, float, ndarray] (form-primitive query) - When k is None: Returns (index, distance_squared, point) for the nearest neighbor, + single_result : tuple[int, float, ndarray] (single primitive, k=None) + Returns (index, distance_squared, point) for the nearest neighbor, where index is the element index, distance_squared is the squared distance, and point is the coordinates of the closest point on the query primitive. + Returns None if no neighbor found within radius. + batch_result : tuple[ndarray, ndarray, ndarray] (batch primitive, k=None) + Returns (ids[N], distances[N], points[N, D]) where ids contains element indices + (-1 for not found), distances contains squared distances (inf for not found), + and points contains closest points on the spatial structure. + single_knn : list[tuple[int, float, ndarray]] (single primitive, k specified) + Returns a list of up to k tuples (index, distance_squared, point) + sorted by distance (closest first). + batch_knn : tuple[ndarray, ndarray, ndarray, ndarray] (batch primitive, k specified) + Returns (ids[N,K], distances[N,K], points[N,K,D], counts[N]) where + ids contains element indices (-1 padding for unfilled slots), + distances contains squared distances, points contains closest points, + and counts[i] gives the number of valid neighbors for query i. form_form_result : tuple[tuple[int, int], tuple[float, ndarray, ndarray]] (form-form query) - When query is a form: Returns ((index0, index1), (distance, point0, point1)) where + Returns ((index0, index1), (distance, point0, point1)) where index0 and index1 are element indices in the two forms, distance is the squared distance, and point0, point1 are the closest points on each form. - multiple_results : list[tuple[int, float, ndarray]] (form-primitive KNN) - When k is specified: Returns a list of up to k tuples (index, distance_squared, point) - sorted by distance (closest first). Examples -------- @@ -94,6 +92,15 @@ def neighbor_search( >>> seg = tf.Segment([[0.5, 0.5, 0], [0.5, 0.5, 1]]) >>> idx, dist2, closest_pt = tf.neighbor_search(mesh, seg) >>> + >>> # Batch query: find nearest neighbor for 100 query points + >>> query_pts = tf.Point(np.random.rand(100, 3).astype(np.float32)) + >>> ids, dists, pts = tf.neighbor_search(cloud, query_pts) + >>> print(f"ids shape: {ids.shape}, dists shape: {dists.shape}") + >>> + >>> # Batch k-NN: find 3 nearest neighbors for 100 query points + >>> ids, dists, pts, counts = tf.neighbor_search(cloud, query_pts, k=3) + >>> print(f"ids shape: {ids.shape}, counts: {counts}") # (100, 3), (100,) + >>> >>> # Form-form neighbor search >>> cloud2 = tf.PointCloud(points + 0.5) >>> (idx0, idx1), (dist, pt0, pt1) = tf.neighbor_search(cloud, cloud2) @@ -103,14 +110,48 @@ def neighbor_search( from .edge_mesh import EdgeMesh from .point_cloud import PointCloud - # Check if query is also a form (for form-form neighbor search) - query_type = type(query) - is_form_form = query_type in (Mesh, EdgeMesh, PointCloud) + is_form_query = isinstance(query, (Mesh, EdgeMesh, PointCloud)) - if is_form_form: + if is_form_query: return _form_form_neighbor_search(spatial_object, query, radius, k) + + # Wrap raw numpy arrays as Point + if isinstance(query, np.ndarray): + if query.ndim == 1: + query = Point(query) + else: + raise TypeError( + f"numpy array queries must be 1D point arrays, got shape {query.shape}") + + if not isinstance(query, Primitive): + raise TypeError( + f"query must be a Primitive, got {type(query).__name__}") + + if spatial_object.dims != query.dims: + raise ValueError( + f"Dimension mismatch: spatial_object has {spatial_object.dims}D, " + f"query has {query.dims}D. " + f"Both must have the same dimensionality (2D or 3D).") + + if spatial_object.dtype != query.dtype: + raise TypeError( + f"Dtype mismatch: spatial_object has {spatial_object.dtype}, " + f"query has {query.dtype}. " + f"Both objects must have the same dtype.") + + meta = extract_meta(spatial_object) + suffix = build_suffix(meta) + + if k is None: + fn = getattr(_trueform.spatial, + f"neighbor_search_{meta.form_name}_fp_{suffix}") + return fn(spatial_object._wrapper, query._wrapper, radius) else: - return _form_prim_neighbor_search(spatial_object, query, radius, k) + if not isinstance(k, int) or k <= 0: + raise ValueError(f"k must be a positive integer, got {k}") + fn = getattr(_trueform.spatial, + f"neighbor_search_knn_{meta.form_name}_fp_{suffix}") + return fn(spatial_object._wrapper, query._wrapper, k, radius) def _form_form_neighbor_search(form0, form1, radius, k): @@ -119,14 +160,12 @@ def _form_form_neighbor_search(form0, form1, radius, k): if k is not None: raise ValueError("KNN (k parameter) is not supported for form-form neighbor_search") - # Validate dimensions match if form0.dims != form1.dims: raise ValueError( f"Dimension mismatch: first form has {form0.dims}D, " f"second form has {form1.dims}D. Both must have the same dimensionality (2D or 3D)." ) - # Get type pair form0_type = type(form0) form1_type = type(form1) type_pair = (form0_type, form1_type) @@ -140,14 +179,11 @@ def _form_form_neighbor_search(form0, form1, radius, k): func_template, needs_swap = NEIGHBOR_SEARCH_FORM_FORM[type_pair] - # Apply dispatch table swap form0_obj = form1 if needs_swap else form0 form1_obj = form0 if needs_swap else form1 - # Apply index canonicalization (int32 before int64 for same types) form0_obj, form1_obj, extra_swap = canonicalize_index_order(form0_obj, form1_obj) - # Build suffix using dispatch utility meta0 = extract_meta(form0_obj) meta1 = extract_meta(form1_obj) suffix = build_suffix_pair(meta0, meta1) @@ -156,90 +192,8 @@ def _form_form_neighbor_search(form0, form1, radius, k): cpp_func = getattr(_trueform.spatial, func_name) result = cpp_func(form0_obj._wrapper, form1_obj._wrapper, radius) - # If forms were swapped, swap results back if result is not None and (needs_swap or extra_swap): (idx0, idx1), (dist, pt0, pt1) = result result = ((idx1, idx0), (dist, pt1, pt0)) return result - - -def _form_prim_neighbor_search(spatial_object, query, radius, k): - """Form x primitive neighbor search using centralized dispatch.""" - - # Normalize query to a primitive type - if isinstance(query, np.ndarray): - # Treat numpy arrays as points - if query.ndim == 1: - query_type = Point - query_data = query - dims = query.shape[0] - else: - raise TypeError( - f"numpy array queries must be 1D point arrays, got shape {query.shape}" - ) - else: - query_type = type(query) - query_data = query.data if hasattr(query, 'data') else query - dims = query.dims if hasattr(query, 'dims') else None - - # Validate dimensions match - obj_dims = spatial_object.dims - if dims is None: - raise TypeError(f"Cannot determine dimensions for query type {query_type}") - if obj_dims != dims: - raise ValueError( - f"Dimension mismatch: spatial_object has {obj_dims}D, query has {dims}D. " - f"Both must have the same dimensionality (2D or 3D)." - ) - - # Get form type and dispatch table - form_type = type(spatial_object) - - if form_type not in NEIGHBOR_SEARCH: - raise TypeError( - f"neighbor_search not implemented for spatial object type: {form_type.__name__}. " - f"Supported types: {', '.join(t.__name__ for t in NEIGHBOR_SEARCH.keys())}" - ) - - # Build suffix using centralized dispatch - meta = extract_meta(spatial_object) - suffix = build_suffix(meta) - - # Convert query_data to match object dtype if necessary - obj_dtype = spatial_object.points.dtype - if isinstance(query_data, np.ndarray) and query_data.dtype != obj_dtype: - query_data = query_data.astype(obj_dtype) - - # Choose dispatch table based on whether k is specified - if k is None: - # Non-KNN query - single nearest neighbor - dispatch_table = NEIGHBOR_SEARCH[form_type] - - if query_type not in dispatch_table: - supported = ", ".join(t.__name__ for t in dispatch_table.keys()) - raise TypeError( - f"neighbor_search not implemented for query type: {query_type.__name__}. " - f"Supported types: {supported}" - ) - - func_name = dispatch_table[query_type].format(suffix) - cpp_func = getattr(_trueform.spatial, func_name) - return cpp_func(spatial_object._wrapper, query_data, radius) - else: - # KNN query - k nearest neighbors - dispatch_table = NEIGHBOR_SEARCH_KNN[form_type] - - if query_type not in dispatch_table: - supported = ", ".join(t.__name__ for t in dispatch_table.keys()) - raise TypeError( - f"neighbor_search (KNN) not implemented for query type: {query_type.__name__}. " - f"Supported types: {supported}" - ) - - if not isinstance(k, int) or k <= 0: - raise ValueError(f"k must be a positive integer, got {k}") - - func_name = dispatch_table[query_type].format(suffix) - cpp_func = getattr(_trueform.spatial, func_name) - return cpp_func(spatial_object._wrapper, query_data, k, radius) diff --git a/python/src/trueform/_spatial/ray_cast.py b/python/src/trueform/_spatial/ray_cast.py new file mode 100644 index 0000000..d424f34 --- /dev/null +++ b/python/src/trueform/_spatial/ray_cast.py @@ -0,0 +1,138 @@ +""" +Unified ray casting API + +Copyright (c) 2025 Ziga Sajovic, XLAB +Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. +Commercial licensing available via info@polydera.com. +https://github.com/polydera/trueform +""" + +from typing import Any, Optional, Tuple, Union +import numpy as np +from .. import _trueform +from .._primitives.primitive import Primitive +from .._dispatch import InputMeta, extract_meta, build_suffix + + +def _prepare_config(config, dtype, n): + """Expand config to (ndarray, ndarray) for C++ binding. + + n == 0: single call → None stays None, scalar tuple → 1-element arrays. + n > 0: batch call → scalar tuple or None → full arrays of length n. + Per-ray arrays pass through unchanged. + """ + if config is None: + if n == 0: + return None + return (np.zeros(n, dtype=dtype), + np.full(n, np.finfo(dtype).max, dtype=dtype)) + + min_v, max_v = config + if isinstance(min_v, np.ndarray): + return config # already per-ray arrays + + if n == 0: + return (np.array([min_v], dtype=dtype), + np.array([max_v], dtype=dtype)) + + return (np.full(n, min_v, dtype=dtype), + np.full(n, max_v, dtype=dtype)) + + +def ray_cast(ray: Any, target: Any, config=None): + """ + Cast a ray against a geometric object and return intersection information. + + For core primitives (Segment, Triangle, Polygon, Line, AABB, Plane): + Returns the parametric distance t if intersection occurs, None otherwise. + hit_point = ray.origin + t * ray.direction + + For spatial structures (PointCloud, Mesh, EdgeMesh): + Returns (element_index, t) if intersection occurs, None otherwise. + - element_index: index of the intersected element (point/face/edge) + - t: parametric distance along the ray + + Supports batch rays and/or batch targets. For batch operations against + primitives, returns ndarray of t values (NaN for misses). For batch + operations against forms, returns (element_ids, t_values) — matching + the single-ray return order. + + Parameters + ---------- + ray : Ray + The ray to cast (single or batch) + target : Segment, Triangle, Polygon, Line, AABB, Plane, Mesh, EdgeMesh, or PointCloud + The geometric object to test against + config : tuple[float, float] or tuple[ndarray, ndarray] or None, optional + Ray configuration constraining the parametric range. + - ``(min_t, max_t)``: single range applied to all rays + - ``(min_ts, max_ts)``: per-ray arrays of shape ``(N,)``, same dtype as ray + If None, uses default (0.0 to infinity). + + Returns + ------- + result : float, tuple, np.ndarray, or None + - For core primitives: float (parametric distance t) or None + - For spatial structures: tuple (element_index, t) or None + - For batch rays × primitive: ndarray of t values (NaN for misses) + - For batch rays × form: (ndarray of element IDs, ndarray of t values) + with -1/NaN for misses + + Examples + -------- + >>> import trueform as tf + >>> import numpy as np + >>> # Ray casting against a triangle + >>> ray = tf.Ray(origin=[0.5, 0.3, 2.0], direction=[0.0, 0.0, -1.0]) + >>> triangle = tf.Triangle(a=[0, 0, 0], b=[1, 0, 0], c=[0.5, 1, 0]) + >>> t = tf.ray_cast(ray, triangle) + >>> if t is not None: + ... hit_point = ray.origin + t * ray.direction + ... print(f"Hit at {hit_point}, t={t}") + >>> + >>> # Ray casting against a mesh with custom range + >>> mesh = tf.Mesh(faces, points) + >>> ray = tf.Ray(origin=[0.3, 0.3, 2.0], direction=[0.0, 0.0, -1.0]) + >>> result = tf.ray_cast(ray, mesh, config=(0.5, 10.0)) + >>> + >>> # Per-ray config for batch rays + >>> rays = tf.Ray(origin=origins, direction=directions) + >>> min_ts = np.zeros(n, dtype=np.float32) + >>> max_ts = np.full(n, 10.0, dtype=np.float32) + >>> ids, ts = tf.ray_cast(rays, mesh, config=(min_ts, max_ts)) + """ + from . import Mesh, EdgeMesh, PointCloud + + is_prim = isinstance(target, Primitive) + is_form = isinstance(target, (Mesh, EdgeMesh, PointCloud)) + + if ray.dims != target.dims: + raise ValueError( + f"Dimension mismatch: ray has {ray.dims}D, target has {target.dims}D. " + f"Both objects must have the same dimensionality (2D or 3D).") + + if ray.dtype != target.dtype: + raise TypeError( + f"Dtype mismatch: ray has {ray.dtype}, target has {target.dtype}. " + f"Both objects must have the same dtype.") + + if is_prim: + # Batch when either ray or target (or both) is batch + br, bt = ray.is_batch, target.is_batch + n = ray.count if br else (target.count if bt else 0) + config = _prepare_config(config, ray.dtype, n) + suffix = build_suffix(InputMeta(None, ray.dtype, None, ray.dims)) + fn = getattr(_trueform.spatial, f"ray_cast_pp_{suffix}") + return fn(ray._wrapper, target._wrapper, config) + + if is_form: + # Batch only when ray is batch (forms are not batch primitives) + n = ray.count if ray.is_batch else 0 + config = _prepare_config(config, ray.dtype, n) + meta = extract_meta(target) + suffix = build_suffix(meta) + fn = getattr(_trueform.spatial, f"ray_cast_{meta.form_name}_fp_{suffix}") + return fn(ray._wrapper, target._wrapper, config) + + raise TypeError( + f"ray_cast not supported for target type: {type(target).__name__}") diff --git a/python/src/trueform/distance.py b/python/src/trueform/distance.py deleted file mode 100644 index d4c42e9..0000000 --- a/python/src/trueform/distance.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Unified distance and distance2 API - -Copyright (c) 2025 Žiga Sajovic, XLAB -Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. -Commercial licensing available via info@polydera.com. -https://github.com/polydera/trueform -""" - -import numpy as np -from typing import Any -from . import _trueform -from ._primitives import Plane - -# Dispatch infrastructure -from ._dispatch import InputMeta, build_suffix -from ._core._dispatch import DISTANCE as CORE_DISTANCE - - -def distance(obj0: Any, obj1: Any) -> float: - """ - Compute the Euclidean distance between two geometric objects. - - This function works with both core primitives (Point, Segment, Polygon, Line, AABB, Ray, Plane) - and spatial data structures (Mesh, PointCloud - when spatial module is available). - - Parameters - ---------- - obj0, obj1 : geometric objects - Any combination of Point, Segment, Polygon, Line, AABB, Ray, Plane, Mesh, PointCloud, etc. - - Returns - ------- - float - Distance between the objects - - Examples - -------- - >>> import trueform as tf - >>> import numpy as np - >>> # Distance from point to AABB - >>> pt = tf.Point([0.5, 0.5]) - >>> box = tf.AABB(min=[1.0, 1.0], max=[2.0, 2.0]) - >>> tf.distance(pt, box) - 0.7071067811865476 - - >>> # Distance between two segments - >>> seg1 = tf.Segment([[0, 0], [1, 0]]) - >>> seg2 = tf.Segment([[0, 2], [1, 2]]) - >>> tf.distance(seg1, seg2) - 2.0 - """ - return _distance_impl(obj0, obj1, "distance") - - -def distance2(obj0: Any, obj1: Any) -> float: - """ - Compute the squared Euclidean distance between two geometric objects. - - This is more efficient than distance() when you only need to compare distances, - as it avoids the square root computation. - - Parameters - ---------- - obj0, obj1 : geometric objects - Any combination of Point, Segment, Polygon, Line, AABB, Ray, Plane, Mesh, PointCloud, etc. - - Returns - ------- - float - Squared distance between the objects - - Examples - -------- - >>> import trueform as tf - >>> import numpy as np - >>> # Squared distance from point to AABB - >>> pt = tf.Point([0.5, 0.5]) - >>> box = tf.AABB(min=[1.0, 1.0], max=[2.0, 2.0]) - >>> tf.distance2(pt, box) - 0.5 - - >>> # Squared distance between two points - >>> pt1 = tf.Point([0, 0, 0]) - >>> pt2 = tf.Point([1, 0, 0]) - >>> tf.distance2(pt1, pt2) - 1.0 - """ - return _distance_impl(obj0, obj1, "distance2") - - -def _distance_impl(obj0: Any, obj1: Any, func_prefix: str) -> float: - """ - Internal implementation for both distance and distance2. - - Parameters - ---------- - obj0, obj1 : geometric objects - func_prefix : str - Either "distance" or "distance2" - """ - # Validate dimensions match - if not hasattr(obj0, 'dims') or not hasattr(obj1, 'dims'): - raise TypeError("Both objects must have 'dims' attribute") - - if obj0.dims != obj1.dims: - raise ValueError( - f"Dimension mismatch: obj0 has {obj0.dims}D, obj1 has {obj1.dims}D. " - f"Both objects must have the same dimensionality (2D or 3D)." - ) - - # Check if either argument is a form (Mesh, EdgeMesh, PointCloud) - from ._spatial import Mesh, EdgeMesh, PointCloud - - is_form0 = isinstance(obj0, (Mesh, EdgeMesh, PointCloud)) - is_form1 = isinstance(obj1, (Mesh, EdgeMesh, PointCloud)) - - if is_form0 or is_form1: - return _spatial_distance(obj0, obj1, is_form0, is_form1, func_prefix) - else: - return _core_distance(obj0, obj1, func_prefix) - - -def _core_distance(obj0, obj1, func_prefix: str) -> float: - """Primitive x primitive distance.""" - type0 = type(obj0) - type1 = type(obj1) - type_pair = (type0, type1) - - if type_pair not in CORE_DISTANCE: - supported = {t.__name__ for pair in CORE_DISTANCE.keys() for t in pair} - raise TypeError( - f"{func_prefix} not implemented for types: {type0.__name__}, {type1.__name__}. " - f"Supported types: {', '.join(sorted(supported))}" - ) - - func_template, needs_swap = CORE_DISTANCE[type_pair] - - # Special case: Plane is 3D only - if (type0 is Plane or type1 is Plane) and obj0.dims != 3: - raise ValueError(f"{func_prefix} with Plane is only supported in 3D") - - # Build suffix: primitives have no index/ngon - meta = InputMeta(None, obj0.dtype, None, obj0.dims) - suffix = build_suffix(meta) - func_name = func_template.format(func_prefix, suffix) - cpp_func = getattr(_trueform.core, func_name) - - # Handle symmetry - if needs_swap: - return cpp_func(obj1.data, obj0.data) - return cpp_func(obj0.data, obj1.data) - - -def _spatial_distance(obj0, obj1, is_form0: bool, is_form1: bool, func_prefix: str) -> float: - """Form x primitive or form x form distance via neighbor_search.""" - from ._spatial import neighbor_search - - # Ensure form is first argument (neighbor_search expects this) - if is_form0: - result = neighbor_search(obj0, obj1, radius=None) - else: - result = neighbor_search(obj1, obj0, radius=None) - - # Extract metric based on form combination - if is_form0 and is_form1: - # Form-form: ((idx0, idx1), (distance_squared, pt0, pt1)) - metric = result[1][0] - else: - # Form-primitive: (index, distance_squared, point) - metric = result[1] - - # Return based on function type - return np.sqrt(metric) if func_prefix == "distance" else metric diff --git a/python/src/trueform/distance_field.py b/python/src/trueform/distance_field.py deleted file mode 100644 index 29acd6f..0000000 --- a/python/src/trueform/distance_field.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -Vectorized distance field computation - -Copyright (c) 2025 Žiga Sajovic, XLAB -Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. -Commercial licensing available via info@polydera.com. -https://github.com/polydera/trueform -""" - -import numpy as np -from typing import Any -from . import _trueform -from ._primitives import Plane, Segment, Polygon, Line, AABB -from ._dispatch import extract_meta, build_suffix - - -# Dispatch table for primitives -_DISTANCE_FIELD_DISPATCH = { - Plane: "distance_field_plane_{}", - Segment: "distance_field_segment_{}", - Polygon: "distance_field_polygon_{}", - Line: "distance_field_line_{}", - AABB: "distance_field_aabb_{}", -} - - -def distance_field(points: Any, primitive: Any) -> np.ndarray: - """ - Compute distance from points to a primitive (vectorized, parallel). - - This function efficiently computes distances from many points to a geometric - primitive using parallel computation. For Plane primitives, returns signed - distance (negative inside, positive outside). For other primitives, returns - unsigned (Euclidean) distance. - - Parameters - ---------- - points : np.ndarray or PointCloud - Points to compute distances for: - - numpy array with shape (N, 2) or (N, 3) - - or PointCloud object - primitive : Plane, Segment, Polygon, Line, or AABB - Target geometric primitive - - Returns - ------- - np.ndarray - Distances with shape (N,) - - Signed distance for Plane (negative inside, positive outside) - - Unsigned Euclidean distance for other primitives - - Examples - -------- - >>> import trueform as tf - >>> import numpy as np - >>> # Distance to a plane - >>> points = np.random.rand(1000, 3).astype(np.float32) - >>> plane = tf.Plane(normal=[0.0, 0.0, 1.0], offset=0.0) - >>> distances = tf.distance_field(points, plane) - >>> distances.shape - (1000,) - >>> - >>> # Distance to a segment - >>> points_2d = np.random.rand(500, 2).astype(np.float32) - >>> segment = tf.Segment([[0.0, 0.0], [1.0, 1.0]]) - >>> distances_2d = tf.distance_field(points_2d, segment) - >>> - >>> # Using with PointCloud - >>> cloud = tf.PointCloud(points) - >>> distances = tf.distance_field(cloud, plane) - >>> - >>> # Use for isocontours (example for later) - >>> # paths, points = tf.make_isocontours(mesh, distances, [0.0, 0.5, 1.0]) - """ - - # Extract points array from PointCloud if needed - from ._spatial.point_cloud import PointCloud - if isinstance(points, PointCloud): - points_array = points.points - else: - points_array = points - - # Validate numpy array - if not isinstance(points_array, np.ndarray): - raise TypeError( - f"Expected numpy array or PointCloud, got {type(points)}" - ) - - if points_array.ndim != 2: - raise ValueError( - f"Expected 2D array with shape (N, 2 or 3), got shape {points_array.shape}" - ) - - dims = points_array.shape[1] - if dims not in [2, 3]: - raise ValueError( - f"Expected 2D or 3D points (shape (N, 2) or (N, 3)), got shape {points_array.shape}" - ) - - # Validate dtype - if points_array.dtype not in [np.float32, np.float64]: - raise TypeError( - f"Points must be float32 or float64, got {points_array.dtype}. " - f"Convert with points.astype(np.float32) or points.astype(np.float64)" - ) - - # Ensure C-contiguous - if not points_array.flags['C_CONTIGUOUS']: - points_array = np.ascontiguousarray(points_array) - - # Validate primitive type - primitive_type = type(primitive) - if primitive_type not in _DISTANCE_FIELD_DISPATCH: - supported = ", ".join(t.__name__ for t in _DISTANCE_FIELD_DISPATCH.keys()) - raise TypeError( - f"distance_field not implemented for primitive type: {primitive_type.__name__}. " - f"Supported types: {supported}" - ) - - # Validate primitive has matching dims - if not hasattr(primitive, 'dims'): - raise TypeError("Primitive must have 'dims' attribute") - - if primitive.dims != dims: - raise ValueError( - f"Dimension mismatch: points have {dims}D, primitive has {primitive.dims}D. " - f"Both must have the same dimensionality (2D or 3D)." - ) - - # Validate primitive dtype matches - if not hasattr(primitive, 'dtype'): - raise TypeError("Primitive must have 'dtype' attribute") - - if primitive.dtype != points_array.dtype: - raise TypeError( - f"Dtype mismatch: points have {points_array.dtype}, primitive has {primitive.dtype}. " - f"Both must have the same dtype (float32 or float64)." - ) - - # Special case: Plane is 3D only - if primitive_type is Plane and dims != 3: - raise ValueError("distance_field with Plane is only supported in 3D") - - # Get variant suffix (e.g., "float3d" or "double2d") - suffix = build_suffix(extract_meta(points_array)) - - # Dispatch to appropriate C++ function - func_template = _DISTANCE_FIELD_DISPATCH[primitive_type] - func_name = func_template.format(suffix) - - return getattr(_trueform.core, func_name)(points_array, primitive.data) diff --git a/python/src/trueform/intersects.py b/python/src/trueform/intersects.py deleted file mode 100644 index d58a2b0..0000000 --- a/python/src/trueform/intersects.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -Unified intersects API - -Copyright (c) 2025 Ziga Sajovic, XLAB -Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. -Commercial licensing available via info@polydera.com. -https://github.com/polydera/trueform -""" -from typing import Any -from . import _trueform -from ._primitives import Plane - -# Dispatch infrastructure -from ._dispatch import ( - InputMeta, - extract_meta, - build_suffix, - build_suffix_pair, - canonicalize_index_order, -) -from ._core._dispatch import INTERSECTS as CORE_INTERSECTS -from ._spatial._dispatch import INTERSECTS_FORM_PRIM, INTERSECTS_FORM_FORM - - -def intersects(obj0: Any, obj1: Any) -> bool: - """ - Check whether two geometric objects intersect. - - This function works with both core primitives (Point, Segment, Polygon, Line, AABB, Ray, Plane) - and spatial data structures (Mesh, EdgeMesh, PointCloud). - - Parameters - ---------- - obj0, obj1 : geometric objects - Any combination of Point, Segment, Polygon, Line, AABB, Ray, Plane, Mesh, EdgeMesh, PointCloud - - Returns - ------- - bool - True if the objects intersect, False otherwise - - Examples - -------- - >>> import trueform as tf - >>> import numpy as np - >>> # Check if point is inside AABB - >>> pt = tf.Point([0.5, 0.5]) - >>> box = tf.AABB(min=[0, 0], max=[1, 1]) - >>> tf.intersects(pt, box) - True - - >>> # Check if two segments intersect - >>> seg1 = tf.Segment([[0, 0], [1, 1]]) - >>> seg2 = tf.Segment([[0, 1], [1, 0]]) - >>> tf.intersects(seg1, seg2) - True - - >>> # Check if ray intersects mesh - >>> faces = np.array([[0, 1, 2]], dtype=np.int32) - >>> points = np.array([[0, 0, 0], [1, 0, 0], [0.5, 1, 0]], dtype=np.float32) - >>> mesh = tf.Mesh(faces, points) - >>> pt = tf.Point([0.5, 0.3, 0.0]) - >>> tf.intersects(mesh, pt) - True - """ - # Validate dimensions match - if not hasattr(obj0, 'dims') or not hasattr(obj1, 'dims'): - raise TypeError("Both objects must have 'dims' attribute") - - if obj0.dims != obj1.dims: - raise ValueError( - f"Dimension mismatch: obj0 has {obj0.dims}D, obj1 has {obj1.dims}D. " - f"Both objects must have the same dimensionality (2D or 3D)." - ) - - # Get types - type0 = type(obj0) - type1 = type(obj1) - type_pair = (type0, type1) - - # Import spatial form types - from ._spatial import Mesh, EdgeMesh, PointCloud - - # Check if this is a spatial operation - is_spatial = type0 in (Mesh, EdgeMesh, PointCloud) or type1 in (Mesh, EdgeMesh, PointCloud) - - if is_spatial: - return _spatial_intersects(obj0, obj1, type0, type1, type_pair) - else: - return _core_intersects(obj0, obj1, type0, type1, type_pair) - - -def _core_intersects(obj0, obj1, type0, type1, type_pair): - """Primitive x primitive intersects.""" - if type_pair not in CORE_INTERSECTS: - supported = {t.__name__ for pair in CORE_INTERSECTS.keys() for t in pair} - raise TypeError( - f"intersects not implemented for types: {type0.__name__}, {type1.__name__}. " - f"Supported core types: {', '.join(sorted(supported))}" - ) - - func_template, needs_swap = CORE_INTERSECTS[type_pair] - - # Special case: Plane is 3D only - if (type0 is Plane or type1 is Plane) and obj0.dims != 3: - raise ValueError("intersects with Plane is only supported in 3D") - - # Build suffix: primitives have no index/ngon - meta = InputMeta(None, obj0.dtype, None, obj0.dims) - suffix = build_suffix(meta) - func_name = func_template.format(suffix) - cpp_func = getattr(_trueform.core, func_name) - - # Handle symmetry - if needs_swap: - return cpp_func(obj1.data, obj0.data) - return cpp_func(obj0.data, obj1.data) - - -def _spatial_intersects(obj0, obj1, type0, type1, type_pair): - """Form x primitive or form x form intersects.""" - from ._spatial import Mesh, EdgeMesh, PointCloud - - both_forms = (type0 in (Mesh, EdgeMesh, PointCloud) and - type1 in (Mesh, EdgeMesh, PointCloud)) - - if both_forms: - return _form_form_intersects(obj0, obj1, type0, type1, type_pair) - else: - return _form_prim_intersects(obj0, obj1, type0, type1, type_pair) - - -def _form_prim_intersects(obj0, obj1, type0, type1, type_pair): - """Form x primitive intersects.""" - - if type_pair not in INTERSECTS_FORM_PRIM: - supported = {t.__name__ for pair in INTERSECTS_FORM_PRIM.keys() for t in pair} - raise TypeError( - f"intersects not implemented for types: {type0.__name__}, {type1.__name__}. " - f"Supported spatial types: {', '.join(sorted(supported))}" - ) - - func_template, needs_swap = INTERSECTS_FORM_PRIM[type_pair] - - # Determine form and primitive based on swap - form_obj = obj1 if needs_swap else obj0 - prim_obj = obj0 if needs_swap else obj1 - - # Build suffix using extract_meta + build_suffix - meta = extract_meta(form_obj) - suffix = build_suffix(meta) - func_name = func_template.format(suffix) - cpp_func = getattr(_trueform.spatial, func_name) - - return cpp_func(form_obj._wrapper, prim_obj.data) - - -def _form_form_intersects(obj0, obj1, type0, type1, type_pair): - """Form x form intersects.""" - - if type_pair not in INTERSECTS_FORM_FORM: - supported = {t.__name__ for pair in INTERSECTS_FORM_FORM.keys() for t in pair} - raise TypeError( - f"intersects not implemented for types: {type0.__name__}, {type1.__name__}. " - f"Supported form-form types: {', '.join(sorted(supported))}" - ) - - func_template, needs_swap = INTERSECTS_FORM_FORM[type_pair] - - # Apply dispatch table swap - form0_obj = obj1 if needs_swap else obj0 - form1_obj = obj0 if needs_swap else obj1 - - # Apply index canonicalization (int32 before int64 for same types) - form0_obj, form1_obj, _ = canonicalize_index_order(form0_obj, form1_obj) - - # Build suffix using extract_meta + build_suffix_pair - meta0 = extract_meta(form0_obj) - meta1 = extract_meta(form1_obj) - suffix = build_suffix_pair(meta0, meta1) - - func_name = func_template.format(suffix) - cpp_func = getattr(_trueform.spatial, func_name) - - return cpp_func(form0_obj._wrapper, form1_obj._wrapper) diff --git a/python/src/trueform/ray_cast.py b/python/src/trueform/ray_cast.py deleted file mode 100644 index 866ca88..0000000 --- a/python/src/trueform/ray_cast.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Unified ray casting API - -Copyright (c) 2025 Žiga Sajovic, XLAB -Licensed for noncommercial use under the PolyForm Noncommercial License 1.0.0. -Commercial licensing available via info@polydera.com. -https://github.com/polydera/trueform -""" - -from typing import Any, Optional, Tuple -from . import _trueform -from ._primitives import Plane - -# Dispatch infrastructure -from ._dispatch import InputMeta, extract_meta, build_suffix -from ._core._dispatch import RAY_CAST as CORE_RAY_CAST -from ._spatial._dispatch import RAY_CAST as SPATIAL_RAY_CAST - - -def ray_cast(ray: Any, target: Any, config: Optional[Tuple[float, float]] = None): - """ - Cast a ray against a geometric object and return intersection information. - - For core primitives (Segment, Polygon, Line, AABB, Plane): - Returns the parametric distance t if intersection occurs, None otherwise. - hit_point = ray.origin + t * ray.direction - - For spatial structures (PointCloud, Mesh, EdgeMesh): - Returns (element_index, t) if intersection occurs, None otherwise. - - element_index: index of the intersected element (point/face/edge) - - t: parametric distance along the ray - - Parameters - ---------- - ray : Ray - The ray to cast - target : Segment, Polygon, Line, AABB, Plane, Mesh, EdgeMesh, or PointCloud - The geometric object to test against - config : tuple[float, float] or None, optional - Ray configuration (min_t, max_t) to constrain the ray casting range. - - min_t: minimum parametric distance (default: 0.0) - - max_t: maximum parametric distance (default: infinity) - Both float('inf') and np.inf are supported for unbounded ranges. - If None, uses default configuration. - - Returns - ------- - result : float, tuple[int, float], or None - - For core primitives: float (parametric distance t) or None - - For spatial structures: tuple (element_index, t) or None - - Examples - -------- - >>> import trueform as tf - >>> import numpy as np - >>> # Ray casting against a polygon - >>> ray = tf.Ray(origin=[0.5, 0.3, 2.0], direction=[0.0, 0.0, -1.0]) - >>> triangle = tf.Polygon([[0, 0, 0], [1, 0, 0], [0.5, 1, 0]]) - >>> t = tf.ray_cast(ray, triangle) - >>> if t is not None: - ... hit_point = ray.origin + t * ray.direction - ... print(f"Hit at {hit_point}, t={t}") - >>> - >>> # Ray casting against a mesh with custom range - >>> faces = np.array([[0, 1, 2], [1, 2, 3]], dtype=np.int32) - >>> points = np.array([[0, 0, 0], [1, 0, 0], [0.5, 1, 0], [0.5, 0, 1]], dtype=np.float32) - >>> mesh = tf.Mesh(faces, points) - >>> ray = tf.Ray(origin=[0.3, 0.3, 2.0], direction=[0.0, 0.0, -1.0]) - >>> # Only check intersections between t=0.5 and t=10.0 - >>> result = tf.ray_cast(ray, mesh, config=(0.5, 10.0)) - >>> if result is not None: - ... face_idx, t = result - ... print(f"Hit face {face_idx} at t={t}") - >>>\ - >>> # Using np.inf for unbounded range (equivalent to default) - >>> result = tf.ray_cast(ray, mesh, config=(0.0, np.inf)) - """ - - # Validate dimensions match - if not hasattr(ray, 'dims') or not hasattr(target, 'dims'): - raise TypeError("Both ray and target must have 'dims' attribute") - - if ray.dims != target.dims: - raise ValueError( - f"Dimension mismatch: ray has {ray.dims}D, target has {target.dims}D. " - f"Both objects must have the same dimensionality (2D or 3D)." - ) - - # Validate dtypes match - if not hasattr(ray, 'dtype') or not hasattr(target, 'dtype'): - raise TypeError("Both ray and target must have 'dtype' attribute") - - if ray.dtype != target.dtype: - raise TypeError( - f"Dtype mismatch: ray has {ray.dtype}, target has {target.dtype}. " - f"Both objects must have the same dtype (float32 or float64)." - ) - - target_type = type(target) - - if target_type in SPATIAL_RAY_CAST: - return _spatial_ray_cast(ray, target, target_type, config) - elif target_type in CORE_RAY_CAST: - return _core_ray_cast(ray, target, target_type, config) - else: - supported_core = ", ".join(t.__name__ for t in CORE_RAY_CAST.keys()) - supported_spatial = ", ".join(t.__name__ for t in SPATIAL_RAY_CAST.keys()) - raise TypeError( - f"ray_cast not implemented for target type: {target_type.__name__}. " - f"Supported types: {supported_core}, {supported_spatial}" - ) - - -def _core_ray_cast(ray, target, target_type, config): - """Ray cast against core primitives.""" - # Special case: Plane is 3D only - if target_type is Plane and ray.dims != 3: - raise ValueError("ray_cast with Plane is only supported in 3D") - - # Build suffix: primitives have no index/ngon, just real+dims - meta = InputMeta(None, ray.dtype, None, ray.dims) - suffix = build_suffix(meta) - func_template = CORE_RAY_CAST[target_type] - func_name = func_template.format(suffix) - cpp_func = getattr(_trueform.core, func_name) - - return cpp_func(ray.data, target.data, config) - - -def _spatial_ray_cast(ray, target, target_type, config): - """Ray cast against spatial forms.""" - # Build suffix using extract_meta + build_suffix - meta = extract_meta(target) - suffix = build_suffix(meta) - func_template = SPATIAL_RAY_CAST[target_type] - func_name = func_template.format(suffix) - cpp_func = getattr(_trueform.spatial, func_name) - - return cpp_func(ray.data, target._wrapper, config) diff --git a/python/tests/test_batch_queries.py b/python/tests/test_batch_queries.py new file mode 100644 index 0000000..98a8d4f --- /dev/null +++ b/python/tests/test_batch_queries.py @@ -0,0 +1,695 @@ +""" +Test batch/broadcasting for primitive and form operations + +Tests all 4 broadcast combinations for prim x prim: + 1. single x single -> scalar + 2. batch x single -> ndarray + 3. single x batch -> ndarray + 4. batch x batch -> ndarray (element-wise) + +Tests form x batch prim for: + - distance, distance2, intersects, ray_cast + - neighbor_search (1-nn batch, k-nn batch) + +Copyright (c) 2025 Žiga Sajovic, XLAB +""" + +import sys +import pytest +import numpy as np +import trueform as tf + + +# --------------------------------------------------------------------------- +# helpers +# --------------------------------------------------------------------------- + +def _make_mesh_3d(dtype): + """Simple 3D triangle mesh (two triangles forming a square in xy-plane at z=0).""" + points = np.array([ + [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0] + ], dtype=dtype) + faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int32) + return tf.Mesh(faces, points) + + +def _make_point_cloud_3d(dtype): + """Simple 3D point cloud.""" + points = np.array([ + [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1] + ], dtype=dtype) + return tf.PointCloud(points) + + +# --------------------------------------------------------------------------- +# prim x prim: distance / distance2 +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +class TestBatchDistance: + + def test_single_x_single(self, dtype): + """Baseline: single point x single segment -> scalar.""" + pt = tf.Point(np.array([0.0, 1.0, 0.0], dtype=dtype)) + seg = tf.Segment(np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], dtype=dtype)) + d = tf.distance(pt, seg) + d2 = tf.distance2(pt, seg) + assert isinstance(d, float), "single x single should return scalar" + assert abs(d - 1.0) < 1e-5 + assert abs(d2 - 1.0) < 1e-5 + + def test_batch_x_single(self, dtype): + """Batch points x single segment -> ndarray.""" + pts_data = np.array([ + [0.0, 1.0, 0.0], + [0.5, 2.0, 0.0], + [1.0, 0.0, 0.0], + ], dtype=dtype) + pts = tf.Point(pts_data) + seg = tf.Segment(np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], dtype=dtype)) + + d = tf.distance(pts, seg) + d2 = tf.distance2(pts, seg) + assert isinstance(d, np.ndarray), "batch x single should return ndarray" + assert d.shape == (3,) + assert d2.shape == (3,) + assert abs(d[0] - 1.0) < 1e-5 # (0,1,0) -> seg = 1 + assert abs(d[1] - 2.0) < 1e-5 # (0.5,2,0) -> seg = 2 + assert abs(d[2] - 0.0) < 1e-5 # (1,0,0) on seg + + def test_single_x_batch(self, dtype): + """Single point x batch segments -> ndarray.""" + pt = tf.Point(np.array([0.0, 1.0, 0.0], dtype=dtype)) + segs_data = np.array([ + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], + [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + ], dtype=dtype) + segs = tf.Segment(segs_data) + + d = tf.distance(pt, segs) + d2 = tf.distance2(pt, segs) + assert isinstance(d, np.ndarray), "single x batch should return ndarray" + assert d.shape == (2,) + assert abs(d[0] - 1.0) < 1e-5 # dist to x-axis seg = 1 + assert abs(d[1] - 1.0) < 1e-5 # dist to z-axis seg (closest pt is origin) + + def test_batch_x_batch(self, dtype): + """Batch points x batch segments (element-wise) -> ndarray.""" + pts_data = np.array([ + [0.0, 1.0, 0.0], + [0.0, 0.0, 2.0], + ], dtype=dtype) + segs_data = np.array([ + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], + [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + ], dtype=dtype) + pts = tf.Point(pts_data) + segs = tf.Segment(segs_data) + + d = tf.distance(pts, segs) + d2 = tf.distance2(pts, segs) + assert isinstance(d, np.ndarray), "batch x batch should return ndarray" + assert d.shape == (2,) + assert abs(d[0] - 1.0) < 1e-5 # pt0 -> seg0 + assert abs(d[1] - 1.0) < 1e-5 # pt1 (0,0,2) -> seg1 (z-axis 0..1) + + def test_swap_order(self, dtype): + """Swapping argument order should give the same result.""" + pts = tf.Point(np.array([[0.0, 1.0, 0.0], [0.5, 2.0, 0.0]], dtype=dtype)) + seg = tf.Segment(np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], dtype=dtype)) + + d_forward = tf.distance(pts, seg) + d_backward = tf.distance(seg, pts) + np.testing.assert_allclose(d_forward, d_backward, atol=1e-5) + + +# --------------------------------------------------------------------------- +# prim x prim: intersects +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +class TestBatchIntersects: + + def test_single_x_single(self, dtype): + """Baseline: single point x single AABB -> bool.""" + pt = tf.Point(np.array([0.5, 0.5], dtype=dtype)) + box = tf.AABB( + min=np.array([0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0], dtype=dtype)) + result = tf.intersects(pt, box) + assert result is True or result == True + + def test_batch_x_single(self, dtype): + """Batch points x single AABB -> ndarray.""" + pts = tf.Point(np.array([ + [0.5, 0.5], # inside + [2.0, 2.0], # outside + [1.0, 1.0], # on corner + ], dtype=dtype)) + box = tf.AABB( + min=np.array([0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0], dtype=dtype)) + + result = tf.intersects(pts, box) + assert isinstance(result, np.ndarray), "batch x single should return ndarray" + assert result.shape == (3,) + assert result[0] == 1 # inside + assert result[1] == 0 # outside + assert result[2] == 1 # on corner + + def test_single_x_batch(self, dtype): + """Single point x batch AABBs -> ndarray.""" + pt = tf.Point(np.array([0.5, 0.5], dtype=dtype)) + boxes = tf.AABB(np.array([ + [[0.0, 0.0], [1.0, 1.0]], # contains point + [[2.0, 2.0], [3.0, 3.0]], # doesn't contain + ], dtype=dtype)) + + result = tf.intersects(pt, boxes) + assert isinstance(result, np.ndarray), "single x batch should return ndarray" + assert result.shape == (2,) + assert result[0] == 1 + assert result[1] == 0 + + def test_batch_x_batch(self, dtype): + """Batch points x batch AABBs (element-wise) -> ndarray.""" + pts = tf.Point(np.array([ + [0.5, 0.5], + [2.5, 2.5], + ], dtype=dtype)) + boxes = tf.AABB(np.array([ + [[0.0, 0.0], [1.0, 1.0]], # pt0 inside + [[2.0, 2.0], [3.0, 3.0]], # pt1 inside + ], dtype=dtype)) + + result = tf.intersects(pts, boxes) + assert isinstance(result, np.ndarray), "batch x batch should return ndarray" + assert result.shape == (2,) + assert result[0] == 1 + assert result[1] == 1 + + def test_swap_order(self, dtype): + """Swapping argument order should give the same result.""" + pts = tf.Point(np.array([[0.5, 0.5], [2.0, 2.0]], dtype=dtype)) + box = tf.AABB( + min=np.array([0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0], dtype=dtype)) + + r1 = tf.intersects(pts, box) + r2 = tf.intersects(box, pts) + np.testing.assert_array_equal(r1, r2) + + +# --------------------------------------------------------------------------- +# prim x prim: closest_point_pair +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +class TestBatchClosestPointPair: + + def test_single_x_single(self, dtype): + """Baseline: single point x single segment -> scalar metric + 2 points.""" + pt = tf.Point(np.array([0.5, 1.0, 0.0], dtype=dtype)) + seg = tf.Segment(np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], dtype=dtype)) + + metric, p0, p1 = tf.closest_point_pair(pt, seg) + assert isinstance(metric, float), "single x single metric should be scalar" + assert abs(metric - 1.0) < 1e-5 # distance² = 1 + assert p0.shape == (3,) + assert p1.shape == (3,) + # p0 should be the point itself + np.testing.assert_allclose(p0, [0.5, 1.0, 0.0], atol=1e-5) + # p1 should be closest on segment + np.testing.assert_allclose(p1, [0.5, 0.0, 0.0], atol=1e-5) + + def test_batch_x_single(self, dtype): + """Batch points x single segment -> ndarray metric + batch points.""" + pts = tf.Point(np.array([ + [0.0, 1.0, 0.0], + [0.5, 2.0, 0.0], + ], dtype=dtype)) + seg = tf.Segment(np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], dtype=dtype)) + + metric, p0, p1 = tf.closest_point_pair(pts, seg) + assert isinstance(metric, np.ndarray) + assert metric.shape == (2,) + assert p0.shape == (2, 3) + assert p1.shape == (2, 3) + assert abs(metric[0] - 1.0) < 1e-5 + assert abs(metric[1] - 4.0) < 1e-5 + + def test_single_x_batch(self, dtype): + """Single point x batch segments -> ndarray metric + batch points.""" + pt = tf.Point(np.array([0.0, 1.0, 0.0], dtype=dtype)) + segs = tf.Segment(np.array([ + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], + [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + ], dtype=dtype)) + + metric, p0, p1 = tf.closest_point_pair(pt, segs) + assert isinstance(metric, np.ndarray) + assert metric.shape == (2,) + assert p0.shape == (2, 3) + assert p1.shape == (2, 3) + + def test_batch_x_batch(self, dtype): + """Batch points x batch segments (element-wise) -> ndarray.""" + pts = tf.Point(np.array([ + [0.0, 1.0, 0.0], + [0.0, 0.0, 2.0], + ], dtype=dtype)) + segs = tf.Segment(np.array([ + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], + [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + ], dtype=dtype)) + + metric, p0, p1 = tf.closest_point_pair(pts, segs) + assert isinstance(metric, np.ndarray) + assert metric.shape == (2,) + assert abs(metric[0] - 1.0) < 1e-5 # pt0 -> seg0 + assert abs(metric[1] - 1.0) < 1e-5 # pt1 (0,0,2) -> seg1 (z: 0..1) + + +# --------------------------------------------------------------------------- +# prim x prim: ray_cast +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +class TestBatchRayCast: + + def test_single_x_single(self, dtype): + """Baseline: single ray x single segment -> scalar t or None.""" + ray = tf.Ray( + origin=np.array([0.0, 0.5], dtype=dtype), + direction=np.array([1.0, 0.0], dtype=dtype)) + seg = tf.Segment(np.array([[1.0, 0.0], [1.0, 1.0]], dtype=dtype)) + + t = tf.ray_cast(ray, seg) + assert t is not None + assert abs(t - 1.0) < 1e-5 + + def test_batch_ray_x_single(self, dtype): + """Batch rays x single segment -> ndarray of t values.""" + rays = tf.Ray(np.array([ + [[0.0, 0.5], [1.0, 0.0]], # hits seg at t=1 + [[0.0, 0.5], [-1.0, 0.0]], # misses (away) + [[0.0, 0.25], [1.0, 0.0]], # hits seg at t=1 + ], dtype=dtype)) + seg = tf.Segment(np.array([[1.0, 0.0], [1.0, 1.0]], dtype=dtype)) + + result = tf.ray_cast(rays, seg) + assert isinstance(result, np.ndarray), "batch ray x single should return ndarray" + assert result.shape == (3,) + assert abs(result[0] - 1.0) < 1e-5 + assert np.isnan(result[1]), "miss should be NaN" + assert abs(result[2] - 1.0) < 1e-5 + + def test_single_ray_x_batch(self, dtype): + """Single ray x batch segments -> ndarray of t values.""" + ray = tf.Ray( + origin=np.array([0.0, 0.5], dtype=dtype), + direction=np.array([1.0, 0.0], dtype=dtype)) + segs = tf.Segment(np.array([ + [[1.0, 0.0], [1.0, 1.0]], # hit at t=1 + [[3.0, 0.0], [3.0, 1.0]], # hit at t=3 + ], dtype=dtype)) + + result = tf.ray_cast(ray, segs) + assert isinstance(result, np.ndarray), "single ray x batch should return ndarray" + assert result.shape == (2,) + assert abs(result[0] - 1.0) < 1e-5 + assert abs(result[1] - 3.0) < 1e-5 + + def test_batch_ray_x_batch(self, dtype): + """Batch rays x batch segments (element-wise) -> ndarray of t values.""" + rays = tf.Ray(np.array([ + [[0.0, 0.5], [1.0, 0.0]], # hits seg0 at t=1 + [[0.0, 0.5], [1.0, 0.0]], # hits seg1 at t=3 + ], dtype=dtype)) + segs = tf.Segment(np.array([ + [[1.0, 0.0], [1.0, 1.0]], # x=1 + [[3.0, 0.0], [3.0, 1.0]], # x=3 + ], dtype=dtype)) + + result = tf.ray_cast(rays, segs) + assert isinstance(result, np.ndarray), "batch x batch should return ndarray" + assert result.shape == (2,) + assert abs(result[0] - 1.0) < 1e-5 + assert abs(result[1] - 3.0) < 1e-5 + + def test_scalar_config(self, dtype): + """Scalar config tuple still works for batch rays.""" + rays = tf.Ray(np.array([ + [[0.0, 0.5], [1.0, 0.0]], # hits seg at t=1 + [[0.0, 0.5], [1.0, 0.0]], # hits seg at t=1 + ], dtype=dtype)) + seg = tf.Segment(np.array([[1.0, 0.0], [1.0, 1.0]], dtype=dtype)) + + result = tf.ray_cast(rays, seg, config=(0.0, 0.5)) + assert result.shape == (2,) + # max_t=0.5 is too short to reach t=1 → both miss + assert np.isnan(result[0]) + assert np.isnan(result[1]) + + def test_per_ray_config_arrays(self, dtype): + """Per-ray config arrays for batch prim x prim.""" + rays = tf.Ray(np.array([ + [[0.0, 0.5], [1.0, 0.0]], # hits seg at t=1 + [[0.0, 0.5], [1.0, 0.0]], # hits seg at t=1 + [[0.0, 0.5], [1.0, 0.0]], # hits seg at t=1 + ], dtype=dtype)) + seg = tf.Segment(np.array([[1.0, 0.0], [1.0, 1.0]], dtype=dtype)) + + min_ts = np.array([0.0, 0.0, 0.5], dtype=dtype) + max_ts = np.array([2.0, 0.5, 2.0], dtype=dtype) + + result = tf.ray_cast(rays, seg, config=(min_ts, max_ts)) + assert result.shape == (3,) + assert abs(result[0] - 1.0) < 1e-5 # hit: max_t=2 covers t=1 + assert np.isnan(result[1]) # miss: max_t=0.5 < t=1 + assert abs(result[2] - 1.0) < 1e-5 # hit: min_t=0.5 < t=1 + + +# --------------------------------------------------------------------------- +# form x prim: distance, distance2, intersects +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +class TestFormPrimBatch: + + def test_form_distance_single(self, dtype): + """Form x single point -> scalar.""" + mesh = _make_mesh_3d(dtype) + pt = tf.Point(np.array([0.5, 0.5, 1.0], dtype=dtype)) + + d = tf.distance(mesh, pt) + assert isinstance(d, float) + assert abs(d - 1.0) < 1e-5 + + def test_form_distance_batch(self, dtype): + """Form x batch points -> ndarray.""" + mesh = _make_mesh_3d(dtype) + pts = tf.Point(np.array([ + [0.5, 0.5, 1.0], + [0.5, 0.5, 2.0], + [0.5, 0.5, 0.0], # on mesh + ], dtype=dtype)) + + d = tf.distance(mesh, pts) + assert isinstance(d, np.ndarray) + assert d.shape == (3,) + assert abs(d[0] - 1.0) < 1e-5 + assert abs(d[1] - 2.0) < 1e-5 + assert abs(d[2]) < 1e-5 + + def test_form_distance2_batch(self, dtype): + """Form x batch points -> ndarray (squared distance).""" + mesh = _make_mesh_3d(dtype) + pts = tf.Point(np.array([ + [0.5, 0.5, 1.0], + [0.5, 0.5, 2.0], + ], dtype=dtype)) + + d2 = tf.distance2(mesh, pts) + assert isinstance(d2, np.ndarray) + assert d2.shape == (2,) + assert abs(d2[0] - 1.0) < 1e-5 + assert abs(d2[1] - 4.0) < 1e-5 + + def test_form_distance_swap(self, dtype): + """distance(prim, form) should equal distance(form, prim).""" + mesh = _make_mesh_3d(dtype) + pts = tf.Point(np.array([ + [0.5, 0.5, 1.0], + [0.5, 0.5, 2.0], + ], dtype=dtype)) + + d1 = tf.distance(mesh, pts) + d2 = tf.distance(pts, mesh) + np.testing.assert_allclose(d1, d2, atol=1e-5) + + def test_form_intersects_single(self, dtype): + """Form x single point -> bool.""" + mesh = _make_mesh_3d(dtype) + pt_on = tf.Point(np.array([0.5, 0.5, 0.0], dtype=dtype)) + pt_off = tf.Point(np.array([0.5, 0.5, 1.0], dtype=dtype)) + + assert tf.intersects(mesh, pt_on) + assert not tf.intersects(mesh, pt_off) + + def test_form_intersects_batch(self, dtype): + """Form x batch points -> ndarray.""" + mesh = _make_mesh_3d(dtype) + pts = tf.Point(np.array([ + [0.5, 0.5, 0.0], # on mesh + [0.5, 0.5, 1.0], # off mesh + [0.0, 0.0, 0.0], # on vertex + ], dtype=dtype)) + + result = tf.intersects(mesh, pts) + assert isinstance(result, np.ndarray) + assert result.shape == (3,) + assert result[0] == 1 + assert result[1] == 0 + assert result[2] == 1 + + def test_form_intersects_swap(self, dtype): + """intersects(prim, form) should equal intersects(form, prim).""" + mesh = _make_mesh_3d(dtype) + pts = tf.Point(np.array([ + [0.5, 0.5, 0.0], + [0.5, 0.5, 1.0], + ], dtype=dtype)) + + r1 = tf.intersects(mesh, pts) + r2 = tf.intersects(pts, mesh) + np.testing.assert_array_equal(r1, r2) + + +# --------------------------------------------------------------------------- +# form x prim: ray_cast (batch rays against form) +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +class TestFormRayCastBatch: + + def test_form_ray_cast_single(self, dtype): + """Single ray x form -> (idx, t) or None.""" + mesh = _make_mesh_3d(dtype) + ray = tf.Ray( + origin=np.array([0.5, 0.5, 2.0], dtype=dtype), + direction=np.array([0.0, 0.0, -1.0], dtype=dtype)) + + result = tf.ray_cast(ray, mesh) + assert result is not None + idx, t = result + assert isinstance(idx, (int, np.integer)) + assert abs(t - 2.0) < 1e-5 + + def test_form_ray_cast_batch(self, dtype): + """Batch rays x form -> (ids, ts) tuple.""" + mesh = _make_mesh_3d(dtype) + rays = tf.Ray(np.array([ + [[0.5, 0.5, 2.0], [0.0, 0.0, -1.0]], # hits mesh at t=2 + [[5.0, 5.0, 2.0], [0.0, 0.0, -1.0]], # misses mesh + [[0.25, 0.25, 1.0], [0.0, 0.0, -1.0]], # hits mesh at t=1 + ], dtype=dtype)) + + ids, ts = tf.ray_cast(rays, mesh) + assert ids.shape == (3,) + assert ts.shape == (3,) + # ray 0: hit at t=2 + assert ids[0] >= 0 + assert abs(ts[0] - 2.0) < 1e-5 + # ray 1: miss -> -1 / NaN + assert np.isnan(ts[1]) + # ray 2: hit at t=1 + assert ids[2] >= 0 + assert abs(ts[2] - 1.0) < 1e-5 + + def test_form_ray_cast_scalar_config(self, dtype): + """Scalar config tuple works for batch rays x form.""" + mesh = _make_mesh_3d(dtype) + rays = tf.Ray(np.array([ + [[0.5, 0.5, 2.0], [0.0, 0.0, -1.0]], # hits at t=2 + [[0.25, 0.25, 1.0], [0.0, 0.0, -1.0]], # hits at t=1 + ], dtype=dtype)) + + # max_t=0.5 is too short for either hit + ids, ts = tf.ray_cast(rays, mesh, config=(0.0, 0.5)) + assert np.isnan(ts[0]) + assert np.isnan(ts[1]) + + def test_form_ray_cast_per_ray_config(self, dtype): + """Per-ray config arrays for batch rays x form.""" + mesh = _make_mesh_3d(dtype) + rays = tf.Ray(np.array([ + [[0.5, 0.5, 2.0], [0.0, 0.0, -1.0]], # hits at t=2 + [[0.5, 0.5, 2.0], [0.0, 0.0, -1.0]], # hits at t=2 + [[0.25, 0.25, 1.0], [0.0, 0.0, -1.0]], # hits at t=1 + ], dtype=dtype)) + + min_ts = np.array([0.0, 0.0, 0.0], dtype=dtype) + max_ts = np.array([3.0, 1.0, 3.0], dtype=dtype) + + ids, ts = tf.ray_cast(rays, mesh, config=(min_ts, max_ts)) + # ray 0: max_t=3 covers t=2 → hit + assert ids[0] >= 0 + assert abs(ts[0] - 2.0) < 1e-5 + # ray 1: max_t=1 < t=2 → miss + assert np.isnan(ts[1]) + # ray 2: max_t=3 covers t=1 → hit + assert ids[2] >= 0 + assert abs(ts[2] - 1.0) < 1e-5 + + +# --------------------------------------------------------------------------- +# form x prim: neighbor_search (1-nn batch and k-nn batch) +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +class TestFormNeighborSearchBatch: + + def test_1nn_single(self, dtype): + """Single query -> (idx, dist2, point).""" + cloud = _make_point_cloud_3d(dtype) + query = tf.Point(np.array([0.1, 0.1, 0.0], dtype=dtype)) + + result = tf.neighbor_search(cloud, query) + assert result is not None + idx, dist2, pt = result + assert isinstance(idx, (int, np.integer)) + assert idx == 0 # closest to [0,0,0] + assert pt.shape == (3,) + + def test_1nn_batch(self, dtype): + """Batch query -> (ids[N], dists[N], pts[N,D]).""" + cloud = _make_point_cloud_3d(dtype) + queries = tf.Point(np.array([ + [0.1, 0.0, 0.0], # closest to [0,0,0] + [0.9, 0.0, 0.0], # closest to [1,0,0] + [0.0, 0.9, 0.0], # closest to [0,1,0] + ], dtype=dtype)) + + result = tf.neighbor_search(cloud, queries) + ids, dists, pts = result + assert ids.shape == (3,) + assert dists.shape == (3,) + assert pts.shape == (3, 3) + assert ids[0] == 0 + assert ids[1] == 1 + assert ids[2] == 2 + + def test_1nn_batch_with_radius(self, dtype): + """Batch 1-nn with radius: some queries may not find neighbors.""" + cloud = _make_point_cloud_3d(dtype) + queries = tf.Point(np.array([ + [0.1, 0.0, 0.0], # within radius of [0,0,0] + [10.0, 10.0, 10.0], # far away + ], dtype=dtype)) + + result = tf.neighbor_search(cloud, queries, radius=0.5) + ids, dists, pts = result + assert ids.shape == (2,) + assert ids[0] == 0 + # id[1] should be -1 (not found) + assert ids[1] == -1 or ids[1] == np.iinfo(ids.dtype).max + + def test_knn_single(self, dtype): + """Single k-nn query -> list of tuples.""" + cloud = _make_point_cloud_3d(dtype) + query = tf.Point(np.array([0.0, 0.0, 0.0], dtype=dtype)) + + result = tf.neighbor_search(cloud, query, k=2) + assert isinstance(result, list) + assert len(result) <= 2 + # First result should be exact match (idx=0, dist2≈0) + idx0, dist0, pt0 = result[0] + assert idx0 == 0 + assert abs(dist0) < 1e-5 + + def test_knn_batch(self, dtype): + """Batch k-nn query -> (ids[N,K], dists[N,K], pts[N,K,D], counts[N]).""" + cloud = _make_point_cloud_3d(dtype) + queries = tf.Point(np.array([ + [0.0, 0.0, 0.0], # on point 0 + [0.5, 0.0, 0.0], # between point 0 and 1 + ], dtype=dtype)) + + result = tf.neighbor_search(cloud, queries, k=3) + ids, dists, pts, counts = result + assert ids.shape == (2, 3) + assert dists.shape == (2, 3) + assert pts.shape == (2, 3, 3) + assert counts.shape == (2,) + # Both queries should find at least 3 neighbors + assert counts[0] == 3 + assert counts[1] == 3 + + def test_knn_batch_with_radius(self, dtype): + """Batch k-nn with radius limiting results.""" + cloud = _make_point_cloud_3d(dtype) + queries = tf.Point(np.array([ + [0.0, 0.0, 0.0], # right on a point, very small radius + [0.5, 0.0, 0.0], # between points + ], dtype=dtype)) + + result = tf.neighbor_search(cloud, queries, k=3, radius=0.01) + ids, dists, pts, counts = result + assert counts[0] >= 1 # should find at least the exact point + # Unfilled slots should have id == -1 + for i in range(2): + for j in range(counts[i], 3): + assert ids[i, j] == -1 or ids[i, j] == np.iinfo(ids.dtype).max + + def test_mesh_1nn_batch(self, dtype): + """Batch 1-nn against mesh.""" + mesh = _make_mesh_3d(dtype) + queries = tf.Point(np.array([ + [0.5, 0.5, 1.0], + [0.5, 0.5, 2.0], + ], dtype=dtype)) + + result = tf.neighbor_search(mesh, queries) + ids, dists, pts = result + assert ids.shape == (2,) + assert dists.shape == (2,) + assert pts.shape == (2, 3) + # points project onto mesh at z=0 + assert abs(pts[0, 2]) < 1e-5 + assert abs(pts[1, 2]) < 1e-5 + + +# --------------------------------------------------------------------------- +# edge cases +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +class TestBatchEdgeCases: + + def test_batch_size_one(self, dtype): + """Batch of size 1 should still return ndarray.""" + pts = tf.Point(np.array([[0.5, 0.5, 0.0]], dtype=dtype)) + seg = tf.Segment(np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]], dtype=dtype)) + + d = tf.distance(pts, seg) + assert isinstance(d, np.ndarray) + assert d.shape == (1,) + + def test_2d_batch(self, dtype): + """Batch operations in 2D.""" + pts = tf.Point(np.array([ + [0.5, 0.5], + [2.0, 2.0], + ], dtype=dtype)) + seg = tf.Segment(np.array([[0.0, 0.0], [1.0, 0.0]], dtype=dtype)) + + d = tf.distance(pts, seg) + assert isinstance(d, np.ndarray) + assert d.shape == (2,) + assert abs(d[0] - 0.5) < 1e-5 + assert abs(d[1] - np.sqrt(5.0)) < 1e-5 # (2,2) to closest pt (1,0) + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-v"])) diff --git a/python/tests/test_closest_metric_point_pair.py b/python/tests/test_closest_metric_point_pair.py index 58733a9..93a0f2d 100644 --- a/python/tests/test_closest_metric_point_pair.py +++ b/python/tests/test_closest_metric_point_pair.py @@ -19,7 +19,7 @@ def test_point_polygon_2d_inside(): [0.0, 1.0] ], dtype=np.float32) - pt_inside = tf.Point([0.5, 0.5]) + pt_inside = tf.Point(np.array([0.5, 0.5], dtype=np.float32)) poly = tf.Polygon(square) dist2, p0, p1 = tf.closest_metric_point_pair(pt_inside, poly) @@ -35,7 +35,7 @@ def test_point_polygon_2d_outside(): [0.0, 1.0] ], dtype=np.float32) - pt_outside = tf.Point([2.0, 0.5]) + pt_outside = tf.Point(np.array([2.0, 0.5], dtype=np.float32)) poly = tf.Polygon(square) dist2, p0, p1 = tf.closest_metric_point_pair(pt_outside, poly) @@ -51,7 +51,7 @@ def test_point_polygon_3d_inside(): [0.5, 1.0, 0.0] ], dtype=np.float64) - pt_inside = tf.Point([0.5, 0.3, 0.0]) + pt_inside = tf.Point(np.array([0.5, 0.3, 0.0], dtype=np.float64)) poly = tf.Polygon(triangle) dist2, p0, p1 = tf.closest_metric_point_pair(pt_inside, poly) @@ -66,7 +66,7 @@ def test_point_polygon_3d_above(): [0.5, 1.0, 0.0] ], dtype=np.float64) - pt_above = tf.Point([0.5, 0.3, 2.0]) + pt_above = tf.Point(np.array([0.5, 0.3, 2.0], dtype=np.float64)) poly = tf.Polygon(triangle) dist2, p0, p1 = tf.closest_metric_point_pair(pt_above, poly) @@ -150,7 +150,7 @@ def test_segment_polygon_2d_intersecting(): [0.0, 1.0] ], dtype=np.float32) - seg_intersect = tf.Segment([[0.5, -0.5], [0.5, 1.5]]) + seg_intersect = tf.Segment(np.array([[0.5, -0.5], [0.5, 1.5]], dtype=np.float32)) poly = tf.Polygon(square) dist2, p0, p1 = tf.closest_metric_point_pair(seg_intersect, poly) @@ -166,7 +166,7 @@ def test_segment_polygon_2d_outside(): [0.0, 1.0] ], dtype=np.float32) - seg_outside = tf.Segment([[2.0, 0.0], [3.0, 0.0]]) + seg_outside = tf.Segment(np.array([[2.0, 0.0], [3.0, 0.0]], dtype=np.float32)) poly = tf.Polygon(square) dist2, p0, p1 = tf.closest_metric_point_pair(seg_outside, poly) @@ -181,7 +181,9 @@ def test_ray_polygon_3d_hitting(): [0.5, 1.0, 0.0] ], dtype=np.float32) - ray_hit = tf.Ray(origin=[0.5, 0.3, 2.0], direction=[0.0, 0.0, -1.0]) + ray_hit = tf.Ray( + origin=np.array([0.5, 0.3, 2.0], dtype=np.float32), + direction=np.array([0.0, 0.0, -1.0], dtype=np.float32)) poly = tf.Polygon(triangle) dist2, p0, p1 = tf.closest_metric_point_pair(ray_hit, poly) @@ -196,7 +198,9 @@ def test_ray_polygon_3d_missing(): [0.5, 1.0, 0.0] ], dtype=np.float32) - ray_miss = tf.Ray(origin=[0.5, 0.3, 2.0], direction=[0.0, 0.0, 1.0]) + ray_miss = tf.Ray( + origin=np.array([0.5, 0.3, 2.0], dtype=np.float32), + direction=np.array([0.0, 0.0, 1.0], dtype=np.float32)) poly = tf.Polygon(triangle) dist2, p0, p1 = tf.closest_metric_point_pair(ray_miss, poly) @@ -212,7 +216,9 @@ def test_line_polygon_2d_intersecting(): [0.0, 1.0] ], dtype=np.float32) - line_intersect = tf.Line(origin=[0.5, -1.0], direction=[0.0, 1.0]) + line_intersect = tf.Line( + origin=np.array([0.5, -1.0], dtype=np.float32), + direction=np.array([0.0, 1.0], dtype=np.float32)) poly = tf.Polygon(square) dist2, p0, p1 = tf.closest_metric_point_pair(line_intersect, poly) @@ -228,7 +234,9 @@ def test_line_polygon_2d_parallel(): [0.0, 1.0] ], dtype=np.float32) - line_parallel = tf.Line(origin=[2.0, 0.0], direction=[0.0, 1.0]) + line_parallel = tf.Line( + origin=np.array([2.0, 0.0], dtype=np.float32), + direction=np.array([0.0, 1.0], dtype=np.float32)) poly = tf.Polygon(square) dist2, p0, p1 = tf.closest_metric_point_pair(line_parallel, poly) @@ -946,5 +954,440 @@ def test_swap_symmetry_segment_polygon(dtype): assert np.allclose(c1_a, c0_b, atol=1e-5), "c1 of A should equal c0 of B" +# ============================================================================== +# Full Matrix Tests - AABB combinations +# ============================================================================== + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_point_outside(dtype, dims): + """Test AABB-Point with point outside box.""" + # AABB [0,0]-[1,1], Point [2, 0.5] -> closest on AABB is [1, 0.5], dist2 = 1.0 + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + pt_coords = np.zeros(dims, dtype=dtype) + pt_coords[0] = 2.0 + pt_coords[1] = 0.5 + pt = tf.Point(pt_coords) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, pt) + + expected_c0 = np.zeros(dims, dtype=dtype) + expected_c0[0] = 1.0 + expected_c0[1] = 0.5 + + assert np.isclose(dist2, 1.0), f"Expected dist2=1.0, got {dist2}" + assert np.allclose(c0, expected_c0, atol=1e-5), f"c0 should be [1, 0.5, ...], got {c0}" + assert np.allclose(c1, pt.data, atol=1e-5), f"c1 should be the point" + assert np.isclose(tf.distance2(box, pt), dist2), "distance2 should match" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_point_inside(dtype, dims): + """Test AABB-Point with point inside box.""" + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + pt_coords = np.full(dims, 0.5, dtype=dtype) + pt = tf.Point(pt_coords) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, pt) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_point_corner(dtype, dims): + """Test AABB-Point with point at diagonal from corner.""" + # AABB [0,0]-[1,1], Point [2, 2] -> closest is [1,1], dist2 = 2.0 + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + pt_coords = np.full(dims, 2.0, dtype=dtype) + pt = tf.Point(pt_coords) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, pt) + + expected_dist2 = float(dims) # 2.0 for 2D, 3.0 for 3D + assert np.isclose(dist2, expected_dist2), f"Expected dist2={expected_dist2}, got {dist2}" + assert np.allclose(c0, max_corner, atol=1e-5), f"c0 should be max corner, got {c0}" + assert np.isclose(tf.distance2(box, pt), dist2), "distance2 should match" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_segment_outside(dtype, dims): + """Test AABB-Segment with segment outside box.""" + # AABB [0,0]-[1,1], Segment [2,0]-[2,1] -> dist = 1 (closest face) + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + seg_pts = np.zeros((2, dims), dtype=dtype) + seg_pts[0, 0] = 2.0 + seg_pts[0, 1] = 0.0 + seg_pts[1, 0] = 2.0 + seg_pts[1, 1] = 1.0 + seg = tf.Segment(seg_pts) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, seg) + + assert np.isclose(dist2, 1.0), f"Expected dist2=1.0, got {dist2}" + assert np.isclose(c0[0], 1.0, atol=1e-5), f"c0 x should be 1.0, got {c0[0]}" + assert np.isclose(c1[0], 2.0, atol=1e-5), f"c1 x should be 2.0, got {c1[0]}" + assert np.isclose(tf.distance2(box, seg), dist2), "distance2 should match" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_segment_crossing(dtype, dims): + """Test AABB-Segment with segment crossing box.""" + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + seg_pts = np.zeros((2, dims), dtype=dtype) + seg_pts[0, 0] = -1.0 + seg_pts[0, 1] = 0.5 + seg_pts[1, 0] = 2.0 + seg_pts[1, 1] = 0.5 + seg = tf.Segment(seg_pts) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, seg) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_ray_hitting(dtype, dims): + """Test AABB-Ray with ray hitting box.""" + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + # Ray from [0.5, 0.5, 5] pointing toward -y (or -last axis) + origin = np.full(dims, 0.5, dtype=dtype) + origin[-1] = 5.0 + direction = np.zeros(dims, dtype=dtype) + direction[-1] = -1.0 + ray = tf.Ray(origin=origin, direction=direction) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, ray) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_ray_missing(dtype, dims): + """Test AABB-Ray with ray pointing away from box.""" + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + # Ray from [2, 0.5, ...] pointing in +x direction (away from box) + origin = np.full(dims, 0.5, dtype=dtype) + origin[0] = 2.0 + direction = np.zeros(dims, dtype=dtype) + direction[0] = 1.0 + ray = tf.Ray(origin=origin, direction=direction) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, ray) + + # Closest on box is [1, 0.5, ...], closest on ray is origin [2, 0.5, ...], dist2 = 1.0 + assert np.isclose(dist2, 1.0), f"Expected dist2=1.0, got {dist2}" + assert np.isclose(c0[0], 1.0, atol=1e-5), f"c0 x should be 1.0, got {c0[0]}" + assert np.isclose(c1[0], 2.0, atol=1e-5), f"c1 x should be 2.0, got {c1[0]}" + assert np.isclose(tf.distance2(box, ray), dist2), "distance2 should match" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_line_intersecting(dtype, dims): + """Test AABB-Line with line passing through box.""" + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + origin = np.full(dims, 0.5, dtype=dtype) + origin[0] = -5.0 + direction = np.zeros(dims, dtype=dtype) + direction[0] = 1.0 + line = tf.Line(origin=origin, direction=direction) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, line) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_line_parallel(dtype, dims): + """Test AABB-Line parallel to box face.""" + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + # Line at x=3 along y-axis + origin = np.zeros(dims, dtype=dtype) + origin[0] = 3.0 + direction = np.zeros(dims, dtype=dtype) + direction[1] = 1.0 + line = tf.Line(origin=origin, direction=direction) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, line) + + assert np.isclose(dist2, 4.0), f"Expected dist2=4.0, got {dist2}" + assert np.isclose(c0[0], 1.0, atol=1e-5), f"c0 x should be 1.0, got {c0[0]}" + assert np.isclose(c1[0], 3.0, atol=1e-5), f"c1 x should be 3.0, got {c1[0]}" + assert np.isclose(tf.distance2(box, line), dist2), "distance2 should match" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_polygon_separated(dtype, dims): + """Test AABB-Polygon with clear separation.""" + # AABB [0,0]-[1,1], triangle at x=3 + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + tri = np.zeros((3, dims), dtype=dtype) + tri[0, 0] = 3.0 + tri[0, 1] = 0.0 + tri[1, 0] = 4.0 + tri[1, 1] = 0.0 + tri[2, 0] = 3.5 + tri[2, 1] = 1.0 + poly = tf.Polygon(tri) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, poly) + + assert np.isclose(dist2, 4.0), f"Expected dist2=4.0, got {dist2}" + assert np.isclose(c0[0], 1.0, atol=1e-5), f"c0 x should be 1.0, got {c0[0]}" + assert np.isclose(c1[0], 3.0, atol=1e-5), f"c1 x should be 3.0, got {c1[0]}" + assert np.isclose(tf.distance2(box, poly), dist2), "distance2 should match" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_polygon_overlapping(dtype, dims): + """Test AABB-Polygon overlapping.""" + min_corner = np.zeros(dims, dtype=dtype) + max_corner = np.ones(dims, dtype=dtype) + box = tf.AABB(min=min_corner, max=max_corner) + + # Triangle that overlaps the AABB + tri = np.zeros((3, dims), dtype=dtype) + tri[0, 0] = 0.5 + tri[0, 1] = 0.5 + tri[1, 0] = 2.0 + tri[1, 1] = 0.5 + tri[2, 0] = 0.5 + tri[2, 1] = 2.0 + poly = tf.Polygon(tri) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, poly) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_aabb_plane_intersecting(dtype): + """Test AABB-Plane with plane cutting through box (3D only).""" + box = tf.AABB( + min=np.array([0.0, 0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0, 1.0], dtype=dtype) + ) + # Plane at z=0.5 cuts through box + plane = tf.Plane(np.array([0.0, 0.0, 1.0, -0.5], dtype=dtype)) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, plane) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_aabb_plane_separated(dtype): + """Test AABB-Plane with plane above box (3D only).""" + box = tf.AABB( + min=np.array([0.0, 0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0, 1.0], dtype=dtype) + ) + # Plane at z=4 + plane = tf.Plane(np.array([0.0, 0.0, 1.0, -4.0], dtype=dtype)) + + dist2, c0, c1 = tf.closest_metric_point_pair(box, plane) + + # Closest on box is any point on z=1 face, closest on plane is projection, dist = 3 + assert np.isclose(dist2, 9.0), f"Expected dist2=9.0, got {dist2}" + assert np.isclose(c0[2], 1.0, atol=1e-5), f"c0 z should be 1.0, got {c0[2]}" + assert np.isclose(c1[2], 4.0, atol=1e-5), f"c1 z should be 4.0, got {c1[2]}" + assert np.isclose(tf.distance2(box, plane), dist2), "distance2 should match" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_aabb_separated(dtype, dims): + """Test AABB-AABB with separated boxes.""" + box1 = tf.AABB( + min=np.zeros(dims, dtype=dtype), + max=np.ones(dims, dtype=dtype) + ) + min2 = np.zeros(dims, dtype=dtype) + min2[0] = 3.0 + max2 = np.ones(dims, dtype=dtype) + max2[0] = 4.0 + box2 = tf.AABB(min=min2, max=max2) + + dist2, c0, c1 = tf.closest_metric_point_pair(box1, box2) + + assert np.isclose(dist2, 4.0), f"Expected dist2=4.0, got {dist2}" + assert np.isclose(c0[0], 1.0, atol=1e-5), f"c0 x should be 1.0, got {c0[0]}" + assert np.isclose(c1[0], 3.0, atol=1e-5), f"c1 x should be 3.0, got {c1[0]}" + assert np.isclose(tf.distance2(box1, box2), dist2), "distance2 should match" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_aabb_overlapping(dtype, dims): + """Test AABB-AABB with overlapping boxes.""" + box1 = tf.AABB( + min=np.zeros(dims, dtype=dtype), + max=np.ones(dims, dtype=dtype) + ) + min2 = np.full(dims, 0.5, dtype=dtype) + max2 = np.full(dims, 1.5, dtype=dtype) + box2 = tf.AABB(min=min2, max=max2) + + dist2, c0, c1 = tf.closest_metric_point_pair(box1, box2) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_aabb_touching(dtype, dims): + """Test AABB-AABB with touching boxes (shared face).""" + box1 = tf.AABB( + min=np.zeros(dims, dtype=dtype), + max=np.ones(dims, dtype=dtype) + ) + min2 = np.zeros(dims, dtype=dtype) + min2[0] = 1.0 + max2 = np.ones(dims, dtype=dtype) + max2[0] = 2.0 + box2 = tf.AABB(min=min2, max=max2) + + dist2, c0, c1 = tf.closest_metric_point_pair(box1, box2) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +@pytest.mark.parametrize("dims", [2, 3]) +def test_aabb_aabb_contained(dtype, dims): + """Test AABB-AABB with one box contained in another.""" + box1 = tf.AABB( + min=np.zeros(dims, dtype=dtype), + max=np.full(dims, 4.0, dtype=dtype) + ) + box2 = tf.AABB( + min=np.ones(dims, dtype=dtype), + max=np.full(dims, 2.0, dtype=dtype) + ) + + dist2, c0, c1 = tf.closest_metric_point_pair(box1, box2) + + assert np.isclose(dist2, 0.0, atol=1e-5), f"Expected dist2=0.0, got {dist2}" + + +# ============================================================================== +# AABB swap symmetry tests +# ============================================================================== + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_swap_symmetry_aabb_point(dtype): + """Test swap symmetry for AABB-Point.""" + box = tf.AABB( + min=np.array([0.0, 0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0, 1.0], dtype=dtype) + ) + pt = tf.Point(np.array([2.0, 0.5, 0.5], dtype=dtype)) + + dist2_a, c0_a, c1_a = tf.closest_metric_point_pair(box, pt) + dist2_b, c0_b, c1_b = tf.closest_metric_point_pair(pt, box) + + assert np.isclose(dist2_a, dist2_b), "Distances should match after swap" + assert np.allclose(c0_a, c1_b, atol=1e-5), "c0 of A should equal c1 of B" + assert np.allclose(c1_a, c0_b, atol=1e-5), "c1 of A should equal c0 of B" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_swap_symmetry_aabb_segment(dtype): + """Test swap symmetry for AABB-Segment.""" + box = tf.AABB( + min=np.array([0.0, 0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0, 1.0], dtype=dtype) + ) + seg = tf.Segment(np.array([[3.0, 0.5, 0.5], [5.0, 0.5, 0.5]], dtype=dtype)) + + dist2_a, c0_a, c1_a = tf.closest_metric_point_pair(box, seg) + dist2_b, c0_b, c1_b = tf.closest_metric_point_pair(seg, box) + + assert np.isclose(dist2_a, dist2_b), "Distances should match after swap" + assert np.allclose(c0_a, c1_b, atol=1e-5), "c0 of A should equal c1 of B" + assert np.allclose(c1_a, c0_b, atol=1e-5), "c1 of A should equal c0 of B" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_swap_symmetry_aabb_aabb(dtype): + """Test swap symmetry for AABB-AABB.""" + box1 = tf.AABB( + min=np.array([0.0, 0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0, 1.0], dtype=dtype) + ) + box2 = tf.AABB( + min=np.array([3.0, 0.0, 0.0], dtype=dtype), + max=np.array([4.0, 1.0, 1.0], dtype=dtype) + ) + + dist2_a, c0_a, c1_a = tf.closest_metric_point_pair(box1, box2) + dist2_b, c0_b, c1_b = tf.closest_metric_point_pair(box2, box1) + + assert np.isclose(dist2_a, dist2_b), "Distances should match after swap" + assert np.allclose(c0_a, c1_b, atol=1e-5), "c0 of A should equal c1 of B" + assert np.allclose(c1_a, c0_b, atol=1e-5), "c1 of A should equal c0 of B" + + +@pytest.mark.parametrize("dtype", [np.float32, np.float64]) +def test_swap_symmetry_aabb_polygon(dtype): + """Test swap symmetry for AABB-Polygon.""" + box = tf.AABB( + min=np.array([0.0, 0.0, 0.0], dtype=dtype), + max=np.array([1.0, 1.0, 1.0], dtype=dtype) + ) + tri = np.array([ + [3.0, 0.0, 0.0], + [4.0, 0.0, 0.0], + [3.5, 1.0, 0.0] + ], dtype=dtype) + poly = tf.Polygon(tri) + + dist2_a, c0_a, c1_a = tf.closest_metric_point_pair(box, poly) + dist2_b, c0_b, c1_b = tf.closest_metric_point_pair(poly, box) + + assert np.isclose(dist2_a, dist2_b), "Distances should match after swap" + assert np.allclose(c0_a, c1_b, atol=1e-5), "c0 of A should equal c1 of B" + assert np.allclose(c1_a, c0_b, atol=1e-5), "c1 of A should equal c0 of B" + + if __name__ == "__main__": sys.exit(pytest.main([__file__, "-v"])) diff --git a/python/tests/test_distance_field.py b/python/tests/test_distance_field.py index f82a071..a199141 100644 --- a/python/tests/test_distance_field.py +++ b/python/tests/test_distance_field.py @@ -1,5 +1,5 @@ """ -Test distance_field functionality +Test batched distance (replaces distance_field tests) Copyright (c) 2025 Žiga Sajovic, XLAB """ @@ -11,91 +11,77 @@ @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_plane_3d(dtype): - """Test distance field to plane in 3D (signed distance)""" +def test_distance_plane_3d(dtype): + """Test batch distance to plane in 3D (signed distance)""" # Plane at z=0 (xy-plane) plane = tf.Plane(np.array([0, 0, 1, 0], dtype=dtype)) # Points above, on, and below the plane - points = np.array([ + points = tf.Point(np.array([ [0.0, 0.0, 2.0], # 2 units above [1.0, 1.0, 0.0], # on plane [0.5, 0.5, -1.0], # 1 unit below [2.0, 2.0, 3.0], # 3 units above - ], dtype=dtype) - - distances = tf.distance_field(points, plane) + ], dtype=dtype)) - # Check shape - assert distances.shape == (4,), f"Expected shape (4,), got {distances.shape}" + distances = tf.distance(points, plane) - # Check signed distances (positive above, negative below, zero on plane) - assert np.isclose(distances[0], 2.0), f"Expected distance 2.0, got {distances[0]}" - assert np.isclose(distances[1], 0.0), f"Expected distance 0.0, got {distances[1]}" - assert np.isclose(distances[2], -1.0), f"Expected distance -1.0, got {distances[2]}" - assert np.isclose(distances[3], 3.0), f"Expected distance 3.0, got {distances[3]}" + assert distances.shape == (4,) + assert np.isclose(distances[0], 2.0) + assert np.isclose(distances[1], 0.0) + assert np.isclose(distances[2], -1.0) + assert np.isclose(distances[3], 3.0) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_segment_2d(dtype): - """Test distance field to segment in 2D""" - # Horizontal segment from (0, 0) to (2, 0) +def test_distance_segment_2d(dtype): + """Test batch distance to segment in 2D""" segment = tf.Segment(np.array([[0.0, 0.0], [2.0, 0.0]], dtype=dtype)) - # Points at various positions - points = np.array([ + points = tf.Point(np.array([ [1.0, 0.0], # on segment (midpoint) [1.0, 1.0], # perpendicular distance 1.0 [3.0, 0.0], # beyond end, distance 1.0 [-1.0, 0.0], # beyond start, distance 1.0 [1.0, 2.0], # perpendicular distance 2.0 - ], dtype=dtype) + ], dtype=dtype)) - distances = tf.distance_field(points, segment) + distances = tf.distance(points, segment) - # Check shape - assert distances.shape == (5,), f"Expected shape (5,), got {distances.shape}" - - # Check distances - assert np.isclose(distances[0], 0.0), f"Expected distance 0.0, got {distances[0]}" - assert np.isclose(distances[1], 1.0), f"Expected distance 1.0, got {distances[1]}" - assert np.isclose(distances[2], 1.0), f"Expected distance 1.0, got {distances[2]}" - assert np.isclose(distances[3], 1.0), f"Expected distance 1.0, got {distances[3]}" - assert np.isclose(distances[4], 2.0), f"Expected distance 2.0, got {distances[4]}" + assert distances.shape == (5,) + assert np.isclose(distances[0], 0.0) + assert np.isclose(distances[1], 1.0) + assert np.isclose(distances[2], 1.0) + assert np.isclose(distances[3], 1.0) + assert np.isclose(distances[4], 2.0) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_segment_3d(dtype): - """Test distance field to segment in 3D""" - # Segment along x-axis from (0, 0, 0) to (2, 0, 0) +def test_distance_segment_3d(dtype): + """Test batch distance to segment in 3D""" segment = tf.Segment(np.array([[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]], dtype=dtype)) - # Points at various positions - points = np.array([ + points = tf.Point(np.array([ [1.0, 0.0, 0.0], # on segment (midpoint) [1.0, 1.0, 0.0], # perpendicular distance 1.0 [1.0, 0.0, 1.0], # perpendicular distance 1.0 [1.0, 1.0, 1.0], # perpendicular distance sqrt(2) [3.0, 0.0, 0.0], # beyond end, distance 1.0 - ], dtype=dtype) - - distances = tf.distance_field(points, segment) + ], dtype=dtype)) - # Check shape - assert distances.shape == (5,), f"Expected shape (5,), got {distances.shape}" + distances = tf.distance(points, segment) - # Check distances - assert np.isclose(distances[0], 0.0), f"Expected distance 0.0, got {distances[0]}" - assert np.isclose(distances[1], 1.0), f"Expected distance 1.0, got {distances[1]}" - assert np.isclose(distances[2], 1.0), f"Expected distance 1.0, got {distances[2]}" - assert np.isclose(distances[3], np.sqrt(2.0)), f"Expected distance sqrt(2), got {distances[3]}" - assert np.isclose(distances[4], 1.0), f"Expected distance 1.0, got {distances[4]}" + assert distances.shape == (5,) + assert np.isclose(distances[0], 0.0) + assert np.isclose(distances[1], 1.0) + assert np.isclose(distances[2], 1.0) + assert np.isclose(distances[3], np.sqrt(2.0)) + assert np.isclose(distances[4], 1.0) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_polygon_2d(dtype): - """Test distance field to polygon in 2D""" - # Unit square +def test_distance_polygon_2d(dtype): + """Test batch distance to polygon in 2D""" square = np.array([ [0.0, 0.0], [1.0, 0.0], @@ -104,37 +90,28 @@ def test_distance_field_polygon_2d(dtype): ], dtype=dtype) poly = tf.Polygon(square) - # Points inside, on, and outside the square - points = np.array([ + points = tf.Point(np.array([ [0.5, 0.5], # inside (center) [0.0, 0.5], # on edge [2.0, 0.5], # outside, distance 1.0 [-1.0, 0.5], # outside, distance 1.0 [0.5, 2.0], # outside, distance 1.0 - ], dtype=dtype) - - distances = tf.distance_field(points, poly) - - # Check shape - assert distances.shape == (5,), f"Expected shape (5,), got {distances.shape}" + ], dtype=dtype)) - # All distances should be non-negative (unsigned distance) - assert np.all(distances >= 0), "All distances should be non-negative" + distances = tf.distance(points, poly) - # Points on or inside should have distance 0 or small - assert distances[0] <= 0.5, f"Expected small distance for center point, got {distances[0]}" - assert np.isclose(distances[1], 0.0), f"Expected distance 0.0 for edge point, got {distances[1]}" - - # Points outside should have positive distance - assert np.isclose(distances[2], 1.0), f"Expected distance 1.0, got {distances[2]}" - assert np.isclose(distances[3], 1.0), f"Expected distance 1.0, got {distances[3]}" - assert np.isclose(distances[4], 1.0), f"Expected distance 1.0, got {distances[4]}" + assert distances.shape == (5,) + assert np.all(distances >= 0) + assert distances[0] <= 0.5 + assert np.isclose(distances[1], 0.0) + assert np.isclose(distances[2], 1.0) + assert np.isclose(distances[3], 1.0) + assert np.isclose(distances[4], 1.0) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_polygon_3d(dtype): - """Test distance field to polygon in 3D""" - # Triangle in xy-plane at z=0 +def test_distance_polygon_3d(dtype): + """Test batch distance to polygon in 3D""" triangle = np.array([ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], @@ -142,238 +119,136 @@ def test_distance_field_polygon_3d(dtype): ], dtype=dtype) poly = tf.Polygon(triangle) - # Points at various positions - points = np.array([ + points = tf.Point(np.array([ [0.5, 0.3, 0.0], # on triangle [0.5, 0.3, 1.0], # above triangle, distance ~1.0 [0.5, 0.3, -1.0], # below triangle, distance ~1.0 [2.0, 0.0, 0.0], # outside in plane - ], dtype=dtype) + ], dtype=dtype)) - distances = tf.distance_field(points, poly) + distances = tf.distance(points, poly) - # Check shape - assert distances.shape == (4,), f"Expected shape (4,), got {distances.shape}" - - # Point on triangle should have distance 0 - assert np.isclose(distances[0], 0.0, atol=1e-5), f"Expected distance 0.0, got {distances[0]}" - - # Points above/below should have positive distance - assert distances[1] > 0, f"Expected positive distance, got {distances[1]}" - assert distances[2] > 0, f"Expected positive distance, got {distances[2]}" - assert distances[3] > 0, f"Expected positive distance, got {distances[3]}" + assert distances.shape == (4,) + assert np.isclose(distances[0], 0.0, atol=1e-5) + assert distances[1] > 0 + assert distances[2] > 0 + assert distances[3] > 0 @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_line_2d(dtype): - """Test distance field to line in 2D""" - # Vertical line at x=1 +def test_distance_line_2d(dtype): + """Test batch distance to line in 2D""" line = tf.Line( origin=np.array([1.0, 0.0], dtype=dtype), - direction=np.array([0.0, 1.0], dtype=dtype) - ) + direction=np.array([0.0, 1.0], dtype=dtype)) - # Points at various positions - points = np.array([ + points = tf.Point(np.array([ [1.0, 0.0], # on line [1.0, 5.0], # on line (different y) [0.0, 0.0], # distance 1.0 [2.0, 0.0], # distance 1.0 [3.0, 0.0], # distance 2.0 - ], dtype=dtype) + ], dtype=dtype)) - distances = tf.distance_field(points, line) + distances = tf.distance(points, line) - # Check shape - assert distances.shape == (5,), f"Expected shape (5,), got {distances.shape}" - - # Check distances - assert np.isclose(distances[0], 0.0), f"Expected distance 0.0, got {distances[0]}" - assert np.isclose(distances[1], 0.0), f"Expected distance 0.0, got {distances[1]}" - assert np.isclose(distances[2], 1.0), f"Expected distance 1.0, got {distances[2]}" - assert np.isclose(distances[3], 1.0), f"Expected distance 1.0, got {distances[3]}" - assert np.isclose(distances[4], 2.0), f"Expected distance 2.0, got {distances[4]}" + assert distances.shape == (5,) + assert np.isclose(distances[0], 0.0) + assert np.isclose(distances[1], 0.0) + assert np.isclose(distances[2], 1.0) + assert np.isclose(distances[3], 1.0) + assert np.isclose(distances[4], 2.0) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_line_3d(dtype): - """Test distance field to line in 3D""" - # Line along z-axis through origin +def test_distance_line_3d(dtype): + """Test batch distance to line in 3D""" line = tf.Line( origin=np.array([0.0, 0.0, 0.0], dtype=dtype), - direction=np.array([0.0, 0.0, 1.0], dtype=dtype) - ) + direction=np.array([0.0, 0.0, 1.0], dtype=dtype)) - # Points at various positions - points = np.array([ + points = tf.Point(np.array([ [0.0, 0.0, 0.0], # on line [0.0, 0.0, 5.0], # on line (different z) [1.0, 0.0, 0.0], # distance 1.0 [0.0, 1.0, 0.0], # distance 1.0 [1.0, 1.0, 0.0], # distance sqrt(2) - ], dtype=dtype) + ], dtype=dtype)) - distances = tf.distance_field(points, line) + distances = tf.distance(points, line) - # Check shape - assert distances.shape == (5,), f"Expected shape (5,), got {distances.shape}" - - # Check distances - assert np.isclose(distances[0], 0.0), f"Expected distance 0.0, got {distances[0]}" - assert np.isclose(distances[1], 0.0), f"Expected distance 0.0, got {distances[1]}" - assert np.isclose(distances[2], 1.0), f"Expected distance 1.0, got {distances[2]}" - assert np.isclose(distances[3], 1.0), f"Expected distance 1.0, got {distances[3]}" - assert np.isclose(distances[4], np.sqrt(2.0)), f"Expected distance sqrt(2), got {distances[4]}" + assert distances.shape == (5,) + assert np.isclose(distances[0], 0.0) + assert np.isclose(distances[1], 0.0) + assert np.isclose(distances[2], 1.0) + assert np.isclose(distances[3], 1.0) + assert np.isclose(distances[4], np.sqrt(2.0)) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_aabb_2d(dtype): - """Test distance field to AABB in 2D""" - # AABB from [0,0] to [1,1] +def test_distance_aabb_2d(dtype): + """Test batch distance to AABB in 2D""" aabb = tf.AABB( min=np.array([0.0, 0.0], dtype=dtype), - max=np.array([1.0, 1.0], dtype=dtype) - ) + max=np.array([1.0, 1.0], dtype=dtype)) - # Points inside, on, and outside the AABB - points = np.array([ + points = tf.Point(np.array([ [0.5, 0.5], # inside (center), distance 0 [0.0, 0.5], # on edge, distance 0 [2.0, 0.5], # outside, distance 1.0 [-1.0, 0.5], # outside, distance 1.0 [2.0, 2.0], # outside corner, distance sqrt(2) - ], dtype=dtype) - - distances = tf.distance_field(points, aabb) + ], dtype=dtype)) - # Check shape - assert distances.shape == (5,), f"Expected shape (5,), got {distances.shape}" + distances = tf.distance(points, aabb) - # Check distances - assert np.isclose(distances[0], 0.0), f"Expected distance 0.0, got {distances[0]}" - assert np.isclose(distances[1], 0.0), f"Expected distance 0.0, got {distances[1]}" - assert np.isclose(distances[2], 1.0), f"Expected distance 1.0, got {distances[2]}" - assert np.isclose(distances[3], 1.0), f"Expected distance 1.0, got {distances[3]}" - assert np.isclose(distances[4], np.sqrt(2.0)), f"Expected distance sqrt(2), got {distances[4]}" + assert distances.shape == (5,) + assert np.isclose(distances[0], 0.0) + assert np.isclose(distances[1], 0.0) + assert np.isclose(distances[2], 1.0) + assert np.isclose(distances[3], 1.0) + assert np.isclose(distances[4], np.sqrt(2.0)) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_aabb_3d(dtype): - """Test distance field to AABB in 3D""" - # AABB cube from [0,0,0] to [1,1,1] +def test_distance_aabb_3d(dtype): + """Test batch distance to AABB in 3D""" aabb = tf.AABB( min=np.array([0.0, 0.0, 0.0], dtype=dtype), - max=np.array([1.0, 1.0, 1.0], dtype=dtype) - ) + max=np.array([1.0, 1.0, 1.0], dtype=dtype)) - # Points inside, on, and outside the AABB - points = np.array([ + points = tf.Point(np.array([ [0.5, 0.5, 0.5], # inside (center), distance 0 [0.0, 0.5, 0.5], # on face, distance 0 [2.0, 0.5, 0.5], # outside, distance 1.0 [0.5, 0.5, 2.0], # outside, distance 1.0 [2.0, 2.0, 2.0], # outside corner, distance sqrt(3) - ], dtype=dtype) - - distances = tf.distance_field(points, aabb) + ], dtype=dtype)) - # Check shape - assert distances.shape == (5,), f"Expected shape (5,), got {distances.shape}" + distances = tf.distance(points, aabb) - # Check distances - assert np.isclose(distances[0], 0.0), f"Expected distance 0.0, got {distances[0]}" - assert np.isclose(distances[1], 0.0), f"Expected distance 0.0, got {distances[1]}" - assert np.isclose(distances[2], 1.0), f"Expected distance 1.0, got {distances[2]}" - assert np.isclose(distances[3], 1.0), f"Expected distance 1.0, got {distances[3]}" - assert np.isclose(distances[4], np.sqrt(3.0)), f"Expected distance sqrt(3), got {distances[4]}" + assert distances.shape == (5,) + assert np.isclose(distances[0], 0.0) + assert np.isclose(distances[1], 0.0) + assert np.isclose(distances[2], 1.0) + assert np.isclose(distances[3], 1.0) + assert np.isclose(distances[4], np.sqrt(3.0)) -@pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_distance_field_with_point_cloud(dtype): - """Test distance_field with PointCloud object instead of numpy array""" - # Create a plane - plane = tf.Plane(np.array([0, 0, 1, 0], dtype=dtype)) - - # Create points as PointCloud - points_array = np.array([ - [0.0, 0.0, 2.0], - [1.0, 1.0, 0.0], - [0.5, 0.5, -1.0], - ], dtype=dtype) - point_cloud = tf.PointCloud(points_array) - - # Compute distances using PointCloud - distances = tf.distance_field(point_cloud, plane) - - # Check shape - assert distances.shape == (3,), f"Expected shape (3,), got {distances.shape}" - - # Check signed distances - assert np.isclose(distances[0], 2.0), f"Expected distance 2.0, got {distances[0]}" - assert np.isclose(distances[1], 0.0), f"Expected distance 0.0, got {distances[1]}" - assert np.isclose(distances[2], -1.0), f"Expected distance -1.0, got {distances[2]}" - - -def test_distance_field_dimension_mismatch(): - """Test that dimension mismatch raises an error""" - points_2d = np.array([[0.0, 0.0], [1.0, 1.0]], dtype=np.float32) - segment_3d = tf.Segment([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) - - with pytest.raises(ValueError) as excinfo: - tf.distance_field(points_2d, segment_3d) - assert "Dimension mismatch" in str(excinfo.value) - - -def test_distance_field_dtype_mismatch(): - """Test that dtype mismatch raises an error""" - points = np.array([[0.0, 0.0, 0.0]], dtype=np.float32) - plane = tf.Plane(np.array([0, 0, 1, 0], dtype=np.float64)) - - with pytest.raises(TypeError) as excinfo: - tf.distance_field(points, plane) - assert "Dtype mismatch" in str(excinfo.value) - - -def test_distance_field_invalid_input(): - """Test that invalid input raises appropriate errors""" - plane = tf.Plane([0, 0, 1, 0]) - - # Not a numpy array or PointCloud - with pytest.raises(TypeError) as excinfo: - tf.distance_field([[0, 0, 0]], plane) - assert "Expected numpy array or PointCloud" in str(excinfo.value) - - # Wrong shape (1D array) - points_1d = np.array([0.0, 0.0, 0.0], dtype=np.float32) - with pytest.raises(ValueError) as excinfo: - tf.distance_field(points_1d, plane) - assert "Expected 2D array" in str(excinfo.value) - - # Wrong number of dimensions (4D points) - points_4d = np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32) - with pytest.raises(ValueError) as excinfo: - tf.distance_field(points_4d, plane) - assert "Expected 2D or 3D points" in str(excinfo.value) - - -def test_distance_field_large_batch(): - """Test distance_field with large number of points (vectorization)""" - # Create 1000 random points +def test_distance_large_batch(): + """Test batch distance with large number of points""" np.random.seed(42) - points = np.random.rand(1000, 3).astype(np.float32) + points = tf.Point(np.random.rand(1000, 3).astype(np.float32)) # Plane at z=0 plane = tf.Plane(np.array([0, 0, 1, 0], dtype=np.float32)) - # Compute distances - distances = tf.distance_field(points, plane) - - # Check shape - assert distances.shape == (1000,), f"Expected shape (1000,), got {distances.shape}" + distances = tf.distance(points, plane) - # Check that all distances are equal to z-coordinates (signed) - expected_distances = points[:, 2] - assert np.allclose(distances, expected_distances), "Distances should equal z-coordinates" + assert distances.shape == (1000,) + # Distances should equal z-coordinates (signed for planes) + expected = points.data[:, 2] + assert np.allclose(distances, expected) if __name__ == "__main__": diff --git a/python/tests/test_gather_ids.py b/python/tests/test_gather_ids.py index 71fb10c..e3182c1 100644 --- a/python/tests/test_gather_ids.py +++ b/python/tests/test_gather_ids.py @@ -321,7 +321,7 @@ def test_point_cloud_gather_ids_segment_within_distance_2d(real_dtype): def test_gather_ids_dimension_mismatch(): """Test that dimension mismatch raises an error""" mesh_2d = create_simple_mesh_2d(np.int32, np.float32) - pt_3d = tf.Point([0.0, 0.0, 0.0]) + pt_3d = tf.Point(np.array([0.0, 0.0, 0.0], dtype=np.float32)) with pytest.raises(ValueError, match="Dimension mismatch"): tf.gather_intersecting_ids(mesh_2d, pt_3d) @@ -330,7 +330,7 @@ def test_gather_ids_dimension_mismatch(): def test_gather_ids_missing_threshold(): """Test that missing threshold for within_distance raises an error""" mesh = create_simple_mesh_2d(np.int32, np.float32) - pt = tf.Point([0.5, 0.5]) + pt = tf.Point(np.array([0.5, 0.5], dtype=np.float32)) with pytest.raises(ValueError, match="distance is required"): tf.gather_ids_within_distance(mesh, pt, distance=None) @@ -349,7 +349,7 @@ def test_gather_ids_return_dtype(): """Test that gather_ids returns correct dtype based on form index type""" # Mesh with int32 indices mesh_int32 = create_simple_mesh_2d(np.int32, np.float32) - pt = tf.Point([0.3, 0.3]) + pt = tf.Point(np.array([0.3, 0.3], dtype=np.float32)) ids = tf.gather_intersecting_ids(mesh_int32, pt) assert ids.dtype == np.int32, "Should return int32 for int32-indexed mesh" diff --git a/python/tests/test_geometry_compute.py b/python/tests/test_geometry_compute.py index e0a9f6a..2851d47 100644 --- a/python/tests/test_geometry_compute.py +++ b/python/tests/test_geometry_compute.py @@ -524,6 +524,147 @@ def test_point_normals_matches_mesh_property(index_dtype, real_dtype): np.testing.assert_allclose(pn_func, pn_prop) +# ============================================================================== +# Triangle Primitive normals Tests +# ============================================================================== + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_triangle_single(real_dtype): + """Normal of a single 3D triangle in the XY plane points along Z.""" + tri = tf.Triangle(np.array([ + [0, 0, 0], + [1, 0, 0], + [0, 1, 0] + ], dtype=real_dtype)) + + n = tf.normals(tri) + assert n.shape == (3,) + assert n.dtype == real_dtype + # Cross product of (1,0,0)x(0,1,0) = (0,0,1) + np.testing.assert_allclose(np.abs(n), [0, 0, 1], atol=1e-6) + + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_triangle_single_unit_vector(real_dtype): + """Normal of a single triangle is a unit vector.""" + tri = tf.Triangle(np.array([ + [1, 2, 3], + [4, 5, 3], + [2, 7, 3] + ], dtype=real_dtype)) + + n = tf.normals(tri) + length = np.linalg.norm(n) + np.testing.assert_allclose(length, 1.0, atol=1e-6) + + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_triangle_batch(real_dtype): + """Normals of a batch of triangles returns (N, 3) array.""" + data = np.array([ + [[0, 0, 0], [1, 0, 0], [0, 1, 0]], # normal ~ (0, 0, 1) + [[0, 0, 0], [0, 1, 0], [0, 0, 1]], # normal ~ (1, 0, 0) + [[0, 0, 0], [0, 0, 1], [1, 0, 0]], # normal ~ (0, 1, 0) + ], dtype=real_dtype) + tris = tf.Triangle(data) + + n = tf.normals(tris) + assert n.shape == (3, 3) + assert n.dtype == real_dtype + + # All should be unit vectors + lengths = np.linalg.norm(n, axis=1) + np.testing.assert_allclose(lengths, 1.0, atol=1e-6) + + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_triangle_batch_values(real_dtype): + """Batch triangle normals have correct directions.""" + # Triangle in XY plane → normal along Z + data = np.array([ + [[0, 0, 0], [1, 0, 0], [0, 1, 0]], + ], dtype=real_dtype) + tris = tf.Triangle(data) + + n = tf.normals(tris) + np.testing.assert_allclose(np.abs(n[0]), [0, 0, 1], atol=1e-6) + + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_triangle_2d_raises(real_dtype): + """Normals raises ValueError for 2D triangles.""" + tri = tf.Triangle(np.array([ + [0, 0], [1, 0], [0, 1] + ], dtype=real_dtype)) + + with pytest.raises(ValueError, match="3D"): + tf.normals(tri) + + +# ============================================================================== +# Polygon Primitive normals Tests +# ============================================================================== + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_polygon_single(real_dtype): + """Normal of a single 3D polygon in the XY plane points along Z.""" + poly = tf.Polygon(np.array([ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0] + ], dtype=real_dtype)) + + n = tf.normals(poly) + assert n.shape == (3,) + assert n.dtype == real_dtype + np.testing.assert_allclose(np.abs(n), [0, 0, 1], atol=1e-6) + + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_polygon_single_unit_vector(real_dtype): + """Normal of a single polygon is a unit vector.""" + poly = tf.Polygon(np.array([ + [1, 2, 3], + [4, 5, 3], + [2, 7, 3], + [0, 4, 3] + ], dtype=real_dtype)) + + n = tf.normals(poly) + length = np.linalg.norm(n) + np.testing.assert_allclose(length, 1.0, atol=1e-6) + + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_polygon_batch(real_dtype): + """Normals of a batch of polygons returns (N, 3) array.""" + data = np.array([ + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], # XY plane + [[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]], # YZ plane + ], dtype=real_dtype) + polys = tf.Polygon(data) + + n = tf.normals(polys) + assert n.shape == (2, 3) + assert n.dtype == real_dtype + + # All should be unit vectors + lengths = np.linalg.norm(n, axis=1) + np.testing.assert_allclose(lengths, 1.0, atol=1e-6) + + +@pytest.mark.parametrize("real_dtype", REAL_DTYPES) +def test_normals_polygon_2d_raises(real_dtype): + """Normals raises ValueError for 2D polygons.""" + poly = tf.Polygon(np.array([ + [0, 0], [1, 0], [1, 1], [0, 1] + ], dtype=real_dtype)) + + with pytest.raises(ValueError, match="3D"): + tf.normals(poly) + + # ============================================================================== # Main runner # ============================================================================== diff --git a/python/tests/test_isobands.py b/python/tests/test_isobands.py index 80e58b0..197e88f 100644 --- a/python/tests/test_isobands.py +++ b/python/tests/test_isobands.py @@ -77,7 +77,7 @@ def test_isobands_basic(index_dtype, real_dtype, mesh_type): # Create scalar field using distance to horizontal plane plane = tf.Plane(np.array([0, 0, 1, 0], dtype=real_dtype)) - distances = tf.distance_field(mesh.points, plane) + distances = tf.distance(tf.Point(mesh.points), plane) # Extract isobands at different z-levels cut_values = np.array([-0.5, 0.0, 0.5], dtype=real_dtype) @@ -117,7 +117,7 @@ def test_isobands_single_cut_value(real_dtype, mesh_type): mesh = create_octahedron(np.int32, real_dtype, mesh_type) plane = tf.Plane(np.array([0, 0, 1, 0], dtype=real_dtype)) - distances = tf.distance_field(mesh.points, plane) + distances = tf.distance(tf.Point(mesh.points), plane) # Single cut value creates 2 bands (result_faces, result_points), labels = tf.isobands(mesh, distances, 0.0) @@ -139,7 +139,7 @@ def test_isobands_selected_bands(mesh_type): mesh = create_octahedron(np.int32, np.float32, mesh_type) plane = tf.Plane(np.array([0, 0, 1, 0], dtype=np.float32)) - distances = tf.distance_field(mesh.points, plane) + distances = tf.distance(tf.Point(mesh.points), plane) # 3 cut values create 4 bands (0, 1, 2, 3) cut_values = np.array([-0.5, 0.0, 0.5], dtype=np.float32) @@ -162,7 +162,7 @@ def test_isobands_with_curves(mesh_type): mesh = create_octahedron(np.int32, np.float32, mesh_type) plane = tf.Plane(np.array([0, 0, 1, 0], dtype=np.float32)) - distances = tf.distance_field(mesh.points, plane) + distances = tf.distance(tf.Point(mesh.points), plane) cut_values = np.array([-0.5, 0.0, 0.5], dtype=np.float32) (result_faces, result_points), labels, (paths, curve_points) = tf.isobands( @@ -196,7 +196,7 @@ def test_isobands_path_indices_valid(mesh_type): mesh = create_octahedron(np.int32, np.float32, mesh_type) plane = tf.Plane(np.array([0, 0, 1, 0], dtype=np.float32)) - distances = tf.distance_field(mesh.points, plane) + distances = tf.distance(tf.Point(mesh.points), plane) cut_values = np.array([0.0], dtype=np.float32) (result_faces, result_points), labels, (paths, curve_points) = tf.isobands( diff --git a/python/tests/test_isocontours.py b/python/tests/test_isocontours.py index b32a71b..043deb5 100644 --- a/python/tests/test_isocontours.py +++ b/python/tests/test_isocontours.py @@ -43,7 +43,7 @@ def test_single_threshold(dtype): # Create scalar field (distance to z=0.5 plane) plane = tf.Plane(np.array([0, 0, 1, -0.5], dtype=dtype)) - scalar_field = tf.distance_field(mesh.points, plane) + scalar_field = tf.distance(tf.Point(mesh.points), plane) # Extract isocontour at 0.0 (z=0.5 plane) paths, points_out = tf.isocontours(mesh, scalar_field, 0.0) @@ -78,7 +78,7 @@ def test_multiple_thresholds(dtype): # Create scalar field plane = tf.Plane(np.array([0, 0, 1, 0], dtype=dtype)) - scalar_field = tf.distance_field(mesh.points, plane) + scalar_field = tf.distance(tf.Point(mesh.points), plane) # Extract multiple isocontours thresholds = np.array([0.0, 0.5, 1.0], dtype=dtype) @@ -97,8 +97,8 @@ def test_multiple_thresholds(dtype): @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -def test_with_distance_field(dtype): - """Test isocontours using distance_field for scalar values""" +def test_with_batch_distance(dtype): + """Test isocontours using batch distance for scalar values""" # Create mesh from STL-like data faces = np.array([ [0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 1], @@ -116,7 +116,7 @@ def test_with_distance_field(dtype): # Create scalar field using distance to horizontal plane plane = tf.Plane(np.array([0, 0, 1, 0], dtype=dtype)) - distances = tf.distance_field(mesh.points, plane) + distances = tf.distance(tf.Point(mesh.points), plane) # Extract isocontour at z=0 paths, curve_points = tf.isocontours(mesh, distances, 0.0) @@ -261,7 +261,7 @@ def test_iteration_over_curves(): mesh = tf.Mesh(faces, points) plane = tf.Plane(np.array([0, 0, 1, 0], dtype=np.float32)) - scalar_field = tf.distance_field(mesh.points, plane) + scalar_field = tf.distance(tf.Point(mesh.points), plane) paths, curve_points = tf.isocontours(mesh, scalar_field, 0.0) @@ -335,7 +335,7 @@ def test_dynamic_mesh_isocontours(mesh_type): # Create scalar field using distance to horizontal plane plane = tf.Plane(np.array([0, 0, 1, 0], dtype=np.float32)) - distances = tf.distance_field(mesh.points, plane) + distances = tf.distance(tf.Point(mesh.points), plane) # Extract isocontour at z=0 paths, curve_points = tf.isocontours(mesh, distances, 0.0) @@ -368,7 +368,7 @@ def test_dynamic_multiple_thresholds(mesh_type): # Create scalar field plane = tf.Plane(np.array([0, 0, 1, 0], dtype=np.float32)) - scalar_field = tf.distance_field(mesh.points, plane) + scalar_field = tf.distance(tf.Point(mesh.points), plane) # Extract multiple isocontours thresholds = np.array([0.0, 0.5, 1.0], dtype=np.float32) diff --git a/python/tests/test_measurements.py b/python/tests/test_measurements.py index dd37ab9..cbc848a 100644 --- a/python/tests/test_measurements.py +++ b/python/tests/test_measurements.py @@ -746,6 +746,216 @@ def test_measurements_sphere_composed_transform(dtype): tf.mean_edge_length(mesh), mel_base * scale, rtol=1e-4) +# ============================================================================== +# Triangle Primitive Area Tests +# ============================================================================== + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_area_triangle_primitive_single_3d(dtype): + """Area of a single 3D Triangle primitive.""" + tri = tf.Triangle(np.array([ + [0, 0, 0], + [2, 0, 0], + [0, 2, 0] + ], dtype=dtype)) + + computed = tf.area(tri) + assert isinstance(computed, float) + np.testing.assert_allclose(computed, 2.0, rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_area_triangle_primitive_single_2d(dtype): + """Area of a single 2D Triangle primitive.""" + tri = tf.Triangle(np.array([ + [0, 0], + [1, 0], + [0, 1] + ], dtype=dtype)) + + computed = tf.area(tri) + assert isinstance(computed, float) + np.testing.assert_allclose(computed, 0.5, rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_area_triangle_primitive_batch(dtype): + """Area of a batch of Triangle primitives returns array.""" + data = np.array([ + [[0, 0, 0], [1, 0, 0], [0, 1, 0]], # area = 0.5 + [[0, 0, 0], [2, 0, 0], [0, 2, 0]], # area = 2.0 + [[0, 0, 0], [3, 0, 0], [0, 3, 0]], # area = 4.5 + ], dtype=dtype) + tris = tf.Triangle(data) + + computed = tf.area(tris) + assert isinstance(computed, np.ndarray) + assert computed.shape == (3,) + np.testing.assert_allclose(computed, [0.5, 2.0, 4.5], rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_area_triangle_primitive_batch_2d(dtype): + """Area of a batch of 2D Triangle primitives.""" + data = np.array([ + [[0, 0], [1, 0], [0, 1]], # area = 0.5 + [[0, 0], [2, 0], [0, 2]], # area = 2.0 + ], dtype=dtype) + tris = tf.Triangle(data) + + computed = tf.area(tris) + assert isinstance(computed, np.ndarray) + assert computed.shape == (2,) + np.testing.assert_allclose(computed, [0.5, 2.0], rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_area_triangle_primitive_unit_right(dtype): + """Unit right triangle has area 0.5.""" + tri = tf.Triangle(a=np.array([0, 0, 0], dtype=dtype), + b=np.array([1, 0, 0], dtype=dtype), + c=np.array([0, 1, 0], dtype=dtype)) + + computed = tf.area(tri) + np.testing.assert_allclose(computed, 0.5, rtol=1e-5) + + +# ============================================================================== +# Polygon Primitive Area Tests (batch) +# ============================================================================== + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_area_polygon_primitive_batch_3d(dtype): + """Area of a batch of 3D Polygon primitives.""" + # Two unit squares + data = np.array([ + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], # area = 1.0 + [[0, 0, 0], [2, 0, 0], [2, 2, 0], [0, 2, 0]], # area = 4.0 + ], dtype=dtype) + polys = tf.Polygon(data) + + computed = tf.area(polys) + assert isinstance(computed, np.ndarray) + assert computed.shape == (2,) + np.testing.assert_allclose(computed, [1.0, 4.0], rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_area_polygon_primitive_batch_2d(dtype): + """Area of a batch of 2D Polygon primitives.""" + data = np.array([ + [[0, 0], [1, 0], [1, 1], [0, 1]], # area = 1.0 + [[0, 0], [3, 0], [3, 2], [0, 2]], # area = 6.0 + ], dtype=dtype) + polys = tf.Polygon(data) + + computed = tf.area(polys) + assert isinstance(computed, np.ndarray) + assert computed.shape == (2,) + np.testing.assert_allclose(computed, [1.0, 6.0], rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_area_polygon_primitive_single_still_works(dtype): + """Single Polygon still works after refactor.""" + poly = tf.Polygon(np.array([ + [0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0] + ], dtype=dtype)) + + computed = tf.area(poly) + np.testing.assert_allclose(computed, 1.0, rtol=1e-5) + + +# ============================================================================== +# Triangle/Polygon Primitive mean_edge_length Tests +# ============================================================================== + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_mean_edge_length_triangle_single(dtype): + """Mean edge length of a unit right triangle.""" + tri = tf.Triangle(np.array([ + [0, 0, 0], + [1, 0, 0], + [0, 1, 0] + ], dtype=dtype)) + + computed = tf.mean_edge_length(tri) + # Edges: 1, 1, sqrt(2) → mean = (2 + sqrt(2)) / 3 + expected = (2.0 + np.sqrt(2)) / 3.0 + np.testing.assert_allclose(computed, expected, rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_mean_edge_length_equilateral_triangle(dtype): + """Equilateral triangle with side=2 has mean edge length 2.""" + tri = tf.Triangle(np.array([ + [0, 0, 0], + [2, 0, 0], + [1, np.sqrt(3), 0] + ], dtype=dtype)) + + computed = tf.mean_edge_length(tri) + np.testing.assert_allclose(computed, 2.0, rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_mean_edge_length_triangle_batch(dtype): + """Mean edge length across a batch of triangles.""" + data = np.array([ + [[0, 0, 0], [1, 0, 0], [0, 1, 0]], + [[0, 0, 0], [2, 0, 0], [0, 2, 0]], + ], dtype=dtype) + tris = tf.Triangle(data) + + computed = tf.mean_edge_length(tris) + # Tri 1: edges 1, 1, sqrt(2). Tri 2: edges 2, 2, 2*sqrt(2) + # Total 6 edges, mean = (1 + 1 + sqrt(2) + 2 + 2 + 2*sqrt(2)) / 6 + expected = (6.0 + 3 * np.sqrt(2)) / 6.0 + np.testing.assert_allclose(computed, expected, rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_mean_edge_length_polygon_single(dtype): + """Mean edge length of a unit square polygon.""" + poly = tf.Polygon(np.array([ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0] + ], dtype=dtype)) + + computed = tf.mean_edge_length(poly) + np.testing.assert_allclose(computed, 1.0, rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_mean_edge_length_polygon_batch(dtype): + """Mean edge length across a batch of polygons.""" + data = np.array([ + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], # unit square, all edges = 1 + [[0, 0, 0], [2, 0, 0], [2, 2, 0], [0, 2, 0]], # 2x2 square, all edges = 2 + ], dtype=dtype) + polys = tf.Polygon(data) + + computed = tf.mean_edge_length(polys) + # 4 edges of length 1 + 4 edges of length 2 → mean = 12/8 = 1.5 + np.testing.assert_allclose(computed, 1.5, rtol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_mean_edge_length_triangle_2d(dtype): + """Mean edge length works for 2D triangles.""" + tri = tf.Triangle(np.array([ + [0, 0], + [1, 0], + [0, 1] + ], dtype=dtype)) + + computed = tf.mean_edge_length(tri) + expected = (2.0 + np.sqrt(2)) / 3.0 + np.testing.assert_allclose(computed, expected, rtol=1e-5) + + # ============================================================================== # Main # ============================================================================== diff --git a/python/tests/test_mesh_neighbor_search.py b/python/tests/test_mesh_neighbor_search.py index 268cb12..6dbf934 100644 --- a/python/tests/test_mesh_neighbor_search.py +++ b/python/tests/test_mesh_neighbor_search.py @@ -83,9 +83,9 @@ def test_mesh_neighbor_search_point(index_dtype, real_dtype, dims, ngon): # Create query point near origin if dims == 2: - query = tf.Point([0.1, 0.1]) + query = tf.Point(np.array([0.1, 0.1], dtype=real_dtype)) else: # dims == 3 - query = tf.Point([0.1, 0.1, 0.1]) + query = tf.Point(np.array([0.1, 0.1, 0.1], dtype=real_dtype)) # Query mesh idx, dist2, closest_pt = tf.neighbor_search(mesh, query) @@ -114,9 +114,9 @@ def test_mesh_neighbor_search_segment(index_dtype, real_dtype, dims, ngon): # Create segment query if dims == 2: - query = tf.Segment([[0.1, 0.1], [0.2, 0.2]]) + query = tf.Segment(np.array([[0.1, 0.1], [0.2, 0.2]], dtype=real_dtype)) else: # dims == 3 - query = tf.Segment([[0.1, 0.1, 0.1], [0.2, 0.2, 0.2]]) + query = tf.Segment(np.array([[0.1, 0.1, 0.1], [0.2, 0.2, 0.2]], dtype=real_dtype)) idx, dist2, closest_pt = tf.neighbor_search(mesh, query) @@ -136,9 +136,9 @@ def test_mesh_neighbor_search_polygon(index_dtype, real_dtype, dims, ngon): # Create polygon query (triangle) if dims == 2: - query = tf.Polygon([[0.1, 0.1], [0.3, 0.1], [0.2, 0.3]]) + query = tf.Polygon(np.array([[0.1, 0.1], [0.3, 0.1], [0.2, 0.3]], dtype=real_dtype)) else: # dims == 3 - query = tf.Polygon([[0.1, 0.1, 0.1], [0.3, 0.1, 0.1], [0.2, 0.3, 0.1]]) + query = tf.Polygon(np.array([[0.1, 0.1, 0.1], [0.3, 0.1, 0.1], [0.2, 0.3, 0.1]], dtype=real_dtype)) idx, dist2, closest_pt = tf.neighbor_search(mesh, query) @@ -158,9 +158,9 @@ def test_mesh_neighbor_search_ray(index_dtype, real_dtype, dims, ngon): # Create ray query if dims == 2: - query = tf.Ray(origin=[0.1, 0.1], direction=[1.0, 1.0]) + query = tf.Ray(origin=np.array([0.1, 0.1], dtype=real_dtype), direction=np.array([1.0, 1.0], dtype=real_dtype)) else: # dims == 3 - query = tf.Ray(origin=[0.1, 0.1, 0.1], direction=[1.0, 1.0, 1.0]) + query = tf.Ray(origin=np.array([0.1, 0.1, 0.1], dtype=real_dtype), direction=np.array([1.0, 1.0, 1.0], dtype=real_dtype)) idx, dist2, closest_pt = tf.neighbor_search(mesh, query) @@ -180,9 +180,9 @@ def test_mesh_neighbor_search_line(index_dtype, real_dtype, dims, ngon): # Create line query if dims == 2: - query = tf.Line(data=[[0.1, 0.1], [1.0, 1.0]]) + query = tf.Line(data=np.array([[0.1, 0.1], [1.0, 1.0]], dtype=real_dtype)) else: # dims == 3 - query = tf.Line(data=[[0.1, 0.1, 0.1], [1.0, 1.0, 1.0]]) + query = tf.Line(data=np.array([[0.1, 0.1, 0.1], [1.0, 1.0, 1.0]], dtype=real_dtype)) idx, dist2, closest_pt = tf.neighbor_search(mesh, query) @@ -202,9 +202,9 @@ def test_mesh_neighbor_search_knn_point(index_dtype, real_dtype, dims, ngon): # Create query point if dims == 2: - query = tf.Point([0.5, 0.5]) + query = tf.Point(np.array([0.5, 0.5], dtype=real_dtype)) else: # dims == 3 - query = tf.Point([0.5, 0.5, 0.5]) + query = tf.Point(np.array([0.5, 0.5, 0.5], dtype=real_dtype)) # Query for k=2 nearest faces k = 2 @@ -232,7 +232,7 @@ def test_mesh_neighbor_search_with_radius_2d_tri(index_dtype, real_dtype): """Test mesh neighbor search with radius constraint""" mesh = create_2d_triangle_mesh(index_dtype, real_dtype) - query = tf.Point([0.1, 0.1]) + query = tf.Point(np.array([0.1, 0.1], dtype=real_dtype)) # Test with large radius - should find something idx1, dist2_1, pt1 = tf.neighbor_search(mesh, query, radius=10.0) @@ -257,7 +257,7 @@ def test_mesh_neighbor_search_knn_with_radius_3d_tri(index_dtype, real_dtype): """Test mesh KNN search with radius constraint""" mesh = create_3d_triangle_mesh(index_dtype, real_dtype) - query = tf.Point([0.5, 0.5, 0.5]) + query = tf.Point(np.array([0.5, 0.5, 0.5], dtype=real_dtype)) # Test KNN with large radius results = tf.neighbor_search(mesh, query, k=2, radius=10.0) @@ -272,7 +272,7 @@ def test_mesh_neighbor_search_with_radius_dynamic(index_dtype, real_dtype): """Test dynamic mesh neighbor search with radius constraint""" mesh = create_3d_dynamic_mesh(index_dtype, real_dtype) - query = tf.Point([0.1, 0.1, 0.1]) + query = tf.Point(np.array([0.1, 0.1, 0.1], dtype=real_dtype)) # Test with large radius - should find something idx1, dist2_1, pt1 = tf.neighbor_search(mesh, query, radius=10.0) diff --git a/python/tests/test_offset_blocked_array.py b/python/tests/test_offset_blocked_array.py index 61cf670..ca2faca 100644 --- a/python/tests/test_offset_blocked_array.py +++ b/python/tests/test_offset_blocked_array.py @@ -156,7 +156,7 @@ def test_mesh_quad_neighbor_search(index_dtype, real_dtype): ) mesh = tf.Mesh(tf.as_offset_blocked(quads), points) - result = tf.neighbor_search(mesh, tf.Point([0.5, 0.5, 0.0])) + result = tf.neighbor_search(mesh, tf.Point(np.array([0.5, 0.5, 0.0], dtype=real_dtype))) assert result is not None diff --git a/python/tests/test_point_cloud_neighbor_search.py b/python/tests/test_point_cloud_neighbor_search.py index 160e447..cb23e78 100644 --- a/python/tests/test_point_cloud_neighbor_search.py +++ b/python/tests/test_point_cloud_neighbor_search.py @@ -16,7 +16,7 @@ def test_neighbor_search_point_2d(): # Query with a point close to [0, 0] # [0.1, 0.1] becomes float64 from numpy, but dispatch will convert to match cloud - query = tf.Point([0.1, 0.1]) + query = tf.Point(np.array([0.1, 0.1], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) assert idx == 0 # Should find point at [0, 0] @@ -29,7 +29,7 @@ def test_neighbor_search_point_3d(): points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float32) cloud = tf.PointCloud(points) - query = tf.Point([0.2, 0.2, 0.2]) + query = tf.Point(np.array([0.2, 0.2, 0.2], dtype=np.float32)) # Test without radius idx, dist2, closest_pt = tf.neighbor_search(cloud, query) @@ -55,7 +55,7 @@ def test_neighbor_search_knn_point(): points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float32) cloud = tf.PointCloud(points) - query = tf.Point([0.1, 0.1, 0.1]) + query = tf.Point(np.array([0.1, 0.1, 0.1], dtype=np.float32)) # Test WITHOUT radius (radius=None) results = tf.neighbor_search(cloud, query, k=3) @@ -90,7 +90,7 @@ def test_neighbor_search_with_radius(): points = np.array([[0, 0, 0], [10, 0, 0], [0, 10, 0]], dtype=np.float32) cloud = tf.PointCloud(points) - query = tf.Point([0.5, 0.5, 0.5]) + query = tf.Point(np.array([0.5, 0.5, 0.5], dtype=np.float32)) # Search with small radius - should find the origin idx, dist2, closest_pt = tf.neighbor_search(cloud, query, radius=2.0) @@ -106,7 +106,7 @@ def test_neighbor_search_knn_with_radius(): points = np.array([[0, 0, 0], [1, 0, 0], [10, 0, 0], [0, 10, 0]], dtype=np.float32) cloud = tf.PointCloud(points) - query = tf.Point([0.0, 0.0, 0.0]) + query = tf.Point(np.array([0.0, 0.0, 0.0], dtype=np.float32)) # Request 4 neighbors but limit radius to exclude far points results = tf.neighbor_search(cloud, query, radius=5.0, k=4) @@ -123,7 +123,7 @@ def test_neighbor_search_segment_2d(): cloud = tf.PointCloud(points) # Segment from [0.5, 0] to [0.5, 1] - query = tf.Segment([[0.5, 0], [0.5, 1]]) + query = tf.Segment(np.array([[0.5, 0], [0.5, 1]], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) # Should find one of the points at distance 0.5 @@ -136,7 +136,7 @@ def test_neighbor_search_segment_3d(): cloud = tf.PointCloud(points) # Segment along x-axis - query = tf.Segment([[0, 0.5, 0], [2, 0.5, 0]]) + query = tf.Segment(np.array([[0, 0.5, 0], [2, 0.5, 0]], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) # Point at [0,0,0] or [2,0,0] should be nearest @@ -149,7 +149,7 @@ def test_neighbor_search_polygon_2d(): cloud = tf.PointCloud(points) # Triangle around origin - query = tf.Polygon([[1, 1], [2, 1], [1.5, 2]]) + query = tf.Polygon(np.array([[1, 1], [2, 1], [1.5, 2]], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) # Origin should be closest @@ -162,7 +162,7 @@ def test_neighbor_search_polygon_3d(): cloud = tf.PointCloud(points) # Triangle in xy-plane - query = tf.Polygon([[1, 1, 0], [2, 1, 0], [1.5, 2, 0]]) + query = tf.Polygon(np.array([[1, 1, 0], [2, 1, 0], [1.5, 2, 0]], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) # Origin should be closest @@ -175,8 +175,7 @@ def test_neighbor_search_ray_3d(): cloud = tf.PointCloud(points) # Ray from [0, 0.5, 0] along x-axis - # Float lists become float64 from numpy, Ray keeps float64 - query = tf.Ray(origin=[0, 0.5, 0], direction=[1, 0, 0]) + query = tf.Ray(origin=np.array([0, 0.5, 0], dtype=np.float32), direction=np.array([1, 0, 0], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) # Some point should be found @@ -191,8 +190,7 @@ def test_neighbor_search_line_3d(): cloud = tf.PointCloud(points) # Line through [0, 0.5, 0] along x-axis - # Float lists become float64 from numpy, Line keeps float64 - query = tf.Line(origin=[0, 0.5, 0], direction=[1, 0, 0]) + query = tf.Line(origin=np.array([0, 0.5, 0], dtype=np.float32), direction=np.array([1, 0, 0], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) # Some point should be found @@ -233,8 +231,7 @@ def test_neighbor_search_dimension_mismatch(): points_3d = np.array([[0, 0, 0], [1, 0, 0]], dtype=np.float32) cloud_3d = tf.PointCloud(points_3d) - # Integer list [0, 0] will be converted to float32 by Point - query_2d = tf.Point([0, 0]) + query_2d = tf.Point(np.array([0, 0], dtype=np.float32)) with pytest.raises(ValueError, match="Dimension mismatch"): tf.neighbor_search(cloud_3d, query_2d) @@ -244,7 +241,7 @@ def test_neighbor_search_invalid_k(): """Test that invalid k value raises error""" points = np.array([[0, 0, 0], [1, 0, 0]], dtype=np.float32) cloud = tf.PointCloud(points) - query = tf.Point([0, 0, 0]) + query = tf.Point(np.array([0, 0, 0], dtype=np.float32)) with pytest.raises(ValueError, match="k must be a positive integer"): tf.neighbor_search(cloud, query, k=0) @@ -258,7 +255,7 @@ def test_neighbor_search_knn_all_points(): points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]], dtype=np.float32) cloud = tf.PointCloud(points) - query = tf.Point([0, 0, 0]) + query = tf.Point(np.array([0, 0, 0], dtype=np.float32)) results = tf.neighbor_search(cloud, query, k=3) assert len(results) == 3 @@ -272,7 +269,7 @@ def test_neighbor_search_knn_more_than_available(): points = np.array([[0, 0, 0], [1, 0, 0]], dtype=np.float32) cloud = tf.PointCloud(points) - query = tf.Point([0, 0, 0]) + query = tf.Point(np.array([0, 0, 0], dtype=np.float32)) results = tf.neighbor_search(cloud, query, k=10) # Should return at most 2 results (all available points) @@ -300,7 +297,7 @@ def test_neighbor_search_with_transformation_2d(): cloud.transformation = transformation # Query point at [5.1, 3.1] in world coordinates - should be close to transformed origin [5, 3] - query = tf.Point([5.1, 3.1]) + query = tf.Point(np.array([5.1, 3.1], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) assert idx == 0 # Should find the origin point (index 0) @@ -333,7 +330,7 @@ def test_neighbor_search_with_transformation_3d(): cloud.transformation = transformation # Query point at [10.2, 5.1, 2.1] in world coordinates - close to transformed origin [10, 5, 2] - query = tf.Point([10.2, 5.1, 2.1]) + query = tf.Point(np.array([10.2, 5.1, 2.1], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) assert idx == 0 # Should find the origin point (index 0) @@ -362,7 +359,7 @@ def test_neighbor_search_with_transformation_knn(): cloud.transformation = transformation # Query at transformed origin in world coordinates - query = tf.Point([5.0, 5.0, 5.0]) + query = tf.Point(np.array([5.0, 5.0, 5.0], dtype=np.float32)) results = tf.neighbor_search(cloud, query, k=3) assert len(results) == 3 @@ -393,7 +390,7 @@ def test_neighbor_search_transformation_clear(): cloud.transformation = transformation # Query should find point at transformed location [10, 0, 0] - query1 = tf.Point([10.1, 0, 0]) + query1 = tf.Point(np.array([10.1, 0, 0], dtype=np.float32)) idx1, dist2_1, _ = tf.neighbor_search(cloud, query1) assert idx1 == 0 assert np.isclose(dist2_1, 0.01, atol=1e-5) @@ -402,7 +399,7 @@ def test_neighbor_search_transformation_clear(): cloud.transformation = None # Now query at original location [0, 0, 0] - query2 = tf.Point([0.1, 0, 0]) + query2 = tf.Point(np.array([0.1, 0, 0], dtype=np.float32)) idx2, dist2_2, _ = tf.neighbor_search(cloud, query2) assert idx2 == 0 assert np.isclose(dist2_2, 0.01, atol=1e-5) @@ -431,7 +428,7 @@ def test_neighbor_search_with_rotation_transformation(): cloud.transformation = transformation # Query point at [0, 1.1, 0] in world coordinates - close to rotated [1, 0, 0] - query = tf.Point([0, 1.1, 0]) + query = tf.Point(np.array([0, 1.1, 0], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) assert idx == 0 # Should find first point @@ -459,7 +456,7 @@ def test_neighbor_search_with_scale_transformation(): cloud.transformation = transformation # Query point at [2.1, 0, 0] in world coordinates - close to scaled [1, 0, 0] - query = tf.Point([2.1, 0, 0]) + query = tf.Point(np.array([2.1, 0, 0], dtype=np.float32)) idx, dist2, closest_pt = tf.neighbor_search(cloud, query) assert idx == 0 # Should find first point diff --git a/python/tests/test_transformed.py b/python/tests/test_transformed.py index 61af66c..626c739 100644 --- a/python/tests/test_transformed.py +++ b/python/tests/test_transformed.py @@ -386,6 +386,205 @@ def test_transformed_dimension_mismatch(): tf.transformed(pt_3d, T_2d) +# ============================================================================== +# Batch Tests +# ============================================================================== + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_point_3d(dtype): + """Test batch point transformation""" + pts = tf.Point(np.array([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ], dtype=dtype)) + assert pts.is_batch + T = create_rotation_z_3d(90, dtype) + + result = tf.transformed(pts, T) + + assert result.is_batch + assert result.count == 3 + expected = np.array([ + [0, 1, 0], + [-1, 0, 0], + [0, 0, 1] + ], dtype=dtype) + assert np.allclose(result.data, expected, atol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_point_2d(dtype): + """Test batch 2D point transformation""" + pts = tf.Point(np.array([ + [1, 0], + [0, 1], + ], dtype=dtype)) + assert pts.is_batch + T = create_rotation_translation_2d(90, 2, 3, dtype) + + result = tf.transformed(pts, T) + + assert result.is_batch + assert result.count == 2 + # [1,0] -> [0,1] + [2,3] = [2,4] + # [0,1] -> [-1,0] + [2,3] = [1,3] + expected = np.array([[2, 4], [1, 3]], dtype=dtype) + assert np.allclose(result.data, expected, atol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_segment_3d(dtype): + """Test batch segment transformation""" + segs = tf.Segment(np.array([ + [[1, 0, 0], [2, 0, 0]], + [[0, 1, 0], [0, 2, 0]], + ], dtype=dtype)) + assert segs.is_batch + T = create_rotation_z_3d(90, dtype) + + result = tf.transformed(segs, T) + + assert result.is_batch + assert result.count == 2 + # seg1: [1,0,0]->[0,1,0], [2,0,0]->[0,2,0] + # seg2: [0,1,0]->[-1,0,0], [0,2,0]->[-2,0,0] + expected = np.array([ + [[0, 1, 0], [0, 2, 0]], + [[-1, 0, 0], [-2, 0, 0]], + ], dtype=dtype) + assert np.allclose(result.data, expected, atol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_ray_3d(dtype): + """Test batch ray transformation""" + rays = tf.Ray(data=np.array([ + [[0, 0, 0], [1, 0, 0]], + [[1, 0, 0], [0, 1, 0]], + ], dtype=dtype)) + assert rays.is_batch + T = create_rotation_z_3d(90, dtype) + + result = tf.transformed(rays, T) + + assert result.is_batch + assert result.count == 2 + # ray1: origin [0,0,0]->[0,0,0], dir [1,0,0]->[0,1,0] + # ray2: origin [1,0,0]->[0,1,0], dir [0,1,0]->[-1,0,0] + assert np.allclose(result.data[0, 0], [0, 0, 0], atol=1e-5) + assert np.allclose(result.data[0, 1], [0, 1, 0], atol=1e-5) + assert np.allclose(result.data[1, 0], [0, 1, 0], atol=1e-5) + assert np.allclose(result.data[1, 1], [-1, 0, 0], atol=1e-5) + # Directions should be unit vectors + assert np.allclose(np.linalg.norm(result.data[:, 1], axis=-1), 1.0, atol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_line_3d(dtype): + """Test batch line transformation""" + lines = tf.Line(data=np.array([ + [[0, 0, 0], [1, 0, 0]], + [[0, 0, 0], [0, 0, 1]], + ], dtype=dtype)) + assert lines.is_batch + T = create_rotation_z_3d(90, dtype) + + result = tf.transformed(lines, T) + + assert result.is_batch + assert result.count == 2 + # line1 dir [1,0,0] -> [0,1,0] + # line2 dir [0,0,1] -> [0,0,1] (z-axis unaffected by z-rotation) + assert np.allclose(result.data[0, 1], [0, 1, 0], atol=1e-5) + assert np.allclose(result.data[1, 1], [0, 0, 1], atol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_aabb_3d(dtype): + """Test batch AABB transformation""" + batch_data = np.array([ + [[0, 0, 0], [1, 1, 1]], + [[2, 0, 0], [3, 1, 1]], + ], dtype=dtype) + boxes = tf.AABB(batch_data) + assert boxes.is_batch + + T = create_rotation_translation_z_3d(0, 10, 0, 0, dtype) # translate x+10 + + result = tf.transformed(boxes, T) + + assert result.is_batch + assert result.count == 2 + # box1: [0,0,0]-[1,1,1] -> [10,0,0]-[11,1,1] + # box2: [2,0,0]-[3,1,1] -> [12,0,0]-[13,1,1] + assert np.allclose(result.data[0, 0], [10, 0, 0], atol=1e-5) + assert np.allclose(result.data[0, 1], [11, 1, 1], atol=1e-5) + assert np.allclose(result.data[1, 0], [12, 0, 0], atol=1e-5) + assert np.allclose(result.data[1, 1], [13, 1, 1], atol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_polygon_3d(dtype): + """Test batch polygon transformation""" + polys = tf.Polygon(np.array([ + [[1, 0, 0], [0, 1, 0], [0, 0, 0]], + [[2, 0, 0], [0, 2, 0], [0, 0, 0]], + ], dtype=dtype)) + assert polys.is_batch + T = create_rotation_z_3d(90, dtype) + + result = tf.transformed(polys, T) + + assert result.is_batch + assert result.count == 2 + # poly1 vertex0: [1,0,0] -> [0,1,0] + assert np.allclose(result.data[0, 0], [0, 1, 0], atol=1e-5) + # poly2 vertex0: [2,0,0] -> [0,2,0] + assert np.allclose(result.data[1, 0], [0, 2, 0], atol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_plane_3d(dtype): + """Test batch plane transformation""" + planes = tf.Plane(np.array([ + [0, 0, 1, 0], # z=0 plane + [0, 0, 1, -5], # z=5 plane + ], dtype=dtype)) + assert planes.is_batch + + # Translate up by 10 + T = create_rotation_translation_z_3d(0, 0, 0, 10, dtype) + + result = tf.transformed(planes, T) + + assert result.is_batch + assert result.count == 2 + # z=0 plane moved up: normal [0,0,1], d = -10 + assert np.allclose(result.data[0, :3], [0, 0, 1], atol=1e-5) + assert np.isclose(result.data[0, 3], -10, atol=1e-5) + # z=5 plane moved up: normal [0,0,1], d = -15 + assert np.allclose(result.data[1, :3], [0, 0, 1], atol=1e-5) + assert np.isclose(result.data[1, 3], -15, atol=1e-5) + + +@pytest.mark.parametrize("dtype", REAL_DTYPES) +def test_transformed_batch_preserves_distances(dtype): + """Test that transformation preserves distances between batch elements.""" + pts = tf.Point(np.array([ + [0, 0, 0], + [3, 0, 0], + ], dtype=dtype)) + T = create_rotation_translation_z_3d(45, 10, 20, 30, dtype) + + result = tf.transformed(pts, T) + + # Distance between transformed points should equal original + original_dist = np.linalg.norm(pts.data[0] - pts.data[1]) + transformed_dist = np.linalg.norm(result.data[0] - result.data[1]) + assert np.isclose(original_dist, transformed_dist, atol=1e-4) + + if __name__ == "__main__": # Run tests with verbose output import sys diff --git a/typescript/README.md b/typescript/README.md index c2027f0..888aea4 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -10,6 +10,18 @@ Real-time geometric processing in TypeScript. WASM-backed NDArrays with vectoriz npm install @polydera/trueform ``` +## Requirements + +- **Node.js** 18+ or any modern browser with WebAssembly support +- **SharedArrayBuffer** — required for multithreading. In browsers, the page must be served with: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +Node.js enables SharedArrayBuffer by default. + ## Quick Tour **NDArray** — WASM-backed numerical arrays: diff --git a/typescript/cpp/include/trueform/ts/spatial/prim_dispatch.hpp b/typescript/cpp/include/trueform/ts/spatial/prim_dispatch.hpp index 73a9df7..f1a6c90 100644 --- a/typescript/cpp/include/trueform/ts/spatial/prim_dispatch.hpp +++ b/typescript/cpp/include/trueform/ts/spatial/prim_dispatch.hpp @@ -171,7 +171,6 @@ auto dispatch_single(Fn &&fn, const float *ptr, prim_type t, int n_verts = 0) case prim_type::polygon: return fn(as_polygon(ptr, n_verts)); } - __builtin_unreachable(); } // ============================================================================ diff --git a/typescript/package-lock.json b/typescript/package-lock.json index ebbd12f..418b916 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -1,12 +1,12 @@ { - "name": "trueform", - "version": "0.1.0", + "name": "@polydera/trueform", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "trueform", - "version": "0.1.0", + "name": "@polydera/trueform", + "version": "0.7.0", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "esbuild": "^0.27.3", diff --git a/typescript/package.json b/typescript/package.json index 6ec9f70..af709be 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -51,6 +51,9 @@ "email": "info@polydera.com", "url": "https://polydera.com" }, + "engines": { + "node": ">=18" + }, "devDependencies": { "esbuild": "^0.27.3", "typescript": "^5.9.3" From b2827cd980c3513f657c54173bcc421ef54c16df Mon Sep 17 00:00:00 2001 From: ZigaSajovic Date: Sat, 28 Feb 2026 13:32:47 +0100 Subject: [PATCH 3/3] Add Node.js test runner, separate Docker build dir, untrack package-lock --- .gitignore | 1 + docs/content/index.md | 4 +- docs/content/ts/1.getting-started/1.index.md | 3 + docs/wasm-examples/extras/build_web.sh | 3 +- typescript/build.mjs | 2 +- typescript/package-lock.json | 515 ------------------- typescript/tests/harness.mjs | 1 + typescript/tests/run.mjs | 45 ++ 8 files changed, 55 insertions(+), 519 deletions(-) delete mode 100644 typescript/package-lock.json create mode 100644 typescript/tests/run.mjs diff --git a/.gitignore b/.gitignore index 9546639..4e36475 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ __pycache__/ include/trueform/version.hpp typescript/LICENSE typescript/LICENSE.noncommercial +typescript/package-lock.json diff --git a/docs/content/index.md b/docs/content/index.md index 46084e5..d18dcc9 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -173,7 +173,7 @@ Integrations title: Python to: /py/getting-started --- - Real-time geometric processing in your Python workflow. NumPy in, NumPy out. + Real-time geometric processing in your Python workflow. Enriched NumPy arrays with vectorized spatial queries, mesh booleans, and topology. NumPy in, NumPy out. ::: :::card @@ -182,7 +182,7 @@ Integrations title: TypeScript to: /ts/getting-started --- - Real-time geometric processing in the browser and Node.js. Vectorized NDArrays with vectorized numerical and geometric queries. + Real-time geometric processing in the browser and Node.js. WASM-backed NDArrays with vectorized numerical and geometric queries. ::: :::card diff --git a/docs/content/ts/1.getting-started/1.index.md b/docs/content/ts/1.getting-started/1.index.md index dce04b6..d169f44 100644 --- a/docs/content/ts/1.getting-started/1.index.md +++ b/docs/content/ts/1.getting-started/1.index.md @@ -7,6 +7,9 @@ navigation: `trueform` is a TypeScript library for real-time geometric processing. WASM-backed NDArrays with support for vectorized numerical operations and spatial queries. Mesh booleans, registration, remeshing — at native speed in the browser and Node.js. Robust to non-manifold flaps, inconsistent winding, and pipeline artifacts. NDArrays in, NDArrays out. +::try-it-banner +:: + NDArrays become typed primitives — single or batched. Primitives compose into forms with cached geometric and topological structures. ::ts-query-diagram diff --git a/docs/wasm-examples/extras/build_web.sh b/docs/wasm-examples/extras/build_web.sh index ab7e96f..884ef29 100755 --- a/docs/wasm-examples/extras/build_web.sh +++ b/docs/wasm-examples/extras/build_web.sh @@ -56,7 +56,8 @@ TS_DIR="${REPO_ROOT}/typescript" if [[ -d "${TS_DIR}" ]]; then echo "Building TypeScript SDK..." - rm -rf "${REPO_ROOT}/build-wasm" + export TF_WASM_BUILD_DIR="build-wasm-docker" + rm -rf "${REPO_ROOT}/${TF_WASM_BUILD_DIR}" cd "${TS_DIR}" npm install npm run build diff --git a/typescript/build.mjs b/typescript/build.mjs index 148ae2d..98d084b 100644 --- a/typescript/build.mjs +++ b/typescript/build.mjs @@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, ".."); -const buildDir = resolve(root, "build-wasm"); +const buildDir = resolve(root, process.env.TF_WASM_BUILD_DIR || "build-wasm"); // -- WASM build (emcmake + cmake) -- if (!existsSync(resolve(buildDir, "CMakeCache.txt"))) { diff --git a/typescript/package-lock.json b/typescript/package-lock.json deleted file mode 100644 index 418b916..0000000 --- a/typescript/package-lock.json +++ /dev/null @@ -1,515 +0,0 @@ -{ - "name": "@polydera/trueform", - "version": "0.7.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@polydera/trueform", - "version": "0.7.0", - "license": "SEE LICENSE IN LICENSE", - "devDependencies": { - "esbuild": "^0.27.3", - "typescript": "^5.9.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - } - } -} diff --git a/typescript/tests/harness.mjs b/typescript/tests/harness.mjs index 4667b48..a86ab3b 100644 --- a/typescript/tests/harness.mjs +++ b/typescript/tests/harness.mjs @@ -20,6 +20,7 @@ export function test(name, fn) { } export function log(text, cls = "") { + if (typeof document === "undefined") return; const div = document.createElement("div"); div.className = cls; div.textContent = text; diff --git a/typescript/tests/run.mjs b/typescript/tests/run.mjs new file mode 100644 index 0000000..491d970 --- /dev/null +++ b/typescript/tests/run.mjs @@ -0,0 +1,45 @@ +// ============================================================================ +// Node.js test runner — runs all tests, prints only failures + summary +// Usage: node tests/run.mjs +// ============================================================================ + +import { tests, setTf } from "./harness.mjs"; + +// Import test modules (same as runner.mjs, minus browser-only ones) +await import("./test_ndarray.mjs"); +await import("./test_primitives.mjs"); +await import("./test_mesh.mjs"); +await import("./test_spatial.mjs"); +await import("./test_geometry.mjs"); +await import("./test_mesh_setters.mjs"); +await import("./test_point_cloud.mjs"); +await import("./test_registration.mjs"); +await import("./test_remesh.mjs"); +await import("./test_topology.mjs"); +await import("./test_reindex.mjs"); +await import("./test_transformations.mjs"); +await import("./test_clean.mjs"); +await import("./test_cut.mjs"); +await import("./test_intersect.mjs"); +await import("./test_io_roundtrip.mjs"); + +// Load WASM module +const tf = await import("../dist/index.js"); +setTf(tf); + +// Run +let passed = 0, failed = 0; +for (const t of tests) { + if (t._marker) continue; + try { + await t.fn(); + passed++; + } catch (e) { + failed++; + console.log(`FAIL: ${t.name}`); + console.log(` ${e.message}`); + } +} + +console.log(`\n${passed} passed, ${failed} failed`); +process.exit(failed > 0 ? 1 : 0);