Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d94d5688 | ||
|
|
e073141f14 | ||
|
|
f54e535c6a | ||
|
|
c6801c82ea | ||
|
|
a67aa13133 | ||
|
|
d43709715d | ||
|
|
2ddbd0a1a0 | ||
|
|
dc4afa950b | ||
|
|
c03a5ac3ad | ||
|
|
cae5a01af1 | ||
|
|
d59b9b93bd | ||
|
|
f3c5ada9f0 | ||
|
|
4395ab622c | ||
|
|
994956d7c3 | ||
|
|
79f735c59a | ||
|
|
dd67e992bd | ||
|
|
5bdb8bc233 | ||
|
|
c9ae400647 | ||
|
|
9c1517e54e | ||
|
|
3b8282e1d5 | ||
|
|
1635864cbc | ||
|
|
65b2ef8198 | ||
|
|
a9055a8abf | ||
|
|
0b7ee87489 | ||
|
|
9690305b8d | ||
|
|
aa88959899 | ||
|
|
dbb0ca76b4 | ||
|
|
9018b2ac45 | ||
|
|
f407e43e5b | ||
|
|
d7cbcb8c2e | ||
|
|
e27c9c100f | ||
|
|
8ad5f8e427 | ||
|
|
afc64f248a | ||
|
|
c0fb3766e4 | ||
|
|
887a5021af | ||
|
|
e83b9bd158 | ||
|
|
2d0c705839 | ||
|
|
b36ec2ce0e | ||
|
|
b8d143dd72 | ||
|
|
be022a58e4 | ||
|
|
30e0af942a | ||
|
|
8c77a6aa05 | ||
|
|
54fa822408 | ||
|
|
7d564cfbc1 | ||
|
|
bc0bded294 | ||
|
|
d52decc4f1 | ||
|
|
8060376e12 | ||
|
|
eba866d266 | ||
|
|
cc4966f062 | ||
|
|
3216a24be8 | ||
|
|
5e566c8c36 | ||
|
|
80edfee978 | ||
|
|
0e0d1c4f69 | ||
|
|
e15f4cf375 | ||
|
|
c9322ca63a | ||
|
|
45826d6c47 | ||
|
|
dd781cdbdf | ||
|
|
638badaf9b | ||
|
|
0149e482bc | ||
|
|
f683e8c9c0 | ||
|
|
1f8e8037f1 | ||
|
|
10a15c1dd4 | ||
|
|
6e1629ddce | ||
|
|
e273f6b09d | ||
|
|
67c468211d | ||
|
|
01c38e42ca | ||
|
|
1991232eb7 | ||
|
|
4551288f95 | ||
|
|
2e29784449 | ||
|
|
2a2cb6f88e | ||
|
|
1a91d8db2a | ||
|
|
82b98d9353 | ||
|
|
7273c9bf0f | ||
|
|
4ff3f7cf68 | ||
|
|
fafe4792da | ||
|
|
b439603eb9 | ||
|
|
b6eea01021 | ||
|
|
b9f7228064 | ||
|
|
0ef5f70434 | ||
|
|
bb6acf1f6e | ||
|
|
5930b5c09d | ||
|
|
a842de97c6 | ||
|
|
2942229b49 |
@@ -1,6 +1,9 @@
|
||||
name: build_wheels
|
||||
|
||||
on: [release, workflow_dispatch]
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_wheels:
|
||||
@@ -13,13 +16,14 @@ jobs:
|
||||
- run: |
|
||||
git config --global submodule.fetchJobs 8
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build wheels
|
||||
uses: pypa/cibuildwheel@v2.15.0
|
||||
- uses: actions/upload-artifact@v3
|
||||
uses: pypa/cibuildwheel@v2.16
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-${{ matrix.os }}
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
make_sdist:
|
||||
@@ -28,17 +32,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Optional, use if you use setuptools_scm
|
||||
submodules: true # Optional, use if you have submodules
|
||||
|
||||
submodules: recursive
|
||||
- name: Build SDist
|
||||
run: pipx run build --sdist
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-sdist
|
||||
path: dist/*.tar.gz
|
||||
|
||||
pypi-publish-beta:
|
||||
pypi-publish:
|
||||
name: Upload release to PyPI
|
||||
needs: [build_wheels, make_sdist]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -48,9 +50,10 @@ jobs:
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifact
|
||||
pattern: python-*
|
||||
merge-multiple: true
|
||||
path: dist
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
@@ -11,11 +11,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DoozyX/clang-format-lint-action@v0.14
|
||||
with:
|
||||
source: '.'
|
||||
exclude: '*/third_party'
|
||||
extensions: 'h,cpp,js,ts,html'
|
||||
clangFormatVersion: 12
|
||||
|
||||
clangFormatVersion: 11
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
options: "--check --verbose"
|
||||
src: "./bindings/python/examples"
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Doxygen Action
|
||||
uses: mattnotmitt/doxygen-action@v1
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
mv ./docs ./public
|
||||
|
||||
- name: Download built examples
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: manifold.yml
|
||||
workflow_conclusion: completed
|
||||
|
||||
@@ -22,18 +22,18 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y libglm-dev libgtest-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip lcov
|
||||
pip install trimesh
|
||||
- uses: actions/checkout@v3
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y libglm-dev libgtest-dev libassimp-dev git libtbb-dev libthrust-dev pkg-config libpython3-dev python3 python3-distutils python3-pip lcov
|
||||
pip install trimesh pytest
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- name: Build ${{matrix.backend}}
|
||||
if: matrix.parallel_backend != 'NONE'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
- name: Test ${{matrix.parallel_backend}}
|
||||
if: matrix.parallel_backend != 'NONE'
|
||||
run: |
|
||||
@@ -42,6 +42,7 @@ jobs:
|
||||
./manifold_test
|
||||
cd ../../
|
||||
python3 bindings/python/examples/run_all.py -e
|
||||
python3 -m pytest
|
||||
- name: Coverage Report
|
||||
# only do code coverage for default sequential backend, it seems that TBB
|
||||
# backend will cause failure
|
||||
@@ -58,12 +59,19 @@ jobs:
|
||||
lcov --capture --directory . --output-file ./code_coverage_test.info
|
||||
lcov --add-tracefile ./code_coverage_init.info --add-tracefile ./code_coverage_test.info --output-file ./code_coverage_total.info
|
||||
lcov --remove ./code_coverage_total.info '/usr/*' '*/third_party/*' '*/test/*' '*/extras/*' '*/bindings/*' --output-file ./code_coverage.info
|
||||
- uses: codecov/codecov-action@v2
|
||||
cd ../
|
||||
- uses: codecov/codecov-action@v4
|
||||
if: matrix.parallel_backend == 'NONE'
|
||||
with:
|
||||
files: build/code_coverage.info
|
||||
fail_ci_if_error: false
|
||||
name: ${{matrix.parallel_backend}}
|
||||
- name: test cmake consumer
|
||||
run: |
|
||||
cd build
|
||||
sudo cmake --install .
|
||||
cd ..
|
||||
./test-cmake.sh
|
||||
|
||||
build_cbind:
|
||||
timeout-minutes: 30
|
||||
@@ -74,15 +82,15 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y libgtest-dev libglm-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- name: Build C bindings with TBB
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=TBB .. && make
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=TBB .. && make
|
||||
- name: Test ${{matrix.parallel_backend}}
|
||||
run: |
|
||||
cd build/test
|
||||
@@ -97,7 +105,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs libglm-dev
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup WASM
|
||||
@@ -105,9 +113,9 @@ jobs:
|
||||
# setup emscripten
|
||||
git clone https://github.com/emscripten-core/emsdk.git
|
||||
cd emsdk
|
||||
./emsdk install latest
|
||||
./emsdk activate latest
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
./emsdk install 3.1.45
|
||||
./emsdk activate 3.1.45
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- name: Build WASM
|
||||
run: |
|
||||
source ./emsdk/emsdk_env.sh
|
||||
@@ -124,12 +132,14 @@ jobs:
|
||||
npm ci
|
||||
npm run build
|
||||
npm test
|
||||
cp ../manifold.* ./dist/
|
||||
- name: Upload WASM files
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wasm
|
||||
path: bindings/wasm/examples/dist/
|
||||
retention-days: 90
|
||||
overwrite: true
|
||||
|
||||
build_windows:
|
||||
timeout-minutes: 30
|
||||
@@ -140,22 +150,29 @@ jobs:
|
||||
runs-on: windows-2019
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- name: Build ${{matrix.backend}}
|
||||
shell: powershell
|
||||
run: |
|
||||
cmake . -DCMAKE_BUILD_TYPE=Release -B build -DMANIFOLD_DEBUG=ON -DPYBIND11_FINDPYTHON=OFF -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64
|
||||
cmake . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64 -B build
|
||||
cd build
|
||||
cmake --build . --target ALL_BUILD --config Release
|
||||
- name: Test ${{matrix.parallel_backend}}
|
||||
shell: bash
|
||||
run: |
|
||||
cd build/test
|
||||
./Release/manifold_test.exe
|
||||
cd build/bin/Release
|
||||
./manifold_test.exe
|
||||
cd ../../
|
||||
- name: test cmake consumer
|
||||
run: |
|
||||
cd build
|
||||
cmake --install .
|
||||
cd ..
|
||||
./test-cmake.sh
|
||||
|
||||
build_mxe:
|
||||
timeout-minutes: 30
|
||||
@@ -166,7 +183,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container: openscad/mxe-x86_64-gui:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build
|
||||
@@ -190,23 +207,30 @@ jobs:
|
||||
- name: Install common dependencies
|
||||
run: |
|
||||
brew install pkg-config googletest assimp
|
||||
pip install trimesh
|
||||
pip install trimesh pytest
|
||||
- name: Install TBB
|
||||
if: matrix.parallel_backend == 'TBB'
|
||||
run: brew install tbb
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
- name: Test
|
||||
run: |
|
||||
cd build/test
|
||||
./manifold_test
|
||||
cd ../../
|
||||
- name: test cmake consumer
|
||||
run: |
|
||||
cd build
|
||||
sudo cmake --install .
|
||||
cd ..
|
||||
./test-cmake.sh
|
||||
|
||||
build_nix:
|
||||
timeout-minutes: 30
|
||||
@@ -216,7 +240,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: cachix/install-nix-action@v22
|
||||
@@ -227,7 +251,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: cachix/install-nix-action@v22
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: publish_npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish_npm:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Download built examples
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: manifold.yml
|
||||
workflow_conclusion: completed
|
||||
name: wasm
|
||||
path: ./bindings/wasm/
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
cd ./bindings/wasm/
|
||||
npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
@@ -1,6 +1,3 @@
|
||||
[submodule "src/third_party/clipper2"]
|
||||
path = src/third_party/clipper2
|
||||
url = https://github.com/AngusJohnson/clipper2
|
||||
[submodule "src/third_party/quickhull"]
|
||||
path = src/third_party/quickhull
|
||||
url = https://github.com/akuukka/quickhull
|
||||
|
||||
@@ -108,7 +108,12 @@
|
||||
"latch": "cpp",
|
||||
"semaphore": "cpp",
|
||||
"span": "cpp",
|
||||
"__sso_allocator": "cpp"
|
||||
"__sso_allocator": "cpp",
|
||||
"source_location": "cpp",
|
||||
"format": "cpp",
|
||||
"numbers": "cpp",
|
||||
"ranges": "cpp",
|
||||
"stop_token": "cpp"
|
||||
},
|
||||
"C_Cpp.clang_format_fallbackStyle": "google",
|
||||
"editor.formatOnSave": true,
|
||||
@@ -117,7 +122,6 @@
|
||||
"-DMANIFOLD_CBIND=ON",
|
||||
"-DMANIFOLD_EXPORT=ON",
|
||||
"-DMANIFOLD_DEBUG=ON",
|
||||
"-DMANIFOLD_USE_CUDA=OFF",
|
||||
"-DMANIFOLD_PAR=TBB",
|
||||
"-DCODE_COVERAGE=OFF",
|
||||
"-DCMAKE_CXX_FLAGS=''" //'-fsanitize=address,undefined'"
|
||||
|
||||
@@ -21,11 +21,26 @@ set(MANIFOLD_PAR "NONE" CACHE STRING "Parallel backend, either \"TBB\" or \"NONE
|
||||
set(MANIFOLD_FLAGS "" CACHE STRING "Manifold compiler flags")
|
||||
option(MANIFOLD_EXPORT "Build mesh export (via assimp) utility library" OFF)
|
||||
option(MANIFOLD_TEST "Enable testing suite" ON)
|
||||
# fuzztest is a rather large dependency
|
||||
option(MANIFOLD_FUZZ "Enable fuzzing tests" OFF)
|
||||
option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF)
|
||||
|
||||
option(MANIFOLD_PYBIND "Build python bindings" ON)
|
||||
option(MANIFOLD_CBIND "Build C (FFI) bindings" OFF)
|
||||
option(MANIFOLD_PYBIND "Build python bindings" OFF)
|
||||
option(MANIFOLD_CBIND "Build C (FFI) bindings" ON)
|
||||
option(MANIFOLD_JSBIND "Build js binding" ${EMSCRIPTEN})
|
||||
option(BUILD_SHARED_LIBS "Build shared library" ON)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
if(MANIFOLD_FUZZ)
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
message(FATAL_ERROR "fuzztest only supports clang" )
|
||||
endif()
|
||||
# we should enable debug checks
|
||||
set(MANIFOLD_DEBUG ON)
|
||||
# enable fuzztest fuzzing mode
|
||||
set(FUZZTEST_FUZZING_MODE ON)
|
||||
# address sanitizer required
|
||||
set(CMAKE_CXX_FLAGS "$CMAKE_CXX_FLAGS -fsanitize=address")
|
||||
endif()
|
||||
|
||||
option(TRACY_ENABLE "Use tracy profiling" OFF)
|
||||
option(TRACY_MEMORY_USAGE "Track memory allocation with tracy (expensive)" OFF)
|
||||
@@ -47,15 +62,18 @@ if(EMSCRIPTEN)
|
||||
set(MANIFOLD_FLAGS -fexceptions -D_LIBCUDACXX_HAS_THREAD_API_EXTERNAL -D_LIBCUDACXX_HAS_THREAD_API_CUDA)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -sALLOW_MEMORY_GROWTH=1 -fexceptions -sDISABLE_EXCEPTION_CATCHING=0")
|
||||
set(MANIFOLD_PYBIND OFF)
|
||||
set(BUILD_SHARED_LIBS OFF)
|
||||
endif()
|
||||
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/manifoldDeps.cmake)
|
||||
|
||||
if(MANIFOLD_PYBIND)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if(Python_VERSION VERSION_LESS 3.12)
|
||||
find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
|
||||
else()
|
||||
find_package(Python COMPONENTS Interpreter Development.SABIModule REQUIRED)
|
||||
endif()
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
endif()
|
||||
|
||||
if(CMAKE_EXPORT_COMPILE_COMMANDS AND NOT EMSCRIPTEN)
|
||||
@@ -68,7 +86,8 @@ if (MSVC)
|
||||
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} /DNOMINMAX)
|
||||
else()
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused -Wno-array-bounds -Wno-stringop-overflow)
|
||||
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused -Wno-array-bounds
|
||||
-Wno-stringop-overflow -Wno-alloc-size-larger-than)
|
||||
else()
|
||||
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
|
||||
endif()
|
||||
@@ -81,7 +100,16 @@ if(CODE_COVERAGE AND NOT MSVC)
|
||||
add_link_options("-coverage")
|
||||
endif()
|
||||
|
||||
include(GNUInstallDirs)
|
||||
# installation rpath
|
||||
set(CMAKE_SKIP_BUILD_RPATH FALSE)
|
||||
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
|
||||
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
|
||||
list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/lib" isSystemDir)
|
||||
if("${isSystemDir}" STREQUAL "-1")
|
||||
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
endif("${isSystemDir}" STREQUAL "-1")
|
||||
|
||||
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(bindings)
|
||||
@@ -95,3 +123,29 @@ if(MANIFOLD_TEST)
|
||||
add_subdirectory(test)
|
||||
add_subdirectory(extras)
|
||||
endif()
|
||||
|
||||
# installation related
|
||||
|
||||
set(MANIFOLD_VERSION_MAJOR 2)
|
||||
set(MANIFOLD_VERSION_MINOR 4)
|
||||
set(MANIFOLD_VERSION_PATCH 5)
|
||||
set(MANIFOLD_VERSION "${MANIFOLD_VERSION_MAJOR}.${MANIFOLD_VERSION_MINOR}.${MANIFOLD_VERSION_PATCH}")
|
||||
set_property(TARGET manifold PROPERTY VERSION "${MANIFOLD_VERSION}")
|
||||
set_property(TARGET manifold PROPERTY SOVERSION 2)
|
||||
|
||||
if(NOT EMSCRIPTEN)
|
||||
include(CMakePackageConfigHelpers)
|
||||
write_basic_package_version_file(
|
||||
${CMAKE_BINARY_DIR}/cmake/manifoldConfigVersion.cmake
|
||||
VERSION ${MANIFOLD_VERSION}
|
||||
COMPATIBILITY SameMajorVersion
|
||||
)
|
||||
install(EXPORT manifoldTargets DESTINATION ${CMAKE_INSTALL_DATADIR}/manifold)
|
||||
configure_file(manifoldConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/manifoldConfig.cmake @ONLY)
|
||||
install(FILES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manifoldDeps.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/cmake/manifoldConfigVersion.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/manifoldConfig.cmake
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/manifold
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
[](https://codecov.io/github/elalish/manifold)
|
||||
[](https://badge.fury.io/py/manifold3d)
|
||||
[](https://badge.fury.io/js/manifold-3d)
|
||||
[](https://twitter.com/intent/follow?screen_name=manifoldcad)
|
||||
|
||||
## Users
|
||||
|
||||
[OpenSCAD](https://openscad.org/), [IFCjs](https://ifcjs.github.io/info/), [Grid.Space](https://grid.space/), and [OCADml](https://github.com/OCADml/OManifold) have all integrated our Manifold geometry kernel! Why? Because its reliability is guaranteed and it's 1,000 times faster than other libraries. See our [usage](https://github.com/elalish/manifold/discussions/340) and [performance](https://github.com/elalish/manifold/discussions/383) discussions for all the latest and to add your own projects & analyses.
|
||||
|
||||
For example, here is a log-log plot of Manifold's performance vs. earlier OpenSCAD geometry backends:
|
||||
## Manifold Frontend Sandboxes
|
||||
|
||||
<img src="https://elalish.github.io/manifold/samples/models/perfSpheres.png" width="350px"/>
|
||||
### [ManifoldCAD.org](https://manifoldcad.org)
|
||||
|
||||
## [ManifoldCAD.org](https://manifoldcad.org)
|
||||
If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid modelling web app where you script in JS/TS. This uses our npm package, [manifold-3d](https://www.npmjs.com/package/manifold-3d), built via WASM. It's not quite as fast as our raw C++, but it's hard to beat for interoperability.
|
||||
|
||||
If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid modelling web app. Our WASM is not multithreaded yet, but it's still quite fast and a good way to test out our Manifold library.
|
||||
*Note for Firefox users: If you find the editor is stuck on **Loading...**, setting
|
||||
`dom.workers.modules.enabled: true` in your `about:config`, as mentioned in
|
||||
[issue#328](https://github.com/elalish/manifold/issues/328#issuecomment-1473847102)
|
||||
may solve the problem.*
|
||||
|
||||
### [Python Colab Example](https://colab.research.google.com/drive/1VxrFYHPSHZgUbl9TeWzCeovlpXrPQ5J5?usp=sharing)
|
||||
|
||||
If you prefer Python to JS/TS, make your own copy of the example notebook above. It demonstrates interop between our [`manifold3d`](https://pypi.org/project/manifold3d/) PyPI library and the popular [`trimesh`](https://pypi.org/project/trimesh/) library, including showing the interactive model right in the notebook and saving 3D model output.
|
||||
|
||||

|
||||
|
||||
### Note for Firefox users
|
||||
|
||||
If you find the editor is stuck on **Loading...**, setting
|
||||
`dom.workers.modules.enabled: true` in your `about:config`, as mentioned in the
|
||||
discussion of the
|
||||
[issue#328](https://github.com/elalish/manifold/issues/328#issuecomment-1473847102)
|
||||
of this repository may solve the problem.
|
||||
|
||||
# Manifold
|
||||
|
||||
[**API Documentation**](https://elalish.github.io/manifold/docs/html/modules.html) | [**Algorithm Documentation**](https://github.com/elalish/manifold/wiki/Manifold-Library) | [**Blog Posts**](https://elalish.blogspot.com/search/label/Manifold) | [**Web Examples**](https://elalish.github.io/manifold/model-viewer.html)
|
||||
[**API Documentation**](https://elalish.github.io/manifold/docs/html/topics.html) | [**Algorithm Documentation**](https://github.com/elalish/manifold/wiki/Manifold-Library) | [**Blog Posts**](https://elalish.blogspot.com/search/label/Manifold) | [**Web Examples**](https://elalish.github.io/manifold/model-viewer.html)
|
||||
|
||||
[Manifold](https://github.com/elalish/manifold) is a geometry library dedicated to creating and operating on manifold triangle meshes. A [manifold mesh](https://github.com/elalish/manifold/wiki/Manifold-Library#manifoldness) is a mesh that represents a solid object, and so is very important in manufacturing, CAD, structural analysis, etc. Further information can be found on the [wiki](https://github.com/elalish/manifold/wiki/Manifold-Library).
|
||||
|
||||
This is a modern C++ library that Github's CI verifies builds and runs on a variety of platforms. Additionally, we build bindings for JavaScript ([manifold-3d](https://www.npmjs.com/package/manifold-3d) on npm), Python, and C to make this library more portable and easy to use.
|
||||
This is a modern C++ library that Github's CI verifies builds and runs on a variety of platforms. Additionally, we build bindings for JavaScript ([manifold-3d](https://www.npmjs.com/package/manifold-3d) on npm), Python ([manifold3d](https://pypi.org/project/manifold3d/)), and C to make this library more portable and easy to use.
|
||||
|
||||
System Dependencies (note that we will automatically download the dependency if there is no such package on the system):
|
||||
- [`GLM`](https://github.com/g-truc/glm/): A compact header-only vector library.
|
||||
@@ -87,6 +88,10 @@ Offline building:
|
||||
The build instructions used by our CI are in [manifold.yml](https://github.com/elalish/manifold/blob/master/.github/workflows/manifold.yml), which is a good source to check if something goes wrong and for instructions specific to other platforms, like Windows.
|
||||
|
||||
### WASM
|
||||
|
||||
> Note that we have only tested emscripten version 3.1.45. It is known that
|
||||
3.1.48 has some issues compiling manifold.
|
||||
|
||||
To build the JS WASM library, first install NodeJS and set up emscripten:
|
||||
|
||||
(on Mac):
|
||||
@@ -120,7 +125,7 @@ use the extension, please add `$BUILD_DIR/bindings/python` to your `PYTHONPATH`,
|
||||
can be found in `bindings/python/examples`. To see exported samples, run:
|
||||
```
|
||||
sudo apt install pkg-config libpython3-dev python3 python3-distutils python3-pip
|
||||
pip install trimesh
|
||||
pip install trimesh pytest
|
||||
python3 run_all.py -e
|
||||
```
|
||||
|
||||
@@ -134,14 +139,45 @@ python binding documentation:
|
||||
|
||||
For more detailed documentation, please refer to the C++ API.
|
||||
|
||||
### Windows Shenanigans
|
||||
|
||||
Windows users should build with `-DBUILD_SHARED_LIBS=OFF`, as enabling shared
|
||||
libraries in general makes things very complicated.
|
||||
|
||||
The DLL file for manifoldc (C FFI bindings) when built with msvc is in `${CMAKE_BINARY_DIR}/bin/${BUILD_TYPE}/manifoldc.dll`.
|
||||
For example, for the following command, the path relative to the project root directory is `build/bin/Release/manifoldc.dll`.
|
||||
```sh
|
||||
cmake . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64 -B build
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! A lower barrier contribution is to simply make a PR that adds a test, especially if it repros an issue you've found. Simply name it prepended with DISABLED_, so that it passes the CI. That will be a very strong signal to me to fix your issue. However, if you know how to fix it yourself, then including the fix in your PR would be much appreciated!
|
||||
|
||||
## Profiling
|
||||
### Formatting
|
||||
|
||||
There is a formatting script `format.sh` that automatically formats everything.
|
||||
It requires clang-format 11 and black formatter for python.
|
||||
|
||||
If you have clang-format installed but without clang-11, you can specify the
|
||||
clang-format executable by setting the `CLANG_FORMAT` environment variable.
|
||||
|
||||
### Profiling
|
||||
|
||||
There is now basic support for the [Tracy profiler](https://github.com/wolfpld/tracy) for our tests.
|
||||
To enable tracing, compile with `-DTRACY_ENABLE=on` cmake option, and run the test with Tracy server running.
|
||||
To enable memory profiling in addition to tracing, compile with `-DTRACY_MEMORY_USAGE=ON` in addition to `-DTRACY_ENABLE=ON`.
|
||||
|
||||
### Fuzzing Support
|
||||
|
||||
We use https://github.com/google/fuzztest for fuzzing the triangulator.
|
||||
|
||||
To enable fuzzing, make sure that you are using clang compiler (`-DCMAKE_CXX_COMPILER=clang -DCMAKE_C_COMPILER=clang`), running Linux, and enable fuzzing support by setting `-DMANIFOLD_FUZZ=ON`.
|
||||
|
||||
To run the fuzzer and minimize testcase, do
|
||||
```
|
||||
../minimizer.sh ./test/polygon_fuzz --fuzz=PolygonFuzz.TriangulationNoCrash
|
||||
```
|
||||
|
||||
## About the author
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
1. Go through Github issues to verify bugs have been fixed and closed.
|
||||
1. Verify [ManifoldCAD.org](https://manifoldcad.org) - check a few examples, run them, download a GLB and a 3MF.
|
||||
1. Verify our three.js [example](https://manifoldcad.org/three) is functional.
|
||||
1. Verify our model-viewer [example](https://manifoldcad.org/model-viewer) is functional - select Union and Intersection.
|
||||
1. Verify [make-manifold](https://manifoldcad.org/make-manifold) is functional. Try dropping [DragonAttenuation.glb](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/DragonAttenuation/glTF-Binary/DragonAttenuation.glb) in and verify you can select "View Manifold GLB" and that the dragon is still present while the backdrop is removed. Download the GLB.
|
||||
1. Make a new branch called the version, e.g. v2.3.0.
|
||||
1. Use VSCode to search and replace the old version with the new - so far in test-cmake.sh, flake.nix, pyproject.toml, and package.json.
|
||||
1. Also update CMakeLists.txt version by searching for "set(MANIFOLD_VERSION_".
|
||||
1. Commit, push, open a PR, verify tests pass, merge.
|
||||
1. On Github, draft a new release, make a new tag with the version number, add release notes, and publish.
|
||||
1. Check the Actions and verify that both PyPI and npm publishing actions ran successfully.
|
||||
1. Verify the npm [package](https://www.npmjs.com/package/manifold-3d?activeTab=code) looks good - unpacked size should be close to 1MB.
|
||||
1. Verify PyPI [package](https://pypi.org/project/manifold3d/#files) looks good - a bunch of built distributions ranging from ~600kB to ~1.1MB.
|
||||
@@ -1,33 +1,31 @@
|
||||
project(manifoldc)
|
||||
|
||||
add_library(${PROJECT_NAME} manifoldc.cpp conv.cpp)
|
||||
# this is a shared library for FFI bindings
|
||||
add_library(${PROJECT_NAME} SHARED manifoldc.cpp conv.cpp)
|
||||
|
||||
if(MANIFOLD_USE_CUDA)
|
||||
set_source_files_properties(manifoldc.cpp conv.cpp PROPERTIES LANGUAGE CUDA)
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
|
||||
if(MSVC)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
|
||||
${CMAKE_BINARY_DIR}/bin )
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
${CMAKE_BINARY_DIR}/bin )
|
||||
endif()
|
||||
|
||||
if(MANIFOLD_EXPORT)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE meshIO)
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC -DMANIFOLD_EXPORT)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE meshIO)
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC -DMANIFOLD_EXPORT)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE manifold sdf cross_section
|
||||
${PROJECT_NAME}
|
||||
PRIVATE manifold sdf cross_section
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT bindings
|
||||
)
|
||||
install(
|
||||
FILES include/manifoldc.h include/types.h
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/manifoldc.h include/types.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
|
||||
@@ -111,6 +111,12 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
|
||||
float normal_x, float normal_y,
|
||||
float normal_z, float offset);
|
||||
|
||||
// 3D to 2D
|
||||
|
||||
ManifoldCrossSection *manifold_slice(void *mem, ManifoldManifold *m,
|
||||
float height);
|
||||
ManifoldCrossSection *manifold_project(void *mem, ManifoldManifold *m);
|
||||
|
||||
// Convex Hulls
|
||||
|
||||
ManifoldManifold *manifold_hull(void *mem, ManifoldManifold *m);
|
||||
@@ -134,6 +140,8 @@ ManifoldManifold *manifold_mirror(void *mem, ManifoldManifold *m, float nx,
|
||||
ManifoldManifold *manifold_warp(void *mem, ManifoldManifold *m,
|
||||
ManifoldVec3 (*fun)(float, float, float));
|
||||
ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine);
|
||||
ManifoldManifold *manifold_refine_to_length(void *mem, ManifoldManifold *m,
|
||||
float length);
|
||||
|
||||
// Manifold Shapes / Constructors
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ namespace {
|
||||
ManifoldMeshGL *level_set(void *mem, float (*sdf)(float, float, float),
|
||||
ManifoldBox *bounds, float edge_length, float level,
|
||||
bool seq) {
|
||||
// typing with std::function rather than auto compiles when CUDA is on,
|
||||
// passing it into GPU (and crashing) is avoided dynamically in `sdf.h`
|
||||
std::function<float(glm::vec3)> fun = [sdf](glm::vec3 v) {
|
||||
return (sdf(v.x, v.y, v.z));
|
||||
};
|
||||
@@ -202,6 +200,17 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
|
||||
return to_c(new (mem) Manifold(trimmed));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_slice(void *mem, ManifoldManifold *m,
|
||||
float height) {
|
||||
auto poly = from_c(m)->Slice(height);
|
||||
return to_c(new (mem) CrossSection(poly));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_project(void *mem, ManifoldManifold *m) {
|
||||
auto poly = from_c(m)->Project();
|
||||
return to_c(new (mem) CrossSection(poly));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_hull(void *mem, ManifoldManifold *m) {
|
||||
auto hulled = from_c(m)->Hull();
|
||||
return to_c(new (mem) Manifold(hulled));
|
||||
@@ -296,6 +305,12 @@ ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine) {
|
||||
return to_c(new (mem) Manifold(refined));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_refine_to_length(void *mem, ManifoldManifold *m,
|
||||
float length) {
|
||||
auto refined = from_c(m)->RefineToLength(length);
|
||||
return to_c(new (mem) Manifold(refined));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_empty(void *mem) {
|
||||
return to_c(new (mem) Manifold());
|
||||
}
|
||||
|
||||
@@ -18,12 +18,31 @@ add_subdirectory(third_party)
|
||||
nanobind_add_module(
|
||||
manifold3d
|
||||
NB_STATIC STABLE_ABI LTO
|
||||
autogen_docstrings.inl
|
||||
manifold3d.cpp)
|
||||
target_link_libraries(manifold3d PRIVATE manifold)
|
||||
target_link_libraries(manifold3d PRIVATE manifold sdf polygon)
|
||||
target_compile_options(manifold3d PRIVATE ${MANIFOLD_FLAGS} -DMODULE_NAME=manifold3d)
|
||||
target_compile_features(manifold3d PUBLIC cxx_std_17)
|
||||
set_target_properties(manifold3d PROPERTIES OUTPUT_NAME "manifold3d")
|
||||
|
||||
message(Python_EXECUTABLE = ${Python_EXECUTABLE})
|
||||
# ideally we should generate a dependency file from python...
|
||||
set(DOCSTRING_DEPS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/manifold.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/constructors.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/sort.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/cross_section/src/cross_section.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/polygon/src/polygon.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/utilities/include/public.h
|
||||
)
|
||||
add_custom_command(
|
||||
OUTPUT autogen_docstrings.inl
|
||||
DEPENDS ${DOCSTRING_DEPS}
|
||||
COMMAND ${Python_EXECUTABLE} ARGS ${CMAKE_CURRENT_SOURCE_DIR}/gen_docs.py
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
target_include_directories(manifold3d PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
if(SKBUILD)
|
||||
install(
|
||||
TARGETS manifold3d
|
||||
@@ -33,7 +52,7 @@ install(
|
||||
else()
|
||||
install(
|
||||
TARGETS manifold3d
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
LIBRARY DESTINATION ${Python_SITEARCH}
|
||||
COMPONENT bindings
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Python Bindings
|
||||
|
||||
## Autogenerated Doc-Strings
|
||||
|
||||
Doc-strings for the python API wrapper are generated by gen_docs.py
|
||||
The script is run automatically during the build to keep python
|
||||
doc strings fully up-to-date with c++ sources.
|
||||
|
||||
It scrapes documentation comments and c++ function signatures
|
||||
from the manifold c++ sources, in order to generate a c++ header file
|
||||
exposing the comments as string variables named by function names.
|
||||
This allows python bindings to re-use the c++ comments directly.
|
||||
|
||||
Some snake-casing of params is applied for python use case.
|
||||
|
||||
---
|
||||
|
||||
When modifying the Manifold C++ sources, you may need to update
|
||||
gen_docs.py. For example, top-level free functions are white-listed,
|
||||
so if you add a new one, you will need to add it in gen_docs.py.
|
||||
|
||||
Similarly, the list of source files to parse is also white listed,
|
||||
so if you define functions in new files that need python wrappers,
|
||||
you will also need to up gen_docs.py.
|
||||
|
||||
To verify that python docs are correct after changes, you can
|
||||
run the following commends from the manifold repo root:
|
||||
```
|
||||
pip install .
|
||||
python -c 'import manifold3d; help(manifold3d)'
|
||||
```
|
||||
|
||||
Alternateively you could generate stubs with roughly the same info
|
||||
```
|
||||
pip install nanobind-stubgen
|
||||
pip install .
|
||||
nanobind-stubgen manifold3d
|
||||
```
|
||||
It will emit some warnings and write a file `manifold3d.pyi`
|
||||
which will show all the function signatures and docstrings.
|
||||
@@ -0,0 +1,122 @@
|
||||
from manifold3d import *
|
||||
import numpy as np
|
||||
|
||||
|
||||
def all_root_level():
|
||||
set_min_circular_angle(10)
|
||||
set_min_circular_edge_length(1)
|
||||
set_circular_segments(22)
|
||||
n = get_circular_segments(1)
|
||||
assert n == 22
|
||||
poly = [[0, 0], [1, 0], [1, 1]]
|
||||
tris = triangulate([poly])
|
||||
tris = triangulate([np.array(poly)])
|
||||
|
||||
|
||||
def all_cross_section():
|
||||
poly = [[0, 0], [1, 0], [1, 1]]
|
||||
c = CrossSection([np.array(poly)])
|
||||
c = CrossSection([poly])
|
||||
c = CrossSection() + c
|
||||
a = c.area()
|
||||
c = CrossSection.batch_boolean(
|
||||
[
|
||||
CrossSection.circle(1),
|
||||
CrossSection.square((3, 3)),
|
||||
CrossSection.circle(3).translate((1, 1)),
|
||||
],
|
||||
OpType.Add,
|
||||
)
|
||||
c = CrossSection.batch_hull([c, c.translate((1, 0))])
|
||||
b = c.bounds()
|
||||
c = CrossSection.circle(1)
|
||||
c = CrossSection.compose([c, c.translate((1, 0))])
|
||||
cs = c.decompose()
|
||||
m = c.extrude(1)
|
||||
c = c.hull()
|
||||
c = CrossSection.hull_points(poly)
|
||||
c = CrossSection.hull_points(np.array(poly))
|
||||
e = c.is_empty()
|
||||
c = c.mirror((0, 1))
|
||||
n = c.num_contour()
|
||||
n = c.num_vert()
|
||||
c = c.offset(1, JoinType.Round)
|
||||
m = c.revolve()
|
||||
c = c.rotate(90)
|
||||
c = c.scale((2, 2))
|
||||
c = c.simplify()
|
||||
c = CrossSection.square((1, 1))
|
||||
p = c.to_polygons()
|
||||
c = c.transform([[1, 0, 0], [0, 1, 0]])
|
||||
c = c.translate((1, 1))
|
||||
c = c.warp(lambda p: (p[0] + 1, p[1] / 2))
|
||||
c = c.warp_batch(lambda ps: ps * [1, 0.5] + [1, 0])
|
||||
|
||||
|
||||
def all_manifold():
|
||||
mesh = Manifold.sphere(1).to_mesh()
|
||||
m = Manifold(mesh)
|
||||
m = Manifold() + m
|
||||
m = m.as_original()
|
||||
m = Manifold.batch_boolean(
|
||||
[
|
||||
Manifold.cylinder(4, 1),
|
||||
Manifold.cube((3, 2, 1)),
|
||||
Manifold.cylinder(5, 3).translate((1, 1, 1)),
|
||||
],
|
||||
OpType.Add,
|
||||
)
|
||||
m = Manifold.batch_hull([m, m.translate((0, 0, 1))])
|
||||
b = m.bounding_box()
|
||||
m = m.calculate_curvature(4, 5)
|
||||
m = Manifold.compose([m, m.translate((5, 0, 0))])
|
||||
m = Manifold.cube((1, 1, 1))
|
||||
m = Manifold.cylinder(1, 1)
|
||||
ms = m.decompose()
|
||||
m = Manifold.extrude(CrossSection.circle(1), 1)
|
||||
m = Manifold.revolve(CrossSection.circle(1))
|
||||
g = m.genus()
|
||||
a = m.surface_area()
|
||||
v = m.volume()
|
||||
m = m.hull()
|
||||
m = m.hull_points(mesh.vert_properties)
|
||||
e = m.is_empty()
|
||||
m = m.mirror((0, 0, 1))
|
||||
n = m.num_edge()
|
||||
n = m.num_prop()
|
||||
n = m.num_prop_vert()
|
||||
n = m.num_tri()
|
||||
n = m.num_vert()
|
||||
i = m.original_id()
|
||||
p = m.precision()
|
||||
c = m.project()
|
||||
m = m.refine(2)
|
||||
m = m.refine_to_length(0.1)
|
||||
i = Manifold.reserve_ids(1)
|
||||
m = m.scale((1, 2, 3))
|
||||
m = m.set_properties(3, lambda pos, prop: pos)
|
||||
c = m.slice(0.5)
|
||||
m = Manifold.smooth(mesh, [0], [0.5])
|
||||
m = Manifold.sphere(1)
|
||||
m, n = m.split(m.translate((1, 0, 0)))
|
||||
m, n = m.split_by_plane((0, 0, 1), 0)
|
||||
e = m.status()
|
||||
m = Manifold.tetrahedron()
|
||||
mesh = m.to_mesh()
|
||||
ok = mesh.merge()
|
||||
m = m.transform([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]])
|
||||
m = m.translate((0, 0, 0))
|
||||
m = m.trim_by_plane((0, 0, 1), 0)
|
||||
m = m.warp(lambda p: (p[0] + 1, p[1] / 2, p[2] * 2))
|
||||
m = m.warp_batch(lambda ps: ps * [1, 0.5, 2] + [1, 0, 0])
|
||||
|
||||
|
||||
def run():
|
||||
all_root_level()
|
||||
all_cross_section()
|
||||
all_manifold()
|
||||
return Manifold()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -20,60 +20,82 @@ from manifold3d import Manifold
|
||||
|
||||
INCHES = 25.4
|
||||
|
||||
brick_depth = (3 + 5/8) * INCHES
|
||||
brick_height = (2 + 1/4) * INCHES
|
||||
brick_length = (7 + 5/8) * INCHES
|
||||
brick_depth = (3 + 5 / 8) * INCHES
|
||||
brick_height = (2 + 1 / 4) * INCHES
|
||||
brick_length = (7 + 5 / 8) * INCHES
|
||||
|
||||
mortar_gap = (3/8) * INCHES
|
||||
mortar_gap = (3 / 8) * INCHES
|
||||
|
||||
|
||||
def brick():
|
||||
return Manifold.cube(brick_length, brick_depth, brick_height)
|
||||
return Manifold.cube([brick_length, brick_depth, brick_height])
|
||||
|
||||
|
||||
def halfbrick():
|
||||
return Manifold.cube((brick_length - mortar_gap)/2, brick_depth, brick_height)
|
||||
return Manifold.cube([(brick_length - mortar_gap) / 2, brick_depth, brick_height])
|
||||
|
||||
|
||||
def row(length):
|
||||
bricks = [brick().translate((brick_length + mortar_gap)*x, 0, 0)
|
||||
for x in range(length)]
|
||||
return Manifold(bricks)
|
||||
bricks = [
|
||||
brick().translate([(brick_length + mortar_gap) * x, 0, 0])
|
||||
for x in range(length)
|
||||
]
|
||||
return sum(bricks, Manifold())
|
||||
|
||||
|
||||
def wall(length, height, alternate=0):
|
||||
bricks = [row(length).translate(
|
||||
((z+alternate) % 2)*(brick_depth+mortar_gap), 0,
|
||||
(brick_height+mortar_gap)*z) for z in range(height)]
|
||||
return Manifold(bricks)
|
||||
bricks = [
|
||||
row(length).translate(
|
||||
[
|
||||
((z + alternate) % 2) * (brick_depth + mortar_gap),
|
||||
0,
|
||||
(brick_height + mortar_gap) * z,
|
||||
]
|
||||
)
|
||||
for z in range(height)
|
||||
]
|
||||
return sum(bricks, Manifold())
|
||||
|
||||
|
||||
def walls(length, width, height):
|
||||
return Manifold([
|
||||
wall(length, height),
|
||||
wall(width, height, 1).rotate(0, 0, 90).translate(brick_depth, 0, 0),
|
||||
wall(length, height, 1).translate(
|
||||
0, (width)*(brick_length+mortar_gap), 0),
|
||||
wall(width, height).rotate(0, 0, 90)
|
||||
.translate((length+0.5)*(brick_length+mortar_gap) - mortar_gap, 0, 0),
|
||||
])
|
||||
return sum(
|
||||
[
|
||||
wall(length, height),
|
||||
wall(width, height, 1).rotate([0, 0, 90]).translate([brick_depth, 0, 0]),
|
||||
wall(length, height, 1).translate(
|
||||
[0, (width) * (brick_length + mortar_gap), 0]
|
||||
),
|
||||
wall(width, height)
|
||||
.rotate([0, 0, 90])
|
||||
.translate(
|
||||
[(length + 0.5) * (brick_length + mortar_gap) - mortar_gap, 0, 0]
|
||||
),
|
||||
],
|
||||
Manifold(),
|
||||
)
|
||||
|
||||
|
||||
def floor(length, width):
|
||||
results = [walls(length, width, 1)]
|
||||
if length > 1 and width > 1:
|
||||
results.append(floor(length-1, width-1).translate(brick_depth +
|
||||
mortar_gap, brick_depth+mortar_gap, 0))
|
||||
results.append(
|
||||
floor(length - 1, width - 1).translate(
|
||||
[brick_depth + mortar_gap, brick_depth + mortar_gap, 0]
|
||||
)
|
||||
)
|
||||
if length == 1 and width > 1:
|
||||
results.append(row(width-1).rotate(0, 0, 90))
|
||||
results.append(row(width - 1).rotate(0, 0, 90))
|
||||
if width == 1 and length > 1:
|
||||
results.append(row(length-1).translate(2*(brick_depth +
|
||||
mortar_gap), brick_depth+mortar_gap, 0))
|
||||
results.append(halfbrick().translate(
|
||||
brick_depth+mortar_gap, brick_depth+mortar_gap, 0))
|
||||
return Manifold(results)
|
||||
results.append(
|
||||
row(length - 1).translate(
|
||||
[2 * (brick_depth + mortar_gap), brick_depth + mortar_gap, 0]
|
||||
)
|
||||
)
|
||||
results.append(
|
||||
halfbrick().translate([brick_depth + mortar_gap, brick_depth + mortar_gap, 0])
|
||||
)
|
||||
return sum(results, Manifold())
|
||||
|
||||
|
||||
def run(width=10, length=10, height=10):
|
||||
return walls(length, width, height) + floor(length, width)
|
||||
|
||||
|
||||
@@ -13,18 +13,21 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from manifold3d import Manifold
|
||||
from functools import reduce
|
||||
|
||||
# https://gist.github.com/ochafik/2db96400e3c1f73558fcede990b8a355#file-cube-with-half-spheres-dents-scad
|
||||
|
||||
|
||||
def run(n=5, overlap=True):
|
||||
a = Manifold.cube(n, n, 0.5).translate(-0.5, -0.5, -0.5)
|
||||
a = Manifold.cube([n, n, 0.5]).translate([-0.5, -0.5, -0.5])
|
||||
|
||||
spheres = [Manifold.sphere(0.45 if overlap else 0.55, 50).translate(i, j, 0)
|
||||
for i in range(n) for j in range(n)]
|
||||
spheres = reduce(lambda a, b: a + b, spheres)
|
||||
|
||||
return a - spheres
|
||||
spheres = [
|
||||
Manifold.sphere(0.45 if overlap else 0.55, 50).translate([i, j, 0])
|
||||
for i in range(n)
|
||||
for j in range(n)
|
||||
]
|
||||
# spheres = reduce(lambda a, b: a + b, spheres)
|
||||
|
||||
return a - sum(spheres, Manifold())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from manifold3d import CrossSection
|
||||
from manifold3d import CrossSection, Manifold
|
||||
|
||||
|
||||
def run():
|
||||
@@ -10,26 +10,34 @@ def run():
|
||||
cross_section = CrossSection(polygons_points)
|
||||
polygons = cross_section.to_polygons()
|
||||
polygon = polygons[0]
|
||||
if set(polygon) != set(polygon_points):
|
||||
raise Exception(f"polygon={polygon} differs from polygon_points={polygon_points}")
|
||||
if set([tuple(p) for p in polygon]) != set(polygon_points):
|
||||
raise Exception(
|
||||
f"polygon={polygon} differs from polygon_points={polygon_points}"
|
||||
)
|
||||
|
||||
# extrude a polygon to create a manifold
|
||||
extruded_polygon = cross_section.extrude(10.0)
|
||||
extruded_polygon = Manifold.extrude(cross_section, 10.0)
|
||||
eps = 0.001
|
||||
observed_volume = extruded_polygon.get_volume()
|
||||
observed_volume = extruded_polygon.volume()
|
||||
expected_volume = 10.0
|
||||
if abs(observed_volume - expected_volume) > eps:
|
||||
raise Exception(f"observed_volume={observed_volume} differs from expected_volume={expected_volume}")
|
||||
observed_surface_area = extruded_polygon.get_surface_area()
|
||||
raise Exception(
|
||||
f"observed_volume={observed_volume} differs from expected_volume={expected_volume}"
|
||||
)
|
||||
observed_surface_area = extruded_polygon.surface_area()
|
||||
expected_surface_area = 42.0
|
||||
if abs(observed_surface_area - expected_surface_area) > eps:
|
||||
raise Exception(f"observed_surface_area={observed_surface_area} differs from expected_surface_area={expected_surface_area}")
|
||||
raise Exception(
|
||||
f"observed_surface_area={observed_surface_area} differs from expected_surface_area={expected_surface_area}"
|
||||
)
|
||||
|
||||
# get bounding box from manifold
|
||||
observed_bbox = extruded_polygon.bounding_box
|
||||
observed_bbox = extruded_polygon.bounding_box()
|
||||
expected_bbox = (0.0, 0.0, 0.0, 1.0, 1.0, 10.0)
|
||||
if observed_bbox != expected_bbox:
|
||||
raise Exception(f"observed_bbox={observed_bbox} differs from expected_bbox={expected_bbox}")
|
||||
raise Exception(
|
||||
f"observed_bbox={observed_bbox} differs from expected_bbox={expected_bbox}"
|
||||
)
|
||||
|
||||
return extruded_polygon
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Copyright 2023 The Manifold Authors.
|
||||
|
||||
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 the License at
|
||||
|
||||
https://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 CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
from manifold3d import Mesh, Manifold
|
||||
|
||||
|
||||
def gyroid(x, y, z):
|
||||
xi = x - math.pi / 4.0
|
||||
yi = y - math.pi / 4.0
|
||||
zi = z - math.pi / 4.0
|
||||
return (
|
||||
math.cos(xi) * math.sin(yi)
|
||||
+ math.cos(yi) * math.sin(zi)
|
||||
+ math.cos(zi) * math.sin(xi)
|
||||
)
|
||||
|
||||
|
||||
def gyroid_levelset(level, period, size, n):
|
||||
return Manifold(
|
||||
Mesh.level_set(
|
||||
gyroid,
|
||||
[-period, -period, -period, period, period, period],
|
||||
period / n,
|
||||
level,
|
||||
)
|
||||
).scale([size / period] * 3)
|
||||
|
||||
|
||||
def rhombic_dodecahedron(size):
|
||||
box = Manifold.cube(size * math.sqrt(2.0) * np.array([1, 1, 2]), True)
|
||||
result = box.rotate([90, 45, 0]) ^ box.rotate([90, 45, 90])
|
||||
return result ^ box.rotate([0, 0, 45])
|
||||
|
||||
|
||||
def gyroid_module(size=20, n=15):
|
||||
period = math.pi * 2.0
|
||||
result = (
|
||||
gyroid_levelset(-0.4, period, size, n) ^ rhombic_dodecahedron(size)
|
||||
) - gyroid_levelset(0.4, period, size, n)
|
||||
return result.rotate([-45, 0, 90]).translate([0, 0, size / math.sqrt(2.0)])
|
||||
|
||||
|
||||
def run(size=20, n=15):
|
||||
return gyroid_module(size, n)
|
||||
@@ -24,10 +24,9 @@ from time import time
|
||||
if __name__ == "__main__":
|
||||
current_file = pathlib.Path(__file__)
|
||||
current_dir = current_file.parent
|
||||
files = [f.parts[-1][:-3]
|
||||
for f in current_dir.glob('*.py') if f != current_file]
|
||||
files = [f.parts[-1][:-3] for f in current_dir.glob("*.py") if f != current_file]
|
||||
|
||||
export_models = len(sys.argv) == 2 and sys.argv[-1] == '-e'
|
||||
export_models = len(sys.argv) == 2 and sys.argv[-1] == "-e"
|
||||
|
||||
for f in files:
|
||||
module = importlib.import_module(f)
|
||||
@@ -41,9 +40,10 @@ if __name__ == "__main__":
|
||||
else:
|
||||
vertices = mesh.vert_properties
|
||||
colors = None
|
||||
meshOut = trimesh.Trimesh(vertices=vertices, faces=mesh.tri_verts,
|
||||
vertex_colors=colors)
|
||||
trimesh.exchange.export.export_mesh(meshOut, f'{f}.glb', 'glb')
|
||||
print(f'Exported model to {f}.glb')
|
||||
meshOut = trimesh.Trimesh(
|
||||
vertices=vertices, faces=mesh.tri_verts, vertex_colors=colors
|
||||
)
|
||||
trimesh.exchange.export.export_mesh(meshOut, f"{f}.glb", "glb")
|
||||
print(f"Exported model to {f}.glb")
|
||||
t1 = time()
|
||||
print(f'Took {(t1-t0)*1000:.1f}ms for {f}')
|
||||
print(f"Took {(t1-t0)*1000:.1f}ms for {f}")
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Copyright 2023 The Manifold Authors.
|
||||
|
||||
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 the License at
|
||||
|
||||
https://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 CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from manifold3d import Manifold, Mesh
|
||||
import numpy as np
|
||||
|
||||
|
||||
def run():
|
||||
# A smoothed manifold demonstrating selective edge sharpening with
|
||||
# smooth() and refine(), see more details at:
|
||||
# https://elalish.blogspot.com/2022/03/smoothing-triangle-meshes.html
|
||||
|
||||
height = 10
|
||||
radius = 30
|
||||
offset = 20
|
||||
wiggles = 12
|
||||
sharpness = 0.8
|
||||
n = 50
|
||||
|
||||
triangles = []
|
||||
positions = [[-offset, 0, height], [-offset, 0, -height]]
|
||||
sharpenedEdges = []
|
||||
|
||||
delta = np.pi / wiggles
|
||||
for i in range(2 * wiggles):
|
||||
theta = (i - wiggles) * delta
|
||||
amp = 0.5 * height * max(np.cos(0.8 * theta), 0)
|
||||
|
||||
positions.append(
|
||||
[
|
||||
radius * np.cos(theta),
|
||||
radius * np.sin(theta),
|
||||
amp * (1 if i % 2 == 0 else -1),
|
||||
]
|
||||
)
|
||||
|
||||
j = i + 1
|
||||
if j == 2 * wiggles:
|
||||
j = 0
|
||||
|
||||
smoothness = 1 - sharpness * np.cos((theta + delta / 2) / 2)
|
||||
halfedge = 3 * len(triangles) + 1
|
||||
sharpenedEdges.append((halfedge, smoothness))
|
||||
triangles.append([0, 2 + i, 2 + j])
|
||||
|
||||
halfedge = 3 * len(triangles) + 1
|
||||
sharpenedEdges.append((halfedge, smoothness))
|
||||
triangles.append([1, 2 + j, 2 + i])
|
||||
|
||||
scallop = Mesh(
|
||||
tri_verts=np.array(triangles, np.int32),
|
||||
vert_properties=np.array(positions, np.float32),
|
||||
)
|
||||
|
||||
def colorCurvature(_pos, oldProp):
|
||||
a = max(0, min(1, oldProp[0] / 3 + 0.5))
|
||||
b = a * a * (3 - 2 * a)
|
||||
red = [1, 0, 0]
|
||||
blue = [0, 0, 1]
|
||||
return [(1 - b) * blue[i] + b * red[i] for i in range(3)]
|
||||
|
||||
edges, smoothing = zip(*sharpenedEdges)
|
||||
return (
|
||||
Manifold.smooth(scallop, edges, smoothing)
|
||||
.refine(n)
|
||||
.calculate_curvature(-1, 0)
|
||||
.set_properties(3, colorCurvature)
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Copyright 2022 The Manifold Authors.
|
||||
|
||||
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 the License at
|
||||
|
||||
https://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 CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from manifold3d import Manifold
|
||||
|
||||
|
||||
def run():
|
||||
a = Manifold.cube([1.0, 1.0, 1.0])
|
||||
b = Manifold.cube([1.0, 1.0, 1.0]).rotate([45.0, 45.0, 45.0])
|
||||
return a.split(b)[0]
|
||||
@@ -4,26 +4,27 @@ import numpy as np
|
||||
|
||||
def fractal(holes, hole, w, position, depth, maxDepth):
|
||||
w /= 3
|
||||
holes.append(hole.scale([w, w, 1.0]).translate(
|
||||
[position[0], position[1], 0.0]))
|
||||
holes.append(hole.scale([w, w, 1.0]).translate([position[0], position[1], 0.0]))
|
||||
if depth == maxDepth:
|
||||
return
|
||||
offsets = np.array([
|
||||
[-w, -w],
|
||||
[-w, 0.0],
|
||||
[-w, w],
|
||||
[0.0, w],
|
||||
[w, w],
|
||||
[w, 0.0],
|
||||
[w, -w],
|
||||
[0.0, -w],
|
||||
])
|
||||
offsets = np.array(
|
||||
[
|
||||
[-w, -w],
|
||||
[-w, 0.0],
|
||||
[-w, w],
|
||||
[0.0, w],
|
||||
[w, w],
|
||||
[w, 0.0],
|
||||
[w, -w],
|
||||
[0.0, -w],
|
||||
]
|
||||
)
|
||||
for offset in offsets:
|
||||
fractal(holes, hole, w, position + offset, depth + 1, maxDepth)
|
||||
|
||||
|
||||
def posColors(pos, _):
|
||||
return [1 - p / 2 for p in pos] + [1.0]
|
||||
return [-p + 0.5 for p in pos] + [1.0]
|
||||
|
||||
|
||||
def run(n=1):
|
||||
@@ -37,4 +38,6 @@ def run(n=1):
|
||||
result -= hole.rotate([90, 0, 0])
|
||||
result -= hole.rotate([0, 90, 0])
|
||||
|
||||
return result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale(100)
|
||||
return (
|
||||
result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale([100] * 3)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from manifold3d import *
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# Creates a classic torus knot, defined as a string wrapping periodically
|
||||
# around the surface of an imaginary donut. If p and q have a common
|
||||
# factor then you will get multiple separate, interwoven knots. This is
|
||||
# an example of using the warp() method, thus avoiding any direct
|
||||
# handling of triangles.
|
||||
|
||||
|
||||
def run(warp_single=False):
|
||||
# The number of times the thread passes through the donut hole.
|
||||
p = 1
|
||||
# The number of times the thread circles the donut.
|
||||
q = 3
|
||||
# Radius of the interior of the imaginary donut.
|
||||
majorRadius = 25
|
||||
# Radius of the small cross-section of the imaginary donut.
|
||||
minorRadius = 10
|
||||
# Radius of the small cross-section of the actual object.
|
||||
threadRadius = 3.75
|
||||
# Number of linear segments making up the threadRadius circle. Default is
|
||||
# getCircularSegments(threadRadius).
|
||||
circularSegments = -1
|
||||
# Number of segments along the length of the knot. Default makes roughly
|
||||
# square facets.
|
||||
linearSegments = -1
|
||||
|
||||
# These default values recreate Matlab Knot by Emmett Lalish:
|
||||
# https://www.thingiverse.com/thing:7080
|
||||
|
||||
kLoops = np.gcd(p, q)
|
||||
pk = p / kLoops
|
||||
qk = q / kLoops
|
||||
n = (
|
||||
circularSegments
|
||||
if circularSegments > 2
|
||||
else get_circular_segments(threadRadius)
|
||||
)
|
||||
m = linearSegments if linearSegments > 2 else n * qk * majorRadius / threadRadius
|
||||
|
||||
offset = 2
|
||||
circle = CrossSection.circle(1, n).translate([offset, 0])
|
||||
|
||||
def ax_rotate(x, theta):
|
||||
a, b = (x + 1) % 3, (x + 2) % 3
|
||||
s, c = np.sin(theta), np.cos(theta)
|
||||
m = np.zeros((len(theta), 4, 4), dtype=np.float32)
|
||||
m[:, a, a], m[:, a, b] = c, s
|
||||
m[:, b, a], m[:, b, b] = -s, c
|
||||
m[:, x, x], m[:, 3, 3] = 1, 1
|
||||
return m
|
||||
|
||||
def func(pts):
|
||||
npts = pts.shape[0]
|
||||
x, y, z = pts[:, 0], pts[:, 1], pts[:, 2]
|
||||
psi = qk * np.arctan2(x, y)
|
||||
theta = psi * pk / qk
|
||||
x1 = np.sqrt(x * x + y * y)
|
||||
phi = np.arctan2(x1 - offset, z)
|
||||
|
||||
v = np.zeros((npts, 4), dtype=np.float32)
|
||||
v[:, 0] = threadRadius * np.cos(phi)
|
||||
v[:, 2] = threadRadius * np.sin(phi)
|
||||
v[:, 3] = 1
|
||||
r = majorRadius + minorRadius * np.cos(theta)
|
||||
|
||||
m1 = ax_rotate(0, -np.arctan2(pk * minorRadius, qk * r))
|
||||
m1[:, 3, 0] = minorRadius
|
||||
m2 = ax_rotate(1, theta)
|
||||
m2[:, 3, 0] = majorRadius
|
||||
m3 = ax_rotate(2, psi)
|
||||
|
||||
v = v[:, None, :] @ m1 @ m2 @ m3
|
||||
pts[:] = v[:, 0, :3]
|
||||
|
||||
def func_single(v):
|
||||
pts = np.array([v])
|
||||
func(pts)
|
||||
return pts[0]
|
||||
|
||||
if warp_single:
|
||||
return Manifold.revolve(circle, int(m)).warp(func_single)
|
||||
else:
|
||||
return Manifold.revolve(circle, int(m)).warp_batch(func)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("warp_single", [True, False])
|
||||
def test_warp(warp_single):
|
||||
m = run(warp_single=warp_single)
|
||||
assert m.volume() == pytest.approx(20785.76)
|
||||
assert m.surface_area() == pytest.approx(11176.8)
|
||||
assert m.genus() == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -1,7 +1,8 @@
|
||||
from manifold3d import Manifold
|
||||
|
||||
|
||||
def run():
|
||||
# for some reason this causes collider error
|
||||
obj = Manifold.cube()
|
||||
obj += Manifold([Manifold.cube()]).rotate(0, 0, 45+180)
|
||||
obj += Manifold.cube().rotate([0, 0, 45 + 180])
|
||||
return obj
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
from os.path import dirname
|
||||
import re
|
||||
|
||||
base = dirname(dirname(dirname(__file__)))
|
||||
|
||||
def snake_case(name):
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
|
||||
|
||||
|
||||
def python_param_modifier(comment):
|
||||
# p = f":{snake_case(m[0][1:])}:"
|
||||
comment = re.sub(r"@(param \w+)", lambda m: f':{snake_case(m[1])}:', comment)
|
||||
# python API renames `MeshGL` to `Mesh`
|
||||
comment = re.sub('mesh_gl', 'mesh', comment)
|
||||
comment = re.sub('MeshGL', 'Mesh', comment)
|
||||
return comment
|
||||
|
||||
|
||||
def method_key(name):
|
||||
name = re.sub(r"\+", "_plus", name)
|
||||
name = re.sub(r"\-", "_minus", name)
|
||||
name = re.sub(r"\^", "_xor", name)
|
||||
name = re.sub(r"\=", "_eq", name)
|
||||
name = re.sub(r"\:", "_", name)
|
||||
name = re.sub(r"\~", "destroy_", name)
|
||||
return name
|
||||
|
||||
|
||||
parens_re = re.compile(r"[^(]+\(([^(]*(\(.*\))*[^(]*\))", flags=re.DOTALL)
|
||||
args_re = re.compile(
|
||||
r"^[^,^\(^\)]*(\(.*\))*[^,^\(^\)]*[\s\&\*]([0-9\w]+)\s*[,\)]", flags=re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def parse_args(s):
|
||||
par = parens_re.match(s)
|
||||
if not par:
|
||||
return None
|
||||
out = []
|
||||
arg_str = par[1]
|
||||
while m := re.search(args_re, arg_str):
|
||||
out += [snake_case(m[2])]
|
||||
arg_str = arg_str[m.span()[1] :]
|
||||
return out
|
||||
|
||||
|
||||
def collect(fname, matcher, param_modifier=python_param_modifier):
|
||||
comment = ""
|
||||
with open(fname) as f:
|
||||
for line in f:
|
||||
line = line.lstrip()
|
||||
if line.startswith("/**"):
|
||||
comment = ""
|
||||
elif line.startswith("*/"):
|
||||
pass
|
||||
elif line.startswith("*") and comment is not None:
|
||||
comment += line[1:].lstrip()
|
||||
elif comment and (m := matcher(line)):
|
||||
while (args := parse_args(line)) is None:
|
||||
line += next(f)
|
||||
if len(line) > 500:
|
||||
break
|
||||
|
||||
method = method_key(snake_case(m[1]))
|
||||
# comment = re.sub(param_re, param_modifier, comment)
|
||||
comment = param_modifier(comment)
|
||||
method = "__".join([method, *args])
|
||||
assert method not in comments
|
||||
comments[method] = comment
|
||||
comment = ""
|
||||
|
||||
|
||||
comments = {}
|
||||
|
||||
method_re = re.compile(r"(\w+::[\w\-\+\^\=\:]+)\(")
|
||||
function_re = re.compile(r"([\w\-\+\^\=\:]+)\(")
|
||||
|
||||
|
||||
# we don't handle inline functions in classes properly
|
||||
# so instead just white-list functions we want
|
||||
def select_functions(s):
|
||||
m = function_re.search(s)
|
||||
if m and "Triangulate" in m[0]:
|
||||
return m
|
||||
if m and "Circular" in m[0]:
|
||||
return m
|
||||
return None
|
||||
|
||||
|
||||
collect(f"{base}/src/manifold/src/manifold.cpp", lambda s: method_re.search(s))
|
||||
collect(f"{base}/src/manifold/src/constructors.cpp", lambda s: method_re.search(s))
|
||||
collect(f"{base}/src/manifold/src/sort.cpp", lambda s: method_re.search(s))
|
||||
collect(
|
||||
f"{base}/src/cross_section/src/cross_section.cpp", lambda s: method_re.search(s)
|
||||
)
|
||||
collect(f"{base}/src/polygon/src/polygon.cpp", select_functions)
|
||||
collect(f"{base}/src/utilities/include/public.h", select_functions)
|
||||
|
||||
comments = dict(sorted(comments.items()))
|
||||
|
||||
gen_h = "autogen_docstrings.inl"
|
||||
with open(gen_h, "w") as f:
|
||||
f.write("#pragma once\n\n")
|
||||
f.write("// --- AUTO GENERATED ---\n")
|
||||
f.write("// gen_docs.py is run by cmake build\n\n")
|
||||
f.write("namespace manifold_docstrings {\n")
|
||||
for key, doc in comments.items():
|
||||
f.write(f'const char* {key} = R"___({doc.strip()})___";\n')
|
||||
f.write("} // namespace manifold_docs")
|
||||
@@ -32,13 +32,21 @@ add_custom_target(js_deps ALL
|
||||
DEPENDS manifoldjs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts)
|
||||
|
||||
# copy WASM build back here for publishing to npm
|
||||
add_custom_command(
|
||||
TARGET js_deps POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_BINARY_DIR}/manifold.*
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts
|
||||
${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# copy WASM build and TS declarations for Vite to package into ManifoldCAD.org
|
||||
add_custom_command(
|
||||
TARGET js_deps POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manifold*
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/examples/built/)
|
||||
|
||||
# copy TS declarations to public so they can be accessed by our editor
|
||||
add_custom_command(
|
||||
TARGET js_deps POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
|
||||
@@ -24,7 +24,7 @@ Since Manifold is a WASM module, it does not automatically garbage-collect like
|
||||
|
||||
## Examples
|
||||
|
||||
Please see our usage [examples](https://github.com/elalish/manifold/tree/master/bindings/wasm/examples) to see how to interface this library with `three.js`, `<model-viewer>`, and `glTF`. Of particular note are the included libraries for lossless roundtrip of manifold meshes through glTF files, via a new extension: [EXT_manifold](https://github.com/KhronosGroup/glTF/pull/2286).
|
||||
Please see our usage [examples](https://github.com/elalish/manifold/tree/master/bindings/wasm/examples) to see how to interface this library with `three.js`, `<model-viewer>`, and `glTF`. Of particular note are the included libraries for lossless roundtrip of manifold meshes through glTF files, via a new extension: [EXT_mesh_manifold](https://github.com/KhronosGroup/glTF/pull/2286).
|
||||
|
||||
## About the author
|
||||
|
||||
|
||||
@@ -138,9 +138,12 @@ EMSCRIPTEN_BINDINGS(whatever) {
|
||||
.function("_Split", &man_js::Split)
|
||||
.function("_SplitByPlane", &man_js::SplitByPlane)
|
||||
.function("_TrimByPlane", &Manifold::TrimByPlane)
|
||||
.function("slice", &Manifold::Slice)
|
||||
.function("project", &Manifold::Project)
|
||||
.function("hull", select_overload<Manifold() const>(&Manifold::Hull))
|
||||
.function("_GetMeshJS", &js::GetMeshJS)
|
||||
.function("refine", &Manifold::Refine)
|
||||
.function("refineToLength", &Manifold::RefineToLength)
|
||||
.function("_Warp", &man_js::Warp)
|
||||
.function("_SetProperties", &man_js::SetProperties)
|
||||
.function("transform", &man_js::Transform)
|
||||
|
||||
@@ -65,7 +65,7 @@ button:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
input {
|
||||
.name {
|
||||
border: none;
|
||||
height: 36px;
|
||||
background-color: transparent;
|
||||
@@ -77,12 +77,12 @@ input {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
.name:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em;
|
||||
margin: 18px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ model-viewer {
|
||||
|
||||
.header .margin {
|
||||
font-size: 16px;
|
||||
margin: 1em 0.5em;
|
||||
margin: 18px 8px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@@ -168,6 +168,29 @@ model-viewer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top-edit {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 36px;
|
||||
top: 18px;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.top-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 36px;
|
||||
background-size: 24px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.squish {
|
||||
margin-right: -12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
@@ -180,6 +203,22 @@ model-viewer {
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.undo {
|
||||
background-image: url(/icons/undo.png);
|
||||
}
|
||||
|
||||
.redo {
|
||||
background-image: url(/icons/redo.png);
|
||||
}
|
||||
|
||||
.star {
|
||||
background-image: url(/icons/star.png);
|
||||
}
|
||||
|
||||
.share {
|
||||
background-image: url(/icons/share.png);
|
||||
}
|
||||
|
||||
.cancel {
|
||||
background-image: url(/icons/close.png);
|
||||
background-size: 20px;
|
||||
@@ -196,10 +235,22 @@ model-viewer {
|
||||
background-image: url(/icons/trash.png);
|
||||
}
|
||||
|
||||
.play {
|
||||
background-image: url(/icons/play.png);
|
||||
}
|
||||
|
||||
.pause {
|
||||
background-image: url(/icons/pause.png);
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.top-icon:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.blue {
|
||||
background-color: rgb(148, 221, 255);
|
||||
}
|
||||
@@ -221,7 +272,52 @@ model-viewer {
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
#animation {
|
||||
display: none;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#play {
|
||||
position: relative;
|
||||
background-color: #ccc;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
margin-right: 16px;
|
||||
background: #ffffff55;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 820px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
@@ -256,4 +352,4 @@ model-viewer {
|
||||
width: calc(50% - 16px);
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import ManifoldWorker from './worker?worker';
|
||||
|
||||
const CODE_START = '<code>';
|
||||
// Loaded globally by examples.js
|
||||
const exampleFunctions = self.examples.functionBodies;
|
||||
|
||||
@@ -24,6 +25,28 @@ if (navigator.serviceWorker) {
|
||||
|
||||
let editor = undefined;
|
||||
|
||||
// Edit UI ------------------------------------------------------------
|
||||
|
||||
const undoButton = document.querySelector('#undo');
|
||||
const redoButton = document.querySelector('#redo');
|
||||
const formatButton = document.querySelector('#format');
|
||||
const shareButton = document.querySelector('#share');
|
||||
|
||||
undoButton.onclick = () => editor.trigger('ignored', 'undo');
|
||||
redoButton.onclick = () => editor.trigger('ignored', 'redo');
|
||||
formatButton.onclick = () =>
|
||||
editor.trigger('ignored', 'editor.action.formatDocument');
|
||||
shareButton.onclick = () => {
|
||||
const url = new URL(window.location.toString());
|
||||
url.hash =
|
||||
'#' +
|
||||
encodeURIComponent(
|
||||
currentFileElement.textContent + CODE_START + editor.getValue());
|
||||
navigator.clipboard.writeText(url.toString());
|
||||
console.log('Shareable link copied to clipboard!');
|
||||
console.log('Consider shortening this URL using tinyURL.com');
|
||||
};
|
||||
|
||||
// File UI ------------------------------------------------------------
|
||||
const fileButton = document.querySelector('#file');
|
||||
const currentFileElement = document.querySelector('#current');
|
||||
@@ -90,9 +113,10 @@ function switchTo(scriptName) {
|
||||
switching = true;
|
||||
currentFileElement.textContent = scriptName;
|
||||
setScript('currentName', scriptName);
|
||||
const code =
|
||||
exampleFunctions.get(scriptName) ?? getScript(scriptName) ?? '';
|
||||
isExample = exampleFunctions.get(scriptName) != null;
|
||||
const code = isExample ? exampleFunctions.get(scriptName).substring(1) :
|
||||
getScript(scriptName) ?? '';
|
||||
window.location.hash = '#' + scriptName;
|
||||
editor.setValue(code);
|
||||
}
|
||||
}
|
||||
@@ -146,6 +170,7 @@ function addEdit(button) {
|
||||
const code = getScript(oldName);
|
||||
const form = document.createElement('form');
|
||||
const inputElement = document.createElement('input');
|
||||
inputElement.classList.add('name');
|
||||
inputElement.value = oldName;
|
||||
label.textContent = '';
|
||||
button.appendChild(form);
|
||||
@@ -203,16 +228,16 @@ function addEdit(button) {
|
||||
}
|
||||
|
||||
const newButton = document.querySelector('#new');
|
||||
function newItem(code) {
|
||||
const name = uniqueName('New Script');
|
||||
function newItem(code, scriptName = undefined) {
|
||||
const name = uniqueName(scriptName ?? 'New Script');
|
||||
setScript(name, code);
|
||||
const nextButton = createDropdownItem(name);
|
||||
newButton.insertAdjacentElement('afterend', nextButton.parentElement);
|
||||
addEdit(nextButton);
|
||||
nextButton.click();
|
||||
return {button: nextButton, name};
|
||||
};
|
||||
newButton.onclick = function() {
|
||||
newItem('');
|
||||
newItem('').button.click();
|
||||
};
|
||||
|
||||
const runButton = document.querySelector('#compile');
|
||||
@@ -225,7 +250,7 @@ function initializeRun() {
|
||||
if (autoExecute) {
|
||||
runButton.click();
|
||||
} else {
|
||||
poster.textContent = 'Auto-run disabled due to prior failure';
|
||||
poster.textContent = 'Auto-run disabled';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +323,24 @@ require(['vs/editor/editor.main'], async function() {
|
||||
addEdit(button);
|
||||
}
|
||||
}
|
||||
switchTo(currentName);
|
||||
|
||||
if (window.location.hash.length > 0) {
|
||||
const fragment = decodeURIComponent(window.location.hash.substring(1));
|
||||
const codeIdx = fragment.indexOf(CODE_START);
|
||||
if (codeIdx != -1) {
|
||||
autoExecute = true;
|
||||
const name = fragment.substring(0, codeIdx);
|
||||
switchTo(
|
||||
newItem(fragment.substring(codeIdx + CODE_START.length), name).name);
|
||||
} else {
|
||||
if (fragment != currentName) {
|
||||
autoExecute = true;
|
||||
}
|
||||
switchTo(fragment);
|
||||
}
|
||||
} else {
|
||||
switchTo(currentName);
|
||||
}
|
||||
|
||||
if (manifoldInitialized) {
|
||||
initializeRun();
|
||||
@@ -308,11 +350,12 @@ require(['vs/editor/editor.main'], async function() {
|
||||
runButton.disabled = false;
|
||||
if (switching) {
|
||||
switching = false;
|
||||
editor.setScrollTop(0);
|
||||
return;
|
||||
}
|
||||
if (isExample) {
|
||||
const cursor = editor.getPosition();
|
||||
newItem(editor.getValue());
|
||||
newItem(editor.getValue()).button.click();
|
||||
editor.setPosition(cursor);
|
||||
}
|
||||
});
|
||||
@@ -322,6 +365,51 @@ require(['vs/editor/editor.main'], async function() {
|
||||
};
|
||||
});
|
||||
|
||||
// Animation ------------------------------------------------------------
|
||||
const mv = document.querySelector('model-viewer');
|
||||
const animationContainer = document.querySelector('#animation');
|
||||
const playButton = document.querySelector('#play');
|
||||
const scrubber = document.querySelector('#scrubber');
|
||||
let paused = false;
|
||||
|
||||
mv.addEventListener('load', () => {
|
||||
const hasAnimation = mv.availableAnimations.length > 0;
|
||||
animationContainer.style.display = hasAnimation ? 'flex' : 'none';
|
||||
if (hasAnimation) {
|
||||
play();
|
||||
}
|
||||
});
|
||||
|
||||
function play() {
|
||||
mv.play();
|
||||
playButton.classList.remove('play');
|
||||
playButton.classList.add('pause');
|
||||
paused = false;
|
||||
scrubber.classList.add('hide');
|
||||
}
|
||||
|
||||
function pause() {
|
||||
mv.pause();
|
||||
playButton.classList.remove('pause');
|
||||
playButton.classList.add('play');
|
||||
paused = true;
|
||||
scrubber.max = mv.duration;
|
||||
scrubber.value = mv.currentTime;
|
||||
scrubber.classList.remove('hide');
|
||||
}
|
||||
|
||||
playButton.onclick = function() {
|
||||
if (paused) {
|
||||
play();
|
||||
} else {
|
||||
pause();
|
||||
}
|
||||
};
|
||||
|
||||
scrubber.oninput = function() {
|
||||
mv.currentTime = scrubber.value;
|
||||
};
|
||||
|
||||
// Execution ------------------------------------------------------------
|
||||
const consoleElement = document.querySelector('#console');
|
||||
const oldLog = console.log;
|
||||
@@ -357,7 +445,6 @@ function finishRun() {
|
||||
`Took ${(Math.round((t1 - t0) / 10) / 100).toLocaleString()} seconds`);
|
||||
}
|
||||
|
||||
const mv = document.querySelector('model-viewer');
|
||||
const output = {
|
||||
glbURL: null,
|
||||
threeMFURL: null
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
// Copyright 2023 The Manifold Authors.
|
||||
//
|
||||
// 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 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 CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Accessor} from '@gltf-transform/core';
|
||||
|
||||
import {EXTManifold} from './manifold-gltf.js';
|
||||
|
||||
export const attributeDefs = {
|
||||
'POSITION': {type: Accessor.Type.VEC3, components: 3},
|
||||
'NORMAL': {type: Accessor.Type.VEC3, components: 3},
|
||||
'TANGENT': {type: Accessor.Type.VEC4, components: 4},
|
||||
'TEXCOORD_0': {type: Accessor.Type.VEC2, components: 2},
|
||||
'TEXCOORD_1': {type: Accessor.Type.VEC2, components: 2},
|
||||
'COLOR_0': {type: Accessor.Type.VEC3, components: 3},
|
||||
'JOINTS_0': {type: Accessor.Type.VEC4, components: 4},
|
||||
'WEIGHTS_0': {type: Accessor.Type.VEC4, components: 4},
|
||||
'SKIP_1': {type: null, components: 1},
|
||||
'SKIP_2': {type: null, components: 2},
|
||||
'SKIP_3': {type: null, components: 3},
|
||||
'SKIP_4': {type: null, components: 4},
|
||||
};
|
||||
|
||||
export function setupIO(io) {
|
||||
return io.registerExtensions([EXTManifold]);
|
||||
}
|
||||
|
||||
function readPrimitive(primitive, numProp, attributes) {
|
||||
const position = primitive.getAttribute('POSITION');
|
||||
const numVert = position.getCount();
|
||||
const vertProperties = [];
|
||||
let offset = 0;
|
||||
for (const attribute of attributes) {
|
||||
const size = attributeDefs[attribute].components;
|
||||
if (attribute === 'SKIP') {
|
||||
offset += size;
|
||||
continue;
|
||||
}
|
||||
const accessor = primitive.getAttribute(attribute);
|
||||
if (accessor) {
|
||||
const array = accessor.getArray();
|
||||
for (let i = 0; i < numVert; ++i) {
|
||||
for (let j = 0; j < size; ++j) {
|
||||
vertProperties[numProp * i + offset + j] = array[i * size + j];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < numVert; ++i) {
|
||||
for (let j = 0; j < size; ++j) {
|
||||
vertProperties[numProp * i + offset + j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
return vertProperties;
|
||||
}
|
||||
|
||||
export function readMesh(mesh, attributes, materials) {
|
||||
const primitives = mesh.listPrimitives();
|
||||
if (primitives.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (attributes.length === 0) {
|
||||
const attributeSet = new Set();
|
||||
for (const primitive of primitives) {
|
||||
const semantics = primitive.listSemantics();
|
||||
for (const semantic of semantics) {
|
||||
attributeSet.add(semantic);
|
||||
}
|
||||
}
|
||||
for (const semantic in attributeDefs) {
|
||||
if (attributeSet.has(semantic)) {
|
||||
attributes.push(semantic);
|
||||
attributeSet.delete(semantic);
|
||||
}
|
||||
}
|
||||
for (const semantic of attributeSet.keys()) {
|
||||
attributes.push(semantic);
|
||||
}
|
||||
}
|
||||
|
||||
if (attributes.length < 1 || attributes[0] !== 'POSITION')
|
||||
throw new Error('First attribute must be "POSITION".');
|
||||
|
||||
const numProp = attributes.map((def) => attributeDefs[def].components)
|
||||
.reduce((a, b) => a + b);
|
||||
|
||||
const manifoldPrimitive = mesh.getExtension('EXT_manifold');
|
||||
|
||||
let vertPropArray = [];
|
||||
let triVertArray = [];
|
||||
const runIndexArray = [0];
|
||||
const mergeFromVert = [];
|
||||
const mergeToVert = [];
|
||||
if (manifoldPrimitive != null) {
|
||||
vertPropArray = readPrimitive(primitives[0], numProp, attributes);
|
||||
for (const primitive of primitives) {
|
||||
triVertArray = [...triVertArray, ...primitive.getIndices().getArray()];
|
||||
runIndexArray.push(triVertArray.length);
|
||||
materials.push(primitive.getMaterial());
|
||||
}
|
||||
const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? [];
|
||||
const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? [];
|
||||
const vert2merge = new Map();
|
||||
for (const [i, idx] of mergeTriVert.entries()) {
|
||||
vert2merge.set(triVertArray[idx], mergeTo[i]);
|
||||
}
|
||||
for (const [from, to] of vert2merge.entries()) {
|
||||
mergeFromVert.push(from);
|
||||
mergeToVert.push(to);
|
||||
}
|
||||
} else {
|
||||
for (const primitive of primitives) {
|
||||
const numVert = vertPropArray.length / numProp;
|
||||
vertPropArray =
|
||||
[...vertPropArray, ...readPrimitive(primitive, numProp, attributes)];
|
||||
triVertArray = [
|
||||
...triVertArray,
|
||||
...primitive.getIndices().getArray().map((i) => i + numVert)
|
||||
];
|
||||
runIndexArray.push(triVertArray.length);
|
||||
materials.push(primitive.getMaterial());
|
||||
}
|
||||
}
|
||||
const vertProperties = new Float32Array(vertPropArray);
|
||||
const triVerts = new Uint32Array(triVertArray);
|
||||
const runIndex = new Uint32Array(runIndexArray);
|
||||
|
||||
return {
|
||||
numProp,
|
||||
triVerts,
|
||||
vertProperties,
|
||||
runIndex,
|
||||
mergeFromVert,
|
||||
mergeToVert
|
||||
};
|
||||
}
|
||||
|
||||
export function writeMesh(doc, manifoldMesh, attributes, materials) {
|
||||
if (doc.getRoot().listBuffers().length === 0) {
|
||||
doc.createBuffer();
|
||||
}
|
||||
const buffer = doc.getRoot().listBuffers()[0];
|
||||
const manifoldExtension = doc.createExtension(EXTManifold);
|
||||
|
||||
const attributeUnion = [];
|
||||
for (const matAttributes of attributes) {
|
||||
matAttributes.forEach((attribute, i) => {
|
||||
if (i >= attributeUnion.length) {
|
||||
attributeUnion.push(attribute);
|
||||
} else {
|
||||
const size = attributeDefs[attribute].components;
|
||||
const unionSize = attributeDefs[attributeUnion[i]].components;
|
||||
if (size != unionSize) {
|
||||
throw new Error(
|
||||
'Attribute sizes do not correspond: ', attribute, ' and ',
|
||||
attributeUnion[i]);
|
||||
}
|
||||
if (attributeDefs[attributeUnion[i]].type == null) {
|
||||
attributeUnion[i] = attribute;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (attributeUnion.length < 1 || attributeUnion[0] !== 'POSITION')
|
||||
throw new Error('First attribute must be "POSITION".');
|
||||
|
||||
const mesh = doc.createMesh();
|
||||
const numPrimitive = manifoldMesh.runIndex.length - 1;
|
||||
for (let run = 0; run < numPrimitive; ++run) {
|
||||
const indices = doc.createAccessor('indices')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array());
|
||||
const primitive = doc.createPrimitive().setIndices(indices);
|
||||
const material = materials[run];
|
||||
if (material) {
|
||||
primitive.setMaterial(material);
|
||||
}
|
||||
mesh.addPrimitive(primitive);
|
||||
}
|
||||
|
||||
const numVert = manifoldMesh.numVert;
|
||||
const numProp = manifoldMesh.numProp;
|
||||
let offset = 0;
|
||||
attributeUnion.forEach((attribute, aIdx) => {
|
||||
if (attribute === 'SKIP') {
|
||||
++offset;
|
||||
return;
|
||||
}
|
||||
const def = attributeDefs[attribute];
|
||||
if (def == null)
|
||||
throw new Error(attribute + ' is not a recognized attribute.');
|
||||
|
||||
const n = def.components;
|
||||
if (offset + n > numProp) throw new Error('Too many attribute channels.');
|
||||
|
||||
const array = new Float32Array(n * numVert);
|
||||
for (let v = 0; v < numVert; ++v) {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
array[n * v + i] =
|
||||
manifoldMesh.vertProperties[numProp * v + offset + i];
|
||||
}
|
||||
}
|
||||
|
||||
const accessor =
|
||||
doc.createAccessor().setBuffer(buffer).setType(def.type).setArray(
|
||||
array);
|
||||
|
||||
mesh.listPrimitives().forEach((primitive, pIdx) => {
|
||||
if (attributes[pIdx].length > aIdx &&
|
||||
attributeDefs[attributes[pIdx][aIdx]].type != null) {
|
||||
primitive.setAttribute(attribute, accessor);
|
||||
}
|
||||
});
|
||||
offset += n;
|
||||
});
|
||||
|
||||
const manifoldPrimitive = manifoldExtension.createManifoldPrimitive();
|
||||
mesh.setExtension('EXT_manifold', manifoldPrimitive);
|
||||
|
||||
const indices = doc.createAccessor('indices')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(manifoldMesh.triVerts);
|
||||
manifoldPrimitive.setIndices(indices);
|
||||
manifoldPrimitive.setRunIndex(manifoldMesh.runIndex);
|
||||
|
||||
const vert2merge = [...Array(manifoldMesh.numVert).keys()];
|
||||
for (const [i, from] of manifoldMesh.mergeFromVert.entries()) {
|
||||
vert2merge[from] = manifoldMesh.mergeToVert[i];
|
||||
}
|
||||
const ind = [];
|
||||
const val = [];
|
||||
for (const [i, vert] of manifoldMesh.triVerts.entries()) {
|
||||
const newVert = vert2merge[vert];
|
||||
if (vert !== newVert) {
|
||||
ind.push(i);
|
||||
val.push(newVert);
|
||||
}
|
||||
}
|
||||
const indicesAccessor = doc.createAccessor()
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(ind));
|
||||
const valuesAccessor = doc.createAccessor()
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(val));
|
||||
manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
export function disposeMesh(mesh) {
|
||||
if (!mesh) return;
|
||||
const primitives = mesh.listPrimitives();
|
||||
for (const primitive of primitives) {
|
||||
primitive.getIndices()?.dispose();
|
||||
for (const accessor of primitive.listAttributes()) {
|
||||
accessor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
mesh.dispose();
|
||||
}
|
||||
|
||||
export async function loadTexture(texture, uri) {
|
||||
const response = await fetch(uri);
|
||||
const blob = await response.blob();
|
||||
texture.setMimeType(blob.type);
|
||||
texture.setImage(new Uint8Array(await blob.arrayBuffer()));
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
// Copyright 2023 The Manifold Authors.
|
||||
//
|
||||
// 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 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 CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Accessor, Document, Material, Mesh, Primitive, Texture, WebIO} from '@gltf-transform/core';
|
||||
|
||||
import {EXTManifold, ManifoldPrimitive} from './manifold-gltf';
|
||||
import {Mesh as ManifoldMesh, MeshOptions} from './public/manifold-encapsulated-types';
|
||||
|
||||
export const attributeDefs = {
|
||||
'POSITION': {type: Accessor.Type.VEC3, components: 3},
|
||||
'NORMAL': {type: Accessor.Type.VEC3, components: 3},
|
||||
'TANGENT': {type: Accessor.Type.VEC4, components: 4},
|
||||
'TEXCOORD_0': {type: Accessor.Type.VEC2, components: 2},
|
||||
'TEXCOORD_1': {type: Accessor.Type.VEC2, components: 2},
|
||||
'COLOR_0': {type: Accessor.Type.VEC3, components: 3},
|
||||
'JOINTS_0': {type: Accessor.Type.VEC4, components: 4},
|
||||
'WEIGHTS_0': {type: Accessor.Type.VEC4, components: 4},
|
||||
'SKIP_1': {type: null, components: 1},
|
||||
'SKIP_2': {type: null, components: 2},
|
||||
'SKIP_3': {type: null, components: 3},
|
||||
'SKIP_4': {type: null, components: 4},
|
||||
};
|
||||
|
||||
export type Attribute = keyof(typeof attributeDefs);
|
||||
|
||||
export interface Properties {
|
||||
material: Material;
|
||||
attributes: Attribute[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this first to register the manifold extension so that readMesh and
|
||||
* writeMesh will work.
|
||||
*/
|
||||
export function setupIO(io: WebIO) {
|
||||
return io.registerExtensions([EXTManifold]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an input mesh into Manifold-compatible data structures, whether it
|
||||
* contains the EXT_mesh_manifold extension or not.
|
||||
*
|
||||
* @param mesh The Mesh to read.
|
||||
* @param attributes An array of attributes representing the order of desired
|
||||
* properties returned in the vertProperties array of the output mesh. If
|
||||
* omitted, this will be populated with the union of all attributes defined
|
||||
* in the primitives of the input mesh. If present, the first entry must be
|
||||
* 'POSITION', and any attributes in the primitives that are not included in
|
||||
* this list will be ignored, while those in the list but not defined in a
|
||||
* primitive will be populated with zeros.
|
||||
* @returns The returned mesh is suitable for initializing a Manifold or Mesh of
|
||||
* the Manifold library if desired. See Manifold documentation if you prefer
|
||||
* to use these GL arrays in a different library. The runProperties array
|
||||
* gives the Material and attributes list associated with each triangle run,
|
||||
* which in turn corresponds to a primitive of the input mesh. These
|
||||
* attributes are the intersection of the attributes present on the
|
||||
* primitive and those requested in the attributes input.
|
||||
*/
|
||||
export function readMesh(mesh: Mesh, attributes: Attribute[] = []):
|
||||
{mesh: MeshOptions, runProperties: Properties[]}|null {
|
||||
const primitives = mesh.listPrimitives();
|
||||
if (primitives.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (attributes.length === 0) {
|
||||
const attributeSet = new Set<Attribute>();
|
||||
for (const primitive of primitives) {
|
||||
const semantics = primitive.listSemantics() as Attribute[];
|
||||
for (const semantic of semantics) {
|
||||
attributeSet.add(semantic);
|
||||
}
|
||||
}
|
||||
let semantic: Attribute;
|
||||
for (semantic in attributeDefs) {
|
||||
if (attributeSet.has(semantic)) {
|
||||
attributes.push(semantic);
|
||||
attributeSet.delete(semantic);
|
||||
}
|
||||
}
|
||||
for (const semantic of attributeSet.keys()) {
|
||||
attributes.push(semantic);
|
||||
}
|
||||
}
|
||||
|
||||
if (attributes.length < 1 || attributes[0] !== 'POSITION')
|
||||
throw new Error('First attribute must be "POSITION".');
|
||||
|
||||
let numProp = 0;
|
||||
const attributeOffsets = attributes.map((numProp = 0, def => {
|
||||
const last = numProp;
|
||||
numProp += attributeDefs[def].components;
|
||||
return last;
|
||||
}));
|
||||
|
||||
const manifoldPrimitive =
|
||||
mesh.getExtension('EXT_mesh_manifold') as ManifoldPrimitive;
|
||||
|
||||
let vertPropArray: number[] = [];
|
||||
let triVertArray: number[] = [];
|
||||
const runIndexArray = [0];
|
||||
const mergeFromVertArray = [];
|
||||
const mergeToVertArray = [];
|
||||
const runProperties: Properties[] = [];
|
||||
if (manifoldPrimitive != null) {
|
||||
const numVert = primitives[0].getAttribute('POSITION')!.getCount();
|
||||
const foundAttribute = attributes.map((a) => attributeDefs[a].type == null);
|
||||
vertPropArray = new Array<number>(numProp * numVert);
|
||||
|
||||
for (const primitive of primitives) {
|
||||
const indices = primitive.getIndices();
|
||||
if (!indices) {
|
||||
console.log('Skipping non-indexed primitive ', primitive.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
const attributesIn = primitive.listSemantics() as Attribute[];
|
||||
|
||||
attributes.forEach((attributeOut, idx) => {
|
||||
if (foundAttribute[idx]) {
|
||||
return;
|
||||
}
|
||||
for (const attributeIn of attributesIn) {
|
||||
if (attributeIn === attributeOut) {
|
||||
foundAttribute[idx] = true;
|
||||
const accessor = primitive.getAttribute(attributeIn)!;
|
||||
writeProperties(
|
||||
vertPropArray, accessor, numProp, attributeOffsets[idx]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
triVertArray = [...triVertArray, ...indices.getArray()!];
|
||||
runIndexArray.push(triVertArray.length);
|
||||
runProperties.push({
|
||||
material: primitive.getMaterial()!,
|
||||
attributes: attributesIn.filter(b => attributes.some(a => a == b))
|
||||
});
|
||||
}
|
||||
const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? [];
|
||||
const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? [];
|
||||
const vert2merge = new Map();
|
||||
for (const [i, idx] of mergeTriVert.entries()) {
|
||||
vert2merge.set(triVertArray[idx], mergeTo[i]);
|
||||
}
|
||||
for (const [from, to] of vert2merge.entries()) {
|
||||
mergeFromVertArray.push(from);
|
||||
mergeToVertArray.push(to);
|
||||
}
|
||||
} else {
|
||||
for (const primitive of primitives) {
|
||||
const indices = primitive.getIndices();
|
||||
if (!indices) {
|
||||
console.log('Skipping non-indexed primitive ', primitive.getName());
|
||||
continue;
|
||||
}
|
||||
const numVert = vertPropArray.length / numProp;
|
||||
vertPropArray =
|
||||
[...vertPropArray, ...readPrimitive(primitive, numProp, attributes)];
|
||||
triVertArray =
|
||||
[...triVertArray, ...indices.getArray()!.map((i) => i + numVert)];
|
||||
runIndexArray.push(triVertArray.length);
|
||||
|
||||
const attributesIn = primitive.listSemantics() as Attribute[];
|
||||
runProperties.push({
|
||||
material: primitive.getMaterial()!,
|
||||
attributes: attributesIn.filter(b => attributes.some(a => a == b))
|
||||
});
|
||||
}
|
||||
}
|
||||
const vertProperties = new Float32Array(vertPropArray);
|
||||
const triVerts = new Uint32Array(triVertArray);
|
||||
const runIndex = new Uint32Array(runIndexArray);
|
||||
const mergeFromVert = new Uint32Array(mergeFromVertArray);
|
||||
const mergeToVert = new Uint32Array(mergeToVertArray);
|
||||
|
||||
const meshOut =
|
||||
{numProp, triVerts, vertProperties, runIndex, mergeFromVert, mergeToVert};
|
||||
|
||||
return {mesh: meshOut, runProperties};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a Manifold Mesh into a glTF Mesh object, using the EXT_mesh_manifold
|
||||
* extension to allow for lossless roundtrip of the manifold mesh through the
|
||||
* glTF file.
|
||||
*
|
||||
* @param doc The glTF Document to which this Mesh will be added.
|
||||
* @param manifoldMesh The Manifold Mesh to convert to glTF.
|
||||
* @param id2properties A map from originalID to Properties that include the
|
||||
* glTF Material and the set of attributes to output. All triangle runs with
|
||||
* the same originalID will be combined into a single output primitive. Any
|
||||
* originalIDs not found in the map will have the glTF default material and
|
||||
* no attributes beyond 'POSITION'. Each attributes array must correspond to
|
||||
* the manifoldMesh vertProperties, thus the first attribute must always be
|
||||
* 'POSITION'. Any properties that should not be output for a given
|
||||
* primitive must use the 'SKIP_*' attributes.
|
||||
* @returns The glTF Mesh to add to the Document.
|
||||
*/
|
||||
export function writeMesh(
|
||||
doc: Document, manifoldMesh: ManifoldMesh,
|
||||
id2properties: Map<number, Properties>): Mesh {
|
||||
if (doc.getRoot().listBuffers().length === 0) {
|
||||
doc.createBuffer();
|
||||
}
|
||||
const buffer = doc.getRoot().listBuffers()[0];
|
||||
const manifoldExtension = doc.createExtension(EXTManifold);
|
||||
|
||||
const mesh = doc.createMesh();
|
||||
const runIndex = [];
|
||||
const attributeUnion: Attribute[] = [];
|
||||
const primitive2attributes = new Map<Primitive, Attribute[]>();
|
||||
const numRun = manifoldMesh.runIndex.length - 1;
|
||||
let lastID = -1;
|
||||
for (let run = 0; run < numRun; ++run) {
|
||||
const id = manifoldMesh.runOriginalID[run];
|
||||
if (id == lastID) {
|
||||
continue;
|
||||
}
|
||||
lastID = id;
|
||||
runIndex.push(manifoldMesh.runIndex[run]);
|
||||
|
||||
const indices = doc.createAccessor('primitive indices of ID ' + id)
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(1));
|
||||
const primitive = doc.createPrimitive().setIndices(indices);
|
||||
|
||||
const properties = id2properties.get(id);
|
||||
if (properties) {
|
||||
const {material, attributes} = properties;
|
||||
if (attributes.length < 1 || attributes[0] !== 'POSITION')
|
||||
throw new Error('First attribute must be "POSITION".');
|
||||
|
||||
primitive.setMaterial(material);
|
||||
primitive2attributes.set(primitive, attributes);
|
||||
|
||||
properties.attributes.forEach((attribute, i) => {
|
||||
if (i >= attributeUnion.length) {
|
||||
attributeUnion.push(attribute);
|
||||
} else {
|
||||
const size = attributeDefs[attribute].components;
|
||||
const unionSize = attributeDefs[attributeUnion[i]].components;
|
||||
if (size != unionSize) {
|
||||
throw new Error(
|
||||
'Attribute sizes do not correspond: ' + attribute + ' and ' +
|
||||
attributeUnion[i]);
|
||||
}
|
||||
if (attributeDefs[attributeUnion[i]].type == null) {
|
||||
attributeUnion[i] = attribute;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
primitive2attributes.set(primitive, ['POSITION']);
|
||||
}
|
||||
|
||||
mesh.addPrimitive(primitive);
|
||||
}
|
||||
runIndex.push(manifoldMesh.runIndex[numRun]);
|
||||
|
||||
const numVert = manifoldMesh.numVert;
|
||||
const numProp = manifoldMesh.numProp;
|
||||
let offset = 0;
|
||||
attributeUnion.forEach((attribute, aIdx) => {
|
||||
const def = attributeDefs[attribute];
|
||||
if (def == null)
|
||||
throw new Error(attribute + ' is not a recognized attribute.');
|
||||
|
||||
if (def.type == null) {
|
||||
++offset;
|
||||
return;
|
||||
}
|
||||
|
||||
const n = def.components;
|
||||
if (offset + n > numProp) throw new Error('Too many attribute channels.');
|
||||
|
||||
const array = new Float32Array(n * numVert);
|
||||
for (let v = 0; v < numVert; ++v) {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
let x = manifoldMesh.vertProperties[numProp * v + offset + i];
|
||||
if (attribute == 'COLOR_0') {
|
||||
x = Math.max(0, Math.min(1, x));
|
||||
}
|
||||
array[n * v + i] = x;
|
||||
}
|
||||
}
|
||||
|
||||
const accessor = doc.createAccessor(attribute)
|
||||
.setBuffer(buffer)
|
||||
.setType(def.type)
|
||||
.setArray(array);
|
||||
|
||||
for (const primitive of mesh.listPrimitives()) {
|
||||
const attributes = primitive2attributes.get(primitive)!;
|
||||
if (attributes.length > aIdx &&
|
||||
attributeDefs[attributes[aIdx]].type != null) {
|
||||
primitive.setAttribute(attribute, accessor);
|
||||
}
|
||||
}
|
||||
offset += n;
|
||||
});
|
||||
|
||||
const manifoldPrimitive = manifoldExtension.createManifoldPrimitive();
|
||||
mesh.setExtension('EXT_mesh_manifold', manifoldPrimitive);
|
||||
|
||||
const indices = doc.createAccessor('manifold indices')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(manifoldMesh.triVerts);
|
||||
manifoldPrimitive.setIndices(indices);
|
||||
manifoldPrimitive.setRunIndex(runIndex);
|
||||
|
||||
const vert2merge = [...Array(manifoldMesh.numVert).keys()];
|
||||
const ind = [];
|
||||
const val = [];
|
||||
if (manifoldMesh.mergeFromVert && manifoldMesh.mergeToVert) {
|
||||
for (const [i, from] of manifoldMesh.mergeFromVert.entries()) {
|
||||
vert2merge[from] = manifoldMesh.mergeToVert[i];
|
||||
}
|
||||
|
||||
for (const [i, vert] of manifoldMesh.triVerts.entries()) {
|
||||
const newVert = vert2merge[vert];
|
||||
if (vert !== newVert) {
|
||||
ind.push(i);
|
||||
val.push(newVert);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ind.length > 0) {
|
||||
const indicesAccessor = doc.createAccessor('merge from')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(ind));
|
||||
const valuesAccessor = doc.createAccessor('merge to')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(val));
|
||||
manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor);
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to dispose of a Mesh, useful when replacing an existing Mesh
|
||||
* with one from writeMesh.
|
||||
*/
|
||||
export function disposeMesh(mesh: Mesh) {
|
||||
if (!mesh) return;
|
||||
const primitives = mesh.listPrimitives();
|
||||
for (const primitive of primitives) {
|
||||
primitive.getIndices()?.dispose();
|
||||
for (const accessor of primitive.listAttributes()) {
|
||||
accessor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
const manifoldPrimitive =
|
||||
mesh.getExtension('EXT_mesh_manifold') as ManifoldPrimitive;
|
||||
if (manifoldPrimitive) {
|
||||
manifoldPrimitive.getIndices()?.dispose();
|
||||
manifoldPrimitive.getMergeIndices()?.dispose();
|
||||
manifoldPrimitive.getMergeValues()?.dispose();
|
||||
}
|
||||
|
||||
mesh.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to download an image and apply it to the given texture.
|
||||
*
|
||||
* @param texture The texture to update
|
||||
* @param uri The location of the image to download
|
||||
*/
|
||||
export async function loadTexture(texture: Texture, uri: string) {
|
||||
const response = await fetch(uri);
|
||||
const blob = await response.blob();
|
||||
texture.setMimeType(blob.type);
|
||||
texture.setImage(new Uint8Array(await blob.arrayBuffer()));
|
||||
}
|
||||
|
||||
function writeProperties(
|
||||
vertProperties: number[], accessor: Accessor, numProp: number,
|
||||
offset: number) {
|
||||
const array = accessor.getArray()!;
|
||||
const size = accessor.getElementSize();
|
||||
const numVert = accessor.getCount();
|
||||
for (let i = 0; i < numVert; ++i) {
|
||||
for (let j = 0; j < size; ++j) {
|
||||
vertProperties[numProp * i + offset + j] = array[i * size + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readPrimitive(
|
||||
primitive: Primitive, numProp: number, attributes: Attribute[]) {
|
||||
const vertProperties: number[] = [];
|
||||
let offset = 0;
|
||||
for (const attribute of attributes) {
|
||||
const size = attributeDefs[attribute].components;
|
||||
if (attributeDefs[attribute].type == null) {
|
||||
offset += size;
|
||||
continue;
|
||||
}
|
||||
const accessor = primitive.getAttribute(attribute);
|
||||
if (accessor) {
|
||||
writeProperties(vertProperties, accessor, numProp, offset);
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
return vertProperties;
|
||||
}
|
||||
@@ -39,8 +39,15 @@
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container header" style="background-color: #A288B3;">
|
||||
<p style="flex: 2;"><a target="_blank" href="https://github.com/elalish/manifold">Manifold</a>CAD </p>
|
||||
<div class="margin" id="file-margin" >
|
||||
<p style="flex: 2;"><a target="_blank" href="https://github.com/elalish/manifold">Manifold</a>CAD
|
||||
<span class="top-edit blue">
|
||||
<button type="button" id="undo" class="top-icon undo squish"></button>
|
||||
<button type="button" id="redo" class="top-icon redo squish"></button>
|
||||
<button type="button" id="format" class="top-icon star squish"></button>
|
||||
<button type="button" id="share" class="top-icon share"></button>
|
||||
</span>
|
||||
</p>
|
||||
<div class="margin" id="file-margin">
|
||||
<button type="button" id="file" class="blue item"><b class="uparrow"></b> <span
|
||||
id="current">Intro</span></button>
|
||||
<div class="dropdown" id="fileDropdown">
|
||||
@@ -66,10 +73,14 @@
|
||||
<div id="editor" class="col"></div>
|
||||
<div class="col container" style="flex-direction: column;">
|
||||
<model-viewer camera-controls min-camera-orbit="auto 0deg auto" max-camera-orbit="auto 180deg auto"
|
||||
shadow-intensity="1" interaction-prompt="none" alt="Editor 3D output">
|
||||
shadow-intensity="1" tone-mapping="commerce" interaction-prompt="none" alt="Editor 3D output">
|
||||
<div class="center" slot="poster">
|
||||
<p id="poster" style="text-align: center; line-height: 36px;"> Loading... </p>
|
||||
</div>
|
||||
<div id="animation">
|
||||
<button type="button" id="play" class="icon pause"></button>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slider" id="scrubber">
|
||||
</div>
|
||||
</model-viewer>
|
||||
<div id="console"></div>
|
||||
</div>
|
||||
@@ -84,6 +95,6 @@
|
||||
integrity="sha512-iQEIc0rsSDujsfjtD+lfyJ1W23Bh/lbgriubKDAym6VlEIDRj9rrbSIyJRyshOrl8s0yRcQ0+gyrZfSLyjJGWQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<script type="module" src="editor.js"></script>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.1.1/model-viewer.min.js"></script>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js"></script>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@
|
||||
<body>
|
||||
<p>Load a glTF/GLB model and our <a href="https://github.com/elalish/manifold">Manifold</a> library will attempt to
|
||||
merge it into a set of manifold objects. If the model is water-tight, you can download the new GLB which will have
|
||||
our <a href="https://github.com/KhronosGroup/glTF/pull/2286">EXT_manifold</a> extension, thus preserving the
|
||||
our <a href="https://github.com/KhronosGroup/glTF/pull/2286">EXT_mesh_manifold</a> extension, thus preserving the
|
||||
manifold data without losing any mesh properties.</p>
|
||||
<p> If the View Manifold GLB checkbox is enabled, then some meshes in the model have open edges and don't represent a
|
||||
solid object. Check this box to see only the manifold parts, which will also enable them to be downloaded. If the
|
||||
@@ -55,17 +55,17 @@
|
||||
<button id="download" disabled>Download manifold GLB</button>
|
||||
<input type="checkbox" id="viewFinal" disabled>
|
||||
<label for="viewFinal">View Manifold GLB</label>
|
||||
<model-viewer camera-controls shadow-intensity="1" alt="Loaded GLB model">
|
||||
<model-viewer camera-controls shadow-intensity="1" tone-mapping="commerce" alt="Loaded GLB model">
|
||||
<span id="poster" slot="poster">Drop a GLB here</span>
|
||||
</model-viewer>
|
||||
</body>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/2.0.1/model-viewer.min.js"></script>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js"></script>
|
||||
<script type="module">
|
||||
import { WebIO } from '@gltf-transform/core';
|
||||
import { prune } from '@gltf-transform/functions';
|
||||
import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions';
|
||||
import { SimpleDropzone } from 'simple-dropzone';
|
||||
import { readMesh, writeMesh, setupIO } from './gltf-io.js';
|
||||
import { readMesh, writeMesh, setupIO, disposeMesh } from './gltf-io';
|
||||
import Module from './built/manifold.js';
|
||||
|
||||
const io = setupIO(new WebIO());
|
||||
@@ -147,31 +147,30 @@
|
||||
const docIn = await io.read(url);
|
||||
const nodes = docIn.getRoot().listNodes();
|
||||
for (const node of nodes) {
|
||||
const attributes = [];
|
||||
const mesh = node.getMesh();
|
||||
if (!mesh) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const materials = [];
|
||||
const tmpMesh = readMesh(mesh, attributes, materials);
|
||||
tmpMesh.runOriginalID = [];
|
||||
const firstID = Manifold.reserveIDs(materials.length);
|
||||
for (let i = 0; i < materials.length; ++i) {
|
||||
tmpMesh.runOriginalID.push(firstID + i);
|
||||
}
|
||||
const manifoldMesh = new Mesh(tmpMesh);
|
||||
mesh.dispose();
|
||||
const tmp = readMesh(mesh);
|
||||
|
||||
const matAttributes = [];
|
||||
materials.forEach(() => matAttributes.push(attributes));
|
||||
tmp.mesh.runOriginalID = [];
|
||||
const id2properties = new Map();
|
||||
const numID = tmp.runProperties.length;
|
||||
const firstID = Manifold.reserveIDs(numID);
|
||||
for (let i = 0; i < numID; ++i) {
|
||||
tmp.mesh.runOriginalID.push(firstID + i);
|
||||
id2properties.set(firstID + i, tmp.runProperties[i]);
|
||||
}
|
||||
const manifoldMesh = new Mesh(tmp.mesh);
|
||||
disposeMesh(mesh);
|
||||
|
||||
manifoldMesh.merge();
|
||||
|
||||
try {
|
||||
// Test manifoldness - will throw if not.
|
||||
const manifold = Manifold(manifoldMesh);
|
||||
node.setMesh(writeMesh(docIn, manifold.getMesh(), matAttributes, materials));
|
||||
node.setMesh(writeMesh(docIn, manifold.getMesh(), id2properties));
|
||||
manifold.delete();
|
||||
anyManifold = true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -12,15 +12,32 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Extension, ExtensionProperty, PropertyType} from '@gltf-transform/core';
|
||||
import {Accessor, Extension, ExtensionProperty, GLTF, IProperty, PropertyType, ReaderContext, WriterContext} from '@gltf-transform/core';
|
||||
|
||||
const NAME = 'EXT_manifold';
|
||||
const NAME = 'EXT_mesh_manifold';
|
||||
const MERGE = 'MERGE';
|
||||
|
||||
export class ManifoldPrimitive extends ExtensionProperty {
|
||||
interface IManifoldPrimitive extends IProperty {
|
||||
mergeIndices: Accessor;
|
||||
mergeValues: Accessor;
|
||||
indices: Accessor;
|
||||
runIndex: number[]|Uint32Array;
|
||||
}
|
||||
|
||||
interface ManifoldDef {
|
||||
manifoldPrimitive: GLTF.IMeshPrimitive;
|
||||
mergeIndices: number;
|
||||
mergeValues: number;
|
||||
}
|
||||
|
||||
export class ManifoldPrimitive extends ExtensionProperty<IManifoldPrimitive> {
|
||||
static EXTENSION_NAME = NAME;
|
||||
public declare extensionName: typeof NAME;
|
||||
public declare propertyType: 'ManifoldPrimitive';
|
||||
public declare parentTypes: [PropertyType.MESH];
|
||||
|
||||
init() {
|
||||
this.EXTENSION_NAME = NAME;
|
||||
ManifoldPrimitive.EXTENSION_NAME = NAME;
|
||||
this.propertyType = 'ManifoldPrimitive';
|
||||
this.parentTypes = [PropertyType.MESH];
|
||||
}
|
||||
@@ -39,7 +56,7 @@ export class ManifoldPrimitive extends ExtensionProperty {
|
||||
return this.getRef('mergeValues');
|
||||
}
|
||||
|
||||
setMerge(indicesAccessor, valuesAccessor) {
|
||||
setMerge(indicesAccessor: Accessor, valuesAccessor: Accessor) {
|
||||
if (indicesAccessor.getCount() !== valuesAccessor.getCount())
|
||||
throw new Error('merge vectors must be the same length.');
|
||||
this.setRef('mergeIndices', indicesAccessor);
|
||||
@@ -50,28 +67,29 @@ export class ManifoldPrimitive extends ExtensionProperty {
|
||||
return this.get('runIndex');
|
||||
}
|
||||
|
||||
setRunIndex(runIndex) {
|
||||
setRunIndex(runIndex: number[]|Uint32Array) {
|
||||
return this.set('runIndex', runIndex);
|
||||
}
|
||||
|
||||
setIndices(indices) {
|
||||
setIndices(indices: Accessor) {
|
||||
return this.setRef('indices', indices);
|
||||
}
|
||||
|
||||
getIndices() {
|
||||
return this.getRef('indices');
|
||||
return this.getRef('indices')!;
|
||||
}
|
||||
}
|
||||
|
||||
export class EXTManifold extends Extension {
|
||||
extensionName = NAME;
|
||||
prewriteTypes = [PropertyType.ACCESSOR];
|
||||
static EXTENSION_NAME = NAME;
|
||||
|
||||
createManifoldPrimitive() {
|
||||
return new ManifoldPrimitive(this.document.getGraph());
|
||||
}
|
||||
|
||||
read(context) {
|
||||
read(context: ReaderContext) {
|
||||
const {json} = context.jsonDoc;
|
||||
const meshDefs = json.meshes || [];
|
||||
|
||||
@@ -82,20 +100,27 @@ export class EXTManifold extends Extension {
|
||||
const manifoldPrimitive = this.createManifoldPrimitive();
|
||||
mesh.setExtension(NAME, manifoldPrimitive);
|
||||
|
||||
const manifoldDef = meshDef.extensions[NAME];
|
||||
const manifoldDef = meshDef.extensions[NAME] as ManifoldDef;
|
||||
|
||||
if (manifoldDef.manifoldPrimitive) {
|
||||
let count = 0;
|
||||
const runIndex = [];
|
||||
runIndex.push(count);
|
||||
for (const primitive of mesh.listPrimitives()) {
|
||||
count += primitive.getIndices().getCount();
|
||||
const indices = primitive.getIndices();
|
||||
if (!indices) {
|
||||
console.log('Skipping non-indexed primitive ', primitive.getName());
|
||||
continue;
|
||||
}
|
||||
count += indices.getCount();
|
||||
runIndex.push(count);
|
||||
}
|
||||
manifoldPrimitive.setRunIndex(runIndex);
|
||||
manifoldPrimitive.setIndices(
|
||||
context.accessors[manifoldDef.manifoldPrimitive.indices!]);
|
||||
}
|
||||
|
||||
if (manifoldDef.mergeIndices && manifoldDef.mergeValues) {
|
||||
if (manifoldDef.mergeIndices != null && manifoldDef.mergeValues != null) {
|
||||
manifoldPrimitive.setMerge(
|
||||
context.accessors[manifoldDef.mergeIndices],
|
||||
context.accessors[manifoldDef.mergeValues]);
|
||||
@@ -105,15 +130,34 @@ export class EXTManifold extends Extension {
|
||||
return this;
|
||||
}
|
||||
|
||||
write(context) {
|
||||
prewrite(context: WriterContext) {
|
||||
this.document.getRoot().listMeshes().forEach((mesh) => {
|
||||
const manifoldPrimitive = mesh.getExtension(NAME) as ManifoldPrimitive;
|
||||
if (!manifoldPrimitive) return;
|
||||
|
||||
const indices = manifoldPrimitive.getIndices();
|
||||
context.addAccessorToUsageGroup(
|
||||
indices, WriterContext.BufferViewUsage.ELEMENT_ARRAY_BUFFER);
|
||||
|
||||
const mergeFrom = manifoldPrimitive.getMergeIndices();
|
||||
const mergeTo = manifoldPrimitive.getMergeValues();
|
||||
if (!mergeFrom || !mergeTo) return;
|
||||
|
||||
context.addAccessorToUsageGroup(mergeFrom, MERGE);
|
||||
context.addAccessorToUsageGroup(mergeTo, MERGE);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
write(context: WriterContext) {
|
||||
const {json} = context.jsonDoc;
|
||||
|
||||
this.document.getRoot().listMeshes().forEach((mesh) => {
|
||||
const manifoldPrimitive = mesh.getExtension(NAME);
|
||||
const manifoldPrimitive = mesh.getExtension(NAME) as ManifoldPrimitive;
|
||||
if (!manifoldPrimitive) return;
|
||||
|
||||
const meshIndex = context.meshIndexMap.get(mesh);
|
||||
const meshDef = json.meshes[meshIndex];
|
||||
const meshIndex = context.meshIndexMap.get(mesh)!;
|
||||
const meshDef = json.meshes![meshIndex];
|
||||
|
||||
const runIndex = manifoldPrimitive.getRunIndex();
|
||||
const numPrimitive = runIndex.length - 1;
|
||||
@@ -124,20 +168,20 @@ export class EXTManifold extends Extension {
|
||||
}
|
||||
|
||||
const mergeIndicesIndex =
|
||||
context.accessorIndexMap.get(manifoldPrimitive.getMergeIndices());
|
||||
context.accessorIndexMap.get(manifoldPrimitive.getMergeIndices()!)!;
|
||||
const mergeValuesIndex =
|
||||
context.accessorIndexMap.get(manifoldPrimitive.getMergeValues());
|
||||
const mergeIndices = json.accessors[mergeIndicesIndex];
|
||||
const mergeValues = json.accessors[mergeValuesIndex];
|
||||
context.accessorIndexMap.get(manifoldPrimitive.getMergeValues()!)!;
|
||||
const mergeIndices = json.accessors![mergeIndicesIndex];
|
||||
const mergeValues = json.accessors![mergeValuesIndex];
|
||||
|
||||
const existingPrimitive = meshDef.primitives[0];
|
||||
const primitive = {
|
||||
indices: context.accessorIndexMap.get(manifoldPrimitive.getIndices()),
|
||||
indices: context.accessorIndexMap.get(manifoldPrimitive.getIndices())!,
|
||||
mode: existingPrimitive.mode,
|
||||
attributes: {'POSITION': existingPrimitive.attributes['POSITION']}
|
||||
};
|
||||
|
||||
const indices = json.accessors[primitive.indices];
|
||||
const indices = json.accessors![primitive.indices];
|
||||
if (!indices) {
|
||||
return;
|
||||
}
|
||||
@@ -146,21 +190,21 @@ export class EXTManifold extends Extension {
|
||||
indices.sparse = {
|
||||
count: mergeIndices.count,
|
||||
indices: {
|
||||
bufferView: mergeIndices.bufferView,
|
||||
bufferView: mergeIndices.bufferView!,
|
||||
byteOffset: mergeIndices.byteOffset,
|
||||
componentType: mergeIndices.componentType
|
||||
},
|
||||
values: {
|
||||
bufferView: mergeValues.bufferView,
|
||||
bufferView: mergeValues.bufferView!,
|
||||
byteOffset: mergeValues.byteOffset,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 0; i < numPrimitive; ++i) {
|
||||
const accessor = json.accessors[meshDef.primitives[i].indices];
|
||||
const accessor = json.accessors![meshDef.primitives[i].indices!];
|
||||
accessor.bufferView = indices.bufferView;
|
||||
accessor.byteOffset = indices.byteOffset + 4 * runIndex[i];
|
||||
accessor.byteOffset = indices.byteOffset! + 4 * runIndex[i];
|
||||
accessor.count = runIndex[i + 1] - runIndex[i];
|
||||
}
|
||||
|
||||
@@ -46,13 +46,14 @@
|
||||
<option value="union">Union</option>
|
||||
</select>
|
||||
<button id="download">Download manifold GLB</button>
|
||||
<model-viewer camera-controls shadow-intensity="1" alt="Output of mesh Boolean operation"></model-viewer>
|
||||
<model-viewer camera-controls shadow-intensity="1" tone-mapping="commerce"
|
||||
alt="Output of mesh Boolean operation"></model-viewer>
|
||||
</body>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/2.0.1/model-viewer.min.js"></script>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js"></script>
|
||||
<script type="module">
|
||||
import { Document, WebIO } from '@gltf-transform/core';
|
||||
import { clearNodeTransform, flatten, prune } from '@gltf-transform/functions';
|
||||
import { loadTexture, readMesh, writeMesh, disposeMesh, setupIO } from './gltf-io.js';
|
||||
import { loadTexture, readMesh, writeMesh, disposeMesh, setupIO } from './gltf-io';
|
||||
import Module from './built/manifold.js';
|
||||
|
||||
const io = setupIO(new WebIO());
|
||||
@@ -63,10 +64,9 @@
|
||||
|
||||
const { Manifold, Mesh } = wasm;
|
||||
|
||||
const attributes = ['POSITION', 'TEXCOORD_0'];
|
||||
const materialMap = new Map();
|
||||
const id2properties = new Map();
|
||||
|
||||
async function readGLB(url, attributes, materialMap) {
|
||||
async function readGLB(url) {
|
||||
const manifolds = [];
|
||||
const docIn = await io.read(url);
|
||||
await docIn.transform(flatten());
|
||||
@@ -75,30 +75,33 @@
|
||||
for (const node of nodes) {
|
||||
clearNodeTransform(node);
|
||||
const materials = [];
|
||||
const manifoldMesh = readMesh(node.getMesh(), attributes, materials);
|
||||
manifoldMesh.runOriginalID = [];
|
||||
const firstID = Manifold.reserveIDs(materials.length);
|
||||
for (let i = 0; i < materials.length; ++i) {
|
||||
manifoldMesh.runOriginalID.push(firstID + i);
|
||||
const tmp = readMesh(node.getMesh());
|
||||
tmp.mesh.runOriginalID = [];
|
||||
const numID = tmp.runProperties.length;
|
||||
const firstID = Manifold.reserveIDs(numID);
|
||||
for (let i = 0; i < numID; ++i) {
|
||||
tmp.mesh.runOriginalID.push(firstID + i);
|
||||
ids.push(firstID + i);
|
||||
id2properties.set(firstID + i, tmp.runProperties[i]);
|
||||
}
|
||||
manifolds.push(Manifold(new Mesh(manifoldMesh)));
|
||||
manifolds.push(Manifold(new Mesh(tmp.mesh)));
|
||||
}
|
||||
// pull in materials, TODO: replace with transfer() when available
|
||||
const startIdx = doc.getRoot().listMaterials().length;
|
||||
doc.merge(docIn);
|
||||
doc.getRoot().listScenes().forEach((s) => s.dispose());
|
||||
doc.getRoot().listBuffers().forEach((s) => s.dispose());
|
||||
doc.getRoot().listAccessors().forEach((s) => s.dispose());
|
||||
for (const [i, id] of ids.entries()) {
|
||||
const material = doc.getRoot().listMaterials()[startIdx + i];
|
||||
materialMap.set(id, material);
|
||||
id2properties.get(id).material = material;
|
||||
}
|
||||
|
||||
return Manifold.union(manifolds);
|
||||
}
|
||||
|
||||
const space = await readGLB('/models/space.glb', attributes, materialMap);
|
||||
const moon = await readGLB('/models/moon.glb', attributes, materialMap);
|
||||
const space = await readGLB('/models/space.glb');
|
||||
const moon = await readGLB('/models/moon.glb');
|
||||
|
||||
const node = doc.createNode();
|
||||
doc.createScene().addChild(node);
|
||||
@@ -129,13 +132,7 @@
|
||||
const manifoldMesh = manifold.rotate([-90, 0, 0]).getMesh();
|
||||
|
||||
disposeMesh(node.getMesh());
|
||||
const materials = [];
|
||||
const matAttributes = [];
|
||||
for (const id of manifoldMesh.runOriginalID) {
|
||||
materials.push(materialMap.get(id));
|
||||
matAttributes.push(attributes);
|
||||
}
|
||||
const mesh = writeMesh(doc, manifoldMesh, matAttributes, materials);
|
||||
const mesh = writeMesh(doc, manifoldMesh, id2properties);
|
||||
node.setMesh(mesh);
|
||||
await doc.transform(prune());
|
||||
|
||||
|
||||
@@ -8,25 +8,25 @@
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:run": "vitest run"
|
||||
"test:dev": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gltf-transform/core": "^3.2.1",
|
||||
"@gltf-transform/extensions": "^3.2.1",
|
||||
"@gltf-transform/functions": "^3.2.1",
|
||||
"@jscadui/3mf-export": "^0.4.2",
|
||||
"@gltf-transform/core": "^3.8.0",
|
||||
"@gltf-transform/extensions": "^3.8.0",
|
||||
"@gltf-transform/functions": "^3.8.0",
|
||||
"@jscadui/3mf-export": "^0.5.0",
|
||||
"fflate": "^0.8.0",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"simple-dropzone": "0.8.3",
|
||||
"three": "0.151.2"
|
||||
"three": "0.158.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^0.31.1",
|
||||
"@vitest/web-worker": "^0.31.1",
|
||||
"typescript": "5.0.4",
|
||||
"vite": "^4.3.2",
|
||||
"typescript": "5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
"vitest": "^0.31.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,11 @@ declare class GLTFNode {
|
||||
clone(parent?: GLTFNode): GLTFNode;
|
||||
}
|
||||
|
||||
type Attribute = 'POSITION'|'NORMAL'|'TANGENT'|'TEXCOORD_0'|'TEXCOORD_1'|
|
||||
'COLOR_0'|'JOINTS_0'|'WEIGHTS_0'|'SKIP_1'|'SKIP_2'|'SKIP_3'|'SKIP_4';
|
||||
|
||||
declare class GLTFMaterial {
|
||||
attributes?: string[];
|
||||
attributes?: Attribute[];
|
||||
roughness?: number;
|
||||
metallic?: number;
|
||||
baseColorFactor?: [number, number, number];
|
||||
@@ -35,15 +38,48 @@ declare class GLTFMaterial {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
declare const globalDefaults: {
|
||||
roughness: number,
|
||||
metallic: number,
|
||||
baseColorFactor: [number, number, number],
|
||||
alpha: number,
|
||||
unlit: boolean,
|
||||
animationLength: number,
|
||||
animationMode: 'loop'|'ping-pong';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set material properties on the input manifold. They will be carried along
|
||||
* through operations.
|
||||
* Returns a shallow copy of the input manifold with the given material
|
||||
* properties applied. They will be carried along through operations.
|
||||
*
|
||||
* @param manifold The object to add properties to - returned for chaining.
|
||||
* @param manifold The input object.
|
||||
* @param material A set of material properties to apply to this manifold.
|
||||
*/
|
||||
declare function setMaterial(
|
||||
manifold: Manifold, material: GLTFMaterial): Manifold;
|
||||
declare function setMaterial(manifold: Manifold, material: GLTFMaterial):
|
||||
Manifold;
|
||||
|
||||
/**
|
||||
* Apply a morphing animation to the input manifold. Specify the start
|
||||
* function which will be applied to the vertex positions of the first frame and
|
||||
* linearly interpolated across the length of the overall animation. This
|
||||
* animation will only be shown if this manifold is used directly on a GLTFNode.
|
||||
*
|
||||
* @param manifold The object to add morphing animation to.
|
||||
* @param func A warping function to apply to the first animation frame.
|
||||
*/
|
||||
declare function setMorphStart(
|
||||
manifold: Manifold, func: (v: Vec3) => void): void;
|
||||
|
||||
/**
|
||||
* Apply a morphing animation to the input manifold. Specify the end
|
||||
* function which will be applied to the vertex positions of the last frame and
|
||||
* linearly interpolated across the length of the overall animation. This
|
||||
* animation will only be shown if this manifold is used directly on a GLTFNode.
|
||||
*
|
||||
* @param manifold The object to add morphing animation to.
|
||||
* @param func A warping function to apply to the last animation frame.
|
||||
*/
|
||||
declare function setMorphEnd(manifold: Manifold, func: (v: Vec3) => void): void;
|
||||
|
||||
/**
|
||||
* Wrap any shape object with this method to display it and any copies in
|
||||
|
||||
@@ -50,9 +50,9 @@ export const examples = {
|
||||
|
||||
TetrahedronPuzzle: function() {
|
||||
// A tetrahedron cut into two identical halves that can screw together as
|
||||
// a puzzle. This only outputs one of the halves. This demonstrates how
|
||||
// redundant points along a polygon can be used to make twisted extrusions
|
||||
// smoother. Based on the screw puzzle by George Hart:
|
||||
// a puzzle. This demonstrates how redundant points along a polygon can be
|
||||
// used to make twisted extrusions smoother. It also showcases animated
|
||||
// assemblies. Based on the screw puzzle by George Hart:
|
||||
// https://www.thingiverse.com/thing:186372
|
||||
|
||||
const edgeLength = 50; // Length of each edge of the overall tetrahedron.
|
||||
@@ -61,19 +61,21 @@ export const examples = {
|
||||
|
||||
const scale = edgeLength / (2 * Math.sqrt(2));
|
||||
|
||||
const tet = Manifold.tetrahedron().scale(scale);
|
||||
const tet = Manifold.tetrahedron().intersect(
|
||||
Manifold.tetrahedron().rotate([0, 0, 90]).scale(2.5));
|
||||
|
||||
const box = [];
|
||||
box.push([1, -1], [1, 1]);
|
||||
box.push([2, -2], [2, 2]);
|
||||
for (let i = 0; i <= nDivisions; ++i) {
|
||||
box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]);
|
||||
box.push([gap / (2 * scale), 2 - i * 4 / nDivisions]);
|
||||
}
|
||||
|
||||
const cyan = [0, 1, 1];
|
||||
const magenta = [1, 0, 1];
|
||||
const fade = (color, pos) => {
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
color[i] = cyan[i] * pos[2] + magenta[i] * (1 - pos[2]);
|
||||
const x = pos[2] / 2;
|
||||
color[i] = cyan[i] * x + magenta[i] * (1 - x);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,25 +84,43 @@ export const examples = {
|
||||
// channels as colors, and sets the factor to white, since our default is
|
||||
// yellow.
|
||||
const screw = setMaterial(
|
||||
Manifold.extrude(box, 1, nDivisions, 270).setProperties(3, fade),
|
||||
Manifold.extrude(box, 2, nDivisions, 270).setProperties(3, fade),
|
||||
{baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']});
|
||||
|
||||
const result = tet.intersect(
|
||||
screw.rotate([0, 0, -45]).translate([0, 0, -0.5]).scale(2 * scale));
|
||||
const result =
|
||||
tet.intersect(screw.rotate([0, 0, -45]).translate([0, 0, -1]))
|
||||
.scale(scale);
|
||||
|
||||
// Assigned materials are only applied to a GLTFNode. Note that material
|
||||
// definitions cascade, applying recursively to all child surfaces, but
|
||||
// overridden by any materials defined lower down. Our default material:
|
||||
// {
|
||||
// roughness = 0.2,
|
||||
// metallic = 1,
|
||||
// baseColorFactor = [1, 1, 0],
|
||||
// alpha = 1,
|
||||
// unlit = false,
|
||||
// name = ''
|
||||
// }
|
||||
const node = new GLTFNode();
|
||||
node.manifold = result;
|
||||
// overridden by any materials defined lower down. The default material
|
||||
// properties, as well as animation parameters can be set via
|
||||
// globalDefaults.
|
||||
|
||||
const layFlat = new GLTFNode();
|
||||
layFlat.rotation =
|
||||
[45, -Math.atan(1 / Math.sqrt(2)) * 180 / Math.PI, 120];
|
||||
layFlat.translation = [0, 0, scale * Math.sqrt(3) / 3];
|
||||
|
||||
const fixed = new GLTFNode(layFlat);
|
||||
fixed.manifold = result;
|
||||
fixed.rotation = [0, 0, 180];
|
||||
|
||||
// For 3MF export, only top-level objects are independently arrangeable.
|
||||
const layFlat2 = layFlat.clone();
|
||||
|
||||
const moving = new GLTFNode(layFlat2);
|
||||
moving.manifold = result;
|
||||
// Use functions to create animation, which runs from t=0 to t=1.
|
||||
moving.translation = (t) => {
|
||||
const a = 1 - t;
|
||||
const x = a > 0.5 ? scale * 2 * (0.5 - a) : 0;
|
||||
return [x, x, -2 * x + scale * 4 * (0.5 - Math.abs(0.5 - a))];
|
||||
};
|
||||
moving.rotation = (t) => [0, 0, 270 * 2 * (1 - t)];
|
||||
|
||||
globalDefaults.animationLength = 10; // seconds
|
||||
globalDefaults.animationMode = 'ping-pong';
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -139,6 +159,7 @@ export const examples = {
|
||||
// Smooth, complex manifolds can be created using the warp() function.
|
||||
// This example recreates the Exploitable Heart by Emmett Lalish:
|
||||
// https://www.thingiverse.com/thing:6190
|
||||
// It also demonstrates the use of setMorph to animate a warping function.
|
||||
|
||||
const func = (v) => {
|
||||
const x2 = v[0] * v[0];
|
||||
@@ -174,9 +195,20 @@ export const examples = {
|
||||
};
|
||||
|
||||
const ball = Manifold.sphere(1, 200);
|
||||
|
||||
const heart = ball.warp(func);
|
||||
const box = heart.boundingBox();
|
||||
const result = heart.scale(100 / (box.max[0] - box.min[0]));
|
||||
const scale = 100 / (box.max[0] - box.min[0]);
|
||||
|
||||
setMorphStart(ball, func);
|
||||
const node = new GLTFNode();
|
||||
node.manifold = ball;
|
||||
node.scale = [scale, scale, scale];
|
||||
|
||||
globalDefaults.animationLength = 5; // seconds
|
||||
globalDefaults.animationMode = 'ping-pong';
|
||||
|
||||
const result = heart.scale(scale);
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -253,71 +285,68 @@ export const examples = {
|
||||
// an example of using the warp() method, thus avoiding any direct
|
||||
// handling of triangles.
|
||||
|
||||
// @param p The number of times the thread passes through the donut hole.
|
||||
// @param q The number of times the thread circles the donut.
|
||||
// @param majorRadius Radius of the interior of the imaginary donut.
|
||||
// @param minorRadius Radius of the small cross-section of the imaginary
|
||||
// donut.
|
||||
// @param threadRadius Radius of the small cross-section of the actual
|
||||
// object.
|
||||
// @param circularSegments Number of linear segments making up the
|
||||
// threadRadius circle. Default is getCircularSegments(threadRadius).
|
||||
// @param linearSegments Number of segments along the length of the knot.
|
||||
// Default makes roughly square facets.
|
||||
// The number of times the thread passes through the donut hole.
|
||||
const p = 1;
|
||||
// The number of times the thread circles the donut.
|
||||
const q = 3;
|
||||
// Radius of the interior of the imaginary donut.
|
||||
const majorRadius = 25;
|
||||
// Radius of the small cross-section of the imaginary donut.
|
||||
const minorRadius = 10;
|
||||
// Radius of the small cross-section of the actual object.
|
||||
const threadRadius = 3.75;
|
||||
// Number of linear segments making up the threadRadius circle. Default is
|
||||
// getCircularSegments(threadRadius).
|
||||
const circularSegments = -1;
|
||||
// Number of segments along the length of the knot. Default makes roughly
|
||||
// square facets.
|
||||
const linearSegments = -1;
|
||||
|
||||
function torusKnot(
|
||||
p, q, majorRadius, minorRadius, threadRadius, circularSegments = 0,
|
||||
linearSegments = 0) {
|
||||
const {vec3} = glMatrix;
|
||||
|
||||
function gcd(a, b) {
|
||||
return b == 0 ? a : gcd(b, a % b);
|
||||
}
|
||||
|
||||
const kLoops = gcd(p, q);
|
||||
p /= kLoops;
|
||||
q /= kLoops;
|
||||
const n = circularSegments > 2 ? circularSegments :
|
||||
getCircularSegments(threadRadius);
|
||||
const m = linearSegments > 2 ? linearSegments :
|
||||
n * q * majorRadius / threadRadius;
|
||||
|
||||
const offset = 2
|
||||
const circle = CrossSection.circle(1, n).translate([offset, 0]);
|
||||
|
||||
const func = (v) => {
|
||||
const psi = q * Math.atan2(v[0], v[1]);
|
||||
const theta = psi * p / q;
|
||||
const x1 = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
||||
const phi = Math.atan2(x1 - offset, v[2]);
|
||||
vec3.set(
|
||||
v, threadRadius * Math.cos(phi), 0, threadRadius * Math.sin(phi));
|
||||
const center = vec3.fromValues(0, 0, 0);
|
||||
const r = majorRadius + minorRadius * Math.cos(theta);
|
||||
vec3.rotateX(v, v, center, -Math.atan2(p * minorRadius, q * r));
|
||||
v[0] += minorRadius;
|
||||
vec3.rotateY(v, v, center, theta);
|
||||
v[0] += majorRadius;
|
||||
vec3.rotateZ(v, v, center, psi);
|
||||
};
|
||||
|
||||
let knot = Manifold.revolve(circle, m).warp(func);
|
||||
|
||||
if (kLoops > 1) {
|
||||
const knots = [];
|
||||
for (let k = 0; k < kLoops; ++k) {
|
||||
knots.push(knot.rotate([0, 0, 360 * (k / kLoops) * (q / p)]));
|
||||
}
|
||||
knot = Manifold.compose(knots);
|
||||
}
|
||||
|
||||
return knot;
|
||||
}
|
||||
|
||||
// This recreates Matlab Knot by Emmett Lalish:
|
||||
// These default values recreate Matlab Knot by Emmett Lalish:
|
||||
// https://www.thingiverse.com/thing:7080
|
||||
|
||||
const result = torusKnot(1, 3, 25, 10, 3.75);
|
||||
const {vec3} = glMatrix;
|
||||
|
||||
function gcd(a, b) {
|
||||
return b == 0 ? a : gcd(b, a % b);
|
||||
}
|
||||
|
||||
const kLoops = gcd(p, q);
|
||||
const pk = p / kLoops;
|
||||
const qk = q / kLoops;
|
||||
const n = circularSegments > 2 ? circularSegments :
|
||||
getCircularSegments(threadRadius);
|
||||
const m = linearSegments > 2 ? linearSegments :
|
||||
n * qk * majorRadius / threadRadius;
|
||||
|
||||
const offset = 2
|
||||
const circle = CrossSection.circle(1, n).translate([offset, 0]);
|
||||
|
||||
const func = (v) => {
|
||||
const psi = qk * Math.atan2(v[0], v[1]);
|
||||
const theta = psi * pk / qk;
|
||||
const x1 = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
||||
const phi = Math.atan2(x1 - offset, v[2]);
|
||||
vec3.set(
|
||||
v, threadRadius * Math.cos(phi), 0, threadRadius * Math.sin(phi));
|
||||
const center = vec3.fromValues(0, 0, 0);
|
||||
const r = majorRadius + minorRadius * Math.cos(theta);
|
||||
vec3.rotateX(v, v, center, -Math.atan2(pk * minorRadius, qk * r));
|
||||
v[0] += minorRadius;
|
||||
vec3.rotateY(v, v, center, theta);
|
||||
v[0] += majorRadius;
|
||||
vec3.rotateZ(v, v, center, psi);
|
||||
};
|
||||
|
||||
const result = Manifold.revolve(circle, m).warp(func);
|
||||
|
||||
for (let k = 0; k < kLoops; ++k) {
|
||||
const node = new GLTFNode();
|
||||
node.manifold = result;
|
||||
node.rotation = [0, 0, 360 * (k / kLoops) * (qk / pk)];
|
||||
node.translation = [0, 0, result.boundingBox().min[2]];
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -350,12 +379,12 @@ export const examples = {
|
||||
|
||||
const hole = Manifold.compose(holes);
|
||||
|
||||
result = Manifold.difference(
|
||||
result,
|
||||
hole,
|
||||
hole.rotate([90, 0, 0]),
|
||||
hole.rotate([0, 90, 0]),
|
||||
);
|
||||
result = Manifold.difference([
|
||||
result,
|
||||
hole,
|
||||
hole.rotate([90, 0, 0]),
|
||||
hole.rotate([0, 90, 0]),
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 240 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 619 B |
|
After Width: | Height: | Size: 675 B |
|
After Width: | Height: | Size: 471 B |
@@ -18,7 +18,7 @@ import {WebIO} from '@gltf-transform/core';
|
||||
import {expect, suite, test} from 'vitest';
|
||||
|
||||
import Module from './built/manifold.js';
|
||||
import {readMesh, setupIO} from './gltf-io.js';
|
||||
import {readMesh, setupIO} from './gltf-io';
|
||||
import {examples} from './public/examples.js';
|
||||
import ManifoldWorker from './worker?worker';
|
||||
|
||||
@@ -63,18 +63,16 @@ async function runExample(name) {
|
||||
const docIn = await io.read(glbURL);
|
||||
const nodes = docIn.getRoot().listNodes();
|
||||
for (const node of nodes) {
|
||||
const mesh = node.getMesh();
|
||||
if (!mesh) {
|
||||
const docMesh = node.getMesh();
|
||||
if (!docMesh) {
|
||||
continue;
|
||||
}
|
||||
const attributes = [];
|
||||
const materials = [];
|
||||
const manifoldMesh = readMesh(mesh, attributes, materials);
|
||||
const manifold = wasm.Manifold(manifoldMesh);
|
||||
const {mesh} = readMesh(docMesh);
|
||||
const manifold = wasm.Manifold(mesh);
|
||||
const prop = manifold.getProperties();
|
||||
const genus = manifold.genus();
|
||||
console.log(genus);
|
||||
manifold.delete();
|
||||
// Return properties of first mesh encountered.
|
||||
resolve({...prop, genus});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -97,8 +95,8 @@ suite('Examples', () => {
|
||||
test('Tetrahedron Puzzle', async () => {
|
||||
const result = await runExample('Tetrahedron Puzzle');
|
||||
expect(result.genus).to.equal(0, 'Genus');
|
||||
expect(result.volume).to.be.closeTo(7297, 1, 'Volume');
|
||||
expect(result.surfaceArea).to.be.closeTo(3303, 1, 'Surface Area');
|
||||
expect(result.volume).to.be.closeTo(7240, 1, 'Volume');
|
||||
expect(result.surfaceArea).to.be.closeTo(3235, 1, 'Surface Area');
|
||||
});
|
||||
|
||||
test('Rounded Frame', async () => {
|
||||
@@ -111,8 +109,8 @@ suite('Examples', () => {
|
||||
test('Heart', async () => {
|
||||
const result = await runExample('Heart');
|
||||
expect(result.genus).to.equal(0, 'Genus');
|
||||
expect(result.volume).to.be.closeTo(282743, 10, 'Volume');
|
||||
expect(result.surfaceArea).to.be.closeTo(22187, 1, 'Surface Area');
|
||||
expect(result.volume).to.be.closeTo(3.342, 0.001, 'Volume');
|
||||
expect(result.surfaceArea).to.be.closeTo(11.51, 0.01, 'Surface Area');
|
||||
});
|
||||
|
||||
test('Scallop', async () => {
|
||||
|
||||
@@ -12,27 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Document, mat4, Material, Node, WebIO} from '@gltf-transform/core';
|
||||
import {Accessor, Animation, AnimationSampler, Document, mat4, Material, Mesh as GLTFMesh, Node, WebIO} from '@gltf-transform/core';
|
||||
import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
|
||||
import {fileForContentTypes, to3dmodel} from '@jscadui/3mf-export';
|
||||
import {fileForContentTypes, FileForRelThumbnail, to3dmodel} from '@jscadui/3mf-export';
|
||||
import {strToU8, Zippable, zipSync} from 'fflate'
|
||||
import * as glMatrix from 'gl-matrix';
|
||||
|
||||
import Module from './built/manifold';
|
||||
//@ts-ignore
|
||||
import {setupIO, writeMesh} from './gltf-io';
|
||||
import type {GLTFMaterial, Quat} from './public/editor';
|
||||
import {Properties, setupIO, writeMesh} from './gltf-io';
|
||||
import {GLTFMaterial, Quat} from './public/editor';
|
||||
import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './public/manifold';
|
||||
|
||||
interface GlobalDefaults {
|
||||
roughness: number;
|
||||
metallic: number;
|
||||
baseColorFactor: [number, number, number];
|
||||
alpha: number;
|
||||
unlit: boolean;
|
||||
animationLength: number;
|
||||
animationMode: 'loop'|'ping-pong';
|
||||
}
|
||||
|
||||
interface WorkerStatic extends ManifoldToplevel {
|
||||
GLTFNode: typeof GLTFNode;
|
||||
show(manifold: Manifold): Manifold;
|
||||
only(manifold: Manifold): Manifold;
|
||||
setMaterial(manifold: Manifold, material: GLTFMaterial): void;
|
||||
setMaterial(manifold: Manifold, material: GLTFMaterial): Manifold;
|
||||
setMorphStart(manifold: Manifold, func: (v: Vec3) => void): void;
|
||||
setMorphEnd(manifold: Manifold, func: (v: Vec3) => void): void;
|
||||
cleanup(): void;
|
||||
}
|
||||
|
||||
const module = await Module() as unknown as WorkerStatic;
|
||||
const module = await Module() as WorkerStatic;
|
||||
module.setup();
|
||||
|
||||
// Faster on modern browsers than Float32Array
|
||||
@@ -48,9 +59,10 @@ const manifoldStaticFunctions = [
|
||||
];
|
||||
// manifold member functions (that return a new manifold)
|
||||
const manifoldMemberFunctions = [
|
||||
'add', 'subtract', 'intersect', 'decompose', 'warp', 'transform', 'translate',
|
||||
'rotate', 'scale', 'mirror', 'refine', 'setProperties', 'asOriginal',
|
||||
'trimByPlane', 'split', 'splitByPlane', 'hull'
|
||||
'add', 'subtract', 'intersect', 'decompose', 'warp',
|
||||
'transform', 'translate', 'rotate', 'scale', 'mirror',
|
||||
'refine', 'refineToLength', 'setProperties', 'asOriginal', 'trimByPlane',
|
||||
'split', 'splitByPlane', 'slice', 'project', 'hull'
|
||||
];
|
||||
// CrossSection static methods (that return a new cross-section)
|
||||
const crossSectionStaticFunctions = [
|
||||
@@ -66,7 +78,8 @@ const crossSectionMemberFunctions = [
|
||||
const toplevelConstructors = ['show', 'only', 'setMaterial'];
|
||||
const toplevel = [
|
||||
'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments',
|
||||
'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection'
|
||||
'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection',
|
||||
'setMorphStart', 'setMorphEnd'
|
||||
];
|
||||
const exposedFunctions = toplevelConstructors.concat(toplevel);
|
||||
|
||||
@@ -83,15 +96,13 @@ function addMembers(
|
||||
const cls = module[className];
|
||||
const obj = areStatic ? cls : cls.prototype;
|
||||
for (const name of methodNames) {
|
||||
if (name != 'cylinder') {
|
||||
const originalFn = obj[name];
|
||||
obj[name] = function(...args: any) {
|
||||
//@ts-ignore
|
||||
const result = originalFn(...args);
|
||||
memoryRegistry.push(result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
const originalFn = obj[name];
|
||||
obj[name] = function(...args: any) {
|
||||
//@ts-ignore
|
||||
const result = originalFn(...args);
|
||||
memoryRegistry.push(result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +138,18 @@ let ghost = false;
|
||||
const shown = new Map<number, Mesh>();
|
||||
const singles = new Map<number, Mesh>();
|
||||
|
||||
const FPS = 30;
|
||||
|
||||
const GLOBAL_DEFAULTS = {
|
||||
roughness: 0.2,
|
||||
metallic: 1,
|
||||
baseColorFactor: [1, 1, 0] as [number, number, number],
|
||||
alpha: 1,
|
||||
unlit: false,
|
||||
animationLength: 1,
|
||||
animationMode: 'loop'
|
||||
};
|
||||
|
||||
const SHOW = {
|
||||
baseColorFactor: [1, 0, 0],
|
||||
alpha: 0.25,
|
||||
@@ -144,8 +167,15 @@ const GHOST = {
|
||||
const nodes = new Array<GLTFNode>();
|
||||
const id2material = new Map<number, GLTFMaterial>();
|
||||
const materialCache = new Map<GLTFMaterial, Material>();
|
||||
let nextGlobalID = 0;
|
||||
const object2globalID = new Map<GLTFNode|Manifold, number>();
|
||||
const manifold2morph = new Map<Manifold, Morph>();
|
||||
// lib3mf doesn't like objectid=0
|
||||
let nextGlobalID = 1;
|
||||
let animation: Animation;
|
||||
let timesAccessor: Accessor;
|
||||
let weightsAccessor: Accessor;
|
||||
let weightsSampler: AnimationSampler;
|
||||
let hasAnimation: boolean;
|
||||
|
||||
function cleanup() {
|
||||
ghost = false;
|
||||
@@ -155,7 +185,13 @@ function cleanup() {
|
||||
id2material.clear();
|
||||
materialCache.clear();
|
||||
object2globalID.clear();
|
||||
nextGlobalID = 0;
|
||||
manifold2morph.clear();
|
||||
nextGlobalID = 1;
|
||||
}
|
||||
|
||||
interface Morph {
|
||||
start?: (v: Vec3) => void;
|
||||
end?: (v: Vec3) => void;
|
||||
}
|
||||
|
||||
interface Mesh3MF {
|
||||
@@ -198,9 +234,9 @@ interface To3MF {
|
||||
class GLTFNode {
|
||||
private _parent?: GLTFNode;
|
||||
manifold?: Manifold;
|
||||
translation?: Vec3;
|
||||
rotation?: Vec3;
|
||||
scale?: Vec3;
|
||||
translation?: Vec3|((t: number) => Vec3);
|
||||
rotation?: Vec3|((t: number) => Vec3);
|
||||
scale?: Vec3|((t: number) => Vec3);
|
||||
material?: GLTFMaterial;
|
||||
name?: string;
|
||||
|
||||
@@ -221,12 +257,32 @@ class GLTFNode {
|
||||
|
||||
module.GLTFNode = GLTFNode;
|
||||
|
||||
const globalDefaults = {...GLOBAL_DEFAULTS};
|
||||
|
||||
module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => {
|
||||
const out = manifold.asOriginal();
|
||||
id2material.set(out.originalID(), material);
|
||||
return out;
|
||||
};
|
||||
|
||||
module.setMorphStart = (manifold: Manifold, func: (v: Vec3) => void): void => {
|
||||
const morph = manifold2morph.get(manifold);
|
||||
if (morph != null) {
|
||||
morph.start = func;
|
||||
} else {
|
||||
manifold2morph.set(manifold, {start: func});
|
||||
}
|
||||
};
|
||||
|
||||
module.setMorphEnd = (manifold: Manifold, func: (v: Vec3) => void): void => {
|
||||
const morph = manifold2morph.get(manifold);
|
||||
if (morph != null) {
|
||||
morph.end = func;
|
||||
} else {
|
||||
manifold2morph.set(manifold, {end: func});
|
||||
}
|
||||
};
|
||||
|
||||
function debug(manifold: Manifold, map: Map<number, Mesh>) {
|
||||
let result = manifold.asOriginal();
|
||||
map.set(result.originalID(), result.getMesh());
|
||||
@@ -271,8 +327,8 @@ function log(...args: any[]) {
|
||||
}
|
||||
|
||||
self.onmessage = async (e) => {
|
||||
const content = e.data +
|
||||
'\nreturn exportModels(typeof result === "undefined" ? undefined : result);\n';
|
||||
const content = 'const globalDefaults = {};\n' + e.data +
|
||||
'\nreturn exportModels(globalDefaults, typeof result === "undefined" ? undefined : result);\n';
|
||||
try {
|
||||
const f = new Function(
|
||||
'exportModels', 'glMatrix', 'module', ...exposedFunctions, content);
|
||||
@@ -288,23 +344,146 @@ self.onmessage = async (e) => {
|
||||
}
|
||||
};
|
||||
|
||||
function createGLTFnode(doc: Document, node: GLTFNode) {
|
||||
function euler2quat(rotation: Vec3): Quat {
|
||||
const {quat} = glMatrix;
|
||||
const deg2rad = Math.PI / 180;
|
||||
const q = quat.create() as Quat;
|
||||
quat.rotateZ(q, q, deg2rad * rotation[2]);
|
||||
quat.rotateY(q, q, deg2rad * rotation[1]);
|
||||
quat.rotateX(q, q, deg2rad * rotation[0]);
|
||||
return q;
|
||||
}
|
||||
|
||||
function addMotion(
|
||||
doc: Document, type: 'translation'|'rotation'|'scale', node: GLTFNode,
|
||||
out: Node): Vec3|null {
|
||||
const motion = node[type];
|
||||
if (motion == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof motion !== 'function') {
|
||||
return motion;
|
||||
}
|
||||
|
||||
const nFrames = timesAccessor.getCount();
|
||||
const nEl = type == 'rotation' ? 4 : 3;
|
||||
const frames = new Float32Array(nEl * nFrames);
|
||||
for (let i = 0; i < nFrames; ++i) {
|
||||
const x = i / (nFrames - 1);
|
||||
const m = motion(
|
||||
globalDefaults.animationMode !== 'ping-pong' ?
|
||||
x :
|
||||
(1 - Math.cos(x * 2 * Math.PI)) / 2);
|
||||
frames.set(nEl === 4 ? euler2quat(m) : m, nEl * i);
|
||||
}
|
||||
|
||||
const framesAccessor =
|
||||
doc.createAccessor(node.name + ' ' + type + ' frames')
|
||||
.setBuffer(doc.getRoot().listBuffers()[0])
|
||||
.setArray(frames)
|
||||
.setType(nEl === 4 ? Accessor.Type.VEC4 : Accessor.Type.VEC3);
|
||||
const sampler = doc.createAnimationSampler()
|
||||
.setInput(timesAccessor)
|
||||
.setOutput(framesAccessor)
|
||||
.setInterpolation('LINEAR');
|
||||
const channel = doc.createAnimationChannel()
|
||||
.setTargetPath(type)
|
||||
.setTargetNode(out)
|
||||
.setSampler(sampler);
|
||||
animation.addSampler(sampler);
|
||||
animation.addChannel(channel);
|
||||
hasAnimation = true;
|
||||
return motion(0);
|
||||
}
|
||||
|
||||
function setMorph(doc: Document, node: Node, manifold: Manifold) {
|
||||
if (manifold2morph.has(manifold)) {
|
||||
const channel = doc.createAnimationChannel()
|
||||
.setTargetPath('weights')
|
||||
.setTargetNode(node)
|
||||
.setSampler(weightsSampler);
|
||||
animation.addChannel(channel);
|
||||
hasAnimation = true;
|
||||
}
|
||||
}
|
||||
|
||||
function morphStart(manifoldMesh: Mesh, morph?: Morph): number[] {
|
||||
const inputPositions: number[] = [];
|
||||
if (morph == null) {
|
||||
return inputPositions;
|
||||
}
|
||||
|
||||
for (let i = 0; i < manifoldMesh.numVert; ++i) {
|
||||
for (let j = 0; j < 3; ++j)
|
||||
inputPositions[i * 3 + j] =
|
||||
manifoldMesh.vertProperties[i * manifoldMesh.numProp + j];
|
||||
}
|
||||
if (morph.start) {
|
||||
for (let i = 0; i < manifoldMesh.numVert; ++i) {
|
||||
const vertProp = manifoldMesh.vertProperties;
|
||||
const offset = i * manifoldMesh.numProp;
|
||||
const pos = inputPositions.slice(offset, offset + 3) as Vec3;
|
||||
morph.start(pos);
|
||||
for (let j = 0; j < 3; ++j) vertProp[offset + j] = pos[j];
|
||||
}
|
||||
}
|
||||
return inputPositions;
|
||||
}
|
||||
|
||||
function morphEnd(
|
||||
doc: Document, manifoldMesh: Mesh, mesh: GLTFMesh, inputPositions: number[],
|
||||
morph?: Morph) {
|
||||
if (morph == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mesh.setWeights([0]);
|
||||
|
||||
mesh.listPrimitives().forEach((primitive, i) => {
|
||||
if (morph.end) {
|
||||
for (let i = 0; i < manifoldMesh.numVert; ++i) {
|
||||
const pos = inputPositions.slice(3 * i, 3 * (i + 1)) as Vec3;
|
||||
morph.end(pos);
|
||||
inputPositions.splice(3 * i, 3, ...pos);
|
||||
}
|
||||
}
|
||||
|
||||
const startPosition = primitive.getAttribute('POSITION')!.getArray()!;
|
||||
const array = new Float32Array(startPosition.length);
|
||||
|
||||
const offset = manifoldMesh.runIndex[i];
|
||||
for (let j = 0; j < array.length; ++j) {
|
||||
array[j] = inputPositions[offset + j] - startPosition[j];
|
||||
}
|
||||
|
||||
const morphAccessor = doc.createAccessor(mesh.getName() + ' morph target')
|
||||
.setBuffer(doc.getRoot().listBuffers()[0])
|
||||
.setArray(array)
|
||||
.setType(Accessor.Type.VEC3);
|
||||
const morphTarget =
|
||||
doc.createPrimitiveTarget().setAttribute('POSITION', morphAccessor);
|
||||
primitive.addTarget(morphTarget);
|
||||
});
|
||||
}
|
||||
|
||||
function createGLTFnode(doc: Document, node: GLTFNode): Node {
|
||||
const out = doc.createNode(node.name);
|
||||
if (node.translation) {
|
||||
out.setTranslation(node.translation);
|
||||
|
||||
const pos = addMotion(doc, 'translation', node, out);
|
||||
if (pos != null) {
|
||||
out.setTranslation(pos);
|
||||
}
|
||||
if (node.rotation) {
|
||||
const {quat} = glMatrix;
|
||||
const deg2rad = Math.PI / 180;
|
||||
const q = quat.create() as Quat;
|
||||
quat.rotateX(q, q, deg2rad * node.rotation[0]);
|
||||
quat.rotateY(q, q, deg2rad * node.rotation[1]);
|
||||
quat.rotateZ(q, q, deg2rad * node.rotation[2]);
|
||||
out.setRotation(q);
|
||||
|
||||
const rot = addMotion(doc, 'rotation', node, out);
|
||||
if (rot != null) {
|
||||
out.setRotation(euler2quat(rot));
|
||||
}
|
||||
if (node.scale) {
|
||||
out.setScale(node.scale);
|
||||
|
||||
const scale = addMotion(doc, 'scale', node, out);
|
||||
if (scale != null) {
|
||||
out.setScale(scale);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -318,15 +497,13 @@ function getBackupMaterial(node?: GLTFNode): GLTFMaterial {
|
||||
return node.material;
|
||||
}
|
||||
|
||||
function makeDefaultedMaterial(doc: Document, {
|
||||
roughness = 0.2,
|
||||
metallic = 1,
|
||||
baseColorFactor = [1, 1, 0],
|
||||
alpha = 1,
|
||||
unlit = false,
|
||||
name = ''
|
||||
}: GLTFMaterial = {}): Material {
|
||||
const material = doc.createMaterial(name);
|
||||
function makeDefaultedMaterial(
|
||||
doc: Document, matIn: GLTFMaterial = {}): Material {
|
||||
const defaults = {...globalDefaults};
|
||||
Object.assign(defaults, matIn);
|
||||
const {roughness, metallic, baseColorFactor, alpha, unlit} = defaults;
|
||||
|
||||
const material = doc.createMaterial(matIn.name ?? '');
|
||||
|
||||
if (unlit) {
|
||||
const unlit = doc.createExtension(KHRMaterialsUnlit).createUnlit();
|
||||
@@ -373,19 +550,23 @@ function addMesh(
|
||||
// From Z-up to Y-up (glTF)
|
||||
const manifoldMesh = manifold.getMesh();
|
||||
|
||||
const materials = new Array<GLTFMaterial>();
|
||||
const attributes = new Array<Array<string>>();
|
||||
const id2properties = new Map<number, Properties>();
|
||||
for (const id of manifoldMesh.runOriginalID!) {
|
||||
const material = id2material.get(id) || backupMaterial;
|
||||
materials.push(material);
|
||||
attributes.push(['POSITION', ...material.attributes ?? []]);
|
||||
id2properties.set(id, {
|
||||
material: getCachedMaterial(doc, ghost ? GHOST : material),
|
||||
attributes: ['POSITION', ...material.attributes ?? []]
|
||||
});
|
||||
}
|
||||
|
||||
const gltfMaterials = materials.map((matDef) => {
|
||||
return getCachedMaterial(doc, ghost ? GHOST : matDef);
|
||||
});
|
||||
const morph = manifold2morph.get(manifold);
|
||||
const inputPositions = morphStart(manifoldMesh, morph);
|
||||
|
||||
node.setMesh(writeMesh(doc, manifoldMesh, attributes, gltfMaterials));
|
||||
const mesh = writeMesh(doc, manifoldMesh, id2properties);
|
||||
mesh.setName(node.getName());
|
||||
node.setMesh(mesh);
|
||||
|
||||
morphEnd(doc, manifoldMesh, mesh, inputPositions, morph);
|
||||
|
||||
const vertices = manifoldMesh.numProp === 3 ?
|
||||
manifoldMesh.vertProperties :
|
||||
@@ -402,21 +583,18 @@ function addMesh(
|
||||
{vertices, indices: manifoldMesh.triVerts, id: `${nextGlobalID++}`});
|
||||
|
||||
for (const [run, id] of manifoldMesh.runOriginalID!.entries()) {
|
||||
let inMesh = shown.get(id);
|
||||
let single = false;
|
||||
if (inMesh == null) {
|
||||
single = true;
|
||||
inMesh = singles.get(id);
|
||||
}
|
||||
const show = shown.has(id);
|
||||
const inMesh = show ? shown.get(id) : singles.get(id);
|
||||
if (inMesh == null) {
|
||||
continue;
|
||||
}
|
||||
const mat = single ? materials[run] : SHOW;
|
||||
const debugNode =
|
||||
doc.createNode('debug')
|
||||
.setMesh(writeMesh(
|
||||
doc, inMesh, attributes, [getCachedMaterial(doc, mat)]))
|
||||
.setMatrix(manifoldMesh.transform(run));
|
||||
|
||||
id2properties.get(id)!.material = getCachedMaterial(
|
||||
doc, show ? SHOW : (id2material.get(id) || backupMaterial));
|
||||
|
||||
const debugNode = doc.createNode('debug')
|
||||
.setMesh(writeMesh(doc, inMesh, id2properties))
|
||||
.setMatrix(manifoldMesh.transform(run));
|
||||
node.addChild(debugNode);
|
||||
}
|
||||
}
|
||||
@@ -450,6 +628,7 @@ function createNodeFromCache(
|
||||
const node = createGLTFnode(doc, nodeDef);
|
||||
const {manifold} = nodeDef;
|
||||
if (manifold != null) {
|
||||
setMorph(doc, node, manifold);
|
||||
const backupMaterial = getBackupMaterial(nodeDef);
|
||||
const cachedNodes = manifold2node.get(manifold);
|
||||
if (cachedNodes == null) {
|
||||
@@ -481,7 +660,10 @@ function createNodeFromCache(
|
||||
return node;
|
||||
}
|
||||
|
||||
async function exportModels(manifold?: Manifold) {
|
||||
async function exportModels(defaults: GlobalDefaults, manifold?: Manifold) {
|
||||
Object.assign(globalDefaults, GLOBAL_DEFAULTS);
|
||||
Object.assign(globalDefaults, defaults);
|
||||
|
||||
const doc = new Document();
|
||||
const halfRoot2 = Math.sqrt(2) / 2;
|
||||
const mm2m = 1 / 1000;
|
||||
@@ -490,6 +672,32 @@ async function exportModels(manifold?: Manifold) {
|
||||
.setScale([mm2m, mm2m, mm2m]);
|
||||
doc.createScene().addChild(wrapper);
|
||||
|
||||
animation = doc.createAnimation('');
|
||||
hasAnimation = false;
|
||||
const nFrames = Math.round(globalDefaults.animationLength * FPS) + 1;
|
||||
const times = new Float32Array(nFrames);
|
||||
const weights = new Float32Array(nFrames);
|
||||
for (let i = 0; i < nFrames; ++i) {
|
||||
const x = i / (nFrames - 1);
|
||||
times[i] = x * globalDefaults.animationLength;
|
||||
weights[i] = globalDefaults.animationMode !== 'ping-pong' ?
|
||||
x :
|
||||
(1 - Math.cos(x * 2 * Math.PI)) / 2;
|
||||
}
|
||||
timesAccessor = doc.createAccessor('animation times')
|
||||
.setBuffer(doc.createBuffer())
|
||||
.setArray(times)
|
||||
.setType(Accessor.Type.SCALAR);
|
||||
weightsAccessor = doc.createAccessor('animation weights')
|
||||
.setBuffer(doc.getRoot().listBuffers()[0])
|
||||
.setArray(weights)
|
||||
.setType(Accessor.Type.SCALAR);
|
||||
weightsSampler = doc.createAnimationSampler()
|
||||
.setInput(timesAccessor)
|
||||
.setOutput(weightsAccessor)
|
||||
.setInterpolation('LINEAR');
|
||||
animation.addSampler(weightsSampler);
|
||||
|
||||
const to3mf = {
|
||||
meshes: [],
|
||||
components: [],
|
||||
@@ -547,13 +755,24 @@ async function exportModels(manifold?: Manifold) {
|
||||
to3mf.items.push({objectID: `${object2globalID.get(manifold)}`});
|
||||
}
|
||||
|
||||
if (!hasAnimation) {
|
||||
timesAccessor.dispose();
|
||||
weightsAccessor.dispose();
|
||||
weightsSampler.dispose();
|
||||
animation.dispose();
|
||||
}
|
||||
|
||||
const glb = await io.writeBinary(doc);
|
||||
const blobGLB = new Blob([glb], {type: 'application/octet-stream'});
|
||||
|
||||
const fileForRelThumbnail = new FileForRelThumbnail();
|
||||
fileForRelThumbnail.add3dModel('3D/3dmodel.model')
|
||||
|
||||
const model = to3dmodel(to3mf);
|
||||
const files: Zippable = {};
|
||||
files['3D/3dmodel.model'] = strToU8(model);
|
||||
files[fileForContentTypes.name] = strToU8(fileForContentTypes.content);
|
||||
files[fileForRelThumbnail.name] = strToU8(fileForRelThumbnail.content);
|
||||
const zipFile = zipSync(files);
|
||||
const blob3MF = new Blob(
|
||||
[zipFile],
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Box, FillRule, JoinType, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types';
|
||||
import {Box, FillRule, JoinType, Mat3, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types';
|
||||
|
||||
/**
|
||||
* Triangulates a set of /epsilon-valid polygons.
|
||||
@@ -571,6 +571,18 @@ export class Manifold {
|
||||
*/
|
||||
refine(n: number): Manifold;
|
||||
|
||||
/**
|
||||
* Increase the density of the mesh by splitting each edge into pieces of
|
||||
* roughly the input length. Interior verts are added to keep the rest of the
|
||||
* triangulation edges also of roughly the same length. If halfedgeTangents
|
||||
* are present (e.g. from the Smooth() constructor), the new vertices will be
|
||||
* moved to the interpolated surface according to their barycentric
|
||||
* coordinates.
|
||||
*
|
||||
* @param length The length that edges will be broken down to.
|
||||
*/
|
||||
refineToLength(length: number): Manifold;
|
||||
|
||||
/**
|
||||
* Create a new copy of this manifold with updated vertex properties by
|
||||
* supplying a function that takes the existing position and properties as
|
||||
@@ -685,6 +697,22 @@ export class Manifold {
|
||||
*/
|
||||
trimByPlane(normal: Vec3, originOffset: number): Manifold;
|
||||
|
||||
/**
|
||||
* Returns the cross section of this object parallel to the X-Y plane at the
|
||||
* specified height. Using a height equal to the bottom
|
||||
* of the bounding box will return the bottom faces, while using a height
|
||||
* equal to the top of the bounding box will return empty.
|
||||
*
|
||||
* @param height Z-level of slice.
|
||||
*/
|
||||
slice(height: number): CrossSection;
|
||||
|
||||
/**
|
||||
* Returns a cross section representing the projected outline of this object
|
||||
* onto the X-Y plane.
|
||||
*/
|
||||
project(): CrossSection;
|
||||
|
||||
// Convex Hulls
|
||||
|
||||
/**
|
||||
@@ -838,19 +866,7 @@ export class Manifold {
|
||||
delete(): void;
|
||||
}
|
||||
|
||||
export class Mesh {
|
||||
constructor(options: {
|
||||
numProp: number,
|
||||
vertProperties: Float32Array,
|
||||
triVerts: Uint32Array,
|
||||
mergeFromVert?: Uint32Array,
|
||||
mergeToVert?: Uint32Array,
|
||||
runIndex?: Uint32Array,
|
||||
runOriginalID?: Uint32Array,
|
||||
runTransform?: Float32Array,
|
||||
faceID?: Uint32Array,
|
||||
halfedgeTangent?: Float32Array
|
||||
});
|
||||
export interface MeshOptions {
|
||||
numProp: number;
|
||||
vertProperties: Float32Array;
|
||||
triVerts: Uint32Array;
|
||||
@@ -861,6 +877,20 @@ export class Mesh {
|
||||
runTransform?: Float32Array;
|
||||
faceID?: Uint32Array;
|
||||
halfedgeTangent?: Float32Array;
|
||||
}
|
||||
|
||||
export class Mesh {
|
||||
constructor(options: MeshOptions);
|
||||
numProp: number;
|
||||
vertProperties: Float32Array;
|
||||
triVerts: Uint32Array;
|
||||
mergeFromVert: Uint32Array;
|
||||
mergeToVert: Uint32Array;
|
||||
runIndex: Uint32Array;
|
||||
runOriginalID: Uint32Array;
|
||||
runTransform: Float32Array;
|
||||
faceID: Uint32Array;
|
||||
halfedgeTangent: Float32Array;
|
||||
get numTri(): number;
|
||||
get numVert(): number;
|
||||
get numRun(): number;
|
||||
|
||||
@@ -22,6 +22,18 @@ export interface SealedFloat32Array<N extends number> extends Float32Array {
|
||||
|
||||
export type Vec2 = [number, number];
|
||||
export type Vec3 = [number, number, number];
|
||||
// 3x3 matrix stored in column-major order
|
||||
export type Mat3 = [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
// 4x4 matrix stored in column-major order
|
||||
export type Mat4 = [
|
||||
number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "manifold-3d",
|
||||
"version": "2.1.0",
|
||||
"version": "2.4.5",
|
||||
"description": "Geometry library for topological robustness",
|
||||
"main": "manifold.js",
|
||||
"files": [
|
||||
|
||||
@@ -41,8 +41,9 @@ endif()
|
||||
if(BUILD_TEST_CGAL)
|
||||
add_executable(perfTestCGAL perf_test_cgal.cpp)
|
||||
find_package(CGAL REQUIRED COMPONENTS Core)
|
||||
find_package(Boost REQUIRED COMPONENTS thread)
|
||||
target_compile_definitions(perfTestCGAL PRIVATE CGAL_USE_GMPXX)
|
||||
target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core)
|
||||
target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core Boost::thread)
|
||||
target_compile_options(perfTestCGAL PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(perfTestCGAL PUBLIC cxx_std_17)
|
||||
endif()
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
{
|
||||
"nodes": {
|
||||
"clipper2-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1701175684,
|
||||
"narHash": "sha256-3TKhb48cABl0QcbeG12xlA1taQ/8/RdUUHSp0Qh85eE=",
|
||||
"owner": "AngusJohnson",
|
||||
"repo": "Clipper2",
|
||||
"rev": "98db5662e8dd1808a5a7b50c5605a2289bb390e8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "AngusJohnson",
|
||||
"ref": "Clipper2_1.3.0",
|
||||
"repo": "Clipper2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -37,11 +54,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1692734709,
|
||||
"narHash": "sha256-SCFnyHCyYjwEmgUsHDDuU0TsbVMKeU1vwkR+r7uS2Rg=",
|
||||
"lastModified": 1704722960,
|
||||
"narHash": "sha256-mKGJ3sPsT6//s+Knglai5YflJUF2DGj7Ai6Ynopz0kI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b85ed9dcbf187b909ef7964774f8847d554fab3b",
|
||||
"rev": "317484b1ead87b9c1b8ac5261a8d2dd748a0492d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -52,9 +69,11 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"clipper2-src": "clipper2-src",
|
||||
"flake-utils": "flake-utils",
|
||||
"gtest-src": "gtest-src",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"thrust-src": "thrust-src"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
@@ -71,6 +90,24 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"thrust-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696873248,
|
||||
"narHash": "sha256-mUhMXGPbO2t83EvI8YNDRLngvLiwfzs4EgRrcxKfhHs=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "756c5afc0750f1413da05bd2b6505180e84c53d4",
|
||||
"revCount": 4758,
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/NVIDIA/thrust.git"
|
||||
},
|
||||
"original": {
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/NVIDIA/thrust.git"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -5,7 +5,15 @@
|
||||
url = "github:google/googletest/v1.14.0";
|
||||
flake = false;
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, gtest-src }:
|
||||
inputs.thrust-src = {
|
||||
url = "git+https://github.com/NVIDIA/thrust.git?submodules=1";
|
||||
flake = false;
|
||||
};
|
||||
inputs.clipper2-src = {
|
||||
url = "github:AngusJohnson/Clipper2/Clipper2_1.3.0";
|
||||
flake = false;
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, gtest-src, thrust-src, clipper2-src }:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let
|
||||
@@ -13,36 +21,9 @@
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
thrust-210 = pkgs.nvidia-thrust.overrideAttrs (_: _: {
|
||||
version = "24486a1";
|
||||
src = builtins.fetchGit {
|
||||
url = "https://github.com/NVIDIA/thrust.git";
|
||||
ref = "main";
|
||||
rev = "24486a169a62a58ef8f824d3dc9613c006b6f5a7";
|
||||
submodules = true;
|
||||
};
|
||||
cmakeFlags = [ "-DTHRUST_ENABLE_HEADER_TESTING=OFF" "-DTHRUST_ENABLE_TESTING=OFF" "-DTHRUST_ENABLE_EXAMPLES=OFF" "-DTHRUST_DEVICE_SYSTEM=CPP" ];
|
||||
fixupPhase = ''
|
||||
cat <<EOT > $out/lib/cmake/thrust/thrust-header-search.cmake
|
||||
# Parse version information from version.h in source tree
|
||||
set(_THRUST_VERSION_INCLUDE_DIR "$out/include")
|
||||
if(EXISTS "\''${_THRUST_VERSION_INCLUDE_DIR}/thrust/version.h")
|
||||
set(_THRUST_VERSION_INCLUDE_DIR "\''${_THRUST_VERSION_INCLUDE_DIR}" CACHE FILEPATH "" FORCE) # Clear old result
|
||||
set_property(CACHE _THRUST_VERSION_INCLUDE_DIR PROPERTY TYPE INTERNAL)
|
||||
endif()
|
||||
EOT
|
||||
cat <<EOT > $out/lib/cmake/libcudacxx/libcudacxx-header-search.cmake
|
||||
# Parse version information from version header:
|
||||
unset(_libcudacxx_VERSION_INCLUDE_DIR CACHE) # Clear old result to force search
|
||||
find_path(_libcudacxx_VERSION_INCLUDE_DIR cuda/std/detail/__config
|
||||
NO_DEFAULT_PATH # Only search explicit paths below:
|
||||
PATHS
|
||||
"\''${CMAKE_CURRENT_LIST_DIR}/../../../include" # Source tree
|
||||
)
|
||||
set_property(CACHE _libcudacxx_VERSION_INCLUDE_DIR PROPERTY TYPE INTERNAL)
|
||||
EOT
|
||||
'';
|
||||
enableParallelBuilding = false;
|
||||
clipper2 = pkgs.clipper2.overrideAttrs (_: _: {
|
||||
version = "1.3.0";
|
||||
src = clipper2-src;
|
||||
});
|
||||
manifold =
|
||||
{ parallel-backend ? "none"
|
||||
@@ -52,28 +33,38 @@
|
||||
}: pkgs.stdenv.mkDerivation {
|
||||
inherit doCheck;
|
||||
pname = "manifold-${parallel-backend}";
|
||||
version = "2.2.0";
|
||||
version = "2.4.5";
|
||||
src = self;
|
||||
nativeBuildInputs = (with pkgs; [
|
||||
cmake
|
||||
thrust-210
|
||||
glm
|
||||
ninja
|
||||
(python39.withPackages
|
||||
(ps: with ps; [ trimesh ]))
|
||||
(python3.withPackages
|
||||
(ps: with ps; [ trimesh pytest ]))
|
||||
gtest
|
||||
pkg-config
|
||||
]) ++ build-tools;
|
||||
buildInputs = with pkgs; [
|
||||
glm
|
||||
clipper2
|
||||
assimp
|
||||
];
|
||||
cmakeFlags = [
|
||||
"-DMANIFOLD_PYBIND=ON"
|
||||
"-DMANIFOLD_CBIND=ON"
|
||||
"-DBUILD_SHARED_LIBS=ON"
|
||||
"-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src}"
|
||||
"-DMANIFOLD_PAR=${pkgs.lib.strings.toUpper parallel-backend}"
|
||||
];
|
||||
prePatch = ''
|
||||
substituteInPlace bindings/python/CMakeLists.txt \
|
||||
--replace 'DESTINATION ''${Python_SITEARCH}' 'DESTINATION "${placeholder "out"}/${pkgs.python3.sitePackages}"'
|
||||
'';
|
||||
checkPhase = ''
|
||||
cd test
|
||||
./manifold_test
|
||||
cd ../../
|
||||
PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python python3 bindings/python/examples/run_all.py
|
||||
PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python python3 -m pytest
|
||||
cd build
|
||||
'';
|
||||
};
|
||||
@@ -84,14 +75,6 @@
|
||||
build-tools = with pkgs; [ tbb_2021_8 pkg-config ];
|
||||
}
|
||||
];
|
||||
devShell = { additional ? [ ] }: pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cmake
|
||||
tbb_2021_8
|
||||
thrust-210
|
||||
gtest
|
||||
] ++ additional;
|
||||
};
|
||||
in
|
||||
{
|
||||
packages = (builtins.listToAttrs
|
||||
@@ -103,11 +86,13 @@
|
||||
parallelBackends)) // {
|
||||
manifold-js = pkgs.buildEmscriptenPackage {
|
||||
name = "manifold-js";
|
||||
version = "beta";
|
||||
version = "2.4.5";
|
||||
src = self;
|
||||
nativeBuildInputs = (with pkgs; [ cmake python39 ]);
|
||||
buildInputs = [ pkgs.nodejs ];
|
||||
configurePhase = ''
|
||||
cp -r ${clipper2-src} clipper2
|
||||
chmod -R +w clipper2
|
||||
mkdir -p .emscriptencache
|
||||
export EM_CACHE=$(pwd)/.emscriptencache
|
||||
mkdir build
|
||||
@@ -115,7 +100,8 @@
|
||||
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
|
||||
-DFETCHCONTENT_SOURCE_DIR_GLM=${pkgs.glm.src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=${gtest-src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-210.src} ..
|
||||
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=../clipper2 ..
|
||||
'';
|
||||
buildPhase = ''
|
||||
emmake make -j''${NIX_BUILD_CORES}
|
||||
@@ -134,15 +120,15 @@
|
||||
# but how should we make it work with other python versions?
|
||||
manifold3d = with pkgs.python3Packages; buildPythonPackage {
|
||||
pname = "manifold3d";
|
||||
version = "2.2.0";
|
||||
version = "2.4.5";
|
||||
src = self;
|
||||
propagatedBuildInputs = [
|
||||
numpy
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
tbb_2021_8
|
||||
thrust-210
|
||||
glm
|
||||
clipper2
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
@@ -153,18 +139,27 @@
|
||||
pathspec
|
||||
pkg-config
|
||||
];
|
||||
SKBUILD_CMAKE_DEFINE = "FETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src}";
|
||||
checkInputs = [
|
||||
trimesh
|
||||
pytest
|
||||
];
|
||||
format = "pyproject";
|
||||
dontUseCmakeConfigure = true;
|
||||
doCheck = true;
|
||||
checkPhase = ''
|
||||
python3 bindings/python/examples/run_all.py
|
||||
python3 -m pytest
|
||||
'';
|
||||
};
|
||||
};
|
||||
devShell = devShell { };
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cmake
|
||||
tbb_2021_8
|
||||
gtest
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
shopt -s extglob
|
||||
if [ -z "$CLANG_FORMAT" ]; then
|
||||
CLANG_FORMAT=$(dirname $(which clang-11))/clang-format
|
||||
fi
|
||||
|
||||
$CLANG_FORMAT -i extras/*.cpp
|
||||
$CLANG_FORMAT -i meshIO/**/*.{h,cpp}
|
||||
$CLANG_FORMAT -i samples/**/*.{h,cpp}
|
||||
$CLANG_FORMAT -i test/*.{h,cpp}
|
||||
$CLANG_FORMAT -i bindings/*/*.cpp
|
||||
$CLANG_FORMAT -i bindings/c/include/*.h
|
||||
$CLANG_FORMAT -i bindings/wasm/**/*.{js,ts,html}
|
||||
$CLANG_FORMAT -i src/!(third_party)/*/*.{h,cpp}
|
||||
black bindings/python/examples/*.py
|
||||
@@ -0,0 +1,4 @@
|
||||
set(MANIFOLD_PAR "@MANIFOLD_PAR@")
|
||||
set(MANIFOLD_EXPORT "@MANIFOLD_EXPORT@")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/manifoldDeps.cmake")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/manifoldTargets.cmake")
|
||||
@@ -0,0 +1,78 @@
|
||||
include(FetchContent)
|
||||
include(GNUInstallDirs)
|
||||
find_package(PkgConfig)
|
||||
if(MANIFOLD_PAR STREQUAL "TBB")
|
||||
find_package(TBB)
|
||||
endif()
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(Clipper2 Clipper2)
|
||||
if(MANIFOLD_PAR STREQUAL "TBB" AND NOT TBB_FOUND)
|
||||
pkg_check_modules(TBB tbb)
|
||||
endif()
|
||||
endif()
|
||||
if(Clipper2_FOUND)
|
||||
add_library(Clipper2 SHARED IMPORTED)
|
||||
set_property(TARGET Clipper2 PROPERTY
|
||||
IMPORTED_LOCATION ${Clipper2_LINK_LIBRARIES})
|
||||
target_include_directories(Clipper2 INTERFACE ${Clipper2_INCLUDE_DIRS})
|
||||
else()
|
||||
message(STATUS "clipper2 not found, downloading from source")
|
||||
set(CLIPPER2_UTILS OFF)
|
||||
set(CLIPPER2_EXAMPLES OFF)
|
||||
set(CLIPPER2_TESTS OFF)
|
||||
set(CLIPPER2_USINGZ "OFF" CACHE STRING "Preempt cache default of USINGZ (we only use 2d)")
|
||||
FetchContent_Declare(Clipper2
|
||||
GIT_REPOSITORY https://github.com/AngusJohnson/Clipper2.git
|
||||
GIT_TAG Clipper2_1.3.0
|
||||
GIT_PROGRESS TRUE
|
||||
SOURCE_SUBDIR CPP
|
||||
)
|
||||
FetchContent_MakeAvailable(Clipper2)
|
||||
if(NOT EMSCRIPTEN)
|
||||
set_target_properties(Clipper2 PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
"$<INSTALL_INTERFACE:include>$<BUILD_INTERFACE:${Clipper2_SOURCE_DIR}/Clipper2Lib/include>")
|
||||
install(TARGETS Clipper2 EXPORT clipper2Targets)
|
||||
install(EXPORT clipper2Targets DESTINATION ${CMAKE_INSTALL_DATADIR}/clipper2)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
FetchContent_Declare(glm
|
||||
GIT_REPOSITORY https://github.com/g-truc/glm.git
|
||||
GIT_TAG b06b775c1c80af51a1183c0e167f9de3b2351a79
|
||||
GIT_PROGRESS TRUE
|
||||
FIND_PACKAGE_ARGS NAMES glm
|
||||
)
|
||||
FetchContent_MakeAvailable(glm)
|
||||
|
||||
if(NOT glm_FOUND)
|
||||
message(STATUS "glm not found, downloading from source")
|
||||
if(NOT EMSCRIPTEN)
|
||||
install(TARGETS glm EXPORT glmTargets)
|
||||
install(TARGETS glm-header-only EXPORT glmTargets)
|
||||
install(EXPORT glmTargets DESTINATION ${CMAKE_INSTALL_DATADIR}/glm)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MANIFOLD_PAR STREQUAL "TBB" AND NOT TBB_FOUND)
|
||||
message(STATUS "tbb not found, downloading from source")
|
||||
include(FetchContent)
|
||||
set(TBB_TEST OFF CACHE INTERNAL "" FORCE)
|
||||
set(TBB_STRICT OFF CACHE INTERNAL "" FORCE)
|
||||
FetchContent_Declare(TBB
|
||||
GIT_REPOSITORY https://github.com/oneapi-src/oneTBB.git
|
||||
GIT_TAG v2021.11.0
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(TBB)
|
||||
set_property(DIRECTORY ${tbb_SOURCE_DIR} PROPERTY EXCLUDE_FROM_ALL YES)
|
||||
# note: we do want to install tbb to the user machine when built from
|
||||
# source
|
||||
if(NOT EMSCRIPTEN)
|
||||
install(TARGETS tbb)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MANIFOLD_EXPORT)
|
||||
find_package(assimp REQUIRED)
|
||||
endif()
|
||||
@@ -14,13 +14,11 @@
|
||||
|
||||
project(meshIO)
|
||||
|
||||
find_package(assimp REQUIRED)
|
||||
|
||||
add_library(${PROJECT_NAME} src/meshIO.cpp)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC manifold
|
||||
@@ -30,13 +28,5 @@ target_link_libraries(${PROJECT_NAME}
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT devel
|
||||
)
|
||||
install(
|
||||
DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/meshIO.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -234,6 +234,7 @@ void ExportMesh(const std::string& filename, const MeshGL& mesh,
|
||||
? 1
|
||||
: mesh.vertProperties[i * mesh.numProp +
|
||||
options.mat.colorChannels[j]];
|
||||
c = glm::saturate(c);
|
||||
mesh_out->mColors[0][i] = aiColor4D(c.r, c.g, c.b, c.a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Modified from https://github.com/google/fuzztest/blob/main/tools/minimizer.sh
|
||||
# If you want to use a specific reproducer, pass it in FUZZTEST_MINIMIZE_REPRODUCER
|
||||
# environment variable.
|
||||
# Otherwise, just run it as
|
||||
# $ ./minimizer.sh ./build/test/polygon_fuzz --fuzz=PolygonFuzz.TriangulationNoCrash
|
||||
# If you want to kill it, try SIGKILL, SIGTERM is not very useful
|
||||
# If you want to end the loop early, rename $(pwd)/reproducers into something
|
||||
# else...
|
||||
|
||||
if [ -z "${FUZZTEST_MINIMIZE_REPRODUCER}"]; then
|
||||
mkdir -p $(pwd)/reproducers
|
||||
FUZZTEST_REPRODUCERS_OUT_DIR=$(pwd)/reproducers "$@"
|
||||
wildcard="$(pwd)/reproducers/*"
|
||||
reproducers=($wildcard)
|
||||
FUZZTEST_MINIMIZE_REPRODUCER="${reproducers[0]}"
|
||||
fi
|
||||
readonly ORIGINAL_REPRODUCER="${FUZZTEST_MINIMIZE_REPRODUCER}"
|
||||
|
||||
for i in {0001..9999}; do
|
||||
echo
|
||||
echo "╔════════════════════════════════════════════════╗"
|
||||
echo "║ Minimization round: ${i} ║"
|
||||
echo "╚════════════════════════════════════════════════╝"
|
||||
echo "Note that to terminate early, simply move $ORIGINAL_REPRODUCER to somewhere else..."
|
||||
echo
|
||||
|
||||
if [ ! -f $ORIGINAL_REPRODUCER ]; then
|
||||
echo "Terminated by the user"
|
||||
break
|
||||
fi
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
FUZZTEST_REPRODUCERS_OUT_DIR="${TEMP_DIR}" \
|
||||
FUZZTEST_MINIMIZE_REPRODUCER="${FUZZTEST_MINIMIZE_REPRODUCER}" \
|
||||
"$@"
|
||||
|
||||
if [ $? -eq 130 ]; then
|
||||
echo
|
||||
echo "╔═══════════════════════════════════════════════╗"
|
||||
echo "║ Minimization terminated. ║"
|
||||
echo "╚═══════════════════════════════════════════════╝"
|
||||
echo
|
||||
echo "Find the smallest reproducer at:"
|
||||
echo
|
||||
echo "${FUZZTEST_MINIMIZE_REPRODUCER}"
|
||||
|
||||
rm -rf "${TEMP_DIR}"
|
||||
break
|
||||
fi
|
||||
|
||||
SMALLER_REPRODUCER=$(find "${TEMP_DIR}" -type f)
|
||||
NEW_NAME="${ORIGINAL_REPRODUCER}-min-${i}"
|
||||
mv "${SMALLER_REPRODUCER}" "${NEW_NAME}"
|
||||
FUZZTEST_MINIMIZE_REPRODUCER="${NEW_NAME}"
|
||||
|
||||
rm -rf "${TEMP_DIR}"
|
||||
done
|
||||
@@ -1,13 +1,13 @@
|
||||
[project]
|
||||
name = "manifold3d"
|
||||
version = "2.2.0"
|
||||
version = "2.4.5"
|
||||
authors = [
|
||||
{ name="Emmett Lalish", email="elalish@gmail.com" },
|
||||
]
|
||||
description = "Library for geometric robustness"
|
||||
readme = "README.md"
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: C++",
|
||||
@@ -42,20 +42,14 @@ sdist.exclude = [
|
||||
"oneTBB", # we may have this when we build with cibuildwheel
|
||||
"bindings/python/third_party/nanobind/tests/",
|
||||
"bindings/python/third_party/nanobind/docs/",
|
||||
"src/third_party/clipper2/CPP/Examples/",
|
||||
"src/third_party/clipper2/CPP/Tests/",
|
||||
"src/third_party/clipper2/CSharp/",
|
||||
"src/third_party/clipper2/Delphi/",
|
||||
"src/third_party/clipper2/DLL/",
|
||||
"src/third_party/clipper2/Tests/",
|
||||
]
|
||||
wheel.packages = ["manifold3d"]
|
||||
cmake.args = ["-DMANIFOLD_PAR=TBB", "-DMANIFOLD_TEST=OFF"]
|
||||
cmake.args = ["-DMANIFOLD_PYBIND=ON", "-DMANIFOLD_PAR=TBB", "-DMANIFOLD_TEST=OFF", "-DBUILD_SHARED_LIBS=OFF"]
|
||||
install.components = ["bindings"]
|
||||
|
||||
[tool.cibuildwheel]
|
||||
build-frontend = "build"
|
||||
test-requires = ["trimesh"]
|
||||
test-requires = ["trimesh", "pytest"]
|
||||
test-command = "python {project}/bindings/python/examples/run_all.py"
|
||||
# Setuptools bug causes collision between pypy and cpython artifacts
|
||||
manylinux-x86_64-image = "manylinux_2_28"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
testpaths =
|
||||
bindings/python/examples
|
||||
|
Before Width: | Height: | Size: 106 KiB |
@@ -15,18 +15,15 @@
|
||||
project (collider)
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} ${SOURCE_FILES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_link_libraries(${PROJECT_NAME} PUBLIC utilities)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
|
||||
target_compile_features(${PROJECT_NAME}
|
||||
PUBLIC cxx_std_17
|
||||
)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT runtime
|
||||
)
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/collider.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -38,7 +38,9 @@ class Collider {
|
||||
Vec<thrust::pair<int, int>> internalChildren_;
|
||||
|
||||
int NumInternal() const { return internalChildren_.size(); };
|
||||
int NumLeaves() const { return NumInternal() + 1; };
|
||||
int NumLeaves() const {
|
||||
return internalChildren_.empty() ? 0 : (NumInternal() + 1);
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace manifold
|
||||
|
||||
@@ -276,6 +276,7 @@ namespace manifold {
|
||||
*/
|
||||
Collider::Collider(const VecView<const Box>& leafBB,
|
||||
const VecView<const uint32_t>& leafMorton) {
|
||||
ZoneScoped;
|
||||
ASSERT(leafBB.size() == leafMorton.size(), userErr,
|
||||
"vectors must be the same length");
|
||||
int num_nodes = 2 * leafBB.size() - 1;
|
||||
@@ -299,6 +300,7 @@ Collider::Collider(const VecView<const Box>& leafBB,
|
||||
*/
|
||||
template <const bool selfCollision, const bool inverted, typename T>
|
||||
SparseIndices Collider::Collisions(const VecView<const T>& queriesIn) const {
|
||||
ZoneScoped;
|
||||
// note that the length is 1 larger than the number of queries so the last
|
||||
// element can store the sum when using exclusive scan
|
||||
if (queriesIn.size() < kSequentialThreshold) {
|
||||
@@ -336,6 +338,7 @@ SparseIndices Collider::Collisions(const VecView<const T>& queriesIn) const {
|
||||
* hierarchy.
|
||||
*/
|
||||
void Collider::UpdateBoxes(const VecView<const Box>& leafBB) {
|
||||
ZoneScoped;
|
||||
ASSERT(leafBB.size() == NumLeaves(), userErr,
|
||||
"must have the same number of updated boxes as original");
|
||||
// copy in leaf node Boxes
|
||||
@@ -355,6 +358,7 @@ void Collider::UpdateBoxes(const VecView<const Box>& leafBB) {
|
||||
* axis-aligned, abort and return false to indicate recalculation is necessary.
|
||||
*/
|
||||
bool Collider::Transform(glm::mat4x3 transform) {
|
||||
ZoneScoped;
|
||||
bool axisAligned = true;
|
||||
for (int row : {0, 1, 2}) {
|
||||
int count = 0;
|
||||
|
||||
@@ -13,28 +13,17 @@
|
||||
# limitations under the License.
|
||||
|
||||
project (cross_section)
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} ${SOURCE_FILES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
target_link_libraries( ${PROJECT_NAME}
|
||||
PUBLIC utilities Clipper2
|
||||
)
|
||||
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC utilities
|
||||
PRIVATE Clipper2)
|
||||
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/cross_section.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "glm/ext/matrix_float3x2.hpp"
|
||||
#include "glm/ext/vector_float2.hpp"
|
||||
#include "public.h"
|
||||
#include "vec_view.h"
|
||||
|
||||
namespace manifold {
|
||||
|
||||
@@ -97,6 +98,8 @@ class CrossSection {
|
||||
CrossSection Mirror(const glm::vec2 ax) const;
|
||||
CrossSection Transform(const glm::mat3x2& m) const;
|
||||
CrossSection Warp(std::function<void(glm::vec2&)> warpFunc) const;
|
||||
CrossSection WarpBatch(
|
||||
std::function<void(VecView<glm::vec2>)> warpFunc) const;
|
||||
CrossSection Simplify(double epsilon = 1e-6) const;
|
||||
|
||||
// Adapted from Clipper2 docs:
|
||||
|
||||
@@ -146,55 +146,63 @@ void decompose_hole(const C2::PolyTreeD* outline,
|
||||
}
|
||||
}
|
||||
|
||||
void flatten(const C2::PolyTreeD* tree, C2::PathsD& polys, int i) {
|
||||
auto n_outlines = tree->Count();
|
||||
if (i < n_outlines) {
|
||||
auto outline = tree->Child(i);
|
||||
flatten(outline, polys, 0);
|
||||
polys.push_back(outline->Polygon());
|
||||
if (i < n_outlines - 1) {
|
||||
flatten(tree, polys, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool V2Lesser(glm::vec2 a, glm::vec2 b) {
|
||||
if (a.x == b.x) return a.y < b.y;
|
||||
return a.x < b.x;
|
||||
}
|
||||
|
||||
double Cross(glm::vec2 a, glm::vec2 b) { return a.x * b.y - a.y * b.x; }
|
||||
|
||||
double SquaredDistance(glm::vec2 a, glm::vec2 b) {
|
||||
auto d = a - b;
|
||||
return glm::dot(d, d);
|
||||
}
|
||||
|
||||
bool IsCw(glm::vec2 a, glm::vec2 b, glm::vec2 c) {
|
||||
double lhs = Cross(a - c, b - c);
|
||||
double rhs = 1e-18 * SquaredDistance(a, c) * SquaredDistance(b, c);
|
||||
return lhs * std::abs(lhs) <= rhs;
|
||||
}
|
||||
|
||||
void HullBacktrack(const SimplePolygon& pts, const int idx,
|
||||
std::vector<int>& keep, const int hold) {
|
||||
const int stop = keep.size() - hold;
|
||||
int i = 0;
|
||||
while (i < stop && !IsCw(pts[idx], pts[keep[keep.size() - 1]],
|
||||
pts[keep[keep.size() - 2]])) {
|
||||
keep.pop_back();
|
||||
i++;
|
||||
void HullBacktrack(const glm::vec2& pt, std::vector<glm::vec2>& stack) {
|
||||
auto sz = stack.size();
|
||||
while (sz >= 2 && CCW(stack[sz - 2], stack[sz - 1], pt, 0.0f) <= 0.0f) {
|
||||
stack.pop_back();
|
||||
sz = stack.size();
|
||||
}
|
||||
}
|
||||
|
||||
// Based on method described here:
|
||||
// https://www.hackerearth.com/practice/math/geometry/line-sweep-technique/tutorial/
|
||||
// Changed to follow:
|
||||
// https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
|
||||
// This is the same algorithm (Andrew, also called Montone Chain).
|
||||
C2::PathD HullImpl(SimplePolygon& pts) {
|
||||
int len = pts.size();
|
||||
if (len < 3) return C2::PathD(); // not enough points to create a polygon
|
||||
std::sort(pts.begin(), pts.end(), V2Lesser);
|
||||
auto keep = std::vector<int>{0, 1};
|
||||
for (int i = 2; i < len; i++) {
|
||||
HullBacktrack(pts, i, keep, 1);
|
||||
keep.push_back(i);
|
||||
|
||||
auto lower = std::vector<glm::vec2>{};
|
||||
for (int i = 0; i < len; i++) {
|
||||
HullBacktrack(pts[i], lower);
|
||||
lower.push_back(pts[i]);
|
||||
}
|
||||
int nLower = keep.size();
|
||||
for (int i = 0; i < len - 1; i++) {
|
||||
int idx = len - 2 - i;
|
||||
HullBacktrack(pts, idx, keep, nLower);
|
||||
if (idx > 0) keep.push_back(idx);
|
||||
auto upper = std::vector<glm::vec2>{};
|
||||
for (int i = len - 1; i >= 0; i--) {
|
||||
HullBacktrack(pts[i], upper);
|
||||
upper.push_back(pts[i]);
|
||||
}
|
||||
auto path = C2::PathD(keep.size());
|
||||
for (int i = 0; i < keep.size(); i++) {
|
||||
path[i] = v2_to_pd(pts[keep[i]]);
|
||||
|
||||
upper.pop_back();
|
||||
lower.pop_back();
|
||||
|
||||
auto path = C2::PathD(lower.size() + upper.size());
|
||||
for (int i = 0; i < lower.size(); i++) {
|
||||
path[i] = v2_to_pd(lower[i]);
|
||||
}
|
||||
auto llen = lower.size();
|
||||
int sz = upper.size(); // "fix" -Waggressive-loop-optimizations warning.
|
||||
for (int i = 0; i < sz; i++) {
|
||||
path[i + llen] = v2_to_pd(upper[i]);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
@@ -555,21 +563,42 @@ CrossSection CrossSection::Transform(const glm::mat3x2& m) const {
|
||||
*/
|
||||
CrossSection CrossSection::Warp(
|
||||
std::function<void(glm::vec2&)> warpFunc) const {
|
||||
auto paths = GetPaths();
|
||||
auto warped = C2::PathsD();
|
||||
warped.reserve(paths->paths_.size());
|
||||
for (auto path : paths->paths_) {
|
||||
auto sz = path.size();
|
||||
auto s = C2::PathD(sz);
|
||||
for (int i = 0; i < sz; ++i) {
|
||||
auto v = v2_of_pd(path[i]);
|
||||
warpFunc(v);
|
||||
s[i] = v2_to_pd(v);
|
||||
return WarpBatch([&warpFunc](VecView<glm::vec2> vecs) {
|
||||
for (glm::vec2& p : vecs) {
|
||||
warpFunc(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as CrossSection::Warp but calls warpFunc with
|
||||
* a VecView which is roughly equivalent to std::span
|
||||
* pointing to all vec2 elements to be modified in-place
|
||||
*
|
||||
* @param warpFunc A function that modifies multiple vertex positions.
|
||||
*/
|
||||
CrossSection CrossSection::WarpBatch(
|
||||
std::function<void(VecView<glm::vec2>)> warpFunc) const {
|
||||
std::vector<glm::vec2> tmp_verts;
|
||||
C2::PathsD paths = GetPaths()->paths_; // deep copy
|
||||
for (C2::PathD const& path : paths) {
|
||||
for (C2::PointD const& p : path) {
|
||||
tmp_verts.push_back(v2_of_pd(p));
|
||||
}
|
||||
warped.push_back(s);
|
||||
}
|
||||
|
||||
warpFunc(VecView<glm::vec2>(tmp_verts.data(), tmp_verts.size()));
|
||||
|
||||
auto cursor = tmp_verts.begin();
|
||||
for (C2::PathD& path : paths) {
|
||||
for (C2::PointD& p : path) {
|
||||
p = v2_to_pd(*cursor);
|
||||
++cursor;
|
||||
}
|
||||
}
|
||||
|
||||
return CrossSection(
|
||||
shared_paths(C2::Union(warped, C2::FillRule::Positive, precision_)));
|
||||
shared_paths(C2::Union(paths, C2::FillRule::Positive, precision_)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -585,7 +614,28 @@ CrossSection CrossSection::Warp(
|
||||
* offseting operations are to be performed, which would compound the issue.
|
||||
*/
|
||||
CrossSection CrossSection::Simplify(double epsilon) const {
|
||||
auto ps = SimplifyPaths(GetPaths()->paths_, epsilon, false);
|
||||
C2::PolyTreeD tree;
|
||||
C2::BooleanOp(C2::ClipType::Union, C2::FillRule::Positive, GetPaths()->paths_,
|
||||
C2::PathsD(), tree, precision_);
|
||||
|
||||
C2::PathsD polys;
|
||||
flatten(&tree, polys, 0);
|
||||
|
||||
// Filter out contours less than epsilon wide.
|
||||
C2::PathsD filtered;
|
||||
for (C2::PathD poly : polys) {
|
||||
auto area = C2::Area(poly);
|
||||
Rect box;
|
||||
for (auto vert : poly) {
|
||||
box.Union(glm::vec2(vert.x, vert.y));
|
||||
}
|
||||
glm::vec2 size = box.Size();
|
||||
if (glm::abs(area) > glm::max(size.x, size.y) * epsilon) {
|
||||
filtered.push_back(poly);
|
||||
}
|
||||
}
|
||||
|
||||
auto ps = SimplifyPaths(filtered, epsilon, true);
|
||||
return CrossSection(shared_paths(ps));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,12 @@ project (manifold)
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} ${SOURCE_FILES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC utilities cross_section
|
||||
PRIVATE collider polygon ${MANIFOLD_INCLUDE} Clipper2 quickhull
|
||||
PUBLIC utilities cross_section sdf
|
||||
PRIVATE collider polygon ${MANIFOLD_INCLUDE} quickhull
|
||||
)
|
||||
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
@@ -29,14 +31,5 @@ target_compile_features(${PROJECT_NAME}
|
||||
PUBLIC cxx_std_17
|
||||
)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/manifold.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "cross_section.h"
|
||||
#include "public.h"
|
||||
#include "vec_view.h"
|
||||
|
||||
namespace manifold {
|
||||
|
||||
@@ -45,9 +46,17 @@ class CsgLeafNode;
|
||||
*/
|
||||
struct MeshGL {
|
||||
/// Number of property vertices
|
||||
uint32_t NumVert() const { return vertProperties.size() / numProp; };
|
||||
uint32_t NumVert() const {
|
||||
if (vertProperties.size() / numProp >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
return vertProperties.size() / numProp;
|
||||
};
|
||||
/// Number of triangles
|
||||
uint32_t NumTri() const { return triVerts.size() / 3; };
|
||||
uint32_t NumTri() const {
|
||||
if (vertProperties.size() / numProp >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
return triVerts.size() / 3;
|
||||
};
|
||||
|
||||
/// Number of properties per vertex, always >= 3.
|
||||
uint32_t numProp = 3;
|
||||
@@ -111,6 +120,31 @@ struct MeshGL {
|
||||
* @brief The central classes of the library
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* This library's internal representation of an oriented, 2-manifold, triangle
|
||||
* mesh - a simple boundary-representation of a solid object. Use this class to
|
||||
* store and operate on solids, and use MeshGL for input and output, or
|
||||
* potentially Mesh if only basic geometry is required.
|
||||
*
|
||||
* In addition to storing geometric data, a Manifold can also store an arbitrary
|
||||
* number of vertex properties. These could be anything, e.g. UV coordinates,
|
||||
* colors, bone weights, etc, but this library is completely agnostic. All
|
||||
* properties are merely float values indexed by channel number. It is up to the
|
||||
* user to associate channel numbers with meaning.
|
||||
*
|
||||
* Manifold allows vertex properties to be shared for efficient storage, or to
|
||||
* have multiple property verts associated with a single geometric vertex,
|
||||
* allowing sudden property changes, e.g. at Boolean intersections, without
|
||||
* sacrificing manifoldness.
|
||||
*
|
||||
* Manifolds also keep track of their relationships to their inputs, via
|
||||
* OriginalIDs and the faceIDs and transforms accessible through MeshGL. This
|
||||
* allows object-level properties to be re-associated with the output after many
|
||||
* operations, particularly useful for materials. Since separate object's
|
||||
* properties are not mixed, there is no requirement that channels have
|
||||
* consistent meaning between different inputs.
|
||||
*/
|
||||
class Manifold {
|
||||
public:
|
||||
/** @name Creation
|
||||
@@ -206,11 +240,12 @@ class Manifold {
|
||||
Manifold Transform(const glm::mat4x3&) const;
|
||||
Manifold Mirror(glm::vec3) const;
|
||||
Manifold Warp(std::function<void(glm::vec3&)>) const;
|
||||
Manifold WarpBatch(std::function<void(VecView<glm::vec3>)>) const;
|
||||
Manifold SetProperties(
|
||||
int, std::function<void(float*, glm::vec3, const float*)>) const;
|
||||
Manifold CalculateCurvature(int gaussianIdx, int meanIdx) const;
|
||||
Manifold Refine(int) const;
|
||||
// Manifold RefineToLength(float);
|
||||
Manifold RefineToLength(float) const;
|
||||
// Manifold RefineToPrecision(float);
|
||||
///@}
|
||||
|
||||
@@ -234,9 +269,20 @@ class Manifold {
|
||||
Manifold TrimByPlane(glm::vec3 normal, float originOffset) const;
|
||||
///@}
|
||||
|
||||
/** @name 2D from 3D
|
||||
*/
|
||||
///@{
|
||||
CrossSection Slice(float height = 0) const;
|
||||
CrossSection Project() const;
|
||||
///@}
|
||||
|
||||
/** @name Convex hull
|
||||
*/
|
||||
///@{
|
||||
Manifold Hull() const;
|
||||
static Manifold Hull(const std::vector<Manifold>& manifolds);
|
||||
static Manifold Hull(const std::vector<glm::vec3>& pts);
|
||||
///@}
|
||||
|
||||
/** @name Testing hooks
|
||||
* These are just for internal testing.
|
||||
|
||||
@@ -28,36 +28,34 @@ namespace {
|
||||
// cases to ensure consistency.
|
||||
|
||||
glm::vec2 Interpolate(glm::vec3 pL, glm::vec3 pR, float x) {
|
||||
float dxL = x - pL.x;
|
||||
float dxR = x - pR.x;
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
if (dxL * dxR > 0) printf("Not in domain!\n");
|
||||
#endif
|
||||
bool useL = fabs(dxL) < fabs(dxR);
|
||||
float lambda = (useL ? dxL : dxR) / (pR.x - pL.x);
|
||||
if (!isfinite(lambda)) return glm::vec2(pL.y, pL.z);
|
||||
const float dxL = x - pL.x;
|
||||
const float dxR = x - pR.x;
|
||||
ASSERT(dxL * dxR <= 0, logicErr, "Boolean manifold error: not in domain");
|
||||
const bool useL = fabs(dxL) < fabs(dxR);
|
||||
const glm::vec3 dLR = pR - pL;
|
||||
const float lambda = (useL ? dxL : dxR) / dLR.x;
|
||||
if (!isfinite(lambda) || !isfinite(dLR.y) || !isfinite(dLR.z))
|
||||
return glm::vec2(pL.y, pL.z);
|
||||
glm::vec2 yz;
|
||||
yz[0] = (useL ? pL.y : pR.y) + lambda * (pR.y - pL.y);
|
||||
yz[1] = (useL ? pL.z : pR.z) + lambda * (pR.z - pL.z);
|
||||
yz[0] = (useL ? pL.y : pR.y) + lambda * dLR.y;
|
||||
yz[1] = (useL ? pL.z : pR.z) + lambda * dLR.z;
|
||||
return yz;
|
||||
}
|
||||
|
||||
glm::vec4 Intersect(const glm::vec3 &pL, const glm::vec3 &pR,
|
||||
const glm::vec3 &qL, const glm::vec3 &qR) {
|
||||
float dyL = qL.y - pL.y;
|
||||
float dyR = qR.y - pR.y;
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
if (dyL * dyR > 0) printf("No intersection!\n");
|
||||
#endif
|
||||
bool useL = fabs(dyL) < fabs(dyR);
|
||||
float dx = pR.x - pL.x;
|
||||
const float dyL = qL.y - pL.y;
|
||||
const float dyR = qR.y - pR.y;
|
||||
ASSERT(dyL * dyR <= 0, logicErr, "Boolean manifold error: no intersection");
|
||||
const bool useL = fabs(dyL) < fabs(dyR);
|
||||
const float dx = pR.x - pL.x;
|
||||
float lambda = (useL ? dyL : dyR) / (dyL - dyR);
|
||||
if (!isfinite(lambda)) lambda = 0.0f;
|
||||
glm::vec4 xyzz;
|
||||
xyzz.x = (useL ? pL.x : pR.x) + lambda * dx;
|
||||
float pDy = pR.y - pL.y;
|
||||
float qDy = qR.y - qL.y;
|
||||
bool useP = fabs(pDy) < fabs(qDy);
|
||||
const float pDy = pR.y - pL.y;
|
||||
const float qDy = qR.y - qL.y;
|
||||
const bool useP = fabs(pDy) < fabs(qDy);
|
||||
xyzz.y = (useL ? (useP ? pL.y : qL.y) : (useP ? pR.y : qR.y)) +
|
||||
lambda * (useP ? pDy : qDy);
|
||||
xyzz.z = (useL ? pL.z : pR.z) + lambda * (pR.z - pL.z);
|
||||
@@ -73,9 +71,9 @@ struct CopyFaceEdges {
|
||||
SparseIndices &pXq1;
|
||||
VecView<const Halfedge> halfedgesQ;
|
||||
|
||||
void operator()(thrust::tuple<int, int> in) {
|
||||
void operator()(thrust::tuple<size_t, size_t> in) {
|
||||
int idx = 3 * thrust::get<0>(in);
|
||||
int i = thrust::get<1>(in);
|
||||
size_t i = thrust::get<1>(in);
|
||||
int pX = p1q1.Get(i, inverted);
|
||||
int q2 = p1q1.Get(i, !inverted);
|
||||
|
||||
@@ -85,17 +83,18 @@ struct CopyFaceEdges {
|
||||
int a = pX;
|
||||
int b = edge.IsForward() ? q1 : edge.pairedHalfedge;
|
||||
if (inverted) std::swap(a, b);
|
||||
pXq1.Set(idx + j, a, b);
|
||||
pXq1.Set(idx + static_cast<size_t>(j), a, b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SparseIndices Filter11(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
const SparseIndices &p1q2, const SparseIndices &p2q1) {
|
||||
ZoneScoped;
|
||||
SparseIndices p1q1(3 * p1q2.size() + 3 * p2q1.size());
|
||||
for_each_n(autoPolicy(p1q2.size()), zip(countAt(0), countAt(0)), p1q2.size(),
|
||||
CopyFaceEdges<false>({p1q2, p1q1, inQ.halfedge_}));
|
||||
for_each_n(autoPolicy(p2q1.size()), zip(countAt(p1q2.size()), countAt(0)),
|
||||
for_each_n(autoPolicy(p1q2.size()), zip(countAt(0_z), countAt(0_z)),
|
||||
p1q2.size(), CopyFaceEdges<false>({p1q2, p1q1, inQ.halfedge_}));
|
||||
for_each_n(autoPolicy(p2q1.size()), zip(countAt(p1q2.size()), countAt(0_z)),
|
||||
p2q1.size(), CopyFaceEdges<true>({p2q1, p1q1, inP.halfedge_}));
|
||||
p1q1.Unique();
|
||||
return p1q1;
|
||||
@@ -105,17 +104,6 @@ inline bool Shadows(float p, float q, float dir) {
|
||||
return p == q ? dir < 0 : p < q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since this function is called from two different places, it is necessary that
|
||||
* it returns identical results for identical input to keep consistency.
|
||||
* Normally this is trivial as computers make floating-point errors, but are
|
||||
* at least deterministic. However, in the case of CUDA, these functions can be
|
||||
* compiled by two different compilers (for the CPU and GPU). We have found that
|
||||
* the different compilers can cause slightly different rounding errors, so it
|
||||
* is critical that the two places this function is called both use the same
|
||||
* compiled function (they must agree on CPU or GPU). This is now taken care of
|
||||
* by the shared policy_ member.
|
||||
*/
|
||||
inline thrust::pair<int, glm::vec2> Shadow01(
|
||||
const int p0, const int q1, VecView<const glm::vec3> vertPosP,
|
||||
VecView<const glm::vec3> vertPosQ, VecView<const Halfedge> halfedgeQ,
|
||||
@@ -149,14 +137,14 @@ inline thrust::pair<int, glm::vec2> Shadow01(
|
||||
|
||||
// https://github.com/scandum/binary_search/blob/master/README.md
|
||||
// much faster than standard binary search on large arrays
|
||||
int monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
|
||||
size_t monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
|
||||
if (array.size() == 0) {
|
||||
return -1;
|
||||
}
|
||||
unsigned int bot = 0;
|
||||
unsigned int top = array.size();
|
||||
size_t bot = 0;
|
||||
size_t top = array.size();
|
||||
while (top >= 65536) {
|
||||
unsigned int mid = top / 4;
|
||||
size_t mid = top / 4;
|
||||
top -= mid * 3;
|
||||
if (key < array[bot + mid * 2]) {
|
||||
if (key >= array[bot + mid]) {
|
||||
@@ -171,7 +159,7 @@ int monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
|
||||
}
|
||||
|
||||
while (top > 3) {
|
||||
unsigned int mid = top / 2;
|
||||
size_t mid = top / 2;
|
||||
if (key >= array[bot + mid]) {
|
||||
bot += mid;
|
||||
}
|
||||
@@ -195,7 +183,7 @@ struct Kernel11 {
|
||||
VecView<const glm::vec3> normalP;
|
||||
const SparseIndices &p1q1;
|
||||
|
||||
void operator()(thrust::tuple<int, glm::vec4 &, int &> inout) {
|
||||
void operator()(thrust::tuple<size_t, glm::vec4 &, int &> inout) {
|
||||
const int p1 = p1q1.Get(thrust::get<0>(inout), false);
|
||||
const int q1 = p1q1.Get(thrust::get<0>(inout), true);
|
||||
glm::vec4 &xyzz11 = thrust::get<1>(inout);
|
||||
@@ -248,12 +236,7 @@ struct Kernel11 {
|
||||
if (s11 == 0) { // No intersection
|
||||
xyzz11 = glm::vec4(NAN);
|
||||
} else {
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
// Assert left and right were both found
|
||||
if (k != 2) {
|
||||
printf("k = %d\n", k);
|
||||
}
|
||||
#endif
|
||||
ASSERT(k == 2, logicErr, "Boolean manifold error: s11");
|
||||
xyzz11 = Intersect(pRL[0], pRL[1], qRL[0], qRL[1]);
|
||||
|
||||
const int p1s = halfedgeP[p1].startVert;
|
||||
@@ -273,11 +256,12 @@ std::tuple<Vec<int>, Vec<glm::vec4>> Shadow11(SparseIndices &p1q1,
|
||||
const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ,
|
||||
float expandP) {
|
||||
ZoneScoped;
|
||||
Vec<int> s11(p1q1.size());
|
||||
Vec<glm::vec4> xyzz11(p1q1.size());
|
||||
|
||||
for_each_n(autoPolicy(p1q1.size()),
|
||||
zip(countAt(0), xyzz11.begin(), s11.begin()), p1q1.size(),
|
||||
zip(countAt(0_z), xyzz11.begin(), s11.begin()), p1q1.size(),
|
||||
Kernel11({inP.vertPos_, inQ.vertPos_, inP.halfedge_, inQ.halfedge_,
|
||||
expandP, inP.vertNormal_, p1q1}));
|
||||
|
||||
@@ -295,7 +279,7 @@ struct Kernel02 {
|
||||
const SparseIndices &p0q2;
|
||||
const bool forward;
|
||||
|
||||
void operator()(thrust::tuple<int, int &, float &> inout) {
|
||||
void operator()(thrust::tuple<size_t, int &, float &> inout) {
|
||||
const int p0 = p0q2.Get(thrust::get<0>(inout), !forward);
|
||||
const int q2 = p0q2.Get(thrust::get<0>(inout), forward);
|
||||
int &s02 = thrust::get<1>(inout);
|
||||
@@ -344,12 +328,7 @@ struct Kernel02 {
|
||||
if (s02 == 0) { // No intersection
|
||||
z02 = NAN;
|
||||
} else {
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
// Assert left and right were both found
|
||||
if (k != 2) {
|
||||
printf("k = %d\n", k);
|
||||
}
|
||||
#endif
|
||||
ASSERT(k == 2, logicErr, "Boolean manifold error: s02");
|
||||
glm::vec3 vertPos = vertPosP[p0];
|
||||
z02 = Interpolate(yzzRL[0], yzzRL[1], vertPos.y)[1];
|
||||
if (forward) {
|
||||
@@ -367,12 +346,13 @@ std::tuple<Vec<int>, Vec<float>> Shadow02(const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ,
|
||||
SparseIndices &p0q2, bool forward,
|
||||
float expandP) {
|
||||
ZoneScoped;
|
||||
Vec<int> s02(p0q2.size());
|
||||
Vec<float> z02(p0q2.size());
|
||||
|
||||
auto vertNormalP = forward ? inP.vertNormal_ : inQ.vertNormal_;
|
||||
for_each_n(autoPolicy(p0q2.size()), zip(countAt(0), s02.begin(), z02.begin()),
|
||||
p0q2.size(),
|
||||
for_each_n(autoPolicy(p0q2.size()),
|
||||
zip(countAt(0_z), s02.begin(), z02.begin()), p0q2.size(),
|
||||
Kernel02({inP.vertPos_, inQ.halfedge_, inQ.vertPos_, expandP,
|
||||
vertNormalP, p0q2, forward}));
|
||||
|
||||
@@ -394,7 +374,7 @@ struct Kernel12 {
|
||||
const bool forward;
|
||||
const SparseIndices &p1q2;
|
||||
|
||||
void operator()(thrust::tuple<int, int &, glm::vec3 &> inout) {
|
||||
void operator()(thrust::tuple<size_t, int &, glm::vec3 &> inout) {
|
||||
int p1 = p1q2.Get(thrust::get<0>(inout), !forward);
|
||||
int q2 = p1q2.Get(thrust::get<0>(inout), forward);
|
||||
int &x12 = thrust::get<1>(inout);
|
||||
@@ -414,7 +394,7 @@ struct Kernel12 {
|
||||
for (int vert : {edge.startVert, edge.endVert}) {
|
||||
const int64_t key = forward ? SparseIndices::EncodePQ(vert, q2)
|
||||
: SparseIndices::EncodePQ(q2, vert);
|
||||
const int idx = monobound_quaternary_search(p0q2, key);
|
||||
const size_t idx = monobound_quaternary_search(p0q2, key);
|
||||
if (idx != -1) {
|
||||
const int s = s02[idx];
|
||||
x12 += s * ((vert == edge.startVert) == forward ? 1 : -1);
|
||||
@@ -435,7 +415,7 @@ struct Kernel12 {
|
||||
const int q1F = edge.IsForward() ? q1 : edge.pairedHalfedge;
|
||||
const int64_t key = forward ? SparseIndices::EncodePQ(p1, q1F)
|
||||
: SparseIndices::EncodePQ(q1F, p1);
|
||||
const int idx = monobound_quaternary_search(p1q1, key);
|
||||
const size_t idx = monobound_quaternary_search(p1q1, key);
|
||||
if (idx != -1) { // s is implicitly zero for anything not found
|
||||
const int s = s11[idx];
|
||||
x12 -= s * (edge.IsForward() ? 1 : -1);
|
||||
@@ -456,12 +436,7 @@ struct Kernel12 {
|
||||
if (x12 == 0) { // No intersection
|
||||
v12 = glm::vec3(NAN);
|
||||
} else {
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
// Assert left and right were both found
|
||||
if (k != 2) {
|
||||
printf("k = %d\n", k);
|
||||
}
|
||||
#endif
|
||||
ASSERT(k == 2, logicErr, "Boolean manifold error: v12");
|
||||
const glm::vec4 xzyy =
|
||||
Intersect(xzyLR0[0], xzyLR0[1], xzyLR1[0], xzyLR1[1]);
|
||||
v12.x = xzyy[0];
|
||||
@@ -476,11 +451,12 @@ std::tuple<Vec<int>, Vec<glm::vec3>> Intersect12(
|
||||
const SparseIndices &p0q2, const Vec<int> &s11, const SparseIndices &p1q1,
|
||||
const Vec<float> &z02, const Vec<glm::vec4> &xyzz11, SparseIndices &p1q2,
|
||||
bool forward) {
|
||||
ZoneScoped;
|
||||
Vec<int> x12(p1q2.size());
|
||||
Vec<glm::vec3> v12(p1q2.size());
|
||||
|
||||
for_each_n(
|
||||
autoPolicy(p1q2.size()), zip(countAt(0), x12.begin(), v12.begin()),
|
||||
autoPolicy(p1q2.size()), zip(countAt(0_z), x12.begin(), v12.begin()),
|
||||
p1q2.size(),
|
||||
Kernel12({p0q2.AsVec64(), s02, z02, p1q1.AsVec64(), s11, xyzz11,
|
||||
inP.halfedge_, inQ.halfedge_, inP.vertPos_, forward, p1q2}));
|
||||
@@ -492,6 +468,7 @@ std::tuple<Vec<int>, Vec<glm::vec3>> Intersect12(
|
||||
|
||||
Vec<int> Winding03(const Manifold::Impl &inP, Vec<int> &vertices, Vec<int> &s02,
|
||||
bool reverse) {
|
||||
ZoneScoped;
|
||||
// verts that are not shadowed (not in p0q2) have winding number zero.
|
||||
Vec<int> w03(inP.NumVert(), 0);
|
||||
// checking is slow, so just sort and reduce
|
||||
@@ -601,6 +578,9 @@ Boolean3::Boolean3(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
Intersect12(inQ, inP, s20, p2q0, s11, p1q1, z20, xyzz11, p2q1_, false);
|
||||
PRINT("x21 size = " << x21_.size());
|
||||
|
||||
if (x12_.size() + x21_.size() >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
|
||||
Vec<int> p0 = p0q2.Copy(false);
|
||||
p0q2.Resize(0);
|
||||
Vec<int> q0 = p2q0.Copy(true);
|
||||
@@ -616,7 +596,6 @@ Boolean3::Boolean3(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
if (ManifoldParams().verbose) {
|
||||
broad.Print("Broad phase");
|
||||
intersections.Print("Intersections");
|
||||
MemUsage();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -101,7 +101,12 @@ std::tuple<Vec<int>, Vec<int>> SizeOutput(
|
||||
const Vec<int> &i03, const Vec<int> &i30, const Vec<int> &i12,
|
||||
const Vec<int> &i21, const SparseIndices &p1q2, const SparseIndices &p2q1,
|
||||
bool invertQ) {
|
||||
ZoneScoped;
|
||||
Vec<int> sidesPerFacePQ(inP.NumTri() + inQ.NumTri(), 0);
|
||||
// note: numFaceR <= facePQ2R.size() = sidesPerFacePQ.size() + 1
|
||||
if (sidesPerFacePQ.size() + 1 >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("boolean result too large");
|
||||
|
||||
auto sidesPerFaceP = sidesPerFacePQ.view(0, inP.NumTri());
|
||||
auto sidesPerFaceQ = sidesPerFacePQ.view(inP.NumTri(), inQ.NumTri());
|
||||
|
||||
@@ -167,13 +172,14 @@ void AddNewEdgeVerts(
|
||||
concurrent_map<std::pair<int, int>, std::vector<EdgePos>> &edgesNew,
|
||||
const SparseIndices &p1q2, const Vec<int> &i12, const Vec<int> &v12R,
|
||||
const Vec<Halfedge> &halfedgeP, bool forward) {
|
||||
ZoneScoped;
|
||||
// For each edge of P that intersects a face of Q (p1q2), add this vertex to
|
||||
// P's corresponding edge vector and to the two new edges, which are
|
||||
// intersections between the face of Q and the two faces of P attached to the
|
||||
// edge. The direction and duplicity are given by i12, while v12R remaps to
|
||||
// the output vert index. When forward is false, all is reversed.
|
||||
auto process = [&](std::function<void(size_t)> lock,
|
||||
std::function<void(size_t)> unlock, int i) {
|
||||
std::function<void(size_t)> unlock, size_t i) {
|
||||
const int edgeP = p1q2.Get(i, !forward);
|
||||
const int faceQ = p1q2.Get(i, forward);
|
||||
const int vert = v12R[i];
|
||||
@@ -216,9 +222,9 @@ void AddNewEdgeVerts(
|
||||
[&](size_t hash) { mutexes[hash % mutexes.size()].unlock(); },
|
||||
std::placeholders::_1);
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<int>(0, p1q2.size(), 32),
|
||||
[&](const tbb::blocked_range<int> &range) {
|
||||
for (int i = range.begin(); i != range.end(); i++) processFun(i);
|
||||
tbb::blocked_range<size_t>(0_z, p1q2.size(), 32),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t i = range.begin(); i != range.end(); i++) processFun(i);
|
||||
},
|
||||
ap);
|
||||
return;
|
||||
@@ -226,7 +232,7 @@ void AddNewEdgeVerts(
|
||||
#endif
|
||||
auto processFun = std::bind(
|
||||
process, [](size_t _) {}, [](size_t _) {}, std::placeholders::_1);
|
||||
for (int i = 0; i < p1q2.size(); ++i) processFun(i);
|
||||
for (size_t i = 0; i < p1q2.size(); ++i) processFun(i);
|
||||
}
|
||||
|
||||
std::vector<Halfedge> PairUp(std::vector<EdgePos> &edgePos) {
|
||||
@@ -237,7 +243,7 @@ std::vector<Halfedge> PairUp(std::vector<EdgePos> &edgePos) {
|
||||
// a heuristic.
|
||||
ASSERT(edgePos.size() % 2 == 0, topologyErr,
|
||||
"Non-manifold edge! Not an even number of points.");
|
||||
int nEdges = edgePos.size() / 2;
|
||||
size_t nEdges = edgePos.size() / 2;
|
||||
auto middle = std::partition(edgePos.begin(), edgePos.end(),
|
||||
[](EdgePos x) { return x.isStart; });
|
||||
ASSERT(middle - edgePos.begin() == nEdges, topologyErr, "Non-manifold edge!");
|
||||
@@ -245,7 +251,7 @@ std::vector<Halfedge> PairUp(std::vector<EdgePos> &edgePos) {
|
||||
std::stable_sort(edgePos.begin(), middle, cmp);
|
||||
std::stable_sort(middle, edgePos.end(), cmp);
|
||||
std::vector<Halfedge> edges;
|
||||
for (int i = 0; i < nEdges; ++i)
|
||||
for (size_t i = 0; i < nEdges; ++i)
|
||||
edges.push_back({edgePos[i].vert, edgePos[i + nEdges].vert, -1, -1});
|
||||
return edges;
|
||||
}
|
||||
@@ -256,6 +262,7 @@ void AppendPartialEdges(Manifold::Impl &outR, Vec<char> &wholeHalfedgeP,
|
||||
Vec<TriRef> &halfedgeRef, const Manifold::Impl &inP,
|
||||
const Vec<int> &i03, const Vec<int> &vP2R,
|
||||
const Vec<int>::IterC faceP2R, bool forward) {
|
||||
ZoneScoped;
|
||||
// Each edge in the map is partially retained; for each of these, look up
|
||||
// their original verts and include them based on their winding number (i03),
|
||||
// while remapping them to the output using vP2R. Use the verts position
|
||||
@@ -282,7 +289,6 @@ void AppendPartialEdges(Manifold::Impl &outR, Vec<char> &wholeHalfedgeP,
|
||||
}
|
||||
|
||||
int inclusion = i03[vStart];
|
||||
bool reversed = inclusion < 0;
|
||||
EdgePos edgePos = {vP2R[vStart],
|
||||
glm::dot(outR.vertPos_[vP2R[vStart]], edgeVec),
|
||||
inclusion > 0};
|
||||
@@ -292,7 +298,6 @@ void AppendPartialEdges(Manifold::Impl &outR, Vec<char> &wholeHalfedgeP,
|
||||
}
|
||||
|
||||
inclusion = i03[vEnd];
|
||||
reversed |= inclusion < 0;
|
||||
edgePos = {vP2R[vEnd], glm::dot(outR.vertPos_[vP2R[vEnd]], edgeVec),
|
||||
inclusion < 0};
|
||||
for (int j = 0; j < glm::abs(inclusion); ++j) {
|
||||
@@ -337,6 +342,7 @@ void AppendNewEdges(
|
||||
Manifold::Impl &outR, Vec<int> &facePtrR,
|
||||
concurrent_map<std::pair<int, int>, std::vector<EdgePos>> &edgesNew,
|
||||
Vec<TriRef> &halfedgeRef, const Vec<int> &facePQ2R, const int numFaceP) {
|
||||
ZoneScoped;
|
||||
// Pair up each edge's verts and distribute to faces based on indices in key.
|
||||
Vec<Halfedge> &halfedgeR = outR.halfedge_;
|
||||
Vec<glm::vec3> &vertPosR = outR.vertPos_;
|
||||
@@ -399,7 +405,6 @@ struct DuplicateHalfedges {
|
||||
if (!thrust::get<0>(in)) return;
|
||||
Halfedge halfedge = thrust::get<1>(in);
|
||||
if (!halfedge.IsForward()) return;
|
||||
const int edgeP = thrust::get<2>(in);
|
||||
|
||||
const int inclusion = i03[halfedge.startVert];
|
||||
if (inclusion == 0) return;
|
||||
@@ -442,6 +447,7 @@ void AppendWholeEdges(Manifold::Impl &outR, Vec<int> &facePtrR,
|
||||
const Vec<char> wholeHalfedgeP, const Vec<int> &i03,
|
||||
const Vec<int> &vP2R, VecView<const int> faceP2R,
|
||||
bool forward) {
|
||||
ZoneScoped;
|
||||
for_each_n(ManifoldParams().deterministic ? ExecutionPolicy::Seq
|
||||
: autoPolicy(inP.halfedge_.size()),
|
||||
zip(wholeHalfedgeP.begin(), inP.halfedge_.begin(), countAt(0)),
|
||||
@@ -463,9 +469,8 @@ struct MapTriRef {
|
||||
}
|
||||
};
|
||||
|
||||
Vec<TriRef> UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ, bool invertQ) {
|
||||
Vec<TriRef> refPQ = outR.meshRelation_.triRef;
|
||||
void UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ, bool invertQ) {
|
||||
const int offsetQ = Manifold::Impl::meshIDCounter_;
|
||||
for_each_n(
|
||||
autoPolicy(outR.NumTri()), outR.meshRelation_.triRef.begin(),
|
||||
@@ -480,7 +485,6 @@ Vec<TriRef> UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
|
||||
outR.meshRelation_.meshIDtransform[pair.first + offsetQ].backSide ^=
|
||||
invertQ;
|
||||
}
|
||||
return refPQ;
|
||||
}
|
||||
|
||||
struct Barycentric {
|
||||
@@ -514,8 +518,9 @@ struct Barycentric {
|
||||
}
|
||||
};
|
||||
|
||||
void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
const Manifold::Impl &inP, const Manifold::Impl &inQ) {
|
||||
void CreateProperties(Manifold::Impl &outR, const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ) {
|
||||
ZoneScoped;
|
||||
const int numPropP = inP.NumProp();
|
||||
const int numPropQ = inQ.NumProp();
|
||||
const int numProp = glm::max(numPropP, numPropQ);
|
||||
@@ -526,7 +531,8 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
outR.meshRelation_.triProperties.resize(numTri);
|
||||
|
||||
Vec<glm::vec3> bary(outR.halfedge_.size());
|
||||
for_each_n(autoPolicy(numTri), zip(countAt(0), refPQ.cbegin()), numTri,
|
||||
for_each_n(autoPolicy(numTri),
|
||||
zip(countAt(0), outR.meshRelation_.triRef.cbegin()), numTri,
|
||||
Barycentric({bary, inP.vertPos_, inQ.vertPos_, outR.vertPos_,
|
||||
inP.halfedge_, inQ.halfedge_, outR.halfedge_,
|
||||
outR.precision_}));
|
||||
@@ -534,6 +540,14 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
using Entry = std::pair<glm::ivec3, int>;
|
||||
int idMissProp = outR.NumVert();
|
||||
std::vector<std::vector<Entry>> propIdx(outR.NumVert() + 1);
|
||||
std::vector<int> propMissIdx[2];
|
||||
propMissIdx[0].resize(inQ.NumPropVert(), -1);
|
||||
propMissIdx[1].resize(inP.NumPropVert(), -1);
|
||||
|
||||
if (static_cast<size_t>(outR.NumVert()) * static_cast<size_t>(numProp) >=
|
||||
std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("too many vertices");
|
||||
|
||||
outR.meshRelation_.properties.reserve(outR.NumVert() * numProp);
|
||||
int idx = 0;
|
||||
|
||||
@@ -541,14 +555,14 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
// Skip collapsed triangles
|
||||
if (outR.halfedge_[3 * tri].startVert < 0) continue;
|
||||
|
||||
const int triPQ = refPQ[tri].tri;
|
||||
const bool PQ = refPQ[tri].meshID == 0;
|
||||
const TriRef ref = outR.meshRelation_.triRef[tri];
|
||||
const bool PQ = ref.meshID == 0;
|
||||
const int oldNumProp = PQ ? numPropP : numPropQ;
|
||||
const auto &properties =
|
||||
PQ ? inP.meshRelation_.properties : inQ.meshRelation_.properties;
|
||||
const glm::ivec3 &triProp = oldNumProp == 0 ? glm::ivec3(-1)
|
||||
: PQ ? inP.meshRelation_.triProperties[triPQ]
|
||||
: inQ.meshRelation_.triProperties[triPQ];
|
||||
: PQ ? inP.meshRelation_.triProperties[ref.tri]
|
||||
: inQ.meshRelation_.triProperties[ref.tri];
|
||||
|
||||
for (const int i : {0, 1, 2}) {
|
||||
const int vert = outR.halfedge_[3 * tri + i].startVert;
|
||||
@@ -556,8 +570,7 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
|
||||
glm::ivec4 key(PQ, idMissProp, -1, -1);
|
||||
if (oldNumProp > 0) {
|
||||
key[1] = vert;
|
||||
int edge = -1;
|
||||
int edge = -2;
|
||||
for (const int j : {0, 1, 2}) {
|
||||
if (uvw[j] == 1) {
|
||||
// On a retained vert, the propVert must also match
|
||||
@@ -571,24 +584,37 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
// On an edge, both propVerts must match
|
||||
const int p0 = triProp[Next3(edge)];
|
||||
const int p1 = triProp[Prev3(edge)];
|
||||
key[1] = vert;
|
||||
key[2] = glm::min(p0, p1);
|
||||
key[3] = glm::max(p0, p1);
|
||||
} else if (edge == -2) {
|
||||
key[1] = vert;
|
||||
}
|
||||
}
|
||||
|
||||
auto &bin = propIdx[key.y];
|
||||
bool bFound = false;
|
||||
for (int k = 0; k < bin.size(); ++k) {
|
||||
if (bin[k].first == glm::ivec3(key.x, key.z, key.w)) {
|
||||
bFound = true;
|
||||
outR.meshRelation_.triProperties[tri][i] = bin[k].second;
|
||||
break;
|
||||
if (key.y == idMissProp && key.z >= 0) {
|
||||
// only key.x/key.z matters
|
||||
auto &entry = propMissIdx[key.x][key.z];
|
||||
if (entry >= 0) {
|
||||
outR.meshRelation_.triProperties[tri][i] = entry;
|
||||
continue;
|
||||
}
|
||||
entry = idx;
|
||||
} else {
|
||||
auto &bin = propIdx[key.y];
|
||||
bool bFound = false;
|
||||
for (int k = 0; k < bin.size(); ++k) {
|
||||
if (bin[k].first == glm::ivec3(key.x, key.z, key.w)) {
|
||||
bFound = true;
|
||||
outR.meshRelation_.triProperties[tri][i] = bin[k].second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bFound) continue;
|
||||
bin.push_back(std::make_pair(glm::ivec3(key.x, key.z, key.w), idx));
|
||||
}
|
||||
if (bFound) continue;
|
||||
bin.push_back(std::make_pair(glm::ivec3(key.x, key.z, key.w), idx));
|
||||
|
||||
outR.meshRelation_.triProperties[tri][i] = idx++;
|
||||
|
||||
for (int p = 0; p < numProp; ++p) {
|
||||
if (p < oldNumProp) {
|
||||
glm::vec3 oldProps;
|
||||
@@ -724,7 +750,6 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
std::tie(faceEdge, facePQ2R) =
|
||||
SizeOutput(outR, inP_, inQ_, i03, i30, i12, i21, p1q2_, p2q1_, invertQ);
|
||||
|
||||
const int numFaceR = faceEdge.size() - 1;
|
||||
// This gets incremented for each halfedge that's added to a face so that the
|
||||
// next one knows where to slot in.
|
||||
Vec<int> facePtrR = faceEdge;
|
||||
@@ -770,12 +795,12 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
if (ManifoldParams().intermediateChecks)
|
||||
ASSERT(outR.IsManifold(), logicErr, "triangulated mesh is not manifold!");
|
||||
|
||||
Vec<TriRef> refPQ = UpdateReference(outR, inP_, inQ_, invertQ);
|
||||
CreateProperties(outR, inP_, inQ_);
|
||||
|
||||
UpdateReference(outR, inP_, inQ_, invertQ);
|
||||
|
||||
outR.SimplifyTopology();
|
||||
|
||||
CreateProperties(outR, refPQ, inP_, inQ_);
|
||||
|
||||
if (ManifoldParams().intermediateChecks)
|
||||
ASSERT(outR.Is2Manifold(), logicErr, "simplified mesh is not 2-manifold!");
|
||||
|
||||
|
||||
@@ -153,10 +153,9 @@ Manifold Manifold::Cube(glm::vec3 size, bool center) {
|
||||
glm::length(size) == 0.) {
|
||||
return Invalid();
|
||||
}
|
||||
auto cube = Manifold(std::make_shared<Impl>(Impl::Shape::Cube));
|
||||
cube = cube.Scale(size);
|
||||
if (center) cube = cube.Translate(-size / 2.0f);
|
||||
return cube.AsOriginal();
|
||||
glm::mat4x3 m =
|
||||
glm::translate(center ? (-size / 2.0f) : glm::vec3(0)) * glm::scale(size);
|
||||
return Manifold(std::make_shared<Impl>(Manifold::Impl::Shape::Cube, m));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +207,7 @@ Manifold Manifold::Sphere(float radius, int circularSegments) {
|
||||
int n = circularSegments > 0 ? (circularSegments + 3) / 4
|
||||
: Quality::GetCircularSegments(radius) / 4;
|
||||
auto pImpl_ = std::make_shared<Impl>(Impl::Shape::Octahedron);
|
||||
pImpl_->Subdivide(n);
|
||||
pImpl_->Subdivide([n](glm::vec3 edge) { return n - 1; });
|
||||
for_each_n(autoPolicy(pImpl_->NumVert()), pImpl_->vertPos_.begin(),
|
||||
pImpl_->NumVert(), ToSphere({radius}));
|
||||
pImpl_->Finish();
|
||||
@@ -220,6 +219,9 @@ Manifold Manifold::Sphere(float radius, int circularSegments) {
|
||||
/**
|
||||
* Constructs a manifold from a set of polygons by extruding them along the
|
||||
* Z-axis.
|
||||
* Note that high twistDegrees with small nDivisions may cause
|
||||
* self-intersection. This is not checked here and it is up to the user to
|
||||
* choose the correct parameters.
|
||||
*
|
||||
* @param crossSection A set of non-overlapping polygons to extrude.
|
||||
* @param height Z-extent of extrusion.
|
||||
@@ -230,11 +232,13 @@ Manifold Manifold::Sphere(float radius, int circularSegments) {
|
||||
* bottom, interpolated linearly for the divisions in between.
|
||||
* @param scaleTop Amount to scale the top (independently in X and Y). If the
|
||||
* scale is {0, 0}, a pure cone is formed with only a single vertex at the top.
|
||||
* Note that scale is applied after twist.
|
||||
* Default {1, 1}.
|
||||
*/
|
||||
Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
|
||||
int nDivisions, float twistDegrees,
|
||||
glm::vec2 scaleTop) {
|
||||
ZoneScoped;
|
||||
auto polygons = crossSection.ToPolygons();
|
||||
if (polygons.size() == 0 || height <= 0.0f) {
|
||||
return Invalid();
|
||||
@@ -264,9 +268,9 @@ Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
|
||||
for (int i = 1; i < nDivisions + 1; ++i) {
|
||||
float alpha = i / float(nDivisions);
|
||||
float phi = alpha * twistDegrees;
|
||||
glm::mat2 transform(cosd(phi), sind(phi), -sind(phi), cosd(phi));
|
||||
glm::vec2 scale = glm::mix(glm::vec2(1.0f), scaleTop, alpha);
|
||||
transform = transform * glm::mat2(scale.x, 0.0f, 0.0f, scale.y);
|
||||
glm::mat2 rotation(cosd(phi), sind(phi), -sind(phi), cosd(phi));
|
||||
glm::mat2 transform = glm::mat2(scale.x, 0.0f, 0.0f, scale.y) * rotation;
|
||||
int j = 0;
|
||||
int idx = 0;
|
||||
for (const auto& poly : polygons) {
|
||||
@@ -320,6 +324,7 @@ Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
|
||||
*/
|
||||
Manifold Manifold::Revolve(const CrossSection& crossSection,
|
||||
int circularSegments, float revolveDegrees) {
|
||||
ZoneScoped;
|
||||
Polygons polygons = crossSection.ToPolygons();
|
||||
|
||||
if (polygons.size() == 0) {
|
||||
@@ -327,8 +332,9 @@ Manifold Manifold::Revolve(const CrossSection& crossSection,
|
||||
}
|
||||
|
||||
const Rect bounds = crossSection.Bounds();
|
||||
const float radius = bounds.max.x;
|
||||
|
||||
if (bounds.max.x <= 0) {
|
||||
if (radius <= 0) {
|
||||
return Invalid();
|
||||
} else if (bounds.min.x < 0) {
|
||||
// Take the x>=0 slice.
|
||||
@@ -340,21 +346,15 @@ Manifold Manifold::Revolve(const CrossSection& crossSection,
|
||||
polygons = (crossSection ^ posBoundingBox).ToPolygons();
|
||||
}
|
||||
|
||||
float radius = 0.0f;
|
||||
for (const auto& poly : polygons) {
|
||||
for (const auto& vert : poly) {
|
||||
radius = fmax(radius, vert.x);
|
||||
}
|
||||
}
|
||||
|
||||
if (revolveDegrees > 360.0f) {
|
||||
revolveDegrees = 360.0f;
|
||||
}
|
||||
const bool isFullRevolution = revolveDegrees == 360.0f;
|
||||
|
||||
const int nDivisions = circularSegments > 2
|
||||
? circularSegments
|
||||
: Quality::GetCircularSegments(radius);
|
||||
const int nDivisions =
|
||||
circularSegments > 2
|
||||
? circularSegments
|
||||
: Quality::GetCircularSegments(radius) * revolveDegrees / 360;
|
||||
|
||||
auto pImpl_ = std::make_shared<Impl>();
|
||||
auto& vertPos = pImpl_->vertPos_;
|
||||
@@ -384,14 +384,14 @@ Manifold Manifold::Revolve(const CrossSection& crossSection,
|
||||
|
||||
if (!isFullRevolution) startPoses.push_back(startPosIndex);
|
||||
|
||||
const glm::vec2 currPolyVertex = poly[polyVert];
|
||||
const glm::vec2 prevPolyVertex =
|
||||
poly[polyVert == 0 ? poly.size() - 1 : polyVert - 1];
|
||||
|
||||
const int prevStartPosIndex =
|
||||
startPosIndex +
|
||||
(polyVert == 0 ? nRevolveAxisVerts + (nSlices * nPosVerts) - nSlices
|
||||
: poly[polyVert - 1].x == 0.0 ? -1
|
||||
: -nSlices);
|
||||
glm::vec2 currPolyVertex = poly[polyVert];
|
||||
glm::vec2 prevPolyVertex =
|
||||
poly[polyVert == 0 ? poly.size() - 1 : polyVert - 1];
|
||||
(polyVert == 0 ? nRevolveAxisVerts + (nSlices * nPosVerts) : 0) +
|
||||
(prevPolyVertex.x == 0.0 ? -1 : -nSlices);
|
||||
|
||||
for (int slice = 0; slice < nSlices; ++slice) {
|
||||
const float phi = slice * dPhi;
|
||||
@@ -464,6 +464,7 @@ Manifold Manifold::Compose(const std::vector<Manifold>& manifolds) {
|
||||
* containing a copy of the original. It is the inverse operation of Compose().
|
||||
*/
|
||||
std::vector<Manifold> Manifold::Decompose() const {
|
||||
ZoneScoped;
|
||||
UnionFind<> uf(NumVert());
|
||||
// Graph graph;
|
||||
auto pImpl_ = GetCsgLeafNode().GetImpl();
|
||||
|
||||
@@ -172,6 +172,7 @@ CsgNodeType CsgLeafNode::GetNodeType() const { return CsgNodeType::Leaf; }
|
||||
*/
|
||||
Manifold::Impl CsgLeafNode::Compose(
|
||||
const std::vector<std::shared_ptr<CsgLeafNode>> &nodes) {
|
||||
ZoneScoped;
|
||||
float precision = -1;
|
||||
int numVert = 0;
|
||||
int numEdge = 0;
|
||||
@@ -454,6 +455,7 @@ std::shared_ptr<CsgLeafNode> CsgOpNode::ToLeafNode() const {
|
||||
std::shared_ptr<Manifold::Impl> CsgOpNode::BatchBoolean(
|
||||
OpType operation,
|
||||
std::vector<std::shared_ptr<const Manifold::Impl>> &results) {
|
||||
ZoneScoped;
|
||||
auto getImplPtr = GetImplPtr();
|
||||
ASSERT(operation != OpType::Subtract, logicErr,
|
||||
"BatchBoolean doesn't support Difference.");
|
||||
@@ -529,6 +531,7 @@ std::shared_ptr<Manifold::Impl> CsgOpNode::BatchBoolean(
|
||||
* `BatchBoolean` instead of using `Compose` for non-intersecting manifolds.
|
||||
*/
|
||||
void CsgOpNode::BatchUnion() const {
|
||||
ZoneScoped;
|
||||
// INVARIANT: children_ is a vector of leaf nodes
|
||||
// this kMaxUnionSize is a heuristic to avoid the pairwise disjoint check
|
||||
// with O(n^2) complexity to take too long.
|
||||
|
||||
@@ -145,29 +145,32 @@ namespace manifold {
|
||||
void Manifold::Impl::SimplifyTopology() {
|
||||
if (!halfedge_.size()) return;
|
||||
|
||||
const int nbEdges = halfedge_.size();
|
||||
const size_t nbEdges = halfedge_.size();
|
||||
auto policy = autoPolicy(nbEdges);
|
||||
int numFlagged = 0;
|
||||
size_t numFlagged = 0;
|
||||
Vec<uint8_t> bflags(nbEdges);
|
||||
|
||||
// In the case of a very bad triangulation, it is possible to create pinched
|
||||
// verts. They must be removed before edge collapse.
|
||||
SplitPinchedVerts();
|
||||
|
||||
Vec<SortEntry> entries;
|
||||
entries.reserve(nbEdges / 2);
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
if (halfedge_[i].IsForward()) {
|
||||
entries.push_back({halfedge_[i].startVert, halfedge_[i].endVert, i});
|
||||
{
|
||||
ZoneScopedN("DedupeEdge");
|
||||
Vec<SortEntry> entries;
|
||||
entries.reserve(nbEdges / 2);
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
if (halfedge_[i].IsForward()) {
|
||||
entries.push_back({halfedge_[i].startVert, halfedge_[i].endVert, i});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stable_sort(policy, entries.begin(), entries.end());
|
||||
for (int i = 0; i < entries.size() - 1; ++i) {
|
||||
if (entries[i].start == entries[i + 1].start &&
|
||||
entries[i].end == entries[i + 1].end) {
|
||||
DedupeEdge(entries[i].index);
|
||||
numFlagged++;
|
||||
stable_sort(policy, entries.begin(), entries.end());
|
||||
for (int i = 0; i < entries.size() - 1; ++i) {
|
||||
if (entries[i].start == entries[i + 1].start &&
|
||||
entries[i].end == entries[i + 1].end) {
|
||||
DedupeEdge(entries[i].index);
|
||||
numFlagged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,13 +181,19 @@ void Manifold::Impl::SimplifyTopology() {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ManifoldParams().cleanupTriangles) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<int> scratchBuffer;
|
||||
scratchBuffer.reserve(10);
|
||||
{
|
||||
ZoneScopedN("CollapseShortEdge");
|
||||
numFlagged = 0;
|
||||
ShortEdge se{halfedge_, vertPos_, precision_};
|
||||
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
for_each_n(policy, countAt(0_z), nbEdges,
|
||||
[&](size_t i) { bflags[i] = se(i); });
|
||||
for (size_t i = 0; i < nbEdges; ++i) {
|
||||
if (bflags[i]) {
|
||||
CollapseEdge(i, scratchBuffer);
|
||||
scratchBuffer.resize(0);
|
||||
@@ -201,10 +210,12 @@ void Manifold::Impl::SimplifyTopology() {
|
||||
#endif
|
||||
|
||||
{
|
||||
ZoneScopedN("CollapseFlaggedEdge");
|
||||
numFlagged = 0;
|
||||
FlagEdge se{halfedge_, meshRelation_.triRef};
|
||||
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
for_each_n(policy, countAt(0_z), nbEdges,
|
||||
[&](size_t i) { bflags[i] = se(i); });
|
||||
for (size_t i = 0; i < nbEdges; ++i) {
|
||||
if (bflags[i]) {
|
||||
CollapseEdge(i, scratchBuffer);
|
||||
scratchBuffer.resize(0);
|
||||
@@ -221,13 +232,15 @@ void Manifold::Impl::SimplifyTopology() {
|
||||
#endif
|
||||
|
||||
{
|
||||
ZoneScopedN("RecursiveEdgeSwap");
|
||||
numFlagged = 0;
|
||||
SwappableEdge se{halfedge_, vertPos_, faceNormal_, precision_};
|
||||
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
|
||||
for_each_n(policy, countAt(0_z), nbEdges,
|
||||
[&](size_t i) { bflags[i] = se(i); });
|
||||
std::vector<int> edgeSwapStack;
|
||||
std::vector<int> visited(halfedge_.size(), -1);
|
||||
int tag = 0;
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
for (size_t i = 0; i < nbEdges; ++i) {
|
||||
if (bflags[i]) {
|
||||
numFlagged++;
|
||||
tag++;
|
||||
@@ -501,25 +514,20 @@ void Manifold::Impl::CollapseEdge(const int edge, std::vector<int>& edges) {
|
||||
const int tri1 = toRemove.pairedHalfedge / 3;
|
||||
const int triVert0 = (edge + 1) % 3;
|
||||
const int triVert1 = toRemove.pairedHalfedge % 3;
|
||||
const int prop0 = triProp.size() > 0 ? triProp[tri0][edge % 3] : -1;
|
||||
const int prop1 = triProp.size() > 0
|
||||
? triProp[tri1][(toRemove.pairedHalfedge + 1) % 3]
|
||||
: -1;
|
||||
current = start;
|
||||
while (current != tri0edge[2]) {
|
||||
current = NextHalfedge(current);
|
||||
|
||||
// Update the shifted triangles to the vertBary of endVert
|
||||
const int tri = current / 3;
|
||||
const int vIdx = current - 3 * tri;
|
||||
|
||||
if (!shortEdge) {
|
||||
if (triProp.size() > 0) {
|
||||
if (triProp[tri][vIdx] == prop0) {
|
||||
triProp[tri][vIdx] = triProp[tri0][triVert0];
|
||||
} else if (triProp[tri][vIdx] == prop1) {
|
||||
triProp[tri][vIdx] = triProp[tri1][triVert1];
|
||||
}
|
||||
if (triProp.size() > 0) {
|
||||
// Update the shifted triangles to the vertBary of endVert
|
||||
const int tri = current / 3;
|
||||
const int vIdx = current - 3 * tri;
|
||||
if (triRef[tri].meshID == triRef[tri0].meshID &&
|
||||
triRef[tri].tri == triRef[tri0].tri) {
|
||||
triProp[tri][vIdx] = triProp[tri0][triVert0];
|
||||
} else if (triRef[tri].meshID == triRef[tri1].meshID &&
|
||||
triRef[tri].tri == triRef[tri1].tri) {
|
||||
triProp[tri][vIdx] = triProp[tri1][triVert1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,6 +664,7 @@ void Manifold::Impl::RecursiveEdgeSwap(const int edge, int& tag,
|
||||
}
|
||||
|
||||
void Manifold::Impl::SplitPinchedVerts() {
|
||||
ZoneScoped;
|
||||
std::vector<bool> vertProcessed(NumVert(), false);
|
||||
std::vector<bool> halfedgeProcessed(halfedge_.size(), false);
|
||||
for (int i = 0; i < halfedge_.size(); ++i) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#define TBB_PREVIEW_CONCURRENT_ORDERED_CONTAINERS 1
|
||||
#include <tbb/concurrent_map.h>
|
||||
#endif
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "impl.h"
|
||||
#include "polygon.h"
|
||||
@@ -39,6 +39,7 @@ using AddTriangle = std::function<void(int, glm::ivec3, glm::vec3, TriRef)>;
|
||||
*/
|
||||
void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
const Vec<TriRef>& halfedgeRef) {
|
||||
ZoneScoped;
|
||||
Vec<glm::ivec3> triVerts;
|
||||
Vec<glm::vec3> triNormal;
|
||||
Vec<TriRef>& triRef = meshRelation_.triRef;
|
||||
@@ -126,19 +127,21 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
auto generalTriangulation = [&](int face) {
|
||||
const glm::vec3 normal = faceNormal_[face];
|
||||
const glm::mat3x2 projection = GetAxisAlignedProjection(normal);
|
||||
const PolygonsIdx polys = Face2Polygons(face, projection, faceEdge);
|
||||
const PolygonsIdx polys =
|
||||
Face2Polygons(halfedge_.cbegin() + faceEdge[face],
|
||||
halfedge_.cbegin() + faceEdge[face + 1], projection);
|
||||
return TriangulateIdx(polys, precision_);
|
||||
};
|
||||
#if MANIFOLD_PAR == 'T' && __has_include(<tbb/tbb.h>)
|
||||
tbb::task_group group;
|
||||
// map from face to triangle
|
||||
tbb::concurrent_unordered_map<int, std::vector<glm::ivec3>> results;
|
||||
Vec<int> triCount(faceEdge.size());
|
||||
Vec<size_t> triCount(faceEdge.size());
|
||||
triCount.back() = 0;
|
||||
// precompute number of triangles per face, and launch async tasks to
|
||||
// triangulate complex faces
|
||||
for_each(autoPolicy(faceEdge.size()), countAt(0),
|
||||
countAt(faceEdge.size() - 1), [&](int face) {
|
||||
for_each(autoPolicy(faceEdge.size()), countAt(0_z),
|
||||
countAt(faceEdge.size() - 1), [&](size_t face) {
|
||||
triCount[face] = faceEdge[face + 1] - faceEdge[face] - 2;
|
||||
ASSERT(triCount[face] >= 1, topologyErr,
|
||||
"face has less than three edges.");
|
||||
@@ -152,13 +155,15 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
group.wait();
|
||||
// prefix sum computation (assign unique index to each face) and preallocation
|
||||
exclusive_scan(autoPolicy(triCount.size()), triCount.begin(), triCount.end(),
|
||||
triCount.begin(), 0);
|
||||
triCount.begin(), 0_z);
|
||||
if (triCount.back() >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("too many triangles");
|
||||
triVerts.resize(triCount.back());
|
||||
triNormal.resize(triCount.back());
|
||||
triRef.resize(triCount.back());
|
||||
|
||||
auto processFace2 = std::bind(
|
||||
processFace, [&](int face) { return std::move(results[face]); },
|
||||
processFace, [&](size_t face) { return std::move(results[face]); },
|
||||
[&](int face, glm::ivec3 tri, glm::vec3 normal, TriRef r) {
|
||||
triVerts[triCount[face]] = tri;
|
||||
triNormal[triCount[face]] = normal;
|
||||
@@ -167,7 +172,7 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
},
|
||||
std::placeholders::_1);
|
||||
// set triangles in parallel
|
||||
for_each(autoPolicy(faceEdge.size()), countAt(0),
|
||||
for_each(autoPolicy(faceEdge.size()), countAt(0_z),
|
||||
countAt(faceEdge.size() - 1), processFace2);
|
||||
#else
|
||||
triVerts.reserve(faceEdge.size());
|
||||
@@ -191,20 +196,16 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
}
|
||||
|
||||
/**
|
||||
* For the input face index, return a set of 2D polygons formed by the input
|
||||
* projection of the vertices.
|
||||
* Returns a set of 2D polygons formed by the input projection of the vertices
|
||||
* of the list of Halfedges, which must be an even-manifold, meaning each vert
|
||||
* must be referenced the same number of times as a startVert and endVert.
|
||||
*/
|
||||
PolygonsIdx Manifold::Impl::Face2Polygons(int face, glm::mat3x2 projection,
|
||||
const Vec<int>& faceEdge) const {
|
||||
const int firstEdge = faceEdge[face];
|
||||
const int lastEdge = faceEdge[face + 1];
|
||||
|
||||
std::map<int, int> vert_edge;
|
||||
for (int edge = firstEdge; edge < lastEdge; ++edge) {
|
||||
const bool inserted =
|
||||
vert_edge.emplace(std::make_pair(halfedge_[edge].startVert, edge))
|
||||
.second;
|
||||
ASSERT(inserted, topologyErr, "face has duplicate vertices.");
|
||||
PolygonsIdx Manifold::Impl::Face2Polygons(VecView<Halfedge>::IterC start,
|
||||
VecView<Halfedge>::IterC end,
|
||||
glm::mat3x2 projection) const {
|
||||
std::multimap<int, int> vert_edge;
|
||||
for (auto edge = start; edge != end; ++edge) {
|
||||
vert_edge.emplace(std::make_pair(edge->startVert, edge - start));
|
||||
}
|
||||
|
||||
PolygonsIdx polys;
|
||||
@@ -217,13 +218,104 @@ PolygonsIdx Manifold::Impl::Face2Polygons(int face, glm::mat3x2 projection,
|
||||
thisEdge = startEdge;
|
||||
polys.push_back({});
|
||||
}
|
||||
int vert = halfedge_[thisEdge].startVert;
|
||||
int vert = (start + thisEdge)->startVert;
|
||||
polys.back().push_back({projection * vertPos_[vert], vert});
|
||||
const auto result = vert_edge.find(halfedge_[thisEdge].endVert);
|
||||
const auto result = vert_edge.find((start + thisEdge)->endVert);
|
||||
ASSERT(result != vert_edge.end(), topologyErr, "non-manifold edge");
|
||||
thisEdge = result->second;
|
||||
vert_edge.erase(result);
|
||||
}
|
||||
return polys;
|
||||
}
|
||||
|
||||
CrossSection Manifold::Impl::Slice(float height) const {
|
||||
Box plane = bBox_;
|
||||
plane.min.z = plane.max.z = height;
|
||||
Vec<Box> query;
|
||||
query.push_back(plane);
|
||||
const SparseIndices collisions =
|
||||
collider_.Collisions<false, false>(query.cview());
|
||||
|
||||
std::unordered_set<int> tris;
|
||||
for (int i = 0; i < collisions.size(); ++i) {
|
||||
const int tri = collisions.Get(i, 1);
|
||||
float min = std::numeric_limits<float>::infinity();
|
||||
float max = -std::numeric_limits<float>::infinity();
|
||||
for (const int j : {0, 1, 2}) {
|
||||
const float z = vertPos_[halfedge_[3 * tri + j].startVert].z;
|
||||
min = glm::min(min, z);
|
||||
max = glm::max(max, z);
|
||||
}
|
||||
|
||||
if (min <= height && max > height) {
|
||||
tris.insert(tri);
|
||||
}
|
||||
}
|
||||
|
||||
Polygons polys;
|
||||
while (!tris.empty()) {
|
||||
const int startTri = *tris.begin();
|
||||
SimplePolygon poly;
|
||||
|
||||
int k = 0;
|
||||
for (const int j : {0, 1, 2}) {
|
||||
if (vertPos_[halfedge_[3 * startTri + j].startVert].z > height &&
|
||||
vertPos_[halfedge_[3 * startTri + Next3(j)].startVert].z <= height) {
|
||||
k = Next3(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int tri = startTri;
|
||||
do {
|
||||
tris.erase(tris.find(tri));
|
||||
if (vertPos_[halfedge_[3 * tri + k].endVert].z <= height) {
|
||||
k = Next3(k);
|
||||
}
|
||||
|
||||
Halfedge up = halfedge_[3 * tri + k];
|
||||
const glm::vec3 below = vertPos_[up.startVert];
|
||||
const glm::vec3 above = vertPos_[up.endVert];
|
||||
const float a = (height - below.z) / (above.z - below.z);
|
||||
poly.push_back(glm::vec2(glm::mix(below, above, a)));
|
||||
|
||||
const int pair = up.pairedHalfedge;
|
||||
tri = pair / 3;
|
||||
k = Next3(pair % 3);
|
||||
} while (tri != startTri);
|
||||
|
||||
polys.push_back(poly);
|
||||
}
|
||||
|
||||
return CrossSection(polys);
|
||||
}
|
||||
|
||||
CrossSection Manifold::Impl::Project() const {
|
||||
const glm::mat3x2 projection = GetAxisAlignedProjection({0, 0, 1});
|
||||
auto policy = autoPolicy(halfedge_.size());
|
||||
|
||||
Vec<Halfedge> cusps(NumEdge());
|
||||
cusps.resize(copy_if<decltype(cusps.begin())>(
|
||||
policy, halfedge_.cbegin(), halfedge_.cend(), cusps.begin(),
|
||||
[&](Halfedge edge) {
|
||||
return faceNormal_[edge.face].z >= 0 &&
|
||||
faceNormal_[halfedge_[edge.pairedHalfedge].face].z <
|
||||
0;
|
||||
}) -
|
||||
cusps.begin());
|
||||
|
||||
PolygonsIdx polysIndexed =
|
||||
Face2Polygons(cusps.cbegin(), cusps.cend(), projection);
|
||||
|
||||
Polygons polys;
|
||||
for (const auto& poly : polysIndexed) {
|
||||
SimplePolygon simple;
|
||||
for (const PolyVert& polyVert : poly) {
|
||||
simple.push_back(polyVert.pos);
|
||||
}
|
||||
polys.push_back(simple);
|
||||
}
|
||||
|
||||
return CrossSection(polys).Simplify(precision_);
|
||||
}
|
||||
} // namespace manifold
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "hashtable.h"
|
||||
#include "mesh_fixes.h"
|
||||
#include "par.h"
|
||||
#include "svd.h"
|
||||
|
||||
namespace {
|
||||
using namespace manifold;
|
||||
@@ -320,6 +321,7 @@ int GetLabels(std::vector<int>& components,
|
||||
|
||||
void DedupePropVerts(manifold::Vec<glm::ivec3>& triProp,
|
||||
const Vec<thrust::pair<int, int>>& vert2vert) {
|
||||
ZoneScoped;
|
||||
std::vector<int> vertLabels;
|
||||
const int numLabels = GetLabels(vertLabels, vert2vert, vert2vert.size());
|
||||
|
||||
@@ -349,6 +351,11 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
const int numVert = meshGL.NumVert();
|
||||
const int numTri = meshGL.NumTri();
|
||||
|
||||
if (meshGL.numProp > 3 &&
|
||||
static_cast<size_t>(numVert) * static_cast<size_t>(meshGL.numProp - 3) >=
|
||||
std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
|
||||
if (meshGL.numProp < 3) {
|
||||
MarkFailure(Error::MissingPositionProperties);
|
||||
return;
|
||||
@@ -379,7 +386,7 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
|
||||
std::vector<int> prop2vert(numVert);
|
||||
std::iota(prop2vert.begin(), prop2vert.end(), 0);
|
||||
for (int i = 0; i < meshGL.mergeFromVert.size(); ++i) {
|
||||
for (size_t i = 0; i < meshGL.mergeFromVert.size(); ++i) {
|
||||
const int from = meshGL.mergeFromVert[i];
|
||||
const int to = meshGL.mergeToVert[i];
|
||||
if (from >= numVert || to >= numVert) {
|
||||
@@ -388,8 +395,8 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
}
|
||||
prop2vert[from] = to;
|
||||
}
|
||||
for (int i = 0; i < numTri; ++i) {
|
||||
for (const int j : {0, 1, 2}) {
|
||||
for (size_t i = 0; i < numTri; ++i) {
|
||||
for (const size_t j : {0, 1, 2}) {
|
||||
const int vert = meshGL.triVerts[3 * i + j];
|
||||
if (vert < 0 || vert >= numVert) {
|
||||
MarkFailure(Error::VertexOutOfBounds);
|
||||
@@ -403,8 +410,8 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
|
||||
if (meshGL.numProp > 3) {
|
||||
relation.triProperties.resize(numTri);
|
||||
for (int i = 0; i < numTri; ++i) {
|
||||
for (const int j : {0, 1, 2}) {
|
||||
for (size_t i = 0; i < numTri; ++i) {
|
||||
for (const size_t j : {0, 1, 2}) {
|
||||
relation.triProperties[i][j] = meshGL.triVerts[3 * i + j];
|
||||
}
|
||||
}
|
||||
@@ -478,11 +485,13 @@ Manifold::Impl::Impl(const Mesh& mesh, const MeshRelationD& relation,
|
||||
const std::vector<float>& propertyTolerance,
|
||||
bool hasFaceIDs)
|
||||
: vertPos_(mesh.vertPos), halfedgeTangent_(mesh.halfedgeTangent) {
|
||||
if (mesh.triVerts.size() >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
meshRelation_ = {relation.originalID, relation.numProp, relation.properties,
|
||||
relation.meshIDtransform};
|
||||
|
||||
Vec<glm::ivec3> triVerts;
|
||||
for (int i = 0; i < mesh.triVerts.size(); ++i) {
|
||||
for (size_t i = 0; i < mesh.triVerts.size(); ++i) {
|
||||
const glm::ivec3 tri = mesh.triVerts[i];
|
||||
// Remove topological degenerates
|
||||
if (tri[0] != tri[1] && tri[1] != tri[2] && tri[2] != tri[0]) {
|
||||
@@ -532,7 +541,7 @@ Manifold::Impl::Impl(const Mesh& mesh, const MeshRelationD& relation,
|
||||
* Create either a unit tetrahedron, cube or octahedron. The cube is in the
|
||||
* first octant, while the others are symmetric about the origin.
|
||||
*/
|
||||
Manifold::Impl::Impl(Shape shape) {
|
||||
Manifold::Impl::Impl(Shape shape, const glm::mat4x3 m) {
|
||||
std::vector<glm::vec3> vertPos;
|
||||
std::vector<glm::ivec3> triVerts;
|
||||
switch (shape) {
|
||||
@@ -545,19 +554,19 @@ Manifold::Impl::Impl(Shape shape) {
|
||||
break;
|
||||
case Shape::Cube:
|
||||
vertPos = {{0.0f, 0.0f, 0.0f}, //
|
||||
{1.0f, 0.0f, 0.0f}, //
|
||||
{1.0f, 1.0f, 0.0f}, //
|
||||
{0.0f, 1.0f, 0.0f}, //
|
||||
{0.0f, 0.0f, 1.0f}, //
|
||||
{0.0f, 1.0f, 0.0f}, //
|
||||
{0.0f, 1.0f, 1.0f}, //
|
||||
{1.0f, 0.0f, 0.0f}, //
|
||||
{1.0f, 0.0f, 1.0f}, //
|
||||
{1.0f, 1.0f, 1.0f}, //
|
||||
{0.0f, 1.0f, 1.0f}};
|
||||
triVerts = {{0, 2, 1}, {0, 3, 2}, //
|
||||
{4, 5, 6}, {4, 6, 7}, //
|
||||
{0, 1, 5}, {0, 5, 4}, //
|
||||
{1, 2, 6}, {1, 6, 5}, //
|
||||
{2, 3, 7}, {2, 7, 6}, //
|
||||
{3, 0, 4}, {3, 4, 7}};
|
||||
{1.0f, 1.0f, 0.0f}, //
|
||||
{1.0f, 1.0f, 1.0f}};
|
||||
triVerts = {{1, 0, 4}, {2, 4, 0}, //
|
||||
{1, 3, 0}, {3, 1, 5}, //
|
||||
{3, 2, 0}, {3, 7, 2}, //
|
||||
{5, 4, 6}, {5, 1, 4}, //
|
||||
{6, 4, 2}, {7, 6, 2}, //
|
||||
{7, 3, 5}, {7, 5, 6}};
|
||||
break;
|
||||
case Shape::Octahedron:
|
||||
vertPos = {{1.0f, 0.0f, 0.0f}, //
|
||||
@@ -573,6 +582,7 @@ Manifold::Impl::Impl(Shape shape) {
|
||||
break;
|
||||
}
|
||||
vertPos_ = vertPos;
|
||||
for (auto& v : vertPos_) v = m * glm::vec4(v, 1.0f);
|
||||
CreateHalfedges(triVerts);
|
||||
Finish();
|
||||
meshRelation_.originalID = ReserveIDs(1);
|
||||
@@ -581,6 +591,7 @@ Manifold::Impl::Impl(Shape shape) {
|
||||
}
|
||||
|
||||
void Manifold::Impl::RemoveUnreferencedVerts(Vec<glm::ivec3>& triVerts) {
|
||||
ZoneScoped;
|
||||
Vec<int> vertOld2New(NumVert() + 1, 0);
|
||||
auto policy = autoPolicy(NumVert());
|
||||
for_each(policy, triVerts.cbegin(), triVerts.cend(),
|
||||
@@ -613,6 +624,7 @@ void Manifold::Impl::InitializeOriginal() {
|
||||
}
|
||||
|
||||
void Manifold::Impl::CreateFaces(const std::vector<float>& propertyTolerance) {
|
||||
ZoneScoped;
|
||||
Vec<float> propertyToleranceD =
|
||||
propertyTolerance.empty() ? Vec<float>(meshRelation_.numProp, kTolerance)
|
||||
: propertyTolerance;
|
||||
@@ -661,6 +673,7 @@ void Manifold::Impl::CreateFaces(const std::vector<float>& propertyTolerance) {
|
||||
* Create the halfedge_ data structure from an input triVerts array like Mesh.
|
||||
*/
|
||||
void Manifold::Impl::CreateHalfedges(const Vec<glm::ivec3>& triVerts) {
|
||||
ZoneScoped;
|
||||
const int numTri = triVerts.size();
|
||||
const int numHalfedge = 3 * numTri;
|
||||
// drop the old value first to avoid copy
|
||||
@@ -714,7 +727,14 @@ void Manifold::Impl::MarkFailure(Error status) {
|
||||
}
|
||||
|
||||
void Manifold::Impl::Warp(std::function<void(glm::vec3&)> warpFunc) {
|
||||
thrust::for_each_n(thrust::host, vertPos_.begin(), NumVert(), warpFunc);
|
||||
WarpBatch([&warpFunc](VecView<glm::vec3> vecs) {
|
||||
thrust::for_each(thrust::host, vecs.begin(), vecs.end(), warpFunc);
|
||||
});
|
||||
}
|
||||
|
||||
void Manifold::Impl::WarpBatch(
|
||||
std::function<void(VecView<glm::vec3>)> warpFunc) {
|
||||
warpFunc(vertPos_.view());
|
||||
CalculateBBox();
|
||||
if (!IsFinite()) {
|
||||
MarkFailure(Error::NonFiniteVertex);
|
||||
@@ -725,11 +745,11 @@ void Manifold::Impl::Warp(std::function<void(glm::vec3&)> warpFunc) {
|
||||
CalculateNormals();
|
||||
SetPrecision();
|
||||
CreateFaces();
|
||||
SimplifyTopology();
|
||||
Finish();
|
||||
}
|
||||
|
||||
Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3& transform_) const {
|
||||
ZoneScoped;
|
||||
if (transform_ == glm::mat4x3(1.0f)) return *this;
|
||||
auto policy = autoPolicy(NumVert());
|
||||
Impl result;
|
||||
@@ -776,11 +796,8 @@ Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3& transform_) const {
|
||||
if (!result.collider_.Transform(transform_)) result.Update();
|
||||
|
||||
result.CalculateBBox();
|
||||
float scale = 0;
|
||||
for (int i : {0, 1, 2})
|
||||
scale =
|
||||
glm::max(scale, transform_[0][i] + transform_[1][i] + transform_[2][i]);
|
||||
result.precision_ *= scale;
|
||||
// Scale the precision by the norm of the 3x3 portion of the transform.
|
||||
result.precision_ *= SpectralNorm(glm::mat3(transform_));
|
||||
// Maximum of inherited precision loss and translational precision loss.
|
||||
result.SetPrecision(result.precision_);
|
||||
return result;
|
||||
@@ -807,6 +824,7 @@ void Manifold::Impl::SetPrecision(float minPrecision) {
|
||||
* recalculation.
|
||||
*/
|
||||
void Manifold::Impl::CalculateNormals() {
|
||||
ZoneScoped;
|
||||
vertNormal_.resize(NumVert());
|
||||
auto policy = autoPolicy(NumTri());
|
||||
fill(policy, vertNormal_.begin(), vertNormal_.end(), glm::vec3(0));
|
||||
@@ -849,6 +867,7 @@ void Manifold::Impl::IncrementMeshIDs() {
|
||||
*/
|
||||
SparseIndices Manifold::Impl::EdgeCollisions(const Impl& Q,
|
||||
bool inverted) const {
|
||||
ZoneScoped;
|
||||
Vec<TmpEdge> edges = CreateTmpEdges(Q.halfedge_);
|
||||
const int numEdge = edges.size();
|
||||
Vec<Box> QedgeBB(numEdge);
|
||||
@@ -863,10 +882,10 @@ SparseIndices Manifold::Impl::EdgeCollisions(const Impl& Q,
|
||||
q1p2 = collider_.Collisions<false, false>(QedgeBB.cview());
|
||||
|
||||
if (inverted)
|
||||
for_each(policy, countAt(0), countAt(q1p2.size()),
|
||||
for_each(policy, countAt(0_z), countAt(q1p2.size()),
|
||||
ReindexEdge<true>({edges, q1p2}));
|
||||
else
|
||||
for_each(policy, countAt(0), countAt(q1p2.size()),
|
||||
for_each(policy, countAt(0_z), countAt(q1p2.size()),
|
||||
ReindexEdge<false>({edges, q1p2}));
|
||||
return q1p2;
|
||||
}
|
||||
@@ -877,6 +896,7 @@ SparseIndices Manifold::Impl::EdgeCollisions(const Impl& Q,
|
||||
*/
|
||||
SparseIndices Manifold::Impl::VertexCollisionsZ(
|
||||
VecView<const glm::vec3> vertsIn, bool inverted) const {
|
||||
ZoneScoped;
|
||||
if (inverted)
|
||||
return collider_.Collisions<false, true>(vertsIn);
|
||||
else
|
||||
|
||||
@@ -59,7 +59,7 @@ struct Manifold::Impl {
|
||||
|
||||
Impl() {}
|
||||
enum class Shape { Tetrahedron, Cube, Octahedron };
|
||||
Impl(Shape);
|
||||
Impl(Shape, const glm::mat4x3 = glm::mat4x3(1));
|
||||
|
||||
Impl(const MeshGL&, std::vector<float> propertyTolerance = {});
|
||||
Impl(const Mesh&, const MeshRelationD& relation,
|
||||
@@ -76,6 +76,7 @@ struct Manifold::Impl {
|
||||
void Update();
|
||||
void MarkFailure(Error status);
|
||||
void Warp(std::function<void(glm::vec3&)> warpFunc);
|
||||
void WarpBatch(std::function<void(VecView<glm::vec3>)> warpFunc);
|
||||
Impl Transform(const glm::mat4x3& transform) const;
|
||||
SparseIndices EdgeCollisions(const Impl& B, bool inverted = false) const;
|
||||
SparseIndices VertexCollisionsZ(VecView<const glm::vec3> vertsIn,
|
||||
@@ -115,8 +116,11 @@ struct Manifold::Impl {
|
||||
|
||||
// face_op.cu
|
||||
void Face2Tri(const Vec<int>& faceEdge, const Vec<TriRef>& halfedgeRef);
|
||||
PolygonsIdx Face2Polygons(int face, glm::mat3x2 projection,
|
||||
const Vec<int>& faceEdge) const;
|
||||
PolygonsIdx Face2Polygons(VecView<Halfedge>::IterC start,
|
||||
VecView<Halfedge>::IterC end,
|
||||
glm::mat3x2 projection) const;
|
||||
CrossSection Slice(float height) const;
|
||||
CrossSection Project() const;
|
||||
|
||||
// edge_op.cu
|
||||
void SimplifyTopology();
|
||||
@@ -134,7 +138,7 @@ struct Manifold::Impl {
|
||||
|
||||
// smoothing.cu
|
||||
void CreateTangents(const std::vector<Smoothness>&);
|
||||
Vec<Barycentric> Subdivide(int n);
|
||||
void Refine(int n);
|
||||
Vec<Barycentric> Subdivide(std::function<int(glm::vec3)>);
|
||||
void Refine(std::function<int(glm::vec3)>);
|
||||
};
|
||||
} // namespace manifold
|
||||
|
||||
@@ -155,6 +155,7 @@ Manifold::Manifold(const Mesh& mesh) {
|
||||
* saving or other operations outside of the context of this library.
|
||||
*/
|
||||
Mesh Manifold::GetMesh() const {
|
||||
ZoneScoped;
|
||||
const Impl& impl = *GetCsgLeafNode().GetImpl();
|
||||
|
||||
Mesh result;
|
||||
@@ -189,6 +190,7 @@ Mesh Manifold::GetMesh() const {
|
||||
* MeshGLs must use the same channels for their normals.
|
||||
*/
|
||||
MeshGL Manifold::GetMeshGL(glm::ivec3 normalIdx) const {
|
||||
ZoneScoped;
|
||||
const Impl& impl = *GetCsgLeafNode().GetImpl();
|
||||
|
||||
const int numProp = NumProp();
|
||||
@@ -289,7 +291,7 @@ MeshGL Manifold::GetMeshGL(glm::ivec3 normalIdx) const {
|
||||
// Duplicate verts with different props
|
||||
std::vector<int> vert2idx(impl.NumVert(), -1);
|
||||
std::vector<std::vector<glm::ivec2>> vertPropPair(impl.NumVert());
|
||||
out.vertProperties.reserve(numVert * out.numProp);
|
||||
out.vertProperties.reserve(numVert * static_cast<size_t>(out.numProp));
|
||||
|
||||
for (int run = 0; run < out.runOriginalID.size(); ++run) {
|
||||
for (int tri = out.runIndex[run] / 3; tri < out.runIndex[run + 1] / 3;
|
||||
@@ -484,7 +486,7 @@ int Manifold::NumOverlaps(const Manifold& other) const {
|
||||
|
||||
overlaps = other.GetCsgLeafNode().GetImpl()->EdgeCollisions(
|
||||
*GetCsgLeafNode().GetImpl());
|
||||
return num_overlaps += overlaps.size();
|
||||
return num_overlaps + overlaps.size();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -567,6 +569,20 @@ Manifold Manifold::Warp(std::function<void(glm::vec3&)> warpFunc) const {
|
||||
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as Manifold::Warp but calls warpFunc with with
|
||||
* a VecView which is roughly equivalent to std::span
|
||||
* pointing to all vec3 elements to be modified in-place
|
||||
*
|
||||
* @param warpFunc A function that modifies multiple vertex positions.
|
||||
*/
|
||||
Manifold Manifold::WarpBatch(
|
||||
std::function<void(VecView<glm::vec3>)> warpFunc) const {
|
||||
auto pImpl = std::make_shared<Impl>(*GetCsgLeafNode().GetImpl());
|
||||
pImpl->WarpBatch(warpFunc);
|
||||
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new copy of this manifold with updated vertex properties by
|
||||
* supplying a function that takes the existing position and properties as
|
||||
@@ -613,7 +629,6 @@ Manifold Manifold::SetProperties(
|
||||
|
||||
pImpl->meshRelation_.numProp = numProp;
|
||||
pImpl->CreateFaces();
|
||||
pImpl->SimplifyTopology();
|
||||
pImpl->Finish();
|
||||
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
|
||||
}
|
||||
@@ -652,7 +667,26 @@ Manifold Manifold::CalculateCurvature(int gaussianIdx, int meanIdx) const {
|
||||
*/
|
||||
Manifold Manifold::Refine(int n) const {
|
||||
auto pImpl = std::make_shared<Impl>(*GetCsgLeafNode().GetImpl());
|
||||
pImpl->Refine(n);
|
||||
if (n > 1) {
|
||||
pImpl->Refine([n](glm::vec3 edge) { return n - 1; });
|
||||
}
|
||||
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the density of the mesh by splitting each edge into pieces of
|
||||
* roughly the input length. Interior verts are added to keep the rest of the
|
||||
* triangulation edges also of roughly the same length. If halfedgeTangents are
|
||||
* present (e.g. from the Smooth() constructor), the new vertices will be moved
|
||||
* to the interpolated surface according to their barycentric coordinates.
|
||||
*
|
||||
* @param length The length that edges will be broken down to.
|
||||
*/
|
||||
Manifold Manifold::RefineToLength(float length) const {
|
||||
length = glm::abs(length);
|
||||
auto pImpl = std::make_shared<Impl>(*GetCsgLeafNode().GetImpl());
|
||||
pImpl->Refine(
|
||||
[length](glm::vec3 edge) { return glm::length(edge) / length; });
|
||||
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
|
||||
}
|
||||
|
||||
@@ -674,6 +708,10 @@ Manifold Manifold::Boolean(const Manifold& second, OpType op) const {
|
||||
return Manifold(pNode_->Boolean(second.pNode_, op));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given boolean operation on a list of Manifolds. In case of
|
||||
* Subtract, all Manifolds in the tail are differenced from the head.
|
||||
*/
|
||||
Manifold Manifold::BatchBoolean(const std::vector<Manifold>& manifolds,
|
||||
OpType op) {
|
||||
if (manifolds.size() == 0)
|
||||
@@ -777,6 +815,24 @@ Manifold Manifold::TrimByPlane(glm::vec3 normal, float originOffset) const {
|
||||
return *this ^ Halfspace(BoundingBox(), normal, originOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cross section of this object parallel to the X-Y plane at the
|
||||
* specified Z height, defaulting to zero. Using a height equal to the bottom of
|
||||
* the bounding box will return the bottom faces, while using a height equal to
|
||||
* the top of the bounding box will return empty.
|
||||
*/
|
||||
CrossSection Manifold::Slice(float height) const {
|
||||
return GetCsgLeafNode().GetImpl()->Slice(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cross section representing the projected outline of this object
|
||||
* onto the X-Y plane.
|
||||
*/
|
||||
CrossSection Manifold::Project() const {
|
||||
return GetCsgLeafNode().GetImpl()->Project();
|
||||
}
|
||||
|
||||
ExecutionParams& ManifoldParams() { return manifoldParams; }
|
||||
|
||||
/**
|
||||
@@ -787,6 +843,7 @@ ExecutionParams& ManifoldParams() { return manifoldParams; }
|
||||
* hull.
|
||||
*/
|
||||
Manifold Manifold::Hull(const std::vector<glm::vec3>& pts) {
|
||||
ZoneScoped;
|
||||
const int numVert = pts.size();
|
||||
if (numVert < 4) return Manifold();
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ struct CheckHalfedges {
|
||||
VecView<const Halfedge> halfedges;
|
||||
VecView<const glm::vec3> vertPos;
|
||||
|
||||
bool operator()(int edge) {
|
||||
bool operator()(size_t edge) {
|
||||
const Halfedge halfedge = halfedges[edge];
|
||||
if (halfedge.startVert == -1 || halfedge.endVert == -1) return true;
|
||||
if (halfedge.pairedHalfedge == -1) return false;
|
||||
@@ -275,7 +275,7 @@ bool Manifold::Impl::IsManifold() const {
|
||||
if (halfedge_.size() == 0) return true;
|
||||
auto policy = autoPolicy(halfedge_.size());
|
||||
|
||||
return all_of(policy, countAt(0), countAt(halfedge_.size()),
|
||||
return all_of(policy, countAt(0_z), countAt(halfedge_.size()),
|
||||
CheckHalfedges({halfedge_, vertPos_}));
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ int Manifold::Impl::NumDegenerateTris() const {
|
||||
}
|
||||
|
||||
Properties Manifold::Impl::GetProperties() const {
|
||||
ZoneScoped;
|
||||
if (IsEmpty()) return {0, 0};
|
||||
// Kahan summation
|
||||
float area = 0;
|
||||
@@ -339,6 +340,7 @@ Properties Manifold::Impl::GetProperties() const {
|
||||
}
|
||||
|
||||
void Manifold::Impl::CalculateCurvature(int gaussianIdx, int meanIdx) {
|
||||
ZoneScoped;
|
||||
if (IsEmpty()) return;
|
||||
if (gaussianIdx < 0 && meanIdx < 0) return;
|
||||
Vec<float> vertMeanCurvature(NumVert(), 0);
|
||||
@@ -370,7 +372,6 @@ void Manifold::Impl::CalculateCurvature(int gaussianIdx, int meanIdx) {
|
||||
numProp, gaussianIdx, meanIdx}));
|
||||
|
||||
CreateFaces();
|
||||
SimplifyTopology();
|
||||
Finish();
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ struct ReindexEdge {
|
||||
VecView<const TmpEdge> edges;
|
||||
SparseIndices& indices;
|
||||
|
||||
void operator()(int i) {
|
||||
void operator()(size_t i) {
|
||||
int& edge = indices.Get(i, inverted);
|
||||
edge = edges[edge].halfedgeIdx;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
#include "impl.h"
|
||||
#include "par.h"
|
||||
|
||||
template <>
|
||||
struct std::hash<glm::ivec3> {
|
||||
size_t operator()(const glm::ivec3& p) const {
|
||||
return std::hash<int>()(p.x) ^ std::hash<int>()(p.y) ^
|
||||
std::hash<int>()(p.z);
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
using namespace manifold;
|
||||
|
||||
@@ -25,14 +33,6 @@ glm::vec3 OrthogonalTo(glm::vec3 in, glm::vec3 ref) {
|
||||
return in;
|
||||
}
|
||||
|
||||
/**
|
||||
* The total number of verts if a triangle is subdivided naturally such that
|
||||
* each edge has edgeVerts verts along it (edgeVerts >= -1).
|
||||
*/
|
||||
int VertsPerTri(int edgeVerts) {
|
||||
return (edgeVerts * edgeVerts + edgeVerts) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retained verts are part of several triangles, and it doesn't matter which one
|
||||
* the vertBary refers to. Here, whichever is last will win and it's done on the
|
||||
@@ -53,131 +53,14 @@ void FillRetainedVerts(Vec<Barycentric>& vertBary,
|
||||
|
||||
struct ReindexHalfedge {
|
||||
VecView<int> half2Edge;
|
||||
const VecView<Halfedge> halfedges;
|
||||
|
||||
void operator()(thrust::tuple<int, TmpEdge> in) {
|
||||
const int edge = thrust::get<0>(in);
|
||||
const int halfedge = thrust::get<1>(in).halfedgeIdx;
|
||||
|
||||
half2Edge[halfedge] = edge;
|
||||
}
|
||||
};
|
||||
|
||||
struct EdgeVerts {
|
||||
VecView<glm::vec3> vertPos;
|
||||
VecView<Barycentric> vertBary;
|
||||
const int startIdx;
|
||||
const int n;
|
||||
|
||||
void operator()(thrust::tuple<int, TmpEdge> in) {
|
||||
int edge = thrust::get<0>(in);
|
||||
TmpEdge edgeVerts = thrust::get<1>(in);
|
||||
|
||||
float invTotal = 1.0f / n;
|
||||
for (int i = 1; i < n; ++i) {
|
||||
const int vert = startIdx + (n - 1) * edge + i - 1;
|
||||
const float v = i * invTotal;
|
||||
const float u = 1 - v;
|
||||
vertPos[vert] =
|
||||
u * vertPos[edgeVerts.first] + v * vertPos[edgeVerts.second];
|
||||
|
||||
const int tri = edgeVerts.halfedgeIdx / 3;
|
||||
const int idx = edgeVerts.halfedgeIdx - 3 * tri;
|
||||
glm::vec3 uvw(0);
|
||||
uvw[idx] = u;
|
||||
uvw[Next3(idx)] = v;
|
||||
vertBary[vert] = {tri, uvw};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct InteriorVerts {
|
||||
VecView<glm::vec3> vertPos;
|
||||
VecView<Barycentric> vertBary;
|
||||
const int startIdx;
|
||||
const int n;
|
||||
VecView<const Halfedge> halfedge;
|
||||
|
||||
void operator()(int tri) {
|
||||
const float invTotal = 1.0f / n;
|
||||
int pos = startIdx + tri * VertsPerTri(n - 2);
|
||||
for (int i = 0; i <= n; ++i) {
|
||||
for (int j = 0; j <= n - i; ++j) {
|
||||
const int k = n - i - j;
|
||||
const float u = invTotal * j;
|
||||
const float v = invTotal * k;
|
||||
const float w = invTotal * i;
|
||||
if (i == 0 || j == 0 || k == 0 || j == n - i) continue;
|
||||
|
||||
vertPos[pos] = u * vertPos[halfedge[3 * tri].startVert] + //
|
||||
v * vertPos[halfedge[3 * tri + 1].startVert] + //
|
||||
w * vertPos[halfedge[3 * tri + 2].startVert];
|
||||
|
||||
vertBary[pos++] = {tri, {u, v, w}};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct SplitTris {
|
||||
VecView<glm::ivec3> triVerts;
|
||||
VecView<Halfedge> halfedge;
|
||||
VecView<const int> half2Edge;
|
||||
const int edgeIdx;
|
||||
const int triIdx;
|
||||
const int n;
|
||||
|
||||
int EdgeVert(int i, int inHalfedge) const {
|
||||
bool forward = halfedge[inHalfedge].IsForward();
|
||||
int edge = forward ? half2Edge[inHalfedge]
|
||||
: half2Edge[halfedge[inHalfedge].pairedHalfedge];
|
||||
return edgeIdx + (n - 1) * edge + (forward ? i - 1 : n - 1 - i);
|
||||
}
|
||||
|
||||
int TriVert(int i, int j, int tri) const {
|
||||
--i;
|
||||
--j;
|
||||
int m = n - 2;
|
||||
int vertsPerTri = (m * m + m) / 2;
|
||||
int vertOffset = (i * (2 * m - i + 1)) / 2 + j;
|
||||
return triIdx + vertsPerTri * tri + vertOffset;
|
||||
}
|
||||
|
||||
int Vert(int i, int j, int tri) const {
|
||||
bool edge0 = i == 0;
|
||||
bool edge1 = j == 0;
|
||||
bool edge2 = j == n - i;
|
||||
if (edge0) {
|
||||
if (edge1)
|
||||
return halfedge[3 * tri + 1].startVert;
|
||||
else if (edge2)
|
||||
return halfedge[3 * tri].startVert;
|
||||
else
|
||||
return EdgeVert(n - j, 3 * tri);
|
||||
} else if (edge1) {
|
||||
if (edge2)
|
||||
return halfedge[3 * tri + 2].startVert;
|
||||
else
|
||||
return EdgeVert(i, 3 * tri + 1);
|
||||
} else if (edge2)
|
||||
return EdgeVert(j, 3 * tri + 2);
|
||||
else
|
||||
return TriVert(i, j, tri);
|
||||
}
|
||||
|
||||
void operator()(int tri) {
|
||||
int pos = n * n * tri;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
for (int j = 0; j < n - i; ++j) {
|
||||
int a = Vert(i, j, tri);
|
||||
int b = Vert(i + 1, j, tri);
|
||||
int c = Vert(i, j + 1, tri);
|
||||
triVerts[pos++] = glm::ivec3(c, a, b);
|
||||
if (j < n - 1 - i) {
|
||||
int d = Vert(i + 1, j + 1, tri);
|
||||
triVerts[pos++] = glm::ivec3(b, d, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
half2Edge[halfedges[halfedge].pairedHalfedge] = edge;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -196,10 +79,13 @@ struct SmoothBezier {
|
||||
const glm::vec3 edgeNormal =
|
||||
(triNormal[edge.face] + triNormal[halfedge[edge.pairedHalfedge].face]) /
|
||||
2.0f;
|
||||
glm::vec3 dir = glm::normalize(glm::cross(glm::cross(edgeNormal, edgeVec),
|
||||
vertNormal[edge.startVert]));
|
||||
glm::vec3 dir = SafeNormalize(glm::cross(glm::cross(edgeNormal, edgeVec),
|
||||
vertNormal[edge.startVert]));
|
||||
|
||||
const float weight = glm::abs(glm::dot(dir, glm::normalize(edgeVec)));
|
||||
float weight = glm::abs(glm::dot(dir, SafeNormalize(edgeVec)));
|
||||
if (weight == 0) {
|
||||
weight = 1;
|
||||
}
|
||||
// Quadratic weighted bezier for circular interpolation
|
||||
const glm::vec4 bz2 =
|
||||
weight *
|
||||
@@ -305,6 +191,312 @@ struct InterpTri {
|
||||
pos = HNormalize(posH);
|
||||
}
|
||||
};
|
||||
|
||||
class Partition {
|
||||
public:
|
||||
// The cached partitions don't have idx - it's added to the copy returned
|
||||
// from GetPartition that contains the mapping of the input divisions into the
|
||||
// sorted divisions that are uniquely cached.
|
||||
glm::ivec3 idx;
|
||||
glm::ivec3 sortedDivisions;
|
||||
Vec<glm::vec3> vertBary;
|
||||
Vec<glm::ivec3> triVert;
|
||||
|
||||
int InteriorOffset() const {
|
||||
return sortedDivisions[0] + sortedDivisions[1] + sortedDivisions[2];
|
||||
}
|
||||
|
||||
int NumInterior() const { return vertBary.size() - InteriorOffset(); }
|
||||
|
||||
static Partition GetPartition(glm::ivec3 divisions) {
|
||||
glm::ivec3 sortedDiv = divisions;
|
||||
glm::ivec3 triIdx = {0, 1, 2};
|
||||
if (sortedDiv[2] > sortedDiv[1]) {
|
||||
std::swap(sortedDiv[2], sortedDiv[1]);
|
||||
std::swap(triIdx[2], triIdx[1]);
|
||||
}
|
||||
if (sortedDiv[1] > sortedDiv[0]) {
|
||||
std::swap(sortedDiv[1], sortedDiv[0]);
|
||||
std::swap(triIdx[1], triIdx[0]);
|
||||
if (sortedDiv[2] > sortedDiv[1]) {
|
||||
std::swap(sortedDiv[2], sortedDiv[1]);
|
||||
std::swap(triIdx[2], triIdx[1]);
|
||||
}
|
||||
}
|
||||
|
||||
Partition partition = GetCachedPartition(sortedDiv);
|
||||
partition.idx = triIdx;
|
||||
|
||||
return partition;
|
||||
}
|
||||
|
||||
Vec<glm::ivec3> Reindex(glm::ivec3 tri, glm::ivec3 edgeOffsets,
|
||||
glm::bvec3 edgeFwd, int interiorOffset) const {
|
||||
Vec<int> newVerts;
|
||||
newVerts.reserve(vertBary.size());
|
||||
glm::ivec3 triIdx = idx;
|
||||
glm::ivec3 outTri = {0, 1, 2};
|
||||
if (idx[1] != Next3(idx[0])) {
|
||||
triIdx = {idx[2], idx[0], idx[1]};
|
||||
edgeFwd = glm::not_(edgeFwd);
|
||||
std::swap(outTri[0], outTri[1]);
|
||||
}
|
||||
for (const int i : {0, 1, 2}) {
|
||||
newVerts.push_back(tri[triIdx[i]]);
|
||||
}
|
||||
for (const int i : {0, 1, 2}) {
|
||||
const int n = sortedDivisions[i] - 1;
|
||||
int offset = edgeOffsets[idx[i]] + (edgeFwd[idx[i]] ? 0 : n - 1);
|
||||
for (int j = 0; j < n; ++j) {
|
||||
newVerts.push_back(offset);
|
||||
offset += edgeFwd[idx[i]] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
const int offset = interiorOffset - newVerts.size();
|
||||
for (int i = newVerts.size(); i < vertBary.size(); ++i) {
|
||||
newVerts.push_back(i + offset);
|
||||
}
|
||||
|
||||
const int numTri = triVert.size();
|
||||
Vec<glm::ivec3> newTriVert(numTri);
|
||||
for_each_n(
|
||||
autoPolicy(numTri), zip(newTriVert.begin(), triVert.begin()), numTri,
|
||||
[&outTri, &newVerts](thrust::tuple<glm::ivec3&, glm::ivec3> inOut) {
|
||||
for (const int j : {0, 1, 2}) {
|
||||
thrust::get<0>(inOut)[outTri[j]] =
|
||||
newVerts[thrust::get<1>(inOut)[j]];
|
||||
}
|
||||
});
|
||||
return newTriVert;
|
||||
}
|
||||
|
||||
private:
|
||||
static inline auto cacheLock = std::mutex();
|
||||
static inline auto cache =
|
||||
std::unordered_map<glm::ivec3, std::unique_ptr<Partition>>();
|
||||
|
||||
// This triangulation is purely topological - it depends only on the number of
|
||||
// divisions of the three sides of the triangle. This allows them to be cached
|
||||
// and reused for similar triangles. The shape of the final surface is defined
|
||||
// by the tangents and the barycentric coordinates of the new verts. The input
|
||||
// must be sorted: n[0] >= n[1] >= n[2] > 0
|
||||
static Partition GetCachedPartition(glm::ivec3 n) {
|
||||
{
|
||||
auto lockGuard = std::lock_guard<std::mutex>(cacheLock);
|
||||
auto cached = cache.find(n);
|
||||
if (cached != cache.end()) {
|
||||
return *cached->second;
|
||||
}
|
||||
}
|
||||
Partition partition;
|
||||
partition.sortedDivisions = n;
|
||||
partition.vertBary.push_back({1, 0, 0});
|
||||
partition.vertBary.push_back({0, 1, 0});
|
||||
partition.vertBary.push_back({0, 0, 1});
|
||||
for (const int i : {0, 1, 2}) {
|
||||
const glm::vec3 nextBary = partition.vertBary[(i + 1) % 3];
|
||||
for (int j = 1; j < n[i]; ++j) {
|
||||
partition.vertBary.push_back(
|
||||
glm::mix(partition.vertBary[i], nextBary, (float)j / n[i]));
|
||||
}
|
||||
}
|
||||
const glm::ivec3 edgeOffsets = {3, 3 + n[0] - 1, 3 + n[0] - 1 + n[1] - 1};
|
||||
|
||||
const float f = n[2] * n[2] + n[0] * n[0];
|
||||
if (n[1] == 1) {
|
||||
if (n[0] == 1) {
|
||||
partition.triVert.push_back({0, 1, 2});
|
||||
} else {
|
||||
PartitionFan(partition.triVert, {0, 1, 2}, n[0] - 1, edgeOffsets[0]);
|
||||
}
|
||||
} else if (n[1] * n[1] > f - glm::sqrt(2.0f) * n[0] * n[2]) { // acute-ish
|
||||
partition.triVert.push_back({edgeOffsets[1] - 1, 1, edgeOffsets[1]});
|
||||
PartitionQuad(partition.triVert, partition.vertBary,
|
||||
{edgeOffsets[1] - 1, edgeOffsets[1], 2, 0},
|
||||
{-1, edgeOffsets[1] + 1, edgeOffsets[2], edgeOffsets[0]},
|
||||
{0, n[1] - 2, n[2] - 1, n[0] - 2},
|
||||
{true, true, true, true});
|
||||
} else { // obtuse -> spit into two acute
|
||||
// portion of n[0] under n[2]
|
||||
const int ns =
|
||||
glm::min(n[0] - 2, (int)glm::round((f - n[1] * n[1]) / (2 * n[0])));
|
||||
// height from n[0]: nh <= n[2]
|
||||
const int nh = glm::max(1., glm::round(glm::sqrt(n[2] * n[2] - ns * ns)));
|
||||
|
||||
const int hOffset = partition.vertBary.size();
|
||||
const glm::vec3 middleBary = partition.vertBary[edgeOffsets[0] + ns - 1];
|
||||
for (int j = 1; j < nh; ++j) {
|
||||
partition.vertBary.push_back(
|
||||
glm::mix(partition.vertBary[2], middleBary, (float)j / nh));
|
||||
}
|
||||
|
||||
partition.triVert.push_back({edgeOffsets[1] - 1, 1, edgeOffsets[1]});
|
||||
PartitionQuad(
|
||||
partition.triVert, partition.vertBary,
|
||||
{edgeOffsets[1] - 1, edgeOffsets[1], 2, edgeOffsets[0] + ns - 1},
|
||||
{-1, edgeOffsets[1] + 1, hOffset, edgeOffsets[0] + ns},
|
||||
{0, n[1] - 2, nh - 1, n[0] - ns - 2}, {true, true, true, true});
|
||||
|
||||
if (n[2] == 1) {
|
||||
PartitionFan(partition.triVert, {0, edgeOffsets[0] + ns - 1, 2}, ns - 1,
|
||||
edgeOffsets[0]);
|
||||
} else {
|
||||
if (ns == 1) {
|
||||
partition.triVert.push_back({hOffset, 2, edgeOffsets[2]});
|
||||
PartitionQuad(partition.triVert, partition.vertBary,
|
||||
{hOffset, edgeOffsets[2], 0, edgeOffsets[0]},
|
||||
{-1, edgeOffsets[2] + 1, -1, hOffset + nh - 2},
|
||||
{0, n[2] - 2, ns - 1, nh - 2},
|
||||
{true, true, true, false});
|
||||
} else {
|
||||
partition.triVert.push_back({hOffset - 1, 0, edgeOffsets[0]});
|
||||
PartitionQuad(
|
||||
partition.triVert, partition.vertBary,
|
||||
{hOffset - 1, edgeOffsets[0], edgeOffsets[0] + ns - 1, 2},
|
||||
{-1, edgeOffsets[0] + 1, hOffset + nh - 2, edgeOffsets[2]},
|
||||
{0, ns - 2, nh - 1, n[2] - 2}, {true, true, false, true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto lockGuard = std::lock_guard<std::mutex>(cacheLock);
|
||||
cache.insert({n, std::make_unique<Partition>(partition)});
|
||||
return partition;
|
||||
}
|
||||
|
||||
// Side 0 has added edges while sides 1 and 2 do not. Fan spreads from vert 2.
|
||||
static void PartitionFan(Vec<glm::ivec3>& triVert, glm::ivec3 cornerVerts,
|
||||
int added, int edgeOffset) {
|
||||
int last = cornerVerts[0];
|
||||
for (int i = 0; i < added; ++i) {
|
||||
const int next = edgeOffset + i;
|
||||
triVert.push_back({last, next, cornerVerts[2]});
|
||||
last = next;
|
||||
}
|
||||
triVert.push_back({last, cornerVerts[1], cornerVerts[2]});
|
||||
}
|
||||
|
||||
// Partitions are parallel to the first edge unless two consecutive edgeAdded
|
||||
// are zero, in which case a terminal triangulation is performed.
|
||||
static void PartitionQuad(Vec<glm::ivec3>& triVert, Vec<glm::vec3>& vertBary,
|
||||
glm::ivec4 cornerVerts, glm::ivec4 edgeOffsets,
|
||||
glm::ivec4 edgeAdded, glm::bvec4 edgeFwd) {
|
||||
auto GetEdgeVert = [&](int edge, int idx) {
|
||||
return edgeOffsets[edge] + (edgeFwd[edge] ? 1 : -1) * idx;
|
||||
};
|
||||
|
||||
ASSERT(glm::all(glm::greaterThanEqual(edgeAdded, glm::ivec4(0))), logicErr,
|
||||
"negative divisions!");
|
||||
|
||||
int corner = -1;
|
||||
int last = 3;
|
||||
int maxEdge = -1;
|
||||
for (const int i : {0, 1, 2, 3}) {
|
||||
if (corner == -1 && edgeAdded[i] == 0 && edgeAdded[last] == 0) {
|
||||
corner = i;
|
||||
}
|
||||
if (edgeAdded[i] > 0) {
|
||||
maxEdge = maxEdge == -1 ? i : -2;
|
||||
}
|
||||
last = i;
|
||||
}
|
||||
if (corner >= 0) { // terminate
|
||||
if (maxEdge >= 0) {
|
||||
glm::ivec4 edge = (glm::ivec4(0, 1, 2, 3) + maxEdge) % 4;
|
||||
const int middle = edgeAdded[maxEdge] / 2;
|
||||
triVert.push_back({cornerVerts[edge[2]], cornerVerts[edge[3]],
|
||||
GetEdgeVert(maxEdge, middle)});
|
||||
int last = cornerVerts[edge[0]];
|
||||
for (int i = 0; i <= middle; ++i) {
|
||||
const int next = GetEdgeVert(maxEdge, i);
|
||||
triVert.push_back({cornerVerts[edge[3]], last, next});
|
||||
last = next;
|
||||
}
|
||||
last = cornerVerts[edge[1]];
|
||||
for (int i = edgeAdded[maxEdge] - 1; i >= middle; --i) {
|
||||
const int next = GetEdgeVert(maxEdge, i);
|
||||
triVert.push_back({cornerVerts[edge[2]], next, last});
|
||||
last = next;
|
||||
}
|
||||
} else {
|
||||
int sideVert = cornerVerts[0]; // initial value is unused
|
||||
for (const int j : {1, 2}) {
|
||||
const int side = (corner + j) % 4;
|
||||
if (j == 2 && edgeAdded[side] > 0) {
|
||||
triVert.push_back(
|
||||
{cornerVerts[side], GetEdgeVert(side, 0), sideVert});
|
||||
} else {
|
||||
sideVert = cornerVerts[side];
|
||||
}
|
||||
for (int i = 0; i < edgeAdded[side]; ++i) {
|
||||
const int nextVert = GetEdgeVert(side, i);
|
||||
triVert.push_back({cornerVerts[corner], sideVert, nextVert});
|
||||
sideVert = nextVert;
|
||||
}
|
||||
if (j == 2 || edgeAdded[side] == 0) {
|
||||
triVert.push_back({cornerVerts[corner], sideVert,
|
||||
cornerVerts[(corner + j + 1) % 4]});
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// recursively partition
|
||||
const int partitions = 1 + glm::min(edgeAdded[1], edgeAdded[3]);
|
||||
glm::ivec4 newCornerVerts = {cornerVerts[1], -1, -1, cornerVerts[0]};
|
||||
glm::ivec4 newEdgeOffsets = {
|
||||
edgeOffsets[1], -1, GetEdgeVert(3, edgeAdded[3] + 1), edgeOffsets[0]};
|
||||
glm::ivec4 newEdgeAdded = {0, -1, 0, edgeAdded[0]};
|
||||
glm::bvec4 newEdgeFwd = {edgeFwd[1], true, edgeFwd[3], edgeFwd[0]};
|
||||
|
||||
for (int i = 1; i < partitions; ++i) {
|
||||
const int cornerOffset1 = (edgeAdded[1] * i) / partitions;
|
||||
const int cornerOffset3 =
|
||||
edgeAdded[3] - 1 - (edgeAdded[3] * i) / partitions;
|
||||
const int nextOffset1 = GetEdgeVert(1, cornerOffset1 + 1);
|
||||
const int nextOffset3 = GetEdgeVert(3, cornerOffset3 + 1);
|
||||
const int added = glm::round(glm::mix(
|
||||
(float)edgeAdded[0], (float)edgeAdded[2], (float)i / partitions));
|
||||
|
||||
newCornerVerts[1] = GetEdgeVert(1, cornerOffset1);
|
||||
newCornerVerts[2] = GetEdgeVert(3, cornerOffset3);
|
||||
newEdgeAdded[0] = std::abs(nextOffset1 - newEdgeOffsets[0]) - 1;
|
||||
newEdgeAdded[1] = added;
|
||||
newEdgeAdded[2] = std::abs(nextOffset3 - newEdgeOffsets[2]) - 1;
|
||||
newEdgeOffsets[1] = vertBary.size();
|
||||
newEdgeOffsets[2] = nextOffset3;
|
||||
|
||||
for (int j = 0; j < added; ++j) {
|
||||
vertBary.push_back(glm::mix(vertBary[newCornerVerts[1]],
|
||||
vertBary[newCornerVerts[2]],
|
||||
(j + 1.0f) / (added + 1.0f)));
|
||||
}
|
||||
|
||||
PartitionQuad(triVert, vertBary, newCornerVerts, newEdgeOffsets,
|
||||
newEdgeAdded, newEdgeFwd);
|
||||
|
||||
newCornerVerts[0] = newCornerVerts[1];
|
||||
newCornerVerts[3] = newCornerVerts[2];
|
||||
newEdgeAdded[3] = newEdgeAdded[1];
|
||||
newEdgeOffsets[0] = nextOffset1;
|
||||
newEdgeOffsets[3] = newEdgeOffsets[1] + newEdgeAdded[1] - 1;
|
||||
newEdgeFwd[3] = false;
|
||||
}
|
||||
|
||||
newCornerVerts[1] = cornerVerts[2];
|
||||
newCornerVerts[2] = cornerVerts[3];
|
||||
newEdgeOffsets[1] = edgeOffsets[2];
|
||||
newEdgeAdded[0] =
|
||||
edgeAdded[1] - std::abs(newEdgeOffsets[0] - edgeOffsets[1]);
|
||||
newEdgeAdded[1] = edgeAdded[2];
|
||||
newEdgeAdded[2] = std::abs(newEdgeOffsets[2] - edgeOffsets[3]) - 1;
|
||||
newEdgeOffsets[2] = edgeOffsets[3];
|
||||
newEdgeFwd[1] = edgeFwd[2];
|
||||
|
||||
PartitionQuad(triVert, vertBary, newCornerVerts, newEdgeOffsets,
|
||||
newEdgeAdded, newEdgeFwd);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
namespace manifold {
|
||||
@@ -320,6 +512,7 @@ namespace manifold {
|
||||
*/
|
||||
void Manifold::Impl::CreateTangents(
|
||||
const std::vector<Smoothness>& sharpenedEdges) {
|
||||
ZoneScoped;
|
||||
const int numHalfedge = halfedge_.size();
|
||||
halfedgeTangent_.resize(numHalfedge);
|
||||
|
||||
@@ -417,72 +610,258 @@ void Manifold::Impl::CreateTangents(
|
||||
}
|
||||
|
||||
/**
|
||||
* Split each edge into n pieces and sub-triangulate each triangle accordingly.
|
||||
* This function doesn't run Finish(), as that is expensive and it'll need to be
|
||||
* run after the new vertices have moved, which is a likely scenario after
|
||||
* refinement (smoothing).
|
||||
* Split each edge into n pieces as defined by calling the edgeDivisions
|
||||
* function, and sub-triangulate each triangle accordingly. This function
|
||||
* doesn't run Finish(), as that is expensive and it'll need to be run after the
|
||||
* new vertices have moved, which is a likely scenario after refinement
|
||||
* (smoothing).
|
||||
*/
|
||||
Vec<Barycentric> Manifold::Impl::Subdivide(int n) {
|
||||
if (n < 2) return Vec<Barycentric>();
|
||||
faceNormal_.resize(0);
|
||||
vertNormal_.resize(0);
|
||||
const int numVert = NumVert();
|
||||
const int numEdge = NumEdge();
|
||||
const int numTri = NumTri();
|
||||
// Append new verts
|
||||
const int vertsPerEdge = n - 1;
|
||||
const int triVertStart = numVert + numEdge * vertsPerEdge;
|
||||
vertPos_.resize(triVertStart + numTri * VertsPerTri(n - 2));
|
||||
Vec<Barycentric> vertBary(vertPos_.size());
|
||||
FillRetainedVerts(vertBary, halfedge_);
|
||||
|
||||
MeshRelationD oldMeshRelation = std::move(meshRelation_);
|
||||
meshRelation_.triRef.resize(n * n * numTri);
|
||||
meshRelation_.originalID = oldMeshRelation.originalID;
|
||||
|
||||
Vec<Barycentric> Manifold::Impl::Subdivide(
|
||||
std::function<int(glm::vec3)> edgeDivisions) {
|
||||
Vec<TmpEdge> edges = CreateTmpEdges(halfedge_);
|
||||
const int numEdge = edges.size();
|
||||
Vec<int> half2Edge(2 * numEdge);
|
||||
auto policy = autoPolicy(numEdge);
|
||||
for_each_n(policy, zip(countAt(0), edges.begin()), numEdge,
|
||||
ReindexHalfedge({half2Edge}));
|
||||
for_each_n(policy, zip(countAt(0), edges.begin()), numEdge,
|
||||
EdgeVerts({vertPos_, vertBary, numVert, n}));
|
||||
ReindexHalfedge({half2Edge, halfedge_}));
|
||||
|
||||
Vec<int> edgeAdded(numEdge);
|
||||
for_each_n(policy, zip(edgeAdded.begin(), edges.cbegin()), numEdge,
|
||||
[edgeDivisions, this](thrust::tuple<int&, TmpEdge> inOut) {
|
||||
const TmpEdge edge = thrust::get<1>(inOut);
|
||||
const glm::vec3 vec =
|
||||
this->vertPos_[edge.first] - this->vertPos_[edge.second];
|
||||
thrust::get<0>(inOut) = edgeDivisions(vec);
|
||||
});
|
||||
|
||||
Vec<int> edgeOffset(numEdge);
|
||||
const int numVert = NumVert();
|
||||
exclusive_scan(policy, edgeAdded.begin(), edgeAdded.end(), edgeOffset.begin(),
|
||||
numVert);
|
||||
|
||||
Vec<Barycentric> vertBary(edgeOffset.back() + edgeAdded.back());
|
||||
const int totalEdgeAdded = vertBary.size() - numVert;
|
||||
FillRetainedVerts(vertBary, halfedge_);
|
||||
for_each_n(policy, zip(edges.begin(), edgeAdded.begin(), edgeOffset.begin()),
|
||||
numEdge, [&vertBary](thrust::tuple<TmpEdge, int, int> in) {
|
||||
const TmpEdge edge = thrust::get<0>(in);
|
||||
const int n = thrust::get<1>(in);
|
||||
const int offset = thrust::get<2>(in);
|
||||
const float frac = 1.0f / (n + 1);
|
||||
const int v0 = edge.halfedgeIdx % 3;
|
||||
const int v1 = Next3(v0);
|
||||
const int tri = edge.halfedgeIdx / 3;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
glm::vec3 uvw(0);
|
||||
uvw[v1] = (i + 1) * frac;
|
||||
uvw[v0] = 1 - uvw[v1];
|
||||
vertBary[offset + i].uvw = uvw;
|
||||
vertBary[offset + i].tri = tri;
|
||||
}
|
||||
});
|
||||
|
||||
const int numTri = NumTri();
|
||||
std::vector<Partition> subTris(numTri);
|
||||
for_each_n(policy, countAt(0), numTri,
|
||||
InteriorVerts({vertPos_, vertBary, triVertStart, n, halfedge_}));
|
||||
// Create sub-triangles
|
||||
Vec<glm::ivec3> triVerts(n * n * numTri);
|
||||
[&subTris, &half2Edge, &edgeAdded](int tri) {
|
||||
glm::ivec3 divisions;
|
||||
for (const int i : {0, 1, 2}) {
|
||||
divisions[i] = edgeAdded[half2Edge[3 * tri + i]] + 1;
|
||||
}
|
||||
subTris[tri] = Partition::GetPartition(divisions);
|
||||
});
|
||||
|
||||
Vec<int> triOffset(numTri);
|
||||
auto numSubTris = thrust::make_transform_iterator(
|
||||
subTris.begin(),
|
||||
[](const Partition& part) { return part.triVert.size(); });
|
||||
exclusive_scan(policy, numSubTris, numSubTris + numTri, triOffset.begin(), 0);
|
||||
|
||||
Vec<int> interiorOffset(numTri);
|
||||
auto numInterior = thrust::make_transform_iterator(
|
||||
subTris.begin(),
|
||||
[](const Partition& part) { return part.NumInterior(); });
|
||||
exclusive_scan(policy, numInterior, numInterior + numTri,
|
||||
interiorOffset.begin(), vertBary.size());
|
||||
|
||||
Vec<glm::ivec3> triVerts(triOffset.back() + subTris.back().triVert.size());
|
||||
vertBary.resize(interiorOffset.back() + subTris.back().NumInterior());
|
||||
Vec<TriRef> triRef(triVerts.size());
|
||||
for_each_n(
|
||||
policy, countAt(0), numTri,
|
||||
SplitTris({triVerts, halfedge_, half2Edge, numVert, triVertStart, n}));
|
||||
CreateHalfedges(triVerts);
|
||||
// Make original since the subdivided faces are intended to be warped into
|
||||
// being non-coplanar, and hence not being related to the original faces.
|
||||
meshRelation_.originalID = ReserveIDs(1);
|
||||
InitializeOriginal();
|
||||
[this, &triVerts, &triRef, &vertBary, &subTris, &edgeOffset, &half2Edge,
|
||||
&triOffset, &interiorOffset](int tri) {
|
||||
glm::ivec3 tri3;
|
||||
glm::ivec3 edgeOffsets;
|
||||
glm::bvec3 edgeFwd;
|
||||
for (const int i : {0, 1, 2}) {
|
||||
const Halfedge& halfedge = this->halfedge_[3 * tri + i];
|
||||
tri3[i] = halfedge.startVert;
|
||||
edgeOffsets[i] = edgeOffset[half2Edge[3 * tri + i]];
|
||||
edgeFwd[i] = halfedge.IsForward();
|
||||
}
|
||||
|
||||
Vec<glm::ivec3> newTris = subTris[tri].Reindex(
|
||||
tri3, edgeOffsets, edgeFwd, interiorOffset[tri]);
|
||||
copy(ExecutionPolicy::Seq, newTris.begin(), newTris.end(),
|
||||
triVerts.begin() + triOffset[tri]);
|
||||
auto start = triRef.begin() + triOffset[tri];
|
||||
fill(ExecutionPolicy::Seq, start, start + newTris.size(),
|
||||
meshRelation_.triRef[tri]);
|
||||
|
||||
const glm::ivec3 idx = subTris[tri].idx;
|
||||
const glm::ivec3 vIdx =
|
||||
idx[1] == Next3(idx[0]) ? idx : glm::ivec3(idx[2], idx[0], idx[1]);
|
||||
glm::ivec3 rIdx;
|
||||
for (const int i : {0, 1, 2}) {
|
||||
rIdx[vIdx[i]] = i;
|
||||
}
|
||||
|
||||
const auto& subBary = subTris[tri].vertBary;
|
||||
transform(ExecutionPolicy::Seq,
|
||||
subBary.begin() + subTris[tri].InteriorOffset(),
|
||||
subBary.end(), vertBary.begin() + interiorOffset[tri],
|
||||
[tri, rIdx](glm::vec3 bary) {
|
||||
return Barycentric(
|
||||
{tri, {bary[rIdx[0]], bary[rIdx[1]], bary[rIdx[2]]}});
|
||||
});
|
||||
});
|
||||
meshRelation_.triRef = triRef;
|
||||
|
||||
Vec<glm::vec3> newVertPos(vertBary.size());
|
||||
for_each_n(
|
||||
policy, zip(newVertPos.begin(), vertBary.begin()), vertBary.size(),
|
||||
[this](thrust::tuple<glm::vec3&, Barycentric> inOut) {
|
||||
const Barycentric bary = thrust::get<1>(inOut);
|
||||
glm::mat3 triPos;
|
||||
for (const int i : {0, 1, 2}) {
|
||||
triPos[i] =
|
||||
this->vertPos_[this->halfedge_[3 * bary.tri + i].startVert];
|
||||
}
|
||||
thrust::get<0>(inOut) = triPos * bary.uvw;
|
||||
});
|
||||
vertPos_ = newVertPos;
|
||||
|
||||
faceNormal_.resize(0);
|
||||
|
||||
if (meshRelation_.numProp > 0) {
|
||||
meshRelation_.properties.resize(meshRelation_.numProp * numVert);
|
||||
meshRelation_.triProperties.resize(meshRelation_.triRef.size());
|
||||
// Fill properties according to barycentric.
|
||||
// Set triProp to share properties on continuous edges.
|
||||
// Duplicate properties will be removed during sorting.
|
||||
const int numPropVert = NumPropVert();
|
||||
const int addedVerts = NumVert() - numVert;
|
||||
const int propOffset = numPropVert - numVert;
|
||||
Vec<float> prop(meshRelation_.numProp *
|
||||
(numPropVert + addedVerts + totalEdgeAdded));
|
||||
|
||||
copy(policy, meshRelation_.properties.begin(),
|
||||
meshRelation_.properties.end(), prop.begin());
|
||||
|
||||
for_each_n(
|
||||
policy, zip(countAt(numPropVert), vertBary.begin() + numVert),
|
||||
addedVerts, [this, &prop](thrust::tuple<int, Barycentric> in) {
|
||||
const int vert = thrust::get<0>(in);
|
||||
const Barycentric bary = thrust::get<1>(in);
|
||||
auto& rel = this->meshRelation_;
|
||||
|
||||
for (int p = 0; p < rel.numProp; ++p) {
|
||||
glm::vec3 triProp;
|
||||
for (const int i : {0, 1, 2}) {
|
||||
triProp[i] =
|
||||
rel.properties[rel.triProperties[bary.tri][i] * rel.numProp +
|
||||
p];
|
||||
}
|
||||
prop[vert * rel.numProp + p] = glm::dot(triProp, bary.uvw);
|
||||
}
|
||||
});
|
||||
|
||||
for_each_n(
|
||||
policy, zip(edges.begin(), edgeAdded.begin(), edgeOffset.begin()),
|
||||
numEdge,
|
||||
[this, &prop, propOffset,
|
||||
addedVerts](thrust::tuple<TmpEdge, int, int> in) {
|
||||
const TmpEdge edge = thrust::get<0>(in);
|
||||
const int n = thrust::get<1>(in);
|
||||
const int offset = thrust::get<2>(in) + propOffset + addedVerts;
|
||||
auto& rel = this->meshRelation_;
|
||||
|
||||
const float frac = 1.0f / (n + 1);
|
||||
const int halfedgeIdx =
|
||||
this->halfedge_[edge.halfedgeIdx].pairedHalfedge;
|
||||
const int v0 = halfedgeIdx % 3;
|
||||
const int v1 = Next3(v0);
|
||||
const int tri = halfedgeIdx / 3;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
glm::vec3 uvw(0);
|
||||
uvw[v1] = (i + 1) * frac;
|
||||
uvw[v0] = 1 - uvw[v1];
|
||||
for (int p = 0; p < rel.numProp; ++p) {
|
||||
glm::vec3 triProp;
|
||||
for (const int j : {0, 1, 2}) {
|
||||
triProp[j] =
|
||||
rel.properties[rel.triProperties[tri][j] * rel.numProp + p];
|
||||
}
|
||||
prop[(offset + i) * rel.numProp + p] = glm::dot(triProp, uvw);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vec<glm::ivec3> triProp(triVerts.size());
|
||||
|
||||
for_each_n(policy, countAt(0), numTri,
|
||||
[this, &triProp, &subTris, &edgeOffset, &half2Edge, &triOffset,
|
||||
&interiorOffset, propOffset, addedVerts](int tri) {
|
||||
auto& rel = this->meshRelation_;
|
||||
const glm::ivec3 tri3 = rel.triProperties[tri];
|
||||
glm::ivec3 edgeOffsets;
|
||||
glm::bvec3 edgeFwd(true);
|
||||
for (const int i : {0, 1, 2}) {
|
||||
const Halfedge& halfedge = this->halfedge_[3 * tri + i];
|
||||
edgeOffsets[i] = edgeOffset[half2Edge[3 * tri + i]];
|
||||
if (!halfedge.IsForward()) {
|
||||
const int pairTri = halfedge.pairedHalfedge / 3;
|
||||
const int j = halfedge.pairedHalfedge % 3;
|
||||
if (rel.triProperties[pairTri][j] !=
|
||||
rel.triProperties[tri][Next3(i)] ||
|
||||
rel.triProperties[pairTri][Next3(j)] !=
|
||||
rel.triProperties[tri][i]) {
|
||||
edgeOffsets[i] += addedVerts;
|
||||
} else {
|
||||
edgeFwd[i] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vec<glm::ivec3> newTris = subTris[tri].Reindex(
|
||||
tri3, edgeOffsets + propOffset, edgeFwd,
|
||||
interiorOffset[tri] + propOffset);
|
||||
copy(ExecutionPolicy::Seq, newTris.begin(), newTris.end(),
|
||||
triProp.begin() + triOffset[tri]);
|
||||
});
|
||||
|
||||
meshRelation_.properties = prop;
|
||||
meshRelation_.triProperties = triProp;
|
||||
}
|
||||
|
||||
CreateHalfedges(triVerts);
|
||||
|
||||
return vertBary;
|
||||
}
|
||||
|
||||
void Manifold::Impl::Refine(int n) {
|
||||
void Manifold::Impl::Refine(std::function<int(glm::vec3)> edgeDivisions) {
|
||||
Manifold::Impl old = *this;
|
||||
Vec<Barycentric> vertBary = Subdivide(n);
|
||||
Vec<Barycentric> vertBary = Subdivide(edgeDivisions);
|
||||
if (vertBary.size() == 0) return;
|
||||
|
||||
if (old.halfedgeTangent_.size() == old.halfedge_.size()) {
|
||||
for_each_n(autoPolicy(NumTri()), zip(vertPos_.begin(), vertBary.begin()),
|
||||
NumVert(),
|
||||
InterpTri({old.halfedge_, old.halfedgeTangent_, old.vertPos_}));
|
||||
// Make original since the subdivided faces have been warped into
|
||||
// being non-coplanar, and hence not being related to the original faces.
|
||||
meshRelation_.originalID = ReserveIDs(1);
|
||||
InitializeOriginal();
|
||||
}
|
||||
|
||||
halfedgeTangent_.resize(0);
|
||||
Finish();
|
||||
}
|
||||
|
||||
} // namespace manifold
|
||||
|
||||
@@ -300,6 +300,7 @@ void Manifold::Impl::Finish() {
|
||||
* Sorts the vertices according to their Morton code.
|
||||
*/
|
||||
void Manifold::Impl::SortVerts() {
|
||||
ZoneScoped;
|
||||
const int numVert = NumVert();
|
||||
Vec<uint32_t> vertMorton(numVert);
|
||||
auto policy = autoPolicy(numVert);
|
||||
@@ -338,6 +339,7 @@ void Manifold::Impl::SortVerts() {
|
||||
* also given.
|
||||
*/
|
||||
void Manifold::Impl::ReindexVerts(const Vec<int>& vertNew2Old, int oldNumVert) {
|
||||
ZoneScoped;
|
||||
Vec<int> vertOld2New(oldNumVert);
|
||||
scatter(autoPolicy(oldNumVert), countAt(0), countAt(NumVert()),
|
||||
vertNew2Old.begin(), vertOld2New.begin());
|
||||
@@ -349,6 +351,7 @@ void Manifold::Impl::ReindexVerts(const Vec<int>& vertNew2Old, int oldNumVert) {
|
||||
* Removes unreferenced property verts and reindexes triProperties.
|
||||
*/
|
||||
void Manifold::Impl::CompactProps() {
|
||||
ZoneScoped;
|
||||
if (meshRelation_.numProp == 0) return;
|
||||
|
||||
const int numVerts = meshRelation_.properties.size() / meshRelation_.numProp;
|
||||
@@ -377,6 +380,7 @@ void Manifold::Impl::CompactProps() {
|
||||
*/
|
||||
void Manifold::Impl::GetFaceBoxMorton(Vec<Box>& faceBox,
|
||||
Vec<uint32_t>& faceMorton) const {
|
||||
ZoneScoped;
|
||||
faceBox.resize(NumTri());
|
||||
faceMorton.resize(NumTri());
|
||||
for_each_n(autoPolicy(NumTri()),
|
||||
@@ -389,6 +393,7 @@ void Manifold::Impl::GetFaceBoxMorton(Vec<Box>& faceBox,
|
||||
* bounding box and Morton code arrays are also sorted accordingly.
|
||||
*/
|
||||
void Manifold::Impl::SortFaces(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton) {
|
||||
ZoneScoped;
|
||||
Vec<int> faceNew2Old(NumTri());
|
||||
auto policy = autoPolicy(faceNew2Old.size());
|
||||
sequence(policy, faceNew2Old.begin(), faceNew2Old.end());
|
||||
@@ -419,6 +424,7 @@ void Manifold::Impl::SortFaces(Vec<Box>& faceBox, Vec<uint32_t>& faceMorton) {
|
||||
* faces to gather into this.
|
||||
*/
|
||||
void Manifold::Impl::GatherFaces(const Vec<int>& faceNew2Old) {
|
||||
ZoneScoped;
|
||||
const int numTri = faceNew2Old.size();
|
||||
if (meshRelation_.triRef.size() == NumTri())
|
||||
Permute(meshRelation_.triRef, faceNew2Old);
|
||||
@@ -441,6 +447,7 @@ void Manifold::Impl::GatherFaces(const Vec<int>& faceNew2Old) {
|
||||
}
|
||||
|
||||
void Manifold::Impl::GatherFaces(const Impl& old, const Vec<int>& faceNew2Old) {
|
||||
ZoneScoped;
|
||||
const int numTri = faceNew2Old.size();
|
||||
auto policy = autoPolicy(numTri);
|
||||
|
||||
@@ -510,6 +517,7 @@ MeshGL::MeshGL(const Mesh& mesh) {
|
||||
* will report a Status if it is not manifold.
|
||||
*/
|
||||
bool MeshGL::Merge() {
|
||||
ZoneScoped;
|
||||
std::multiset<std::pair<int, int>> openEdges;
|
||||
|
||||
std::vector<int> merge(NumVert());
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
project (polygon)
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} ${SOURCE_FILES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
target_link_libraries( ${PROJECT_NAME}
|
||||
PUBLIC utilities
|
||||
)
|
||||
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_link_libraries(${PROJECT_NAME} PUBLIC utilities)
|
||||
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/polygon.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -13,26 +13,13 @@
|
||||
// limitations under the License.
|
||||
|
||||
#include "polygon.h"
|
||||
#if MANIFOLD_PAR == 'T'
|
||||
#include "tbb/tbb.h"
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
#if MANIFOLD_PAR == 'T' && TBB_INTERFACE_VERSION >= 10000 && \
|
||||
__has_include(<pstl/glue_execution_defs.h>)
|
||||
#include <execution>
|
||||
#endif
|
||||
#include <list>
|
||||
#include <map>
|
||||
#if __has_include(<memory_resource>)
|
||||
#include <memory_resource>
|
||||
#endif
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <stack>
|
||||
|
||||
#include "optional_assert.h"
|
||||
#include "utils.h"
|
||||
|
||||
namespace {
|
||||
using namespace manifold;
|
||||
@@ -41,6 +28,11 @@ static ExecutionParams params;
|
||||
|
||||
constexpr float kBest = -std::numeric_limits<float>::infinity();
|
||||
|
||||
// it seems that MSVC cannot optimize glm::determinant(glm::mat2(a, b))
|
||||
constexpr float determinant2x2(glm::vec2 a, glm::vec2 b) {
|
||||
return a.x * b.y - a.y * b.x;
|
||||
}
|
||||
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
struct PolyEdge {
|
||||
int startVert, endVert;
|
||||
@@ -181,6 +173,8 @@ void PrintFailure(const std::exception &e, const PolygonsIdx &polys,
|
||||
class EarClip {
|
||||
public:
|
||||
EarClip(const PolygonsIdx &polys, float precision) : precision_(precision) {
|
||||
ZoneScoped;
|
||||
|
||||
int numVert = 0;
|
||||
for (const SimplePolygonIdx &poly : polys) {
|
||||
numVert += poly.size();
|
||||
@@ -199,6 +193,8 @@ class EarClip {
|
||||
}
|
||||
|
||||
std::vector<glm::ivec3> Triangulate() {
|
||||
ZoneScoped;
|
||||
|
||||
for (const VertItr start : holes_) {
|
||||
CutKeyhole(start);
|
||||
}
|
||||
@@ -249,10 +245,10 @@ class EarClip {
|
||||
// two points and terminates.
|
||||
struct Vert {
|
||||
int mesh_idx;
|
||||
float cost;
|
||||
qItr ear;
|
||||
glm::vec2 pos, rightDir;
|
||||
VertItr left, right;
|
||||
float cost;
|
||||
|
||||
// Shorter than half of precision, to be conservative so that it doesn't
|
||||
// cause CW triangles that exceed precision due to rounding error.
|
||||
@@ -286,7 +282,8 @@ class EarClip {
|
||||
VertItr center = tail;
|
||||
VertItr last = center;
|
||||
|
||||
while (nextL != nextR && tail != nextR) {
|
||||
while (nextL != nextR && tail != nextR &&
|
||||
nextL != (toLeft ? right : left)) {
|
||||
const glm::vec2 edgeL = nextL->pos - center->pos;
|
||||
const float l2 = glm::dot(edgeL, edgeL);
|
||||
if (l2 <= p2) {
|
||||
@@ -380,10 +377,10 @@ class EarClip {
|
||||
// goes to the outside. No need to check the other side, since all verts are
|
||||
// processed in the EarCost loop.
|
||||
float SignedDist(VertItr v, glm::vec2 unit, float precision) const {
|
||||
float d = glm::determinant(glm::mat2(unit, v->pos - pos));
|
||||
if (glm::abs(d) < precision) {
|
||||
d = glm::max(d, glm::determinant(glm::mat2(unit, v->right->pos - pos)));
|
||||
d = glm::max(d, glm::determinant(glm::mat2(unit, v->left->pos - pos)));
|
||||
float d = determinant2x2(unit, v->pos - pos);
|
||||
if (std::abs(d) < precision) {
|
||||
d = glm::max(d, determinant2x2(unit, v->right->pos - pos));
|
||||
d = glm::max(d, determinant2x2(unit, v->left->pos - pos));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
@@ -394,8 +391,7 @@ class EarClip {
|
||||
float cost = glm::min(SignedDist(v, rightDir, precision),
|
||||
SignedDist(v, left->rightDir, precision));
|
||||
|
||||
const float openCost =
|
||||
glm::determinant(glm::mat2(openSide, v->pos - right->pos));
|
||||
const float openCost = determinant2x2(openSide, v->pos - right->pos);
|
||||
return glm::min(cost, openCost);
|
||||
}
|
||||
|
||||
@@ -429,9 +425,11 @@ class EarClip {
|
||||
return totalCost < -1 ? kBest : 0;
|
||||
}
|
||||
VertItr test = right->right;
|
||||
auto lid = left->mesh_idx;
|
||||
auto rid = right->mesh_idx;
|
||||
while (test != left) {
|
||||
if (test->mesh_idx != mesh_idx && test->mesh_idx != left->mesh_idx &&
|
||||
test->mesh_idx != right->mesh_idx) { // Skip duplicated verts
|
||||
if (test->mesh_idx != mesh_idx && test->mesh_idx != lid &&
|
||||
test->mesh_idx != rid) { // Skip duplicated verts
|
||||
float cost = Cost(test, openSide, precision);
|
||||
if (cost < -precision) {
|
||||
cost = DelaunayCost(test->pos - center, scale, precision);
|
||||
@@ -548,19 +546,22 @@ class EarClip {
|
||||
float bound = 0;
|
||||
for (const SimplePolygonIdx &poly : polys) {
|
||||
auto vert = poly.begin();
|
||||
polygon_.push_back({vert->idx, earsQueue_.end(), vert->pos});
|
||||
polygon_.push_back({vert->idx, 0.0f, earsQueue_.end(), vert->pos});
|
||||
const VertItr first = std::prev(polygon_.end());
|
||||
|
||||
bound = glm::max(
|
||||
bound, glm::max(std::abs(first->pos.x), std::abs(first->pos.y)));
|
||||
VertItr last = first;
|
||||
// This is not the real rightmost start, but just an arbitrary vert for
|
||||
// now to identify each polygon.
|
||||
starts.push_back(first);
|
||||
|
||||
for (++vert; vert != poly.end(); ++vert) {
|
||||
polygon_.push_back({vert->idx, earsQueue_.end(), vert->pos});
|
||||
VertItr next = std::prev(polygon_.end());
|
||||
|
||||
bound = glm::max(
|
||||
bound, glm::max(glm::abs(next->pos.x), glm::abs(next->pos.y)));
|
||||
bound, glm::max(std::abs(vert->pos.x), std::abs(vert->pos.y)));
|
||||
|
||||
polygon_.push_back({vert->idx, 0.0f, earsQueue_.end(), vert->pos});
|
||||
VertItr next = std::prev(polygon_.end());
|
||||
|
||||
Link(last, next);
|
||||
last = next;
|
||||
@@ -578,6 +579,8 @@ class EarClip {
|
||||
// Find the actual rightmost starts after degenerate removal. Also calculate
|
||||
// the polygon bounding boxes.
|
||||
void FindStart(VertItr first) {
|
||||
const glm::vec2 origin = first->pos;
|
||||
|
||||
VertItr start = first;
|
||||
float maxX = -std::numeric_limits<float>::infinity();
|
||||
Rect bBox;
|
||||
@@ -587,7 +590,8 @@ class EarClip {
|
||||
|
||||
auto AddPoint = [&](VertItr v) {
|
||||
bBox.Union(v->pos);
|
||||
const double area1 = glm::determinant(glm::dmat2(v->pos, v->right->pos));
|
||||
const double area1 =
|
||||
determinant2x2(v->pos - origin, v->right->pos - origin);
|
||||
const double t1 = area + area1;
|
||||
areaCompensation += (area - t1) + area1;
|
||||
area = t1;
|
||||
@@ -605,9 +609,9 @@ class EarClip {
|
||||
|
||||
area += areaCompensation;
|
||||
const glm::vec2 size = bBox.Size();
|
||||
const double minArea = precision_ * glm::max(size.x, size.y);
|
||||
const float minArea = precision_ * glm::max(size.x, size.y);
|
||||
|
||||
if (area < -minArea) {
|
||||
if (glm::isfinite(maxX) && area < -minArea) {
|
||||
holes_.insert(start);
|
||||
hole2BBox_.insert({start, bBox});
|
||||
} else {
|
||||
@@ -623,7 +627,6 @@ class EarClip {
|
||||
// incorrect due to precision, we check for polygon edges both ahead and
|
||||
// behind to ensure all valid options are found.
|
||||
void CutKeyhole(const VertItr start) {
|
||||
const float startX = start->pos.x;
|
||||
const Rect bBox = hole2BBox_[start];
|
||||
const int onTop = start->pos.y >= bBox.max.y - precision_ ? 1
|
||||
: start->pos.y <= bBox.min.y + precision_ ? -1
|
||||
@@ -668,7 +671,6 @@ class EarClip {
|
||||
// and returns it. It does so by finding any reflex verts inside the triangle
|
||||
// containing the best connection and the initial horizontal line.
|
||||
VertItr FindCloserBridge(VertItr start, VertItr edge, int onTop) {
|
||||
const float p2 = precision_ * precision_;
|
||||
VertItr best = edge->pos.x > edge->right->pos.x ? edge : edge->right;
|
||||
const float maxX = best->pos.x;
|
||||
const float above = best->pos.y > start->pos.y ? 1 : -1;
|
||||
@@ -743,6 +745,8 @@ class EarClip {
|
||||
// The main ear-clipping loop. This is called once for each simple polygon -
|
||||
// all holes have already been key-holed and joined to an outer polygon.
|
||||
void TriangulatePoly(VertItr start) {
|
||||
ZoneScoped;
|
||||
|
||||
// A simple polygon always creates two fewer triangles than it has verts.
|
||||
int numTri = -2;
|
||||
earsQueue_.clear();
|
||||
|
||||
@@ -14,25 +14,12 @@
|
||||
|
||||
project(sdf)
|
||||
|
||||
add_library(${PROJECT_NAME} src/sdf.cpp)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
add_library(${PROJECT_NAME} OBJECT src/sdf.cpp)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_link_libraries(${PROJECT_NAME} PUBLIC utilities)
|
||||
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/sdf.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -151,6 +151,7 @@ struct ComputeVerts {
|
||||
}
|
||||
|
||||
inline void operator()(Uint64 mortonCode) {
|
||||
ZoneScoped;
|
||||
if (gridVerts.Full()) return;
|
||||
|
||||
const glm::ivec4 gridIndex = DecodeMorton(mortonCode);
|
||||
@@ -205,6 +206,7 @@ struct BuildTris {
|
||||
}
|
||||
|
||||
void operator()(int idx) {
|
||||
ZoneScoped;
|
||||
Uint64 basekey = gridVerts.KeyAt(idx);
|
||||
if (basekey == kOpen) return;
|
||||
|
||||
@@ -305,7 +307,6 @@ Mesh LevelSet(std::function<float(glm::vec3)> sdf, Box bounds, float edgeLength,
|
||||
Mesh out;
|
||||
|
||||
const glm::vec3 dim = bounds.Size();
|
||||
const float maxDim = std::max(dim[0], std::max(dim[1], dim[2]));
|
||||
const glm::ivec3 gridSize(dim / edgeLength);
|
||||
const glm::vec3 spacing = dim / (glm::vec3(gridSize));
|
||||
|
||||
@@ -317,14 +318,14 @@ Mesh LevelSet(std::function<float(glm::vec3)> sdf, Box bounds, float edgeLength,
|
||||
// active.
|
||||
const auto pol = canParallel ? autoPolicy(maxMorton) : ExecutionPolicy::Seq;
|
||||
|
||||
int tableSize = glm::min(
|
||||
size_t tableSize = glm::min(
|
||||
2 * maxMorton, static_cast<Uint64>(10 * glm::pow(maxMorton, 0.667)));
|
||||
HashTable<GridVert, identity> gridVerts(tableSize);
|
||||
Vec<glm::vec3> vertPos(gridVerts.Size() * 7);
|
||||
|
||||
while (1) {
|
||||
Vec<int> index(1, 0);
|
||||
for_each_n(pol, countAt(0), maxMorton + 1,
|
||||
for_each_n(pol, countAt(0_z), maxMorton + 1,
|
||||
ComputeVerts({vertPos, index, gridVerts.D(), sdf, bounds.min,
|
||||
gridSize + 1, spacing, level}));
|
||||
|
||||
@@ -333,6 +334,7 @@ Mesh LevelSet(std::function<float(glm::vec3)> sdf, Box bounds, float edgeLength,
|
||||
const Uint64 lastMorton =
|
||||
MortonCode(glm::ivec4((lastVert - bounds.min) / spacing, 1));
|
||||
const float ratio = static_cast<float>(maxMorton) / lastMorton;
|
||||
|
||||
if (ratio > 1000) // do not trust the ratio if it is too large
|
||||
tableSize *= 2;
|
||||
else
|
||||
@@ -348,7 +350,7 @@ Mesh LevelSet(std::function<float(glm::vec3)> sdf, Box bounds, float edgeLength,
|
||||
Vec<glm::ivec3> triVerts(gridVerts.Entries() * 12); // worst case
|
||||
|
||||
Vec<int> index(1, 0);
|
||||
for_each_n(pol, countAt(0), gridVerts.Size(),
|
||||
for_each_n(pol, countAt(0_z), gridVerts.Size(),
|
||||
BuildTris({triVerts, index, gridVerts.D()}));
|
||||
triVerts.resize(index[0]);
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
set(CLIPPER2_UTILS OFF)
|
||||
set(CLIPPER2_EXAMPLES OFF)
|
||||
set(CLIPPER2_TESTS OFF)
|
||||
set(CLIPPER2_USINGZ "OFF" CACHE STRING "Preempt cache default of USINGZ (we only use 2d)")
|
||||
add_subdirectory(clipper2/CPP)
|
||||
add_library(quickhull OBJECT quickhull/QuickHull.cpp)
|
||||
target_include_directories(quickhull PUBLIC
|
||||
$<INSTALL_INTERFACE:quickhull>
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/quickhull>)
|
||||
|
||||
add_library(quickhull quickhull/QuickHull.cpp)
|
||||
target_include_directories(quickhull PUBLIC quickhull)
|
||||
target_compile_features(quickhull PUBLIC cxx_std_17)
|
||||
install(TARGETS quickhull EXPORT manifoldTargets)
|
||||
|
||||
|
||||
@@ -18,61 +18,36 @@ add_library(${PROJECT_NAME} INTERFACE)
|
||||
message("Parallel Backend: ${MANIFOLD_PAR}")
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(glm
|
||||
GIT_REPOSITORY https://github.com/g-truc/glm.git
|
||||
GIT_TAG 0.9.9.8
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
FIND_PACKAGE_ARGS NAMES glm
|
||||
)
|
||||
FetchContent_MakeAvailable(glm)
|
||||
|
||||
FetchContent_Declare(Thrust
|
||||
GIT_REPOSITORY https://github.com/NVIDIA/thrust.git
|
||||
GIT_TAG 2.1.0
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
FIND_PACKAGE_ARGS NAMES Thrust
|
||||
)
|
||||
FetchContent_MakeAvailable(Thrust)
|
||||
find_package(Thrust QUIET)
|
||||
if(NOT Thrust_FOUND AND NOT DEFINED thrust_SOURCE_DIR)
|
||||
message("thrust not found, downloading from source")
|
||||
FetchContent_Populate(Thrust)
|
||||
endif()
|
||||
|
||||
if (TRACY_ENABLE)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(tracy
|
||||
GIT_REPOSITORY https://github.com/wolfpld/tracy.git
|
||||
GIT_TAG v0.9.1
|
||||
GIT_TAG v0.10
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(tracy)
|
||||
target_link_libraries(${PROJECT_NAME} INTERFACE TracyClient)
|
||||
target_compile_options(${PROJECT_NAME} INTERFACE -DTRACY_NO_CODE_TRANSFER=1)
|
||||
endif()
|
||||
|
||||
if(MANIFOLD_PAR STREQUAL "TBB")
|
||||
find_package(PkgConfig)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(TBB tbb)
|
||||
endif()
|
||||
if(NOT TBB_FOUND)
|
||||
message(STATUS "tbb not found, downloading from source")
|
||||
include(FetchContent)
|
||||
set(TBB_TEST OFF CACHE INTERNAL "" FORCE)
|
||||
set(TBB_STRICT OFF CACHE INTERNAL "" FORCE)
|
||||
FetchContent_Declare(TBB
|
||||
GIT_REPOSITORY https://github.com/oneapi-src/oneTBB.git
|
||||
GIT_TAG v2021.10.0
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(TBB)
|
||||
set_property(DIRECTORY ${TBB_SOURCE_DIR} PROPERTY EXCLUDE_FROM_ALL YES)
|
||||
endif()
|
||||
target_compile_options(${PROJECT_NAME} INTERFACE -DMANIFOLD_PAR='T')
|
||||
if(TARGET TBB::tbb)
|
||||
target_link_libraries(${PROJECT_NAME} INTERFACE TBB::tbb)
|
||||
else()
|
||||
target_include_directories(${PROJECT_NAME} INTERFACE ${TBB_INCLUDE_DIRS})
|
||||
target_include_directories(${PROJECT_NAME} INTERFACE $<BUILD_INTERFACE:${TBB_INCLUDE_DIRS}>)
|
||||
target_link_libraries(${PROJECT_NAME} INTERFACE ${TBB_LINK_LIBRARIES})
|
||||
endif()
|
||||
elseif(MANIFOLD_PAR STREQUAL "NONE")
|
||||
@@ -82,20 +57,20 @@ else()
|
||||
"Should be \"TBB\" or \"NONE\"")
|
||||
endif()
|
||||
|
||||
target_include_directories(${PROJECT_NAME} INTERFACE ${PROJECT_SOURCE_DIR}/include)
|
||||
target_include_directories(${PROJECT_NAME} INTERFACE
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_link_libraries(${PROJECT_NAME} INTERFACE glm::glm)
|
||||
if(DEFINED Thrust_SOURCE_DIR)
|
||||
target_include_directories(${PROJECT_NAME} INTERFACE
|
||||
${Thrust_SOURCE_DIR}
|
||||
${Thrust_SOURCE_DIR}/dependencies/libcudacxx/include
|
||||
)
|
||||
else()
|
||||
target_include_directories(${PROJECT_NAME} INTERFACE
|
||||
${THRUST_ROOT}
|
||||
${THRUST_ROOT}/dependencies/libcudacxx/include
|
||||
)
|
||||
|
||||
if(NOT DEFINED thrust_SOURCE_DIR)
|
||||
set(thrust_SOURCE_DIR ${_THRUST_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
target_include_directories(${PROJECT_NAME} INTERFACE
|
||||
$<BUILD_INTERFACE:${thrust_SOURCE_DIR}>
|
||||
$<BUILD_INTERFACE:${thrust_SOURCE_DIR}/dependencies/libcudacxx/include>
|
||||
)
|
||||
|
||||
target_compile_options(${PROJECT_NAME} INTERFACE
|
||||
-DTHRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_${MANIFOLD_PAR}
|
||||
)
|
||||
@@ -107,9 +82,5 @@ endif()
|
||||
|
||||
target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_17)
|
||||
|
||||
install(
|
||||
FILES include/public.h
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/public.h include/vec_view.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -63,13 +63,15 @@ namespace manifold {
|
||||
template <typename V, hash_fun_t H = hash64bit>
|
||||
class HashTableD {
|
||||
public:
|
||||
HashTableD(Vec<Uint64>& keys, Vec<V>& values, Vec<uint32_t>& used,
|
||||
HashTableD(Vec<Uint64>& keys, Vec<V>& values, std::atomic<size_t>& used,
|
||||
uint32_t step = 1)
|
||||
: step_{step}, keys_{keys}, values_{values}, used_{used} {}
|
||||
|
||||
int Size() const { return keys_.size(); }
|
||||
|
||||
bool Full() const { return AtomicLoad(used_[0]) * 2 > Size(); }
|
||||
bool Full() const {
|
||||
return used_.load(std::memory_order_relaxed) * 2 > Size();
|
||||
}
|
||||
|
||||
void Insert(Uint64 key, const V& val) {
|
||||
uint32_t idx = H(key) & (Size() - 1);
|
||||
@@ -78,7 +80,7 @@ class HashTableD {
|
||||
Uint64& k = keys_[idx];
|
||||
const Uint64 found = AtomicCAS(k, kOpen, key);
|
||||
if (found == kOpen) {
|
||||
AtomicAdd(used_[0], 0x1u);
|
||||
used_.fetch_add(1, std::memory_order_relaxed);
|
||||
values_[idx] = val;
|
||||
return;
|
||||
}
|
||||
@@ -117,27 +119,44 @@ class HashTableD {
|
||||
uint32_t step_;
|
||||
VecView<Uint64> keys_;
|
||||
VecView<V> values_;
|
||||
VecView<uint32_t> used_;
|
||||
std::atomic<size_t>& used_;
|
||||
};
|
||||
|
||||
template <typename V, hash_fun_t H = hash64bit>
|
||||
class HashTable {
|
||||
public:
|
||||
HashTable(uint32_t size, uint32_t step = 1)
|
||||
: keys_{1 << (int)ceil(log2(size)), kOpen},
|
||||
values_{1 << (int)ceil(log2(size)), {}},
|
||||
table_{keys_, values_, used_, step} {}
|
||||
HashTable(size_t size, uint32_t step = 1)
|
||||
: keys_{size == 0 ? 0 : 1_z << (int)ceil(log2(size)), kOpen},
|
||||
values_{size == 0 ? 0 : 1_z << (int)ceil(log2(size)), {}},
|
||||
step_(step) {}
|
||||
|
||||
HashTableD<V, H> D() { return table_; }
|
||||
HashTable(const HashTable& other)
|
||||
: keys_(other.keys_),
|
||||
values_(other.values_),
|
||||
used_(other.used_),
|
||||
step_(other.step_) {}
|
||||
|
||||
int Entries() const { return AtomicLoad(used_[0]); }
|
||||
HashTable& operator=(const HashTable& other) {
|
||||
if (this == &other) return *this;
|
||||
keys_ = other.keys_;
|
||||
values_ = other.values_;
|
||||
used_.store(other.used_.load());
|
||||
step_ = other.step_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
int Size() const { return table_.Size(); }
|
||||
HashTableD<V, H> D() { return {keys_, values_, used_, step_}; }
|
||||
|
||||
bool Full() const { return AtomicLoad(used_[0]) * 2 > Size(); }
|
||||
int Entries() const { return used_.load(std::memory_order_relaxed); }
|
||||
|
||||
size_t Size() const { return keys_.size(); }
|
||||
|
||||
bool Full() const {
|
||||
return used_.load(std::memory_order_relaxed) * 2 > Size();
|
||||
}
|
||||
|
||||
float FilledFraction() const {
|
||||
return static_cast<float>(AtomicLoad(used_[0])) / Size();
|
||||
return static_cast<float>(used_.load(std::memory_order_relaxed)) / Size();
|
||||
}
|
||||
|
||||
Vec<V>& GetValueStore() { return values_; }
|
||||
@@ -147,8 +166,8 @@ class HashTable {
|
||||
private:
|
||||
Vec<Uint64> keys_;
|
||||
Vec<V> values_;
|
||||
Vec<uint32_t> used_ = Vec<uint32_t>(1, 0);
|
||||
HashTableD<V, H> table_;
|
||||
std::atomic<size_t> used_ = 0;
|
||||
uint32_t step_;
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
||||
@@ -51,7 +51,7 @@ enum class ExecutionPolicy {
|
||||
// - Sequential for small workload,
|
||||
// - Parallel (CPU) for medium workload,
|
||||
// - GPU for large workload if available.
|
||||
inline constexpr ExecutionPolicy autoPolicy(int size) {
|
||||
inline constexpr ExecutionPolicy autoPolicy(size_t size) {
|
||||
// some random numbers
|
||||
if (size <= (1 << 12)) {
|
||||
return ExecutionPolicy::Seq;
|
||||
|
||||