diff --git a/.clang-format b/.clang-format index 54e2ee3..4ccf9e4 100644 --- a/.clang-format +++ b/.clang-format @@ -85,10 +85,6 @@ PenaltyBreakString: 1000 PenaltyExcessCharacter: 150 PenaltyReturnTypeOnItsOwnLine: 300 PointerAlignment: Middle -RawStringFormats: - - Delimiter: pb - Language: TextProto - BasedOnStyle: google ReflowComments: true SortIncludes: true SortUsingDeclarations: true @@ -107,4 +103,3 @@ Standard: Cpp11 TabWidth: 2 UseTab: Never ... - diff --git a/.clang-format-common.sh b/.clang-format-common.sh deleted file mode 100644 index ecb6508..0000000 --- a/.clang-format-common.sh +++ /dev/null @@ -1,15 +0,0 @@ -# This script is meant to be sourced from other scripts - -# Check for clang-format, prefer 6.0 if available -if [[ -x "$(command -v clang-format-6.0)" ]]; then - clang_format=clang-format-6.0 -elif [[ -x "$(command -v clang-format)" ]]; then - clang_format=clang-format -else - echo "clang-format or clang-format-6.0 must be installed" - exit 1 -fi - -# Find all source files in the project minus those that are auto-generated or we do not maintain -src_files=`find src tests include -type f \( -name '*.cpp' -or -name '*.h' \)` - diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8a3a22f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake .#mesh-sampling diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ac6b8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77ed47b..3ed4be2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,14 +10,13 @@ on: jobs: clang-format: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v1 - - name: Install clang-format-6.0 + - uses: actions/checkout@v4 + - name: Install clang-format run: | sudo apt-get -qq update - sudo apt-get -qq remove clang-6.0 libclang1-6.0 libclang-common-6.0-dev libllvm6.0 - sudo apt-get -qq install clang-format-6.0 clang-format + sudo apt-get -qq install clang-format-14 - name: Run clang-format-check run: | ./.clang-format-check.sh @@ -27,39 +26,43 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-16.04, ubuntu-18.04] - build-type: [RelWithDebInfo, Release] + os: [ubuntu-22.04, ubuntu-24.04] + build-type: [Debug, RelWithDebInfo] compiler: [gcc, clang] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Install cmake - run: | - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | sudo apt-key add - - sudo apt-add-repository -y 'deb https://apt.kitware.com/ubuntu/ bionic main' - sudo apt-get -qq update - sudo apt-get -qq install cmake - cmake --version - name: Install dependencies uses: jrl-umi3218/github-actions/install-dependencies@master with: compiler: ${{ matrix.compiler }} build-type: ${{ matrix.build-type }} ubuntu: | - apt: libeigen3-dev libpcl-dev libproj-dev libassimp-dev libboost-test-dev doxygen doxygen-latex + apt: libeigen3-dev libqhull-dev libproj-dev libassimp-dev libgtest-dev doxygen doxygen-latex - name: Build and test uses: jrl-umi3218/github-actions/build-cmake-project@master with: compiler: ${{ matrix.compiler }} build-type: ${{ matrix.build-type }} # Checks that finding the library and building against it works - - name: Test consumption - uses: arntanguy/github-actions/build-cmake-project@topic/skip-install - with: - compiler: ${{ matrix.compiler }} - build-type: Release - project-dir: tests/test_mesh_sampling - skip-install: "true" + - name: Check usage + shell: bash + run: | + set -x + export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}" + export CMAKE_PREFIX_PATH="/usr/lib/x86_64-linux-gnu/cmake:${CMAKE_PREFIX_PATH}" + + ./.github/workflows/scripts/test-usage.sh ${{ matrix.build-type }} + check: + if: always() + name: check-build-ubuntu + runs-on: ubuntu-latest + needs: + - build + steps: + - uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/mesh_sampling.recipe b/.github/workflows/mesh_sampling.recipe deleted file mode 100644 index fcab720..0000000 --- a/.github/workflows/mesh_sampling.recipe +++ /dev/null @@ -1,3 +0,0 @@ -# git-build-recipe format 0.4 deb-version {debversion}+{time}+{git-commit} -https://github.com/arntanguy/mesh_sampling @REF@ -run git submodule update --init diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..a0ac072 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,39 @@ +name: "CI - Nix" +on: + push: + branches: + - devel + - master + - main + pull_request: + branches: + - devel + - master + - main +jobs: + nix: + runs-on: "${{ matrix.os }}-latest" + strategy: + fail-fast: false + matrix: + os: [ubuntu] + steps: + - uses: actions/checkout@v6 + - uses: cachix/install-nix-action@v31 + - uses: cachix/cachix-action@v17 + with: + name: mc-rtc-nix + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + extraPullNames: "ros,gepetto" + - run: nix flake check -L + - run: nix build -L + check: + if: always() + name: check-macos-linux-nix + runs-on: ubuntu-latest + needs: + - nix + steps: + - uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index eb25fcb..1f6cba7 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -1,77 +1,38 @@ name: Package mesh_sampling - -# This workflow only runs when pushing to master or pushing a tag -# -# On master, it will: -# - Build packages for selected Debian/Ubuntu distro -# - Upload the packages to https://dl.bintray.com/arntanguy/ppa-head - on: + repository_dispatch: + types: + - package-master + - package-release push: - paths-ignore: - - ".github/workflows/build.yml" branches: - - "**" - + - "**" + tags: + - v* + pull_request: + branches: + - "**" jobs: - # This job build binary packages for Ubuntu - build-packages: - strategy: - fail-fast: false - matrix: - dist: [xenial, bionic, focal] - arch: [amd64] - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v1 - - name: Prepare recipe - run: | - set -x - cp .github/workflows/mesh_sampling.recipe /tmp/mesh_sampling.recipe - export REF=`echo ${{ github.ref }} | sed -e 's@refs/[a-z]*/@@'` - sed -i "s#@REF@#${REF}#" /tmp/mesh_sampling.recipe - echo "Prepared recipe" - echo "###############" - cat /tmp/mesh_sampling.recipe - - name: Build package - uses: arntanguy/github-actions/build-package@topic/cmake3.11 - with: - dist: ${{ matrix.dist }} - arch: ${{ matrix.arch }} - recipe: /tmp/mesh_sampling.recipe - - uses: actions/upload-artifact@v1 - with: - name: packages-${{ matrix.dist }}-${{ matrix.arch }} - path: /tmp/packages-${{ matrix.dist }}-${{ matrix.arch }}/ - # This job upload binary packages for Ubuntu - upload-packages: - needs: build-packages - strategy: - max-parallel: 1 - fail-fast: false - matrix: - dist: [bionic,focal] - arch: [amd64] - runs-on: ubuntu-18.04 - if: github.ref == 'refs/heads/master' && github.repository == 'arntanguy/mesh_sampling' + package: + uses: jrl-umi3218/github-actions/.github/workflows/package-project.yml@master + with: + latest-cmake: true + update-stable-and-head: true + matrix: | + { + "dist": ["jammy", "noble", "resolute"], + "arch": ["amd64"] + } + secrets: + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} + GH_TOKEN: ${{ secrets.GH_PAGES_TOKEN }} + check: + if: always() + name: check-package-ubuntu + runs-on: ubuntu-latest + needs: + - package steps: - - name: Download packages - uses: actions/download-artifact@v1 - with: - name: packages-${{ matrix.dist }}-${{ matrix.arch }} - - name: Upload - uses: jrl-umi3218/github-actions/upload-package@master - with: - dist: ${{ matrix.dist }} - arch: ${{ matrix.arch }} - subject: arntanguy - repo: ppa-head - package: | - name: mesh-sampling - desc: "mesh-sampling Convert CAD models to PCL pointcloud" - licenses: [BSD 2-Clause] - vcs_url: https://github.com/arntanguy/mesh_sampling - version: 1.0.0 - path: packages-${{ matrix.dist }}-${{ matrix.arch }} - BINTRAY_API_KEY: ${{ secrets.BINTRAY_API_KEY }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + - uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/scripts/test-usage.sh b/.github/workflows/scripts/test-usage.sh new file mode 100755 index 0000000..e236b86 --- /dev/null +++ b/.github/workflows/scripts/test-usage.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +readonly CMAKE_BUILD_TYPE=$1 +readonly this_dir=`cd $(dirname $0); pwd` +readonly root_dir=`cd $this_dir/../../../; pwd` +readonly project_dir=$root_dir/tests/test_mesh_sampling + +mkdir -p $project_dir/build + +cd $project_dir/build +cmake ../ -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} || exit 1 +cmake --build . --config ${CMAKE_BUILD_TYPE} || exit 1 diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml new file mode 100644 index 0000000..5d76fd5 --- /dev/null +++ b/.github/workflows/update-flake-lock.yml @@ -0,0 +1,32 @@ +name: update-flake-lock +on: + workflow_dispatch: + schedule: + # runs at 2:00 AM on the 25th day of every month + - cron: '0 2 25 * *' +jobs: + update-flake-inputs: + runs-on: ubuntu-slim + permissions: + contents: write + pull-requests: write + steps: + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.MC_RTC_NIX_APP_ID }} + private-key: ${{ secrets.MC_RTC_NIX_APP_PRIVATE_KEY }} + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ steps.app-token.outputs.token }} + - name: Setup Nix + uses: cachix/install-nix-action@v31 + - name: Update flake inputs + uses: mic92/update-flake-inputs@v1 + with: + github-token: ${{ steps.app-token.outputs.token }} + pr-labels: 'no-changelog' + git-author-name: 'arntanguy[bot]' + git-author-email: '67139+arntanguy@users.noreply.github.com' diff --git a/.gitignore b/.gitignore index 7c286d9..c57ed1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ compile_commands.json *.pyc build/ +.cache/ +.vscode/ +.direnv/ +result diff --git a/.gitmodules b/.gitmodules index 60c9164..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "cmake"] - path = cmake - url = git://github.com/jrl-umi3218/jrl-cmakemodules.git diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 0000000..ae695c9 --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,27 @@ +pull_request_rules: + # automatically queues when a bot pr succeeds (see queue_rules) + - name: merge [bot] PRs when CI pass + conditions: + - or: + - author = dependabot[bot] + - author = gepetto-flake-updater[bot] + - author = mc-rtc-nixpkgs-flake-updater[bot] + - author = github-actions[bot] + - author = pre-commit-ci[bot] + actions: + queue: + + # runs when commenting @mergifyio queue + - name: queue PRs with "queue" label when all CI checks pass + conditions: + - label = queued + actions: + queue: + +# ensure that required CI jobs suceed +queue_rules: + - name: default + merge_conditions: + - check-success = "check-macos-linux-nix" + - check-success = "check-build-ubuntu" + - check-success = "check-package-ubuntu" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..dcc4888 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +ci: + autoupdate_schedule: quarterly +exclude: '^ext/' +repos: +- repo: meta + hooks: + - id: check-useless-excludes + - id: check-hooks-apply +- repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.7 + hooks: + - id: clang-format + types_or: [c++, c] +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: destroyed-symlinks + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: trailing-whitespace + +- repo: https://github.com/cheshirekow/cmake-format-precommit + rev: v0.6.13 + hooks: + - id: cmake-format diff --git a/CMakeLists.txt b/CMakeLists.txt index 79c204d..678a87c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,55 +1,69 @@ -cmake_minimum_required(VERSION 3.11) +cmake_minimum_required(VERSION 3.22) set(PROJECT_NAME mesh_sampling) set(PROJECT_DESCRIPTION "Sample pointcloud from meshes") -set(PROJECT_URL https://github.com/arntanguy/mesh_sampling) -set(PROJECT_VERSION 1.0.0) +set(PROJECT_URL https://github.com/jrl-umi3218/mesh_sampling) +set(PROJECT_VERSION 1.1.0) set(PROJECT_USE_CMAKE_EXPORT TRUE) set(PROJECT_DEBUG_POSTFIX "_d") set(CXX_DISABLE_WERROR 1) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) -include(cmake/base.cmake) -include(cmake/eigen.cmake) -search_for_eigen() +# Check if the submodule cmake have been initialized +set(JRL_CMAKE_MODULES "${CMAKE_CURRENT_LIST_DIR}/cmake") +if(EXISTS "${JRL_CMAKE_MODULES}/base.cmake") + message(STATUS "JRL cmakemodules found in 'cmake/' git submodule") +else() + find_package(jrl-cmakemodules QUIET CONFIG) + if(jrl-cmakemodules_FOUND) + get_property( + JRL_CMAKE_MODULES + TARGET jrl-cmakemodules::jrl-cmakemodules + PROPERTY INTERFACE_INCLUDE_DIRECTORIES) + message(STATUS "JRL cmakemodules found on system at ${JRL_CMAKE_MODULES}") + else() + message(STATUS "JRL cmakemodules not found. Let's fetch it.") + include(FetchContent) + FetchContent_Declare( + "jrl-cmakemodules" + GIT_REPOSITORY "https://github.com/jrl-umi3218/jrl-cmakemodules.git") + FetchContent_MakeAvailable("jrl-cmakemodules") + FetchContent_GetProperties("jrl-cmakemodules" SOURCE_DIR JRL_CMAKE_MODULES) + endif() +endif() +include(${JRL_CMAKE_MODULES}/base.cmake) -project(${PROJECT_NAME} LANGUAGES CXX VERSION ${PROJECT_VERSION}) -list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/CMakeModules) +project( + ${PROJECT_NAME} + LANGUAGES CXX + VERSION ${PROJECT_VERSION}) -# Note: -# Instead of these find_package calls add_project_dependecy should be used to populate the -# mesh_sampling_DEPENDENCIES of the generated mesh_samplingConfig.cmake file -# add_project_dependency(mesh_sampling_3rd_party_assimp REQUIRED) -# add_project_dependency(mesh_sampling_3rd_party_PCL REQUIRED) +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/CMakeModules) -# However, since we are using custom cmake scripts to look for these libraries and create clean modern cmake targets, using add_project_dependency leads to the mesh_samplingConfig script to try and find these dependencies before the path to the required mesh_sampling_3rd_party_*.cmake have been included. -# -# For now, manually add those to the mesh_samplingConfig.cmake script +# Note: Since we are using custom cmake scripts to look at +# mesh_sampling_3rd_party_assimp and create clean modern cmake targets, using +# add_project_dependency leads to the mesh_samplingConfig script to try and find +# these dependencies before the path to the required +# mesh_sampling_3rd_party_*.cmake have been included. find_package(mesh_sampling_3rd_party_assimp REQUIRED) -find_package(mesh_sampling_3rd_party_PCL REQUIRED) -add_project_dependency(Boost COMPONENTS filesystem program_options REQUIRED) - -# Export scripts to find the custom PCL/assimp dependencies -install(FILES - CMakeModules/Findmesh_sampling_3rd_party_PCL.cmake - DESTINATION lib/cmake/mesh_sampling - RENAME mesh_sampling_3rd_party_PCLConfig.cmake -) +find_package(CLI11 REQUIRED) +add_project_dependency(Eigen3 REQUIRED NO_MODULE) +add_project_dependency(Qhull REQUIRED) -install(FILES - CMakeModules/Findmesh_sampling_3rd_party_assimp.cmake +install( + FILES CMakeModules/Findmesh_sampling_3rd_party_assimp.cmake DESTINATION lib/cmake/mesh_sampling - RENAME mesh_sampling_3rd_party_assimpConfig.cmake -) + RENAME mesh_sampling_3rd_party_assimpConfig.cmake) -# Export custom package dependencies to the mesh_samplingConfig script such that they are found -# when importing the package with find_package(mesh_sampling) +# Export custom package dependencies to the mesh_samplingConfig script such that +# they are found when importing the package with find_package(mesh_sampling) set(PACKAGE_EXTRA_MACROS -"include(\"\$\{CMAKE_CURRENT_LIST_DIR\}/mesh_sampling_3rd_party_assimpConfig.cmake\") -include(\"\$\{CMAKE_CURRENT_LIST_DIR\}/mesh_sampling_3rd_party_PCLConfig.cmake\")" + "include(\"\$\{CMAKE_CURRENT_LIST_DIR\}/mesh_sampling_3rd_party_assimpConfig.cmake\")" ) add_subdirectory(src) +add_subdirectory(utils) + if(${BUILD_TESTING}) add_subdirectory(tests) endif() diff --git a/CMakeModules/FindCLI11.cmake b/CMakeModules/FindCLI11.cmake new file mode 100644 index 0000000..87c1c1d --- /dev/null +++ b/CMakeModules/FindCLI11.cmake @@ -0,0 +1,16 @@ +# Try to find system CLI11 first +find_package(CLI11 CONFIG QUIET) + +if(NOT CLI11_FOUND) + include(FetchContent) + FetchContent_Declare( + cli11_proj + QUIET + GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git + GIT_TAG v2.5.0) + FetchContent_MakeAvailable(cli11_proj) + # Alias target for compatibility if needed + if(NOT TARGET CLI11::CLI11 AND TARGET cli11) + add_library(CLI11::CLI11 ALIAS cli11) + endif() +endif() diff --git a/CMakeModules/Findmesh_sampling_3rd_party_PCL.cmake b/CMakeModules/Findmesh_sampling_3rd_party_PCL.cmake deleted file mode 100644 index 9e86cec..0000000 --- a/CMakeModules/Findmesh_sampling_3rd_party_PCL.cmake +++ /dev/null @@ -1,35 +0,0 @@ -# -# Copyright 2015-2019 CNRS-UM LIRMM, CNRS-AIST JRL -# - -# Try to find the GEOS library -# -# In order: -# - try to find_package(geos) (upstream package) -# - try to use geos-config to find the geos installation prefix -# - try to find the geos library -# -# If the library is found, then you can use the PCL::PCL target -# -# This module will use GEOS_PREFIX as an hint to find either the geos-config -# executable or the geos library - -if(NOT TARGET mesh_sampling::PCL) - message(STATUS "Checking for module 'PCL >1.7 (common, io)'") - find_package(PCL 1.7 COMPONENTS common io QUIET) - # On ubuntu 16.04, PCL erroneously links against a non-existant vtkproj4 target - list(REMOVE_ITEM PCL_LIBRARIES "vtkproj4") - if(NOT ${PCL_FOUND}) - message(FATAL_ERROR " Could not find the PCL 1.7 library") - else() - message(STATUS " Found PCL ${PCL_VERSION} (common, io)") - add_library(mesh_sampling::PCL INTERFACE IMPORTED) - set_target_properties(mesh_sampling::PCL PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${PCL_INCLUDE_DIRS}" - ) - # If possible, we use target_link_libraries to avoid problems with link-type keywords, - # see https://github.com/PointCloudLibrary/pcl/issues/2989 - # target_link_libraries on imported libraries is supported only since CMake 3.11 - target_link_libraries(mesh_sampling::PCL INTERFACE ${PCL_LIBRARIES}) - endif() -endif() diff --git a/CMakeModules/Findmesh_sampling_3rd_party_assimp.cmake b/CMakeModules/Findmesh_sampling_3rd_party_assimp.cmake index 58a13c7..c11860c 100644 --- a/CMakeModules/Findmesh_sampling_3rd_party_assimp.cmake +++ b/CMakeModules/Findmesh_sampling_3rd_party_assimp.cmake @@ -2,29 +2,25 @@ # Copyright 2015-2019 CNRS-UM LIRMM, CNRS-AIST JRL # -# Try to find the GEOS library +# Try to find the ASSIMP library # -# In order: -# - try to find_package(geos) (upstream package) -# - try to use geos-config to find the geos installation prefix -# - try to find the geos library +# If the library is found, then you can use the mesh_sampling::assimp target # -# If the library is found, then you can use the PCL::PCL target -# -# This module will use GEOS_PREFIX as an hint to find either the geos-config -# executable or the geos library if(NOT TARGET mesh_sampling::assimp) message(STATUS "Checking for module 'ASSIMP'") find_package(assimp QUIET) - if(NOT ${ASSIMP_FOUND}) + if(NOT ${assimp_FOUND}) message(FATAL_ERROR "Could not find the ASSIMP library") - else() - message(STATUS "Found assimp library: ${ASSIMP_LIBRARIES}") - add_library(mesh_sampling::assimp INTERFACE IMPORTED) - set_target_properties(mesh_sampling::assimp PROPERTIES - #INTERFACE_INCLUDE_DIRECTORIES ${ASSIMP_INCLUDE_DIRS} - INTERFACE_LINK_LIBRARIES "${ASSIMP_LIBRARIES}" - ) endif() + + message( + STATUS + "Found assimp library: ${assimp_VERSION_MAJOR}.${assimp_VERSION_MINOR}.${assimp_VERSION_PATCH}" + ) + add_library(mesh_sampling::assimp INTERFACE IMPORTED) + set_target_properties( + mesh_sampling::assimp + PROPERTIES # INTERFACE_INCLUDE_DIRECTORIES ${ASSIMP_INCLUDE_DIRS} + INTERFACE_LINK_LIBRARIES "${ASSIMP_LIBRARIES}") endif() diff --git a/README.md b/README.md index 3aa49c5..e081fdc 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # mesh_sampling [![License](https://img.shields.io/badge/License-BSD%202--Clause-green.svg)](https://opensource.org/licenses/BSD-2-Clause) -[![CI of mesh_sampling](https://github.com/arntanguy/mesh_sampling/workflows/CI%20of%20mesh_sampling/badge.svg)](https://github.com/arntanguy/mesh_sampling/actions?query=workflow%3A%22CI+of+mesh_sampling%22) -[![Package mesh_sampling](https://github.com/arntanguy/mesh_sampling/workflows/Package%20mesh_sampling/badge.svg)](https://github.com/arntanguy/mesh_sampling/actions?query=workflow%3A%22Package%20mesh_sampling%22) +[![CI of mesh_sampling](https://github.com/jrl-umi3218/mesh_sampling/workflows/CI%20of%20mesh_sampling/badge.svg)](https://github.com/jrl-umi3218/mesh_sampling/actions?query=workflow%3A%22CI+of+mesh_sampling%22) +[![Package mesh_sampling](https://github.com/jrl-umi3218/mesh_sampling/workflows/Package%20mesh_sampling/badge.svg)](https://github.com/jrl-umi3218/mesh_sampling/actions?query=workflow%3A%22Package%20mesh_sampling%22) C++ Implementation of pointcloud generation from mesh sampling methods. -![Sampling example](https://raw.githubusercontent.com/arntanguy/mesh_sampling/master/sample/sampling_example.png) +![Sampling example](https://raw.githubusercontent.com/jrl-umi3218/mesh_sampling/master/sample/sampling_example.png) So far, the following samplers have been implemented: @@ -17,15 +17,22 @@ It is provided as-is, and could probably be optimized should the need arise. Fee ## Installation -### From Ubunu packages (bionic and focal) +### Nix + +Use: +- `nix run github:jrl-umi3218/mesh-sampling -- ` to run the CLI tool + +Or clone this repository `gh repo clone jrl-umi3218/mesh_sampling` and: +- `nix run .#mesh-sampling -- ` to run the CLI tool +- `nix build .#mesh-sampling` to build the project +- `nix develop .#mesh-sampling` to enter a development shell + - `cmake -B build $cmakeFlags && cmake --build build` to build + +### From Ubuntu packages (22.04, 24.04, 26.04) ```sh -# Make sure you have required tools -sudo apt install apt-transport-https lsb-release ca-certificates gnupg -# Add our key -sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key F6D3710D0B5016967A994DFFA650E12EFF6D3EDE -# Add our repository -echo "deb https://dl.bintray.com/arntanguy/ppa-head $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/arntanguy-head.list +# Setup the mirror +curl -1sLf 'https://dl.cloudsmith.io/public/mc-rtc/stable/setup.deb.sh' | sudo -E bash # Install packages sudo apt install libmesh-sampling-dev ``` @@ -33,10 +40,9 @@ sudo apt install libmesh-sampling-dev ### From source Requirements: -- cmake >3.11 +- cmake >3.22 - Eigen3 -- PCL 1.7 -- Boost (program_options and filesystem) +- libqhull-dev If you do not already have a recent cmake installation (>3.11), you will need to install it. On Ubuntu bionic, this can be done by adding the official [Kitware PPA](https://apt.kitware.com/), and updating cmake @@ -65,7 +71,7 @@ sudo apt install cmake You can now build and install this package ``` -git clone --recursive https://github.com/arntanguy/mesh_sampling.git +git clone --recursive https://github.com/jrl-umi3218/mesh_sampling.git cd mesh_sampling mkdir build && cd build cmake .. @@ -77,10 +83,20 @@ sudo make install ### Command-line tool -A simple binary executable `mesh_sampling` is provided. It'll convert any model supported by ASSIMP into its corresponding pointcloud with a given number of points. The command line is of the general form: +A simple binary executable `mesh_sampling` is provided. It'll convert any model supported by ASSIMP into its corresponding pointcloud with a given number of points. The command line is of the general form: + +* From file to file + +``` +mesh_sampling --in /path/to/model. --out /path/to/cloud/cloud. --type xyz_rgb_normal --samples 10000 --binary +``` + +* From folder to folder + +Here the default cloud format is `.qc`. For all files in `/path/to/models` a file `/path/to/cloud/filename.qc` will be generated. ``` -mesh_sampling /path/to/model. /path/to/cloud/cloud. --type xyz_rgb_normal --samples 10000 --binary +mesh_sampling /path/to/models --out /path/to/cloud --type xyz_rgb_normal --samples 10000 --binary ``` Where: @@ -92,15 +108,23 @@ See `mesh_sampling --help` for more options. Example: ```bash -mesh_sampling /path/to/model.dae /tmp/cloud.pcd --type xyz_rgb_normal --samples 10000 --binary +mesh_sampling --in /path/to/model.dae --out /tmp/cloud.pcd --type xyz_rgb_normal --samples 10000 --binary pcl_viewer /tmp/cloud.pcd -normals_scale 5 -normals 1 ``` ### Generating convex files using qhull -To generate convex files, you will first need to convex your mesh to qhull's `.qc` format, then use `qconvex` to generate the convex hull. +To generate convex files, you need to add `--convex` option to the command line. The convex file will be generated in the specified folder as `filename-ch.txt`. + +* From file +```bash +mesh_sampling --in /path/to/model. --out /tmp/test.qc --convex /path --type xyz --samples 10000 ``` -mesh_sampling /path/to/model. /tmp/test.qc --type xyz --samples 10000 -qconvex TI /tmp/test.qc TO /tmp/test-ch.txt Qt o f + +* From folder (check all files with respect to the supported extensions ".ply", ".pcd", ".qc", ".stl", ".dae") +```bash +mesh_sampling --in /path/to/models --out /tmp --convex /tmp --type xyz --samples 10000 ``` + +> Cloud saving is optional, you can remove `--out` option to avoid it. diff --git a/cmake b/cmake deleted file mode 160000 index 811bfc4..0000000 --- a/cmake +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 811bfc472940425a0a96cca7e01331f6cf68fced diff --git a/debian/changelog b/debian/changelog index 87c44ce..bda3ad3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +mesh-sampling (1.1.0-1ubuntu1) unstable; urgency=low + + * Upstream update + + -- Arnaud Tanguy Mon, 15 Jun 2026 19:20:00 +0000 + mesh-sampling (1.0.0-1ubuntu1) unstable; urgency=low * Initial release diff --git a/debian/control b/debian/control index 2ad5497..820cff9 100644 --- a/debian/control +++ b/debian/control @@ -9,26 +9,21 @@ Vcs-Browser: http://github.com/arntanguy/mesh_sampling Build-Depends: debhelper (>= 9), pkg-config, cmake, - libboost-filesystem-dev, - libboost-program-options-dev, - libpcl-dev, libassimp-dev, - libproj-dev, doxygen, - libeigen3-dev (>= 3.2) + libeigen3-dev (>= 3.2), + libgtest-dev, + libqhull-dev Package: libmesh-sampling-dev Section: libdevel Architecture: any Depends: pkg-config, mesh-sampling (= ${binary:Version}), - libboost-filesystem-dev, - libboost-program-options-dev, libeigen3-dev (>= 3.2), - libpcl-common-dev, - libpcl-io-dev, libassimp-dev, - libproj-dev, + libgtest-dev, + libqhull-dev, ${misc:Depends} Description: mesh_sampling Automatic sampling of 3D pointcloud from CAD models mesh_sampling implements samplers that convert from 3D CAD models to PCL pointcloud (PCD) diff --git a/debian/libmesh-sampling-dev.install b/debian/libmesh-sampling-dev.install index 5ce1303..b8ee110 100644 --- a/debian/libmesh-sampling-dev.install +++ b/debian/libmesh-sampling-dev.install @@ -1,3 +1,4 @@ usr/include/* usr/lib/*/pkgconfig/* +usr/lib/*/cmake/* usr/lib/cmake/* diff --git a/debian/mesh-sampling.lintian-overrides b/debian/mesh-sampling.lintian-overrides new file mode 100644 index 0000000..3588de4 --- /dev/null +++ b/debian/mesh-sampling.lintian-overrides @@ -0,0 +1 @@ +mesh-sampling: lacks-ldconfig-trigger usr/lib/libmesh_sampling.so diff --git a/debian/rules b/debian/rules index 041308b..0a49099 100755 --- a/debian/rules +++ b/debian/rules @@ -4,7 +4,7 @@ export DH_VERBOSE=1 %: - dh $@ + dh $@ override_dh_auto_configure: - dh_auto_configure + dh_auto_configure -- -DFETCHCONTENT_FULLY_DISCONNECTED=OFF diff --git a/ext/effolkronium/random.hpp b/ext/effolkronium/random.hpp new file mode 100644 index 0000000..ff0fe92 --- /dev/null +++ b/ext/effolkronium/random.hpp @@ -0,0 +1,1744 @@ +/* +______ ___ _ _______ ________ __ +| ___ \/ _ \ | \ | | _ \ _ | \/ | Random for modern C++ +| |_/ / /_\ \| \| | | | | | | | . . | +| /| _ || . ` | | | | | | | |\/| | version 1.4.0 +| |\ \| | | || |\ | |/ /\ \_/ / | | | +\_| \_\_| |_/\_| \_/___/ \___/\_| |_/ https://github.com/effolkronium/random + +Licensed under the MIT License . +Copyright (c) 2017-2021 effolkronium + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files( the "Software" ), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef EFFOLKRONIUM_RANDOM_HPP +#define EFFOLKRONIUM_RANDOM_HPP + +#include +#include // timed seed +#include +#include +#include +#include // std::forward, std::declval +#include // std::shuffle, std::next, std::distance +#include // std::begin, std::end, std::iterator_traits +#include // std::numeric_limits +#include +#include + +namespace effolkronium { + + namespace details { + /// Key type for getting common type numbers or objects + struct common{ }; + + /// True if type T is applicable by a std::uniform_int_distribution + template + struct is_uniform_int { + static constexpr bool value = + std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value; + }; + + /// True if type T is applicable by a std::uniform_real_distribution + template + struct is_uniform_real { + static constexpr bool value = + std::is_same::value + || std::is_same::value + || std::is_same::value; + }; + + /// True if type T is plain byte + template + struct is_byte { + static constexpr bool value = + std::is_same::value + || std::is_same::value; + }; + + /// True if type T is plain number type + template + struct is_supported_number { + static constexpr bool value = + is_byte ::value + || is_uniform_real::value + || is_uniform_int ::value; + }; + + /// True if type T is character type + template + struct is_supported_character { + static constexpr bool value = + std::is_same::value + || std::is_same::value + || std::is_same::value + || std::is_same::value; + }; + + /// True if type T is iterator + template + struct is_iterator { + private: + static char test( ... ); + + template ::difference_type, + typename = typename std::iterator_traits::pointer, + typename = typename std::iterator_traits::reference, + typename = typename std::iterator_traits::value_type, + typename = typename std::iterator_traits::iterator_category + > static long test( U&& ); + public: + static constexpr bool value = std::is_same< + decltype( test( std::declval( ) ) ), long>::value; + }; + + template + class has_reserve + { + private: + template + static char test(...); + + template + static long test(decltype(&C::reserve)); + public: + static constexpr bool value = std::is_same< + decltype(test(0)), long>::value; + }; + + template + class has_insert + { + private: + template + static char test(...); + + template + static long test(decltype(&C::insert)); + public: + static constexpr bool value = std::is_same< + decltype(test(0)), long>::value; + }; + + } // namespace details + + /// Default seeder for 'random' classes + struct seeder_default { + /// return seed sequence + std::seed_seq& operator() ( ) { + // MinGW issue, std::random_device returns constant value + // Use std::seed_seq with additional seed from C++ chrono + return seed_seq; + } + private: + std::seed_seq seed_seq{ { + static_cast( std::random_device{ }( ) ), + static_cast( std::chrono::steady_clock::now( ) + .time_since_epoch( ).count( ) ), + } }; + }; + + /** + * \brief Base template class for random + * with static API and static internal member storage + * \note it is NOT thread safe but more efficient then + * basic_random_thread_local + * \param Engine A random engine with interface like in the std::mt19937 + * \param Seeder A seeder type which return seed for internal engine + * through operator() + */ + template< + typename Engine, + typename Seeder = seeder_default, + template class IntegerDist = std::uniform_int_distribution, + template class RealDist = std::uniform_real_distribution, + typename BoolDist = std::bernoulli_distribution + > + class basic_random_static { + public: + basic_random_static( ) = delete; + + /// Type of used random number engine + using engine_type = Engine; + + /// Type of used random number seeder + using seeder_type = Seeder; + + /// Type of used integer distribution + template + using integer_dist_t = IntegerDist; + + /// Type of used real distribution + template + using real_dist_t = RealDist; + + /// Type of used bool distribution + using bool_dist_t = BoolDist; + + /// Key type for getting common type numbers or objects + using common = details::common; + + /** + * \return The minimum value + * potentially generated by the random-number engine + */ + static constexpr typename Engine::result_type min( ) { + return Engine::min( ); + } + + /** + * \return The maximum value + * potentially generated by the random-number engine + */ + static constexpr typename Engine::result_type max( ) { + return Engine::max( ); + } + + /// Advances the internal state by z times + static void discard( const unsigned long long z ) { + engine_instance( ).discard( z ); + } + + /// Reseed by Seeder + static void reseed( ) { + Seeder seeder; + seed( seeder( ) ); + } + + /** + * \brief Reinitializes the internal state + * of the random-number engine using new seed value + * \param value The seed value to use + * in the initialization of the internal state + */ + static void seed( const typename Engine::result_type value = + Engine::default_seed ) { + engine_instance( ).seed( value ); + } + + /** + * \brief Reinitializes the internal state + * of the random-number engine using new seed value + * \param seq The seed sequence + * to use in the initialization of the internal state + */ + template + static void seed( Sseq& seq ) { + engine_instance( ).seed( seq ); + } + + /// return random number from engine in [min(), max()] range + static typename Engine::result_type get( ) { + return engine_instance( )( ); + } + + /** + * \brief Compares internal pseudo-random number engine + * with 'other' pseudo-random number engine. + * Two engines are equal, if their internal states + * are equivalent, that is, if they would generate + * equivalent values for any number of calls of operator() + * \param other The engine, with which the internal engine will be compared + * \return true, if other and internal engine are equal + */ + static bool is_equal( const Engine& other ) { + return engine_instance( ) == other; + } + + /** + * \brief Serializes the internal state of the + * internal pseudo-random number engine as a sequence + * of decimal numbers separated by one or more spaces, + * and inserts it to the stream ost. The fill character + * and the formatting flags of the stream are + * ignored and unaffected. + * \param ost The output stream to insert the data to + */ + template + static void serialize( std::basic_ostream& ost ) { + ost << engine_instance( ); + } + + /** + * \brief Restores the internal state of the + * internal pseudo-random number engine from + * the serialized representation, which + * was created by an earlier call to 'serialize' + * using a stream with the same imbued locale and + * the same CharT and Traits. + * If the input cannot be deserialized, + * internal engine is left unchanged and failbit is raised on ist + * \param ost The input stream to extract the data from + */ + template + static void deserialize( std::basic_istream& ist ) { + ist >> engine_instance( ); + } + + /** + * \brief Generate a random integer number in a [from; to] range + * by std::uniform_int_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random integer number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + static typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + if( from < to ) // Allow range from higher to lower + return IntegerDist{ from, to }( engine_instance( ) ); + return IntegerDist{ to, from }( engine_instance( ) ); + } + + /** + * \brief Generate a random real number in a [from; to] range + * by std::uniform_real_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random real number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + static typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + if( from < to ) // Allow range from higher to lower + return RealDist{ from, to }( engine_instance( ) ); + return RealDist{ to, from }( engine_instance( ) ); + } + + /** + * \brief Generate a random byte number in a [from; to] range + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random byte number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + static typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + // Choose between short and unsigned short for byte conversion + using short_t = typename std::conditional::value, + short, unsigned short>::type; + + return static_cast( get( from, to ) ); + } + + /** + * \brief Generate a random common_type number in a [from; to] range + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random common_type number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Allow implicit type conversion + * \note Prevent implicit type conversion from singed to unsigned types + * Why? std::common_type chooses unsigned value, + * then Signed value will be converted to Unsigned value + * which gives us a wrong range for random values. + * https://stackoverflow.com/a/5416498/5734836 + */ + template< + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type + > + static typename std::enable_if< + std::is_same::value + && details::is_supported_number::value + && details::is_supported_number::value + // Prevent implicit type conversion from singed to unsigned types + && std::is_signed::value != std::is_unsigned::value + , C>::type get( A from = std::numeric_limits::min( ), + B to = std::numeric_limits::max( ) ) { + return get( static_cast( from ), static_cast( to ) ); + } + + /** + * \brief Generate a random character in a [from; to] range + * by std::uniform_int_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random character in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + static typename std::enable_if::value + , T>::type get(T from = std::numeric_limits::min(), + T to = std::numeric_limits::max()) { + if (from < to) // Allow range from higher to lower + return static_cast(IntegerDist{ static_cast(from), static_cast(to) }(engine_instance())); + return static_cast(IntegerDist{ static_cast(to), static_cast(from) }(engine_instance())); + } + + /** + * \brief Generate a bool value with specific probability + * by std::bernoulli_distribution + * \param probability The probability of generating true in [0; 1] range + * 0 means always false, 1 means always true + * \return 'true' with 'probability' probability ('false' otherwise) + */ + template + static typename std::enable_if::value + , bool>::type get( const double probability = 0.5 ) { + assert( 0 <= probability && 1 >= probability ); // out of [0; 1] range + return BoolDist{ probability }( engine_instance( ) ); + } + + /** + * \brief Return random value from initilizer_list + * \param init_list initilizer_list with values + * \return Random value from initilizer_list + * \note Should be 1 or more elements in initilizer_list + * \note Warning! Elements in initilizer_list can't be moved: + * https://stackoverflow.com/a/8193157/5734836 + */ + template + static T get( std::initializer_list init_list ) { + assert( 0u != init_list.size( ) ); + return *get( init_list.begin( ), init_list.end( ) ); + } + + /** + * \brief Return random iterator from iterator range + * \param first, last - the range of elements + * \return Random iterator from [first, last) range + * \note If first == last, return last + */ + template + static typename std::enable_if::value + , InputIt>::type get( InputIt first, InputIt last ) { + const auto size = std::distance( first, last ); + if( 0 == size ) return last; + using diff_t = typename std::iterator_traits::difference_type; + return std::next( first, get( 0, size - 1 ) ); + } + + /** + * \brief Return random iterator from Container + * \param container The container with elements + * \return Random iterator from container + * \note If container is empty return std::end( container ) iterator + */ + template + static auto get( Container& container ) -> + typename std::enable_if::value + , decltype(std::begin(container)) + >::type { + return get( std::begin( container ), std::end( container ) ); + } + + /** + * \brief Return container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + * \note Container "reserve" method will be called before generation + */ + template class Container, typename A> + static typename std::enable_if< + details::has_reserve>::value + , Container>::type get(A from, A to, std::size_t size) { + Container container; + + container.reserve(size); + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(from, to)); + + return container; + } + + /** + * \brief Return container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + * \note Container "reserve" method will be called before generation + */ + template< + template class Container, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + static typename std::enable_if< + std::is_same::value + && details::has_reserve>::value + , Container>::type get(A start, B end, std::size_t size) { + Container container; + + container.reserve(size); + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template class Container, typename A> + static typename std::enable_if< + !details::has_reserve>::value + , Container>::type get(A start, A end, std::size_t size) { + Container container; + + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template< + template class Container, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + static typename std::enable_if< + std::is_same::value + && !details::has_reserve>::value + , Container>::type get(A start, B end, std::size_t size) { + Container container; + + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return array-like container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param N The number of elements in resulting container + * \return Container filled with random values + */ + template class Container, std::size_t N, typename A> + static typename std::enable_if< + !details::has_insert>::value + , Container>::type get(A start, A end) { + Container container = {{ 0 }}; + + for (std::size_t i = 0; i < N; ++i) + container[i] = get(start, end); + + return container; + } + + /** + * \brief Return array-like container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template< + template class Container, + std::size_t N, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + static typename std::enable_if< + std::is_same::value + && !details::has_insert>::value + , Container>::type get(A start, B end) { + Container container = {{ 0 }}; + + for (std::size_t i = 0; i < N; ++i) + container[i] = get(start, end); + + return container; + } + + /** + * \brief Return random pointer from built-in array + * \param array The built-in array with elements + * \return Pointer to random element in array + */ + template + static T* get( T( &array )[ N ] ) { + return std::addressof( array[ get( 0, N - 1 ) ] ); + } + + /** + * \brief Return value from custom Dist distribution + * seeded by internal random engine + * \param Dist The type of custom distribution with next concept: + * http://en.cppreference.com/w/cpp/concept/RandomNumberDistribution + * \param args The arguments which will be forwarded to Dist constructor + * \return Value from custom distribution + */ + template + static typename Dist::result_type get( Args&&... args ) { + return Dist{ std::forward( args )... }( engine_instance( ) ); + } + + /** + * \brief Return value from custom 'dist' distribution + * seeded by internal random engine + * \param dist The custom distribution with next concept: + * http://en.cppreference.com/w/cpp/concept/RandomNumberDistribution + * \param args The arguments which will be forwarded to Dist constructor + * \return Value from custom 'dist' distribution + */ + template + static typename Dist::result_type get( Dist& dist ) { + return dist( engine_instance( ) ); + } + + /** + * \brief Reorders the elements in the given range [first, last) + * such that each possible permutation of those elements + * has equal probability of appearance. + * \param first, last - the range of elements to shuffle randomly + */ + template + static void shuffle( RandomIt first, RandomIt last ) { + std::shuffle( first, last, engine_instance( ) ); + } + + /** + * \brief Reorders the elements in the given container + * such that each possible permutation of those elements + * has equal probability of appearance. + * \param container - the container with elements to shuffle randomly + */ + template + static void shuffle( Container& container ) { + shuffle( std::begin( container ), std::end( container ) ); + } + + /// return internal engine by copy + static Engine get_engine( ) { + return engine_instance( ); + } + + /// return internal engine by ref + static Engine& engine() { + return engine_instance(); + } + protected: + /// get reference to the static engine instance + static Engine& engine_instance( ) { + static Engine engine{ Seeder{ }( ) }; + return engine; + } + }; + + /** + * \brief Base template class for random + * with thread_local API and thread_local internal member storage + * \note it IS thread safe but less efficient then + * basic_random_static + * \param Engine A random engine with interface like in the std::mt19937 + * \param Seeder A seeder type which return seed for internal engine + * through operator() + */ + template< + typename Engine, + typename Seeder = seeder_default, + template class IntegerDist = std::uniform_int_distribution, + template class RealDist = std::uniform_real_distribution, + typename BoolDist = std::bernoulli_distribution + > + class basic_random_thread_local { + public: + basic_random_thread_local( ) = delete; + + /// Type of used random number engine + using engine_type = Engine; + + /// Type of used random number seeder + using seeder_type = Seeder; + + /// Type of used integer distribution + template + using integer_dist_t = IntegerDist; + + /// Type of used real distribution + template + using real_dist_t = RealDist; + + /// Type of used bool distribution + using bool_dist_t = BoolDist; + + /// Key type for getting common type numbers or objects + using common = details::common; + + /** + * \return The minimum value + * potentially generated by the random-number engine + */ + static constexpr typename Engine::result_type min( ) { + return Engine::min( ); + } + + /** + * \return The maximum value + * potentially generated by the random-number engine + */ + static constexpr typename Engine::result_type max( ) { + return Engine::max( ); + } + + /// Advances the internal state by z times + static void discard( const unsigned long long z ) { + engine_instance( ).discard( z ); + } + + /// Reseed by Seeder + static void reseed( ) { + Seeder seeder; + seed( seeder( ) ); + } + + /** + * \brief Reinitializes the internal state + * of the random-number engine using new seed value + * \param value The seed value to use + * in the initialization of the internal state + */ + static void seed( const typename Engine::result_type value = + Engine::default_seed ) { + engine_instance( ).seed( value ); + } + + /** + * \brief Reinitializes the internal state + * of the random-number engine using new seed value + * \param seq The seed sequence + * to use in the initialization of the internal state + */ + template + static void seed( Sseq& seq ) { + engine_instance( ).seed( seq ); + } + + /// return random number from engine in [min(), max()] range + static typename Engine::result_type get( ) { + return engine_instance( )( ); + } + + /** + * \brief Compares internal pseudo-random number engine + * with 'other' pseudo-random number engine. + * Two engines are equal, if their internal states + * are equivalent, that is, if they would generate + * equivalent values for any number of calls of operator() + * \param other The engine, with which the internal engine will be compared + * \return true, if other and internal engine are equal + */ + static bool is_equal( const Engine& other ) { + return engine_instance( ) == other; + } + + /** + * \brief Serializes the internal state of the + * internal pseudo-random number engine as a sequence + * of decimal numbers separated by one or more spaces, + * and inserts it to the stream ost. The fill character + * and the formatting flags of the stream are + * ignored and unaffected. + * \param ost The output stream to insert the data to + */ + template + static void serialize( std::basic_ostream& ost ) { + ost << engine_instance( ); + } + + /** + * \brief Restores the internal state of the + * internal pseudo-random number engine from + * the serialized representation, which + * was created by an earlier call to 'serialize' + * using a stream with the same imbued locale and + * the same CharT and Traits. + * If the input cannot be deserialized, + * internal engine is left unchanged and failbit is raised on ist + * \param ost The input stream to extract the data from + */ + template + static void deserialize( std::basic_istream& ist ) { + ist >> engine_instance( ); + } + + /** + * \brief Generate a random integer number in a [from; to] range + * by std::uniform_int_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random integer number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + static typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + if( from < to ) // Allow range from higher to lower + return IntegerDist{ from, to }( engine_instance( ) ); + return IntegerDist{ to, from }( engine_instance( ) ); + } + + /** + * \brief Generate a random real number in a [from; to] range + * by std::uniform_real_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random real number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + static typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + if( from < to ) // Allow range from higher to lower + return RealDist{ from, to }( engine_instance( ) ); + return RealDist{ to, from }( engine_instance( ) ); + } + + /** + * \brief Generate a random byte number in a [from; to] range + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random byte number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + static typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + // Choose between short and unsigned short for byte conversion + using short_t = typename std::conditional::value, + short, unsigned short>::type; + + return static_cast( get( from, to ) ); + } + + /** + * \brief Generate a random common_type number in a [from; to] range + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random common_type number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Allow implicit type conversion + * \note Prevent implicit type conversion from singed to unsigned types + * Why? std::common_type chooses unsigned value, + * then Signed value will be converted to Unsigned value + * which gives us a wrong range for random values. + * https://stackoverflow.com/a/5416498/5734836 + */ + template< + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type + > + static typename std::enable_if< + std::is_same::value + && details::is_supported_number::value + && details::is_supported_number::value + // Prevent implicit type conversion from singed to unsigned types + && std::is_signed::value != std::is_unsigned::value + , C>::type get( A from = std::numeric_limits::min( ), + B to = std::numeric_limits::max( ) ) { + return get( static_cast( from ), static_cast( to ) ); + } + + /** + * \brief Generate a random character in a [from; to] range + * by std::uniform_int_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random character in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + static typename std::enable_if::value + , T>::type get(T from = std::numeric_limits::min(), + T to = std::numeric_limits::max()) { + if (from < to) // Allow range from higher to lower + return static_cast(IntegerDist{ static_cast(from), static_cast(to) }(engine_instance())); + return static_cast(IntegerDist{ static_cast(to), static_cast(from) }(engine_instance())); + } + + /** + * \brief Generate a bool value with specific probability + * by std::bernoulli_distribution + * \param probability The probability of generating true in [0; 1] range + * 0 means always false, 1 means always true + * \return 'true' with 'probability' probability ('false' otherwise) + */ + template + static typename std::enable_if::value + , bool>::type get( const double probability = 0.5 ) { + assert( 0 <= probability && 1 >= probability ); // out of [0; 1] range + return BoolDist{ probability }( engine_instance( ) ); + } + + /** + * \brief Return random value from initilizer_list + * \param init_list initilizer_list with values + * \return Random value from initilizer_list + * \note Should be 1 or more elements in initilizer_list + * \note Warning! Elements in initilizer_list can't be moved: + * https://stackoverflow.com/a/8193157/5734836 + */ + template + static T get( std::initializer_list init_list ) { + assert( 0u != init_list.size( ) ); + return *get( init_list.begin( ), init_list.end( ) ); + } + + /** + * \brief Return random iterator from iterator range + * \param first, last - the range of elements + * \return Random iterator from [first, last) range + * \note If first == last, return last + */ + template + static typename std::enable_if::value + , InputIt>::type get( InputIt first, InputIt last ) { + const auto size = std::distance( first, last ); + if( 0 == size ) return last; + using diff_t = typename std::iterator_traits::difference_type; + return std::next( first, get( 0, size - 1 ) ); + } + + /** + * \brief Return random iterator from Container + * \param container The container with elements + * \return Random iterator from container + * \note If container is empty return std::end( container ) iterator + */ + template + static auto get( Container& container ) -> + typename std::enable_if::value + , decltype(std::begin(container)) + >::type { + return get( std::begin( container ), std::end( container ) ); + } + + /** + * \brief Return container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + * \note Container "reserve" method will be called before generation + */ + template class Container, typename A> + static typename std::enable_if< + details::has_reserve>::value + , Container>::type get(A from, A to, std::size_t size) { + Container container; + + container.reserve(size); + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(from, to)); + + return container; + } + + /** + * \brief Return container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + * \note Container "reserve" method will be called before generation + */ + template< + template class Container, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + static typename std::enable_if< + std::is_same::value + && details::has_reserve>::value + , Container>::type get(A start, B end, std::size_t size) { + Container container; + + container.reserve(size); + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template class Container, typename A> + static typename std::enable_if< + !details::has_reserve>::value + , Container>::type get(A start, A end, std::size_t size) { + Container container; + + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template< + template class Container, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + static typename std::enable_if< + std::is_same::value + && !details::has_reserve>::value + , Container>::type get(A start, B end, std::size_t size) { + Container container; + + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return array-like container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param N The number of elements in resulting container + * \return Container filled with random values + */ + template class Container, std::size_t N, typename A> + static typename std::enable_if< + !details::has_insert>::value + , Container>::type get(A start, A end) { + Container container = {{ 0 }}; + + for (std::size_t i = 0; i < N; ++i) + container[i] = get(start, end); + + return container; + } + + /** + * \brief Return array-like container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template< + template class Container, + std::size_t N, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + static typename std::enable_if< + std::is_same::value + && !details::has_insert>::value + , Container>::type get(A start, B end) { + Container container = {{ 0 }}; + + for (std::size_t i = 0; i < N; ++i) + container[i] = get(start, end); + + return container; + } + + /** + * \brief Return random pointer from built-in array + * \param array The built-in array with elements + * \return Pointer to random element in array + */ + template + static T* get( T( &array )[ N ] ) { + return std::addressof( array[ get( 0, N - 1 ) ] ); + } + + /** + * \brief Return value from custom Dist distribution + * seeded by internal random engine + * \param Dist The type of custom distribution with next concept: + * http://en.cppreference.com/w/cpp/concept/RandomNumberDistribution + * \param args The arguments which will be forwarded to Dist constructor + * \return Value from custom distribution + */ + template + static typename Dist::result_type get( Args&&... args ) { + return Dist{ std::forward( args )... }( engine_instance( ) ); + } + + /** + * \brief Return value from custom 'dist' distribution + * seeded by internal random engine + * \param dist The custom distribution with next concept: + * http://en.cppreference.com/w/cpp/concept/RandomNumberDistribution + * \param args The arguments which will be forwarded to Dist constructor + * \return Value from custom 'dist' distribution + */ + template + static typename Dist::result_type get( Dist& dist ) { + return dist( engine_instance( ) ); + } + + /** + * \brief Reorders the elements in the given range [first, last) + * such that each possible permutation of those elements + * has equal probability of appearance. + * \param first, last - the range of elements to shuffle randomly + */ + template + static void shuffle( RandomIt first, RandomIt last ) { + std::shuffle( first, last, engine_instance( ) ); + } + + /** + * \brief Reorders the elements in the given container + * such that each possible permutation of those elements + * has equal probability of appearance. + * \param container - the container with elements to shuffle randomly + */ + template + static void shuffle( Container& container ) { + shuffle( std::begin( container ), std::end( container ) ); + } + + /// return internal engine by copy + static Engine get_engine( ) { + return engine_instance( ); + } + + /// return internal engine by ref + static Engine& engine() { + return engine_instance(); + } + protected: + /// get reference to the thread local engine instance + static Engine& engine_instance( ) { + thread_local Engine engine{ Seeder{ }( ) }; + return engine; + } + }; + + /** + * \brief Base template class for random + * with local API and local internal member storage + * \note it IS thread safe but less efficient then + * basic_random_static + * \param Engine A random engine with interface like in the std::mt19937 + * \param Seeder A seeder type which return seed for internal engine + * through operator() + */ + template< + typename Engine, + typename Seeder = seeder_default, + template class IntegerDist = std::uniform_int_distribution, + template class RealDist = std::uniform_real_distribution, + typename BoolDist = std::bernoulli_distribution + > + class basic_random_local { + public: + /// Type of used random number engine + using engine_type = Engine; + + /// Type of used random number seeder + using seeder_type = Seeder; + + /// Type of used integer distribution + template + using integer_dist_t = IntegerDist; + + /// Type of used real distribution + template + using real_dist_t = RealDist; + + /// Type of used bool distribution + using bool_dist_t = BoolDist; + + /// Key type for getting common type numbers or objects + using common = details::common; + + /** + * \return The minimum value + * potentially generated by the random-number engine + */ + static constexpr typename Engine::result_type min( ) { + return Engine::min( ); + } + + /** + * \return The maximum value + * potentially generated by the random-number engine + */ + static constexpr typename Engine::result_type max( ) { + return Engine::max( ); + } + + /// Advances the internal state by z times + void discard( const unsigned long long z ) { + m_engine.discard( z ); + } + + /// Reseed by Seeder + void reseed( ) { + Seeder seeder; + seed( seeder( ) ); + } + + /** + * \brief Reinitializes the internal state + * of the random-number engine using new seed value + * \param value The seed value to use + * in the initialization of the internal state + */ + void seed( const typename Engine::result_type value = + Engine::default_seed ) { + m_engine.seed( value ); + } + + /** + * \brief Reinitializes the internal state + * of the random-number engine using new seed value + * \param seq The seed sequence + * to use in the initialization of the internal state + */ + template + void seed( Sseq& seq ) { + m_engine.seed( seq ); + } + + /// return random number from engine in [min(), max()] range + typename Engine::result_type get( ) { + return m_engine( ); + } + + /** + * \brief Compares internal pseudo-random number engine + * with 'other' pseudo-random number engine. + * Two engines are equal, if their internal states + * are equivalent, that is, if they would generate + * equivalent values for any number of calls of operator() + * \param other The engine, with which the internal engine will be compared + * \return true, if other and internal engine are equal + */ + bool is_equal( const Engine& other ) { + return m_engine == other; + } + + /** + * \brief Serializes the internal state of the + * internal pseudo-random number engine as a sequence + * of decimal numbers separated by one or more spaces, + * and inserts it to the stream ost. The fill character + * and the formatting flags of the stream are + * ignored and unaffected. + * \param ost The output stream to insert the data to + */ + template + void serialize( std::basic_ostream& ost ) { + ost << m_engine; + } + + /** + * \brief Restores the internal state of the + * internal pseudo-random number engine from + * the serialized representation, which + * was created by an earlier call to 'serialize' + * using a stream with the same imbued locale and + * the same CharT and Traits. + * If the input cannot be deserialized, + * internal engine is left unchanged and failbit is raised on ist + * \param ost The input stream to extract the data from + */ + template + void deserialize( std::basic_istream& ist ) { + ist >> m_engine; + } + + /** + * \brief Generate a random integer number in a [from; to] range + * by std::uniform_int_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random integer number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + if( from < to ) // Allow range from higher to lower + return IntegerDist{ from, to }( m_engine ); + return IntegerDist{ to, from }( m_engine ); + } + + /** + * \brief Generate a random real number in a [from; to] range + * by std::uniform_real_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random real number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + if( from < to ) // Allow range from higher to lower + return RealDist{ from, to }( m_engine ); + return RealDist{ to, from }( m_engine ); + } + + /** + * \brief Generate a random byte number in a [from; to] range + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random byte number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + typename std::enable_if::value + , T>::type get( T from = std::numeric_limits::min( ), + T to = std::numeric_limits::max( ) ) { + // Choose between short and unsigned short for byte conversion + using short_t = typename std::conditional::value, + short, unsigned short>::type; + + return static_cast( get( from, to ) ); + } + + /** + * \brief Generate a random common_type number in a [from; to] range + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random common_type number in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Allow implicit type conversion + * \note Prevent implicit type conversion from singed to unsigned types + * Why? std::common_type chooses unsigned value, + * then Signed value will be converted to Unsigned value + * which gives us a wrong range for random values. + * https://stackoverflow.com/a/5416498/5734836 + */ + template< + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type + > + typename std::enable_if< + std::is_same::value + && details::is_supported_number::value + && details::is_supported_number::value + // Prevent implicit type conversion from singed to unsigned types + && std::is_signed::value != std::is_unsigned::value + , C>::type get( A from = std::numeric_limits::min( ), + B to = std::numeric_limits::max( ) ) { + return get( static_cast( from ), static_cast( to ) ); + } + + /** + * \brief Generate a random character in a [from; to] range + * by std::uniform_int_distribution + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \return A random character in a [from; to] range + * \note Allow both: 'from' <= 'to' and 'from' >= 'to' + * \note Prevent implicit type conversion + */ + template + typename std::enable_if::value + , T>::type get(T from = std::numeric_limits::min(), + T to = std::numeric_limits::max()) { + if (from < to) // Allow range from higher to lower + return static_cast(IntegerDist{ static_cast(from), static_cast(to) }(m_engine)); + return static_cast(IntegerDist{ static_cast(to), static_cast(from) }(m_engine)); + } + + /** + * \brief Generate a bool value with specific probability + * by std::bernoulli_distribution + * \param probability The probability of generating true in [0; 1] range + * 0 means always false, 1 means always true + * \return 'true' with 'probability' probability ('false' otherwise) + */ + template + typename std::enable_if::value + , bool>::type get( const double probability = 0.5 ) { + assert( 0 <= probability && 1 >= probability ); // out of [0; 1] range + return BoolDist{ probability }( m_engine ); + } + + /** + * \brief Return random value from initilizer_list + * \param init_list initilizer_list with values + * \return Random value from initilizer_list + * \note Should be 1 or more elements in initilizer_list + * \note Warning! Elements in initilizer_list can't be moved: + * https://stackoverflow.com/a/8193157/5734836 + */ + template + T get( std::initializer_list init_list ) { + assert( 0u != init_list.size( ) ); + return *get( init_list.begin( ), init_list.end( ) ); + } + + /** + * \brief Return random iterator from iterator range + * \param first, last - the range of elements + * \return Random iterator from [first, last) range + * \note If first == last, return last + */ + template + typename std::enable_if::value + , InputIt>::type get( InputIt first, InputIt last ) { + const auto size = std::distance( first, last ); + if( 0 == size ) return last; + using diff_t = typename std::iterator_traits::difference_type; + return std::next( first, get( 0, size - 1 ) ); + } + + /** + * \brief Return random iterator from Container + * \param container The container with elements + * \return Random iterator from container + * \note If container is empty return std::end( container ) iterator + */ + template + auto get( Container& container ) -> + typename std::enable_if::value + , decltype(std::begin(container)) + >::type { + return get( std::begin( container ), std::end( container ) ); + } + + /** + * \brief Return container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + * \note Container "reserve" method will be called before generation + */ + template class Container, typename A> + typename std::enable_if< + details::has_reserve>::value + , Container>::type get(A from, A to, std::size_t size) { + Container container; + + container.reserve(size); + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(from, to)); + + return container; + } + + /** + * \brief Return container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + * \note Container "reserve" method will be called before generation + */ + template< + template class Container, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + typename std::enable_if< + std::is_same::value + && details::has_reserve>::value + , Container>::type get(A start, B end, std::size_t size) { + Container container; + + container.reserve(size); + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template class Container, typename A> + typename std::enable_if< + !details::has_reserve>::value + , Container>::type get(A start, A end, std::size_t size) { + Container container; + + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template< + template class Container, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + typename std::enable_if< + std::is_same::value + && !details::has_reserve>::value + , Container>::type get(A start, B end, std::size_t size) { + Container container; + + for (std::size_t i = 0; i < size; ++i) + container.insert(std::end(container), get(start, end)); + + return container; + } + + /** + * \brief Return array-like container filled with random values + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param N The number of elements in resulting container + * \return Container filled with random values + */ + template class Container, std::size_t N, typename A> + typename std::enable_if< + !details::has_insert>::value + , Container>::type get(A start, A end) { + Container container = {{ 0 }}; + + for (std::size_t i = 0; i < N; ++i) + container[i] = get(start, end); + + return container; + } + + /** + * \brief Return array-like container filled with random common_type values + * \param Key The Key type for this version of 'get' method + * Type should be '(THIS_TYPE)::common' struct + * \param from The first limit number of a random range + * \param to The second limit number of a random range + * \param size The number of elements in resulting container + * \return Container filled with random values + */ + template< + template class Container, + std::size_t N, + typename Key, + typename A, + typename B, + typename C = typename std::common_type::type> + typename std::enable_if< + std::is_same::value + && !details::has_insert>::value + , Container>::type get(A start, B end) { + Container container = {{ 0 }}; + + for (std::size_t i = 0; i < N; ++i) + container[i] = get(start, end); + + return container; + } + + /** + * \brief Return random pointer from built-in array + * \param array The built-in array with elements + * \return Pointer to random element in array + */ + template + T* get( T( &array )[ N ] ) { + return std::addressof( array[ get( 0, N - 1 ) ] ); + } + + /** + * \brief Return value from custom Dist distribution + * seeded by internal random engine + * \param Dist The type of custom distribution with next concept: + * http://en.cppreference.com/w/cpp/concept/RandomNumberDistribution + * \param args The arguments which will be forwarded to Dist constructor + * \return Value from custom distribution + */ + template + typename Dist::result_type get( Args&&... args ) { + return Dist{ std::forward( args )... }( m_engine ); + } + + /** + * \brief Return value from custom 'dist' distribution + * seeded by internal random engine + * \param dist The custom distribution with next concept: + * http://en.cppreference.com/w/cpp/concept/RandomNumberDistribution + * \param args The arguments which will be forwarded to Dist constructor + * \return Value from custom 'dist' distribution + */ + template + typename Dist::result_type get( Dist& dist ) { + return dist( m_engine ); + } + + /** + * \brief Reorders the elements in the given range [first, last) + * such that each possible permutation of those elements + * has equal probability of appearance. + * \param first, last - the range of elements to shuffle randomly + */ + template + void shuffle( RandomIt first, RandomIt last ) { + std::shuffle( first, last, m_engine ); + } + + /** + * \brief Reorders the elements in the given container + * such that each possible permutation of those elements + * has equal probability of appearance. + * \param container - the container with elements to shuffle randomly + */ + template + void shuffle( Container& container ) { + shuffle( std::begin( container ), std::end( container ) ); + } + + /// return internal engine by copy + Engine get_engine( ) const { + return m_engine; + } + + /// return internal engine by ref + Engine& engine() { + return m_engine; + } + protected: + /// return engine seeded by Seeder + static Engine make_seeded_engine( ) { + // Make seeder instance for seed return by reference like std::seed_seq + return Engine{ Seeder{ }( ) }; + } + protected: + /// The random number engine + Engine m_engine{ make_seeded_engine( ) }; + }; + + /** + * \brief The basic static random alias based on a std::mt19937 + * \note It uses static methods API and data with static storage + * \note Not thread safe but more prefomance + */ + using random_static = basic_random_static; + + /** + * \brief The basic static random alias based on a std::mt19937 + * \note It uses static methods API and data with thread_local storage + * \note Thread safe but less perfomance + */ + using random_thread_local = basic_random_thread_local; + + /** + * \brief The basic static random alias based on a std::mt19937 + * \note It uses non static methods API and data with auto storage + * \note Not thread safe. Should construct on the stack at local scope + */ + using random_local = basic_random_local; + +} // namespace effolkronium + +#endif // #ifndef EFFOLKRONIUM_RANDOM_HPP diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..249b7d9 --- /dev/null +++ b/flake.lock @@ -0,0 +1,1336 @@ +{ + "nodes": { + "ccache-trigger": { + "locked": { + "lastModified": 1657739266, + "narHash": "sha256-vLy8GQr0noEcoA+jX24FgUVBA/poV36zDWAUChN3hIY=", + "owner": "boolean-option", + "repo": "false", + "rev": "d06b4794a134686c70a1325df88a6e6768c6b212", + "type": "github" + }, + "original": { + "owner": "boolean-option", + "repo": "false", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_3": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "mc-rtc-nix", + "gepetto", + "system-manager", + "userborn", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_3": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_4": { + "inputs": { + "nixpkgs-lib": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "system-manager", + "userborn", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flakoboros": { + "inputs": { + "flake-parts": "flake-parts", + "nix-ros-overlay": "nix-ros-overlay", + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros", + "nix-ros-overlay", + "nixpkgs" + ], + "systems": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros", + "nix-ros-overlay", + "flake-utils", + "systems" + ], + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1781324212, + "narHash": "sha256-lqIEMqeuJ450S7JdWTo8JjF3YlahGNR2FCz/AlA9ZhM=", + "owner": "gepetto", + "repo": "flakoboros", + "rev": "78aa318051a5345d82931df66c5c8d0844edcb2a", + "type": "github" + }, + "original": { + "owner": "gepetto", + "repo": "flakoboros", + "type": "github" + } + }, + "flakoboros_2": { + "inputs": { + "flake-parts": "flake-parts_3", + "nix-ros-overlay": "nix-ros-overlay_2", + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros", + "nix-ros-overlay", + "nixpkgs" + ], + "systems": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros", + "nix-ros-overlay", + "flake-utils", + "systems" + ], + "treefmt-nix": "treefmt-nix_2" + }, + "locked": { + "lastModified": 1776113076, + "narHash": "sha256-rQp/6mgQc4szevay50RbGXOXwL3xC7oHktywFPrmbAM=", + "owner": "gepetto", + "repo": "flakoboros", + "rev": "07a89fe2a47f2f0364e795cf3e69c045de14a3b2", + "type": "github" + }, + "original": { + "owner": "gepetto", + "repo": "flakoboros", + "type": "github" + } + }, + "gazebros2nix": { + "inputs": { + "flake-parts": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros", + "flake-parts" + ], + "flakoboros": "flakoboros", + "nix-ros-overlay": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros", + "nix-ros-overlay" + ], + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros", + "nixpkgs" + ], + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "systems": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros", + "systems" + ], + "treefmt-nix": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros", + "treefmt-nix" + ], + "uv2nix": "uv2nix" + }, + "locked": { + "lastModified": 1781362845, + "narHash": "sha256-IxhhDOW/aLRltOGHiYTP+Oqr2baRPqtrJYIVvfuw5aE=", + "owner": "gepetto", + "repo": "gazebros2nix", + "rev": "f45bbdedc06ae9060a14834ee2896199f979d062", + "type": "github" + }, + "original": { + "owner": "gepetto", + "repo": "gazebros2nix", + "type": "github" + } + }, + "gazebros2nix_2": { + "inputs": { + "flake-parts": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros", + "flake-parts" + ], + "flakoboros": "flakoboros_2", + "nix-ros-overlay": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros", + "nix-ros-overlay" + ], + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros", + "nixpkgs" + ], + "pyproject-build-systems": "pyproject-build-systems_2", + "pyproject-nix": "pyproject-nix_2", + "systems": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros", + "systems" + ], + "treefmt-nix": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros", + "treefmt-nix" + ], + "uv2nix": "uv2nix_2" + }, + "locked": { + "lastModified": 1777754176, + "narHash": "sha256-bnR2Aor+qSLVCcUUV/IjjHM5AnJR6cpf0AljUSiUuco=", + "owner": "gepetto", + "repo": "gazebros2nix", + "rev": "29d82bf45d91a5e9ca9f8c40f963ad20168e6ae1", + "type": "github" + }, + "original": { + "owner": "gepetto", + "repo": "gazebros2nix", + "type": "github" + } + }, + "gepetto": { + "inputs": { + "flake-parts": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flake-parts" + ], + "flakoboros": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros" + ], + "gazebros2nix": "gazebros2nix", + "home-manager": "home-manager", + "nix-ros-overlay": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "nix-ros-overlay" + ], + "nix-system-graphics": "nix-system-graphics", + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "nixpkgs" + ], + "system-manager": "system-manager", + "systems": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "systems" + ], + "treefmt-nix": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "treefmt-nix" + ] + }, + "locked": { + "lastModified": 1781410865, + "narHash": "sha256-EC0VzThwka/bPooGIN63c8dEXVaEshw61bm5lNYmI54=", + "owner": "gepetto", + "repo": "nix", + "rev": "ae989cac5c2b3f51131dd54b8fb949ee1b7b2563", + "type": "github" + }, + "original": { + "owner": "gepetto", + "repo": "nix", + "type": "github" + } + }, + "gepetto_2": { + "inputs": { + "flake-parts": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flake-parts" + ], + "flakoboros": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros" + ], + "gazebros2nix": "gazebros2nix_2", + "home-manager": "home-manager_2", + "nix-ros-overlay": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "nix-ros-overlay" + ], + "nix-system-graphics": "nix-system-graphics_2", + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "nixpkgs" + ], + "system-manager": "system-manager_2", + "systems": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "systems" + ], + "treefmt-nix": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "treefmt-nix" + ] + }, + "locked": { + "lastModified": 1777773493, + "narHash": "sha256-6zivX47/UVo1dR9iGNRBETvz7/igFrc2SrqCbJqxtWY=", + "owner": "gepetto", + "repo": "nix", + "rev": "506ba63b0f3f7f1b1ac5e215f957feb4b12013d6", + "type": "github" + }, + "original": { + "owner": "gepetto", + "repo": "nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "system-manager", + "userborn", + "pre-commit-hooks-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gitignore_2": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "system-manager", + "userborn", + "pre-commit-hooks-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1779506708, + "narHash": "sha256-QOD/CNm196nCJRheux/URi4/HE66fthdOMqCJoPP1Y0=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "3ee51fbdac8c8bdfe1e7e1fcaba6520a563f394f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-25.11", + "repo": "home-manager", + "type": "github" + } + }, + "home-manager_2": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777771528, + "narHash": "sha256-YycygK6n7KeW1YCobdFJcORWzkmrvNcp6xT+IovA0d4=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "0585fbf645640973e3398863bbaf3bd1ddce4a51", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-25.11", + "repo": "home-manager", + "type": "github" + } + }, + "jrl-cmakemodulesv2": { + "inputs": { + "gepetto": "gepetto_2" + }, + "locked": { + "lastModified": 1781271796, + "narHash": "sha256-ib/hYwNKA5ibWn7ufQ45Gy+VRKNfR552xkr80OsCPhk=", + "owner": "ahoarau", + "repo": "jrl-cmakemodules", + "rev": "4cfa6b85b49e5b6b2e0f02a9076892afd1050fb1", + "type": "github" + }, + "original": { + "owner": "ahoarau", + "ref": "jrl-next", + "repo": "jrl-cmakemodules", + "type": "github" + } + }, + "make-shell": { + "inputs": { + "flake-compat": "flake-compat_3" + }, + "locked": { + "lastModified": 1733933815, + "narHash": "sha256-9JjM7eT66W4NJAXpGUsdyAFXhBxFWR2Z9LZwUa7Hli0=", + "owner": "nicknovitski", + "repo": "make-shell", + "rev": "ffeceae9956df03571ea8e96ef77c2924f13a63c", + "type": "github" + }, + "original": { + "owner": "nicknovitski", + "repo": "make-shell", + "type": "github" + } + }, + "mc-rtc-nix": { + "inputs": { + "ccache-trigger": "ccache-trigger", + "flake-parts": [ + "mc-rtc-nix", + "gepetto", + "flake-parts" + ], + "flakoboros": [ + "mc-rtc-nix", + "gepetto", + "flakoboros" + ], + "gepetto": "gepetto", + "jrl-cmakemodulesv2": "jrl-cmakemodulesv2", + "make-shell": "make-shell", + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "nixpkgs" + ], + "private-trigger": "private-trigger", + "systems": [ + "mc-rtc-nix", + "gepetto", + "systems" + ] + }, + "locked": { + "lastModified": 1781526996, + "narHash": "sha256-8w8+uAZvMtKPqTwYlKPMf7dlQXest6ABMFaCexWf66A=", + "owner": "mc-rtc", + "repo": "nixpkgs", + "rev": "1e6c166bb80d317cf438b5913e873efba9727c4b", + "type": "github" + }, + "original": { + "owner": "mc-rtc", + "repo": "nixpkgs", + "type": "github" + } + }, + "nix-ros-overlay": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rosdistro": "rosdistro" + }, + "locked": { + "lastModified": 1781297240, + "narHash": "sha256-OpYxlrSlcqJyknjgIuvv8dtEEbHuuZoe+fJzclrBgmg=", + "owner": "lopsided98", + "repo": "nix-ros-overlay", + "rev": "9e9b9a2953a195985e1e6c99e353fbb767380a2d", + "type": "github" + }, + "original": { + "owner": "lopsided98", + "ref": "develop", + "repo": "nix-ros-overlay", + "type": "github" + } + }, + "nix-ros-overlay_2": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1775764892, + "narHash": "sha256-l1UbHppd5KaU4K/TQPIXf9wnVifH4Tgrx5z2GFoq/bw=", + "owner": "lopsided98", + "repo": "nix-ros-overlay", + "rev": "0a4e8cf51001d2d1c613f95dadd3853da42c0164", + "type": "github" + }, + "original": { + "owner": "lopsided98", + "ref": "develop", + "repo": "nix-ros-overlay", + "type": "github" + } + }, + "nix-system-graphics": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1763296639, + "narHash": "sha256-K9JBscC7ApwCnl0wR0sVkxrKFsoDYVqXN5fOujvyBWA=", + "owner": "soupglasses", + "repo": "nix-system-graphics", + "rev": "ac37f0f3ec0cb15d63a520918433c794d01d9dac", + "type": "github" + }, + "original": { + "owner": "soupglasses", + "repo": "nix-system-graphics", + "type": "github" + } + }, + "nix-system-graphics_2": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1763296639, + "narHash": "sha256-K9JBscC7ApwCnl0wR0sVkxrKFsoDYVqXN5fOujvyBWA=", + "owner": "soupglasses", + "repo": "nix-system-graphics", + "rev": "ac37f0f3ec0cb15d63a520918433c794d01d9dac", + "type": "github" + }, + "original": { + "owner": "soupglasses", + "repo": "nix-system-graphics", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1777168982, + "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-lib_2": { + "locked": { + "lastModified": 1774748309, + "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "333c4e0545a6da976206c74db8773a1645b5870a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1771369470, + "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0182a361324364ae3f436a63005877674cf45efb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks-nix": { + "inputs": { + "flake-compat": [ + "mc-rtc-nix", + "gepetto", + "system-manager", + "userborn", + "flake-compat" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "system-manager", + "userborn", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769069492, + "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "pre-commit-hooks-nix_2": { + "inputs": { + "flake-compat": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "system-manager", + "userborn", + "flake-compat" + ], + "gitignore": "gitignore_2", + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "system-manager", + "userborn", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769069492, + "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "private-trigger": { + "locked": { + "lastModified": 1657739266, + "narHash": "sha256-vLy8GQr0noEcoA+jX24FgUVBA/poV36zDWAUChN3hIY=", + "owner": "boolean-option", + "repo": "false", + "rev": "d06b4794a134686c70a1325df88a6e6768c6b212", + "type": "github" + }, + "original": { + "owner": "boolean-option", + "repo": "false", + "type": "github" + } + }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "nixpkgs" + ], + "pyproject-nix": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "pyproject-nix" + ], + "uv2nix": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "uv2nix" + ] + }, + "locked": { + "lastModified": 1779676664, + "narHash": "sha256-MbXylBTkWqVm8/VYjoULtMoVRgWBN1gSHbeRKsOsPlU=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "7bff980f37fc24e09dbc986643719900c139bf12", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-build-systems_2": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "nixpkgs" + ], + "pyproject-nix": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "pyproject-nix" + ], + "uv2nix": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "uv2nix" + ] + }, + "locked": { + "lastModified": 1776659114, + "narHash": "sha256-qapCOQmR++yZSY43dzrp3wCrkOTLpod+ONtJWBk6iKU=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "ffaa2161dd5d63e0e94591f86b54fc239660fb2e", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780416763, + "narHash": "sha256-rRK3IFixgsrK6S3e14Xz9HAZm7+kMAIl3oi5zZlcwYI=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "ad83f1ead0e78e57b188f35cb57298affb06fc5f", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "pyproject-nix_2": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776715674, + "narHash": "sha256-Gs1VnEkCkkRZxJQAC/Dhz0Jbfi22mFXChbtNg9w/Ybg=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "69f57f27e52a87c54e28138a75ec741cd46663c9", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "mc-rtc-nix": "mc-rtc-nix" + } + }, + "rosdistro": { + "flake": false, + "locked": { + "lastModified": 1780665549, + "narHash": "sha256-QzobMKNpcpS4uWETzPxXkt0F66Sji15N+xsK1JcEWDU=", + "owner": "ros", + "repo": "rosdistro", + "rev": "bcec45c05427f0ad6860cb323695995d8cb1e4bd", + "type": "github" + }, + "original": { + "owner": "ros", + "repo": "rosdistro", + "type": "github" + } + }, + "system-manager": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "nixpkgs" + ], + "userborn": "userborn" + }, + "locked": { + "lastModified": 1778508009, + "narHash": "sha256-mp63mt+EwfKiVZX+4BC/59g5oTbsXNtSYESVci3opso=", + "owner": "numtide", + "repo": "system-manager", + "rev": "dc1baae12eed1758755e73f8aff7fca5502c6e9f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "system-manager", + "type": "github" + } + }, + "system-manager_2": { + "inputs": { + "flake-compat": "flake-compat_2", + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "nixpkgs" + ], + "userborn": "userborn_2" + }, + "locked": { + "lastModified": 1777545354, + "narHash": "sha256-T3u9Ixg0SX6bYYXHYEZ7O+MW0pQ1qxnjIQKCPM+irN4=", + "owner": "numtide", + "repo": "system-manager", + "rev": "6eac5ac077960363d3807b1c74f47103d1f62efd", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "system-manager", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_4": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "flakoboros", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1780220602, + "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "db947814a175b7ca6ded66e21383d938df01c227", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, + "treefmt-nix_2": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "flakoboros", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775636079, + "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, + "userborn": { + "inputs": { + "flake-compat": [ + "mc-rtc-nix", + "gepetto", + "system-manager", + "flake-compat" + ], + "flake-parts": "flake-parts_2", + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "system-manager", + "nixpkgs" + ], + "pre-commit-hooks-nix": "pre-commit-hooks-nix", + "systems": "systems_2" + }, + "locked": { + "lastModified": 1770377964, + "narHash": "sha256-q2pnlX2IW0kg80GLFnwWd/GigIpkuZnyKPLhrgJql3E=", + "owner": "jfroche", + "repo": "userborn", + "rev": "55c2cd7952c207a62736a5bbd9499ea73da18d24", + "type": "github" + }, + "original": { + "owner": "jfroche", + "ref": "system-manager", + "repo": "userborn", + "type": "github" + } + }, + "userborn_2": { + "inputs": { + "flake-compat": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "system-manager", + "flake-compat" + ], + "flake-parts": "flake-parts_4", + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "system-manager", + "nixpkgs" + ], + "pre-commit-hooks-nix": "pre-commit-hooks-nix_2", + "systems": "systems_4" + }, + "locked": { + "lastModified": 1770377964, + "narHash": "sha256-q2pnlX2IW0kg80GLFnwWd/GigIpkuZnyKPLhrgJql3E=", + "owner": "jfroche", + "repo": "userborn", + "rev": "55c2cd7952c207a62736a5bbd9499ea73da18d24", + "type": "github" + }, + "original": { + "owner": "jfroche", + "ref": "system-manager", + "repo": "userborn", + "type": "github" + } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "nixpkgs" + ], + "pyproject-nix": [ + "mc-rtc-nix", + "gepetto", + "gazebros2nix", + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1781249037, + "narHash": "sha256-Us5r9461lhAZ0cR4oTf7NQuNbp8ETFu0d683AF3mIPE=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "ca656fb2cf1c2834e83413c7b2caa2b2d0c2b366", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + }, + "uv2nix_2": { + "inputs": { + "nixpkgs": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "nixpkgs" + ], + "pyproject-nix": [ + "mc-rtc-nix", + "jrl-cmakemodulesv2", + "gepetto", + "gazebros2nix", + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1777463177, + "narHash": "sha256-1PcD0+IZPQXyvmXJ1OYH+23sRc9IyOKrUUBYZonVBm8=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "6c53dcf4d3f63240f57e0b0c826cb15eda61f249", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..eb28742 --- /dev/null +++ b/flake.nix @@ -0,0 +1,25 @@ +{ + description = "mesh-sampling: a tool and library to convert meshes to pointclouds / convex files"; + + inputs.mc-rtc-nix.url = "github:mc-rtc/nixpkgs"; + + outputs = + inputs: + inputs.mc-rtc-nix.lib.mkFlakoboros inputs ( + { lib, ... }: + { + overrideAttrs.mesh-sampling = + { drv-prev, pkgs-final, ... }: + { + src = lib.cleanSource ./.; + buildInputs = [ + pkgs-final.cli11 + pkgs-final.jrl-cmakemodules + ]; + nativeBuildInputs = drv-prev.nativeBuildInputs ++ [ pkgs-final.gtest ]; + doCheck = true; + meta.mainProgram = "mesh_sampling"; + }; + } + ); +} diff --git a/include/mesh_sampling/assimp_scene.h b/include/mesh_sampling/assimp_scene.h index 4fbcb85..69bdb57 100644 --- a/include/mesh_sampling/assimp_scene.h +++ b/include/mesh_sampling/assimp_scene.h @@ -1,34 +1,27 @@ #pragma once -#include // C++ importer interface -#include // Post processing flags -#include // Output data structure +#include +#include +#include +#include +#include #include +namespace fs = std::filesystem; namespace mesh_sampling { -class ASSIMPScene +struct ASSIMPScene { - // Create an instance of the Importer class - // The importer will automatically delete the scene - Assimp::Importer importer; - const aiScene * scene_; + ASSIMPScene(const std::string & model_path) : modelPath_(model_path) + { + loadScene(); + } -public: - ASSIMPScene(const std::string & model_path) - : // And have it read the given file with some example postprocessing - // Usually - if speed is not the most important aspect for you - you'll - // propably to request more postprocessing than we do in this example. - scene_(importer.ReadFile(model_path, - aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_GenNormals - | aiProcess_FixInfacingNormals)) + ASSIMPScene(const std::string & model_path, float scale) : modelPath_(model_path) { - // If the import failed, report it - if(!scene_) - { - throw std::runtime_error(importer.GetErrorString()); - } + importer.SetPropertyFloat(AI_CONFIG_GLOBAL_SCALE_FACTOR_KEY, scale); + loadScene(); } /** @@ -40,6 +33,51 @@ class ASSIMPScene { return scene_; } + + /** + * @brief Export the scene to a file in a format supported by ASSIMP + * + * @param path Export path + */ + void exportScene(const std::string & path, const bool binary = true) + { + fs::path out_path(path); + auto ext = out_path.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); }); + if(ext.empty()) + { + throw std::runtime_error("Could't export scene " + modelPath_ + " to " + path + ": invalid extension"); + } + ext.erase(0, 1); // remove leading "." + if(binary) ext += "b"; // ASSIMP suffixes binary export formats with "b" + Assimp::Exporter exporter; + aiReturn ret = exporter.Export(scene_, ext, path); + if(ret != AI_SUCCESS) + { + throw std::runtime_error("ASSIMP failed to export scene " + modelPath_ + " to " + path + ": " + + exporter.GetErrorString()); + } + } + +protected: + void loadScene() + { + scene_ = + importer.ReadFile(modelPath_, aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_GenNormals + | aiProcess_FixInfacingNormals | aiProcess_GlobalScale); + + // If the import failed, report it + if(!scene_) + { + throw std::runtime_error(importer.GetErrorString()); + } + } + +protected: + // The importer will automatically delete the scene + Assimp::Importer importer; + const aiScene * scene_; + std::string modelPath_; }; } // namespace mesh_sampling diff --git a/include/mesh_sampling/mesh_sampling.h b/include/mesh_sampling/mesh_sampling.h new file mode 100644 index 0000000..fb86b4c --- /dev/null +++ b/include/mesh_sampling/mesh_sampling.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include + +class aiScene; + +namespace fs = std::filesystem; + +namespace mesh_sampling +{ +struct ASSIMPScene; + +using CloudT = std::vector; + +const auto supported_cloud_type = std::vector{"xyz", "xyz_rgb", "xyz_normal", "xyz_rgb_normal"}; +const auto supported_extensions = std::vector{".ply", ".qc", ".stl", ".obj", ".dae"}; + +class MeshSampling +{ +public: + MeshSampling() = default; + MeshSampling(const fs::path & in_path, float scale = 1); + ~MeshSampling() = default; + + /** + * @brief Load mesh from full path or directory + * + * @param in_path path or directory + * @param scale scaling factor (default: 1) + */ + void load(const fs::path & in_path, float scale = 1); + + /** + * @brief Create a cloud + * + * @tparam PointT + * @param N number of sampling points (default: 2000) + * @return pcl::PointCloud + */ + CloudT cloud(const unsigned N = 2000); + + /** + * @brief Create and save a pointcloud + * + * @tparam PointT + * @param scene ASSIMPScene + * @param N number sampling points + * @param out_path if empty or directory, file is not saved (default: {}) + * @param binary_mode is binary (default: false) + * @return pcl::PointCloud + */ + CloudT create_cloud(const aiScene * scene, unsigned N, const fs::path & out_path = {}, bool binary_mode = false); + + /** + * @brief Create and save pointclouds, to be used when importing meshes from a directory. If out_path is empty clouds + * are not saved + * + * @tparam PointT + * @param N number of sampling points + * @param out_path directory to save the clouds (default: {}) + * @param extension saving extension (default: ".qc") + * @param binary_mode is binary mnode (default: false) + */ + std::map create_clouds(unsigned N, + const fs::path & out_path = {}, + const std::string & extension = ".qc", + bool binary_mode = false); + + /** + * @brief Create a convex object using qhull library and save it to a file + * + * @tparam PointT + * @param cloud PCL point cloud + * @param out_path output directory + */ + std::string create_convex(const CloudT & cloud, const fs::path & out_path = {}); + + std::map create_convexes(const std::map & clouds, + const fs::path & out_path = {}, + bool stop_on_fail = true); + + std::string run_qconvex_on_file(const fs::path & cloud_path, const fs::path & convex_path); + std::map create_convexes_qconvex(const std::map & clouds, + const fs::path & out_path, + bool stop_on_fail = true); + + /** + * @brief Export all meshes to a file in a format support by ASSIMP + * + * @param out_path Export path + * @param binary is binary (defaukt:false) + */ + void convertTo(const fs::path & out_path, bool binary = false); + + /** + * @brief Check supported type + * + * @param type type to check + * @param supported list of supported types (default : "xyz", "xyz_rgb", "xyz_normal", "xyz_rgb_normal") + * @return bool + */ + bool check_supported(const std::string & type, const std::vector & supported = supported_cloud_type); + + /** Get mesh at index + * + * \param path mesh path + * @return ASSIMPScene& + */ + ASSIMPScene * mesh(const std::string & path); + +private: + class Impl; + std::map> meshes_; +}; + +}; // namespace mesh_sampling diff --git a/include/mesh_sampling/qhull_io.h b/include/mesh_sampling/qhull_io.h index 07c2716..e375efe 100644 --- a/include/mesh_sampling/qhull_io.h +++ b/include/mesh_sampling/qhull_io.h @@ -3,17 +3,17 @@ */ #pragma once +#include #include -#include -#include - +#include namespace mesh_sampling { + +using CloudT = std::vector; namespace io { -template -bool saveQhullFile(const std::string & path, const pcl::PointCloud & cloud) +bool saveQhullFile(const std::string & path, const CloudT & cloud) { std::ofstream file; file.open(path, std::ios::out); @@ -26,7 +26,7 @@ bool saveQhullFile(const std::string & path, const pcl::PointCloud & clo file << std::to_string(cloud.size()) << std::endl; for(const auto & point : cloud) { - file << point.x << " " << point.y << " " << point.z << std::endl; + file << point.x() << " " << point.y() << " " << point.z() << std::endl; } return true; diff --git a/include/mesh_sampling/weighted_random_sampling.h b/include/mesh_sampling/weighted_random_sampling.h index 029c96c..894161f 100644 --- a/include/mesh_sampling/weighted_random_sampling.h +++ b/include/mesh_sampling/weighted_random_sampling.h @@ -7,13 +7,12 @@ #include #include // Output data structure #include -#include -#include #include #include namespace mesh_sampling { + double triangle_area(const Eigen::Vector3f & v1, const Eigen::Vector3f & v2, const Eigen::Vector3f & v3); Eigen::Vector3f random_point_in_triangle(const Eigen::Vector3f & v1, const Eigen::Vector3f & v2, @@ -63,12 +62,6 @@ std::vector weighted_random_choice(const std::vector & samples return vec; } -template -T randMToN(T M, T N) -{ - return M + (static_cast(std::rand()) / (RAND_MAX / (N - M))); -} - /** * @brief Create pointcloud from MESH using weighted random sampling * @@ -77,10 +70,10 @@ T randMToN(T M, T N) * https://github.com/daavoo/pyntcloud/blob/master/pyntcloud/samplers/s_mesh.py * https://medium.com/@daviddelaiglesiacastro/3f-point-cloud-generation-from-3f-triangular-mesh-bbb602ecf238 */ -template + class WeightedRandomSampling { - using CloudT = pcl::PointCloud; + using CloudT = std::vector; private: const aiScene * scene; @@ -196,61 +189,6 @@ class WeightedRandomSampling process_scene(scene); } - /** - * @brief This function computes and fill the point color only if the PointT - * supports it (PointXYZRGB...) - * - * @param[out] p point to fill - * @param[in] idx triangle id - * - * @return - */ - template - typename std::enable_if::value, void>::type fill_color(PointT & p, const size_t idx) - { - // HAS COLOR - const Eigen::Vector3f color = random_point_in_triangle(v1_rgb[idx], v2_rgb[idx], v3_rgb[idx]); - p.r = static_cast(color.x()); - p.g = static_cast(color.y()); - p.b = static_cast(color.z()); - } - - // Does nothing if the PointT does not support colors - template - typename std::enable_if::value, void>::type fill_color(PointT & /*p*/, - const size_t /*idx*/) - { - } - - /** - * @brief This function computes and fill the point normals only if the PointT - * supports it (PointNormal, PointXYZRGBNormal...) - * - * @param[out] p point to fill - * @param[in] idx triangle id - * - * @return void - */ - template - typename std::enable_if::value, void>::type fill_normal(PointT & p, const size_t idx) - { - const auto & n1 = v1_normal[idx]; - const auto & n2 = v2_normal[idx]; - const auto & n3 = v3_normal[idx]; - Eigen::Vector3f n = (n1 + n2 + n3).normalized(); - - p.normal_x = n.x(); - p.normal_y = n.y(); - p.normal_z = n.z(); - } - - // Does nothing if the PointT does not support colors - template - typename std::enable_if::value, void>::type fill_normal(PointT & /*p*/, - const size_t /*idx*/) - { - } - /** * @brief Compute a weighted random sampling of the scene. * @@ -279,14 +217,7 @@ class WeightedRandomSampling cloud->reserve(static_cast(N)); for(const auto idx : random_idx) { - const Eigen::Vector3f point = random_point_in_triangle(v1_xyz[idx], v2_xyz[idx], v3_xyz[idx]); - PointT p; - p.x = point.x(); - p.y = point.y(); - p.z = point.z(); - fill_color(p, idx); - fill_normal(p, idx); - cloud->push_back(p); + cloud->push_back(random_point_in_triangle(v1_xyz[idx], v2_xyz[idx], v3_xyz[idx])); } return cloud; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ecfaae4..92a8a87 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,28 +1,27 @@ -add_library(mesh_sampling SHARED - weighted_random_sampling.cpp - ../include/mesh_sampling/assimp_scene.h - ../include/mesh_sampling/weighted_random_sampling.h - ../include/mesh_sampling/qhull_io.h - ) -target_link_libraries(mesh_sampling PUBLIC mesh_sampling::assimp) -target_link_libraries(mesh_sampling PUBLIC mesh_sampling::PCL) -target_include_directories(mesh_sampling PUBLIC $ $) +add_library(mesh_sampling SHARED mesh_sampling.cpp weighted_random_sampling.cpp) -install(TARGETS mesh_sampling - EXPORT "${TARGETS_EXPORT_NAME}" - RUNTIME DESTINATION bin - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib) +target_link_libraries( + mesh_sampling PUBLIC mesh_sampling::assimp Qhull::qhullcpp Qhull::qhull_r + Eigen3::Eigen) -install( - DIRECTORY ${CMAKE_SOURCE_DIR}/include/ DESTINATION include) +if(TARGET Eigen3::Eigen) + target_link_libraries(mesh_sampling PUBLIC Eigen3::Eigen) +else() # legacy for older eigen installations, this is most likely no longer + # needed + target_include_directories(mesh_sampling SYSTEM ${Eigen_INCLUDE_DIR}) +endif() +target_include_directories( + mesh_sampling PUBLIC $ + $) -add_executable(mesh_sampling_bin mesh_sampling.cpp) -target_link_libraries(mesh_sampling_bin PUBLIC mesh_sampling) -target_link_libraries(mesh_sampling_bin PUBLIC Boost::program_options Boost::filesystem) -set_target_properties(mesh_sampling_bin PROPERTIES OUTPUT_NAME mesh_sampling) +target_include_directories(mesh_sampling + PRIVATE $) -install(TARGETS mesh_sampling_bin +install( + TARGETS mesh_sampling + EXPORT "${TARGETS_EXPORT_NAME}" RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib) + +install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/ DESTINATION include) diff --git a/src/mesh_sampling.cpp b/src/mesh_sampling.cpp index 1baf9ef..58949bb 100644 --- a/src/mesh_sampling.cpp +++ b/src/mesh_sampling.cpp @@ -1,145 +1,307 @@ -/* - * Copyright 2017-2020 CNRS-UM LIRMM, CNRS-AIST JRL - */ +#include "mesh_sampling/mesh_sampling.h" -#include -#include - -#include -#include -#include +// Deactivate clang to ensure include order required by libqhullcpp +// clang-format off +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include -#include -#include -#include -namespace bfs = boost::filesystem; -#include -namespace po = boost::program_options; +//clang-format on + +#include -using namespace mesh_sampling; +using namespace orgQhull; -template -void create_cloud(const aiScene * scene, unsigned N, const bfs::path & out_path, bool binary_mode = false) +namespace mesh_sampling { - WeightedRandomSampling sampler(scene); - auto cloud = sampler.weighted_random_sampling(N); - auto out = out_path.string(); - auto extension = boost::algorithm::to_lower_copy(out_path.extension().string()); - bool success = true; - if(extension == ".pcd") +MeshSampling::MeshSampling(const fs::path & in_path, float scale) +{ + load(in_path, scale); +} + +std::string MeshSampling::create_convex(const CloudT & cloud, const fs::path & out_path) +{ + if(cloud.empty()) + { + throw std::invalid_argument("create_convex: input cloud is empty."); + } + + char * buffer = nullptr; + size_t size = 0; + FILE * out_stream = open_memstream(&buffer, &size); + + if(out_stream == nullptr) + { + throw std::runtime_error("create_convex: failed to open memory stream."); + } + + // Create Qhull object + Qhull qhull; + + std::ofstream ofs; + if(!out_path.empty()){ + ofs.open(out_path.c_str()); + + if(!ofs.is_open()) + { + throw std::invalid_argument("create_convex: could not open file :" + out_path.string()); + } + } + + qhull.qh()->fout = out_stream; + + // Convert PCL cloud to a flat array for Qhull input + std::vector qhull_input; + qhull_input.reserve(cloud.size() * 3); + for(const auto & pt : cloud) { - success = pcl::io::savePCDFile(out, *cloud, binary_mode) == 0; + qhull_input.push_back(pt.x()); + qhull_input.push_back(pt.y()); + qhull_input.push_back(pt.z()); } - else if(extension == ".ply") + + try { - success = pcl::io::savePLYFile(out, *cloud, binary_mode) == 0; + qhull.setErrorStream(&std::cerr); + qhull.runQhull("pcl_input", 3, cloud.size(), qhull_input.data(), "Qt"); // 3D, triangulate option + qhull.outputQhull("o f"); } - else if(extension == ".qc") + catch(const std::exception & e) { - success = mesh_sampling::io::saveQhullFile(out, *cloud); + fclose(out_stream); + free(buffer); + throw std::runtime_error(std::string("Qhull run failed: ") + e.what()); } - else + + fflush(out_stream); + fclose(out_stream); + if(buffer == nullptr || size <= 0) { + std::cout << "Buffer is empty or null.\n"; + throw std::runtime_error("create_convex: Qhull did not produce any output"); + } + std::string output(buffer, size); + free(buffer); + + if (!out_path.empty()) { + ofs << output; + std::cout << "Convex file saved to " << out_path << std::endl; + } + + return output; +} + +std::string MeshSampling::run_qconvex_on_file(const fs::path & cloud_path, const fs::path & convex_path) +{ + // 1. Build the qconvex command + std::ostringstream cmd; + cmd << "qconvex TI " << cloud_path << " TO " << convex_path << " Qt o f"; + std::cout << "cmd would be: " << cmd.str() << std::endl; + + // 2. Run the command + int ret = std::system(cmd.str().c_str()); + if(ret != 0) { - std::cerr << "Output pointcloud type " << extension << " is not supported"; - exit(1); + throw std::runtime_error("run_qconvex_on_file: qconvex failed to run."); } - if(!success) + // 3. Read and return the output from convex_path (name-ch.txt) + std::ifstream ifs(convex_path); + if(!ifs.is_open()) { - std::cerr << "Saving to " << out << " failed." << std::endl; - exit(1); + throw std::runtime_error("run_qconvex_on_file: could not open output file: " + convex_path.string()); } + std::string output((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + ifs.close(); + + return output; } -int main(int argc, char ** argv) +std::map MeshSampling::create_convexes_qconvex(const std::map & clouds, + const fs::path & out_path, + bool stop_on_fail) { - po::variables_map vm; - po::options_description desc("Allowed Options"); - - // clang-format off - desc.add_options() - ("help", "Produce this message") - ("in", po::value(), "Input mesh (supported by ASSIMP)") - ("out", po::value(), "Output file (ply, pcd, qc)") - ("samples", po::value()->default_value(10000), "Number of points to sample") - ("type", po::value()->default_value("xyz_rgb_normal"), "Type of cloud to generate (xyz, xyz_rgb, xyz_rgb_normal)") - ("binary", po::bool_switch()->default_value(false), "Outputs pointcloud in binary format (default: false)"); - // clang-format on - - po::positional_options_description pos; - pos.add("in", 1); - pos.add("out", 1); - - // parse arguments and save them in the variable map (vm) - po::store(po::command_line_parser(argc, argv).options(desc).positional(pos).run(), vm); - po::notify(vm); - - if(!vm.count("in") || !vm.count("out") || vm.count("help")) - { - std::cout << "mesh_sampling is a command-line tool to sample pointcloud from CAD meshes\n\n"; - std::cout << "Usage: mesh_sampling [options] in out\n\n"; - std::cout << desc << "\n"; - return !vm.count("help"); - } - - std::string in_path = vm["in"].as(); - std::string out_path = vm["out"].as(); - bfs::path in_p(in_path); - bfs::path out_p(out_path); - auto out_extension = boost::algorithm::to_lower_copy(out_p.extension().string()); - unsigned N = vm["samples"].as(); - std::string cloud_type = vm["type"].as(); - bool cloud_binary = vm["binary"].as(); - auto supported_cloud_type = std::vector{"xyz", "xyz_rgb", "xyz_normal", "xyz_rgb_normal"}; - auto supported_extensions = std::vector{".ply", ".pcd", ".qc"}; - - auto check_supported = [](const std::vector & supported, const std::string & value) { - if(std::find(supported.begin(), supported.end(), value) == supported.end()) + if(!out_path.empty() && !fs::is_directory(out_path)) + { + throw std::invalid_argument("create_convexes: out_path has to be a directory {" + out_path.string() + "}"); + } + + std::map output; + + for(const auto & cloud : clouds) + { + try { - std::cerr << "This program only supports: "; - std::copy(supported.begin(), supported.end(), std::ostream_iterator(std::cerr, ", ")); - std::cerr << std::endl; - exit(1); + fs::path input_path = cloud.first; // full path to .qc file + std::string base = input_path.stem().string(); // e.g., "mesh1" + fs::path convex_path = input_path.parent_path() / (base + "-ch.txt"); + + output[cloud.first] = run_qconvex_on_file(input_path, convex_path); } - }; - check_supported(supported_cloud_type, cloud_type); - check_supported(supported_extensions, out_extension); - - // Generate pointcloud - // ASSIMPScene loader should be used and kept in scope for as long as the mesh - // is needed. - // The default constructor loads the mesh with all the required - // post-processing according to point type (normal computation...) - std::unique_ptr mesh = nullptr; - try + catch(const std::exception & e) + { + std::cerr << e.what() << std::endl; + + if(stop_on_fail) return {}; + } + } + + return output; +} + +std::map MeshSampling::create_convexes(const std::map & clouds, + const fs::path & out_path, + bool stop_on_fail) +{ + if(!out_path.empty() && !fs::is_directory(out_path)) { - mesh = std::unique_ptr(new ASSIMPScene(in_path)); + throw std::invalid_argument("create_convexes: out_path has to be a directory {" + out_path.string() + "}"); } - catch(std::runtime_error & e) + + std::map output; + + for(const auto & cloud : clouds) { - std::cerr << e.what() << std::endl; - return -1; + try + { + if(out_path.empty()) + output[cloud.first] = create_convex(cloud.second, {}); + else + output[cloud.first] = create_convex(cloud.second, out_path / (fs::path(cloud.first).filename().stem().string() + "-ch.txt")); + } + catch(const std::exception & e) + { + std::cerr << e.what() << std::endl; + + if(stop_on_fail) return {}; + } } - if(cloud_type == "xyz") + return output; +} + +std::map MeshSampling::create_clouds(unsigned N, + const fs::path & out_path, + const std::string & extension, + bool binary_mode) +{ + std::map clouds; + if(!out_path.empty()) { - create_cloud(mesh->scene(), N, out_p, cloud_binary); + if(!fs::is_directory(out_path) && meshes_.size() > 1) + { + throw std::runtime_error("[Error] create_clouds, out_path is not a directory"); + } + + if(extension.empty()) + { + throw std::runtime_error("[Error] create_clouds, extension is not set"); + } + + for(const auto & mesh : meshes_) + { + auto path = fs::is_directory(out_path) ? out_path / (fs::path(mesh.first).filename().stem().string() + extension) + : out_path; + auto cloud = create_cloud(mesh.second->scene(), N, path, binary_mode); + clouds.insert({path.string(), cloud}); + } } - else if(cloud_type == "xyz_rgb") + else { - create_cloud(mesh->scene(), N, out_p, cloud_binary); + for(const auto & mesh : meshes_) + { + auto cloud = create_cloud(mesh.second->scene(), N, {}, binary_mode); + clouds.insert({mesh.first, cloud}); + } } - else if(cloud_type == "xyz_normal") + + return clouds; +} + +CloudT MeshSampling::cloud(const unsigned N) +{ + WeightedRandomSampling sampler(meshes_.begin()->second->scene()); + auto cloud = sampler.weighted_random_sampling(N); + return *cloud; +} + +CloudT MeshSampling::create_cloud(const aiScene * scene, + const unsigned N, + const fs::path & out_path, + bool binary_mode) +{ + WeightedRandomSampling sampler(scene); + auto cloud = sampler.weighted_random_sampling(N); + + if(!out_path.empty() && !fs::is_directory(out_path)) { - create_cloud(mesh->scene(), N, out_p, cloud_binary); + auto extension = out_path.extension().string(); + bool success = io::saveQhullFile(out_path, *cloud); + + if(!success) + { + std::cerr << "Saving to " << out_path << " failed." << std::endl; + return {}; + } + else + { + std::cout << "Saving cloud to " << out_path << " success" << std::endl; + } } - else if(cloud_type == "xyz_rgb_normal") + + return *cloud; +} + +void MeshSampling::load(const fs::path & in_path, float scale) +{ + try { - create_cloud(mesh->scene(), N, out_p, cloud_binary); + if(fs::is_directory(in_path)) + { + for(const auto & dir_entry : std::filesystem::directory_iterator{in_path}){ + if(check_supported(dir_entry.path().extension(), supported_extensions)){ + meshes_.insert( + std::make_pair(dir_entry.path(), std::make_shared(dir_entry.path(), scale))); + } + } + } + else + { + meshes_.insert(std::make_pair(in_path, std::make_shared(in_path, scale))); + } } + catch(std::runtime_error & e) + { + std::cerr << e.what() << std::endl; + return; + } +} - return 0; +void MeshSampling::convertTo(const fs::path & out_path, bool binary) +{ + for(auto & mesh : meshes_) mesh.second->exportScene(out_path, binary); } + +bool MeshSampling::check_supported(const std::string & type, const std::vector & supported) +{ + return std::any_of(supported.begin(), supported.end(), + [&](const std::string & ext) { return std::equal(ext.begin(), ext.end(), type.begin(), type.end(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); }); }); +} + +ASSIMPScene * MeshSampling::mesh(const std::string & path) + { + if(meshes_.count(path) > 0) return meshes_.begin()->second.get(); + + return nullptr; + } + +} // namespace mesh_sampling diff --git a/src/weighted_random_sampling.cpp b/src/weighted_random_sampling.cpp index 6e8fdea..70a8d5e 100644 --- a/src/weighted_random_sampling.cpp +++ b/src/weighted_random_sampling.cpp @@ -2,17 +2,14 @@ * Copyright 2017-2020 CNRS-UM LIRMM, CNRS-AIST JRL */ -#include "mesh_sampling/weighted_random_sampling.h" +#include +#include +#include +using Random = effolkronium::random_static; namespace mesh_sampling { -// Explicit instantiation for common types -template class WeightedRandomSampling; -template class WeightedRandomSampling; -template class WeightedRandomSampling; -template class WeightedRandomSampling; - double triangle_area(const Eigen::Vector3f & v1, const Eigen::Vector3f & v2, const Eigen::Vector3f & v3) { return 0.5 * (v2 - v1).cross(v3 - v1).norm(); @@ -30,8 +27,8 @@ Eigen::Vector3f random_point_in_triangle(const Eigen::Vector3f & v1, const Eigen::Vector3f & v2, const Eigen::Vector3f & v3) { - float u = randMToN(0, 1); - float v = randMToN(0, 1); + float u = Random::get(0.0f, 1.0f); + float v = Random::get(0.0f, 1.0f); if(u + v > 1) { u = 1 - u; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1cbefcc..c77e957 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,10 +1,12 @@ -find_package(Boost REQUIRED COMPONENTS unit_test_framework) +find_package(GTest REQUIRED) +include(GoogleTest) macro(mesh_sampling_test NAME) add_executable(${NAME} ${NAME}.cpp) - target_link_libraries(${NAME} PUBLIC Boost::unit_test_framework Boost::disable_autolinking ${ARGN}) - target_compile_definitions(${NAME} PRIVATE -DBOOST_TEST_DYN_LINK -DBOOST_TEST_MAIN) - add_test(${NAME} ${NAME}) + target_link_libraries(${NAME} PUBLIC GTest::gtest ${ARGN}) + target_compile_definitions( + ${NAME} PRIVATE -DPROJECT_SOURCE_DIR="${PROJECT_SOURCE_DIR}") + gtest_discover_tests(${NAME}) endmacro() mesh_sampling_test(testWeightedRandomSampling mesh_sampling) diff --git a/tests/testWeightedRandomSampling.cpp b/tests/testWeightedRandomSampling.cpp index 12d6081..ba13b26 100644 --- a/tests/testWeightedRandomSampling.cpp +++ b/tests/testWeightedRandomSampling.cpp @@ -2,15 +2,16 @@ * Copyright 2015-2019 CNRS-UM LIRMM, CNRS-AIST JRL */ -#include - +#include #include #include #include +#include #include -#include -#include -#include + +#ifndef PROJECT_SOURCE_DIR +# error "PROJECT_SOURCE_DIR must be defined to compile this file" +#endif using namespace mesh_sampling; @@ -18,29 +19,35 @@ using namespace mesh_sampling; // and samples N point from it. // It only tests that the object can be loaded, and that the number of points // after sampling is as expected -BOOST_AUTO_TEST_CASE(TestWeightedRandomSampling) +TEST(TestWeightedRandomSampling, Test1) { - const std::string model_path = "../../sample/suzanne.obj"; + const std::string model_path = PROJECT_SOURCE_DIR "/sample/suzanne.obj"; size_t N = 1000; - BOOST_REQUIRE_THROW(ASSIMPScene("wrong path.dae"), std::runtime_error); - auto mesh = std::unique_ptr(new ASSIMPScene(model_path)); + ASSERT_THROW(ASSIMPScene("wrong path.dae"), std::runtime_error); + MeshSampling sampler(model_path); std::cout << "Sampling " << N << " points from " << model_path << std::endl; - WeightedRandomSampling sampling_xyz(mesh->scene()); + WeightedRandomSampling sampling_xyz(sampler.mesh(model_path)->scene()); auto cloud_xyz = sampling_xyz.weighted_random_sampling(N); - pcl::io::savePCDFileASCII("/tmp/example_xyz.pcd", *cloud_xyz); - BOOST_REQUIRE(cloud_xyz->size() == N); + // pcl::io::savePCDFileASCII("/tmp/example_xyz.pcd", *cloud_xyz); + ASSERT_TRUE(cloud_xyz->size() == N); - WeightedRandomSampling sampling_rgb(mesh->scene()); + WeightedRandomSampling sampling_rgb(sampler.mesh(model_path)->scene()); auto cloud_rgb = sampling_rgb.weighted_random_sampling(N); - BOOST_REQUIRE(cloud_rgb->size() == N); + ASSERT_TRUE(cloud_rgb->size() == N); - WeightedRandomSampling sampling_normal(mesh->scene()); + WeightedRandomSampling sampling_normal(sampler.mesh(model_path)->scene()); auto cloud_normal = sampling_normal.weighted_random_sampling(N); - BOOST_REQUIRE(cloud_normal->size() == N); + ASSERT_TRUE(cloud_normal->size() == N); - WeightedRandomSampling sampling_rgb_normal(mesh->scene()); + WeightedRandomSampling sampling_rgb_normal(sampler.mesh(model_path)->scene()); auto cloud_rgb_normal = sampling_rgb_normal.weighted_random_sampling(N); - BOOST_REQUIRE(cloud_rgb_normal->size() == N); + ASSERT_TRUE(cloud_rgb_normal->size() == N); +} + +int main(int argc, char ** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/tests/test_mesh_sampling/CMakeLists.txt b/tests/test_mesh_sampling/CMakeLists.txt index e562283..5cf3b6c 100644 --- a/tests/test_mesh_sampling/CMakeLists.txt +++ b/tests/test_mesh_sampling/CMakeLists.txt @@ -2,16 +2,18 @@ cmake_minimum_required(VERSION 3.11) set(PROJECT_NAME test_mesh_sampling) set(PROJECT_DESCRIPTION "Sample pointcloud from meshes") -set(PROJECT_URL https://github.com/arntanguy/mesh_sampling) +set(PROJECT_URL https://github.com/jrl-umi3218/mesh_sampling) set(PROJECT_VERSION 1.0.0) set(PROJECT_USE_CMAKE_EXPORT FALSE) set(PROJECT_DEBUG_POSTFIX "_d") set(CXX_DISABLE_WERROR 1) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) -project(${PROJECT_NAME} LANGUAGES CXX VERSION ${PROJECT_VERSION}) +project( + ${PROJECT_NAME} + LANGUAGES CXX + VERSION ${PROJECT_VERSION}) find_package(mesh_sampling REQUIRED) include_directories(include) add_subdirectory(src) - diff --git a/tests/test_mesh_sampling/src/CMakeLists.txt b/tests/test_mesh_sampling/src/CMakeLists.txt index 876d524..5e1b23e 100644 --- a/tests/test_mesh_sampling/src/CMakeLists.txt +++ b/tests/test_mesh_sampling/src/CMakeLists.txt @@ -1,3 +1,5 @@ add_executable(test_mesh_sampling main.cpp) target_link_libraries(test_mesh_sampling PUBLIC mesh_sampling::mesh_sampling) -target_include_directories(test_mesh_sampling PUBLIC $ $) +target_include_directories( + test_mesh_sampling PUBLIC $ + $) diff --git a/tests/test_mesh_sampling/src/main.cpp b/tests/test_mesh_sampling/src/main.cpp index c56a581..a97f5d3 100644 --- a/tests/test_mesh_sampling/src/main.cpp +++ b/tests/test_mesh_sampling/src/main.cpp @@ -18,10 +18,8 @@ #include #include +#include #include -#include -#include -#include using namespace mesh_sampling; @@ -55,36 +53,18 @@ int main(int argc, char ** argv) help(); } - // ASSIMPScene loader should be used and kept in scope for as long as the mesh - // is needed. - // The default constructor loads the mesh with all the required - // post-processing according to point type (normal computation...) - std::unique_ptr mesh = nullptr; - try - { - mesh = std::unique_ptr(new ASSIMPScene(model_path)); - } - catch(std::runtime_error & e) - { - std::cerr << e.what() << std::endl; - return -1; - } + MeshSampling sampler(model_path); std::cout << "Sampling " << N << " points from " << model_path << std::endl; - WeightedRandomSampling sampling_xyz(mesh->scene()); + WeightedRandomSampling sampling_xyz(sampler.mesh(model_path)->scene()); auto cloud_xyz = sampling_xyz.weighted_random_sampling(N); - pcl::io::savePCDFileASCII("/tmp/example_xyz.pcd", *cloud_xyz); - std::cout << "Cloud size: " << cloud_xyz->size() << ", expected: " << N << std::endl; - WeightedRandomSampling sampling_rgb(mesh->scene()); + WeightedRandomSampling sampling_rgb(sampler.mesh(model_path)->scene()); auto cloud_rgb = sampling_rgb.weighted_random_sampling(N); - pcl::io::savePCDFileASCII("/tmp/example_rgb.pcd", *cloud_rgb); - WeightedRandomSampling sampling_normal(mesh->scene()); + WeightedRandomSampling sampling_normal(sampler.mesh(model_path)->scene()); auto cloud_normal = sampling_normal.weighted_random_sampling(N); - pcl::io::savePCDFileASCII("/tmp/example_normal.pcd", *cloud_normal); - WeightedRandomSampling sampling_rgb_normal(mesh->scene()); + WeightedRandomSampling sampling_rgb_normal(sampler.mesh(model_path)->scene()); auto cloud_rgb_normal = sampling_rgb_normal.weighted_random_sampling(N); - pcl::io::savePCDFileASCII("/tmp/example_rgb_normal.pcd", *cloud_rgb_normal); } diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt new file mode 100644 index 0000000..9d9f575 --- /dev/null +++ b/utils/CMakeLists.txt @@ -0,0 +1,9 @@ +add_executable(mesh_sampling_bin mesh_sampling.cpp) +target_link_libraries(mesh_sampling_bin PUBLIC mesh_sampling CLI11::CLI11) +set_target_properties(mesh_sampling_bin PROPERTIES OUTPUT_NAME mesh_sampling) + +install( + TARGETS mesh_sampling_bin + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib) diff --git a/utils/mesh_sampling.cpp b/utils/mesh_sampling.cpp new file mode 100644 index 0000000..f0cd950 --- /dev/null +++ b/utils/mesh_sampling.cpp @@ -0,0 +1,91 @@ +/* + * Copyright 2017-2025 CNRS-UM LIRMM, CNRS-AIST JRL + */ + +#include +#include + +namespace fs = std::filesystem; + +using namespace mesh_sampling; + +int main(int argc, char ** argv) +{ + CLI::App app("mesh_sampling"); + argv = app.ensure_utf8(argv); + + fs::path in; + app.add_option("in,--in", in, "Input mesh (supported by ASSIMP)")->required()->check(CLI::ExistingPath); + + fs::path out; + app.add_option("out,--out", out, "Output file (ply, pcd, qc, stl)"); + + fs::path convex; + app.add_option("--convex", convex, "Output convex directory")->check(CLI::ExistingPath); + + bool use_qconvex = false; + app.add_flag("--use-qconvex", use_qconvex, "Use qconvex executable instead of libqhullcpp"); + + unsigned int N; + app.add_option("--samples", N, "Number of points to sample")->default_val(10000)->check(CLI::PositiveNumber); + + float scale; + app.add_option("--scale", scale, "Scale factor applied to the mesh")->default_val(1.0)->check(CLI::Range(0, 1)); + + std::string cloud_type = "xyz_rgb_normal"; + app.add_option("--type, -t", cloud_type, "Type of cloud to generate (xyz, xyz_rgb, xyz_rgb_normal)"); + + bool binary_format; + app.add_option("--binary, -b", binary_format, "Outputs in binary format (default: false)")->default_val(false); + + bool convert; + app.add_option("--convert", convert, "Convert from one mesh type to another (supported by ASSIMP)") + ->default_val(false); + + try + { + app.parse(argc, argv); + } + catch(const CLI::ParseError & e) + { + // If parsing fails, print the error, print the help menu, and exit cleanly + std::cout << (e.get_exit_code() == 0 ? "" : "Error: ") << e.what() << "\n\n"; + return app.exit(CLI::CallForHelp()); + } + + MeshSampling mesh_sampler(in); + + if(!mesh_sampler.check_supported(cloud_type)) + { + std::cerr << "Type not suppported : "; + std::copy(mesh_sampling::supported_cloud_type.begin(), mesh_sampling::supported_cloud_type.end(), + std::ostream_iterator(std::cerr, ", ")); + std::cerr << std::endl; + } + + if(convert) + { + mesh_sampler.convertTo(out, binary_format); + } + else + { + // Only these types are supported for convex generation + if(cloud_type == "xyz" || cloud_type == "xyz_rgb" || cloud_type == "xyz_normal" || cloud_type == "xyz_rgb_normal") + { + auto mesh = mesh_sampler.create_clouds(N, out, ".qc", binary_format); + if(!convex.empty()) + { + if(use_qconvex) + mesh_sampler.create_convexes_qconvex(mesh, convex, true); + else + mesh_sampler.create_convexes(mesh, convex, true); + } + } + else + { + std::cerr << "Convex generation only supported for cloud types : xyz, xyz_rgb, xyz_normal, xyz_rgb_normal" + << std::endl; + } + } + return 0; +}