diff --git a/.github/tsan.suppressions.ini b/.github/tsan.suppressions.ini index 254af5c6..35001261 100644 --- a/.github/tsan.suppressions.ini +++ b/.github/tsan.suppressions.ini @@ -11,3 +11,5 @@ race:openvdb::*::tools::mesh_to_volume_internal::ComputeIntersectingVoxelSign race:openvdb::*::tools::v2s_internal::UpdatePoints # See https://github.com/embree/embree/issues/469 race_top:embree::parallel_any_of +# Well... this one isn't gonna fix itself. +race:^triangleinit diff --git a/LagrangeOptions.cmake.sample b/LagrangeOptions.cmake.sample index b15107be..3348f708 100644 --- a/LagrangeOptions.cmake.sample +++ b/LagrangeOptions.cmake.sample @@ -107,7 +107,7 @@ # set(USE_SANITIZER # "Address;Undefined" # CACHE STRING -# "Compile with a sanitizer. Options are: Address, Memory, MemoryWithOrigins, Undefined, Thread, Leak, 'Address;Undefined'" +# "Compile with a sanitizer. Options are: Address, Memory, MemoryWithOrigins, Undefined, Thread, Leak, 'Address;Undefined', CFI" # ) # Code coverage. diff --git a/VERSION b/VERSION index 476d3ceb..fe675047 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.21.1 +6.22.0 diff --git a/cmake/recipes/external/CPM.cmake b/cmake/recipes/external/CPM.cmake index 70aebf10..10a692eb 100644 --- a/cmake/recipes/external/CPM.cmake +++ b/cmake/recipes/external/CPM.cmake @@ -1,1154 +1,41 @@ -# CPM.cmake - CMake's missing package manager -# =========================================== -# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. # -# MIT License -# ----------- -#[[ - Copyright (c) 2019-2022 Lars Melchior and contributors - - 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. -]] - -cmake_minimum_required(VERSION 3.14 FATAL_ERROR) - -# Initialize logging prefix -if(NOT CPM_INDENT) - set(CPM_INDENT - "CPM:" - CACHE INTERNAL "" - ) -endif() - -if(NOT COMMAND cpm_message) - function(cpm_message) - message(${ARGV}) - endfunction() -endif() - -set(CURRENT_CPM_VERSION 0.38.1) - -get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) -if(CPM_DIRECTORY) - if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) - if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) - message( - AUTHOR_WARNING - "${CPM_INDENT} \ -A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ -It is recommended to upgrade CPM to the most recent version. \ -See https://github.com/cpm-cmake/CPM.cmake for more information." - ) - endif() - if(${CMAKE_VERSION} VERSION_LESS "3.17.0") - include(FetchContent) - endif() - return() - endif() - - get_property( - CPM_INITIALIZED GLOBAL "" - PROPERTY CPM_INITIALIZED - SET - ) - if(CPM_INITIALIZED) - return() - endif() -endif() - -if(CURRENT_CPM_VERSION MATCHES "development-version") - message( - WARNING "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ -Please update to a recent release if possible. \ -See https://github.com/cpm-cmake/CPM.cmake for details." - ) -endif() - -set_property(GLOBAL PROPERTY CPM_INITIALIZED true) - -macro(cpm_set_policies) - # the policy allows us to change options without caching - cmake_policy(SET CMP0077 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) - - # the policy allows us to change set(CACHE) without caching - if(POLICY CMP0126) - cmake_policy(SET CMP0126 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) - endif() - - # The policy uses the download time for timestamp, instead of the timestamp in the archive. This - # allows for proper rebuilds when a projects url changes - if(POLICY CMP0135) - cmake_policy(SET CMP0135 NEW) - set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) - endif() -endmacro() -cpm_set_policies() - -option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" - $ENV{CPM_USE_LOCAL_PACKAGES} -) -option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" - $ENV{CPM_LOCAL_PACKAGES_ONLY} -) -option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) -option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" - $ENV{CPM_DONT_UPDATE_MODULE_PATH} -) -option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" - $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} -) -option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK - "Add all packages added through CPM.cmake to the package lock" - $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} -) -option(CPM_USE_NAMED_CACHE_DIRECTORIES - "Use additional directory of package name in cache on the most nested level." - $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} -) - -set(CPM_VERSION - ${CURRENT_CPM_VERSION} - CACHE INTERNAL "" -) -set(CPM_DIRECTORY - ${CPM_CURRENT_DIRECTORY} - CACHE INTERNAL "" -) -set(CPM_FILE - ${CMAKE_CURRENT_LIST_FILE} - CACHE INTERNAL "" -) -set(CPM_PACKAGES - "" - CACHE INTERNAL "" -) -set(CPM_DRY_RUN - OFF - CACHE INTERNAL "Don't download or configure dependencies (for testing)" -) +# Copyright 2022 Adobe +# All Rights Reserved. +# +# NOTICE: Adobe permits you to use, modify, and distribute this file in +# accordance with the terms of the Adobe license agreement accompanying +# it. +# +set(CPM_DOWNLOAD_VERSION 0.39.0) -if(DEFINED ENV{CPM_SOURCE_CACHE}) - set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") else() - set(CPM_SOURCE_CACHE_DEFAULT OFF) + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") endif() -set(CPM_SOURCE_CACHE - ${CPM_SOURCE_CACHE_DEFAULT} - CACHE PATH "Directory to download CPM dependencies" -) - -if(NOT CPM_DONT_UPDATE_MODULE_PATH) - set(CPM_MODULE_PATH - "${CMAKE_BINARY_DIR}/CPM_modules" - CACHE INTERNAL "" - ) - # remove old modules - file(REMOVE_RECURSE ${CPM_MODULE_PATH}) - file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) - # locally added CPM modules should override global packages - set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") -endif() +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) -if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) - set(CPM_PACKAGE_LOCK_FILE - "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" - CACHE INTERNAL "" +function(download_cpm) + message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") + file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} ) - file(WRITE ${CPM_PACKAGE_LOCK_FILE} - "# CPM Package Lock\n# This file should be committed to version control\n\n" - ) -endif() - -include(FetchContent) - -# Try to infer package name from git repository uri (path or url) -function(cpm_package_name_from_git_uri URI RESULT) - if("${URI}" MATCHES "([^/:]+)/?.git/?$") - set(${RESULT} - ${CMAKE_MATCH_1} - PARENT_SCOPE - ) - else() - unset(${RESULT} PARENT_SCOPE) - endif() -endfunction() - -# Try to infer package name and version from a url -function(cpm_package_name_and_ver_from_url url outName outVer) - if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") - # We matched an archive - set(filename "${CMAKE_MATCH_1}") - - if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") - # We matched - (ie foo-1.2.3) - set(${outName} - "${CMAKE_MATCH_1}" - PARENT_SCOPE - ) - set(${outVer} - "${CMAKE_MATCH_2}" - PARENT_SCOPE - ) - elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") - # We couldn't find a name, but we found a version - # - # In many cases (which we don't handle here) the url would look something like - # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly - # distinguish the package name from the irrelevant bits. Moreover if we try to match the - # package name from the filename, we'd get bogus at best. - unset(${outName} PARENT_SCOPE) - set(${outVer} - "${CMAKE_MATCH_1}" - PARENT_SCOPE - ) - else() - # Boldly assume that the file name is the package name. - # - # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but - # such cases should be quite rare. No popular service does this... we think. - set(${outName} - "${filename}" - PARENT_SCOPE - ) - unset(${outVer} PARENT_SCOPE) - endif() - else() - # No ideas yet what to do with non-archives - unset(${outName} PARENT_SCOPE) - unset(${outVer} PARENT_SCOPE) - endif() -endfunction() - -function(cpm_find_package NAME VERSION) - string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") - find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) - if(${CPM_ARGS_NAME}_FOUND) - if(DEFINED ${CPM_ARGS_NAME}_VERSION) - set(VERSION ${${CPM_ARGS_NAME}_VERSION}) - endif() - cpm_message(STATUS "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") - CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") - set(CPM_PACKAGE_FOUND - YES - PARENT_SCOPE - ) - else() - set(CPM_PACKAGE_FOUND - NO - PARENT_SCOPE - ) - endif() -endfunction() - -# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from -# finding the system library -function(cpm_create_module_file Name) - if(NOT CPM_DONT_UPDATE_MODULE_PATH) - # erase any previous modules - file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake - "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" - ) - endif() endfunction() -# Find a package locally or fallback to CPMAddPackage -function(CPMFindPackage) - set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) - - cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) - - if(NOT DEFINED CPM_ARGS_VERSION) - if(DEFINED CPM_ARGS_GIT_TAG) - cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) - endif() - endif() - - set(downloadPackage ${CPM_DOWNLOAD_ALL}) - if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) - set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) - elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) - set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) - endif() - if(downloadPackage) - CPMAddPackage(${ARGN}) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") - if(CPM_PACKAGE_ALREADY_ADDED) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) - - if(NOT CPM_PACKAGE_FOUND) - CPMAddPackage(${ARGN}) - cpm_export_variables(${CPM_ARGS_NAME}) - endif() - -endfunction() - -# checks if a package has been added before -function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) - if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) - CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) - if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") - message( - WARNING - "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." - ) - endif() - cpm_get_fetch_properties(${CPM_ARGS_NAME}) - set(${CPM_ARGS_NAME}_ADDED NO) - set(CPM_PACKAGE_ALREADY_ADDED - YES - PARENT_SCOPE - ) - cpm_export_variables(${CPM_ARGS_NAME}) - else() - set(CPM_PACKAGE_ALREADY_ADDED - NO - PARENT_SCOPE - ) - endif() -endfunction() - -# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of -# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted -# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 -function(cpm_parse_add_package_single_arg arg outArgs) - # Look for a scheme - if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") - string(TOLOWER "${CMAKE_MATCH_1}" scheme) - set(uri "${CMAKE_MATCH_2}") - - # Check for CPM-specific schemes - if(scheme STREQUAL "gh") - set(out "GITHUB_REPOSITORY;${uri}") - set(packageType "git") - elseif(scheme STREQUAL "gl") - set(out "GITLAB_REPOSITORY;${uri}") - set(packageType "git") - elseif(scheme STREQUAL "bb") - set(out "BITBUCKET_REPOSITORY;${uri}") - set(packageType "git") - # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine - # type - elseif(arg MATCHES ".git/?(@|#|$)") - set(out "GIT_REPOSITORY;${arg}") - set(packageType "git") - else() - # Fall back to a URL - set(out "URL;${arg}") - set(packageType "archive") - - # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. - # We just won't bother with the additional complexity it will induce in this function. SVN is - # done by multi-arg - endif() - else() - if(arg MATCHES ".git/?(@|#|$)") - set(out "GIT_REPOSITORY;${arg}") - set(packageType "git") - else() - # Give up - message(FATAL_ERROR "${CPM_INDENT} Can't determine package type of '${arg}'") - endif() - endif() - - # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs - # containing '@' can be used - string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") - - # Parse the rest according to package type - if(packageType STREQUAL "git") - # For git repos we interpret #... as a tag or branch or commit hash - string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") - elseif(packageType STREQUAL "archive") - # For archives we interpret #... as a URL hash. - string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") - # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url - # should do this at a later point - else() - # We should never get here. This is an assertion and hitting it means there's a bug in the code - # above. A packageType was set, but not handled by this if-else. - message(FATAL_ERROR "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'") - endif() - - set(${outArgs} - ${out} - PARENT_SCOPE - ) -endfunction() - -# Check that the working directory for a git repo is clean -function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) - - find_package(Git REQUIRED) - - if(NOT GIT_EXECUTABLE) - # No git executable, assume directory is clean - set(${isClean} - TRUE - PARENT_SCOPE - ) - return() - endif() - - # check for uncommitted changes - execute_process( - COMMAND ${GIT_EXECUTABLE} status --porcelain - RESULT_VARIABLE resultGitStatus - OUTPUT_VARIABLE repoStatus - OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET - WORKING_DIRECTORY ${repoPath} - ) - if(resultGitStatus) - # not supposed to happen, assume clean anyway - message(WARNING "${CPM_INDENT} Calling git status on folder ${repoPath} failed") - set(${isClean} - TRUE - PARENT_SCOPE - ) - return() - endif() - - if(NOT "${repoStatus}" STREQUAL "") - set(${isClean} - FALSE - PARENT_SCOPE - ) - return() - endif() - - # check for committed changes - execute_process( - COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} - RESULT_VARIABLE resultGitDiff - OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET - WORKING_DIRECTORY ${repoPath} - ) - - if(${resultGitDiff} EQUAL 0) - set(${isClean} - TRUE - PARENT_SCOPE - ) - else() - set(${isClean} - FALSE - PARENT_SCOPE - ) - endif() - -endfunction() - -# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload -# FetchContent calls. As these are internal cmake properties, this method should be used carefully -# and may need modification in future CMake versions. Source: -# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 -function(cpm_override_fetchcontent contentName) - cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") - if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") - message(FATAL_ERROR "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") - endif() - - string(TOLOWER ${contentName} contentNameLower) - set(prefix "_FetchContent_${contentNameLower}") - - set(propertyName "${prefix}_sourceDir") - define_property( - GLOBAL - PROPERTY ${propertyName} - BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" - FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" - ) - set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") - - set(propertyName "${prefix}_binaryDir") - define_property( - GLOBAL - PROPERTY ${propertyName} - BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" - FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" - ) - set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") - - set(propertyName "${prefix}_populated") - define_property( - GLOBAL - PROPERTY ${propertyName} - BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" - FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" - ) - set_property(GLOBAL PROPERTY ${propertyName} TRUE) -endfunction() - -# Download and add a package from source -function(CPMAddPackage) - cpm_set_policies() - - list(LENGTH ARGN argnLength) - if(argnLength EQUAL 1) - cpm_parse_add_package_single_arg("${ARGN}" ARGN) - - # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM - set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") - endif() - - set(oneValueArgs - NAME - FORCE - VERSION - GIT_TAG - DOWNLOAD_ONLY - GITHUB_REPOSITORY - GITLAB_REPOSITORY - BITBUCKET_REPOSITORY - GIT_REPOSITORY - SOURCE_DIR - DOWNLOAD_COMMAND - FIND_PACKAGE_ARGUMENTS - NO_CACHE - SYSTEM - GIT_SHALLOW - EXCLUDE_FROM_ALL - SOURCE_SUBDIR - ) - - set(multiValueArgs URL OPTIONS) - - cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") - - # Set default values for arguments - - if(NOT DEFINED CPM_ARGS_VERSION) - if(DEFINED CPM_ARGS_GIT_TAG) - cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) - endif() - endif() - - if(CPM_ARGS_DOWNLOAD_ONLY) - set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) - else() - set(DOWNLOAD_ONLY NO) - endif() - - if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) - set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") - elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) - set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") - elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) - set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") - endif() - - if(DEFINED CPM_ARGS_GIT_REPOSITORY) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) - if(NOT DEFINED CPM_ARGS_GIT_TAG) - set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) - endif() - - # If a name wasn't provided, try to infer it from the git repo - if(NOT DEFINED CPM_ARGS_NAME) - cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) - endif() - endif() - - set(CPM_SKIP_FETCH FALSE) - - if(DEFINED CPM_ARGS_GIT_TAG) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) - # If GIT_SHALLOW is explicitly specified, honor the value. - if(DEFINED CPM_ARGS_GIT_SHALLOW) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) - endif() - endif() - - if(DEFINED CPM_ARGS_URL) - # If a name or version aren't provided, try to infer them from the URL - list(GET CPM_ARGS_URL 0 firstUrl) - cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) - # If we fail to obtain name and version from the first URL, we could try other URLs if any. - # However multiple URLs are expected to be quite rare, so for now we won't bother. - - # If the caller provided their own name and version, they trump the inferred ones. - if(NOT DEFINED CPM_ARGS_NAME) - set(CPM_ARGS_NAME ${nameFromUrl}) - endif() - if(NOT DEFINED CPM_ARGS_VERSION) - set(CPM_ARGS_VERSION ${verFromUrl}) - endif() - - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") - endif() - - # Check for required arguments - - if(NOT DEFINED CPM_ARGS_NAME) - message( - FATAL_ERROR - "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" - ) - endif() - - # Check if package has been added before - cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") - if(CPM_PACKAGE_ALREADY_ADDED) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - # Check for manual overrides - if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") - set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) - set(CPM_${CPM_ARGS_NAME}_SOURCE "") - CPMAddPackage( - NAME "${CPM_ARGS_NAME}" - SOURCE_DIR "${PACKAGE_SOURCE}" - EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" - SYSTEM "${CPM_ARGS_SYSTEM}" - OPTIONS "${CPM_ARGS_OPTIONS}" - SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" - DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" - FORCE True - ) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - # Check for available declaration - if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") - set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) - set(CPM_DECLARATION_${CPM_ARGS_NAME} "") - CPMAddPackage(${declaration}) - cpm_export_variables(${CPM_ARGS_NAME}) - # checking again to ensure version and option compatibility - cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") - return() - endif() - - if(NOT CPM_ARGS_FORCE) - if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) - cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) - - if(CPM_PACKAGE_FOUND) - cpm_export_variables(${CPM_ARGS_NAME}) - return() - endif() - - if(CPM_LOCAL_PACKAGES_ONLY) - message( - SEND_ERROR - "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" - ) - endif() - endif() - endif() - - CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") - - if(DEFINED CPM_ARGS_GIT_TAG) - set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") - elseif(DEFINED CPM_ARGS_SOURCE_DIR) - set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") - else() - set(PACKAGE_INFO "${CPM_ARGS_VERSION}") - endif() - - if(DEFINED FETCHCONTENT_BASE_DIR) - # respect user's FETCHCONTENT_BASE_DIR if set - set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) - else() - set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) - endif() - - if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) - elseif(DEFINED CPM_ARGS_SOURCE_DIR) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) - if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) - # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work - # for relative paths. - get_filename_component( - source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} - ) - else() - set(source_directory ${CPM_ARGS_SOURCE_DIR}) - endif() - if(NOT EXISTS ${source_directory}) - string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) - # remove timestamps so CMake will re-download the dependency - file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") - endif() - elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) - string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) - set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) - list(SORT origin_parameters) - if(CPM_USE_NAMED_CACHE_DIRECTORIES) - string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") - set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) - else() - string(SHA1 origin_hash "${origin_parameters}") - set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) - endif() - # Expand `download_directory` relative path. This is important because EXISTS doesn't work for - # relative paths. - get_filename_component(download_directory ${download_directory} ABSOLUTE) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) - - if(CPM_SOURCE_CACHE) - file(LOCK ${download_directory}/../cmake.lock) - endif() - - if(EXISTS ${download_directory}) - if(CPM_SOURCE_CACHE) - file(LOCK ${download_directory}/../cmake.lock RELEASE) - endif() - - cpm_store_fetch_properties( - ${CPM_ARGS_NAME} "${download_directory}" - "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" - ) - cpm_get_fetch_properties("${CPM_ARGS_NAME}") - - if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) - # warn if cache has been changed since checkout - cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) - if(NOT ${IS_CLEAN}) - message( - WARNING "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" - ) - endif() - endif() - - cpm_add_subdirectory( - "${CPM_ARGS_NAME}" - "${DOWNLOAD_ONLY}" - "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" - "${${CPM_ARGS_NAME}_BINARY_DIR}" - "${CPM_ARGS_EXCLUDE_FROM_ALL}" - "${CPM_ARGS_SYSTEM}" - "${CPM_ARGS_OPTIONS}" - ) - set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") - - # As the source dir is already cached/populated, we override the call to FetchContent. - set(CPM_SKIP_FETCH TRUE) - cpm_override_fetchcontent( - "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" - BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" - ) - - else() - # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but - # it should guarantee no commit hash get mis-detected. - if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) - cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) - if(NOT ${IS_HASH}) - list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) - endif() - endif() - - # remove timestamps so CMake will re-download the dependency - file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) - set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") - endif() - endif() - - cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") - - if(CPM_PACKAGE_LOCK_ENABLED) - if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) - cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") - elseif(CPM_ARGS_SOURCE_DIR) - cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") - else() - cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") - endif() - endif() - - cpm_message( - STATUS "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" - ) - - if(NOT CPM_SKIP_FETCH) - cpm_declare_fetch( - "${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}" "${PACKAGE_INFO}" "${CPM_ARGS_UNPARSED_ARGUMENTS}" - ) - cpm_fetch_package("${CPM_ARGS_NAME}" populated) - if(CPM_CACHE_SOURCE AND download_directory) - file(LOCK ${download_directory}/../cmake.lock RELEASE) - endif() - if(${populated}) - cpm_add_subdirectory( - "${CPM_ARGS_NAME}" - "${DOWNLOAD_ONLY}" - "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" - "${${CPM_ARGS_NAME}_BINARY_DIR}" - "${CPM_ARGS_EXCLUDE_FROM_ALL}" - "${CPM_ARGS_SYSTEM}" - "${CPM_ARGS_OPTIONS}" - ) - endif() - cpm_get_fetch_properties("${CPM_ARGS_NAME}") - endif() - - set(${CPM_ARGS_NAME}_ADDED YES) - cpm_export_variables("${CPM_ARGS_NAME}") -endfunction() - -# Fetch a previously declared package -macro(CPMGetPackage Name) - if(DEFINED "CPM_DECLARATION_${Name}") - CPMAddPackage(NAME ${Name}) - else() - message(SEND_ERROR "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available") - endif() -endmacro() - -# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set -macro(cpm_export_variables name) - set(${name}_SOURCE_DIR - "${${name}_SOURCE_DIR}" - PARENT_SCOPE - ) - set(${name}_BINARY_DIR - "${${name}_BINARY_DIR}" - PARENT_SCOPE - ) - set(${name}_ADDED - "${${name}_ADDED}" - PARENT_SCOPE - ) - set(CPM_LAST_PACKAGE_NAME - "${name}" - PARENT_SCOPE - ) -endmacro() - -# declares a package, so that any call to CPMAddPackage for the package name will use these -# arguments instead. Previous declarations will not be overridden. -macro(CPMDeclarePackage Name) - if(NOT DEFINED "CPM_DECLARATION_${Name}") - set("CPM_DECLARATION_${Name}" "${ARGN}") - endif() -endmacro() - -function(cpm_add_to_package_lock Name) - if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) - cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) - file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") - endif() -endfunction() - -function(cpm_add_comment_to_package_lock Name) - if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) - cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) - file(APPEND ${CPM_PACKAGE_LOCK_FILE} - "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" - ) - endif() -endfunction() - -# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to -# update it -macro(CPMUsePackageLock file) - if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) - get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) - if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) - include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) - endif() - if(NOT TARGET cpm-update-package-lock) - add_custom_target( - cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} - ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} - ) - endif() - set(CPM_PACKAGE_LOCK_ENABLED true) - endif() -endmacro() - -# registers a package that has been added to CPM -function(CPMRegisterPackage PACKAGE VERSION) - list(APPEND CPM_PACKAGES ${PACKAGE}) - set(CPM_PACKAGES - ${CPM_PACKAGES} - CACHE INTERNAL "" - ) - set("CPM_PACKAGE_${PACKAGE}_VERSION" - ${VERSION} - CACHE INTERNAL "" - ) -endfunction() - -# retrieve the current version of the package to ${OUTPUT} -function(CPMGetPackageVersion PACKAGE OUTPUT) - set(${OUTPUT} - "${CPM_PACKAGE_${PACKAGE}_VERSION}" - PARENT_SCOPE - ) -endfunction() - -# declares a package in FetchContent_Declare -function(cpm_declare_fetch PACKAGE VERSION INFO) - if(${CPM_DRY_RUN}) - cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") - return() - endif() - - FetchContent_Declare(${PACKAGE} ${ARGN}) -endfunction() - -# returns properties for a package previously defined by cpm_declare_fetch -function(cpm_get_fetch_properties PACKAGE) - if(${CPM_DRY_RUN}) - return() - endif() - - set(${PACKAGE}_SOURCE_DIR - "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" - PARENT_SCOPE - ) - set(${PACKAGE}_BINARY_DIR - "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" - PARENT_SCOPE - ) -endfunction() - -function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) - if(${CPM_DRY_RUN}) - return() - endif() - - set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR - "${source_dir}" - CACHE INTERNAL "" - ) - set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR - "${binary_dir}" - CACHE INTERNAL "" - ) -endfunction() - -# adds a package as a subdirectory if viable, according to provided options -function( - cpm_add_subdirectory - PACKAGE - DOWNLOAD_ONLY - SOURCE_DIR - BINARY_DIR - EXCLUDE - SYSTEM - OPTIONS -) - - if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) - set(addSubdirectoryExtraArgs "") - if(EXCLUDE) - list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) - endif() - if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") - # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM - list(APPEND addSubdirectoryExtraArgs SYSTEM) - endif() - if(OPTIONS) - foreach(OPTION ${OPTIONS}) - cpm_parse_option("${OPTION}") - set(${OPTION_KEY} "${OPTION_VALUE}") - endforeach() - endif() - set(CPM_OLD_INDENT "${CPM_INDENT}") - set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") - add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) - set(CPM_INDENT "${CPM_OLD_INDENT}") - endif() -endfunction() - -# downloads a previously declared package via FetchContent and exports the variables -# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope -function(cpm_fetch_package PACKAGE populated) - set(${populated} - FALSE - PARENT_SCOPE - ) - if(${CPM_DRY_RUN}) - cpm_message(STATUS "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") - return() - endif() - - FetchContent_GetProperties(${PACKAGE}) - - string(TOLOWER "${PACKAGE}" lower_case_name) - - if(NOT ${lower_case_name}_POPULATED) - FetchContent_Populate(${PACKAGE}) - set(${populated} - TRUE - PARENT_SCOPE - ) - endif() - - cpm_store_fetch_properties( - ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} - ) - - set(${PACKAGE}_SOURCE_DIR - ${${lower_case_name}_SOURCE_DIR} - PARENT_SCOPE - ) - set(${PACKAGE}_BINARY_DIR - ${${lower_case_name}_BINARY_DIR} - PARENT_SCOPE - ) -endfunction() - -# splits a package option -function(cpm_parse_option OPTION) - string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") - string(LENGTH "${OPTION}" OPTION_LENGTH) - string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) - if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) - # no value for key provided, assume user wants to set option to "ON" - set(OPTION_VALUE "ON") - else() - math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") - string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) - endif() - set(OPTION_KEY - "${OPTION_KEY}" - PARENT_SCOPE - ) - set(OPTION_VALUE - "${OPTION_VALUE}" - PARENT_SCOPE - ) -endfunction() - -# guesses the package version from a git tag -function(cpm_get_version_from_git_tag GIT_TAG RESULT) - string(LENGTH ${GIT_TAG} length) - if(length EQUAL 40) - # GIT_TAG is probably a git hash - set(${RESULT} - 0 - PARENT_SCOPE - ) - else() - string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) - set(${RESULT} - ${CMAKE_MATCH_1} - PARENT_SCOPE - ) - endif() -endfunction() - -# guesses if the git tag is a commit hash or an actual tag or a branch name. -function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) - string(LENGTH "${GIT_TAG}" length) - # full hash has 40 characters, and short hash has at least 7 characters. - if(length LESS 7 OR length GREATER 40) - set(${RESULT} - 0 - PARENT_SCOPE - ) - else() - if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") - set(${RESULT} - 1 - PARENT_SCOPE - ) - else() - set(${RESULT} - 0 - PARENT_SCOPE - ) - endif() - endif() -endfunction() - -function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) - set(oneValueArgs - NAME - FORCE - VERSION - GIT_TAG - DOWNLOAD_ONLY - GITHUB_REPOSITORY - GITLAB_REPOSITORY - GIT_REPOSITORY - SOURCE_DIR - DOWNLOAD_COMMAND - FIND_PACKAGE_ARGUMENTS - NO_CACHE - SYSTEM - GIT_SHALLOW - ) - set(multiValueArgs OPTIONS) - cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - foreach(oneArgName ${oneValueArgs}) - if(DEFINED CPM_ARGS_${oneArgName}) - if(${IS_IN_COMMENT}) - string(APPEND PRETTY_OUT_VAR "#") - endif() - if(${oneArgName} STREQUAL "SOURCE_DIR") - string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} - ${CPM_ARGS_${oneArgName}} - ) - endif() - string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") - endif() - endforeach() - foreach(multiArgName ${multiValueArgs}) - if(DEFINED CPM_ARGS_${multiArgName}) - if(${IS_IN_COMMENT}) - string(APPEND PRETTY_OUT_VAR "#") - endif() - string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") - foreach(singleOption ${CPM_ARGS_${multiArgName}}) - if(${IS_IN_COMMENT}) - string(APPEND PRETTY_OUT_VAR "#") - endif() - string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") - endforeach() - endif() - endforeach() - - if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") - if(${IS_IN_COMMENT}) - string(APPEND PRETTY_OUT_VAR "#") - endif() - string(APPEND PRETTY_OUT_VAR " ") - foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) - string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") - endforeach() - string(APPEND PRETTY_OUT_VAR "\n") +if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) + download_cpm() +else() + # resume download if it previously failed + file(READ ${CPM_DOWNLOAD_LOCATION} check) + if("${check}" STREQUAL "") + download_cpm() endif() + unset(check) +endif() - set(${OUT_VAR} - ${PRETTY_OUT_VAR} - PARENT_SCOPE - ) - -endfunction() +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/cmake/recipes/external/entt.cmake b/cmake/recipes/external/entt.cmake index c665a211..594706d8 100644 --- a/cmake/recipes/external/entt.cmake +++ b/cmake/recipes/external/entt.cmake @@ -20,7 +20,5 @@ include(CPM) CPMAddPackage( NAME entt GITHUB_REPOSITORY skypjack/entt - GIT_TAG v3.9.0 + GIT_TAG v3.13.2 ) - -set_target_properties(aob PROPERTIES FOLDER third_party) diff --git a/cmake/recipes/external/fmt.cmake b/cmake/recipes/external/fmt.cmake new file mode 100644 index 00000000..8100401e --- /dev/null +++ b/cmake/recipes/external/fmt.cmake @@ -0,0 +1,33 @@ +# +# Copyright 2024 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +if(TARGET fmt::fmt) + return() +endif() + +message(STATUS "Third-party (external): creating target 'fmt::fmt'") + +set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "fmt") +option(FMT_INSTALL "Generate the install target." ON) + +include(CPM) +CPMAddPackage( + NAME fmt + GITHUB_REPOSITORY fmtlib/fmt + GIT_TAG 10.2.1 + # Other versions to test with: + # GIT_TAG 10.1.1 + # GIT_TAG 9.1.0 + # GIT_TAG 8.1.1 +) + +set_target_properties(fmt PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(fmt PROPERTIES FOLDER third_party) diff --git a/cmake/recipes/external/gl3w.cmake b/cmake/recipes/external/gl3w.cmake index 49e6be43..e04230e0 100644 --- a/cmake/recipes/external/gl3w.cmake +++ b/cmake/recipes/external/gl3w.cmake @@ -27,4 +27,5 @@ endblock() add_library(gl3w::gl3w ALIAS gl3w) +set_target_properties(gl3w PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(gl3w PROPERTIES FOLDER third_party) diff --git a/cmake/recipes/external/glfw.cmake b/cmake/recipes/external/glfw.cmake index 527d8f1e..bacf1b4c 100644 --- a/cmake/recipes/external/glfw.cmake +++ b/cmake/recipes/external/glfw.cmake @@ -38,7 +38,7 @@ include(CPM) CPMAddPackage( NAME glfw GITHUB_REPOSITORY glfw/glfw - GIT_TAG tags/3.3.6 + GIT_TAG 3.3.6 ) add_library(glfw::glfw ALIAS glfw) diff --git a/cmake/recipes/external/imgui.cmake b/cmake/recipes/external/imgui.cmake index 6cdf9735..733533c3 100644 --- a/cmake/recipes/external/imgui.cmake +++ b/cmake/recipes/external/imgui.cmake @@ -43,6 +43,7 @@ endblock() target_compile_definitions(imgui PUBLIC IMGUI_DISABLE_OBSOLETE_KEYIO) target_compile_definitions(imgui PUBLIC IMGUI_DEFINE_MATH_OPERATORS) +set_target_properties(imgui PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(imgui PROPERTIES FOLDER third_party) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") diff --git a/cmake/recipes/external/imguizmo.cmake b/cmake/recipes/external/imguizmo.cmake index 52ab665a..2ceae42d 100644 --- a/cmake/recipes/external/imguizmo.cmake +++ b/cmake/recipes/external/imguizmo.cmake @@ -33,6 +33,7 @@ target_include_directories(imguizmo PUBLIC "${imguizmo_SOURCE_DIR}") include(imgui) target_link_libraries(imguizmo PUBLIC imgui::imgui) +set_target_properties(imguizmo PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(imguizmo PROPERTIES FOLDER third_party) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") diff --git a/cmake/recipes/external/nasoq.cmake b/cmake/recipes/external/nasoq.cmake index 2b6eed79..330efef9 100644 --- a/cmake/recipes/external/nasoq.cmake +++ b/cmake/recipes/external/nasoq.cmake @@ -21,15 +21,16 @@ endif() include(blas) include(eigen) include(metis) +include(lapack) option(NASOQ_WITH_EIGEN "Build NASOQ Eigen interface" ON) -# Note: For now, Nasoq's CMake code to find OpenBLAS is broken on Linux, so we default to MKL. + if("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "arm64" OR "${CMAKE_OSX_ARCHITECTURES}" MATCHES "arm64") - # apple M1 + # Change to accelerate after https://github.com/sympiler/nasoq/pull/27 is merged set(NASOQ_BLAS_BACKEND "OpenBLAS" CACHE STRING "BLAS implementation for NASOQ to use") else() - # windows, linux, apple intel + # use MKL on windows, linux, apple intel set(NASOQ_BLAS_BACKEND "MKL" CACHE STRING "BLAS implementation for NASOQ to use") endif() @@ -47,7 +48,7 @@ include(CPM) CPMAddPackage( NAME nasoq GITHUB_REPOSITORY sympiler/nasoq - GIT_TAG 3565bba5c984e24a1e22f3555e2ba09e31c4486f + GIT_TAG fc2051dfa991160cd6dd326d0fb1580ffb77b93b ) target_link_libraries(nasoq PUBLIC BLAS::BLAS) diff --git a/cmake/recipes/external/sanitizers.cmake b/cmake/recipes/external/sanitizers.cmake index 045719d2..776471e7 100644 --- a/cmake/recipes/external/sanitizers.cmake +++ b/cmake/recipes/external/sanitizers.cmake @@ -1,7 +1,7 @@ # Source: https://github.com/StableCoder/cmake-scripts # SPDX-License-Identifier: Apache-2.0 # -# Copyright (C) 2018 by George Cave - gcave@stablecoder.ca +# Copyright (C) 2018-2022 by George Cave - gcave@stablecoder.ca # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of @@ -19,11 +19,13 @@ # # All modifications are Copyright 2020 Adobe. +include(CheckCXXSourceCompiles) + set(USE_SANITIZER "" CACHE STRING - "Compile with a sanitizer. Options are: Address, Memory, MemoryWithOrigins, Undefined, Thread, Leak, 'Address;Undefined'" + "Compile with a sanitizer. Options are: Address, Memory, MemoryWithOrigins, Undefined, Thread, Leak, 'Address;Undefined', CFI" ) function(append value) @@ -34,52 +36,182 @@ function(append value) endforeach(variable) endfunction() +function(append_quoteless value) + foreach(variable ${ARGN}) + set(${variable} + ${${variable}} ${value} + PARENT_SCOPE) + endforeach(variable) +endfunction() + +function(test_san_flags return_var flags) + set(QUIET_BACKUP ${CMAKE_REQUIRED_QUIET}) + set(CMAKE_REQUIRED_QUIET TRUE) + unset(${return_var} CACHE) + set(FLAGS_BACKUP ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "${flags}") + check_cxx_source_compiles("int main() { return 0; }" ${return_var}) + set(CMAKE_REQUIRED_FLAGS "${FLAGS_BACKUP}") + set(CMAKE_REQUIRED_QUIET "${QUIET_BACKUP}") +endfunction() + if(USE_SANITIZER) + append("-fno-omit-frame-pointer" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + + unset(SANITIZER_SELECTED_FLAGS) + if(UNIX) - append("-fno-omit-frame-pointer" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) if(uppercase_CMAKE_BUILD_TYPE STREQUAL "DEBUG") append("-O1" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) endif() - if(USE_SANITIZER MATCHES "([Aa]ddress);([Uu]ndefined)" - OR USE_SANITIZER MATCHES "([Uu]ndefined);([Aa]ddress)") - message(STATUS "Building with Address, Undefined sanitizers") - append("-fsanitize=address,undefined" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) - elseif(USE_SANITIZER MATCHES "([Aa]ddress)") + if(USE_SANITIZER MATCHES "([Aa]ddress)") # Optional: -fno-optimize-sibling-calls -fsanitize-address-use-after-scope - message(STATUS "Building with Address sanitizer") - append("-fsanitize=address" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) - elseif(USE_SANITIZER MATCHES "([Mm]emory([Ww]ith[Oo]rigins)?)") + message(STATUS "Testing with Address sanitizer") + set(SANITIZER_ADDR_FLAG "-fsanitize=address") + test_san_flags(SANITIZER_ADDR_AVAILABLE ${SANITIZER_ADDR_FLAG}) + if(SANITIZER_ADDR_AVAILABLE) + message(STATUS " Building with Address sanitizer") + append("${SANITIZER_ADDR_FLAG}" SANITIZER_SELECTED_FLAGS) + + if(AFL) + append_quoteless(AFL_USE_ASAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + endif() + else() + message( + FATAL_ERROR + "Address sanitizer not available for ${CMAKE_CXX_COMPILER}") + endif() + endif() + + if(USE_SANITIZER MATCHES "([Mm]emory([Ww]ith[Oo]rigins)?)") # Optional: -fno-optimize-sibling-calls -fsanitize-memory-track-origins=2 - append("-fsanitize=memory" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + set(SANITIZER_MEM_FLAG "-fsanitize=memory") if(USE_SANITIZER MATCHES "([Mm]emory[Ww]ith[Oo]rigins)") - message(STATUS "Building with MemoryWithOrigins sanitizer") - append("-fsanitize-memory-track-origins" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + message(STATUS "Testing with MemoryWithOrigins sanitizer") + append("-fsanitize-memory-track-origins" SANITIZER_MEM_FLAG) else() - message(STATUS "Building with Memory sanitizer") + message(STATUS "Testing with Memory sanitizer") endif() - elseif(USE_SANITIZER MATCHES "([Uu]ndefined)") - message(STATUS "Building with Undefined sanitizer") - append("-fsanitize=undefined" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + test_san_flags(SANITIZER_MEM_AVAILABLE ${SANITIZER_MEM_FLAG}) + if(SANITIZER_MEM_AVAILABLE) + if(USE_SANITIZER MATCHES "([Mm]emory[Ww]ith[Oo]rigins)") + message(STATUS " Building with MemoryWithOrigins sanitizer") + else() + message(STATUS " Building with Memory sanitizer") + endif() + append("${SANITIZER_MEM_FLAG}" SANITIZER_SELECTED_FLAGS) + + if(AFL) + append_quoteless(AFL_USE_MSAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + endif() + else() + message( + FATAL_ERROR + "Memory [With Origins] sanitizer not available for ${CMAKE_CXX_COMPILER}" + ) + endif() + endif() + + if(USE_SANITIZER MATCHES "([Uu]ndefined)") + message(STATUS "Testing with Undefined Behaviour sanitizer") + set(SANITIZER_UB_FLAG "-fsanitize=undefined") if(EXISTS "${BLACKLIST_FILE}") - append("-fsanitize-blacklist=${BLACKLIST_FILE}" CMAKE_C_FLAGS - CMAKE_CXX_FLAGS) + append("-fsanitize-blacklist=${BLACKLIST_FILE}" SANITIZER_UB_FLAG) endif() - elseif(USE_SANITIZER MATCHES "([Tt]hread)") - message(STATUS "Building with Thread sanitizer") - append("-fsanitize=thread" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) - elseif(USE_SANITIZER MATCHES "([Ll]eak)") - message(STATUS "Building with Leak sanitizer") - append("-fsanitize=leak" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + test_san_flags(SANITIZER_UB_AVAILABLE ${SANITIZER_UB_FLAG}) + if(SANITIZER_UB_AVAILABLE) + message(STATUS " Building with Undefined Behaviour sanitizer") + append("${SANITIZER_UB_FLAG}" SANITIZER_SELECTED_FLAGS) + + if(AFL) + append_quoteless(AFL_USE_UBSAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + endif() + else() + message( + FATAL_ERROR + "Undefined Behaviour sanitizer not available for ${CMAKE_CXX_COMPILER}" + ) + endif() + endif() + + if(USE_SANITIZER MATCHES "([Tt]hread)") + message(STATUS "Testing with Thread sanitizer") + set(SANITIZER_THREAD_FLAG "-fsanitize=thread") + test_san_flags(SANITIZER_THREAD_AVAILABLE ${SANITIZER_THREAD_FLAG}) + if(SANITIZER_THREAD_AVAILABLE) + message(STATUS " Building with Thread sanitizer") + append("${SANITIZER_THREAD_FLAG}" SANITIZER_SELECTED_FLAGS) + + if(AFL) + append_quoteless(AFL_USE_TSAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + endif() + else() + message( + FATAL_ERROR "Thread sanitizer not available for ${CMAKE_CXX_COMPILER}" + ) + endif() + endif() + + if(USE_SANITIZER MATCHES "([Ll]eak)") + message(STATUS "Testing with Leak sanitizer") + set(SANITIZER_LEAK_FLAG "-fsanitize=leak") + test_san_flags(SANITIZER_LEAK_AVAILABLE ${SANITIZER_LEAK_FLAG}) + if(SANITIZER_LEAK_AVAILABLE) + message(STATUS " Building with Leak sanitizer") + append("${SANITIZER_LEAK_FLAG}" SANITIZER_SELECTED_FLAGS) + + if(AFL) + append_quoteless(AFL_USE_LSAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + endif() + else() + message( + FATAL_ERROR "Thread sanitizer not available for ${CMAKE_CXX_COMPILER}" + ) + endif() + endif() + + if(USE_SANITIZER MATCHES "([Cc][Ff][Ii])") + message(STATUS "Testing with Control Flow Integrity(CFI) sanitizer") + set(SANITIZER_CFI_FLAG "-fsanitize=cfi") + test_san_flags(SANITIZER_CFI_AVAILABLE ${SANITIZER_CFI_FLAG}) + if(SANITIZER_CFI_AVAILABLE) + message(STATUS " Building with Control Flow Integrity(CFI) sanitizer") + append("${SANITIZER_LEAK_FLAG}" SANITIZER_SELECTED_FLAGS) + + if(AFL) + append_quoteless(AFL_USE_CFISAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + endif() + else() + message( + FATAL_ERROR + "Control Flow Integrity(CFI) sanitizer not available for ${CMAKE_CXX_COMPILER}" + ) + endif() + endif() + + message(STATUS "Sanitizer flags: ${SANITIZER_SELECTED_FLAGS}") + test_san_flags(SANITIZER_SELECTED_COMPATIBLE ${SANITIZER_SELECTED_FLAGS}) + if(SANITIZER_SELECTED_COMPATIBLE) + message(STATUS " Building with ${SANITIZER_SELECTED_FLAGS}") + append("${SANITIZER_SELECTED_FLAGS}" CMAKE_C_FLAGS CMAKE_CXX_FLAGS) else() message( - FATAL_ERROR "Unsupported value of USE_SANITIZER: ${USE_SANITIZER}") + FATAL_ERROR + " Sanitizer flags ${SANITIZER_SELECTED_FLAGS} are not compatible.") endif() elseif(MSVC) if(USE_SANITIZER MATCHES "([Aa]ddress)") message(STATUS "Building with Address sanitizer") - # Do use AddressSanitizer you need to disable incompatible options. See details here: + + # To use AddressSanitizer you need to disable incompatible options. See details here: # https://learn.microsoft.com/en-us/cpp/sanitizers/asan?view=msvc-170#ide-msbuild # # Please note that there are some issues with using it directly from the IDE, it may work better to @@ -100,6 +232,11 @@ if(USE_SANITIZER) # Do not use program database for debug builds (since it requires incremental linking). set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") + + if(AFL) + append_quoteless(AFL_USE_ASAN=1 CMAKE_C_COMPILER_LAUNCHER + CMAKE_CXX_COMPILER_LAUNCHER) + endif() else() message( FATAL_ERROR diff --git a/cmake/recipes/external/spdlog.cmake b/cmake/recipes/external/spdlog.cmake index 5505384c..310d8188 100644 --- a/cmake/recipes/external/spdlog.cmake +++ b/cmake/recipes/external/spdlog.cmake @@ -16,17 +16,27 @@ endif() message(STATUS "Third-party (external): creating target 'spdlog::spdlog'") option(SPDLOG_INSTALL "Generate the install target" ON) +option(SPDLOG_FMT_EXTERNAL "Use external fmt library instead of bundled" OFF) + +if(SPDLOG_FMT_EXTERNAL) + include(fmt) +endif() + set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "spdlog") +# Versions of fmt bundled with spdlog: +# - spdlog 1.13.0 -> fmt 9.1.0 +# - spdlog 1.12.0 -> fmt 9.1.0 +# - spdlog 1.11.0 -> fmt 9.1.0 +# - spdlog 1.10.0 -> fmt 8.1.1 include(CPM) CPMAddPackage( NAME spdlog GITHUB_REPOSITORY gabime/spdlog - GIT_TAG v1.11.0 + GIT_TAG v1.13.0 ) set_target_properties(spdlog PROPERTIES POSITION_INDEPENDENT_CODE ON) - set_target_properties(spdlog PROPERTIES FOLDER third_party) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang" OR diff --git a/modules/core/CMakeLists.txt b/modules/core/CMakeLists.txt index 937d8c6d..7921c92a 100644 --- a/modules/core/CMakeLists.txt +++ b/modules/core/CMakeLists.txt @@ -124,6 +124,24 @@ if(LAGRANGE_USE_PCH) ) endif() +set(LAGRANGE_FMT_EIGEN_FORMATTER "" CACHE PATH + "Include file to inject to overwite Lagrange's fmt::formatter<> for Eigen types" +) +if(LAGRANGE_FMT_EIGEN_FORMATTER) + file(REAL_PATH "${LAGRANGE_FMT_EIGEN_FORMATTER}" LAGRANGE_FMT_EIGEN_FORMATTER_REAL EXPAND_TILDE) + if(EXISTS "${LAGRANGE_FMT_EIGEN_FORMATTER_REAL}") + message(STATUS "Using user-provided Eigen formatter: ${LAGRANGE_FMT_EIGEN_FORMATTER_REAL}") + target_compile_definitions(lagrange_core PUBLIC + LA_FMT_EIGEN_FORMATTER="${LAGRANGE_FMT_EIGEN_FORMATTER_REAL}" + ) + else() + message(FATAL_ERROR + "Non-empty LAGRANGE_FMT_EIGEN_FORMATTER provided, but does not exist: " + "${LAGRANGE_FMT_EIGEN_FORMATTER_REAL}" + ) + endif() +endif() + # 3. unit tests and examples if(LAGRANGE_UNIT_TESTS) add_subdirectory(tests) diff --git a/modules/core/include/lagrange/Attribute.h b/modules/core/include/lagrange/Attribute.h index 601782fa..9247c225 100644 --- a/modules/core/include/lagrange/Attribute.h +++ b/modules/core/include/lagrange/Attribute.h @@ -114,6 +114,24 @@ class AttributeBase /// [[nodiscard]] size_t get_num_channels() const { return m_num_channels; } + /// + /// Sets the attribute usage tag. + /// + /// @note No check is performed, use with caution! + /// + /// @param[in] usage New usage tag. + /// + void unsafe_set_usage(AttributeUsage usage) { m_usage = usage; } + + /// + /// Sets the attribute element type. + /// + /// @note No check is performed, use with caution! + /// + /// @param[in] element New element type. + /// + void unsafe_set_element_type(AttributeElement element) { m_element = element; } + protected: /// Element type (vertex, facet, indexed, etc.). AttributeElement m_element; diff --git a/modules/core/include/lagrange/experimental/Array.h b/modules/core/include/lagrange/experimental/Array.h index 0eb5dd4a..cd56126a 100644 --- a/modules/core/include/lagrange/experimental/Array.h +++ b/modules/core/include/lagrange/experimental/Array.h @@ -268,9 +268,9 @@ class EigenArray : public ArrayBase return string_format( "EigenArray>", scalar_name, - EigenType::RowsAtCompileTime, - EigenType::ColsAtCompileTime, - EigenType::Options); + static_cast(EigenType::RowsAtCompileTime), + static_cast(EigenType::ColsAtCompileTime), + static_cast(EigenType::Options)); } private: @@ -348,9 +348,9 @@ class EigenArrayRef<_EigenType, false> : public ArrayBase return string_format( "EigenArrayRef>", scalar_name, - EigenType::RowsAtCompileTime, - EigenType::ColsAtCompileTime, - EigenType::Options); + static_cast(EigenType::RowsAtCompileTime), + static_cast(EigenType::ColsAtCompileTime), + static_cast(EigenType::Options)); } private: @@ -419,9 +419,9 @@ class EigenArrayRef<_EigenType, true> : public ArrayBase return string_format( "EigenArrayRef>", scalar_name, - EigenType::RowsAtCompileTime, - EigenType::ColsAtCompileTime, - EigenType::Options); + static_cast(EigenType::RowsAtCompileTime), + static_cast(EigenType::ColsAtCompileTime), + static_cast(EigenType::Options)); } private: diff --git a/modules/core/include/lagrange/internal/fast_edge_sort.h b/modules/core/include/lagrange/internal/fast_edge_sort.h new file mode 100644 index 00000000..654a414a --- /dev/null +++ b/modules/core/include/lagrange/internal/fast_edge_sort.h @@ -0,0 +1,126 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +#include +#include + +namespace lagrange::internal { + +template +struct UnorientedEdge +{ + Index v1; + Index v2; + Index id; + + UnorientedEdge(Index x, Index y, Index c) + : v1(std::min(x, y)) + , v2(std::max(x, y)) + , id(c) + {} + + auto key() const { return std::make_pair(v1, v2); } + + bool operator<(const UnorientedEdge& e) const { return key() < e.key(); } + + bool operator!=(const UnorientedEdge& e) const { return key() != e.key(); } +}; + +/// +/// Sort an array of edges using a parallel bucket sort. +/// +/// @todo Maybe we can implement a local cache system for SurfaceMesh<> to reuse tmp buffers. +/// Maybe this would make the add_vertex/add_facet functions efficient enough that we do +/// not need to allocate them all at once in our `triangulate_polygonal_facets` +/// function. +/// +/// @param[in] num_edges Number of edges to sort. +/// @param[in] num_vertices Number of vertices in the mesh. +/// @param[in] get_edge Callback to retrieve the n-th edge endpoints. Must be safe to +/// call from multiple threads. +/// @param[in] vertex_to_first_edge Optional buffer of size num_vertices + 1 to avoid internal +/// allocations on repeated uses. +/// +/// @tparam Index Edge index type. +/// @tparam Func Callback function to retrieve edge endpoints. +/// +/// @return A vector of sorted edge indices. Edges with repeated endpoints will be continuous in +/// the sorted array. +/// +template +std::vector fast_edge_sort( + Index num_edges, + Index num_vertices, + Func get_edge, + span vertex_to_first_edge = {}) +{ + std::vector local_buffer; + if (vertex_to_first_edge.empty()) { + local_buffer.assign(num_vertices + 1, 0); + vertex_to_first_edge = local_buffer; + } else { + std::fill(vertex_to_first_edge.begin(), vertex_to_first_edge.end(), Index(0)); + } + la_runtime_assert(vertex_to_first_edge.size() == static_cast(num_vertices) + 1); + // Count number of edges starting at each vertex + for (Index e = 0; e < num_edges; ++e) { + std::array v = get_edge(e); + if (v[0] > v[1]) { + std::swap(v[0], v[1]); + } + vertex_to_first_edge[v[0] + 1]++; + } + // Prefix sum to compute actual offsets + std::partial_sum( + vertex_to_first_edge.begin(), + vertex_to_first_edge.end(), + vertex_to_first_edge.begin()); + la_runtime_assert(vertex_to_first_edge.back() == num_edges); + // Bucket each edge id to its respective starting vertex + std::vector edge_ids(num_edges); + for (Index e = 0; e < num_edges; ++e) { + std::array v = get_edge(e); + if (v[0] > v[1]) { + std::swap(v[0], v[1]); + } + edge_ids[vertex_to_first_edge[v[0]]++] = e; + } + // Shift back the offset buffer 'vertex_to_first_edge' (can use std::shift_right in C++20 :p) + std::rotate( + vertex_to_first_edge.rbegin(), + vertex_to_first_edge.rbegin() + 1, + vertex_to_first_edge.rend()); + vertex_to_first_edge.front() = 0; + // Sort each bucket in parallel + tbb::parallel_for(Index(0), num_vertices, [&](Index v) { + tbb::parallel_sort( + edge_ids.begin() + vertex_to_first_edge[v], + edge_ids.begin() + vertex_to_first_edge[v + 1], + [&](Index ei, Index ej) { + auto vi = get_edge(ei); + auto vj = get_edge(ej); + if (vi[0] > vi[1]) std::swap(vi[0], vi[1]); + if (vj[0] > vj[1]) std::swap(vj[0], vj[1]); + return vi < vj; + }); + }); + return edge_ids; +} + +} // namespace lagrange::internal diff --git a/modules/core/include/lagrange/internal/invert_mapping.h b/modules/core/include/lagrange/internal/invert_mapping.h index e171df5b..bc5f9cae 100644 --- a/modules/core/include/lagrange/internal/invert_mapping.h +++ b/modules/core/include/lagrange/internal/invert_mapping.h @@ -16,60 +16,94 @@ #include #include +#include #include namespace lagrange::internal { +/// +/// A simple struct representing the inverse of a 1-to-many mapping. Target element `i` is mapped to +/// source elements with index in the range from `mapping.data[mapping.offsets[i]]` to +/// `mapping.data[mapping.offsets[i+1]]`. +/// +/// @tparam Index Mapping index type. +/// +template +struct InverseMapping +{ + /// A flat array of indices of the source elements. + std::vector data; + + /// An array of `data` offset indices. It is of size `num_target_elements + 1`. + std::vector offsets; +}; + /// /// Compute the target-to-source (i.e. backward) mapping from an input source-to-target (i.e. /// forward) mapping. /// /// @note The input source-to-target mapping may be a 1-to-many mapping, where multiple source -/// elements may be mapped to a single target element. -/// -/// @param[in] num_source_entries The number source entries. -/// @param[in] old2new Source-to-target mapping function. -/// @param[in] num_target_entries The total number of target elements. +/// elements may be mapped to a single target element. If a target element is set to +/// `invalid()`, no backward mapping will be created for that target element. /// -/// @tparam Index The index type. -/// @tparam Function Mapping function type. +/// @param[in] num_source_elements The number of source elements. +/// @param[in] old_to_new Source-to-target mapping function. +/// @param[in] num_target_elements The total number of target elements. If set to +/// `invalid()`, it is automatically calculated from the +/// forward mapping. /// -/// @return The target-to-source mapping, which is a tuple consists of 2 index arrays, -/// mapping_data and mapping_offsets. +/// @tparam Index The index type. +/// @tparam Function Mapping function type. /// -/// `mapping_data` is a flat array of indices of the source elements. `mapping_offsets` is an array -/// of `mapping_data` offset indices. It is of size `num_target_entires + 1`. Target element `i` is -/// mapped to source elements with index in the range from `mapping_data[mapping_offsets[i]]` to -/// `mapping_data[mapping_offsets[i+1]]`. +/// @return A struct representing the target-to-source mapping. /// template -auto invert_mapping(Index num_source_entries, Function old2new, Index num_target_entries) - -> std::tuple, std::vector> +InverseMapping invert_mapping( + Index num_source_elements, + Function old_to_new, + Index num_target_elements = invalid()) { - std::vector mapping_offsets(num_target_entries + 1, 0); - std::vector mapping_data; + const bool has_target_count = num_target_elements != invalid(); + InverseMapping mapping; + mapping.offsets.assign(has_target_count ? num_target_elements + 1 : num_source_elements + 1, 0); - for (Index i = 0; i < num_source_entries; ++i) { - Index j = old2new(i); - if (j == invalid()) continue; + for (Index i = 0; i < num_source_elements; ++i) { + Index j = old_to_new(i); + if (j == invalid()) { + continue; + } la_runtime_assert( - j < num_target_entries, - "Mapped element index cannot exceeds target number of elements!"); - ++mapping_offsets[j + 1]; + j < static_cast(mapping.offsets.size()), + fmt::format( + "Mapped element index cannot exceeds {} number of elements!", + has_target_count ? "target" : "source")); + ++mapping.offsets[j + 1]; } - std::partial_sum(mapping_offsets.begin(), mapping_offsets.end(), mapping_offsets.begin()); - mapping_data.resize(mapping_offsets.back()); - for (Index i = 0; i < num_source_entries; i++) { - Index j = old2new(i); - if (j == invalid()) continue; - mapping_data[mapping_offsets[j]++] = i; + if (!has_target_count) { + // If the number of target elements is not provided, we need to resize our offset array now + num_target_elements = num_source_elements; + while (num_target_elements != 0 && mapping.offsets[num_target_elements] == 0) { + --num_target_elements; + } + mapping.offsets.resize(num_target_elements + 1); } - std::rotate(mapping_offsets.begin(), std::prev(mapping_offsets.end()), mapping_offsets.end()); - mapping_offsets[0] = 0; + std::partial_sum(mapping.offsets.begin(), mapping.offsets.end(), mapping.offsets.begin()); + la_debug_assert(mapping.offsets.back() <= num_source_elements); + mapping.data.resize(mapping.offsets.back()); + for (Index i = 0; i < num_source_elements; i++) { + Index j = old_to_new(i); + if (j == invalid()) { + continue; + } + mapping.data[mapping.offsets[j]++] = i; + } - return {std::move(mapping_data), std::move(mapping_offsets)}; + std::rotate(mapping.offsets.begin(), std::prev(mapping.offsets.end()), mapping.offsets.end()); + mapping.offsets[0] = 0; + + return mapping; } /// @@ -77,32 +111,30 @@ auto invert_mapping(Index num_source_entries, Function old2new, Index num_target /// forward) mapping. /// /// @note The input source-to-target mapping may be a 1-to-many mapping, where multiple source -/// elements may be mapped to a single target element. -/// -/// @param[in] old2new Source-to-target mapping. -/// @param[in] num_target_entries The total number of target elements. +/// elements may be mapped to a single target element. If a target element is set to +/// `invalid()`, no backward mapping will be created for that target element. /// -/// @tparam Index The index type. +/// @param[in] old_to_new Source-to-target mapping. +/// @param[in] num_target_elements The total number of target elements. If set to +/// `invalid()`, it is automatically calculated from the +/// forward mapping. /// -/// @return The target-to-source mapping, which is a tuple consists of 2 index arrays, -/// mapping_data and mapping_offsets. +/// @tparam Index The index type. /// -/// `mapping_data` is a flat array of indices of the source elements. `mapping_offsets` is an array -/// of `mapping_data` offset indices. It is of size `num_target_entires + 1`. Target element `i` is -/// mapped to source elements with index in the range from `mapping_data[mapping_offsets[i]]` to -/// `mapping_data[mapping_offsets[i+1]]`. +/// @return A struct representing the target-to-source mapping. /// /// @overload /// template -auto invert_mapping(span old2new, Index num_target_entries) - -> std::tuple, std::vector> +InverseMapping invert_mapping( + span old_to_new, + Index num_target_elements = invalid()) { - Index num_source_entries = static_cast(old2new.size()); + Index num_source_elements = static_cast(old_to_new.size()); return invert_mapping( - num_source_entries, - [&](Index i) { return old2new[i]; }, - num_target_entries); + num_source_elements, + [&](Index i) { return old_to_new[i]; }, + num_target_elements); } } // namespace lagrange::internal diff --git a/modules/core/include/lagrange/internal/visit_attribute.h b/modules/core/include/lagrange/internal/visit_attribute.h index 6efa3e5d..24631630 100644 --- a/modules/core/include/lagrange/internal/visit_attribute.h +++ b/modules/core/include/lagrange/internal/visit_attribute.h @@ -20,7 +20,7 @@ namespace lagrange::internal { /// -/// Apply a function to a mesh attribute. +/// Apply a function to a read-only mesh attribute. /// /// @param[in] mesh Input mesh. /// @param[in] id Attribute id to apply the function to. @@ -30,13 +30,13 @@ namespace lagrange::internal { /// @tparam Scalar Mesh scalar type. /// @tparam Index Mesh index type. /// -/// @note To make this a public API function, we probably need (1) a _read a _write variant, -/// (2) a name vs id variant, and (3) maybe a variant for indexed vs non-indexed to -/// avoid having to constexpr our way through all possibilities. Or maybe we just make -/// the function take an `AttributeBase &` as input? +/// @note To make this a public API function, we probably need (1) a name vs id variant, and +/// (2) maybe a variant for indexed vs non-indexed to avoid having to constexpr our way +/// through all possibilities. Or maybe we just make the function take an `AttributeBase +/// &` as input? /// template -void visit_attribute(const SurfaceMesh& mesh, AttributeId id, Func&& func) +void visit_attribute_read(const SurfaceMesh& mesh, AttributeId id, Func&& func) { const auto& attr = mesh.get_attribute_base(id); auto type = attr.get_value_type(); @@ -56,4 +56,41 @@ void visit_attribute(const SurfaceMesh& mesh, AttributeId id, Fun } } +/// +/// Apply a function to a writeable mesh attribute. +/// +/// @param[in] mesh Input mesh. +/// @param[in] id Attribute id to apply the function to. +/// @param func Function to apply. +/// +/// @tparam Func Function type. +/// @tparam Scalar Mesh scalar type. +/// @tparam Index Mesh index type. +/// +/// @note To make this a public API function, we probably need (1) a name vs id variant, and +/// (2) maybe a variant for indexed vs non-indexed to avoid having to constexpr our way +/// through all possibilities. Or maybe we just make the function take an `AttributeBase +/// &` as input? +/// +template +void visit_attribute_write(SurfaceMesh& mesh, AttributeId id, Func&& func) +{ + const auto& attr = mesh.get_attribute_base(id); + auto type = attr.get_value_type(); + bool is_indexed = (attr.get_element_type() == AttributeElement::Indexed); + switch (type) { +#define LA_X_visit(_, ValueType) \ + case make_attribute_value_type(): { \ + if (is_indexed) { \ + func(mesh.template ref_indexed_attribute(id)); \ + } else { \ + func(mesh.template ref_attribute(id)); \ + } \ + break; \ + } + LA_ATTRIBUTE_X(visit, 0) +#undef LA_X_visit + } +} + } // namespace lagrange::internal diff --git a/modules/core/include/lagrange/legacy/select_facets_in_frustum.h b/modules/core/include/lagrange/legacy/select_facets_in_frustum.h new file mode 100644 index 00000000..30213a11 --- /dev/null +++ b/modules/core/include/lagrange/legacy/select_facets_in_frustum.h @@ -0,0 +1,259 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace lagrange { +LAGRANGE_LEGACY_INLINE +namespace legacy { + +/** + * Select all facets that intersect the cone/frustrum bounded by 4 planes + * defined by (n_i, p_i), where n_i is the plane normal and p_i is a point on + * the plane. + * + * When `greedy` is true, return as soon as the first facet is selected. + * + * When `greedy` is false, check all facets and store the result in a facet + * attribute named `is_selected`. + * + * @param ni normal vector of plane i. + * @param pi a point on plane i. + * @param greedy whether to stop as soon as the first facet is selected. + * @return whether any facet is selected. + */ +template +bool select_facets_in_frustum( + MeshType& mesh, + const Eigen::PlainObjectBase& n0, + const Eigen::PlainObjectBase& p0, + const Eigen::PlainObjectBase& n1, + const Eigen::PlainObjectBase& p1, + const Eigen::PlainObjectBase& n2, + const Eigen::PlainObjectBase& p2, + const Eigen::PlainObjectBase& n3, + const Eigen::PlainObjectBase& p3, + bool greedy = false) +{ + static_assert(MeshTrait::is_mesh(), "Input is not a mesh."); + using AttributeArray = typename MeshType::AttributeArray; + using Scalar = typename MeshType::Scalar; + using Index = typename MeshType::Index; + + const auto num_facets = mesh.get_num_facets(); + const auto& vertices = mesh.get_vertices(); + const auto& facets = mesh.get_facets(); + + // Per-thread buffers to store intermediate results. + struct LocalBuffers + { + Point3D v0, v1, v2; // Triangle vertices. + Point3D q0, q1, q2, q3; // Intermediate tet vertices. + std::array r0, r1, r2; // Triangle projections. + }; + tbb::enumerable_thread_specific temp_vars; // we can safely remove this and just declare these variables in the lambdas (as long as there's no memory allocation on the heap, this is not necessary) + + + auto edge_overlap_with_negative_octant = [](const Point3D& q0, const Point3D& q1) -> bool { + Scalar t_min = 0, t_max = 1; + const Point3D e = q0 - q1; + if (e[0] > 0) { + t_max = std::min(t_max, -q1[0] / e[0]); + } else if (e[0] < 0) { + t_min = std::max(t_min, -q1[0] / e[0]); + } else { + if (q1[0] >= 0) return false; + } + + if (e[1] > 0) { + t_max = std::min(t_max, -q1[1] / e[1]); + } else if (e[1] < 0) { + t_min = std::max(t_min, -q1[1] / e[1]); + } else { + if (q1[1] >= 0) return false; + } + + if (e[2] > 0) { + t_max = std::min(t_max, -q1[2] / e[2]); + } else if (e[2] < 0) { + t_min = std::max(t_min, -q1[2] / e[2]); + } else { + if (q1[2] >= 0) return false; + } + + return t_max >= t_min; + }; + + auto compute_plane = + [](const Point3D& q0, const Point3D& q1, const Point3D& q2) -> std::pair { + const Point3D n = { + q0[2] * (q2[1] - q1[1]) + q1[2] * (q0[1] - q2[1]) + q2[2] * (q1[1] - q0[1]), + q0[0] * (q2[2] - q1[2]) + q1[0] * (q0[2] - q2[2]) + q2[0] * (q1[2] - q0[2]), + q0[1] * (q2[0] - q1[0]) + q1[1] * (q0[0] - q2[0]) + q2[1] * (q1[0] - q0[0])}; + const Scalar c = (n.dot(q0) + n.dot(q1) + n.dot(q2)) / 3; + return {n, c}; + }; + + // Compute the orientation of 2D traingle (v0, v1, O), where O = (0, 0). + auto orient2D_inexact = [](const std::array& v0, + const std::array& v1) -> int { + const auto r = v0[0] * v1[1] - v0[1] * v1[0]; + if (r > 0) return 1; + if (r < 0) return -1; + return 0; + }; + + auto triangle_intersects_negative_axis = [&](const Point3D& q0, + const Point3D& q1, + const Point3D& q2, + const Point3D& n, + const Scalar c, + const int axis) -> bool { + auto& r0 = temp_vars.local().r0; + auto& r1 = temp_vars.local().r1; + auto& r2 = temp_vars.local().r2; + + r0[0] = q0[(axis + 1) % 3]; + r0[1] = q0[(axis + 2) % 3]; + r1[0] = q1[(axis + 1) % 3]; + r1[1] = q1[(axis + 2) % 3]; + r2[0] = q2[(axis + 1) % 3]; + r2[1] = q2[(axis + 2) % 3]; + + auto o01 = orient2D_inexact(r0, r1); + auto o12 = orient2D_inexact(r1, r2); + auto o20 = orient2D_inexact(r2, r0); + if (o01 == o12 && o01 == o20) { + if (o01 == 0) { + // Triangle projection is degenerate. + // Note that the case where axis is coplanar with the triangle + // is treated as no intersection (which is debatale). + return false; + } else { + // Triangle projection contains the origin. + // Check axis intercept. + return (c < 0 && n[axis] > 0) || (c > 0 && n[axis] < 0); + } + } else { + // Note that we treat the case where the axis intersect triangle at + // its boundary as no intersection. + return false; + } + }; + + auto triangle_intersects_negative_axes = + [&](const Point3D& q0, const Point3D& q1, const Point3D& q2) -> bool { + const auto r = compute_plane(q0, q1, q2); + if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 0)) return true; + if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 1)) return true; + if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 2)) return true; + return false; + }; + + auto tet_overlap_with_negative_octant = + [&](const Point3D& q0, const Point3D& q1, const Point3D& q2, const Point3D& q3) -> bool { + // Check 1: Check if any tet vertices is in negative octant. + if ((q0.array() < 0).all()) return true; + if ((q1.array() < 0).all()) return true; + if ((q2.array() < 0).all()) return true; + if ((q3.array() < 0).all()) return true; + + // Check 2: Check if any tet edges cross the negative octant. + if (edge_overlap_with_negative_octant(q0, q1)) return true; + if (edge_overlap_with_negative_octant(q0, q2)) return true; + if (edge_overlap_with_negative_octant(q0, q3)) return true; + if (edge_overlap_with_negative_octant(q1, q2)) return true; + if (edge_overlap_with_negative_octant(q1, q3)) return true; + if (edge_overlap_with_negative_octant(q2, q3)) return true; + + // Check 3: Check if -X, -Y or -Z axis intersect the tet. + if (triangle_intersects_negative_axes(q0, q1, q2)) return true; + if (triangle_intersects_negative_axes(q1, q2, q3)) return true; + if (triangle_intersects_negative_axes(q2, q3, q0)) return true; + if (triangle_intersects_negative_axes(q3, q0, q1)) return true; + + // All check failed iff tet does not intersect the negative octant. + return false; + }; + + AttributeArray attr; + if (!greedy) { + attr.resize(num_facets, 1); + attr.setZero(); + } + std::atomic_bool r{false}; + + tbb::parallel_for( + tbb::blocked_range(0, num_facets), + [&](const tbb::blocked_range& tbb_range) { + for (auto fi = tbb_range.begin(); fi != tbb_range.end(); fi++) { + if (tbb_utils::is_cancelled()) break; + + // Triangle (v0, v1, v2) intersect the cone defined by 4 planes + // iff the tetrahedron (q0, q1, q2, q3) does not intersect the negative octant. + // This can be proved by Farkas' lemma. + + auto& v0 = temp_vars.local().v0; + auto& v1 = temp_vars.local().v1; + auto& v2 = temp_vars.local().v2; + + auto& q0 = temp_vars.local().q0; + auto& q1 = temp_vars.local().q1; + auto& q2 = temp_vars.local().q2; + auto& q3 = temp_vars.local().q3; + + v0 = vertices.row(facets(fi, 0)); + v1 = vertices.row(facets(fi, 1)); + v2 = vertices.row(facets(fi, 2)); + + q0 = {(v0 - p0).dot(n0), (v1 - p0).dot(n0), (v2 - p0).dot(n0)}; + q1 = {(v0 - p1).dot(n1), (v1 - p1).dot(n1), (v2 - p1).dot(n1)}; + q2 = {(v0 - p2).dot(n2), (v1 - p2).dot(n2), (v2 - p2).dot(n2)}; + q3 = {(v0 - p3).dot(n3), (v1 - p3).dot(n3), (v2 - p3).dot(n3)}; + + const auto ri = !tet_overlap_with_negative_octant(q0, q1, q2, q3); + + if (ri) r = true; + + if (!greedy) { + attr(fi, 0) = ri; + } + + if (greedy && ri) { + tbb_utils::cancel_group_execution(); + break; + } + } + }); + + if (!greedy) { + + mesh.add_facet_attribute("is_selected"); + mesh.import_facet_attribute("is_selected", attr); + } + return r.load(); +} + +} // namespace legacy +} // namespace lagrange diff --git a/modules/core/include/lagrange/map_attribute.h b/modules/core/include/lagrange/map_attribute.h index a7f0b88c..19e7d522 100644 --- a/modules/core/include/lagrange/map_attribute.h +++ b/modules/core/include/lagrange/map_attribute.h @@ -87,6 +87,9 @@ AttributeId map_attribute( /// a value attribute, its number of rows must match the number of target mesh element (or number of /// corners if the target is an indexed attribute). /// +/// @todo To be truly in-place ideally the new AttributeId should be the same as the old +/// one. +/// /// @param[in,out] mesh Input mesh. Modified to add a new attribute. /// @param[in] id Id of the input attribute to map. /// @param[in] new_element New attribute element type. @@ -108,6 +111,9 @@ AttributeId map_attribute_in_place( /// a value attribute, its number of rows must match the number of target mesh element (or number of /// corners if the target is an indexed attribute). /// +/// @todo To be truly in-place ideally the new AttributeId should be the same as the old +/// one. +/// /// @param[in,out] mesh Input mesh. Modified to add a new attribute. /// @param[in] name Name of the attribute to map. /// @param[in] new_element New attribute element type. diff --git a/modules/core/include/lagrange/mesh_convert.impl.h b/modules/core/include/lagrange/mesh_convert.impl.h index a67064e2..41240d1d 100644 --- a/modules/core/include/lagrange/mesh_convert.impl.h +++ b/modules/core/include/lagrange/mesh_convert.impl.h @@ -23,8 +23,8 @@ #include #include #include +#include -#include #include namespace lagrange { @@ -264,79 +264,6 @@ SurfaceMesh to_surface_mesh_wrap(MeshType&& mesh) std::forward(mesh)); } -namespace mesh_convert_detail { - -// -// TODO: -// 1. Make this function standalone -// 2. Reuse it to implement a faster version of SurfaceMesh::update_edges_range_internal() -// 3. Profile and benchmark timings -// 4. Maybe we can implement a local cache system for SurfaceMesh<> to reuse tmp buffers. Maybe this -// would make the add_vertex/add_facet functions efficient enough that we do not need to allocate -// them all at once in our `triangulate_polygonal_facets` function. -// -template -std::vector fast_edge_sort( - Index num_edges, - Index num_vertices, - Func get_edge, - span vertex_to_first_edge = {}) -{ - std::vector local_buffer; - if (vertex_to_first_edge.empty()) { - local_buffer.assign(num_vertices + 1, 0); - vertex_to_first_edge = local_buffer; - } else { - std::fill(vertex_to_first_edge.begin(), vertex_to_first_edge.end(), Index(0)); - } - la_runtime_assert(vertex_to_first_edge.size() == static_cast(num_vertices) + 1); - // Count number of edges starting at each vertex - for (Index e = 0; e < num_edges; ++e) { - std::array v = get_edge(e); - if (v[0] > v[1]) { - std::swap(v[0], v[1]); - } - vertex_to_first_edge[v[0] + 1]++; - } - // Prefix sum to compute actual offsets - std::partial_sum( - vertex_to_first_edge.begin(), - vertex_to_first_edge.end(), - vertex_to_first_edge.begin()); - la_runtime_assert(vertex_to_first_edge.back() == num_edges); - // Bucket each edge id to its respective starting vertex - std::vector edge_ids(num_edges); - for (Index e = 0; e < num_edges; ++e) { - std::array v = get_edge(e); - if (v[0] > v[1]) { - std::swap(v[0], v[1]); - } - edge_ids[vertex_to_first_edge[v[0]]++] = e; - } - // Shift back the offset buffer 'vertex_to_first_edge' (can use std::shift_right in C++20 :p) - std::rotate( - vertex_to_first_edge.rbegin(), - vertex_to_first_edge.rbegin() + 1, - vertex_to_first_edge.rend()); - vertex_to_first_edge.front() = 0; - // Sort each bucket in parallel - tbb::parallel_for(Index(0), num_vertices, [&](Index v) { - tbb::parallel_sort( - edge_ids.begin() + vertex_to_first_edge[v], - edge_ids.begin() + vertex_to_first_edge[v + 1], - [&](Index ei, Index ej) { - auto vi = get_edge(ei); - auto vj = get_edge(ej); - if (vi[0] > vi[1]) std::swap(vi[0], vi[1]); - if (vj[0] > vj[1]) std::swap(vj[0], vj[1]); - return vi < vj; - }); - }); - return edge_ids; -} - -} // namespace mesh_convert_detail - template std::unique_ptr to_legacy_mesh(const SurfaceMesh& mesh) { @@ -387,12 +314,12 @@ std::unique_ptr to_legacy_mesh(const SurfaceMesh& mesh) const auto num_vertices = static_cast(mesh.get_num_vertices()); auto buffer = std::make_unique( (num_vertices + 1) * std::max(sizeof(Index), sizeof(MeshIndex))); - old_edge_ids = mesh_convert_detail::fast_edge_sort( + old_edge_ids = internal::fast_edge_sort( mesh.get_num_edges(), mesh.get_num_vertices(), [&](Index e) -> std::array { return mesh.get_edge_vertices(e); }, {reinterpret_cast(buffer.get()), num_vertices + 1}); - new_edge_ids = mesh_convert_detail::fast_edge_sort( + new_edge_ids = internal::fast_edge_sort( new_mesh->get_num_edges(), new_mesh->get_num_vertices(), [&](MeshIndex e) -> std::array { return new_mesh->get_edge_vertices(e); }, diff --git a/modules/core/include/lagrange/select_facets_in_frustum.h b/modules/core/include/lagrange/select_facets_in_frustum.h index 789ac1db..28580b2d 100644 --- a/modules/core/include/lagrange/select_facets_in_frustum.h +++ b/modules/core/include/lagrange/select_facets_in_frustum.h @@ -11,244 +11,81 @@ */ #pragma once +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif + +#include #include -#include -#include -#include -#include +namespace lagrange { -#include -#include -#include -#include +/// +/// @defgroup group-surfacemesh-utils Mesh utilities +/// @ingroup group-surfacemesh +/// +/// Various mesh processing utilities +/// +/// @{ + +/// +/// An array of four planes that define a frustum. +/// +template +struct Frustum +{ + /// A plane defined by a normal and a point. + struct Plane + { + std::array normal; + std::array point; + }; -namespace lagrange { + /// Four planes that define a frustum. + std::array planes; +}; + +/// +/// Option struct for selecting facets. +/// +struct FrustumSelectionOptions +{ + /// If true, then select_facets_in_frustum will stop after it finds the first facet. + bool greedy = false; + + /// The output attribute name for the selection. + /// This attribute will be of type uint8_t and is only created/updated if greedy is false. + std::string_view output_attribute_name = "@is_selected"; +}; /** * Select all facets that intersect the cone/frustrum bounded by 4 planes * defined by (n_i, p_i), where n_i is the plane normal and p_i is a point on * the plane. * - * When `greedy` is true, return as soon as the first facet is selected. + * @param[in, out] mesh The input mesh. + * @param[in] frustum A collection of four planes. + * @param[in] options Optional arguments (greedy, output_attribute_name). + * + * @tparam Scalar Mesh scalar type. + * @tparam Index Mesh index type. * - * When `greedy` is false, check all facets and store the result in a facet - * attribute named `is_selected`. + * @return bool Whether any facet is selected. * - * @param ni normal vector of plane i. - * @param pi a point on plane i. - * @param greedy whether to stop as soon as the first facet is selected. - * @return whether any facet is selected. + * @note When `options.greedy` is true, this function returns as soon as the first facet + * is selected. + * + * @post If `options.greedy` is false, the computed selection is stored in `mesh` as a + * facet attribute named `options.output_attribute_name`. + * + * @see Frustum and FrustumSelectionOptions. */ -template +template bool select_facets_in_frustum( - MeshType& mesh, - const Eigen::PlainObjectBase& n0, - const Eigen::PlainObjectBase& p0, - const Eigen::PlainObjectBase& n1, - const Eigen::PlainObjectBase& p1, - const Eigen::PlainObjectBase& n2, - const Eigen::PlainObjectBase& p2, - const Eigen::PlainObjectBase& n3, - const Eigen::PlainObjectBase& p3, - bool greedy = false) -{ - static_assert(MeshTrait::is_mesh(), "Input is not a mesh."); - using AttributeArray = typename MeshType::AttributeArray; - using Scalar = typename MeshType::Scalar; - using Index = typename MeshType::Index; - - const auto num_facets = mesh.get_num_facets(); - const auto& vertices = mesh.get_vertices(); - const auto& facets = mesh.get_facets(); - - // Per-thread buffers to store intermediate results. - struct LocalBuffers - { - Point3D v0, v1, v2; // Triangle vertices. - Point3D q0, q1, q2, q3; // Intermediate tet vertices. - std::array r0, r1, r2; // Triangle projections. - }; - tbb::enumerable_thread_specific temp_vars; - - auto edge_overlap_with_negative_octant = [](const Point3D& q0, const Point3D& q1) -> bool { - Scalar t_min = 0, t_max = 1; - const Point3D e = q0 - q1; - if (e[0] > 0) { - t_max = std::min(t_max, -q1[0] / e[0]); - } else if (e[0] < 0) { - t_min = std::max(t_min, -q1[0] / e[0]); - } else { - if (q1[0] >= 0) return false; - } - - if (e[1] > 0) { - t_max = std::min(t_max, -q1[1] / e[1]); - } else if (e[1] < 0) { - t_min = std::max(t_min, -q1[1] / e[1]); - } else { - if (q1[1] >= 0) return false; - } - - if (e[2] > 0) { - t_max = std::min(t_max, -q1[2] / e[2]); - } else if (e[2] < 0) { - t_min = std::max(t_min, -q1[2] / e[2]); - } else { - if (q1[2] >= 0) return false; - } - - return t_max >= t_min; - }; - - auto compute_plane = - [](const Point3D& q0, const Point3D& q1, const Point3D& q2) -> std::pair { - const Point3D n = { - q0[2] * (q2[1] - q1[1]) + q1[2] * (q0[1] - q2[1]) + q2[2] * (q1[1] - q0[1]), - q0[0] * (q2[2] - q1[2]) + q1[0] * (q0[2] - q2[2]) + q2[0] * (q1[2] - q0[2]), - q0[1] * (q2[0] - q1[0]) + q1[1] * (q0[0] - q2[0]) + q2[1] * (q1[0] - q0[0])}; - const Scalar c = (n.dot(q0) + n.dot(q1) + n.dot(q2)) / 3; - return {n, c}; - }; - - // Compute the orientation of 2D traingle (v0, v1, O), where O = (0, 0). - auto orient2D_inexact = [](const std::array& v0, - const std::array& v1) -> int { - const auto r = v0[0] * v1[1] - v0[1] * v1[0]; - if (r > 0) return 1; - if (r < 0) return -1; - return 0; - }; - - auto triangle_intersects_negative_axis = [&](const Point3D& q0, - const Point3D& q1, - const Point3D& q2, - const Point3D& n, - const Scalar c, - const int axis) -> bool { - auto& r0 = temp_vars.local().r0; - auto& r1 = temp_vars.local().r1; - auto& r2 = temp_vars.local().r2; - - r0[0] = q0[(axis + 1) % 3]; - r0[1] = q0[(axis + 2) % 3]; - r1[0] = q1[(axis + 1) % 3]; - r1[1] = q1[(axis + 2) % 3]; - r2[0] = q2[(axis + 1) % 3]; - r2[1] = q2[(axis + 2) % 3]; - - auto o01 = orient2D_inexact(r0, r1); - auto o12 = orient2D_inexact(r1, r2); - auto o20 = orient2D_inexact(r2, r0); - if (o01 == o12 && o01 == o20) { - if (o01 == 0) { - // Triangle projection is degenerate. - // Note that the case where axis is coplanar with the triangle - // is treated as no intersection (which is debatale). - return false; - } else { - // Triangle projection contains the origin. - // Check axis intercept. - return (c < 0 && n[axis] > 0) || (c > 0 && n[axis] < 0); - } - } else { - // Note that we treat the case where the axis intersect triangle at - // its boundary as no intersection. - return false; - } - }; - - auto triangle_intersects_negative_axes = - [&](const Point3D& q0, const Point3D& q1, const Point3D& q2) -> bool { - const auto r = compute_plane(q0, q1, q2); - if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 0)) return true; - if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 1)) return true; - if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 2)) return true; - return false; - }; - - auto tet_overlap_with_negative_octant = - [&](const Point3D& q0, const Point3D& q1, const Point3D& q2, const Point3D& q3) -> bool { - // Check 1: Check if any tet vertices is in negative octant. - if ((q0.array() < 0).all()) return true; - if ((q1.array() < 0).all()) return true; - if ((q2.array() < 0).all()) return true; - if ((q3.array() < 0).all()) return true; - - // Check 2: Check if any tet edges cross the negative octant. - if (edge_overlap_with_negative_octant(q0, q1)) return true; - if (edge_overlap_with_negative_octant(q0, q2)) return true; - if (edge_overlap_with_negative_octant(q0, q3)) return true; - if (edge_overlap_with_negative_octant(q1, q2)) return true; - if (edge_overlap_with_negative_octant(q1, q3)) return true; - if (edge_overlap_with_negative_octant(q2, q3)) return true; - - // Check 3: Check if -X, -Y or -Z axis intersect the tet. - if (triangle_intersects_negative_axes(q0, q1, q2)) return true; - if (triangle_intersects_negative_axes(q1, q2, q3)) return true; - if (triangle_intersects_negative_axes(q2, q3, q0)) return true; - if (triangle_intersects_negative_axes(q3, q0, q1)) return true; - - // All check failed iff tet does not intersect the negative octant. - return false; - }; - - AttributeArray attr; - if (!greedy) { - attr.resize(num_facets, 1); - attr.setZero(); - } - std::atomic_bool r{false}; - - tbb::parallel_for( - tbb::blocked_range(0, num_facets), - [&](const tbb::blocked_range& tbb_range) { - for (auto fi = tbb_range.begin(); fi != tbb_range.end(); fi++) { - if (tbb_utils::is_cancelled()) break; - - // Triangle (v0, v1, v2) intersect the cone defined by 4 planes - // iff the tetrahedron (q0, q1, q2, q3) does not intersect the negative octant. - // This can be proved by Farcus' lemma. - - auto& v0 = temp_vars.local().v0; - auto& v1 = temp_vars.local().v1; - auto& v2 = temp_vars.local().v2; - - auto& q0 = temp_vars.local().q0; - auto& q1 = temp_vars.local().q1; - auto& q2 = temp_vars.local().q2; - auto& q3 = temp_vars.local().q3; - - v0 = vertices.row(facets(fi, 0)); - v1 = vertices.row(facets(fi, 1)); - v2 = vertices.row(facets(fi, 2)); - - q0 = {(v0 - p0).dot(n0), (v1 - p0).dot(n0), (v2 - p0).dot(n0)}; - q1 = {(v0 - p1).dot(n1), (v1 - p1).dot(n1), (v2 - p1).dot(n1)}; - q2 = {(v0 - p2).dot(n2), (v1 - p2).dot(n2), (v2 - p2).dot(n2)}; - q3 = {(v0 - p3).dot(n3), (v1 - p3).dot(n3), (v2 - p3).dot(n3)}; - - const auto ri = !tet_overlap_with_negative_octant(q0, q1, q2, q3); - - if (ri) r = true; - - if (!greedy) { - attr(fi, 0) = ri; - } - - if (greedy && ri) { - tbb_utils::cancel_group_execution(); - break; - } - } - }); - - if (!greedy) { - mesh.add_facet_attribute("is_selected"); - mesh.import_facet_attribute("is_selected", attr); - } - return r.load(); -} + SurfaceMesh& mesh, + const Frustum& frustum, + const FrustumSelectionOptions& options = {}); +/// @} } // namespace lagrange diff --git a/modules/core/include/lagrange/utils/fmt_eigen.h b/modules/core/include/lagrange/utils/fmt_eigen.h index c3c62ca6..f70ee790 100644 --- a/modules/core/include/lagrange/utils/fmt_eigen.h +++ b/modules/core/include/lagrange/utils/fmt_eigen.h @@ -12,47 +12,152 @@ #pragma once +// Dealing with fmt-Eigen shenanigans. Ostream support was deprecated in fmt v9.x, and removed from +// the library in fmt v10.x [1]. Supposedly this was causing ODR violations [2] and was a source of +// headaches. The consequence of this is that formatting Eigen objects is broken with fmt >= v9.x. +// Neither the fmt author [3] nor the Eigen maintainers are really interested in shipping & +// supporting a fmt::formatter<> for Eigen objects. We provide one here as a workaround until a +// better solution comes along. We also provide an option to override this header with your own +// hook in case this conflicts with another fmt::formatter<> somewhere else in your codebase. +// +// [1]: https://github.com/fmtlib/fmt/issues/3318 +// [2]: https://github.com/fmtlib/fmt/issues/2357 +// [3]: https://github.com/fmtlib/fmt/issues/3465 + // clang-format off #include #include +#include +#include #include // clang-format on #include -#if defined(LAGRANGE_FMT_EIGEN_FIX) && defined(_MSC_VER) -// MSVC crashes occasionally when using fmt with Eigen. e.g. -// ``` -// Eigen::Matrix3f m; -// logger().info("{}", m); // C1001: internal compiler error -// ``` -// This manifests as a C1001 internal compiler error, make it impossible to -// debug. This workaround is a temporary fix until MSCV fixes the bug. -// -// MSVC bug report: https://developercommunity.visualstudio.com/t/10376323 -// Version affected: 17.6, 17.7 -// Bug is fixed in version 17.8 +#ifdef LA_FMT_EIGEN_FORMATTER + + // User-provided fmt::formatter<> for Eigen types. + #include LA_FMT_EIGEN_FORMATTER + +#elif defined(SPDLOG_USE_STD_FORMAT) + +// It's still a bit early for C++20 format support... + +#else + + // spdlog with fmt (either bundled or external, doesn't matter at this point) + #if FMT_VERSION >= 100200 + + // Use the new nested formatter with fmt >= 10.2.0. + // This support nested Eigen types as well as padding/format specifiers. + #include + +template +struct fmt::formatter, T>::value, char>> + : fmt::nested_formatter +{ + auto format(T const& a, format_context& ctx) const + { + return this->write_padded(ctx, [&](auto out) { + for (Eigen::Index ir = 0; ir < a.rows(); ir++) { + for (Eigen::Index ic = 0; ic < a.cols(); ic++) { + out = fmt::format_to(out, "{} ", this->nested(a(ir, ic))); + } + out = fmt::format_to(out, "\n"); + } + return out; + }); + } +}; + +template +struct fmt::is_range< + Derived, + std::enable_if_t, Derived>::value, char>> + : std::false_type +{ +}; + + #elif (FMT_VERSION >= 100000) || (FMT_VERSION >= 90000 && !defined(FMT_DEPRECATED_OSTREAM)) || \ + (defined(LAGRANGE_FMT_EIGEN_FIX) && defined(_MSC_VER)) + + // fmt >= 10.x or fmt 9.x without deprecated ostream support. + // + // We also uses a fmt::formatter<> with fmt v9.x and certain versions of MSVC to workaround + // a compiler bug, triggered by code like this: + // ``` + // Eigen::Matrix3f m; + // logger().info("{}", m); // C1001: internal compiler error + // ``` + // This manifests as a C1001 internal compiler error, make it impossible to + // debug. + // + // MSVC bug report: https://developercommunity.visualstudio.com/t/10376323 + // Version affected: 17.6, 17.7 + // Bug is fixed in version 17.8 + // + #include -#include -#include template -struct fmt::formatter, Derived>::value, char>> { - template - constexpr auto parse(ParseContext& ctx) { return ctx.begin(); } - - template - auto format(const Eigen::DenseBase& m, FormatContext& ctx) { - std::stringstream ss; - ss << m; - return fmt::format_to(ctx.out(), "{}", ss.str()); + template + constexpr auto parse(ParseContext& ctx) + { + return m_underlying.parse(ctx); } + + template + auto format(const Derived& mat, FormatContext& ctx) const + { + auto out = ctx.out(); + + for (Eigen::Index row = 0; row < mat.rows(); ++row) { + for (Eigen::Index col = 0; col < mat.cols(); ++col) { + out = fmt::format_to(out, " "); + out = m_underlying.format(mat.coeff(row, col), ctx); + } + + if (row < mat.rows() - 1) { + out = fmt::format_to(out, "\n"); + } + } + + return out; + } + +private: + fmt::formatter m_underlying; }; -#else -// clang-format off -#include -#include -#include -// clang-format on + +template +struct fmt::is_range< + Derived, + std::enable_if_t, Derived>::value, char>> + : std::false_type +{ +}; + + #else + + // Include legacy ostr support + + // clang-format off + #include + #include + #include + // clang-format on + +template +struct fmt::is_range< + Derived, + std::enable_if_t, Derived>::value, char>> + : std::false_type +{ +}; + + #endif + #endif diff --git a/modules/core/include/lagrange/utils/strings.h b/modules/core/include/lagrange/utils/strings.h index a0e571b5..d49284c9 100644 --- a/modules/core/include/lagrange/utils/strings.h +++ b/modules/core/include/lagrange/utils/strings.h @@ -24,7 +24,6 @@ #include #include - namespace lagrange { /// @defgroup group-utils-misc Miscellaneous @@ -103,7 +102,12 @@ LA_CORE_API std::string to_upper(std::string str); template std::string string_format(fmt::format_string format, Args&&... args) { + // TODO: Remove this string_format in our next major release... + #if FMT_VERSION >= 90100 + return fmt::format(fmt::runtime(format), std::forward(args)...); + #else return fmt::format(format, std::forward(args)...); + #endif } /// @} diff --git a/modules/core/include/lagrange/utils/warnoff.h b/modules/core/include/lagrange/utils/warnoff.h index 84525544..18256997 100644 --- a/modules/core/include/lagrange/utils/warnoff.h +++ b/modules/core/include/lagrange/utils/warnoff.h @@ -44,6 +44,7 @@ #pragma clang diagnostic ignored "-Wunused-private-field" #pragma clang diagnostic ignored "-Wmissing-field-initializers" #pragma clang diagnostic ignored "-Wconversion" + #pragma clang diagnostic ignored "-Wunused-function" #elif defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-function-type" @@ -73,6 +74,7 @@ #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wunused-result" #pragma GCC diagnostic ignored "-Wunused-variable" + #pragma GCC diagnostic ignored "-Wunused-function" #elif defined(_MSC_VER) #pragma warning(push) #pragma warning(disable : 26439) // This kind of function may not throw. Declare it 'noexcept' diff --git a/modules/core/python/scripts/meshstat.py b/modules/core/python/scripts/meshstat.py index d122a41f..ccdb267a 100755 --- a/modules/core/python/scripts/meshstat.py +++ b/modules/core/python/scripts/meshstat.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -""" Print basic information about a mesh file """ +"""Print basic information about a mesh file""" import argparse import lagrange @@ -69,7 +69,7 @@ def print_basic_info(mesh, info): elif mesh.vertex_per_facet == 4: print("facet type: quads") else: - print(f"facet type: polygons (mesh.vertex_per_facet)") + print(f"facet type: polygons ({mesh.vertex_per_facet})") info["is_regular"] = True info["vertex_per_facet"] = mesh.vertex_per_facet diff --git a/modules/core/python/tests/test_attribute.py b/modules/core/python/tests/test_attribute.py index 2917a928..ab0cc785 100644 --- a/modules/core/python/tests/test_attribute.py +++ b/modules/core/python/tests/test_attribute.py @@ -30,7 +30,7 @@ def test_basics(self, single_triangle_with_index): data = attr.data assert not data.flags["OWNDATA"] - #assert not data.flags["WRITEABLE"] + # assert not data.flags["WRITEABLE"] assert len(data.shape) == 1 assert data.shape[0] == 3 @@ -101,7 +101,7 @@ def test_implicit_conversion(self, single_triangle_with_index): mesh = single_triangle_with_index attr = mesh.attribute("vertex_index") - attr.data = [1, 2 ,3] + attr.data = [1, 2, 3] assert np.array_equal(attr.data, [1, 2, 3]) assert not attr.external diff --git a/modules/core/python/tests/test_cast_attribute.py b/modules/core/python/tests/test_cast_attribute.py index 4e57c29c..381d3cda 100644 --- a/modules/core/python/tests/test_cast_attribute.py +++ b/modules/core/python/tests/test_cast_attribute.py @@ -36,10 +36,12 @@ def test_simple(self, single_triangle): assert mesh.attribute("test").dtype == np.uint8 # Cast to another attribute with type int32 - attr_id = lagrange.cast_attribute(mesh, "test", np.int32, output_attribute_name="test2") + attr_id = lagrange.cast_attribute( + mesh, "test", np.int32, output_attribute_name="test2" + ) assert mesh.get_attribute_name(attr_id) == "test2" assert mesh.attribute("test2").dtype == np.int32 - assert mesh.attribute("test").dtype == np.uint8 # Same as before + assert mesh.attribute("test").dtype == np.uint8 # Same as before # Cast to python float attr_id = lagrange.cast_attribute(mesh, "test", float) diff --git a/modules/core/python/tests/test_compute_centroid.py b/modules/core/python/tests/test_compute_centroid.py index af2b0e0d..0221999b 100644 --- a/modules/core/python/tests/test_compute_centroid.py +++ b/modules/core/python/tests/test_compute_centroid.py @@ -18,6 +18,7 @@ from .assets import single_triangle, cube + class TestComputeCentroid: def test_cube(self, cube): mesh = cube @@ -27,7 +28,7 @@ def test_cube(self, cube): def test_triangle(self, single_triangle): mesh = single_triangle c = lagrange.compute_mesh_centroid(mesh) - assert np.all(c == [1/3, 1/3, 1/3]) + assert np.all(c == [1 / 3, 1 / 3, 1 / 3]) def test_empty_mesh(self): mesh = lagrange.SurfaceMesh() diff --git a/modules/core/python/tests/test_compute_seam_edges.py b/modules/core/python/tests/test_compute_seam_edges.py index 061d81eb..e695a2fb 100644 --- a/modules/core/python/tests/test_compute_seam_edges.py +++ b/modules/core/python/tests/test_compute_seam_edges.py @@ -16,6 +16,7 @@ from .assets import cube_with_uv, cube + class TestComputeCentroid: def test_cube(self, cube_with_uv): mesh = cube_with_uv diff --git a/modules/core/python/tests/test_compute_vertex_normal.py b/modules/core/python/tests/test_compute_vertex_normal.py index c0b3649f..876ac118 100644 --- a/modules/core/python/tests/test_compute_vertex_normal.py +++ b/modules/core/python/tests/test_compute_vertex_normal.py @@ -17,6 +17,7 @@ from .assets import cube + class TestComputeVertexNormal: def test_cube(self, cube): mesh = cube diff --git a/modules/core/python/tests/test_indexed_attribute.py b/modules/core/python/tests/test_indexed_attribute.py index 4f010732..d69fcaef 100644 --- a/modules/core/python/tests/test_indexed_attribute.py +++ b/modules/core/python/tests/test_indexed_attribute.py @@ -42,7 +42,5 @@ def test_attribute_basics(self, cube): assert attr_values.num_channels == 1 assert not attr_indices.external - assert ( - attr_indices.element_type == lagrange.AttributeElement.Corner - ) + assert attr_indices.element_type == lagrange.AttributeElement.Corner assert attr_indices.num_channels == 1 diff --git a/modules/core/python/tests/test_permute_vertices.py b/modules/core/python/tests/test_permute_vertices.py index 33daec90..62956f4d 100644 --- a/modules/core/python/tests/test_permute_vertices.py +++ b/modules/core/python/tests/test_permute_vertices.py @@ -65,7 +65,9 @@ def test_with_uv(self, cube_with_uv): # Corner index should be unchnaged. corner_index_attr = mesh.attribute("corner_index") - assert np.all(corner_index_attr.data == np.arange(mesh.num_corners, dtype=np.intc)) + assert np.all( + corner_index_attr.data == np.arange(mesh.num_corners, dtype=np.intc) + ) for i in range(mesh.num_vertices): ci = mesh.get_first_corner_around_vertex(i) diff --git a/modules/core/python/tests/test_surface_mesh.py b/modules/core/python/tests/test_surface_mesh.py index 3e1506a7..e2bae3b9 100644 --- a/modules/core/python/tests/test_surface_mesh.py +++ b/modules/core/python/tests/test_surface_mesh.py @@ -159,9 +159,7 @@ def test_create_attribute_with_init_values(self, single_triangle): assert attr.dtype == attr.data.dtype # Corner attribute - id = mesh.create_attribute( - "corner_index", initial_values=np.array([1, 2, 3]) - ) + id = mesh.create_attribute("corner_index", initial_values=np.array([1, 2, 3])) attr = mesh.attribute(id) assert attr.element_type == lagrange.AttributeElement.Corner assert attr.usage == lagrange.AttributeUsage.Scalar @@ -169,9 +167,7 @@ def test_create_attribute_with_init_values(self, single_triangle): assert attr.dtype == attr.data.dtype # Facet attribute - id = mesh.create_attribute( - "facet_index", initial_values=[0], num_channels=1 - ) + id = mesh.create_attribute("facet_index", initial_values=[0], num_channels=1) attr = mesh.attribute(id) assert attr.element_type == lagrange.AttributeElement.Facet assert attr.usage == lagrange.AttributeUsage.Scalar @@ -206,9 +202,7 @@ def test_create_attribute_with_init_values(self, single_triangle): def test_edges(self, single_triangle, cube): mesh = single_triangle - mesh.initialize_edges( - np.array([[0, 1], [1, 2], [2, 0]], dtype=np.uint32) - ) + mesh.initialize_edges(np.array([[0, 1], [1, 2], [2, 0]], dtype=np.uint32)) assert mesh.has_edges assert mesh.num_edges == 3 assert mesh.is_boundary_edge(0) diff --git a/modules/core/python/tests/test_unify_index_buffer.py b/modules/core/python/tests/test_unify_index_buffer.py index c4f9a8ff..066c0e21 100644 --- a/modules/core/python/tests/test_unify_index_buffer.py +++ b/modules/core/python/tests/test_unify_index_buffer.py @@ -13,6 +13,7 @@ from .assets import cube + class TestUnifyIndexBuffer: def test_empty_mesh(self): input_mesh = lagrange.SurfaceMesh() @@ -38,4 +39,3 @@ def test_mesh_with_attribute(self, cube): assert output_mesh2.num_vertices != mesh.num_vertices assert output_mesh2.num_vertices == 24 assert output_mesh2.num_facets == 6 - diff --git a/modules/core/python/tests/utils.py b/modules/core/python/tests/utils.py index 7cc06a5a..fee8705f 100644 --- a/modules/core/python/tests/utils.py +++ b/modules/core/python/tests/utils.py @@ -10,10 +10,7 @@ # governing permissions and limitations under the License. # def assert_sharing_raw_data(buf1, buf2): - assert ( - buf1.__array_interface__["data"][0] - == buf2.__array_interface__["data"][0] - ) + assert buf1.__array_interface__["data"][0] == buf2.__array_interface__["data"][0] def address(buf): diff --git a/modules/core/src/Attribute.cpp b/modules/core/src/Attribute.cpp index a0a6ba06..ea652b86 100644 --- a/modules/core/src/Attribute.cpp +++ b/modules/core/src/Attribute.cpp @@ -619,6 +619,10 @@ void Attribute::clear_views() // Explicit template instantiation //////////////////////////////////////////////////////////////////////////////// +// This needs to be first for GCC +#define LA_X_attr(_, ValueType) template class LA_CORE_API Attribute; +LA_ATTRIBUTE_X(attr, 0) + // Workaround for cartesian product of attr type with itself... // clang-format off #define LA_ATTRIBUTE2_X(mode, data) \ @@ -650,7 +654,4 @@ LA_ATTRIBUTE_X(cast_from_aux, 0) } LA_ATTRIBUTE_X(attribute_get_type, 0) -#define LA_X_attr(_, ValueType) template class LA_CORE_API Attribute; -LA_ATTRIBUTE_X(attr, 0) - } // namespace lagrange diff --git a/modules/core/src/SurfaceMesh.cpp b/modules/core/src/SurfaceMesh.cpp index 3db56275..6e2ded0f 100644 --- a/modules/core/src/SurfaceMesh.cpp +++ b/modules/core/src/SurfaceMesh.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -1756,6 +1757,8 @@ void SurfaceMesh::remove_vertices(span vertices_to_r return; // nothing to remove } + // TODO: Assert whether we have edge attributes? + // 1st step: create index remapping const Index num_vertices_old = get_num_vertices(); Index num_vertices_new = 0; @@ -2218,25 +2221,7 @@ void SurfaceMesh::update_edges_range_internal( const Index corner_begin = get_facet_corner_begin(facet_begin); const Index corner_end = get_facet_corner_end(facet_end - 1); - // Compute corner -> edge mapping - struct UnorientedEdge - { - Index v1; - Index v2; - Index id; - - UnorientedEdge(Index x, Index y, Index c) - : v1(std::min(x, y)) - , v2(std::max(x, y)) - , id(c) - {} - - auto key() const { return std::make_pair(v1, v2); } - - bool operator<(const UnorientedEdge& e) const { return key() < e.key(); } - - bool operator!=(const UnorientedEdge& e) const { return key() != e.key(); } - }; + using UnorientedEdge = internal::UnorientedEdge; auto corner_to_vertex = get_corner_to_vertex().get_all(); auto corner_to_edge = ref_attribute(m_reserved_ids.corner_to_edge()).ref_all(); @@ -2291,6 +2276,8 @@ void SurfaceMesh::update_edges_range_internal( } } } + // NOTE: I tried our fast_edge_sort() implement, and it turns out it's not really faster since + // it needs to do a copy of the whole buffer... Something to revisit later. tbb::parallel_sort(edge_to_corner.begin(), edge_to_corner.end()); // Sort user-defined edges (if available) @@ -3138,139 +3125,147 @@ AttributeId SurfaceMesh::wrap_as_attribute_internal( // Explicit template instantiations //////////////////////////////////////////////////////////////////////////////// +// Explicit instantiation of the SurfaceMesh class +// This needs to be first for GCC +#define LA_X_surface_mesh_class(_, Scalar, Index) \ + template class LA_CORE_API SurfaceMesh; +LA_SURFACE_MESH_X(surface_mesh_class, 0) + // Explicit instantiation of the templated mesh attribute methods. -#define LA_X_surface_mesh_attr(ValueType, Scalar, Index) \ - template LA_CORE_API AttributeId SurfaceMesh::create_attribute( \ - std::string_view name, \ - AttributeElement element, \ - size_t num_channels, \ - AttributeUsage usage, \ - span initial_values, \ - span initial_indices, \ - AttributeCreatePolicy policy); \ - template LA_CORE_API AttributeId SurfaceMesh::create_attribute( \ - std::string_view name, \ - AttributeElement element, \ - AttributeUsage usage, \ - size_t num_channels, \ - span initial_values, \ - span initial_indices, \ - AttributeCreatePolicy policy); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_attribute( \ - std::string_view name, \ - AttributeElement element, \ - AttributeUsage usage, \ - size_t num_channels, \ - span values_view); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_attribute( \ - std::string_view name, \ - AttributeElement element, \ - AttributeUsage usage, \ - size_t num_channels, \ - SharedSpan shared_values); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_attribute( \ - std::string_view name, \ - AttributeElement element, \ - AttributeUsage usage, \ - size_t num_channels, \ - span values_view); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_attribute( \ - std::string_view name, \ - AttributeElement element, \ - AttributeUsage usage, \ - size_t num_channels, \ - SharedSpan shared_values); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_indexed_attribute( \ - std::string_view name, \ - AttributeUsage usage, \ - size_t num_values, \ - size_t num_channels, \ - span values_view, \ - span indices_view); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_indexed_attribute( \ - std::string_view name, \ - AttributeUsage usage, \ - size_t num_values, \ - size_t num_channels, \ - SharedSpan shared_values, \ - SharedSpan shared_indices); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_indexed_attribute( \ - std::string_view name, \ - AttributeUsage usage, \ - size_t num_values, \ - size_t num_channels, \ - span values_view, \ - SharedSpan shared_indices); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_indexed_attribute( \ - std::string_view name, \ - AttributeUsage usage, \ - size_t num_values, \ - size_t num_channels, \ - SharedSpan shared_values, \ - span indices_view); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_indexed_attribute( \ - std::string_view name, \ - AttributeUsage usage, \ - size_t num_values, \ - size_t num_channels, \ - span values_view, \ - span indices_view); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_indexed_attribute( \ - std::string_view name, \ - AttributeUsage usage, \ - size_t num_values, \ - size_t num_channels, \ - SharedSpan shared_values, \ - SharedSpan shared_indices); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_indexed_attribute( \ - std::string_view name, \ - AttributeUsage usage, \ - size_t num_values, \ - size_t num_channels, \ - span values_view, \ - SharedSpan shared_indices); \ - template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_indexed_attribute( \ - std::string_view name, \ - AttributeUsage usage, \ - size_t num_values, \ - size_t num_channels, \ - SharedSpan shared_values, \ - span indices_view); \ - template LA_CORE_API std::shared_ptr> \ - SurfaceMesh::delete_and_export_attribute( \ - std::string_view name, \ - AttributeDeletePolicy delete_policy, \ - AttributeExportPolicy export_policy); \ - template LA_CORE_API std::shared_ptr> \ - SurfaceMesh::delete_and_export_const_attribute( \ - std::string_view name, \ - AttributeDeletePolicy delete_policy, \ - AttributeExportPolicy export_policy); \ - template LA_CORE_API std::shared_ptr> \ - SurfaceMesh::delete_and_export_indexed_attribute( \ - std::string_view name, \ - AttributeExportPolicy policy); \ - template LA_CORE_API std::shared_ptr> \ - SurfaceMesh::delete_and_export_const_indexed_attribute( \ - std::string_view name, \ - AttributeExportPolicy policy); \ - template LA_CORE_API bool SurfaceMesh::is_attribute_type(std::string_view name) \ - const; \ - template LA_CORE_API bool SurfaceMesh::is_attribute_type(AttributeId id) const; \ - template LA_CORE_API const Attribute& SurfaceMesh::get_attribute( \ - std::string_view name) const; \ - template LA_CORE_API const Attribute& SurfaceMesh::get_attribute(AttributeId id) \ - const; \ - template LA_CORE_API Attribute& SurfaceMesh::ref_attribute( \ - std::string_view name); \ - template LA_CORE_API Attribute& SurfaceMesh::ref_attribute(AttributeId id); \ - template LA_CORE_API const IndexedAttribute& \ - SurfaceMesh::get_indexed_attribute(std::string_view name) const; \ - template LA_CORE_API const IndexedAttribute& \ - SurfaceMesh::get_indexed_attribute(AttributeId id) const; \ - template LA_CORE_API IndexedAttribute& \ - SurfaceMesh::ref_indexed_attribute(std::string_view name); \ - template LA_CORE_API IndexedAttribute& \ +#define LA_X_surface_mesh_attr(ValueType, Scalar, Index) \ + template LA_CORE_API AttributeId SurfaceMesh::create_attribute( \ + std::string_view name, \ + AttributeElement element, \ + size_t num_channels, \ + AttributeUsage usage, \ + span initial_values, \ + span initial_indices, \ + AttributeCreatePolicy policy); \ + template LA_CORE_API AttributeId SurfaceMesh::create_attribute( \ + std::string_view name, \ + AttributeElement element, \ + AttributeUsage usage, \ + size_t num_channels, \ + span initial_values, \ + span initial_indices, \ + AttributeCreatePolicy policy); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_attribute( \ + std::string_view name, \ + AttributeElement element, \ + AttributeUsage usage, \ + size_t num_channels, \ + span values_view); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_attribute( \ + std::string_view name, \ + AttributeElement element, \ + AttributeUsage usage, \ + size_t num_channels, \ + SharedSpan shared_values); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_attribute( \ + std::string_view name, \ + AttributeElement element, \ + AttributeUsage usage, \ + size_t num_channels, \ + span values_view); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_attribute( \ + std::string_view name, \ + AttributeElement element, \ + AttributeUsage usage, \ + size_t num_channels, \ + SharedSpan shared_values); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_indexed_attribute( \ + std::string_view name, \ + AttributeUsage usage, \ + size_t num_values, \ + size_t num_channels, \ + span values_view, \ + span indices_view); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_indexed_attribute( \ + std::string_view name, \ + AttributeUsage usage, \ + size_t num_values, \ + size_t num_channels, \ + SharedSpan shared_values, \ + SharedSpan shared_indices); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_indexed_attribute( \ + std::string_view name, \ + AttributeUsage usage, \ + size_t num_values, \ + size_t num_channels, \ + span values_view, \ + SharedSpan shared_indices); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_indexed_attribute( \ + std::string_view name, \ + AttributeUsage usage, \ + size_t num_values, \ + size_t num_channels, \ + SharedSpan shared_values, \ + span indices_view); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_indexed_attribute( \ + std::string_view name, \ + AttributeUsage usage, \ + size_t num_values, \ + size_t num_channels, \ + span values_view, \ + span indices_view); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_indexed_attribute( \ + std::string_view name, \ + AttributeUsage usage, \ + size_t num_values, \ + size_t num_channels, \ + SharedSpan shared_values, \ + SharedSpan shared_indices); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_indexed_attribute( \ + std::string_view name, \ + AttributeUsage usage, \ + size_t num_values, \ + size_t num_channels, \ + span values_view, \ + SharedSpan shared_indices); \ + template LA_CORE_API AttributeId SurfaceMesh::wrap_as_const_indexed_attribute( \ + std::string_view name, \ + AttributeUsage usage, \ + size_t num_values, \ + size_t num_channels, \ + SharedSpan shared_values, \ + span indices_view); \ + template LA_CORE_API std::shared_ptr> \ + SurfaceMesh::delete_and_export_attribute( \ + std::string_view name, \ + AttributeDeletePolicy delete_policy, \ + AttributeExportPolicy export_policy); \ + template LA_CORE_API std::shared_ptr> \ + SurfaceMesh::delete_and_export_const_attribute( \ + std::string_view name, \ + AttributeDeletePolicy delete_policy, \ + AttributeExportPolicy export_policy); \ + template LA_CORE_API std::shared_ptr> \ + SurfaceMesh::delete_and_export_indexed_attribute( \ + std::string_view name, \ + AttributeExportPolicy policy); \ + template LA_CORE_API std::shared_ptr> \ + SurfaceMesh::delete_and_export_const_indexed_attribute( \ + std::string_view name, \ + AttributeExportPolicy policy); \ + template LA_CORE_API bool SurfaceMesh::is_attribute_type( \ + std::string_view name) const; \ + template LA_CORE_API bool SurfaceMesh::is_attribute_type( \ + AttributeId id) const; \ + template LA_CORE_API const Attribute& SurfaceMesh::get_attribute( \ + std::string_view name) const; \ + template LA_CORE_API const Attribute& SurfaceMesh::get_attribute( \ + AttributeId id) const; \ + template LA_CORE_API Attribute& SurfaceMesh::ref_attribute( \ + std::string_view name); \ + template LA_CORE_API Attribute& SurfaceMesh::ref_attribute( \ + AttributeId id); \ + template LA_CORE_API const IndexedAttribute& \ + SurfaceMesh::get_indexed_attribute(std::string_view name) const; \ + template LA_CORE_API const IndexedAttribute& \ + SurfaceMesh::get_indexed_attribute(AttributeId id) const; \ + template LA_CORE_API IndexedAttribute& \ + SurfaceMesh::ref_indexed_attribute(std::string_view name); \ + template LA_CORE_API IndexedAttribute& \ SurfaceMesh::ref_indexed_attribute(AttributeId id); #define LA_X_surface_mesh_aux(_, ValueType) LA_SURFACE_MESH_X(surface_mesh_attr, ValueType) @@ -3279,16 +3274,17 @@ LA_ATTRIBUTE_X(surface_mesh_aux, 0) // Explicit instantiation of the SurfaceMesh::create_attribute_from() method. #define fst(first, second) first #define snd(first, second) second -#define LA_X_surface_mesh_mesh_other(ScalarIndex, OtherScalar, OtherIndex) \ - template LA_CORE_API SurfaceMesh \ - SurfaceMesh::stripped_copy( \ - const SurfaceMesh& other); \ - template LA_CORE_API SurfaceMesh \ - SurfaceMesh::stripped_move( \ - SurfaceMesh&& other); \ - template LA_CORE_API AttributeId SurfaceMesh::create_attribute_from( \ - std::string_view name, \ - const SurfaceMesh& source_mesh, \ +#define LA_X_surface_mesh_mesh_other(ScalarIndex, OtherScalar, OtherIndex) \ + template LA_CORE_API SurfaceMesh \ + SurfaceMesh::stripped_copy( \ + const SurfaceMesh& other); \ + template LA_CORE_API SurfaceMesh \ + SurfaceMesh::stripped_move( \ + SurfaceMesh&& other); \ + template LA_CORE_API AttributeId \ + SurfaceMesh::create_attribute_from( \ + std::string_view name, \ + const SurfaceMesh& source_mesh, \ std::string_view source_name); // NOTE: This is a dirty workaround because nesting two LA_SURFACE_MESH_X macros doesn't quite work. @@ -3301,8 +3297,4 @@ LA_ATTRIBUTE_X(surface_mesh_aux, 0) LA_SURFACE_MESH2_X(surface_mesh_mesh_other, (Scalar, Index)) LA_SURFACE_MESH_X(surface_mesh_mesh_aux, 0) -// Explicit instantiation of the SurfaceMesh class -#define LA_X_surface_mesh_class(_, Scalar, Index) template class LA_CORE_API SurfaceMesh; -LA_SURFACE_MESH_X(surface_mesh_class, 0) - } // namespace lagrange diff --git a/modules/core/src/cast_attribute.cpp b/modules/core/src/cast_attribute.cpp index 1533ac7c..468eacfe 100644 --- a/modules/core/src/cast_attribute.cpp +++ b/modules/core/src/cast_attribute.cpp @@ -40,7 +40,7 @@ AttributeId cast_attribute( source_attr_base.get_usage(), source_attr_base.get_num_channels()); - internal::visit_attribute(mesh, source_id, [&](auto& source_attr) { + internal::visit_attribute_read(mesh, source_id, [&](auto& source_attr) { using AttributeType = std::decay_t; if constexpr (AttributeType::IsIndexed) { auto& target_attr = mesh.template ref_indexed_attribute(target_id); @@ -77,7 +77,7 @@ AttributeId cast_attribute_in_place(SurfaceMesh& mesh, AttributeI std::string target_name(mesh.get_attribute_name(source_id)); AttributeId target_id = invalid_attribute_id(); - internal::visit_attribute(mesh, source_id, [&](auto& source_attr_) { + internal::visit_attribute_read(mesh, source_id, [&](auto& source_attr_) { using AttributeType = std::decay_t; using SourceValueType = typename AttributeType::ValueType; diff --git a/modules/core/src/compute_seam_edges.cpp b/modules/core/src/compute_seam_edges.cpp index f7bf5be2..929da3cf 100644 --- a/modules/core/src/compute_seam_edges.cpp +++ b/modules/core/src/compute_seam_edges.cpp @@ -75,7 +75,7 @@ AttributeId compute_seam_edges( }); }; - internal::visit_attribute(mesh, source_id, [&](auto&& attr) { + internal::visit_attribute_read(mesh, source_id, [&](auto&& attr) { using AttributeType = std::decay_t; if constexpr (AttributeType::IsIndexed) { process_attribute(attr); diff --git a/modules/core/src/internal/bucket_sort.h b/modules/core/src/internal/bucket_sort.h index c488b293..2c516663 100644 --- a/modules/core/src/internal/bucket_sort.h +++ b/modules/core/src/internal/bucket_sort.h @@ -77,9 +77,11 @@ BucketSortResult bucket_sort( element_representative[e] = element_representative[r]; } - std::tie(sorted_elements, representative_offsets) = invert_mapping( + auto mapping = invert_mapping( {element_representative.data(), element_representative.size()}, num_representatives); + sorted_elements = std::move(mapping.data); + representative_offsets = std::move(mapping.offsets); return result; } @@ -122,8 +124,12 @@ bucket_sort(std::vector& elements, Index num_buckets, Function get_repres auto& num_representatives = result.num_representatives; auto& representative_offsets = result.representative_offsets; num_representatives = num_buckets; - std::tie(elements, representative_offsets) = - invert_mapping(static_cast(elements.size()), get_representative, num_representatives); + auto mapping = invert_mapping( + static_cast(elements.size()), + get_representative, + num_representatives); + elements = std::move(mapping.data); + representative_offsets = std::move(mapping.offsets); return result; } diff --git a/modules/core/src/map_attribute.cpp b/modules/core/src/map_attribute.cpp index dca45891..42ba9deb 100644 --- a/modules/core/src/map_attribute.cpp +++ b/modules/core/src/map_attribute.cpp @@ -270,6 +270,9 @@ AttributeId map_attribute_in_place( AttributeId id, AttributeElement new_element) { + // TODO: Optimize use-case when new element type has same cardinality as old element type (e.g. + // AttributeElement::Value). In this case we can reuse the buffer in place without allocating a + // new one. auto get_unique_name = [&](auto name) -> std::string { if (!mesh.has_attribute(name)) { return name; @@ -304,24 +307,24 @@ AttributeId map_attribute_in_place( return map_attribute_in_place(mesh, mesh.get_attribute_id(name), new_element); } -#define LA_X_map_attribute(_, Scalar, Index) \ +#define LA_X_map_attribute(_, Scalar, Index) \ template LA_CORE_API AttributeId map_attribute( \ - SurfaceMesh& mesh, \ - AttributeId id, \ - std::string_view new_name, \ - AttributeElement new_element); \ + SurfaceMesh& mesh, \ + AttributeId id, \ + std::string_view new_name, \ + AttributeElement new_element); \ template LA_CORE_API AttributeId map_attribute( \ - SurfaceMesh& mesh, \ - std::string_view old_name, \ - std::string_view new_name, \ - AttributeElement new_element); \ + SurfaceMesh& mesh, \ + std::string_view old_name, \ + std::string_view new_name, \ + AttributeElement new_element); \ template LA_CORE_API AttributeId map_attribute_in_place( \ - SurfaceMesh& mesh, \ - AttributeId id, \ - AttributeElement new_element); \ + SurfaceMesh& mesh, \ + AttributeId id, \ + AttributeElement new_element); \ template LA_CORE_API AttributeId map_attribute_in_place( \ - SurfaceMesh& mesh, \ - std::string_view name, \ + SurfaceMesh& mesh, \ + std::string_view name, \ AttributeElement new_element); LA_SURFACE_MESH_X(map_attribute, 0) diff --git a/modules/core/src/remap_vertices.cpp b/modules/core/src/remap_vertices.cpp index 48a1038c..8fbb7e86 100644 --- a/modules/core/src/remap_vertices.cpp +++ b/modules/core/src/remap_vertices.cpp @@ -12,6 +12,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -26,6 +29,109 @@ namespace lagrange { +namespace { + +template +using InverseMapping = internal::InverseMapping; + +// Remap an attribute. If multiple entries are mapped to the same slot, they will be averaged. +template +void remap_average( + Attribute& attr, + const InverseMapping& new_to_old, + Index num_out_elements) +{ + auto usage = attr.get_usage(); + if (usage == AttributeUsage::VertexIndex || usage == AttributeUsage::FacetIndex || + usage == AttributeUsage::CornerIndex || usage == AttributeUsage::EdgeIndex) { + throw Error("remap_vertices cannot average indices!"); + } + + auto data = matrix_ref(attr); + // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. + Eigen::Matrix data_copy = data; + + for (Index i = 0; i < num_out_elements; i++) { + data.row(i).setZero(); + for (Index j = new_to_old.offsets[i]; j < new_to_old.offsets[i + 1]; j++) { + data.row(i) += data_copy.row(new_to_old.data[j]); + } + data.row(i) /= static_cast(new_to_old.offsets[i + 1] - new_to_old.offsets[i]); + } + attr.resize_elements(num_out_elements); +} + +// Remap an attribute. If multiple entries are mapped to the same slot, keep the first. +template +auto remap_keep_first( + Attribute& attr, + const InverseMapping& new_to_old, + Index num_out_elements) +{ + auto data = matrix_ref(attr); + // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. + Eigen::Matrix data_copy = data; + + for (Index i = 0; i < num_out_elements; i++) { + const Index j = new_to_old.offsets[i]; + data.row(i) = data_copy.row(new_to_old.data[j]); + } + attr.resize_elements(num_out_elements); +} + +// Remap an attribute. If multiple entries are mapped to the same slot, throw error. +template +auto remap_injective( + Attribute& attr, + const InverseMapping& new_to_old, + Index num_out_elements) +{ + auto data = matrix_ref(attr); + // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. + Eigen::Matrix data_copy = data; + + for (Index i = 0; i < num_out_elements; i++) { + const Index j = new_to_old.offsets[i]; + la_runtime_assert( + new_to_old.offsets[i + 1] == j + 1, + "Vertex mapping policy does not allow collision."); + data.row(i) = data_copy.row(new_to_old.data[j]); + } + attr.resize_elements(num_out_elements); +} + +template +void remap_attribute( + Attribute& attr, + const InverseMapping& new_to_old, + Index num_out_elements, + RemapVerticesOptions options) +{ + if constexpr (std::is_integral_v) { + switch (options.collision_policy_integral) { + case MappingPolicy::Average: remap_average(attr, new_to_old, num_out_elements); break; + case MappingPolicy::KeepFirst: remap_keep_first(attr, new_to_old, num_out_elements); break; + case MappingPolicy::Error: remap_injective(attr, new_to_old, num_out_elements); break; + default: + throw Error(fmt::format( + "Unsupported integer collision policy {}", + static_cast(options.collision_policy_integral))); + } + } else { + switch (options.collision_policy_float) { + case MappingPolicy::Average: remap_average(attr, new_to_old, num_out_elements); break; + case MappingPolicy::KeepFirst: remap_keep_first(attr, new_to_old, num_out_elements); break; + case MappingPolicy::Error: remap_injective(attr, new_to_old, num_out_elements); break; + default: + throw Error(fmt::format( + "Unsupported float collision policy {}", + static_cast(options.collision_policy_float))); + } + } +} + +} // namespace + template void remap_vertices( SurfaceMesh& mesh, @@ -35,158 +141,90 @@ void remap_vertices( const Index num_vertices = mesh.get_num_vertices(); la_runtime_assert((Index)old_to_new.size() == num_vertices); - // The internal data structure for edges (e.g. $vertex_to_first_corner and - // $next_corner_around_vertex) cannot be easily updated. - if (mesh.has_edges()) { - throw Error( - "Remap vertices will invalidate edge data in mesh. Please clear_edges() first."); - } - // Compute the backward order. - std::vector new_to_old_indices(num_vertices + 1, 0); - std::vector new_to_old(num_vertices); - for (Index i = 0; i < num_vertices; ++i) { - Index j = old_to_new[i]; - la_runtime_assert( - j < num_vertices, - "New vertex index cannot exceeds existing number of vertices!"); - ++new_to_old_indices[j + 1]; - } - size_t num_out_vertices = num_vertices; - for (; num_out_vertices != 0 && new_to_old_indices[num_out_vertices] == 0; --num_out_vertices) { - } - new_to_old_indices.resize(num_out_vertices + 1); - std::partial_sum( - new_to_old_indices.begin(), - new_to_old_indices.end(), - new_to_old_indices.begin()); - la_debug_assert(new_to_old_indices.back() == num_vertices); - - for (Index i = 0; i < num_vertices; i++) { - Index j = old_to_new[i]; - new_to_old[new_to_old_indices[j]++] = i; - } - std::rotate( - new_to_old_indices.begin(), - std::prev(new_to_old_indices.end()), - new_to_old_indices.end()); - new_to_old_indices[0] = 0; + auto new_to_old = internal::invert_mapping(old_to_new); + la_debug_assert(new_to_old.offsets.back() == num_vertices); + const Index num_out_vertices = static_cast(new_to_old.offsets.size() - 1); // Surjective check! - for (Index i = 0; i < num_out_vertices; i++) { + for (Index i = 0; i < num_out_vertices; ++i) { la_runtime_assert( - new_to_old_indices[i] < new_to_old_indices[i + 1], + new_to_old.offsets[i] < new_to_old.offsets[i + 1], "old_to_new mapping is not surjective!"); } - // Remap an attribute. If multiple entries are mapped to the same slot, they will be averaged. - auto remap_average = [&](auto&& attr) { - auto usage = attr.get_usage(); - if (usage == AttributeUsage::VertexIndex || usage == AttributeUsage::FacetIndex || - usage == AttributeUsage::CornerIndex) { - throw Error("remap_vertices cannot average indices!"); - } - - auto data = matrix_ref(attr); - using ValueType = typename std::decay_t::Scalar; - // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. - Eigen::Matrix data_copy = data; + std::vector attr_with_edge_element_type; + std::vector attr_with_edge_index_usage; + std::vector> old_edges; + bool had_edges = mesh.has_edges(); + if (mesh.has_edges()) { + // To preserve edge attributes we do the following: + // 1. Turn every non-reserved attribute into a "value" attribute. + // 2. Keep track of attributes with EdgeIndex usage tag and update their tag to `Scalar`. + // 3. Clear edges to remove internal connectivity information (it's much easier/safer to + // rebuild it from scratch!) + // 4. After vertex deletion, recompute edge connectivity. + // 5. Resize out value attributes to the new number of edges, and remap/average values + // accordingly. + // 6. Update their element type to "edge". - for (Index i = 0; i < num_out_vertices; i++) { - data.row(i).setZero(); - for (Index j = new_to_old_indices[i]; j < new_to_old_indices[i + 1]; j++) { - data.row(i) += data_copy.row(new_to_old[j]); + seq_foreach_named_attribute_read(mesh, [&](std::string_view name, auto&& attr) { + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + if (!mesh.attr_name_is_reserved(name)) { + // Re-tag existing non-reserved edge attributes as "value" attributes. + if (attr.get_element_type() == AttributeElement::Edge) { + auto id = mesh.get_attribute_id(name); + attr_with_edge_element_type.push_back(id); + auto& attr_ref = mesh.template ref_attribute(id); + attr_ref.unsafe_set_element_type(AttributeElement::Value); + } + // Keep track of attributes with EdgeIndex usage tag. Update tag to `Scalar` to + // avoid all values being remapped to `0` by our call to `clear_edges()`. + if (attr.get_usage() == AttributeUsage::EdgeIndex) { + auto id = mesh.get_attribute_id(name); + attr_with_edge_index_usage.push_back(id); + if constexpr (AttributeType::IsIndexed) { + auto& attr_ref = mesh.template ref_indexed_attribute(id); + attr_ref.unsafe_set_usage(AttributeUsage::Scalar); + } else { + auto& attr_ref = mesh.template ref_attribute(id); + attr_ref.unsafe_set_usage(AttributeUsage::Scalar); + } + } } - data.row(i) /= - static_cast(new_to_old_indices[i + 1] - new_to_old_indices[i]); - } - attr.resize_elements(num_out_vertices); - }; - - // Remap an attribute. If multiple entries are mapped to the same slot, keep the first. - auto remap_keep_first = [&](auto&& attr) { - auto data = matrix_ref(attr); - using ValueType = typename std::decay_t::Scalar; - // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. - Eigen::Matrix data_copy = data; - - for (Index i = 0; i < num_out_vertices; i++) { - const Index j = new_to_old_indices[i]; - data.row(i) = data_copy.row(new_to_old[j]); - } - attr.resize_elements(num_out_vertices); - }; - - // Remap an attribute. If multiple entries are mapped to the same slot, throw error. - auto remap_injective = [&](auto&& attr) { - auto data = matrix_ref(attr); - using ValueType = typename std::decay_t::Scalar; - // A temp copy is necessary as we cannot make any assumptions about `old_to_new` order. - Eigen::Matrix data_copy = data; - - for (Index i = 0; i < num_out_vertices; i++) { - const Index j = new_to_old_indices[i]; - la_runtime_assert( - new_to_old_indices[i + 1] == j + 1, - "Vertex mapping policy does not allow collision."); - data.row(i) = data_copy.row(new_to_old[j]); + }); + + old_edges.reserve(mesh.get_num_edges()); + for (Index e = 0; e < mesh.get_num_edges(); ++e) { + auto v = mesh.get_edge_vertices(e); + old_edges.emplace_back(old_to_new[v[0]], old_to_new[v[1]], e); } - attr.resize_elements(num_out_vertices); - }; - // Remap vertex attributes. + // No we can clear edges and remove reserved connectivity-related attributes. + mesh.clear_edges(); + } + + // Remap per-vertex attributes. + // TODO: We may want to cache the tmp copy buffers to avoid repeated allocations. par_foreach_named_attribute_write( mesh, [&](std::string_view name, auto&& attr) { using AttributeType = std::decay_t; using ValueType = typename AttributeType::ValueType; if (name == mesh.attr_name_vertex_to_first_corner() || - name == mesh.attr_name_next_corner_around_vertex()) + name == mesh.attr_name_next_corner_around_vertex()) { return; - - if constexpr (std::is_integral_v) { - switch (options.collision_policy_integral) { - case MappingPolicy::Average: - remap_average(std::forward(attr)); - break; - case MappingPolicy::KeepFirst: - remap_keep_first(std::forward(attr)); - break; - case MappingPolicy::Error: - remap_injective(std::forward(attr)); - break; - default: - throw Error(fmt::format( - "Unsupported collision policy {}", - int(options.collision_policy_integral))); - } - } else { - switch (options.collision_policy_float) { - case MappingPolicy::Average: - remap_average(std::forward(attr)); - break; - case MappingPolicy::KeepFirst: - remap_keep_first(std::forward(attr)); - break; - case MappingPolicy::Error: - remap_injective(std::forward(attr)); - break; - default: - throw Error(fmt::format( - "Unsupported collision policy {}", - int(options.collision_policy_float))); - } } + + remap_attribute(attr, new_to_old, num_out_vertices, options); }); - // Update vertex indices. + // Update attributes with VertexIndex usage tag in other element types. par_foreach_named_attribute_read(mesh, [&](std::string_view name, auto&& attr) { using AttributeType = std::decay_t; using ValueType = typename AttributeType::ValueType; - // Only remap vertex indices that are not associated with vertex element because vertex - // attributes have already been updated in previous step. - if (attr.get_usage() == AttributeUsage::VertexIndex && - attr.get_element_type() != AttributeElement::Vertex) { + if (attr.get_usage() == AttributeUsage::VertexIndex) { auto& attr_ref = mesh.template ref_attribute(name); auto data = attr_ref.ref_all(); std::transform(data.begin(), data.end(), data.begin(), [&](ValueType i) { @@ -195,19 +233,82 @@ void remap_vertices( } }); - // Remap vertices. This must be done last because `remove_vertices` also delete facets adjacent + // Remap vertices. This must be done last because `remove_vertices` also delete facets adjacent // to the vertices. // - // TODO: we has already resized the vertex-to-position attribute, but there is no way of + // TODO: we have already resized the vertex-to-position attribute, but there is no way of // changing `SurfaceMesh::m_num_vertices`. Using `SurfaceMesh::remove_vertices` works, but it // feels like an overkill and a fragile solution... mesh.remove_vertices([&](Index i) { return i >= num_out_vertices; }); + + // Re-create connectivity + if (had_edges) { + mesh.initialize_edges(); + + tbb::parallel_sort(old_edges.begin(), old_edges.end()); + + std::vector old_to_new_edges(old_edges.size()); + + Index new_num_edges = 0; + for (auto it_begin = old_edges.begin(); it_begin != old_edges.end();) { + Index edge_id = new_num_edges; + // First the first edge after it_begin that has a different key + auto it_end = + std::find_if(it_begin, old_edges.end(), [&](auto e) { return (e != *it_begin); }); + // Process range of similar edges + for (auto it = it_begin; it != it_end; ++it) { + old_to_new_edges[it->id] = edge_id; + } + ++new_num_edges; + it_begin = it_end; + } + auto new_to_old_edges = internal::invert_mapping(old_to_new_edges); + + // Remap values and resize per-edge attributes. + for (auto id : attr_with_edge_element_type) { + internal::visit_attribute_write(mesh, id, [&](auto&& attr) { + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + + if constexpr (AttributeType::IsIndexed) { + la_debug_assert(false, "Logic error in remap_vertices."); + } else { + remap_attribute( + attr, + new_to_old_edges, + mesh.get_num_edges(), + options); + } + + attr.unsafe_set_element_type(AttributeElement::Edge); + }); + } + + // Update attributes with EdgeIndex usage tag in other element types. + for (auto id : attr_with_edge_index_usage) { + internal::visit_attribute_write(mesh, id, [&](auto&& attr) { + using AttributeType = std::decay_t; + using ValueType = typename AttributeType::ValueType; + attr.unsafe_set_usage(AttributeUsage::EdgeIndex); + auto data = [&]() -> decltype(auto) { + if constexpr (AttributeType::IsIndexed) { + return attr.values().ref_all(); + } else { + return attr.ref_all(); + } + }(); + std::transform(data.begin(), data.end(), data.begin(), [&](ValueType i) { + return static_cast(old_to_new_edges[static_cast(i)]); + }); + }); + } + } } -#define LA_X_remap_vertices(_, Scalar, Index) \ +#define LA_X_remap_vertices(_, Scalar, Index) \ template LA_CORE_API void remap_vertices( \ - SurfaceMesh&, \ - span, \ + SurfaceMesh&, \ + span, \ RemapVerticesOptions); LA_SURFACE_MESH_X(remap_vertices, 0) diff --git a/modules/core/src/select_facets_in_frustum.cpp b/modules/core/src/select_facets_in_frustum.cpp new file mode 100644 index 00000000..8427abf2 --- /dev/null +++ b/modules/core/src/select_facets_in_frustum.cpp @@ -0,0 +1,255 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + + + +namespace lagrange { + +template +bool select_facets_in_frustum( + SurfaceMesh& mesh, + const Frustum& frustum, + const FrustumSelectionOptions& options) +{ + using Point3D = typename Eigen::Vector3; + const Point3D n0(frustum.planes[0].normal.data()); + const Point3D n1(frustum.planes[1].normal.data()); + const Point3D n2(frustum.planes[2].normal.data()); + const Point3D n3(frustum.planes[3].normal.data()); + const Point3D p0(frustum.planes[0].point.data()); + const Point3D p1(frustum.planes[1].point.data()); + const Point3D p2(frustum.planes[2].point.data()); + const Point3D p3(frustum.planes[3].point.data()); + + const Index num_facets = mesh.get_num_facets(); + + // Per-thread buffers to store intermediate results. + struct LocalBuffers + { + Point3D v0, v1, v2; // Triangle vertices. + Point3D q0, q1, q2, q3; // Intermediate tet vertices. + std::array r0, r1, r2; // Triangle projections. + }; + tbb::enumerable_thread_specific temp_vars; + + auto edge_overlap_with_negative_octant = [](const Point3D& q0, const Point3D& q1) -> bool { + Scalar t_min = 0, t_max = 1; + const Point3D e = {q0[0] - q1[0], q0[1] - q1[1], q0[2] - q1[2]}; + if (e[0] > 0) { + t_max = std::min(t_max, -q1[0] / e[0]); + } else if (e[0] < 0) { + t_min = std::max(t_min, -q1[0] / e[0]); + } else { + if (q1[0] >= 0) return false; + } + + if (e[1] > 0) { + t_max = std::min(t_max, -q1[1] / e[1]); + } else if (e[1] < 0) { + t_min = std::max(t_min, -q1[1] / e[1]); + } else { + if (q1[1] >= 0) return false; + } + + if (e[2] > 0) { + t_max = std::min(t_max, -q1[2] / e[2]); + } else if (e[2] < 0) { + t_min = std::max(t_min, -q1[2] / e[2]); + } else { + if (q1[2] >= 0) return false; + } + + return t_max >= t_min; + }; + + auto compute_plane = + [](const Point3D& q0, const Point3D& q1, const Point3D& q2) -> std::pair { + const Point3D n = { + q0[2] * (q2[1] - q1[1]) + q1[2] * (q0[1] - q2[1]) + q2[2] * (q1[1] - q0[1]), + q0[0] * (q2[2] - q1[2]) + q1[0] * (q0[2] - q2[2]) + q2[0] * (q1[2] - q0[2]), + q0[1] * (q2[0] - q1[0]) + q1[1] * (q0[0] - q2[0]) + q2[1] * (q1[0] - q0[0])}; + const Scalar c = (n.dot(q0) + n.dot(q1) + n.dot(q2)) / 3; + return {n, c}; + }; + + // Compute the orientation of 2D traingle (v0, v1, O), where O = (0, 0). + auto orient2D_inexact = [](const std::array& v0, + const std::array& v1) -> int { + const auto r = v0[0] * v1[1] - v0[1] * v1[0]; + if (r > 0) return 1; + if (r < 0) return -1; + return 0; + }; + + auto triangle_intersects_negative_axis = [&](const Point3D& q0, + const Point3D& q1, + const Point3D& q2, + const Point3D& n, + const Scalar c, + const int axis) -> bool { + auto& r0 = temp_vars.local().r0; + auto& r1 = temp_vars.local().r1; + auto& r2 = temp_vars.local().r2; + + r0[0] = q0[(axis + 1) % 3]; + r0[1] = q0[(axis + 2) % 3]; + r1[0] = q1[(axis + 1) % 3]; + r1[1] = q1[(axis + 2) % 3]; + r2[0] = q2[(axis + 1) % 3]; + r2[1] = q2[(axis + 2) % 3]; + + auto o01 = orient2D_inexact(r0, r1); + auto o12 = orient2D_inexact(r1, r2); + auto o20 = orient2D_inexact(r2, r0); + if (o01 == o12 && o01 == o20) { + if (o01 == 0) { + // Triangle projection is degenerate. + // Note that the case where axis is coplanar with the triangle + // is treated as no intersection (which is debatale). + return false; + } else { + // Triangle projection contains the origin. + // Check axis intercept. + return (c < 0 && n[axis] > 0) || (c > 0 && n[axis] < 0); + } + } else { + // Note that we treat the case where the axis intersect triangle at + // its boundary as no intersection. + return false; + } + }; + + auto triangle_intersects_negative_axes = + [&](const Point3D& q0, const Point3D& q1, const Point3D& q2) -> bool { + const auto r = compute_plane(q0, q1, q2); + if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 0)) return true; + if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 1)) return true; + if (triangle_intersects_negative_axis(q0, q1, q2, r.first, r.second, 2)) return true; + return false; + }; + + auto tet_overlap_with_negative_octant = + [&](const Point3D& q0, const Point3D& q1, const Point3D& q2, const Point3D& q3) -> bool { + // Check 1: Check if any tet vertices is in negative octant. + if ((q0.array() < 0).all()) return true; + if ((q1.array() < 0).all()) return true; + if ((q2.array() < 0).all()) return true; + if ((q3.array() < 0).all()) return true; + + // Check 2: Check if any tet edges cross the negative octant. + if (edge_overlap_with_negative_octant(q0, q1)) return true; + if (edge_overlap_with_negative_octant(q0, q2)) return true; + if (edge_overlap_with_negative_octant(q0, q3)) return true; + if (edge_overlap_with_negative_octant(q1, q2)) return true; + if (edge_overlap_with_negative_octant(q1, q3)) return true; + if (edge_overlap_with_negative_octant(q2, q3)) return true; + + // Check 3: Check if -X, -Y or -Z axis intersect the tet. + if (triangle_intersects_negative_axes(q0, q1, q2)) return true; + if (triangle_intersects_negative_axes(q1, q2, q3)) return true; + if (triangle_intersects_negative_axes(q2, q3, q0)) return true; + if (triangle_intersects_negative_axes(q3, q0, q1)) return true; + + // All check failed iff tet does not intersect the negative octant. + return false; + }; + + // AttributeArray attr; + lagrange::span attr_ref; + if (!options.greedy) { + AttributeId id = internal::find_or_create_attribute( + mesh, + options.output_attribute_name, + Facet, + AttributeUsage::Scalar, + /* number of channels */ 1, + /* this should already trigger writing the memory and therefore copy on write */ + internal::ResetToDefault::Yes); + auto& attr = + mesh.template ref_attribute(id); // this may also trigger copy on write + la_debug_assert(static_cast(attr.get_num_elements()) == num_facets); + attr_ref = attr.ref_all(); + } + + std::atomic_bool r{false}; + + const auto& vertex_positions = vertex_view(mesh); + tbb::parallel_for( + tbb::blocked_range(0, num_facets), + [&](const tbb::blocked_range& tbb_range) { + for (auto fi = tbb_range.begin(); fi != tbb_range.end(); fi++) { + if (tbb_utils::is_cancelled()) break; + + const auto facet_vertices = mesh.get_facet_vertices(fi); + + // Triangle (v0, v1, v2) intersect the cone defined by 4 planes + // iff the tetrahedron (q0, q1, q2, q3) does not intersect the negative octant. + // This can be proved by Farkas' lemma. + + auto& v0 = temp_vars.local().v0; + auto& v1 = temp_vars.local().v1; + auto& v2 = temp_vars.local().v2; + + auto& q0 = temp_vars.local().q0; + auto& q1 = temp_vars.local().q1; + auto& q2 = temp_vars.local().q2; + auto& q3 = temp_vars.local().q3; + + v0 = vertex_positions.row(facet_vertices[0]).template head<3>(); + v1 = vertex_positions.row(facet_vertices[1]).template head<3>(); + v2 = vertex_positions.row(facet_vertices[2]).template head<3>(); + + q0 = {(v0 - p0).dot(n0), (v1 - p0).dot(n0), (v2 - p0).dot(n0)}; + q1 = {(v0 - p1).dot(n1), (v1 - p1).dot(n1), (v2 - p1).dot(n1)}; + q2 = {(v0 - p2).dot(n2), (v1 - p2).dot(n2), (v2 - p2).dot(n2)}; + q3 = {(v0 - p3).dot(n3), (v1 - p3).dot(n3), (v2 - p3).dot(n3)}; + + const auto ri = !tet_overlap_with_negative_octant(q0, q1, q2, q3); + + if (ri) r = true; + + if (!attr_ref.empty() /* same as !options.greedy */) { + attr_ref[fi] = uint8_t(ri); + } + + if (options.greedy && ri) { + tbb_utils::cancel_group_execution(); + break; + } + } + }); + + return r.load(); +} + +#define LA_X_select_facets_in_frustum(_, Scalar, Index) \ + template LA_CORE_API bool select_facets_in_frustum( \ + SurfaceMesh& mesh, \ + const Frustum& frustum, \ + const FrustumSelectionOptions& options); +LA_SURFACE_MESH_X(select_facets_in_frustum, 0) + +} // namespace lagrange diff --git a/modules/core/src/weld_indexed_attribute.cpp b/modules/core/src/weld_indexed_attribute.cpp index ea4a6fa8..243e87cd 100644 --- a/modules/core/src/weld_indexed_attribute.cpp +++ b/modules/core/src/weld_indexed_attribute.cpp @@ -93,9 +93,7 @@ void weld_indexed_attribute( return; } - std::vector mapping_data, mapping_offsets; - std::tie(mapping_data, mapping_offsets) = - internal::invert_mapping({old2new.data(), old2new.size()}, count); + auto mapping = internal::invert_mapping({old2new.data(), old2new.size()}, count); Attribute attr_welded_values( attr_values.get_element_type(), @@ -105,12 +103,12 @@ void weld_indexed_attribute( auto welded_values = matrix_ref(attr_welded_values); welded_values.setZero(); tbb::parallel_for((Index)0, count, [&](Index i) { - for (Index j = mapping_offsets[i]; j < mapping_offsets[i + 1]; j++) { - welded_values.row(i) += values.row(mapping_data[j]); + for (Index j = mapping.offsets[i]; j < mapping.offsets[i + 1]; j++) { + welded_values.row(i) += values.row(mapping.data[j]); } - if (mapping_offsets[i + 1] - mapping_offsets[i] > 0) { + if (mapping.offsets[i + 1] - mapping.offsets[i] > 0) { welded_values.row(i) /= - static_cast(mapping_offsets[i + 1] - mapping_offsets[i]); + static_cast(mapping.offsets[i + 1] - mapping.offsets[i]); } }); attr_values = std::move(attr_welded_values); @@ -140,8 +138,10 @@ void weld_indexed_attribute(SurfaceMesh& mesh, AttributeId attr_i #undef LA_X_weld_indexed_attribute } -#define LA_X_weld_indexed_attribute(ValueType, Scalar, Index) \ - template LA_CORE_API void weld_indexed_attribute(SurfaceMesh&, AttributeId); +#define LA_X_weld_indexed_attribute(ValueType, Scalar, Index) \ + template LA_CORE_API void weld_indexed_attribute( \ + SurfaceMesh&, \ + AttributeId); LA_SURFACE_MESH_X(weld_indexed_attribute, 0) diff --git a/modules/core/tests/fmt/CMakeLists.txt b/modules/core/tests/fmt/CMakeLists.txt new file mode 100644 index 00000000..12d54221 --- /dev/null +++ b/modules/core/tests/fmt/CMakeLists.txt @@ -0,0 +1,56 @@ +# +# Copyright 2024 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# + +# This is a standalone root CMakeLists.txt to test compilation of our fmt::formatter for Eigen +# types. By (manually) tuning various compile options you should be able to easily try out the +# following configurations: +# +# - Spdlog with internal fmt version +# - External fmt 8, 9, 10, 10.2 +# - std::format (C++20) +# - User-provided fmt::format<> +# +cmake_minimum_required(VERSION 3.25) +project(test_fmt) + +set(LAGRANGE_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../../..") + +message("LAGRANGE_ROOT: ${LAGRANGE_ROOT}") + +list(PREPEND CMAKE_MODULE_PATH "${LAGRANGE_ROOT}/cmake/recipes/external") + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +option(SPDLOG_INSTALL "Generate the install target" OFF) +option(SPDLOG_FMT_EXTERNAL "Use external fmt library instead of bundled" ON) + +include(spdlog) +include(eigen) + +include(CTest) + +include(catch2) +FetchContent_GetProperties(catch2) +include("${catch2_SOURCE_DIR}/extras/Catch.cmake") + +add_executable(test_fmt test_fmt.cpp) +target_link_libraries(test_fmt PRIVATE spdlog::spdlog Eigen3::Eigen Catch2::Catch2WithMain) +target_include_directories(test_fmt PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../../include") + +# Uncomment to test user-provided formatter +# target_compile_definitions(test_fmt PRIVATE +# LA_FMT_EIGEN_FORMATTER="${CMAKE_CURRENT_LIST_DIR}/user_fmt_formatter.h" +# ) + +catch_discover_tests(test_fmt) diff --git a/modules/core/tests/fmt/test_fmt.cpp b/modules/core/tests/fmt/test_fmt.cpp new file mode 100644 index 00000000..2c60852c --- /dev/null +++ b/modules/core/tests/fmt/test_fmt.cpp @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include "../../include/lagrange/utils/fmt_eigen.h" + +#include + +TEST_CASE("Format Vector", "[fmt]") +{ + Eigen::Vector3f v(1.f, 2.35f, 3.9999f); + spdlog::info("Vector: {}", v); +} + +TEST_CASE("Format Matrix", "[fmt]") +{ + spdlog::set_error_handler([](const std::string& msg) { throw std::runtime_error(msg); }); + + Eigen::Matrix3f test; + // clang-format off + test << 1.f, 2.75f, 3.19191919f, + -4.f, 5.17f, 6.f, + 7.f, 8.000000002f, 9.999999999f; + // clang-format on +#if FMT_VERSION >= 100200 + // This will format without error + spdlog::info("{:.2f}\n", test); +#elif (FMT_VERSION >= 100000) || (FMT_VERSION >= 90000 && !defined(FMT_DEPRECATED_OSTREAM)) || \ + (defined(LAGRANGE_FMT_EIGEN_FIX) && defined(_MSC_VER)) + // This should also format without error + spdlog::info("{:.2f}\n", test); +#else + // This will not compile with legacy ostream formatter. + REQUIRE_THROWS(spdlog::info("{:.2f}\n", test)); +#endif +} + +#if FMT_VERSION >= 100200 +// This test will only compile with the new nested formatter +TEST_CASE("Format Nested", "[fmt]") +{ + Eigen::Vector3 test; + // clang-format off + test.x() << 1.f, 2.75f, + 3.f, 4.999f; + test.y() << 5.f, 6.00001f, + -7.f, 8.17f; + test.z() << 9.f, 10.f, + 11.09f, 12.f; + // clang-format on + spdlog::info("{:.2f}\n", test); +} +#endif diff --git a/modules/core/tests/fmt/user_fmt_formatter.h b/modules/core/tests/fmt/user_fmt_formatter.h new file mode 100644 index 00000000..a2083ef9 --- /dev/null +++ b/modules/core/tests/fmt/user_fmt_formatter.h @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#pragma message("Using user-provided fmt::formatter<> for Eigen types") + +template +struct fmt::formatter, T>::value, char>> + : fmt::nested_formatter +{ + auto format(T const& a, format_context& ctx) const + { + return this->write_padded(ctx, [&](auto out) { + for (Eigen::Index ir = 0; ir < a.rows(); ir++) { + for (Eigen::Index ic = 0; ic < a.cols(); ic++) { + out = fmt::format_to(out, "{} ", this->nested(a(ir, ic))); + } + out = fmt::format_to(out, "\n"); + } + return out; + }); + } +}; diff --git a/modules/core/tests/test_initialize_edges.cpp b/modules/core/tests/test_initialize_edges.cpp new file mode 100644 index 00000000..6dd4fba6 --- /dev/null +++ b/modules/core/tests/test_initialize_edges.cpp @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include + +#include + +TEST_CASE("initialize_edges", "[core][!benchmark]") +{ + using Scalar = float; + using Index = uint32_t; + + auto mesh = lagrange::testing::load_surface_mesh("open/core/dragon.obj"); + + BENCHMARK_ADVANCED("initialize_edges") + (Catch::Benchmark::Chronometer meter) + { + auto copy = mesh; + meter.measure([&]() { + copy.initialize_edges(); + return copy.get_num_edges(); + }); + }; +} diff --git a/modules/core/tests/test_internal_angles.cpp b/modules/core/tests/test_internal_angles.cpp index 86f068bb..572520df 100644 --- a/modules/core/tests/test_internal_angles.cpp +++ b/modules/core/tests/test_internal_angles.cpp @@ -11,10 +11,10 @@ */ #include #include -#include #include +#include -#include +#include TEST_CASE("Internal angles - precision", "[core]") { diff --git a/modules/core/tests/test_logger.cpp b/modules/core/tests/test_logger.cpp index a8aa1671..1e734b33 100644 --- a/modules/core/tests/test_logger.cpp +++ b/modules/core/tests/test_logger.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ #include +#include // clang-format off #include @@ -32,3 +33,9 @@ TEST_CASE("Logger", "[next]") } lagrange::logger().debug("This should not appear"); } + +TEST_CASE("Logger Eigen", "[next]") +{ + Eigen::Vector3f v(1, 2, 3); + lagrange::logger().info("Vector: {}", v); +} diff --git a/modules/core/tests/test_mesh_convert.cpp b/modules/core/tests/test_mesh_convert.cpp index 964d9e88..94ad5c3e 100644 --- a/modules/core/tests/test_mesh_convert.cpp +++ b/modules/core/tests/test_mesh_convert.cpp @@ -710,7 +710,7 @@ void edge_sort_fast(std::vector>& edges) num_vertices = std::max(num_vertices, e[1] + 1); } std::vector buckets(num_vertices + 1); - auto ids = lagrange::mesh_convert_detail::fast_edge_sort( + auto ids = lagrange::internal::fast_edge_sort( num_edges, num_vertices, [&](int e) -> std::array { return edges[e]; }, diff --git a/modules/core/tests/test_remap_vertices.cpp b/modules/core/tests/test_remap_vertices.cpp index 8cf84e2d..caffaa42 100644 --- a/modules/core/tests/test_remap_vertices.cpp +++ b/modules/core/tests/test_remap_vertices.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -127,7 +128,7 @@ TEST_CASE("remap_vertices", "[core][surface][utilities]") mesh.initialize_edges(); std::vector old_to_new{0, 2, 1, 0}; - LA_REQUIRE_THROWS(remap_vertices(mesh, old_to_new)); + remap_vertices(mesh, old_to_new); } SECTION("Invalid ordering") @@ -195,10 +196,179 @@ TEST_CASE("remap_vertices", "[core][surface][utilities]") auto& attr = mesh.get_attribute(id); REQUIRE(attr.get_num_elements() == 4); - REQUIRE(attr.get(0) == 3); - REQUIRE(attr.get(1) == 2); - REQUIRE(attr.get(2) == 1); - REQUIRE(attr.get(3) == 0); + REQUIRE(attr.get(0) == 0); + REQUIRE(attr.get(1) == 1); + REQUIRE(attr.get(2) == 2); + REQUIRE(attr.get(3) == 3); } } } + +TEST_CASE("remap_vertices 6 vtx", "[core][surface][utilities]") +{ + using Scalar = double; + using Index = uint32_t; + + lagrange::SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({1, 1, 0}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(3, 4, 5); + + SECTION("merge one side - no edges") + { + std::vector old_to_new{0, 1, 2, 1, 2, 3}; + { + auto copy = mesh; + copy.initialize_edges(); + REQUIRE(copy.get_num_edges() == 6); + } + lagrange::remap_vertices(mesh, old_to_new); + mesh.initialize_edges(); + REQUIRE(mesh.get_num_vertices() == 4); + REQUIRE(mesh.get_num_edges() == 5); + REQUIRE(mesh.get_num_facets() == 2); + } + + SECTION("merge one side - with edges") + { + std::vector old_to_new{0, 1, 2, 1, 2, 3}; + // clang-format off + mesh.initialize_edges(std::vector{ + 0, 1, + 1, 2, + 2, 0, + 3, 4, + 4, 5, + 5, 3, + }); + // clang-format on + auto edge_index_id = mesh.create_attribute( + "edge_index", + lagrange::AttributeElement::Edge, + lagrange::AttributeUsage::EdgeIndex, + 1, + std::vector{0, 1, 2, 3, 4, 5}); + auto edge_scalar_id = mesh.create_attribute( + "edge_scalar", + lagrange::AttributeElement::Edge, + lagrange::AttributeUsage::Scalar, + 1, + std::vector{0, 1, 2, 3, 4, 5}); + auto v2e_index_id = mesh.create_attribute( + "v2e_index", + lagrange::AttributeElement::Vertex, + lagrange::AttributeUsage::EdgeIndex, + 1, + std::vector{0, 1, 2, 3, 4, 5}); + auto v2e_scalar_id = mesh.create_attribute( + "v2e_scalar", + lagrange::AttributeElement::Vertex, + lagrange::AttributeUsage::Scalar, + 1, + std::vector{0, 1, 2, 3, 4, 5}); + lagrange::remap_vertices(mesh, old_to_new); + REQUIRE(mesh.get_num_vertices() == 4); + REQUIRE(mesh.get_num_edges() == 5); + REQUIRE(mesh.get_num_facets() == 2); + auto edge_index = lagrange::attribute_vector_view(mesh, edge_index_id); + auto edge_scalar = lagrange::attribute_vector_view(mesh, edge_scalar_id); + auto v2e_index = lagrange::attribute_vector_view(mesh, v2e_index_id); + auto v2e_scalar = lagrange::attribute_vector_view(mesh, v2e_scalar_id); + Eigen::VectorX expected_edge_index(5); + Eigen::VectorX expected_edge_scalar(5); + Eigen::VectorX expected_v2e_index(4); + Eigen::VectorX expected_v2e_scalar(4); + expected_edge_index << 0, 1, 2, 3, 4; + expected_edge_scalar << 0, 2, 1, 5, 4; + expected_v2e_index << 0, 2, 1, 3; + expected_v2e_scalar << 0, 1, 2, 5; + REQUIRE(edge_index == expected_edge_index); + REQUIRE(edge_scalar == expected_edge_scalar); + REQUIRE(v2e_index == expected_v2e_index); + REQUIRE(v2e_scalar == expected_v2e_scalar); + } + + SECTION("merge two tris - no edges") + { + std::vector old_to_new{0, 1, 2, 1, 2, 0}; + { + auto copy = mesh; + copy.initialize_edges(); + REQUIRE(copy.get_num_edges() == 6); + } + lagrange::remap_vertices(mesh, old_to_new); + mesh.initialize_edges(); + REQUIRE(mesh.get_num_vertices() == 3); + REQUIRE(mesh.get_num_edges() == 3); + REQUIRE(mesh.get_num_facets() == 2); // duplicate facet! + lagrange::remove_duplicate_facets(mesh); + REQUIRE(mesh.get_num_facets() == 1); + auto V = vertex_view(mesh); + REQUIRE(V(0, 0) == 0.5); + REQUIRE(V(0, 1) == 0.5); + } + + SECTION("merge two tris - with edges") + { + std::vector old_to_new{0, 1, 2, 1, 2, 0}; + // clang-format off + mesh.initialize_edges(std::vector{ + 0, 1, + 1, 2, + 2, 0, + 3, 4, + 4, 5, + 5, 3, + }); + // clang-format on + auto edge_index_id = mesh.create_attribute( + "edge_index", + lagrange::AttributeElement::Edge, + lagrange::AttributeUsage::EdgeIndex, + 1, + std::vector{0, 1, 2, 3, 4, 5}); + auto edge_scalar_id = mesh.create_attribute( + "edge_scalar", + lagrange::AttributeElement::Edge, + lagrange::AttributeUsage::Scalar, + 1, + std::vector{0, 1, 2, 3, 4, 5}); + auto v2e_index_id = mesh.create_attribute( + "v2e_index", + lagrange::AttributeElement::Vertex, + lagrange::AttributeUsage::EdgeIndex, + 1, + std::vector{0, 1, 2, 3, 4, 5}); + auto v2e_scalar_id = mesh.create_attribute( + "v2e_scalar", + lagrange::AttributeElement::Vertex, + lagrange::AttributeUsage::Scalar, + 1, + std::vector{0, 1, 2, 3, 4, 5}); + lagrange::remap_vertices(mesh, old_to_new); + REQUIRE(mesh.get_num_vertices() == 3); + REQUIRE(mesh.get_num_edges() == 3); + REQUIRE(mesh.get_num_facets() == 2); // duplicate facet! + auto edge_index = lagrange::attribute_vector_view(mesh, edge_index_id); + auto edge_scalar = lagrange::attribute_vector_view(mesh, edge_scalar_id); + auto v2e_index = lagrange::attribute_vector_view(mesh, v2e_index_id); + auto v2e_scalar = lagrange::attribute_vector_view(mesh, v2e_scalar_id); + Eigen::VectorX expected_edge_index(3); + Eigen::VectorX expected_edge_scalar(3); + Eigen::VectorX expected_v2e_index(3); + Eigen::VectorX expected_v2e_scalar(3); + expected_edge_index << 0, 1, 2; + expected_edge_scalar << 0, 2, 1; + expected_v2e_index << 0, 2, 1; + expected_v2e_scalar << 0, 1, 2; + REQUIRE(edge_index == expected_edge_index); + REQUIRE(edge_scalar == expected_edge_scalar); + REQUIRE(v2e_index == expected_v2e_index); + REQUIRE(v2e_scalar == expected_v2e_scalar); + } +} diff --git a/modules/core/tests/test_select_facets_in_frustum.cpp b/modules/core/tests/test_select_facets_in_frustum.cpp index 32e4bb09..2de899e2 100644 --- a/modules/core/tests/test_select_facets_in_frustum.cpp +++ b/modules/core/tests/test_select_facets_in_frustum.cpp @@ -17,10 +17,15 @@ #include #include +#include + namespace { + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + template -void run() +void run_legacy() { using namespace lagrange; @@ -236,8 +241,240 @@ void run() } } } +#endif + +template +void run() +{ + using namespace lagrange; + SECTION("rectangle SurfaceMesh") + { + // 2 +-----+ 3 + // |\ | + // | \ | + // | \| + // 0 +-----+ 1 + using Index = uint32_t; + SurfaceMesh surface_mesh; + surface_mesh.add_vertex({0, 0, 0}); + surface_mesh.add_vertex({1, 0, 0}); + surface_mesh.add_vertex({0, 1, 0}); + surface_mesh.add_vertex({1, 1, 0}); + surface_mesh.add_triangle(0, 1, 2); + surface_mesh.add_triangle(2, 1, 3); + + auto select_point = [&](Scalar x, Scalar y, Scalar margin = 0.1) { + // Frustum frustum{ + // {{ + // { {{1, 0, 0}}, {{x - margin, 0, 0}} }, + // { {{-1, 0, 0}}, {{x + margin, 0, 0}} }, + // { {{0, 1, 0}}, {{0, y - margin, 0}} }, + // { {{0, -1, 0}}, {{0, y + margin, 0}} } + // }} + // }; + // Below is equivalent to the above (many) braces initialization + typename Frustum::Plane plane0{{1, 0, 0}, {x - margin, 0, 0}}; + typename Frustum::Plane plane1{{-1, 0, 0}, {x + margin, 0, 0}}; + typename Frustum::Plane plane2{{0, 1, 0}, {0, y - margin, 0}}; + typename Frustum::Plane plane3{{0, -1, 0}, {0, y + margin, 0}}; + Frustum frustum{{plane0, plane1, plane2, plane3}}; + + select_facets_in_frustum(surface_mesh, frustum, FrustumSelectionOptions()); + }; + + SECTION("select all") + { + Frustum frustum{ + {{{{{1, 0, 0}}, {{-1, 0, 0}}}, + {{{-1, 0, 0}}, {{2, 0, 0}}}, + {{{0, 1, 0}}, {{0, -1, 0}}}, + {{{0, -1, 0}}, {{0, 2, 0}}}}}}; + + select_facets_in_frustum(surface_mesh, frustum, FrustumSelectionOptions()); + + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); // does this work? + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] > 0); + REQUIRE(attr_ref[1] > 0); + } + + SECTION("select none") + { + select_facets_in_frustum( + surface_mesh, + Frustum{ + {{{{{1, 0, 0}}, {{1.1f, 0, 0}}}, + {{{-1, 0, 0}}, {{2, 0, 0}}}, + {{{0, 1, 0}}, {{0, -1, 0}}}, + {{{0, -1, 0}}, {{0, 2, 0}}}}}}); + + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] == 0); + REQUIRE(attr_ref[1] == 0); + } + + SECTION("select none again") + { + select_facets_in_frustum( + surface_mesh, + Frustum{ + {{{{{1, 0, 0}}, {{2, 0, 0}}}, + {{{-1, 0, 0}}, {{-1, 0, 0}}}, + {{{0, 1, 0}}, {{0, 2, 0}}}, + {{{0, -1, 0}}, {{0, -1, 0}}}}}}); + + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] == 0); + REQUIRE(attr_ref[1] == 0); + } + + SECTION("select none 3") + { + select_facets_in_frustum( + surface_mesh, + Frustum{ + {{{{{1, 0, 0}}, {{-1, 0, 0}}}, + {{{-1, 0, 0}}, {{2, 0, 0}}}, + {{{0, 0, 1}}, {{0, 0, 0.5f}}}, + {{{0, 0, -1}}, {{0, 0, 1}}}}}}); + + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] == 0); + REQUIRE(attr_ref[1] == 0); + } + + SECTION("select all again") + { + select_facets_in_frustum( + surface_mesh, + Frustum{ + {{{{{1, 0, 0}}, {{0.4f, 0, 0}}}, + {{{-1, 0, 0}}, {{0.6f, 0, 0}}}, + {{{0, 0, 1}}, {{0, 0, -0.1f}}}, + {{{0, 0, -1}}, {{0, 0, 0.1f}}}}}}); + + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] > 0); + REQUIRE(attr_ref[1] > 0); + } + + SECTION("select just the origin") + { + select_point(0, 0); + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] > 0); + REQUIRE(attr_ref[1] == 0); + } + + SECTION("select (1, 1)") + { + select_point(1, 1); + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] == 0); + REQUIRE(attr_ref[1] > 0); + } + + SECTION("select (0, 1)") + { + select_point(0, 1); + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] > 0); + REQUIRE(attr_ref[1] > 0); + } + + SECTION("select (1, 0)") + { + select_point(1, 0); + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] > 0); + REQUIRE(attr_ref[1] > 0); + } + + SECTION("select (0.5, 0.5)") + { + select_point(0.5, 0.5); + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] > 0); + REQUIRE(attr_ref[1] > 0); + } + + SECTION("select (0.25, 0.25)") + { + select_point(0.25, 0.25); + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] > 0); + REQUIRE(attr_ref[1] == 0); + } + + SECTION("select (0.75, 0.75)") + { + select_point(0.75, 0.75); + REQUIRE(surface_mesh.has_attribute("@is_selected")); + + const auto& attr = surface_mesh.template get_attribute("@is_selected"); + REQUIRE(attr.get_num_elements() == 2); + const auto& attr_ref = attr.get_all(); + REQUIRE(attr_ref[0] == 0); + REQUIRE(attr_ref[1] > 0); + } + } +} } // namespace +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +TEST_CASE("legacy::select_facets_in_frustum", "[select_facets_in_frustum]") +{ + run_legacy(); +} +TEST_CASE("legacy::select_facets_in_frustum", "[select_facets_in_frustum]") +{ + run_legacy(); +} +#endif + TEST_CASE("select_facets_in_frustum", "[select_facets_in_frustum]") { run(); @@ -245,4 +482,4 @@ TEST_CASE("select_facets_in_frustum", "[select_facets_in_frustum]") TEST_CASE("select_facets_in_frustum", "[select_facets_in_frustum]") { run(); -} +} \ No newline at end of file diff --git a/modules/io/include/lagrange/io/types.h b/modules/io/include/lagrange/io/types.h index e307a808..70604453 100644 --- a/modules/io/include/lagrange/io/types.h +++ b/modules/io/include/lagrange/io/types.h @@ -14,6 +14,7 @@ #include #include #include +#include #include @@ -74,13 +75,27 @@ struct SaveOptions */ struct LoadOptions { + LA_IGNORE_DEPRECATION_WARNING_BEGIN + + LoadOptions() = default; + LoadOptions(LoadOptions&&) = default; + LoadOptions& operator=(LoadOptions&&) = default; + LoadOptions(const LoadOptions&) = default; + LoadOptions& operator=(const LoadOptions&) = default; + + /// Load object ids as facet attribute + [[deprecated("Use load_object_ids instead")]] bool load_object_id = true; + + LA_IGNORE_DEPRECATION_WARNING_END + + /// Triangulate any polygonal facet with > 3 vertices bool triangulate = false; /// Load vertex normals bool load_normals = true; - /// Load tangents and bitangent + /// Load tangents and bitangents bool load_tangents = true; /// Load texture coordinates @@ -95,8 +110,8 @@ struct LoadOptions /// Load vertex colors as vertex attribute bool load_vertex_colors = true; - /// Load object id as facet attribute - bool load_object_id = true; + /// Load object ids as facet attribute + bool load_object_ids = true; /// Load external images bool load_images = true; diff --git a/modules/io/python/src/io.cpp b/modules/io/python/src/io.cpp index 03288043..ed08087d 100644 --- a/modules/io/python/src/io.cpp +++ b/modules/io/python/src/io.cpp @@ -65,7 +65,7 @@ void populate_io_module(nb::module_& m) .def_rw("load_weights", &io::LoadOptions::load_weights) .def_rw("load_materials", &io::LoadOptions::load_materials) .def_rw("load_vertex_colors", &io::LoadOptions::load_vertex_colors) - .def_rw("load_object_id", &io::LoadOptions::load_object_id) + .def_rw("load_object_ids", &io::LoadOptions::load_object_ids) .def_rw("search_path", &io::LoadOptions::search_path); //.def_rw("extension_converters", &io::LoadOptions::extension_converters); @@ -132,17 +132,53 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl m.def( "load_mesh", - [](const fs::path& filename, bool triangulate) { + [](const fs::path& filename, + bool triangulate, + bool load_normals, + bool load_tangents, + bool load_uvs, + bool load_weights, + bool load_materials, + bool load_vertex_colors, + bool load_object_ids, + bool load_images, + const fs::path& search_path) { io::LoadOptions opts; opts.triangulate = triangulate; + opts.load_normals = load_normals; + opts.load_tangents = load_tangents; + opts.load_uvs = load_uvs; + opts.load_weights = load_weights; + opts.load_materials = load_materials; + opts.load_vertex_colors = load_vertex_colors; + opts.load_object_ids = load_object_ids; + opts.load_images = load_images; + opts.search_path = search_path; return io::load_mesh(filename, opts); }, "filename"_a, - "triangulate"_a = false, + "triangulate"_a = io::LoadOptions().triangulate, + "load_normals"_a = io::LoadOptions().load_normals, + "load_tangents"_a = io::LoadOptions().load_tangents, + "load_uvs"_a = io::LoadOptions().load_uvs, + "load_weights"_a = io::LoadOptions().load_weights, + "load_materials"_a = io::LoadOptions().load_materials, + "load_vertex_colors"_a = io::LoadOptions().load_vertex_colors, + "load_object_ids"_a = io::LoadOptions().load_object_ids, + "load_images"_a = io::LoadOptions().load_images, + "search_path"_a = io::LoadOptions().search_path, R"(Load mesh from a file. -:param filename: The input file name. -:param triangulate: Whether to triangulate the mesh if it is not already triangulated. Defaults to False. +:param filename: The input file name. +:param triangulate: Whether to triangulate the mesh if it is not already triangulated. Defaults to False. +:param load_normals: Whether to load vertex normals from mesh if available. Defaults to True. +:param load_tangents: Whether to load tangents and bitangents from mesh if available. Defaults to True. +:param load_uvs: Whether to load texture coordinates from mesh if available. Defaults to True. +:param load_weights: Whether to load skinning weights attributes from mesh if available. Defaults to True. +:param load_materials: Whether to load material ids from mesh if available. Defaults to True. +:param load_vertex_colors: Whether to load vertex colors from mesh if available. Defaults to True. +:param load_object_id: Whether to load object ids from mesh if available. Defaults to True. +:param load_images: Whether to load external images if available. Defaults to True. :return SurfaceMesh: The mesh object extracted from the input string.)"); diff --git a/modules/io/python/tests/test_io.py b/modules/io/python/tests/test_io.py index ddd04814..08e16925 100644 --- a/modules/io/python/tests/test_io.py +++ b/modules/io/python/tests/test_io.py @@ -62,9 +62,7 @@ def assert_same_vertices_and_facets(mesh, mesh2): def match_attribute(mesh, mesh2, id1, id2): attr_name = "__unit_test__" # Convert both attributes to corner attribute and compare the per-corner value. - id1 = lagrange.map_attribute( - mesh, id1, attr_name, lagrange.AttributeElement.Corner - ) + id1 = lagrange.map_attribute(mesh, id1, attr_name, lagrange.AttributeElement.Corner) id2 = lagrange.map_attribute( mesh2, id2, attr_name, lagrange.AttributeElement.Corner ) @@ -130,12 +128,8 @@ def __save_and_load__( required = not exact_match # Check special attributes. - assert_same_attribute( - mesh, mesh2, lagrange.AttributeUsage.UV, required - ) - assert_same_attribute( - mesh, mesh2, lagrange.AttributeUsage.Normal, required - ) + assert_same_attribute(mesh, mesh2, lagrange.AttributeUsage.UV, required) + assert_same_attribute(mesh, mesh2, lagrange.AttributeUsage.Normal, required) if filename.suffix != ".obj": assert_same_attribute( mesh, mesh2, lagrange.AttributeUsage.Color, required @@ -178,7 +172,7 @@ def save_and_load(self, mesh, attribute_ids=None): self.__save_and_load__( ext, mesh, - binary=(ext==".glb"), + binary=(ext == ".glb"), exact_match=exact_match, selected_attributes=attribute_ids, as_scene=True, @@ -197,9 +191,7 @@ def test_single_triangle(self, triangle): mesh = triangle self.save_and_load(mesh) - def test_single_triangle_with_attributes( - self, triangle_with_uv_normal_color - ): + def test_single_triangle_with_attributes(self, triangle_with_uv_normal_color): mesh = triangle_with_uv_normal_color self.save_and_load(mesh) diff --git a/modules/io/src/load_mesh.cpp b/modules/io/src/load_mesh.cpp index 4d67d2b2..0df07203 100644 --- a/modules/io/src/load_mesh.cpp +++ b/modules/io/src/load_mesh.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +64,8 @@ MeshType load_mesh(const fs::path& filename, const LoadOptions& options) return load_mesh_msh(filename, options); } else if (ext == ".gltf" || ext == ".glb") { return load_mesh_gltf(filename, options); + } else if (ext == ".fbx") { + return load_mesh_fbx(filename, options); } else { #ifdef LAGRANGE_WITH_ASSIMP return load_mesh_assimp(filename, options); diff --git a/modules/io/src/load_mesh_ply.cpp b/modules/io/src/load_mesh_ply.cpp index 8fb5ca55..abf94bc0 100644 --- a/modules/io/src/load_mesh_ply.cpp +++ b/modules/io/src/load_mesh_ply.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -303,13 +304,20 @@ MeshType load_mesh_ply(std::istream& input_stream, const LoadOptions& options) happly::Element& vertex_element = ply.getElement("vertex"); const Index num_vertices = Index(vertex_element.count); { - const std::vector& xPos = vertex_element.getProperty("x"); - const std::vector& yPos = vertex_element.getProperty("y"); - const std::vector& zPos = vertex_element.getProperty("z"); + // Use double to ensure we can always load in the data with Happly's type promotion. + const std::vector& xPos = vertex_element.getProperty("x"); + const std::vector& yPos = vertex_element.getProperty("y"); + const std::vector& zPos = vertex_element.getProperty("z"); + if constexpr (std::is_same_v) { + if (vertex_element.hasPropertyType("x")) { + logger().warn("Loading PLY file with scalar type float but the file contains " + "double precision data. This may result in loss of precision."); + } + } mesh.add_vertices(num_vertices, [&](Index v, span p) -> void { - p[0] = xPos[v]; - p[1] = yPos[v]; - p[2] = zPos[v]; + p[0] = static_cast(xPos[v]); + p[1] = static_cast(yPos[v]); + p[2] = static_cast(zPos[v]); }); } diff --git a/modules/io/src/load_obj.cpp b/modules/io/src/load_obj.cpp index 7929bc94..01b7edee 100644 --- a/modules/io/src/load_obj.cpp +++ b/modules/io/src/load_obj.cpp @@ -182,7 +182,7 @@ ObjReaderResult extract_mes // Initialize object id Attribute* id_attr = nullptr; - if (options.load_object_id) { + if (options.load_object_ids) { auto id = mesh.template create_attribute( AttributeName::object_id, AttributeElement::Facet, @@ -253,7 +253,7 @@ ObjReaderResult extract_mes if (num_invalid_uv) { logger().warn( "Found {} vertices without UV indices. UV attribute will have invalid values.", - num_invalid_uv); + num_invalid_uv.load()); } logger().trace("[load_mesh_obj] Loading complete"); diff --git a/modules/io/src/save_mesh_obj.cpp b/modules/io/src/save_mesh_obj.cpp index ddbb9f89..e46ecbda 100644 --- a/modules/io/src/save_mesh_obj.cpp +++ b/modules/io/src/save_mesh_obj.cpp @@ -71,7 +71,8 @@ void save_mesh_obj( std::string found_uv_name; std::string found_nrm_name; const Attribute* uv_indices = nullptr; - const Attribute* nrm_indices = nullptr; + span nrm_indices; + std::vector nrm_index_buffer; seq_foreach_named_attribute_read(mesh, [&](std::string_view name, auto&& attr) { using AttributeType = std::decay_t; @@ -124,10 +125,30 @@ void save_mesh_obj( const Attribute* values = nullptr; if constexpr (AttributeType::IsIndexed) { values = &attr.values(); - nrm_indices = &attr.indices(); - } else { + nrm_indices = attr.indices().get_all(); + } else if (attr.get_element_type() == AttributeElement::Vertex) { + values = &attr; + nrm_indices = mesh.get_corner_to_vertex().get_all(); + } else if (attr.get_element_type() == AttributeElement::Facet) { values = &attr; - nrm_indices = &mesh.get_corner_to_vertex(); + nrm_index_buffer.resize(mesh.get_num_corners()); + for (Index ci = 0; ci < mesh.get_num_corners(); ci++) { + nrm_index_buffer[ci] = mesh.get_corner_facet(ci); + } + nrm_indices = nrm_index_buffer; + } else if (attr.get_element_type() == AttributeElement::Corner) { + values = &attr; + nrm_index_buffer.resize(mesh.get_num_corners()); + for (Index ci = 0; ci < mesh.get_num_corners(); ci++) { + nrm_index_buffer[ci] = ci; + } + nrm_indices = nrm_index_buffer; + } else { + logger().warn( + "Skipping normal attribute '{}' due to unsupported element type", + found_nrm_name); + found_nrm_name.clear(); + return; } la_runtime_assert(attr.get_num_channels() == 3); for (Index vn = 0; vn < values->get_num_elements(); ++vn) { @@ -149,14 +170,14 @@ void save_mesh_obj( // vertex_index/texture_index/normal_index Index v = vtx_indices[lv] + 1; Index vt = (uv_indices ? uv_indices->get(first_corner + lv) : 0) + 1; - Index vn = (nrm_indices ? nrm_indices->get(first_corner + lv) : 0) + 1; - if (!uv_indices && !nrm_indices) { + Index vn = (!nrm_indices.empty() ? nrm_indices[first_corner + lv] : 0) + 1; + if (!uv_indices && nrm_indices.empty()) { fmt::print(output_stream, " {}", v); - } else if (uv_indices && !nrm_indices) { + } else if (uv_indices && nrm_indices.empty()) { fmt::print(output_stream, " {}/{}", v, vt); - } else if (uv_indices && nrm_indices) { + } else if (uv_indices && !nrm_indices.empty()) { fmt::print(output_stream, " {}/{}/{}", v, vt, vn); - } else if (!uv_indices && nrm_indices) { + } else if (!uv_indices && !nrm_indices.empty()) { fmt::print(output_stream, " {}//{}", v, vn); } } diff --git a/modules/python/lagrange/_logging.py b/modules/python/lagrange/_logging.py index 6b8c860a..fc589d0e 100644 --- a/modules/python/lagrange/_logging.py +++ b/modules/python/lagrange/_logging.py @@ -16,9 +16,7 @@ logger = logging.getLogger("lagrange") handler = logging.StreamHandler() -formatter = logging.Formatter( - "[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s" -) +formatter = logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s") class ColorFormatter(logging.Formatter): diff --git a/modules/scene/include/lagrange/scene/SimpleScene.h b/modules/scene/include/lagrange/scene/SimpleScene.h index af44e2dd..1b14db71 100644 --- a/modules/scene/include/lagrange/scene/SimpleScene.h +++ b/modules/scene/include/lagrange/scene/SimpleScene.h @@ -131,6 +131,19 @@ class LA_SCENE_API SimpleScene return m_instances[mesh_index][instance_index]; } + /// + /// Get a reference to a mesh instance in the scene. + /// + /// @param[in] mesh_index Index of the parent mesh in the scene. + /// @param[in] instance_index Local instance index respective to the parent mesh. + /// + /// @return Reference to the specified mesh instance. + /// + InstanceType& ref_instance(Index mesh_index, Index instance_index) + { + return m_instances[mesh_index][instance_index]; + } + /// /// Pre-allocate a number of meshes in the scene. /// diff --git a/modules/scene/include/lagrange/scene/simple_scene_convert.h b/modules/scene/include/lagrange/scene/simple_scene_convert.h index 1492405c..2de4d951 100644 --- a/modules/scene/include/lagrange/scene/simple_scene_convert.h +++ b/modules/scene/include/lagrange/scene/simple_scene_convert.h @@ -52,6 +52,8 @@ SimpleScene meshes_to_simple_scene( /// /// Converts a scene into a concatenated mesh with all the transforms applied. /// +/// @todo Add option to flip facets when transform has negative determinant. +/// /// @param[in] scene Scene to convert. /// @param[in] transform_options Options to use when applying mesh transformations. /// @param[in] preserve_attributes Preserve shared attributes and map them to the output mesh. diff --git a/modules/scene/python/scripts/extract_texture.py b/modules/scene/python/scripts/extract_texture.py new file mode 100755 index 00000000..311725bf --- /dev/null +++ b/modules/scene/python/scripts/extract_texture.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python + +# +# Copyright 2024 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# + +"""Extract texture from a mesh""" + +import argparse +import lagrange +from pathlib import Path +from PIL import Image + + +def parse_args(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", help="input mesh file") + parser.add_argument("output", help="output mesh file") + return parser.parse_args() + + +def dump_texture(img, filename): + img.uri = filename + img_buffer = img.image + buffer = img_buffer.data.reshape( + (img_buffer.height, img_buffer.width, img_buffer.num_channels) + ) + if img_buffer.num_channels == 4: + im = Image.fromarray(buffer, "RGBA") + elif img_buffer.num_channels == 3: + im = Image.fromarray(buffer, "RGB") + elif img_buffer.num_channels == 1: + im = Image.fromarray(buffer, "L") + else: + raise ValueError(f"Unsupported number of channels: {img_buffer.num_channels}") + im.save(filename) + + +def main(): + args = parse_args() + scene = lagrange.io.load_scene(args.input) + + output_filename = Path(args.output) + basename = output_filename.stem + texture_count = 0 + for node in scene.nodes: + for instance in node.meshes: + assert instance.mesh != lagrange.invalid_index + for mat_id in instance.materials: + mat = scene.materials[mat_id] + if mat.base_color_texture.index != lagrange.invalid_index: + tex = scene.textures[mat.base_color_texture.index] + assert tex.image != lagrange.invalid_index + img = scene.images[tex.image] + if len(img.image.data) != 0: + texture_filename = output_filename.with_suffix( + ".png" + ).with_stem(f"{basename}_{texture_count:03}") + dump_texture(img, texture_filename) + texture_count += 1 + + lagrange.io.save_scene(args.output, scene) + + +if __name__ == "__main__": + main() diff --git a/modules/subdivision/src/TopologyRefinerFactory.h b/modules/subdivision/src/TopologyRefinerFactory.h index 0166b1bd..fcfd2de7 100644 --- a/modules/subdivision/src/TopologyRefinerFactory.h +++ b/modules/subdivision/src/TopologyRefinerFactory.h @@ -81,7 +81,7 @@ bool TopologyRefinerFactory::assignComponentTags( const auto& options = conv.options; if (options.edge_sharpness_attr.has_value()) { - lagrange::internal::visit_attribute( + lagrange::internal::visit_attribute_read( mesh, options.edge_sharpness_attr.value(), [&](auto&& attr) { @@ -126,7 +126,7 @@ bool TopologyRefinerFactory::assignComponentTags( } if (options.vertex_sharpness_attr.has_value()) { - lagrange::internal::visit_attribute( + lagrange::internal::visit_attribute_read( mesh, options.vertex_sharpness_attr.value(), [&](auto&& attr) { @@ -153,7 +153,7 @@ bool TopologyRefinerFactory::assignComponentTags( } if (options.face_hole_attr.has_value()) { - lagrange::internal::visit_attribute( + lagrange::internal::visit_attribute_read( mesh, options.face_hole_attr.value(), [&](auto&& attr) { @@ -195,7 +195,7 @@ bool TopologyRefinerFactory::assignFaceVaryingTopology( // TODO: Only define one fvar channel for each different set of indices (factorize shared set of // indices)? for (lagrange::AttributeId attr_id : conv.face_varying_attributes) { - lagrange::internal::visit_attribute(mesh, attr_id, [&](auto&& attr) { + lagrange::internal::visit_attribute_read(mesh, attr_id, [&](auto&& attr) { using AttributeType = std::decay_t; if constexpr (!AttributeType::IsIndexed) { la_runtime_assert( diff --git a/modules/subdivision/src/mesh_subdivision.cpp b/modules/subdivision/src/mesh_subdivision.cpp index ba1b4f2d..1fbf3d72 100644 --- a/modules/subdivision/src/mesh_subdivision.cpp +++ b/modules/subdivision/src/mesh_subdivision.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ #include +#include #include #include @@ -555,7 +556,7 @@ SurfaceMesh subdivide_mesh( // Interpolate per-vertex data (including vertex positions) auto interpolate_attribute = [&](AttributeId id, bool smooth) { - lagrange::internal::visit_attribute(input_mesh, id, [&](auto&& attr) { + lagrange::internal::visit_attribute_read(input_mesh, id, [&](auto&& attr) { using AttributeType = std::decay_t; using ValueType = typename AttributeType::ValueType; if constexpr (!(std::is_same_v || @@ -620,7 +621,7 @@ SurfaceMesh subdivide_mesh( // Interpolate face-varying data (such as UVs) int fvar_index = 0; for (auto id : interpolated_attr.face_varying_attributes) { - lagrange::internal::visit_attribute(input_mesh, id, [&](auto&& attr) { + lagrange::internal::visit_attribute_read(input_mesh, id, [&](auto&& attr) { using AttributeType = std::decay_t; using ValueType = typename AttributeType::ValueType; if constexpr (!(std::is_same_v || @@ -679,7 +680,7 @@ SurfaceMesh subdivide_mesh( } #define LA_X_subdivide_mesh(_, Scalar, Index) \ - template SurfaceMesh subdivide_mesh( \ + template LA_SUBDIVISION_API SurfaceMesh subdivide_mesh( \ const SurfaceMesh& mesh, \ SubdivisionOptions options); LA_SURFACE_MESH_X(subdivide_mesh, 0) diff --git a/modules/subdivision/src/midpoint_subdivision.cpp b/modules/subdivision/src/midpoint_subdivision.cpp index d6c1f1f5..33429cc1 100644 --- a/modules/subdivision/src/midpoint_subdivision.cpp +++ b/modules/subdivision/src/midpoint_subdivision.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ #include +#include #include #include @@ -58,7 +59,7 @@ SurfaceMesh midpoint_subdivision(const SurfaceMesh } #define LA_X_midpoint_subdivision(_, Scalar, Index) \ - template SurfaceMesh midpoint_subdivision( \ + template LA_SUBDIVISION_API SurfaceMesh midpoint_subdivision( \ const SurfaceMesh& mesh); LA_SURFACE_MESH_X(midpoint_subdivision, 0) diff --git a/modules/subdivision/src/sqrt_subdivision.cpp b/modules/subdivision/src/sqrt_subdivision.cpp index b7ca4de5..226936b3 100644 --- a/modules/subdivision/src/sqrt_subdivision.cpp +++ b/modules/subdivision/src/sqrt_subdivision.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ #include +#include #include #include @@ -110,7 +111,7 @@ SurfaceMesh sqrt_subdivision(const SurfaceMesh& me } #define LA_X_sqrt_subdivision(_, Scalar, Index) \ - template SurfaceMesh sqrt_subdivision(const SurfaceMesh& mesh); + template LA_SUBDIVISION_API SurfaceMesh sqrt_subdivision(const SurfaceMesh& mesh); LA_SURFACE_MESH_X(sqrt_subdivision, 0) } // namespace lagrange::subdivision diff --git a/modules/ui/README.md b/modules/ui/README.md index 06221530..213c52d1 100644 --- a/modules/ui/README.md +++ b/modules/ui/README.md @@ -53,7 +53,7 @@ Lagrange UI uses an **Entity-Component-System (ECS)** architecture: - Components define data and behavior (but no logic) - Systems define logic (but no data). -See *[ECS implementation section](#entity-component-system)* for more information about ECS and how it's implemented in Lagrange UI. The underlying library for ECS is [`entt`](https://github.com/skypjack/entt). +See *[ECS implementation section](#entity-component-system)* for more information about ECS and how it's implemented in Lagrange UI. The underlying library for ECS is [`entt`](https://github.com/skypjack/entt). Recommended namespace usage @@ -72,7 +72,7 @@ viewer.run([](){ //Or viewer.run([](ui::Registry & r){ - //Main loop code + //Main loop code return should_continue_running; }); ``` @@ -108,7 +108,7 @@ registry.emplace(entity, MyPositionComponent(0,0,0)); ### Loading mesh -Creates and entity that represents the mesh. This entity is only a resource - it is not rendered. +Creates and entity that represents the mesh. This entity is only a resource - it is not rendered. It can be referenced by components that need this geometry for rendering/picking/etc. These entities have `MeshData` component attached that contains a `lagrange::MeshBase` pointer. @@ -213,7 +213,7 @@ colormap_coolwarm