Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0856539bfc | ||
|
|
621835683f | ||
|
|
bec8164120 | ||
|
|
062399c1c7 | ||
|
|
d51ee709c2 | ||
|
|
206871ac11 | ||
|
|
2055b9c412 | ||
|
|
5e103931f1 | ||
|
|
77051ead35 | ||
|
|
83b78f8356 | ||
|
|
6c6193c25b | ||
|
|
e79d343bec | ||
|
|
1d6a89ac87 | ||
|
|
8651bf8bd5 | ||
|
|
1514c15184 | ||
|
|
cc83e1b7aa | ||
|
|
111350f1ef | ||
|
|
b4f583fcae | ||
|
|
8011fb6072 | ||
|
|
9290d420b0 | ||
|
|
fca66380f5 | ||
|
|
8497e44915 | ||
|
|
a7eac92087 | ||
|
|
15399474fa | ||
|
|
2d513446f7 | ||
|
|
abff7e8ed3 | ||
|
|
7c454dc7a4 | ||
|
|
ffdd97d4d3 | ||
|
|
7facd0ecc0 | ||
|
|
59760b3369 | ||
|
|
5f9b484b6c | ||
|
|
18edefc6c4 | ||
|
|
93e06a2de8 | ||
|
|
047159e2c4 |
@@ -0,0 +1,57 @@
|
||||
name: build_wheels
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_wheels:
|
||||
name: Build wheels on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest]
|
||||
steps:
|
||||
- run: |
|
||||
git config --global submodule.fetchJobs 8
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build wheels
|
||||
uses: pypa/cibuildwheel@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
make_sdist:
|
||||
name: Make SDist
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build SDist
|
||||
run: pipx run build --sdist
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: dist/*.tar.gz
|
||||
|
||||
pypi-publish:
|
||||
name: Upload release to PyPI
|
||||
needs: [build_wheels, make_sdist]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/manifold3d
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifact
|
||||
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
|
||||
|
||||
@@ -15,47 +15,43 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
cuda_support: [ON, OFF]
|
||||
parallel_backend: [NONE, OMP, TBB]
|
||||
parallel_backend: [NONE, TBB]
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event.pull_request.draft == false
|
||||
container:
|
||||
image: docker://nvidia/cuda:12.0.1-devel-ubuntu22.04
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt install -y libomp-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip lcov
|
||||
pip install trimesh
|
||||
- uses: actions/checkout@v3
|
||||
sudo apt-get -y update
|
||||
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' || matrix.cuda_support != 'OFF'
|
||||
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}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} .. && make
|
||||
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
|
||||
# note that the test for CUDA backend does not really test CUDA, as we
|
||||
# don't have CUDA GPU on GitHub Action
|
||||
if: matrix.parallel_backend != 'NONE' || matrix.cuda_support != 'OFF'
|
||||
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: |
|
||||
export PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python
|
||||
cd build/test
|
||||
./manifold_test
|
||||
cd ../../
|
||||
python3 bindings/python/examples/run_all.py
|
||||
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
|
||||
# perhaps issue related to invalid memory access?
|
||||
if: matrix.parallel_backend == 'NONE' && matrix.cuda_support == 'OFF'
|
||||
if: matrix.parallel_backend == 'NONE'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} -DCODE_COVERAGE=ON .. && make
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DCODE_COVERAGE=ON .. && make
|
||||
lcov --capture --initial --directory . --output-file ./code_coverage_init.info
|
||||
cd test
|
||||
./manifold_test
|
||||
@@ -63,36 +59,39 @@ 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
|
||||
if: matrix.parallel_backend == 'NONE' && matrix.cuda_support == 'OFF'
|
||||
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}}-${{matrix.cuda_support}}
|
||||
name: ${{matrix.parallel_backend}}
|
||||
- name: test cmake consumer
|
||||
run: |
|
||||
cd build
|
||||
sudo cmake --install .
|
||||
cd ..
|
||||
./test-cmake.sh
|
||||
|
||||
build_cbind:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event.pull_request.draft == false
|
||||
container:
|
||||
image: docker://nvidia/cuda:12.0.1-devel-ubuntu22.04
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt install -y libomp-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip
|
||||
- uses: actions/checkout@v3
|
||||
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@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- name: Build C bindings with OMP and CUDA
|
||||
- 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=OMP -DMANIFOLD_USE_CUDA=ON .. && make
|
||||
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
|
||||
# note that the test for CUDA backend does not really test CUDA, as we
|
||||
# don't have CUDA GPU on GitHub Action
|
||||
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
|
||||
./manifold_test --gtest_filter=CBIND.*
|
||||
@@ -105,8 +104,8 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs
|
||||
- uses: actions/checkout@v3
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs libglm-dev
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup WASM
|
||||
@@ -114,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
|
||||
@@ -133,8 +132,9 @@ 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/
|
||||
@@ -144,45 +144,56 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
parallel_backend: [NONE]
|
||||
cuda_support: [OFF]
|
||||
parallel_backend: [NONE, TBB]
|
||||
max-parallel: 1
|
||||
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: Install CUDA
|
||||
if: matrix.backend == 'CUDA'
|
||||
env:
|
||||
cuda: "11.3.0"
|
||||
visual_studio: "Visual Studio 16 2019"
|
||||
shell: powershell
|
||||
run: |
|
||||
Invoke-RestMethod https://raw.githubusercontent.com/ptheywood/cuda-cmake-github-actions/21811d232fc775fa8d44dddf7f8c5ce2cf8d2b81/scripts/actions/install_cuda_windows.ps1 -OutFile install_cuda_windows.ps1
|
||||
.\install_cuda_windows.ps1
|
||||
- name: nvcc check
|
||||
if: matrix.backend == 'CUDA'
|
||||
shell: powershell
|
||||
run: |
|
||||
nvcc -V
|
||||
ls $env:CUDA_PATH
|
||||
ls $env:CUDA_PATH\bin
|
||||
ls $env:CUDA_PATH\include
|
||||
- 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}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} -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}} with CUDA ${{matrix.cuda_support}}
|
||||
- name: Test ${{matrix.parallel_backend}}
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
strategy:
|
||||
matrix:
|
||||
parallel_backend: [NONE, TBB]
|
||||
max-parallel: 1
|
||||
runs-on: ubuntu-latest
|
||||
container: openscad/mxe-x86_64-gui:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
- name: Test
|
||||
run: |
|
||||
cd build/test
|
||||
./Release/manifold_test.exe
|
||||
./manifold_test
|
||||
|
||||
build_mac:
|
||||
timeout-minutes: 30
|
||||
@@ -194,38 +205,53 @@ jobs:
|
||||
steps:
|
||||
- name: Install common dependencies
|
||||
run: |
|
||||
brew install pkg-config assimp
|
||||
pip install trimesh
|
||||
- name: Install OpenMP
|
||||
if: matrix.parallel_backend == 'OMP'
|
||||
run: brew install libomp
|
||||
brew install pkg-config googletest assimp
|
||||
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
|
||||
strategy:
|
||||
matrix:
|
||||
variant: [none, omp, tbb, none-cuda, omp-cuda, tbb-cuda, js]
|
||||
variant: [none, tbb, js]
|
||||
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@v15
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- run: nix build -L '.?submodules=1#manifold-${{matrix.variant}}'
|
||||
|
||||
build_nix_python:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: cachix/install-nix-action@v22
|
||||
- run: nix build -L '.?submodules=1#manifold3d'
|
||||
|
||||
@@ -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,19 +1,6 @@
|
||||
[submodule "third_party/thrust"]
|
||||
path = src/third_party/thrust
|
||||
url = https://github.com/NVIDIA/thrust
|
||||
[submodule "third_party/glm"]
|
||||
path = src/third_party/glm
|
||||
url = https://github.com/g-truc/glm
|
||||
[submodule "third_party/google_test"]
|
||||
path = test/third_party/google_test
|
||||
url = https://github.com/google/googletest.git
|
||||
[submodule "third_party/pybind11"]
|
||||
path = bindings/python/third_party/pybind11
|
||||
url = https://github.com/pybind/pybind11
|
||||
branch = stable
|
||||
[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
|
||||
[submodule "bindings/python/third_party/nanobind"]
|
||||
path = bindings/python/third_party/nanobind
|
||||
url = https://github.com/wjakob/nanobind
|
||||
|
||||
@@ -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,8 +122,7 @@
|
||||
"-DMANIFOLD_CBIND=ON",
|
||||
"-DMANIFOLD_EXPORT=ON",
|
||||
"-DMANIFOLD_DEBUG=ON",
|
||||
"-DMANIFOLD_USE_CUDA=OFF",
|
||||
"-DMANIFOLD_PAR=NONE",
|
||||
"-DMANIFOLD_PAR=TBB",
|
||||
"-DCODE_COVERAGE=OFF",
|
||||
"-DCMAKE_CXX_FLAGS=''" //'-fsanitize=address,undefined'"
|
||||
],
|
||||
@@ -140,4 +144,9 @@
|
||||
"Verts"
|
||||
],
|
||||
"javascript.format.semicolons": "insert",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"python.formatting.provider": "none",
|
||||
"clang-format.executable": "clang-format",
|
||||
}
|
||||
@@ -16,68 +16,101 @@ cmake_minimum_required(VERSION 3.18)
|
||||
project(manifold LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_VERBOSE_MAKEFILE ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
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_USE_CUDA "Enable GPU support via CUDA" OFF)
|
||||
option(MANIFOLD_PYBIND "Build python bindings" ON)
|
||||
option(MANIFOLD_CBIND "Build C (FFI) bindings" OFF)
|
||||
set(MANIFOLD_PAR "NONE" CACHE STRING "Parallel backend, either \"TBB\" or \"OpenMP\" or \"NONE\"")
|
||||
set(MANIFOLD_FLAGS -O3)
|
||||
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)
|
||||
option(BUILD_TEST_CGAL "Build CGAL performance comparisons" OFF)
|
||||
if(TRACY_ENABLE)
|
||||
option(CMAKE_BUILD_TYPE "Build type" RelWithDebInfo)
|
||||
if(TRACY_MEMORY_USAGE)
|
||||
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} -DTRACY_MEMORY_USAGE)
|
||||
endif()
|
||||
if(NOT MSVC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer")
|
||||
endif()
|
||||
else()
|
||||
option(CMAKE_BUILD_TYPE "Build type" Release)
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
message("Building for 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)
|
||||
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()
|
||||
|
||||
option(PYBIND11_FINDPYTHON "Enable PyBind to perform FindPython for you" ON)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/manifoldDeps.cmake)
|
||||
|
||||
option(BUILD_TEST_CGAL "Build CGAL performance comparisons" OFF)
|
||||
|
||||
option(BUILD_SHARED_LIBS "Build dynamic/shared libraries" OFF)
|
||||
set(PYBIND11_DIR ${PROJECT_SOURCE_DIR}/bindings/python/third_party/pybind11)
|
||||
set(THRUST_INC_DIR ${PROJECT_SOURCE_DIR}/src/third_party/thrust)
|
||||
|
||||
if(BUILD_SHARED_LIBS OR MANIFOLD_CBIND)
|
||||
# Allow shared libraries to be relocatable (add Place Independent Code flag).
|
||||
# Also include when statically linking C bindings to avoid issues with bundling
|
||||
# artefacts in host languages using them for FFI.
|
||||
add_compile_options(-fPIC)
|
||||
if(MANIFOLD_PYBIND)
|
||||
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(MANIFOLD_USE_CUDA)
|
||||
enable_language(CUDA)
|
||||
find_package(CUDA REQUIRED)
|
||||
|
||||
# we cannot set THRUST_INC_DIR when building with CUDA, otherwise the
|
||||
# compiler will not use the system CUDA headers which causes incompatibility
|
||||
# clear THRUST_INC_DIR, we use the one from nvcc
|
||||
set(THRUST_INC_DIR "")
|
||||
set(MANIFOLD_NVCC_RELEASE_FLAGS -O3 -lineinfo)
|
||||
set(MANIFOLD_NVCC_DEBUG_FLAGS -G)
|
||||
set(MANIFOLD_NVCC_FLAGS -Xcudafe --diag_suppress=esa_on_defaulted_function_ignored --extended-lambda --expt-relaxed-constexpr
|
||||
"$<$<CONFIG:RELEASE>:${MANIFOLD_NVCC_RELEASE_FLAGS}>"
|
||||
"$<$<CONFIG:DEBUG>:${MANIFOLD_NVCC_DEBUG_FLAGS}>")
|
||||
if(CMAKE_EXPORT_COMPILE_COMMANDS AND NOT EMSCRIPTEN)
|
||||
# for nixos
|
||||
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES
|
||||
${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
|
||||
endif()
|
||||
|
||||
if(NOT MSVC)
|
||||
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
|
||||
add_compile_options(
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:${WARNING_FLAGS}>"
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=${WARNING_FLAGS}>")
|
||||
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 -Wno-alloc-size-larger-than)
|
||||
else()
|
||||
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
|
||||
endif()
|
||||
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${WARNING_FLAGS} -O3)
|
||||
endif()
|
||||
|
||||
if(CODE_COVERAGE AND NOT MSVC)
|
||||
set(COVERAGE_FLAGS -coverage -fno-inline-small-functions -fkeep-inline-functions -fkeep-static-functions)
|
||||
add_compile_options(
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:${COVERAGE_FLAGS}>"
|
||||
"$<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=-coverage>")
|
||||
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${COVERAGE_FLAGS})
|
||||
add_link_options("-coverage")
|
||||
endif()
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -90,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 1)
|
||||
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,41 +1,46 @@
|
||||
[](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 GPU-accelerated, 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.
|
||||
|
||||
We have four core dependencies, making use of submodules to ensure compatibility:
|
||||
- `graphlite`: connected components algorithm
|
||||
- `Clipper2`: provides our 2D subsystem
|
||||
- `GLM`: a compact vector library
|
||||
- `Thrust`: Nvidia's parallel algorithms library (basically a superset of C++17 std::parallel_algorithms)
|
||||
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.
|
||||
- [`Thrust`](https://github.com/NVIDIA/thrust): NVIDIA's parallel algorithms library (basically a superset of C++17 std::parallel_algorithms)
|
||||
- [`tbb`](https://github.com/oneapi-src/oneTBB/): Intel's thread building blocks library. (only when `MANIFOLD_PAR=TBB` is enabled)
|
||||
- [`gtest`](https://github.com/google/googletest/): Google test library (only when test is enabled, i.e. `MANIFOLD_TEST=ON`)
|
||||
|
||||
Other dependencies:
|
||||
- [`Clipper2`](https://github.com/AngusJohnson/Clipper2): provides our 2D subsystem
|
||||
- [`quickhull`](https://github.com/akuukka/quickhull): 3D convex hull algorithm.
|
||||
|
||||
## What's here
|
||||
|
||||
@@ -43,7 +48,9 @@ This library is fast with guaranteed manifold output. As such you need manifold
|
||||
|
||||
The most significant contribution here is a guaranteed-manifold [mesh Boolean](https://github.com/elalish/manifold/wiki/Manifold-Library#mesh-boolean) algorithm, which I believe is the first of its kind. If you know of another, please open a discussion - a mesh Boolean algorithm robust to edge cases has been an open problem for many years. Likewise, if the Boolean here ever fails you, please submit an issue! This Boolean forms the basis of a CAD kernel, as it allows simple shapes to be combined into more complex ones.
|
||||
|
||||
To aid in speed, this library makes extensive use of parallelization, generally through Nvidia's Thrust library. You can switch between the CUDA, OMP, TBB, and serial C++ backends by setting a CMake flag. Not everything is so parallelizable, for instance a [polygon triangulation](https://github.com/elalish/manifold/wiki/Manifold-Library#polygon-triangulation) algorithm is included which is serial. Even if compiled for CUDA, the code will still run without a GPU, falling back to the serial version of the algorithms. The WASM build is serial-only for now, but still fast.
|
||||
To aid in speed, this library makes extensive use of parallelization, generally through Nvidia's Thrust library. You can switch between the TBB, and serial C++ backends by setting a CMake flag. Not everything is so parallelizable, for instance a [polygon triangulation](https://github.com/elalish/manifold/wiki/Manifold-Library#polygon-triangulation) algorithm is included which is serial. Even if compiled with parallel backend, the code will still fall back to the serial version of the algorithms if the problem size is small. The WASM build is serial-only for now, but still fast.
|
||||
|
||||
> Note: OMP and CUDA backends are now removed
|
||||
|
||||
Look in the [samples](https://github.com/elalish/manifold/tree/master/samples) directory for examples of how to use this library to make interesting 3D models. You may notice that some of these examples bare a certain resemblance to my OpenSCAD designs on [Thingiverse](https://www.thingiverse.com/emmett), which is no accident. Much as I love OpenSCAD, my library is dramatically faster and the code is more flexible.
|
||||
|
||||
@@ -61,16 +68,30 @@ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON .. && make
|
||||
test/manifold_test
|
||||
```
|
||||
|
||||
CMake flags (usage e.g. `-DMANIFOLD_USE_CUDA=ON`):
|
||||
- `MANIFOLD_USE_CUDA=[<OFF>, ON]`: Provides Nvidia GPU parallelization, requires CUDA Toolkit and its nvcc compiler.
|
||||
- `MANIFOLD_PAR=[<NONE>, OMP, TBB]`: Provides multi-thread parallelization, requires `libomp-dev` or `libtbb-dev`.
|
||||
CMake flags (usage e.g. `-DMANIFOLD_DEBUG=ON`):
|
||||
- `MANIFOLD_JSBIND=[OFF, <ON>]`: Build js binding when using emscripten.
|
||||
- `MANIFOLD_CBIND=[<OFF>, ON]`: Build C FFI binding.
|
||||
- `MANIFOLD_PYBIND=[OFF, <ON>]`: Build python binding.
|
||||
- `MANIFOLD_PAR=[<NONE>, TBB]`: Provides multi-thread parallelization, requires `libtbb-dev` if `TBB` backend is selected.
|
||||
- `MANIFOLD_EXPORT=[<OFF>, ON]`: Enables GLB export of 3D models from the tests, requires `libassimp-dev`.
|
||||
- `MANIFOLD_DEBUG=[<OFF>, ON]`: Enables internal assertions and exceptions.
|
||||
- `MANIFOLD_TEST=[OFF, <ON>]`: Build unittests.
|
||||
- `TRACY_ENABLE=[<OFF>, ON]`: Enable integration with tracy profiler.
|
||||
See profiling section below.
|
||||
- `BUILD_TEST_CGAL=[<OFF>, ON]`: Builds a CGAL-based performance [comparison](https://github.com/elalish/manifold/tree/master/extras), requires `libcgal-dev`.
|
||||
|
||||
Offline building:
|
||||
- `FETCHCONTENT_SOURCE_DIR_GLM`: path to glm source.
|
||||
- `FETCHCONTENT_SOURCE_DIR_GOOGLETEST`: path to googletest source.
|
||||
- `FETCHCONTENT_SOURCE_DIR_THRUST`: path to NVIDIA thrust source.
|
||||
|
||||
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):
|
||||
@@ -99,12 +120,12 @@ node test/manifold_test.js
|
||||
### Python
|
||||
|
||||
The CMake script will build the python binding `manifold3d` automatically. To
|
||||
use the extension, please add `$BUILD_DIR/tools` to your `PYTHONPATH`, where
|
||||
use the extension, please add `$BUILD_DIR/bindings/python` to your `PYTHONPATH`, where
|
||||
`$BUILD_DIR` is the build directory for CMake. Examples using the python binding
|
||||
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
|
||||
```
|
||||
|
||||
@@ -118,10 +139,46 @@ 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!
|
||||
|
||||
### 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
|
||||
|
||||
This library was started by [Emmett Lalish](https://elalish.blogspot.com/). I am currently a Google employee and this is my 20% project, not an official Google project. At my day job I'm the maintainer of [\<model-viewer\>](https://modelviewer.dev/). I was the first employee at a 3D video startup, [Omnivor](https://www.omnivor.io/), and before that I worked on 3D printing at Microsoft, including [3D Builder](https://www.microsoft.com/en-us/p/3d-builder/9wzdncrfj3t6?activetab=pivot%3Aoverviewtab). Originally an aerospace engineer, I started at a small DARPA contractor doing seedling projects, one of which became [Sea Hunter](https://en.wikipedia.org/wiki/Sea_Hunter). I earned my doctorate from the University of Washington in control theory and published some [papers](https://www.researchgate.net/scientific-contributions/75011026_Emmett_Lalish).
|
||||
|
||||
@@ -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.
|
||||
@@ -20,6 +20,6 @@ if(NOT EMSCRIPTEN AND MANIFOLD_PYBIND)
|
||||
add_subdirectory(python)
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
if(EMSCRIPTEN AND MANIFOLD_JSBIND)
|
||||
add_subdirectory(wasm)
|
||||
endif()
|
||||
|
||||
@@ -1,22 +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 graphlite 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} EXPORT manifoldTargets)
|
||||
install(FILES include/manifoldc.h include/types.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#include <conv.h>
|
||||
#include <manifold.h>
|
||||
#include <public.h>
|
||||
// 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.
|
||||
|
||||
#include "conv.h"
|
||||
#include "manifold.h"
|
||||
#include "public.h"
|
||||
#include "types.h"
|
||||
|
||||
ManifoldBox *manifold_box(void *mem, float x1, float y1, float z1, float x2,
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
#include <conv.h>
|
||||
#include <manifold.h>
|
||||
#include <sdf.h>
|
||||
// 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.
|
||||
|
||||
#include "conv.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "cross_section.h"
|
||||
#include "include/types.h"
|
||||
#include "manifold.h"
|
||||
#include "public.h"
|
||||
#include "sdf.h"
|
||||
#include "types.h"
|
||||
|
||||
ManifoldManifold *to_c(manifold::Manifold *m) {
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
#include <conv.h>
|
||||
#include <manifold.h>
|
||||
#include <public.h>
|
||||
// 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.
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "conv.h"
|
||||
#include "cross_section.h"
|
||||
#include "manifold.h"
|
||||
#include "public.h"
|
||||
#include "types.h"
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_empty(void *mem) {
|
||||
@@ -106,6 +119,30 @@ ManifoldCrossSection *manifold_cross_section_intersection(
|
||||
return to_c(new (mem) CrossSection(cs));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_hull(void *mem,
|
||||
ManifoldCrossSection *cs) {
|
||||
auto hulled = from_c(cs)->Hull();
|
||||
return to_c(new (mem) CrossSection(hulled));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_batch_hull(
|
||||
void *mem, ManifoldCrossSectionVec *css) {
|
||||
auto hulled = CrossSection::Hull(*from_c(css));
|
||||
return to_c(new (mem) CrossSection(hulled));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_hull_simple_polygon(
|
||||
void *mem, ManifoldSimplePolygon *ps) {
|
||||
auto hulled = CrossSection::Hull(*from_c(ps));
|
||||
return to_c(new (mem) CrossSection(hulled));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_hull_polygons(
|
||||
void *mem, ManifoldPolygons *ps) {
|
||||
auto hulled = CrossSection::Hull(*from_c(ps));
|
||||
return to_c(new (mem) CrossSection(hulled));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_translate(void *mem,
|
||||
ManifoldCrossSection *cs,
|
||||
float x, float y) {
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
#pragma once
|
||||
#include <manifold.h>
|
||||
#include <public.h>
|
||||
#include <sdf.h>
|
||||
#include <types.h>
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "cross_section.h"
|
||||
#include "manifold.h"
|
||||
#include "public.h"
|
||||
#include "sdf.h"
|
||||
#include "types.h"
|
||||
|
||||
using namespace manifold;
|
||||
using ManifoldVec = std::vector<Manifold>;
|
||||
using CrossSectionVec = std::vector<CrossSection>;
|
||||
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
// 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.
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <types.h>
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
@@ -8,6 +23,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
// Polygons
|
||||
|
||||
ManifoldSimplePolygon *manifold_simple_polygon(void *mem, ManifoldVec2 *ps,
|
||||
size_t length);
|
||||
ManifoldPolygons *manifold_polygons(void *mem, ManifoldSimplePolygon **ps,
|
||||
@@ -24,6 +40,7 @@ ManifoldVec2 manifold_polygons_get_point(ManifoldPolygons *ps, int simple_idx,
|
||||
int pt_idx);
|
||||
|
||||
// Mesh Construction
|
||||
|
||||
ManifoldMeshGL *manifold_meshgl(void *mem, float *vert_props, size_t n_verts,
|
||||
size_t n_props, uint32_t *tri_verts,
|
||||
size_t n_tris);
|
||||
@@ -94,7 +111,20 @@ 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);
|
||||
ManifoldManifold *manifold_batch_hull(void *mem, ManifoldManifoldVec *ms);
|
||||
ManifoldManifold *manifold_hull_pts(void *mem, ManifoldVec3 *ps, size_t length);
|
||||
|
||||
// Manifold Transformations
|
||||
|
||||
ManifoldManifold *manifold_translate(void *mem, ManifoldManifold *m, float x,
|
||||
float y, float z);
|
||||
ManifoldManifold *manifold_rotate(void *mem, ManifoldManifold *m, float x,
|
||||
@@ -110,8 +140,11 @@ 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
|
||||
|
||||
ManifoldManifold *manifold_empty(void *mem);
|
||||
ManifoldManifold *manifold_copy(void *mem, ManifoldManifold *m);
|
||||
ManifoldManifold *manifold_tetrahedron(void *mem);
|
||||
@@ -137,6 +170,7 @@ ManifoldManifoldVec *manifold_decompose(void *mem, ManifoldManifold *m);
|
||||
ManifoldManifold *manifold_as_original(void *mem, ManifoldManifold *m);
|
||||
|
||||
// Manifold Info
|
||||
|
||||
int manifold_is_empty(ManifoldManifold *m);
|
||||
ManifoldError manifold_status(ManifoldManifold *m);
|
||||
int manifold_num_vert(ManifoldManifold *m);
|
||||
@@ -204,6 +238,17 @@ ManifoldCrossSection *manifold_cross_section_difference(
|
||||
ManifoldCrossSection *manifold_cross_section_intersection(
|
||||
void *mem, ManifoldCrossSection *a, ManifoldCrossSection *b);
|
||||
|
||||
// CrossSection Convex Hulls
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_hull(void *mem,
|
||||
ManifoldCrossSection *cs);
|
||||
ManifoldCrossSection *manifold_cross_section_batch_hull(
|
||||
void *mem, ManifoldCrossSectionVec *css);
|
||||
ManifoldCrossSection *manifold_cross_section_hull_simple_polygon(
|
||||
void *mem, ManifoldSimplePolygon *ps);
|
||||
ManifoldCrossSection *manifold_cross_section_hull_polygons(
|
||||
void *mem, ManifoldPolygons *ps);
|
||||
|
||||
// CrossSection Transformation
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_translate(void *mem,
|
||||
@@ -264,10 +309,9 @@ ManifoldRect *manifold_rect_mul(void *mem, ManifoldRect *r, float x, float y);
|
||||
int manifold_rect_does_overlap_rect(ManifoldRect *a, ManifoldRect *r);
|
||||
int manifold_rect_is_empty(ManifoldRect *r);
|
||||
int manifold_rect_is_finite(ManifoldRect *r);
|
||||
ManifoldCrossSection *manifold_rect_as_cross_section(void *mem,
|
||||
ManifoldRect *r);
|
||||
|
||||
// Bounding Box
|
||||
|
||||
ManifoldBox *manifold_box(void *mem, float x1, float y1, float z1, float x2,
|
||||
float y2, float z2);
|
||||
ManifoldVec3 manifold_box_min(ManifoldBox *b);
|
||||
@@ -292,11 +336,13 @@ int manifold_box_does_overlap_box(ManifoldBox *a, ManifoldBox *b);
|
||||
int manifold_box_is_finite(ManifoldBox *b);
|
||||
|
||||
// Static Quality Globals
|
||||
|
||||
void manifold_set_min_circular_angle(float degrees);
|
||||
void manifold_set_min_circular_edge_length(float length);
|
||||
void manifold_set_circular_segments(int number);
|
||||
|
||||
// Manifold Mesh Extraction
|
||||
|
||||
int manifold_meshgl_num_prop(ManifoldMeshGL *m);
|
||||
int manifold_meshgl_num_vert(ManifoldMeshGL *m);
|
||||
int manifold_meshgl_num_tri(ManifoldMeshGL *m);
|
||||
@@ -319,6 +365,7 @@ uint32_t *manifold_meshgl_face_id(void *mem, ManifoldMeshGL *m);
|
||||
float *manifold_meshgl_halfedge_tangent(void *mem, ManifoldMeshGL *m);
|
||||
|
||||
// memory size
|
||||
|
||||
size_t manifold_manifold_size();
|
||||
size_t manifold_manifold_vec_size();
|
||||
size_t manifold_cross_section_size();
|
||||
@@ -332,6 +379,7 @@ size_t manifold_rect_size();
|
||||
size_t manifold_curvature_size();
|
||||
|
||||
// destruction
|
||||
|
||||
void manifold_destruct_manifold(ManifoldManifold *m);
|
||||
void manifold_destruct_manifold_vec(ManifoldManifoldVec *ms);
|
||||
void manifold_destruct_cross_section(ManifoldCrossSection *m);
|
||||
@@ -343,6 +391,7 @@ void manifold_destruct_box(ManifoldBox *b);
|
||||
void manifold_destruct_rect(ManifoldRect *b);
|
||||
|
||||
// pointer free + destruction
|
||||
|
||||
void manifold_delete_manifold(ManifoldManifold *m);
|
||||
void manifold_delete_manifold_vec(ManifoldManifoldVec *ms);
|
||||
void manifold_delete_cross_section(ManifoldCrossSection *cs);
|
||||
@@ -354,6 +403,7 @@ void manifold_delete_box(ManifoldBox *b);
|
||||
void manifold_delete_rect(ManifoldRect *b);
|
||||
|
||||
// MeshIO / Export
|
||||
|
||||
#ifdef MANIFOLD_EXPORT
|
||||
ManifoldMaterial *manifold_material(void *mem);
|
||||
void manifold_material_set_roughness(ManifoldMaterial *mat, float roughness);
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
#include <conv.h>
|
||||
#include <cross_section.h>
|
||||
#include <manifold.h>
|
||||
@@ -24,13 +38,10 @@ 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));
|
||||
};
|
||||
auto pol = seq ? std::make_optional(ExecutionPolicy::Seq) : std::nullopt;
|
||||
auto mesh = LevelSet(fun, *from_c(bounds), edge_length, level, pol);
|
||||
auto mesh = LevelSet(fun, *from_c(bounds), edge_length, level, !seq);
|
||||
return to_c(new (mem) MeshGL(mesh));
|
||||
}
|
||||
ManifoldMeshGL *level_set_context(
|
||||
@@ -43,8 +54,7 @@ ManifoldMeshGL *level_set_context(
|
||||
std::function<float(glm::vec3)> fun = [sdf](glm::vec3 v) {
|
||||
return (sdf(v.x, v.y, v.z));
|
||||
};
|
||||
auto pol = seq ? std::make_optional(ExecutionPolicy::Seq) : std::nullopt;
|
||||
auto mesh = LevelSet(fun, *from_c(bounds), edge_length, level, pol);
|
||||
auto mesh = LevelSet(fun, *from_c(bounds), edge_length, level, !seq);
|
||||
return to_c(new (mem) MeshGL(mesh));
|
||||
}
|
||||
} // namespace
|
||||
@@ -190,6 +200,37 @@ 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));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_batch_hull(void *mem, ManifoldManifoldVec *ms) {
|
||||
auto hulled = Manifold::Hull(*from_c(ms));
|
||||
return to_c(new (mem) Manifold(hulled));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_hull_pts(void *mem, ManifoldVec3 *ps,
|
||||
size_t length) {
|
||||
std::vector<glm::vec3> vec(length);
|
||||
for (int i = 0; i < length; ++i) {
|
||||
vec[i] = {ps[i].x, ps[i].y, ps[i].z};
|
||||
}
|
||||
auto hulled = Manifold::Hull(vec);
|
||||
return to_c(new (mem) Manifold(hulled));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_translate(void *mem, ManifoldManifold *m, float x,
|
||||
float y, float z) {
|
||||
auto v = glm::vec3(x, y, z);
|
||||
@@ -264,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());
|
||||
}
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
// 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.
|
||||
|
||||
#include "meshIO.h"
|
||||
|
||||
#include <conv.h>
|
||||
|
||||
#include "include/types.h"
|
||||
#include "conv.h"
|
||||
#include "types.h"
|
||||
|
||||
// C <-> C++ conversions
|
||||
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
#include <conv.h>
|
||||
#include <manifold.h>
|
||||
#include <public.h>
|
||||
// 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.
|
||||
|
||||
#include "conv.h"
|
||||
#include "cross_section.h"
|
||||
#include "manifold.h"
|
||||
#include "public.h"
|
||||
#include "types.h"
|
||||
|
||||
ManifoldRect *manifold_rect(void *mem, float x1, float y1, float x2, float y2) {
|
||||
@@ -85,9 +98,3 @@ int manifold_rect_does_overlap_rect(ManifoldRect *a, ManifoldRect *r) {
|
||||
int manifold_rect_is_empty(ManifoldRect *r) { return from_c(r)->IsEmpty(); }
|
||||
|
||||
int manifold_rect_is_finite(ManifoldRect *r) { return from_c(r)->IsFinite(); }
|
||||
|
||||
ManifoldCrossSection *manifold_rect_as_cross_section(void *mem,
|
||||
ManifoldRect *r) {
|
||||
auto cs = from_c(r)->AsCrossSection();
|
||||
return to_c(new (mem) CrossSection(cs));
|
||||
}
|
||||
|
||||
@@ -15,10 +15,44 @@
|
||||
project(python)
|
||||
|
||||
add_subdirectory(third_party)
|
||||
pybind11_add_module(manifold3d manifold3d.cpp)
|
||||
target_link_libraries(manifold3d PRIVATE manifold)
|
||||
target_compile_options(manifold3d PRIVATE ${MANIFOLD_FLAGS})
|
||||
nanobind_add_module(
|
||||
manifold3d
|
||||
NB_STATIC STABLE_ABI LTO
|
||||
autogen_docstrings.inl
|
||||
manifold3d.cpp)
|
||||
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)
|
||||
target_include_directories(manifold3d
|
||||
PRIVATE ${PYBIND11_DIR}/include
|
||||
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
|
||||
LIBRARY DESTINATION ${SKBUILD_PLATLIB_DIR}
|
||||
COMPONENT bindings
|
||||
)
|
||||
else()
|
||||
install(
|
||||
TARGETS manifold3d
|
||||
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, FillRule
|
||||
from manifold3d import CrossSection, Manifold
|
||||
|
||||
|
||||
def run():
|
||||
@@ -10,29 +10,37 @@ 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=} differs from {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=} differs from {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=} differs from {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=} differs from {expected_bbox=}")
|
||||
raise Exception(
|
||||
f"observed_bbox={observed_bbox} differs from expected_bbox={expected_bbox}"
|
||||
)
|
||||
|
||||
return extruded_polygon
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
run()
|
||||
|
||||
@@ -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)
|
||||
@@ -36,11 +35,15 @@ if __name__ == "__main__":
|
||||
mesh = model.to_mesh()
|
||||
if export_models:
|
||||
if mesh.vert_properties.shape[1] > 3:
|
||||
vertices = np.hsplit(mesh.vert_properties, 3)[0]
|
||||
vertices = mesh.vert_properties[:, :3]
|
||||
colors = (mesh.vert_properties[:, 3:] * 255).astype(np.uint8)
|
||||
else:
|
||||
vertices = mesh.vert_properties
|
||||
meshOut = trimesh.Trimesh(vertices=vertices, faces=mesh.tri_verts)
|
||||
trimesh.exchange.export.export_mesh(meshOut, f'{f}.glb', 'glb')
|
||||
print(f'Exported model to {f}.glb')
|
||||
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")
|
||||
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]
|
||||
@@ -0,0 +1,43 @@
|
||||
from manifold3d import Manifold
|
||||
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]))
|
||||
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],
|
||||
]
|
||||
)
|
||||
for offset in offsets:
|
||||
fractal(holes, hole, w, position + offset, depth + 1, maxDepth)
|
||||
|
||||
|
||||
def posColors(pos, _):
|
||||
return [-p + 0.5 for p in pos] + [1.0]
|
||||
|
||||
|
||||
def run(n=1):
|
||||
result = Manifold.cube([1, 1, 1], True)
|
||||
holes = []
|
||||
fractal(holes, result, 1.0, np.array([0.0, 0.0]), 1, n)
|
||||
|
||||
hole = Manifold.compose(holes)
|
||||
|
||||
result -= hole
|
||||
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] * 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")
|
||||
@@ -1 +1 @@
|
||||
add_subdirectory(pybind11)
|
||||
add_subdirectory(nanobind)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
#include <manifold.h>
|
||||
#include <polygon.h>
|
||||
#include <sdf.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "cross_section.h"
|
||||
#include "helpers.cpp"
|
||||
#include "manifold.h"
|
||||
#include "polygon.h"
|
||||
#include "sdf.h"
|
||||
|
||||
using namespace emscripten;
|
||||
using namespace manifold;
|
||||
@@ -125,6 +125,10 @@ EMSCRIPTEN_BINDINGS(whatever) {
|
||||
function("_crossSectionUnionN", &cross_js::UnionN);
|
||||
function("_crossSectionDifferenceN", &cross_js::DifferenceN);
|
||||
function("_crossSectionIntersectionN", &cross_js::IntersectionN);
|
||||
function("_crossSectionCollectVertices", &cross_js::CollectVertices);
|
||||
function("_crossSectionHullPoints",
|
||||
select_overload<CrossSection(std::vector<glm::vec2>)>(
|
||||
&CrossSection::Hull));
|
||||
|
||||
class_<Manifold>("Manifold")
|
||||
.constructor(&man_js::FromMeshJS)
|
||||
@@ -134,8 +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)
|
||||
@@ -176,6 +184,10 @@ EMSCRIPTEN_BINDINGS(whatever) {
|
||||
function("_manifoldUnionN", &man_js::UnionN);
|
||||
function("_manifoldDifferenceN", &man_js::DifferenceN);
|
||||
function("_manifoldIntersectionN", &man_js::IntersectionN);
|
||||
function("_manifoldCollectVertices", &man_js::CollectVertices);
|
||||
function("_manifoldHullPoints",
|
||||
select_overload<Manifold(const std::vector<glm::vec3>&)>(
|
||||
&Manifold::Hull));
|
||||
|
||||
// Quality Globals
|
||||
function("setMinCircularAngle", &Quality::SetMinCircularAngle);
|
||||
|
||||
@@ -472,6 +472,36 @@ Module.setup = function() {
|
||||
Module.CrossSection.difference = crossSectionBatchbool('DifferenceN');
|
||||
Module.CrossSection.intersection = crossSectionBatchbool('IntersectionN');
|
||||
|
||||
function pushVec2(vec, ps) {
|
||||
toVec(vec, ps, p => {
|
||||
if (p instanceof Array) return {x: p[0], y: p[1]};
|
||||
return p;
|
||||
})
|
||||
}
|
||||
|
||||
Module.CrossSection.hull = function(...args) {
|
||||
if (args.length == 1) args = args[0];
|
||||
let pts = new Module.Vector_vec2();
|
||||
for (const cs of args) {
|
||||
if (cs instanceof CrossSectionCtor) {
|
||||
Module._crossSectionCollectVertices(pts, cs);
|
||||
} else if (
|
||||
cs instanceof Array && cs.length == 2 && typeof cs[0] == 'number') {
|
||||
pts.push_back({x: cs[0], y: cs[1]});
|
||||
} else if (cs.x) {
|
||||
pts.push_back(cs);
|
||||
} else {
|
||||
const wrap =
|
||||
((cs[0].length == 2 && typeof cs[0][0] == 'number') || cs[0].x);
|
||||
const polys = wrap ? [cs] : cs;
|
||||
for (const poly of polys) pushVec2(pts, poly);
|
||||
}
|
||||
}
|
||||
const result = Module._crossSectionHullPoints(pts);
|
||||
pts.delete();
|
||||
return result;
|
||||
};
|
||||
|
||||
Module.CrossSection.prototype = Object.create(CrossSectionCtor.prototype);
|
||||
|
||||
// Because the constructor and prototype are being replaced, instanceof will
|
||||
@@ -596,6 +626,33 @@ Module.setup = function() {
|
||||
return out;
|
||||
};
|
||||
|
||||
function pushVec3(vec, ps) {
|
||||
toVec(vec, ps, p => {
|
||||
if (p instanceof Array) return {x: p[0], y: p[1], z: p[2]};
|
||||
return p;
|
||||
})
|
||||
}
|
||||
|
||||
Module.Manifold.hull = function(...args) {
|
||||
if (args.length == 1) args = args[0];
|
||||
let pts = new Module.Vector_vec3();
|
||||
for (const m of args) {
|
||||
if (m instanceof ManifoldCtor) {
|
||||
Module._manifoldCollectVertices(pts, m);
|
||||
} else if (
|
||||
m instanceof Array && m.length == 3 && typeof m[0] == 'number') {
|
||||
pts.push_back({x: m[0], y: m[1], z: m[2]});
|
||||
} else if (m.x) {
|
||||
pts.push_back(m);
|
||||
} else {
|
||||
pushVec3(pts, m);
|
||||
}
|
||||
}
|
||||
const result = Module._manifoldHullPoints(pts);
|
||||
pts.delete();
|
||||
return result;
|
||||
};
|
||||
|
||||
Module.Manifold.prototype = Object.create(ManifoldCtor.prototype);
|
||||
|
||||
// Because the constructor and prototype are being replaced, instanceof will
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
declare module '@jscadui/3mf-export';
|
||||
@@ -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,18 +130,14 @@ model-viewer {
|
||||
|
||||
.header .margin {
|
||||
font-size: 16px;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
#file {
|
||||
width: 250px;
|
||||
margin: 18px 8px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 250px;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -150,6 +146,10 @@ model-viewer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.uparrow {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@@ -168,17 +168,57 @@ 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;
|
||||
position: absolute;
|
||||
width: 36px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
background-size: 20px;
|
||||
background-repeat: no-repeat;
|
||||
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;
|
||||
@@ -193,13 +233,24 @@ model-viewer {
|
||||
|
||||
.trash {
|
||||
background-image: url(/icons/trash.png);
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -212,7 +263,61 @@ model-viewer {
|
||||
background-color: rgb(255, 177, 177);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
#file-margin {
|
||||
position: relative;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#save {
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
#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%;
|
||||
@@ -220,5 +325,31 @@ model-viewer {
|
||||
|
||||
.col {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
#file-margin {
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: .2em 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header .margin {
|
||||
margin: 0.2em 16px;
|
||||
}
|
||||
|
||||
#button-container {
|
||||
flex-direction: row;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
#button-container .margin {
|
||||
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,22 +25,55 @@ 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 currentElement = document.querySelector('#current');
|
||||
const arrow = document.querySelector('.uparrow');
|
||||
const dropdown = document.querySelector('.dropdown');
|
||||
const currentFileElement = document.querySelector('#current');
|
||||
const fileArrow = document.querySelector('#file .uparrow');
|
||||
const fileDropdown = document.querySelector('#fileDropdown');
|
||||
const saveContainer = document.querySelector('#save');
|
||||
const saveDropdown = document.querySelector('#saveDropdown');
|
||||
const saveArrow = document.querySelector('#save .uparrow');
|
||||
|
||||
const hideDropdown = function() {
|
||||
dropdown.classList.remove('show');
|
||||
arrow.classList.remove('down');
|
||||
fileDropdown.classList.remove('show');
|
||||
saveDropdown.classList.remove('show');
|
||||
fileArrow.classList.remove('down');
|
||||
saveArrow.classList.remove('down');
|
||||
};
|
||||
const toggleDropdown = function(event) {
|
||||
const toggleFileDropdown = function(event) {
|
||||
event.stopPropagation();
|
||||
dropdown.classList.toggle('show');
|
||||
arrow.classList.toggle('down');
|
||||
fileDropdown.classList.toggle('show');
|
||||
fileArrow.classList.toggle('down');
|
||||
};
|
||||
fileButton.onclick = toggleDropdown;
|
||||
const toggleSaveDropdown = function(event) {
|
||||
event.stopPropagation();
|
||||
saveDropdown.classList.toggle('show');
|
||||
saveArrow.classList.toggle('down');
|
||||
};
|
||||
fileButton.onclick = toggleFileDropdown;
|
||||
saveArrow.parentElement.onclick = toggleSaveDropdown;
|
||||
document.body.onclick = hideDropdown;
|
||||
|
||||
const prefix = 'ManifoldCAD';
|
||||
@@ -62,7 +96,7 @@ function nthKey(n) {
|
||||
|
||||
function saveCurrent() {
|
||||
if (editor) {
|
||||
const currentName = currentElement.textContent;
|
||||
const currentName = currentFileElement.textContent;
|
||||
if (!exampleFunctions.get(currentName)) {
|
||||
setScript(currentName, editor.getValue());
|
||||
}
|
||||
@@ -77,11 +111,12 @@ let isExample = true;
|
||||
function switchTo(scriptName) {
|
||||
if (editor) {
|
||||
switching = true;
|
||||
currentElement.textContent = scriptName;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -135,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);
|
||||
@@ -148,8 +184,8 @@ function addEdit(button) {
|
||||
if (!input) return;
|
||||
const newName = uniqueName(input);
|
||||
label.textContent = newName;
|
||||
if (currentElement.textContent == oldName) {
|
||||
currentElement.textContent = newName;
|
||||
if (currentFileElement.textContent == oldName) {
|
||||
currentFileElement.textContent = newName;
|
||||
}
|
||||
removeScript(oldName);
|
||||
setScript(newName, code);
|
||||
@@ -182,7 +218,7 @@ function addEdit(button) {
|
||||
}, {once: true});
|
||||
} else if (performance.now() - lastClick > 500) {
|
||||
removeScript(label.textContent);
|
||||
if (currentElement.textContent == label.textContent) {
|
||||
if (currentFileElement.textContent == label.textContent) {
|
||||
switchTo('Intro');
|
||||
}
|
||||
const container = button.parentElement;
|
||||
@@ -192,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');
|
||||
@@ -214,7 +250,7 @@ function initializeRun() {
|
||||
if (autoExecute) {
|
||||
runButton.click();
|
||||
} else {
|
||||
poster.textContent = 'Auto-run disabled due to prior failure';
|
||||
poster.textContent = 'Auto-run disabled';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,10 +305,10 @@ require(['vs/editor/editor.main'], async function() {
|
||||
|
||||
for (const [name] of exampleFunctions) {
|
||||
const button = createDropdownItem(name);
|
||||
dropdown.appendChild(button.parentElement);
|
||||
fileDropdown.appendChild(button.parentElement);
|
||||
}
|
||||
|
||||
let currentName = currentElement.textContent;
|
||||
let currentName = currentFileElement.textContent;
|
||||
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const key = nthKey(i);
|
||||
@@ -287,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();
|
||||
@@ -297,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);
|
||||
}
|
||||
});
|
||||
@@ -311,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;
|
||||
@@ -346,8 +445,10 @@ function finishRun() {
|
||||
`Took ${(Math.round((t1 - t0) / 10) / 100).toLocaleString()} seconds`);
|
||||
}
|
||||
|
||||
const mv = document.querySelector('model-viewer');
|
||||
let objectURL = null;
|
||||
const output = {
|
||||
glbURL: null,
|
||||
threeMFURL: null
|
||||
};
|
||||
let manifoldWorker = null;
|
||||
|
||||
function createWorker() {
|
||||
@@ -370,10 +471,16 @@ function createWorker() {
|
||||
finishRun();
|
||||
runButton.disabled = true;
|
||||
|
||||
URL.revokeObjectURL(objectURL);
|
||||
objectURL = e.data.objectURL;
|
||||
mv.src = objectURL;
|
||||
if (objectURL == null) {
|
||||
if (output.threeMFURL != null) {
|
||||
URL.revokeObjectURL(output.threeMFURL);
|
||||
output.threeMFURL = null;
|
||||
}
|
||||
URL.revokeObjectURL(output.glbURL);
|
||||
output.glbURL = e.data.glbURL;
|
||||
output.threeMFURL = e.data.threeMFURL;
|
||||
threemfButton.disabled = output.threeMFURL == null;
|
||||
mv.src = output.glbURL;
|
||||
if (output.glbURL == null) {
|
||||
mv.showPoster();
|
||||
poster.textContent = 'Error';
|
||||
createWorker();
|
||||
@@ -411,10 +518,24 @@ runButton.onclick = function() {
|
||||
}
|
||||
};
|
||||
|
||||
const downloadButton = document.querySelector('#download');
|
||||
downloadButton.onclick = function() {
|
||||
const link = document.createElement('a');
|
||||
link.download = 'manifold.glb';
|
||||
link.href = objectURL;
|
||||
link.click();
|
||||
};
|
||||
function clickSave(saveButton, filename, outputName) {
|
||||
const container = saveButton.parentElement;
|
||||
return () => {
|
||||
const oldSave = saveContainer.firstElementChild;
|
||||
if (oldSave !== container) {
|
||||
saveDropdown.insertBefore(oldSave, saveDropdown.firstElementChild);
|
||||
saveContainer.insertBefore(container, saveDropdown);
|
||||
container.appendChild(saveArrow.parentElement);
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
link.download = filename;
|
||||
link.href = output[outputName];
|
||||
link.click();
|
||||
};
|
||||
}
|
||||
|
||||
const glbButton = document.querySelector('#glb');
|
||||
glbButton.onclick = clickSave(glbButton, 'manifold.glb', 'glbURL');
|
||||
|
||||
const threemfButton = document.querySelector('#threemf');
|
||||
threemfButton.onclick = clickSave(threemfButton, 'manifold.3mf', 'threeMFURL');
|
||||
|
||||
@@ -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,25 +39,48 @@
|
||||
<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" style="position: relative;">
|
||||
<button type="button" id="file" class="blue"><b class="uparrow"></b> <span
|
||||
<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">
|
||||
<div class="dropdown" id="fileDropdown">
|
||||
<button type="button" class="green item" id="new">New</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="compile" class="margin green" disabled><span>Run</span></button>
|
||||
<button type="button" id="download" class="blue margin">Save GLB</button>
|
||||
<div class="container" id="button-container">
|
||||
<button type="button" id="compile" class="margin green" disabled><span>Run</span></button>
|
||||
<div id="save" class="margin" style="position: relative; ">
|
||||
<div class="item">
|
||||
<button type="button" id="glb" class="blue item">Save GLB</button>
|
||||
<button type="button" class="icon"><b class="uparrow" style="right: 2px;"></b></button>
|
||||
</div>
|
||||
<div class="dropdown" id="saveDropdown">
|
||||
<div class="item">
|
||||
<button type="button" id="threemf" class="blue item">Save 3MF</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" style="flex: 2;">
|
||||
<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>
|
||||
@@ -72,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>
|
||||
@@ -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,23 +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",
|
||||
"@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';
|
||||
|
||||
@@ -39,7 +39,7 @@ function initialized(worker) {
|
||||
});
|
||||
}
|
||||
|
||||
let objectURL = null;
|
||||
let glbURL = null;
|
||||
|
||||
async function runExample(name) {
|
||||
const worker = new ManifoldWorker();
|
||||
@@ -55,26 +55,24 @@ async function runExample(name) {
|
||||
|
||||
worker.onmessage = async function(e) {
|
||||
try {
|
||||
URL.revokeObjectURL(objectURL);
|
||||
objectURL = e.data.objectURL;
|
||||
if (objectURL == null) {
|
||||
reject('no objectURL');
|
||||
URL.revokeObjectURL(glbURL);
|
||||
glbURL = e.data.glbURL;
|
||||
if (glbURL == null) {
|
||||
reject('no glbURL)');
|
||||
}
|
||||
const docIn = await io.read(objectURL);
|
||||
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,25 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Document, 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, 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
|
||||
@@ -42,29 +55,31 @@ io.registerExtensions(KHRONOS_EXTENSIONS);
|
||||
// manifold static methods (that return a new manifold)
|
||||
const manifoldStaticFunctions = [
|
||||
'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'compose',
|
||||
'union', 'difference', 'intersection', 'levelSet', 'smooth', 'ofMesh'
|
||||
'union', 'difference', 'intersection', 'levelSet', 'smooth', 'ofMesh', 'hull'
|
||||
];
|
||||
// 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'
|
||||
'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 = [
|
||||
'square', 'circle', 'union', 'difference', 'intersection', 'compose',
|
||||
'ofPolygons'
|
||||
'ofPolygons', 'hull'
|
||||
];
|
||||
// CrossSection member functions (that return a new cross-section)
|
||||
const crossSectionMemberFunctions = [
|
||||
'add', 'subtract', 'intersect', 'rectClip', 'decompose', 'transform',
|
||||
'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset'
|
||||
'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset', 'hull'
|
||||
];
|
||||
// top level functions that construct a new manifold/mesh
|
||||
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);
|
||||
|
||||
@@ -81,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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,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,
|
||||
@@ -142,6 +167,15 @@ const GHOST = {
|
||||
const nodes = new Array<GLTFNode>();
|
||||
const id2material = new Map<number, GLTFMaterial>();
|
||||
const materialCache = new Map<GLTFMaterial, Material>();
|
||||
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;
|
||||
@@ -150,14 +184,59 @@ function cleanup() {
|
||||
nodes.length = 0;
|
||||
id2material.clear();
|
||||
materialCache.clear();
|
||||
object2globalID.clear();
|
||||
manifold2morph.clear();
|
||||
nextGlobalID = 1;
|
||||
}
|
||||
|
||||
interface Morph {
|
||||
start?: (v: Vec3) => void;
|
||||
end?: (v: Vec3) => void;
|
||||
}
|
||||
|
||||
interface Mesh3MF {
|
||||
id: string;
|
||||
vertices: Float32Array;
|
||||
indices: Uint32Array;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface Child3MF {
|
||||
objectID: string;
|
||||
transform?: mat4;
|
||||
}
|
||||
|
||||
interface Component3MF {
|
||||
id: string;
|
||||
children: Array<Child3MF>;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface Header {
|
||||
unit?: 'micron'|'millimeter'|'centimeter'|'inch'|'foot'|'meter';
|
||||
title?: string;
|
||||
author?: string;
|
||||
description?: string;
|
||||
application?: string;
|
||||
creationDate?: string;
|
||||
license?: string;
|
||||
modificationDate?: string;
|
||||
}
|
||||
|
||||
interface To3MF {
|
||||
meshes: Array<Mesh3MF>;
|
||||
components: Array<Component3MF>;
|
||||
items: Array<Child3MF>;
|
||||
precision: number;
|
||||
header: Header;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -178,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());
|
||||
@@ -228,13 +327,13 @@ function log(...args: any[]) {
|
||||
}
|
||||
|
||||
self.onmessage = async (e) => {
|
||||
const content = e.data +
|
||||
'\nreturn exportGLB(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(
|
||||
'exportGLB', 'glMatrix', 'module', ...exposedFunctions, content);
|
||||
'exportModels', 'glMatrix', 'module', ...exposedFunctions, content);
|
||||
await f(
|
||||
exportGLB, glMatrix, module, //@ts-ignore
|
||||
exportModels, glMatrix, module, //@ts-ignore
|
||||
...exposedFunctions.map(name => module[name]));
|
||||
} catch (error: any) {
|
||||
console.log(error.toString());
|
||||
@@ -245,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;
|
||||
}
|
||||
|
||||
@@ -275,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();
|
||||
@@ -307,7 +527,7 @@ function getCachedMaterial(doc: Document, matDef: GLTFMaterial): Material {
|
||||
}
|
||||
|
||||
function addMesh(
|
||||
doc: Document, node: Node, manifold: Manifold,
|
||||
doc: Document, to3mf: To3MF, node: Node, manifold: Manifold,
|
||||
backupMaterial: GLTFMaterial = {}) {
|
||||
const numTri = manifold.numTri();
|
||||
if (numTri == 0) {
|
||||
@@ -330,36 +550,51 @@ 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 :
|
||||
new Float32Array(manifoldMesh.numVert * 3);
|
||||
if (manifoldMesh.numProp > 3) {
|
||||
for (let i = 0; i < manifoldMesh.numVert; ++i) {
|
||||
for (let j = 0; j < 3; ++j)
|
||||
vertices[i * 3 + j] =
|
||||
manifoldMesh.vertProperties[i * manifoldMesh.numProp + j];
|
||||
}
|
||||
}
|
||||
object2globalID.set(manifold, nextGlobalID);
|
||||
to3mf.meshes.push(
|
||||
{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);
|
||||
}
|
||||
}
|
||||
@@ -388,17 +623,19 @@ function cloneNodeNewMaterial(
|
||||
}
|
||||
|
||||
function createNodeFromCache(
|
||||
doc: Document, nodeDef: GLTFNode,
|
||||
doc: Document, to3MF: To3MF, nodeDef: GLTFNode,
|
||||
manifold2node: Map<Manifold, Map<GLTFMaterial, Node>>): Node {
|
||||
const node = createGLTFnode(doc, nodeDef);
|
||||
if (nodeDef.manifold != null) {
|
||||
const {manifold} = nodeDef;
|
||||
if (manifold != null) {
|
||||
setMorph(doc, node, manifold);
|
||||
const backupMaterial = getBackupMaterial(nodeDef);
|
||||
const cachedNodes = manifold2node.get(nodeDef.manifold);
|
||||
const cachedNodes = manifold2node.get(manifold);
|
||||
if (cachedNodes == null) {
|
||||
addMesh(doc, node, nodeDef.manifold, backupMaterial);
|
||||
addMesh(doc, to3MF, node, manifold, backupMaterial);
|
||||
const cache = new Map<GLTFMaterial, Node>();
|
||||
cache.set(backupMaterial, node);
|
||||
manifold2node.set(nodeDef.manifold, cache);
|
||||
manifold2node.set(manifold, cache);
|
||||
} else {
|
||||
const cachedNode = cachedNodes.get(backupMaterial);
|
||||
if (cachedNode == null) {
|
||||
@@ -412,10 +649,21 @@ function createNodeFromCache(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object2globalID.set(nodeDef, nextGlobalID);
|
||||
to3MF.components.push({
|
||||
id: `${nextGlobalID++}`,
|
||||
name: nodeDef.name,
|
||||
children:
|
||||
manifold == null ? [] : [{objectID: `${object2globalID.get(manifold)}`}]
|
||||
});
|
||||
return node;
|
||||
}
|
||||
|
||||
async function exportGLB(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;
|
||||
@@ -424,13 +672,53 @@ async function exportGLB(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: [],
|
||||
items: [],
|
||||
precision: 7,
|
||||
header: {
|
||||
unit: 'millimeter',
|
||||
title: 'ManifoldCAD.org model',
|
||||
description: 'ManifoldCAD.org model',
|
||||
application: 'ManifoldCAD.org',
|
||||
}
|
||||
} as To3MF;
|
||||
|
||||
if (nodes.length > 0) {
|
||||
const node2gltf = new Map<GLTFNode, Node>();
|
||||
const manifold2node = new Map<Manifold, Map<GLTFMaterial, Node>>();
|
||||
let leafNodes = 0;
|
||||
|
||||
for (const nodeDef of nodes) {
|
||||
node2gltf.set(nodeDef, createNodeFromCache(doc, nodeDef, manifold2node));
|
||||
node2gltf.set(
|
||||
nodeDef, createNodeFromCache(doc, to3mf, nodeDef, manifold2node));
|
||||
if (nodeDef.manifold) {
|
||||
++leafNodes;
|
||||
}
|
||||
@@ -438,10 +726,19 @@ async function exportGLB(manifold?: Manifold) {
|
||||
|
||||
for (const nodeDef of nodes) {
|
||||
const gltfNode = node2gltf.get(nodeDef)!;
|
||||
if (nodeDef.parent == null) {
|
||||
const child = {
|
||||
objectID: `${object2globalID.get(nodeDef)}`,
|
||||
transform: gltfNode.getMatrix()
|
||||
};
|
||||
const {parent} = nodeDef;
|
||||
if (parent == null) {
|
||||
wrapper.addChild(gltfNode);
|
||||
to3mf.items.push(child);
|
||||
} else {
|
||||
node2gltf.get(nodeDef.parent)!.addChild(gltfNode);
|
||||
node2gltf.get(parent)!.addChild(gltfNode);
|
||||
const parent3mf = to3mf.components.find(
|
||||
(comp) => comp.id == `${object2globalID.get(parent)}`)!;
|
||||
parent3mf.children.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,12 +750,36 @@ async function exportGLB(manifold?: Manifold) {
|
||||
return;
|
||||
}
|
||||
const node = doc.createNode();
|
||||
addMesh(doc, node, manifold);
|
||||
addMesh(doc, to3mf, node, manifold);
|
||||
wrapper.addChild(node);
|
||||
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 blob = new Blob([glb], {type: 'application/octet-stream'});
|
||||
self.postMessage({objectURL: URL.createObjectURL(blob)});
|
||||
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],
|
||||
{type: 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml'});
|
||||
|
||||
self.postMessage({
|
||||
glbURL: URL.createObjectURL(blobGLB),
|
||||
threeMFURL: URL.createObjectURL(blob3MF)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
#include <manifold.h>
|
||||
#include <polygon.h>
|
||||
#include <sdf.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "cross_section.h"
|
||||
#include "manifold.h"
|
||||
#include "polygon.h"
|
||||
#include "sdf.h"
|
||||
|
||||
using namespace emscripten;
|
||||
using namespace manifold;
|
||||
@@ -159,6 +159,11 @@ CrossSection Offset(CrossSection& cross_section, double delta, int join_type,
|
||||
: CrossSection::JoinType::Miter;
|
||||
return cross_section.Offset(delta, jt, miter_limit, arc_tolerance);
|
||||
}
|
||||
|
||||
void CollectVertices(std::vector<glm::vec2>& verts, const CrossSection& cs) {
|
||||
auto polys = cs.ToPolygons();
|
||||
for (auto poly : polys) verts.insert(verts.end(), poly.begin(), poly.end());
|
||||
}
|
||||
} // namespace cross_js
|
||||
|
||||
namespace man_js {
|
||||
@@ -219,4 +224,9 @@ std::vector<Manifold> SplitByPlane(Manifold& m, glm::vec3 normal,
|
||||
auto [a, b] = m.SplitByPlane(normal, originOffset);
|
||||
return {a, b};
|
||||
}
|
||||
|
||||
void CollectVertices(std::vector<glm::vec3>& verts, const Manifold& manifold) {
|
||||
Mesh mesh = manifold.GetMesh();
|
||||
verts.insert(verts.end(), mesh.vertPos.begin(), mesh.vertPos.end());
|
||||
}
|
||||
} // namespace man_js
|
||||
|
||||
@@ -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.
|
||||
@@ -111,6 +111,7 @@ export class CrossSection {
|
||||
* @param v The vector to add to every vertex.
|
||||
*/
|
||||
translate(v: Vec2): CrossSection;
|
||||
translate(x: number, y?: number): CrossSection;
|
||||
|
||||
/**
|
||||
* Applies a (Z-axis) rotation to the CrossSection, in degrees. This operation
|
||||
@@ -173,11 +174,6 @@ export class CrossSection {
|
||||
delta: number, joinType?: JoinType, miterLimit?: number,
|
||||
circularSegments?: number): CrossSection;
|
||||
|
||||
/**
|
||||
* Compute the convex hull of the contours in this CrossSection.
|
||||
*/
|
||||
hull(): CrossSection;
|
||||
|
||||
/**
|
||||
* Remove vertices from the contours in this CrossSection that are less than
|
||||
* the specified distance epsilon from an imaginary line that passes through
|
||||
@@ -246,6 +242,18 @@ export class CrossSection {
|
||||
*/
|
||||
static intersection(polygons: (CrossSection|Polygons)[]): CrossSection;
|
||||
|
||||
// Convex Hulls
|
||||
|
||||
/**
|
||||
* Compute the convex hull of the contours in this CrossSection.
|
||||
*/
|
||||
hull(): CrossSection;
|
||||
|
||||
/**
|
||||
* Compute the convex hull of all points in a list of polygons/cross-sections.
|
||||
*/
|
||||
static hull(polygons: (CrossSection|Polygons)[]): CrossSection;
|
||||
|
||||
// Topological Operations
|
||||
|
||||
/**
|
||||
@@ -507,6 +515,7 @@ export class Manifold {
|
||||
* @param v The vector to add to every vertex.
|
||||
*/
|
||||
translate(v: Vec3): Manifold;
|
||||
translate(x: number, y?: number, z?: number): Manifold;
|
||||
|
||||
/**
|
||||
* Applies an Euler angle rotation to the manifold, first about the X axis,
|
||||
@@ -519,6 +528,7 @@ export class Manifold {
|
||||
* @param v [X, Y, Z] rotation in degrees.
|
||||
*/
|
||||
rotate(v: Vec3): Manifold;
|
||||
rotate(x: number, y?: number, z?: number): Manifold;
|
||||
|
||||
/**
|
||||
* Scale this Manifold in space. This operation can be chained. Transforms are
|
||||
@@ -561,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
|
||||
@@ -675,6 +697,35 @@ 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
|
||||
|
||||
/**
|
||||
* Compute the convex hull of all points in this Manifold.
|
||||
*/
|
||||
hull(): Manifold;
|
||||
|
||||
/**
|
||||
* Compute the convex hull of all points contained within a set of Manifolds
|
||||
* and point vectors.
|
||||
*/
|
||||
static hull(points: (Manifold|Vec3)[]): Manifold;
|
||||
|
||||
// Topological Operations
|
||||
|
||||
/**
|
||||
@@ -815,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;
|
||||
@@ -838,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.1",
|
||||
"description": "Geometry library for topological robustness",
|
||||
"main": "manifold.js",
|
||||
"files": [
|
||||
|
||||
@@ -24,6 +24,13 @@ target_link_libraries(largeSceneTest manifold)
|
||||
target_compile_options(largeSceneTest PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(largeSceneTest PUBLIC cxx_std_17)
|
||||
|
||||
if(MANIFOLD_DEBUG)
|
||||
add_executable(minimizeTestcase minimize_testcase.cpp)
|
||||
target_link_libraries(minimizeTestcase polygon)
|
||||
target_compile_options(minimizeTestcase PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(minimizeTestcase PUBLIC cxx_std_17)
|
||||
endif()
|
||||
|
||||
if(MANIFOLD_EXPORT)
|
||||
add_executable(convertFile convert_file.cpp)
|
||||
target_link_libraries(convertFile manifold meshIO)
|
||||
@@ -34,9 +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_compile_definitions(perfTestCGAL PRIVATE CGAL_DEBUG)
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
|
||||
#include <execution>
|
||||
#endif
|
||||
|
||||
#include "polygon.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace manifold;
|
||||
|
||||
inline float cross(glm::vec2 p, glm::vec2 q) { return p.x * q.y - p.y * q.x; }
|
||||
|
||||
// return true if p intersects with q
|
||||
// note that we don't care about collinear, intersection in the ends etc.
|
||||
// https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
|
||||
inline bool intersect(glm::vec2 p0, glm::vec2 p1, glm::vec2 q0, glm::vec2 q1,
|
||||
float precision) {
|
||||
glm::vec2 r = p1 - p0;
|
||||
glm::vec2 s = q1 - q0;
|
||||
// allow some error in the intersection point
|
||||
float epsilon_r = 2.0 * precision / glm::length(r);
|
||||
float epsilon_s = 2.0 * precision / glm::length(s);
|
||||
float rxs = cross(r, s);
|
||||
// in case they are nearly collinear, ignore them...
|
||||
// this is to avoid treating degenerate triangles as intersecting
|
||||
//
|
||||
// note that this does not correspond to a fixed angle,
|
||||
// but seems to work well enough
|
||||
if (rxs < kTolerance && rxs > -kTolerance) {
|
||||
return false;
|
||||
}
|
||||
float u = cross(q0 - p0, r) / rxs;
|
||||
float t = cross(q0 - p0, s) / rxs;
|
||||
// we only care about intersection in the middle of both lines, excluding
|
||||
// their ends
|
||||
if (epsilon_r <= u && u <= (1 - epsilon_r) && //
|
||||
epsilon_s <= t && t <= (1 - epsilon_s)) {
|
||||
// there can be cases which r and s are close to collinear,
|
||||
// so even though the lines are not intersecting,
|
||||
// perturbation along r/s is not enough.
|
||||
//
|
||||
// in that case, apply perturbation perpendicular to r
|
||||
glm::vec2 r_orth = glm::normalize(glm::vec2(-r.y, r.x)) * precision;
|
||||
float u1 = cross(q0 - p0 + r_orth, r) / rxs;
|
||||
float u2 = cross(q0 - p0 - r_orth, r) / rxs;
|
||||
float t1 = cross(q0 - p0 + r_orth, s) / rxs;
|
||||
float t2 = cross(q0 - p0 - r_orth, s) / rxs;
|
||||
return 0 <= u1 && u1 <= 1 && //
|
||||
0 <= t1 && t1 <= 1 && //
|
||||
0 <= u2 && u2 <= 1 && //
|
||||
0 <= t2 && t2 <= 1;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// check if removing point j in polygon i will introduce self-intersection
|
||||
// this checks if the new edge j-1 <-> j+1 intersects with any other edges,
|
||||
// assuming the original polygon does not contain self-intersection
|
||||
bool safeToRemove(const Polygons &polys, int i, int j, float precision) {
|
||||
if (polys[i].size() == 3) return false;
|
||||
glm::vec2 prev = polys[i][j == 0 ? polys[i].size() - 1 : (j - 1)];
|
||||
glm::vec2 next = polys[i][j == (polys[i].size() - 1) ? 0 : (j + 1)];
|
||||
for (int k = 0; k < polys.size(); k++) {
|
||||
auto ll = [&](size_t l) {
|
||||
return l == (polys[k].size() - 1) ? 0 : (l + 1);
|
||||
};
|
||||
const glm::vec2 *polysk = polys[k].data();
|
||||
if (!std::all_of(
|
||||
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
|
||||
std::execution::par,
|
||||
#endif
|
||||
countAt((size_t)0), countAt(polys[k].size()), [=](size_t l) {
|
||||
if (i == k && (l == j || ll(l) == j)) return true;
|
||||
return !intersect(prev, next, polysk[l], polysk[ll(l)],
|
||||
precision);
|
||||
}))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::pair<int, int> findIndex(const Polygons &polys, int i) {
|
||||
int outer = 0;
|
||||
while (i >= polys[outer].size()) i -= polys[outer++].size();
|
||||
return std::make_pair(outer, i);
|
||||
}
|
||||
|
||||
void Dump(const Polygons &polys) {
|
||||
for (const SimplePolygon &poly : polys) {
|
||||
std::cout << "polys.push_back({" << std::setprecision(9) << std::endl;
|
||||
for (const glm::vec2 v : poly) {
|
||||
std::cout << " {" << v.x << ", " << v.y << "}, //" << std::endl;
|
||||
}
|
||||
std::cout << "});" << std::endl;
|
||||
}
|
||||
for (const SimplePolygon &poly : polys) {
|
||||
std::cout << "show(array([" << std::endl;
|
||||
for (const glm::vec2 v : poly) {
|
||||
std::cout << " [" << v.x << ", " << v.y << "]," << std::endl;
|
||||
}
|
||||
std::cout << "]))" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void DumpTriangulation(const Polygons &polys, float precision) {
|
||||
ExecutionParams oldParams = PolygonParams();
|
||||
manifold::PolygonParams().processOverlaps = true;
|
||||
std::vector<glm::ivec3> result = Triangulate(polys);
|
||||
PolygonParams() = oldParams;
|
||||
for (const glm::ivec3 &tri : result) {
|
||||
std::pair<int, int> xid = findIndex(polys, tri.x);
|
||||
std::pair<int, int> yid = findIndex(polys, tri.y);
|
||||
std::pair<int, int> zid = findIndex(polys, tri.z);
|
||||
glm::vec2 x = polys[xid.first][xid.second];
|
||||
glm::vec2 y = polys[yid.first][yid.second];
|
||||
glm::vec2 z = polys[zid.first][zid.second];
|
||||
printf("show(array([[%.7f, %.7f], [%.7f, %.7f], [%.7f, %.7f]]))\n", x.x,
|
||||
x.y, y.x, y.y, z.x, z.y);
|
||||
if (CCW(x, y, z, precision) == -1) {
|
||||
printf("^ not CCW\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// x direction ray
|
||||
// idk which direction, but it is a fixed direction...
|
||||
// https://stackoverflow.com/questions/11716268/point-in-polygon-algorithm
|
||||
bool rayHit(glm::vec2 point, glm::vec2 q0, glm::vec2 q1) {
|
||||
return ((q0.y >= point.y) != (q1.y >= point.y)) &&
|
||||
(point.x <= (q1.x - q0.x) * (point.y - q0.y) / (q1.y - q0.y) + q0.x);
|
||||
}
|
||||
|
||||
// get the list of polygons inside polygon i
|
||||
// this enumerates over the first point in all other polygons,
|
||||
// and check if they are inside the current polygon
|
||||
// this algorithm assumes that polygons are non-intersecting
|
||||
std::vector<int> getChildren(const Polygons &polys, int i) {
|
||||
std::vector<int> results;
|
||||
for (int j = 0; j < polys.size(); j++) {
|
||||
if (i == j) continue;
|
||||
glm::vec2 point = polys[j][0];
|
||||
auto k1 = [&](size_t k) {
|
||||
return k == (polys[i].size() - 1) ? 0 : (k + 1);
|
||||
};
|
||||
int count = std::count_if(
|
||||
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
|
||||
std::execution::par,
|
||||
#endif
|
||||
countAt((size_t)0), countAt(polys[i].size()),
|
||||
[&](size_t k) { return rayHit(point, polys[i][k], polys[i][k1(k)]); });
|
||||
if (count % 2) results.push_back(j);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// assuming polys is valid
|
||||
// we try to remove vertices from polys such that
|
||||
// 1. the updated polys is still valid (no overlapping edges, correct winding
|
||||
// direction)
|
||||
// 2. same error in triangulation (either geometryErr or overlapping triangles)
|
||||
void simplify(Polygons &polys, float precision = -1) {
|
||||
bool removedSomething = true;
|
||||
std::string msg;
|
||||
while (removedSomething) {
|
||||
int points = 0;
|
||||
for (const SimplePolygon &poly : polys) points += poly.size();
|
||||
std::cout << "trying to simplify " << points << " points" << std::endl;
|
||||
|
||||
removedSomething = false;
|
||||
// try to remove simple polygons
|
||||
for (int i = 0; i < polys.size(); i++) {
|
||||
std::vector<int> children = getChildren(polys, i);
|
||||
// if there are children, we can't remove it or we will mess up with
|
||||
// winding direction...
|
||||
if (!children.empty()) continue;
|
||||
SimplePolygon poly = std::move(polys[i]);
|
||||
polys.erase(polys.begin() + i);
|
||||
try {
|
||||
std::vector<glm::ivec3> result = Triangulate(polys, precision);
|
||||
polys.insert(polys.begin() + i, std::move(poly));
|
||||
} catch (geometryErr &e) {
|
||||
if (msg.size() > 0 && msg.compare(e.what()) != 0) {
|
||||
polys.insert(polys.begin() + i, std::move(poly));
|
||||
} else {
|
||||
removedSomething = true;
|
||||
msg = e.what();
|
||||
}
|
||||
} catch (topologyErr &e) {
|
||||
polys.insert(polys.begin() + i, std::move(poly));
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < polys.size(); i++) {
|
||||
std::vector<int> children = getChildren(polys, i);
|
||||
for (int j = 0; j < polys[i].size(); j++) {
|
||||
// removed vertex cannot change inclusion relation
|
||||
// we just check if the vertex
|
||||
// x: intersects with j0, j (original edge 1)
|
||||
// y: intersects with j, j1 (original edge 2)
|
||||
// z: intersects with j0, j1 (new edge)
|
||||
// if (x ^ y ^ z) is true, it means that the count % 2 is changed,
|
||||
// and we changed inclusion relation, so vertex j cannot be removed
|
||||
int j0 = j == 0 ? polys[i].size() - 1 : j - 1;
|
||||
int j1 = j == polys[i].size() - 1 ? 0 : j + 1;
|
||||
if (std::any_of(children.begin(), children.end(), [&](int k) {
|
||||
return rayHit(polys[k][0], polys[i][j0], polys[i][j]) ^
|
||||
rayHit(polys[k][0], polys[i][j], polys[i][j1]) ^
|
||||
rayHit(polys[k][0], polys[i][j0], polys[i][j1]);
|
||||
}))
|
||||
continue;
|
||||
|
||||
// removed vertex cannot introduce intersection
|
||||
if (!safeToRemove(polys, i, j, precision)) continue;
|
||||
glm::vec2 removed = polys[i][j];
|
||||
polys[i].erase(polys[i].begin() + j);
|
||||
try {
|
||||
std::vector<glm::ivec3> result = Triangulate(polys, precision);
|
||||
polys[i].insert(polys[i].begin() + j, removed);
|
||||
} catch (geometryErr &e) {
|
||||
if (msg.size() > 0 && msg.compare(e.what()) != 0) {
|
||||
polys[i].insert(polys[i].begin() + j, removed);
|
||||
} else {
|
||||
removedSomething = true;
|
||||
msg = e.what();
|
||||
}
|
||||
} catch (topologyErr &e) {
|
||||
polys[i].insert(polys[i].begin() + j, removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << "Error message for triangulation:" << std::endl
|
||||
<< msg.c_str() << std::endl;
|
||||
}
|
||||
|
||||
struct Edge {
|
||||
glm::vec2 west; // -x side vertex
|
||||
glm::vec2 east; // +x side vertex
|
||||
size_t i, j; // indices of origin vertex
|
||||
bool operator<(const Edge &other) const {
|
||||
return west.x < other.west.x ||
|
||||
(west.x == other.west.x && east.x < other.east.x);
|
||||
}
|
||||
Edge(const Polygons &polys, size_t i, size_t j) : i(i), j(j) {
|
||||
size_t j1 = j == polys[i].size() - 1 ? 0 : j + 1;
|
||||
east = polys[i][j];
|
||||
west = polys[i][j1];
|
||||
if (west.x > east.x) std::swap(east, west);
|
||||
}
|
||||
};
|
||||
|
||||
int isValid(const Polygons &polys, float precision = -1) {
|
||||
size_t numEdge = 0;
|
||||
for (const SimplePolygon &poly : polys) numEdge += poly.size();
|
||||
std::vector<Edge> edges;
|
||||
edges.reserve(numEdge);
|
||||
for (size_t i = 0; i < polys.size(); i++)
|
||||
for (size_t j = 0; j < polys[i].size(); j++)
|
||||
edges.push_back(Edge(polys, i, j));
|
||||
std::sort(edges.begin(), edges.end());
|
||||
// check intersection
|
||||
std::mutex mutex;
|
||||
std::vector<std::pair<Edge, Edge>> overlappingPairs;
|
||||
|
||||
std::for_each( //
|
||||
countAt((size_t)0), countAt(edges.size()), [&](size_t i) {
|
||||
// check all subsequent edges e' until e'.west.x > e.east.x
|
||||
// this ensures we checked all edges e' where
|
||||
// e.west.x < e'.west.x < e.east.x
|
||||
Edge e = edges[i];
|
||||
for (size_t j = i + 1; j < edges.size() && edges[j].west.x < e.east.x;
|
||||
j++) {
|
||||
if (intersect(e.west, e.east, edges[j].west, edges[j].east,
|
||||
precision)) {
|
||||
mutex.lock();
|
||||
overlappingPairs.push_back(std::make_pair(e, edges[j]));
|
||||
mutex.unlock();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!overlappingPairs.empty()) {
|
||||
std::cout << "found " << overlappingPairs.size() << " self-intersection"
|
||||
<< std::endl;
|
||||
for (const std::pair<Edge, Edge> &pairs : overlappingPairs) {
|
||||
std::cout << pairs.first.i << " " << pairs.first.j << "<->" //
|
||||
<< pairs.second.i << " " << pairs.second.j << std::endl;
|
||||
printf("array([[%.7f, %.7f], [%.7f, %.7f]])\n", pairs.first.west.x,
|
||||
pairs.first.west.y, pairs.first.east.x, pairs.first.east.y);
|
||||
printf("array([[%.7f, %.7f], [%.7f, %.7f]])\n", pairs.second.west.x,
|
||||
pairs.second.west.y, pairs.second.east.x, pairs.second.east.y);
|
||||
}
|
||||
} else {
|
||||
std::cout << "No self-intersection" << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
PolygonParams().intermediateChecks = true;
|
||||
PolygonParams().processOverlaps = false;
|
||||
PolygonParams().suppressErrors = true;
|
||||
|
||||
if (argc < 2) {
|
||||
std::cerr << "Usage: " << argv[0] << " <input file>" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
std::ifstream fin(argv[1]);
|
||||
std::string line;
|
||||
float precision = -1;
|
||||
Polygons polys;
|
||||
SimplePolygon poly;
|
||||
// search for precision first
|
||||
while (std::getline(fin, line)) {
|
||||
int index = line.find("Precision = ");
|
||||
if (index != std::string::npos) {
|
||||
std::istringstream iss(line.substr(index + 12));
|
||||
if (!(iss >> precision)) {
|
||||
std::cerr << "Error parsing precision" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::cout << "Precision = " << precision << std::endl;
|
||||
// fill polygons
|
||||
while (std::getline(fin, line)) {
|
||||
if (line.length() > 5 && line.substr(0, 5).compare(" {") == 0) {
|
||||
std::istringstream iss(line.substr(5));
|
||||
float x, y;
|
||||
if (!(iss >> x)) {
|
||||
std::cerr << "Error parsing coordinate:" << std::endl;
|
||||
std::cerr << "Line: " << line << std::endl;
|
||||
return 1;
|
||||
}
|
||||
iss.ignore(2);
|
||||
if (!(iss >> y)) {
|
||||
std::cerr << "Error parsing coordinate:" << std::endl;
|
||||
std::cerr << "Line: " << line << std::endl;
|
||||
return 1;
|
||||
}
|
||||
poly.push_back(glm::vec2(x, y));
|
||||
} else if (line.length() >= 3 && line.substr(0, 3).compare("});") == 0) {
|
||||
if (poly.size() < 3) {
|
||||
std::cerr << "Error: empty/invalid polygon" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
polys.push_back(std::move(poly));
|
||||
poly.clear();
|
||||
} else if (line.length() >= 5 && line.substr(0, 5).compare("array") == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (polys.size() == 0) {
|
||||
std::cerr << "Error: empty polygon" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (precision == -1) {
|
||||
float bound = 0;
|
||||
for (const SimplePolygon &poly : polys) {
|
||||
for (const glm::vec2 &pt : poly) {
|
||||
bound = glm::max(bound, glm::abs(pt.x));
|
||||
bound = glm::max(bound, glm::abs(pt.y));
|
||||
}
|
||||
}
|
||||
precision = bound * kTolerance;
|
||||
}
|
||||
|
||||
std::cout << "------------" << std::endl;
|
||||
|
||||
isValid(polys, precision);
|
||||
|
||||
simplify(polys, precision);
|
||||
|
||||
std::cout << "------------" << std::endl;
|
||||
std::cout << "Final polygon:" << std::endl;
|
||||
Dump(polys);
|
||||
|
||||
std::cout << "------------" << std::endl;
|
||||
std::cout << "Erroneous triangulation:" << std::endl;
|
||||
DumpTriangulation(polys, precision);
|
||||
return 0;
|
||||
}
|
||||
@@ -57,10 +57,9 @@ void manifoldToCGALSurfaceMesh(Manifold &manifold, TriangleMesh &cgalMesh) {
|
||||
int main(int argc, char **argv) {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Manifold sphere = Manifold::Sphere(1, (8 << i) * 4);
|
||||
Manifold sphere2 = sphere;
|
||||
sphere2.Translate(glm::vec3(0.5));
|
||||
Manifold sphere2 = sphere.Translate(glm::vec3(0.5));
|
||||
|
||||
TriangleMesh cgalSphere, cgalSphere2, cgalOut;
|
||||
TriangleMesh cgalSphere, cgalSphere2;
|
||||
manifoldToCGALSurfaceMesh(sphere, cgalSphere);
|
||||
manifoldToCGALSurfaceMesh(sphere2, cgalSphere2);
|
||||
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"clipper2-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1649676176,
|
||||
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
|
||||
"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": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -15,13 +35,30 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gtest-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1690989893,
|
||||
"narHash": "sha256-t0RchAHTJbuI5YW4uyBPykTvcjy90JW9AOPNjIhwh6U=",
|
||||
"owner": "google",
|
||||
"repo": "googletest",
|
||||
"rev": "f8d7d77c06936315286eb55f8de22cd23c188571",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "google",
|
||||
"ref": "v1.14.0",
|
||||
"repo": "googletest",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1689008574,
|
||||
"narHash": "sha256-VFMgyHDiqsGDkRg73alv6OdHJAqhybryWHv77bSCGIw=",
|
||||
"lastModified": 1704722960,
|
||||
"narHash": "sha256-mKGJ3sPsT6//s+Knglai5YflJUF2DGj7Ai6Ynopz0kI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4a729ce4b1fe5ec4fffc71c67c96aa5184ebb462",
|
||||
"rev": "317484b1ead87b9c1b8ac5261a8d2dd748a0492d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -32,8 +69,44 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"clipper2-src": "clipper2-src",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"gtest-src": "gtest-src",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"thrust-src": "thrust-src"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
{
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
inputs.gtest-src = {
|
||||
url = "github:google/googletest/v1.14.0";
|
||||
flake = false;
|
||||
};
|
||||
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
|
||||
@@ -10,97 +21,87 @@
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
clipper2 = pkgs.clipper2.overrideAttrs (_: _: {
|
||||
version = "1.3.0";
|
||||
src = clipper2-src;
|
||||
});
|
||||
manifold =
|
||||
{ parallel-backend ? "none"
|
||||
, cuda-support ? false
|
||||
, doCheck ? true
|
||||
, build-tools ? [ ]
|
||||
, ...
|
||||
}: pkgs.stdenv.mkDerivation {
|
||||
inherit doCheck;
|
||||
pname =
|
||||
if cuda-support then
|
||||
"manifold-${parallel-backend}-cuda"
|
||||
else
|
||||
"manifold-${parallel-backend}";
|
||||
version = "beta";
|
||||
pname = "manifold-${parallel-backend}";
|
||||
version = "2.4.1";
|
||||
src = self;
|
||||
nativeBuildInputs = (with pkgs; [ cmake (python39.withPackages (ps: with ps; [ trimesh ])) ]) ++ build-tools ++
|
||||
(if cuda-support then with pkgs.cudaPackages; [ cuda_nvcc cuda_cudart cuda_cccl pkgs.addOpenGLRunpath ] else [ ]);
|
||||
nativeBuildInputs = (with pkgs; [
|
||||
cmake
|
||||
ninja
|
||||
(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}"
|
||||
"-DMANIFOLD_USE_CUDA=${if cuda-support then "ON" else "OFF"}"
|
||||
];
|
||||
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
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp src/manifold/libmanifold.a $out/
|
||||
cp extras/perfTest $out
|
||||
cp bindings/python/manifold3d* $out
|
||||
'';
|
||||
};
|
||||
parallelBackends = [
|
||||
{ parallel-backend = "none"; }
|
||||
{
|
||||
parallel-backend = "omp";
|
||||
build-tools = [ pkgs.llvmPackages_13.openmp ];
|
||||
}
|
||||
{
|
||||
parallel-backend = "tbb";
|
||||
build-tools = with pkgs; [ tbb pkg-config ];
|
||||
build-tools = with pkgs; [ tbb_2021_8 pkg-config ];
|
||||
}
|
||||
];
|
||||
buildMatrix = with pkgs; with lib; lists.flatten (map
|
||||
(env: map
|
||||
(x: x // env)
|
||||
parallelBackends) [
|
||||
{ cuda-support = false; }
|
||||
{
|
||||
cuda-support = true;
|
||||
}
|
||||
]);
|
||||
devShell = { additional ? [ ] }: pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cmake
|
||||
llvmPackages_13.openmp
|
||||
clang-tools
|
||||
clang_13
|
||||
emscripten
|
||||
tbb
|
||||
lcov
|
||||
] ++ additional;
|
||||
};
|
||||
in
|
||||
{
|
||||
packages = (builtins.listToAttrs
|
||||
(map
|
||||
(x: {
|
||||
name = "manifold-" + x.parallel-backend + (if
|
||||
x.cuda-support then "-cuda" else "");
|
||||
name = "manifold-" + x.parallel-backend;
|
||||
value = manifold x;
|
||||
})
|
||||
buildMatrix)) // {
|
||||
parallelBackends)) // {
|
||||
manifold-js = pkgs.buildEmscriptenPackage {
|
||||
name = "manifold-js";
|
||||
version = "beta";
|
||||
version = "2.4.1";
|
||||
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
|
||||
cd build
|
||||
mkdir cache
|
||||
export EM_CACHE=$(pwd)/cache
|
||||
emcmake cmake -DCMAKE_BUILD_TYPE=Release ..
|
||||
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
|
||||
-DFETCHCONTENT_SOURCE_DIR_GLM=${pkgs.glm.src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=${gtest-src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=../clipper2 ..
|
||||
'';
|
||||
buildPhase = ''
|
||||
emmake make -j''${NIX_BUILD_CORES}
|
||||
@@ -116,13 +117,47 @@
|
||||
cp {extras,wasm}/*.wasm $out/
|
||||
'';
|
||||
};
|
||||
# but how should we make it work with other python versions?
|
||||
manifold3d = with pkgs.python3Packages; buildPythonPackage {
|
||||
pname = "manifold3d";
|
||||
version = "2.4.1";
|
||||
src = self;
|
||||
propagatedBuildInputs = [
|
||||
numpy
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
tbb_2021_8
|
||||
glm
|
||||
clipper2
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
ninja
|
||||
setuptools
|
||||
scikit-build-core
|
||||
pyproject-metadata
|
||||
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 { };
|
||||
devShells.cuda = devShell {
|
||||
additional = with pkgs.cudaPackages; [
|
||||
cuda_nvcc
|
||||
cuda_cudart
|
||||
cuda_cccl
|
||||
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
|
||||
@@ -29,3 +27,6 @@ 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} 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
|
||||
@@ -0,0 +1,72 @@
|
||||
[project]
|
||||
name = "manifold3d"
|
||||
version = "2.4.1"
|
||||
authors = [
|
||||
{ name="Emmett Lalish", email="elalish@gmail.com" },
|
||||
]
|
||||
description = "Library for geometric robustness"
|
||||
readme = "README.md"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: C++",
|
||||
"Topic :: Multimedia :: Graphics :: 3D Modeling",
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
|
||||
dependencies = [
|
||||
"numpy; python_version<'3.12'",
|
||||
"numpy>=1.26.0b1; python_version>='3.12'"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/elalish/manifold"
|
||||
"Bug Tracker" = "https://github.com/elalish/manifold/issues"
|
||||
|
||||
[build-system]
|
||||
requires = ["scikit-build-core"]
|
||||
build-backend = "scikit_build_core.build"
|
||||
|
||||
[tool.scikit-build]
|
||||
cmake.minimum-version = "3.18"
|
||||
sdist.exclude = [
|
||||
".github",
|
||||
"bindings/c",
|
||||
"bindings/wasm",
|
||||
"docs",
|
||||
"extras",
|
||||
"meshIO",
|
||||
"samples",
|
||||
"test",
|
||||
"oneTBB", # we may have this when we build with cibuildwheel
|
||||
"bindings/python/third_party/nanobind/tests/",
|
||||
"bindings/python/third_party/nanobind/docs/",
|
||||
]
|
||||
wheel.packages = ["manifold3d"]
|
||||
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", "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"
|
||||
musllinux-x86_64-image = "musllinux_1_2"
|
||||
skip = ["*-win32", "*_i686", "pp*", "*-musllinux*"]
|
||||
|
||||
# only clone TBB once
|
||||
before-all = "git clone --depth 1 --branch v2021.10.0 https://github.com/oneapi-src/oneTBB.git"
|
||||
[tool.cibuildwheel.config-settings]
|
||||
"cmake.define.FETCHCONTENT_SOURCE_DIR_TBB" = "./oneTBB"
|
||||
"cmake.define.FETCHCONTENT_UPDATES_DISCONNECTED" = "ON"
|
||||
|
||||
[tool.cibuildwheel.macos]
|
||||
archs = ["x86_64", "arm64"]
|
||||
environment = "MACOSX_DEPLOYMENT_TARGET=10.14"
|
||||
test-skip = "*-macosx_arm64 *-macosx_universal2:arm64"
|
||||
|
||||
[tool.cibuildwheel.windows]
|
||||
before-build = "pip install delvewheel"
|
||||
repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}"
|
||||
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
testpaths =
|
||||
bindings/python/examples
|
||||
@@ -14,37 +14,17 @@
|
||||
|
||||
project(samples)
|
||||
|
||||
add_library(samples src/bracelet.cpp src/knot.cpp src/menger_sponge.cpp src/rounded_frame.cpp src/scallop.cpp src/tet_puzzle.cpp)
|
||||
add_library(samples src/bracelet.cpp src/knot.cpp src/menger_sponge.cpp
|
||||
src/rounded_frame.cpp src/scallop.cpp src/tet_puzzle.cpp
|
||||
src/gyroid_module.cpp src/condensed_matter.cpp)
|
||||
|
||||
target_include_directories(samples
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(samples
|
||||
PUBLIC manifold
|
||||
PUBLIC manifold sdf
|
||||
)
|
||||
|
||||
target_compile_options(samples PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(samples PUBLIC cxx_std_17)
|
||||
|
||||
project(samplesGPU)
|
||||
|
||||
add_library(samplesGPU src/gyroid_module.cpp)
|
||||
|
||||
if(MANIFOLD_USE_CUDA)
|
||||
set_source_files_properties(src/gyroid_module.cpp PROPERTIES LANGUAGE CUDA)
|
||||
set_property(TARGET samplesGPU PROPERTY CUDA_ARCHITECTURES 61)
|
||||
endif()
|
||||
|
||||
target_include_directories(samplesGPU
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(samplesGPU
|
||||
PUBLIC manifold sdf
|
||||
)
|
||||
|
||||
target_compile_options(samplesGPU PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(samplesGPU
|
||||
PUBLIC cxx_std_17
|
||||
)
|
||||
|
||||
@@ -51,5 +51,7 @@ Manifold TetPuzzle(float edgeLength, float gap, int nDivisions);
|
||||
Manifold Scallop();
|
||||
|
||||
Manifold GyroidModule(float size = 20, int n = 20);
|
||||
|
||||
Manifold CondensedMatter(int fn = 16);
|
||||
/** @} */ // end of Samples
|
||||
} // namespace manifold
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB |
@@ -0,0 +1,149 @@
|
||||
// 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.
|
||||
// From
|
||||
// https://gist.github.com/ochafik/70a6b15e982b7ccd5a79ff9afd99dbcf#file-condensed-matter-scad
|
||||
|
||||
#include "samples.h"
|
||||
|
||||
namespace {
|
||||
using namespace manifold;
|
||||
using namespace glm;
|
||||
|
||||
constexpr float AtomicRadiusN2 = 0.65;
|
||||
constexpr float BondPairN2 = 1.197;
|
||||
constexpr float AtomicRadiusSi = 1.1;
|
||||
constexpr float LatticeCellSizeSi = 5.4309;
|
||||
constexpr float fccOffset = 0.25;
|
||||
constexpr float AtomicRadiusC = 0.7;
|
||||
constexpr float LatticeCellSizeC = 3.65;
|
||||
constexpr float cellLenA = 2.464;
|
||||
constexpr float cellLenB = cellLenA;
|
||||
constexpr float cellLenC = 6.711;
|
||||
constexpr float cellAngleA = 90;
|
||||
constexpr float cellAngleB = cellAngleA;
|
||||
constexpr float cellAngleC = 120;
|
||||
constexpr float LayerSeperationC = 3.364;
|
||||
|
||||
Manifold bond(int fn, vec3 p1 = {0, 0, 0}, vec3 p2 = {1, 1, 1}, float ar1 = 1.0,
|
||||
float ar2 = 2.0) {
|
||||
float cyR = std::min(ar1, ar2) / 5.0;
|
||||
float dist = length(p1 - p2);
|
||||
vec3 cyC = (p1 + p2) / 2.0f;
|
||||
float beta = degrees(acos((p1.z - p2.z) / dist));
|
||||
float gamma = degrees(atan2(p1.y - p2.y, p1.x - p2.x));
|
||||
vec3 rot = {0.0, beta, gamma};
|
||||
return Manifold::Cylinder(dist, cyR, -1, fn, true)
|
||||
.Rotate(rot.x, rot.y, rot.z)
|
||||
.Translate(cyC);
|
||||
}
|
||||
|
||||
Manifold bondPair(int fn, float d = 0.0, float ar = 1.0) {
|
||||
float axD = pow(d, 1.0 / 3.0);
|
||||
vec3 p1 = {+axD, -axD, -axD};
|
||||
vec3 p2 = {-axD, +axD, +axD};
|
||||
Manifold sphere = Manifold::Sphere(ar, fn);
|
||||
return sphere.Translate(p1) + sphere.Translate(p2) + bond(fn, p1, p2, ar, ar);
|
||||
}
|
||||
|
||||
Manifold hexagonalClosePacked(int fn, vec3 dst = {1.0, 1.0, 1.0},
|
||||
float ar = 1.0) {
|
||||
std::vector<Manifold> parts;
|
||||
vec3 p1 = {0, 0, 0};
|
||||
parts.push_back(Manifold::Sphere(ar, fn));
|
||||
float baseAg = 30;
|
||||
vec3 ag = {baseAg, baseAg + 120, baseAg + 240};
|
||||
vec3 points[] = {{cosd(ag.x) * dst.x, sind(ag.x) * dst.x, 0},
|
||||
{cosd(ag.y) * dst.y, sind(ag.y) * dst.y, 0},
|
||||
{cosd(ag.z) * dst.z, sind(ag.z) * dst.z, 0}};
|
||||
for (vec3 p2 : points) {
|
||||
parts.push_back(Manifold::Sphere(ar, fn).Translate(p2));
|
||||
parts.push_back(bond(fn, p1, p2, ar, ar));
|
||||
}
|
||||
return Manifold::BatchBoolean(parts, OpType::Add);
|
||||
}
|
||||
|
||||
Manifold fccDiamond(int fn, float ar = 1.0, float unitCell = 2.0,
|
||||
float fccOffset = 0.25) {
|
||||
std::vector<Manifold> parts;
|
||||
float huc = unitCell / 2.0;
|
||||
float od = fccOffset * unitCell;
|
||||
vec3 interstitial[] = {
|
||||
{+od, +od, +od}, {+od, -od, -od}, {-od, +od, -od}, {-od, -od, +od}};
|
||||
vec3 corners[] = {{+huc, +huc, +huc},
|
||||
{+huc, -huc, -huc},
|
||||
{-huc, +huc, -huc},
|
||||
{-huc, -huc, +huc}};
|
||||
vec3 fcc[] = {{+huc, 0, 0}, {-huc, 0, 0}, {0, +huc, 0},
|
||||
{0, -huc, 0}, {0, 0, +huc}, {0, 0, -huc}};
|
||||
for (auto p : corners) parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
|
||||
|
||||
for (auto p : fcc) parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
|
||||
for (auto p : interstitial)
|
||||
parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
|
||||
|
||||
vec3 bonds[][2] = {{interstitial[0], corners[0]}, {interstitial[0], fcc[0]},
|
||||
{interstitial[0], fcc[2]}, {interstitial[0], fcc[4]},
|
||||
{interstitial[1], corners[1]}, {interstitial[1], fcc[0]},
|
||||
{interstitial[1], fcc[3]}, {interstitial[1], fcc[5]},
|
||||
{interstitial[2], corners[2]}, {interstitial[2], fcc[1]},
|
||||
{interstitial[2], fcc[2]}, {interstitial[2], fcc[5]},
|
||||
{interstitial[3], corners[3]}, {interstitial[3], fcc[1]},
|
||||
{interstitial[3], fcc[3]}, {interstitial[3], fcc[4]}};
|
||||
for (auto b : bonds) parts.push_back(bond(fn, b[0], b[1], ar, ar));
|
||||
|
||||
return Manifold::BatchBoolean(parts, OpType::Add);
|
||||
}
|
||||
|
||||
Manifold SiCell(int fn, float x = 1.0, float y = 1.0, float z = 1.0) {
|
||||
return fccDiamond(fn, AtomicRadiusSi, LatticeCellSizeSi, fccOffset)
|
||||
.Translate({LatticeCellSizeSi * x, LatticeCellSizeSi * y,
|
||||
LatticeCellSizeSi * z});
|
||||
}
|
||||
|
||||
Manifold SiN2Cell(int fn, float x = 1.0, float y = 1.0, float z = 1.0) {
|
||||
float n2Offset = LatticeCellSizeSi / 8;
|
||||
return bondPair(fn, BondPairN2, AtomicRadiusN2)
|
||||
.Translate({LatticeCellSizeSi * x - n2Offset,
|
||||
LatticeCellSizeSi * y + n2Offset,
|
||||
LatticeCellSizeSi * z + n2Offset}) +
|
||||
SiCell(fn, x, y, z);
|
||||
}
|
||||
|
||||
Manifold GraphiteCell(int fn, vec3 xyz = {1.0, 1.0, 1.0}) {
|
||||
vec3 loc = {(cellLenA * xyz.x * cosd(30) * 2),
|
||||
((cellLenB * sind(30)) + cellLenC) * xyz.y, xyz.z};
|
||||
return hexagonalClosePacked(fn, {cellLenA, cellLenB, cellLenC}, AtomicRadiusC)
|
||||
.Translate(loc);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace manifold {
|
||||
Manifold CondensedMatter(int fn) {
|
||||
std::vector<Manifold> parts;
|
||||
float siOffset = 3.0 * LatticeCellSizeSi / 8.0;
|
||||
for (int x = -3; x <= 3; x++)
|
||||
for (int y = -1; y <= 2; y++)
|
||||
parts.push_back(
|
||||
GraphiteCell(fn, {x + (y % 2 == 0 ? 0.0 : 0.5), y,
|
||||
LayerSeperationC * 0.5 + LatticeCellSizeSi * 1.5})
|
||||
.Translate({0, -siOffset, 0})
|
||||
.Rotate(0, 0, 45));
|
||||
|
||||
float xyPlane[] = {-2, -1, 0, +1, +2};
|
||||
for (float x : xyPlane)
|
||||
for (float y : xyPlane) parts.push_back(SiN2Cell(fn, x, y, 1));
|
||||
return Manifold::BatchBoolean(parts, OpType::Add);
|
||||
}
|
||||
} // namespace manifold
|
||||
@@ -12,13 +12,15 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "manifold.h"
|
||||
#include "samples.h"
|
||||
#include "sdf.h"
|
||||
|
||||
namespace {
|
||||
using namespace manifold;
|
||||
|
||||
struct Gyroid {
|
||||
__host__ __device__ float operator()(glm::vec3 p) const {
|
||||
float operator()(glm::vec3 p) const {
|
||||
p -= glm::pi<float>() / 4;
|
||||
return cos(p.x) * sin(p.y) + cos(p.y) * sin(p.z) + cos(p.z) * sin(p.x);
|
||||
}
|
||||
@@ -54,4 +56,4 @@ Manifold GyroidModule(float size, int n) {
|
||||
|
||||
return result.Rotate(-45, 0, 90).Translate({0, 0, size / glm::sqrt(2.0f)});
|
||||
}
|
||||
} // namespace manifold
|
||||
} // namespace manifold
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
add_subdirectory(third_party)
|
||||
# do not add third_party installations
|
||||
add_subdirectory(third_party EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(utilities)
|
||||
add_subdirectory(collider)
|
||||
add_subdirectory(cross_section)
|
||||
|
||||
@@ -16,15 +16,14 @@ project (collider)
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
|
||||
|
||||
if(MANIFOLD_USE_CUDA)
|
||||
set_source_files_properties(${SOURCE_FILES} PROPERTIES LANGUAGE CUDA)
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
|
||||
endif()
|
||||
|
||||
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)
|
||||
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/collider.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#pragma once
|
||||
#include "public.h"
|
||||
#include "sparse.h"
|
||||
#include "vec_dh.h"
|
||||
#include "vec.h"
|
||||
|
||||
namespace manifold {
|
||||
|
||||
@@ -23,20 +23,24 @@ namespace manifold {
|
||||
class Collider {
|
||||
public:
|
||||
Collider() {}
|
||||
Collider(const VecDH<Box>& leafBB, const VecDH<uint32_t>& leafMorton);
|
||||
Collider(const VecView<const Box>& leafBB,
|
||||
const VecView<const uint32_t>& leafMorton);
|
||||
bool Transform(glm::mat4x3);
|
||||
void UpdateBoxes(const VecDH<Box>& leafBB);
|
||||
template <const bool selfCollision = false, typename T>
|
||||
SparseIndices Collisions(const VecDH<T>& queriesIn) const;
|
||||
void UpdateBoxes(const VecView<const Box>& leafBB);
|
||||
template <const bool selfCollision = false, const bool inverted = false,
|
||||
typename T>
|
||||
SparseIndices Collisions(const VecView<const T>& queriesIn) const;
|
||||
|
||||
private:
|
||||
VecDH<Box> nodeBBox_;
|
||||
VecDH<int> nodeParent_;
|
||||
Vec<Box> nodeBBox_;
|
||||
Vec<int> nodeParent_;
|
||||
// even nodes are leaves, odd nodes are internal, root is 1
|
||||
VecDH<thrust::pair<int, int>> internalChildren_;
|
||||
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
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
// Adjustable parameters
|
||||
constexpr int kInitialLength = 128;
|
||||
constexpr int kLengthMultiple = 4;
|
||||
constexpr int kSequentialThreshold = 512;
|
||||
// Fundamental constants
|
||||
constexpr int kRoot = 1;
|
||||
|
||||
@@ -59,36 +60,30 @@ uint32_t __inline clz(uint32_t value) {
|
||||
namespace {
|
||||
using namespace manifold;
|
||||
|
||||
__host__ __device__ bool IsLeaf(int node) { return node % 2 == 0; }
|
||||
__host__ __device__ bool IsInternal(int node) { return node % 2 == 1; }
|
||||
__host__ __device__ int Node2Internal(int node) { return (node - 1) / 2; }
|
||||
__host__ __device__ int Internal2Node(int internal) { return internal * 2 + 1; }
|
||||
__host__ __device__ int Node2Leaf(int node) { return node / 2; }
|
||||
__host__ __device__ int Leaf2Node(int leaf) { return leaf * 2; }
|
||||
bool IsLeaf(int node) { return node % 2 == 0; }
|
||||
bool IsInternal(int node) { return node % 2 == 1; }
|
||||
int Node2Internal(int node) { return (node - 1) / 2; }
|
||||
int Internal2Node(int internal) { return internal * 2 + 1; }
|
||||
int Node2Leaf(int node) { return node / 2; }
|
||||
int Leaf2Node(int leaf) { return leaf * 2; }
|
||||
|
||||
struct CreateRadixTree {
|
||||
int* nodeParent_;
|
||||
thrust::pair<int, int>* internalChildren_;
|
||||
const VecDc<uint32_t> leafMorton_;
|
||||
VecView<int> nodeParent_;
|
||||
VecView<thrust::pair<int, int>> internalChildren_;
|
||||
const VecView<const uint32_t> leafMorton_;
|
||||
|
||||
__host__ __device__ int PrefixLength(uint32_t a, uint32_t b) const {
|
||||
int PrefixLength(uint32_t a, uint32_t b) const {
|
||||
// count-leading-zeros is used to find the number of identical highest-order
|
||||
// bits
|
||||
#ifdef __CUDA_ARCH__
|
||||
return __clz(a ^ b);
|
||||
#else
|
||||
|
||||
#ifdef _MSC_VER
|
||||
// return __lzcnt(a ^ b);
|
||||
return clz(a ^ b);
|
||||
#else
|
||||
return __builtin_clz(a ^ b);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
__host__ __device__ int PrefixLength(int i, int j) const {
|
||||
int PrefixLength(int i, int j) const {
|
||||
if (j < 0 || j >= leafMorton_.size()) {
|
||||
return -1;
|
||||
} else {
|
||||
@@ -103,7 +98,7 @@ struct CreateRadixTree {
|
||||
}
|
||||
}
|
||||
|
||||
__host__ __device__ int RangeEnd(int i) const {
|
||||
int RangeEnd(int i) const {
|
||||
// Determine direction of range (+1 or -1)
|
||||
int dir = PrefixLength(i, i + 1) - PrefixLength(i, i - 1);
|
||||
dir = (dir > 0) - (dir < 0);
|
||||
@@ -121,7 +116,7 @@ struct CreateRadixTree {
|
||||
return i + dir * length;
|
||||
}
|
||||
|
||||
__host__ __device__ int FindSplit(int first, int last) const {
|
||||
int FindSplit(int first, int last) const {
|
||||
int commonPrefix = PrefixLength(first, last);
|
||||
// Find the furthest object that shares more than commonPrefix bits with the
|
||||
// first one, using binary search.
|
||||
@@ -138,7 +133,7 @@ struct CreateRadixTree {
|
||||
return split;
|
||||
}
|
||||
|
||||
__host__ __device__ void operator()(int internal) {
|
||||
void operator()(int internal) {
|
||||
int first = internal;
|
||||
// Find the range of objects with a common prefix
|
||||
int last = RangeEnd(first);
|
||||
@@ -157,36 +152,27 @@ struct CreateRadixTree {
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T, const bool allocateOnly, const bool selfCollision>
|
||||
template <typename T, const bool selfCollision, typename Recorder>
|
||||
struct FindCollisions {
|
||||
thrust::pair<int*, int*> queryTri_;
|
||||
int* counts;
|
||||
const Box* nodeBBox_;
|
||||
const thrust::pair<int, int>* internalChildren_;
|
||||
VecView<const Box> nodeBBox_;
|
||||
VecView<const thrust::pair<int, int>> internalChildren_;
|
||||
Recorder recorder;
|
||||
|
||||
__host__ __device__ int RecordCollision(int node,
|
||||
thrust::tuple<T, int>& query) {
|
||||
int RecordCollision(int node, thrust::tuple<T, int>& query) {
|
||||
const T& queryObj = thrust::get<0>(query);
|
||||
const int queryIdx = thrust::get<1>(query);
|
||||
int& count = counts[queryIdx];
|
||||
|
||||
bool overlaps = nodeBBox_[node].DoesOverlap(queryObj);
|
||||
if (overlaps && IsLeaf(node)) {
|
||||
const int leafIdx = Node2Leaf(node);
|
||||
int leafIdx = Node2Leaf(node);
|
||||
if (!selfCollision || leafIdx != queryIdx) {
|
||||
if (allocateOnly) {
|
||||
count++;
|
||||
} else {
|
||||
int pos = count++;
|
||||
queryTri_.first[pos] = queryIdx;
|
||||
queryTri_.second[pos] = leafIdx;
|
||||
}
|
||||
recorder.record(queryIdx, leafIdx);
|
||||
}
|
||||
}
|
||||
return overlaps && IsInternal(node); // Should traverse into node
|
||||
}
|
||||
|
||||
__host__ __device__ void operator()(thrust::tuple<T, int> query) {
|
||||
void operator()(thrust::tuple<T, int> query) {
|
||||
// stack cannot overflow because radix tree has max depth 30 (Morton code) +
|
||||
// 32 (index).
|
||||
int stack[64];
|
||||
@@ -195,7 +181,7 @@ struct FindCollisions {
|
||||
int node = kRoot;
|
||||
const int queryIdx = thrust::get<1>(query);
|
||||
// same implies that this query do not have any collision
|
||||
if (!allocateOnly && counts[queryIdx] == counts[queryIdx + 1]) return;
|
||||
if (recorder.earlyexit(queryIdx)) return;
|
||||
while (1) {
|
||||
int internal = Node2Internal(node);
|
||||
int child1 = internalChildren_[internal].first;
|
||||
@@ -214,16 +200,56 @@ struct FindCollisions {
|
||||
}
|
||||
}
|
||||
}
|
||||
recorder.end(queryIdx);
|
||||
}
|
||||
};
|
||||
|
||||
struct BuildInternalBoxes {
|
||||
Box* nodeBBox_;
|
||||
int* counter_;
|
||||
const int* nodeParent_;
|
||||
const thrust::pair<int, int>* internalChildren_;
|
||||
struct CountCollisions {
|
||||
VecView<int> counts;
|
||||
VecView<char> empty;
|
||||
void record(int queryIdx, int _leafIdx) { counts[queryIdx]++; }
|
||||
bool earlyexit(int _queryIdx) { return false; }
|
||||
void end(int queryIdx) {
|
||||
if (counts[queryIdx] == 0) empty[queryIdx] = 1;
|
||||
}
|
||||
};
|
||||
|
||||
__host__ __device__ void operator()(int leaf) {
|
||||
template <const bool inverted>
|
||||
struct SeqCollisionRecorder {
|
||||
SparseIndices& queryTri_;
|
||||
void record(int queryIdx, int leafIdx) const {
|
||||
if (inverted)
|
||||
queryTri_.Add(leafIdx, queryIdx);
|
||||
else
|
||||
queryTri_.Add(queryIdx, leafIdx);
|
||||
}
|
||||
bool earlyexit(int queryIdx) const { return false; }
|
||||
void end(int queryIdx) const {}
|
||||
};
|
||||
|
||||
template <const bool inverted>
|
||||
struct ParCollisionRecorder {
|
||||
SparseIndices& queryTri;
|
||||
VecView<int> counts;
|
||||
VecView<char> empty;
|
||||
void record(int queryIdx, int leafIdx) {
|
||||
int pos = counts[queryIdx]++;
|
||||
if (inverted)
|
||||
queryTri.Set(pos, leafIdx, queryIdx);
|
||||
else
|
||||
queryTri.Set(pos, queryIdx, leafIdx);
|
||||
}
|
||||
bool earlyexit(int queryIdx) const { return empty[queryIdx] == 1; }
|
||||
void end(int queryIdx) const {}
|
||||
};
|
||||
|
||||
struct BuildInternalBoxes {
|
||||
VecView<Box> nodeBBox_;
|
||||
VecView<int> counter_;
|
||||
const VecView<int> nodeParent_;
|
||||
const VecView<thrust::pair<int, int>> internalChildren_;
|
||||
|
||||
void operator()(int leaf) {
|
||||
int node = Leaf2Node(leaf);
|
||||
do {
|
||||
node = nodeParent_[node];
|
||||
@@ -237,9 +263,7 @@ struct BuildInternalBoxes {
|
||||
|
||||
struct TransformBox {
|
||||
const glm::mat4x3 transform;
|
||||
__host__ __device__ void operator()(Box& box) {
|
||||
box = box.Transform(transform);
|
||||
}
|
||||
void operator()(Box& box) { box = box.Transform(transform); }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -250,8 +274,9 @@ namespace manifold {
|
||||
* bounding boxes and corresponding Morton codes. It is assumed these vectors
|
||||
* are already sorted by increasing Morton code.
|
||||
*/
|
||||
Collider::Collider(const VecDH<Box>& leafBB,
|
||||
const VecDH<uint32_t>& leafMorton) {
|
||||
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;
|
||||
@@ -261,8 +286,7 @@ Collider::Collider(const VecDH<Box>& leafBB,
|
||||
internalChildren_.resize(leafBB.size() - 1, thrust::make_pair(-1, -1));
|
||||
// organize tree
|
||||
for_each_n(autoPolicy(NumInternal()), countAt(0), NumInternal(),
|
||||
CreateRadixTree(
|
||||
{nodeParent_.ptrD(), internalChildren_.ptrD(), leafMorton}));
|
||||
CreateRadixTree({nodeParent_, internalChildren_, leafMorton}));
|
||||
UpdateBoxes(leafBB);
|
||||
}
|
||||
|
||||
@@ -274,47 +298,59 @@ Collider::Collider(const VecDH<Box>& leafBB,
|
||||
* the query vector is the leaf vector, set selfCollision to true, which will
|
||||
* then not report any collisions between an index and itself.
|
||||
*/
|
||||
template <const bool selfCollision, typename T>
|
||||
SparseIndices Collider::Collisions(const VecDH<T>& queriesIn) const {
|
||||
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
|
||||
VecDH<int> counts(queriesIn.size() + 1, 0);
|
||||
auto policy = autoPolicy(queriesIn.size());
|
||||
// compute the number of collisions to determine the size for allocation and
|
||||
// offset, this avoids the need for atomic
|
||||
for_each_n(policy, zip(queriesIn.cbegin(), countAt(0)), queriesIn.size(),
|
||||
FindCollisions<T, true, selfCollision>(
|
||||
{thrust::pair<int*, int*>(nullptr, nullptr), counts.ptrD(),
|
||||
nodeBBox_.ptrD(), internalChildren_.ptrD()}));
|
||||
// compute start index for each query and total count
|
||||
exclusive_scan(policy, counts.begin(), counts.end(), counts.begin());
|
||||
SparseIndices queryTri(counts.back());
|
||||
// actually recording collisions
|
||||
for_each_n(policy, zip(queriesIn.cbegin(), countAt(0)), queriesIn.size(),
|
||||
FindCollisions<T, false, selfCollision>(
|
||||
{queryTri.ptrDpq(), counts.ptrD(), nodeBBox_.ptrD(),
|
||||
internalChildren_.ptrD()}));
|
||||
return queryTri;
|
||||
if (queriesIn.size() < kSequentialThreshold) {
|
||||
SparseIndices queryTri;
|
||||
for_each_n(ExecutionPolicy::Seq, zip(queriesIn.cbegin(), countAt(0)),
|
||||
queriesIn.size(),
|
||||
FindCollisions<T, selfCollision, SeqCollisionRecorder<inverted>>{
|
||||
nodeBBox_, internalChildren_, {queryTri}});
|
||||
return queryTri;
|
||||
} else {
|
||||
// compute the number of collisions to determine the size for allocation and
|
||||
// offset, this avoids the need for atomic
|
||||
Vec<int> counts(queriesIn.size() + 1, 0);
|
||||
Vec<char> empty(queriesIn.size(), 0);
|
||||
for_each_n(ExecutionPolicy::Par, zip(queriesIn.cbegin(), countAt(0)),
|
||||
queriesIn.size(),
|
||||
FindCollisions<T, selfCollision, CountCollisions>{
|
||||
nodeBBox_, internalChildren_, {counts, empty}});
|
||||
// compute start index for each query and total count
|
||||
exclusive_scan(ExecutionPolicy::Par, counts.begin(), counts.end(),
|
||||
counts.begin(), 0, std::plus<int>());
|
||||
if (counts.back() == 0) return SparseIndices(0);
|
||||
SparseIndices queryTri(counts.back());
|
||||
// actually recording collisions
|
||||
for_each_n(ExecutionPolicy::Par, zip(queriesIn.cbegin(), countAt(0)),
|
||||
queriesIn.size(),
|
||||
FindCollisions<T, selfCollision, ParCollisionRecorder<inverted>>{
|
||||
nodeBBox_, internalChildren_, {queryTri, counts, empty}});
|
||||
return queryTri;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate the collider's internal bounding boxes without changing the
|
||||
* hierarchy.
|
||||
*/
|
||||
void Collider::UpdateBoxes(const VecDH<Box>& leafBB) {
|
||||
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
|
||||
strided_range<VecDH<Box>::Iter> leaves(nodeBBox_.begin(), nodeBBox_.end(), 2);
|
||||
strided_range<Vec<Box>::Iter> leaves(nodeBBox_.begin(), nodeBBox_.end(), 2);
|
||||
auto policy = autoPolicy(NumInternal());
|
||||
copy(policy, leafBB.cbegin(), leafBB.cend(), leaves.begin());
|
||||
// create global counters
|
||||
VecDH<int> counter(NumInternal(), 0);
|
||||
Vec<int> counter(NumInternal(), 0);
|
||||
// kernel over leaves to save internal Boxes
|
||||
for_each_n(
|
||||
policy, countAt(0), NumLeaves(),
|
||||
BuildInternalBoxes({nodeBBox_.ptrD(), counter.ptrD(), nodeParent_.cptrD(),
|
||||
internalChildren_.cptrD()}));
|
||||
BuildInternalBoxes({nodeBBox_, counter, nodeParent_, internalChildren_}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,6 +358,7 @@ void Collider::UpdateBoxes(const VecDH<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;
|
||||
@@ -337,12 +374,22 @@ bool Collider::Transform(glm::mat4x3 transform) {
|
||||
return axisAligned;
|
||||
}
|
||||
|
||||
template SparseIndices Collider::Collisions<true, Box>(const VecDH<Box>&) const;
|
||||
template SparseIndices Collider::Collisions<true, false, Box>(
|
||||
const VecView<const Box>&) const;
|
||||
|
||||
template SparseIndices Collider::Collisions<false, Box>(
|
||||
const VecDH<Box>&) const;
|
||||
template SparseIndices Collider::Collisions<false, false, Box>(
|
||||
const VecView<const Box>&) const;
|
||||
|
||||
template SparseIndices Collider::Collisions<false, glm::vec3>(
|
||||
const VecDH<glm::vec3>&) const;
|
||||
template SparseIndices Collider::Collisions<false, false, glm::vec3>(
|
||||
const VecView<const glm::vec3>&) const;
|
||||
|
||||
template SparseIndices Collider::Collisions<true, true, Box>(
|
||||
const VecView<const Box>&) const;
|
||||
|
||||
template SparseIndices Collider::Collisions<false, true, Box>(
|
||||
const VecView<const Box>&) const;
|
||||
|
||||
template SparseIndices Collider::Collisions<false, true, glm::vec3>(
|
||||
const VecView<const glm::vec3>&) const;
|
||||
|
||||
} // namespace manifold
|
||||
|
||||
@@ -13,16 +13,17 @@
|
||||
# limitations under the License.
|
||||
|
||||
project (cross_section)
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
target_link_libraries( ${PROJECT_NAME}
|
||||
PUBLIC utilities Clipper2
|
||||
)
|
||||
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} EXPORT manifoldTargets)
|
||||
install(FILES include/cross_section.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -14,27 +14,23 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <clipper2/clipper.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "clipper2/clipper.core.h"
|
||||
#include "clipper2/clipper.offset.h"
|
||||
#include "glm/ext/matrix_float3x2.hpp"
|
||||
#include "glm/ext/vector_float2.hpp"
|
||||
#include "public.h"
|
||||
|
||||
namespace C2 = Clipper2Lib;
|
||||
#include "vec_view.h"
|
||||
|
||||
namespace manifold {
|
||||
|
||||
class Rect;
|
||||
|
||||
/** @addtogroup Core
|
||||
* @{
|
||||
*/
|
||||
|
||||
struct PathImpl;
|
||||
|
||||
/**
|
||||
* Two-dimensional cross sections guaranteed to be without self-intersections,
|
||||
* or overlaps between polygons (from construction onwards). This class makes
|
||||
@@ -102,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:
|
||||
@@ -169,70 +167,10 @@ class CrossSection {
|
||||
///@}
|
||||
|
||||
private:
|
||||
mutable std::shared_ptr<const C2::PathsD> paths_;
|
||||
mutable std::shared_ptr<const PathImpl> paths_;
|
||||
mutable glm::mat3x2 transform_ = glm::mat3x2(1.0f);
|
||||
CrossSection(C2::PathsD paths);
|
||||
C2::PathsD GetPaths() const;
|
||||
};
|
||||
/** @} */
|
||||
|
||||
/** @addtogroup Connections
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Axis-aligned rectangular bounds.
|
||||
*/
|
||||
class Rect {
|
||||
public:
|
||||
glm::vec2 min = glm::vec2(0);
|
||||
glm::vec2 max = glm::vec2(0);
|
||||
|
||||
/** @name Creation
|
||||
* Constructors
|
||||
*/
|
||||
///@{
|
||||
Rect();
|
||||
~Rect();
|
||||
Rect(const Rect& other);
|
||||
Rect& operator=(const Rect& other);
|
||||
Rect(Rect&&) noexcept;
|
||||
Rect& operator=(Rect&&) noexcept;
|
||||
Rect(const glm::vec2 a, const glm::vec2 b);
|
||||
///@}
|
||||
|
||||
/** @name Information
|
||||
* Details of the rectangle
|
||||
*/
|
||||
///@{
|
||||
glm::vec2 Size() const;
|
||||
float Area() const;
|
||||
float Scale() const;
|
||||
glm::vec2 Center() const;
|
||||
bool Contains(const glm::vec2& pt) const;
|
||||
bool Contains(const Rect& other) const;
|
||||
bool DoesOverlap(const Rect& other) const;
|
||||
bool IsEmpty() const;
|
||||
bool IsFinite() const;
|
||||
///@}
|
||||
|
||||
/** @name Modification
|
||||
*/
|
||||
///@{
|
||||
void Union(const glm::vec2 p);
|
||||
Rect Union(const Rect& other) const;
|
||||
Rect operator+(const glm::vec2 shift) const;
|
||||
Rect& operator+=(const glm::vec2 shift);
|
||||
Rect operator*(const glm::vec2 scale) const;
|
||||
Rect& operator*=(const glm::vec2 scale);
|
||||
Rect Transform(const glm::mat3x2& m) const;
|
||||
///@}
|
||||
|
||||
/** @name Conversion
|
||||
*/
|
||||
///@{
|
||||
CrossSection AsCrossSection() const;
|
||||
///@}
|
||||
CrossSection(std::shared_ptr<const PathImpl> paths);
|
||||
std::shared_ptr<const PathImpl> GetPaths() const;
|
||||
};
|
||||
/** @} */
|
||||
} // namespace manifold
|
||||
|
||||
@@ -14,8 +14,22 @@
|
||||
|
||||
#include "cross_section.h"
|
||||
|
||||
#include "clipper2/clipper.core.h"
|
||||
#include "clipper2/clipper.h"
|
||||
#include "clipper2/clipper.offset.h"
|
||||
|
||||
namespace C2 = Clipper2Lib;
|
||||
|
||||
using namespace manifold;
|
||||
|
||||
namespace manifold {
|
||||
struct PathImpl {
|
||||
PathImpl(const C2::PathsD paths_) : paths_(paths_) {}
|
||||
operator const C2::PathsD&() const { return paths_; }
|
||||
const C2::PathsD paths_;
|
||||
};
|
||||
} // namespace manifold
|
||||
|
||||
namespace {
|
||||
const int precision_ = 8;
|
||||
|
||||
@@ -96,8 +110,8 @@ C2::PathsD transform(const C2::PathsD ps, const glm::mat3x2 m) {
|
||||
return transformed;
|
||||
}
|
||||
|
||||
std::shared_ptr<const C2::PathsD> shared_paths(const C2::PathsD& ps) {
|
||||
return std::make_shared<const C2::PathsD>(ps);
|
||||
std::shared_ptr<const PathImpl> shared_paths(const C2::PathsD& ps) {
|
||||
return std::make_shared<const PathImpl>(ps);
|
||||
}
|
||||
|
||||
// forward declaration for mutual recursion
|
||||
@@ -132,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;
|
||||
}
|
||||
@@ -191,7 +213,9 @@ namespace manifold {
|
||||
/**
|
||||
* The default constructor is an empty cross-section (containing no contours).
|
||||
*/
|
||||
CrossSection::CrossSection() { paths_ = shared_paths(C2::PathsD()); }
|
||||
CrossSection::CrossSection() {
|
||||
paths_ = std::make_shared<const PathImpl>(C2::PathsD());
|
||||
}
|
||||
|
||||
CrossSection::~CrossSection() = default;
|
||||
CrossSection::CrossSection(CrossSection&&) noexcept = default;
|
||||
@@ -218,7 +242,7 @@ CrossSection& CrossSection::operator=(const CrossSection& other) {
|
||||
};
|
||||
|
||||
// Private, skips unioning.
|
||||
CrossSection::CrossSection(C2::PathsD ps) { paths_ = shared_paths(ps); }
|
||||
CrossSection::CrossSection(std::shared_ptr<const PathImpl> ps) { paths_ = ps; }
|
||||
|
||||
/**
|
||||
* Create a 2d cross-section from a single contour. A boolean union operation
|
||||
@@ -271,13 +295,13 @@ CrossSection::CrossSection(const Rect& rect) {
|
||||
// Private
|
||||
// All access to paths_ should be done through the GetPaths() method, which
|
||||
// applies the accumulated transform_
|
||||
C2::PathsD CrossSection::GetPaths() const {
|
||||
std::shared_ptr<const PathImpl> CrossSection::GetPaths() const {
|
||||
if (transform_ == glm::mat3x2(1.0f)) {
|
||||
return *paths_;
|
||||
return paths_;
|
||||
}
|
||||
paths_ = shared_paths(transform(*paths_, transform_));
|
||||
paths_ = shared_paths(::transform(paths_->paths_, transform_));
|
||||
transform_ = glm::mat3x2(1.0f);
|
||||
return *paths_;
|
||||
return paths_;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,7 +333,7 @@ CrossSection CrossSection::Square(const glm::vec2 size, bool center) {
|
||||
p[2] = C2::PointD(x, y);
|
||||
p[3] = C2::PointD(0.0, y);
|
||||
}
|
||||
return CrossSection(C2::PathsD{p});
|
||||
return CrossSection(shared_paths(C2::PathsD{p}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,7 +354,7 @@ CrossSection CrossSection::Circle(float radius, int circularSegments) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
circle[i] = C2::PointD(radius * cosd(dPhi * i), radius * sind(dPhi * i));
|
||||
}
|
||||
return CrossSection(C2::PathsD{circle});
|
||||
return CrossSection(shared_paths(C2::PathsD{circle}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,9 +363,9 @@ CrossSection CrossSection::Circle(float radius, int circularSegments) {
|
||||
CrossSection CrossSection::Boolean(const CrossSection& second,
|
||||
OpType op) const {
|
||||
auto ct = cliptype_of_op(op);
|
||||
auto res = C2::BooleanOp(ct, C2::FillRule::Positive, GetPaths(),
|
||||
second.GetPaths(), precision_);
|
||||
return CrossSection(res);
|
||||
auto res = C2::BooleanOp(ct, C2::FillRule::Positive, GetPaths()->paths_,
|
||||
second.GetPaths()->paths_, precision_);
|
||||
return CrossSection(shared_paths(res));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,19 +382,19 @@ CrossSection CrossSection::BatchBoolean(
|
||||
auto subjs = crossSections[0].GetPaths();
|
||||
int n_clips = 0;
|
||||
for (int i = 1; i < crossSections.size(); ++i) {
|
||||
n_clips += crossSections[i].GetPaths().size();
|
||||
n_clips += crossSections[i].GetPaths()->paths_.size();
|
||||
}
|
||||
auto clips = C2::PathsD();
|
||||
clips.reserve(n_clips);
|
||||
for (int i = 1; i < crossSections.size(); ++i) {
|
||||
auto ps = crossSections[i].GetPaths();
|
||||
clips.insert(clips.end(), ps.begin(), ps.end());
|
||||
clips.insert(clips.end(), ps->paths_.begin(), ps->paths_.end());
|
||||
}
|
||||
|
||||
auto ct = cliptype_of_op(op);
|
||||
auto res =
|
||||
C2::BooleanOp(ct, C2::FillRule::Positive, subjs, clips, precision_);
|
||||
return CrossSection(res);
|
||||
auto res = C2::BooleanOp(ct, C2::FillRule::Positive, subjs->paths_, clips,
|
||||
precision_);
|
||||
return CrossSection(shared_paths(res));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,7 +465,7 @@ std::vector<CrossSection> CrossSection::Decompose() const {
|
||||
}
|
||||
|
||||
C2::PolyTreeD tree;
|
||||
C2::BooleanOp(C2::ClipType::Union, C2::FillRule::Positive, GetPaths(),
|
||||
C2::BooleanOp(C2::ClipType::Union, C2::FillRule::Positive, GetPaths()->paths_,
|
||||
C2::PathsD(), tree, precision_);
|
||||
|
||||
auto polys = std::vector<C2::PathsD>();
|
||||
@@ -451,7 +475,7 @@ std::vector<CrossSection> CrossSection::Decompose() const {
|
||||
auto comps = std::vector<CrossSection>(n_polys);
|
||||
// reverse the stack while wrapping
|
||||
for (int i = 0; i < n_polys; ++i) {
|
||||
comps[n_polys - i - 1] = CrossSection(polys[i]);
|
||||
comps[n_polys - i - 1] = CrossSection(shared_paths(polys[i]));
|
||||
}
|
||||
|
||||
return comps;
|
||||
@@ -539,20 +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.size());
|
||||
for (auto path : 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);
|
||||
}
|
||||
return CrossSection(C2::Union(warped, C2::FillRule::Positive, precision_));
|
||||
|
||||
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(paths, C2::FillRule::Positive, precision_)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -568,8 +614,29 @@ CrossSection CrossSection::Warp(
|
||||
* offseting operations are to be performed, which would compound the issue.
|
||||
*/
|
||||
CrossSection CrossSection::Simplify(double epsilon) const {
|
||||
auto ps = SimplifyPaths(GetPaths(), epsilon, false);
|
||||
return CrossSection(ps);
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -607,11 +674,17 @@ CrossSection CrossSection::Offset(double delta, JoinType jointype,
|
||||
arc_tol = (std::cos(Clipper2Lib::PI / n) - 1) * -scaled_delta;
|
||||
}
|
||||
auto ps =
|
||||
C2::InflatePaths(GetPaths(), delta, jt(jointype), C2::EndType::Polygon,
|
||||
miter_limit, precision_, arc_tol);
|
||||
return CrossSection(ps);
|
||||
C2::InflatePaths(GetPaths()->paths_, delta, jt(jointype),
|
||||
C2::EndType::Polygon, miter_limit, precision_, arc_tol);
|
||||
return CrossSection(shared_paths(ps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the convex hull enveloping a set of cross-sections.
|
||||
*
|
||||
* @param crossSections A vector of cross-sections over which to compute a
|
||||
* convex hull.
|
||||
*/
|
||||
CrossSection CrossSection::Hull(
|
||||
const std::vector<CrossSection>& crossSections) {
|
||||
int n = 0;
|
||||
@@ -619,24 +692,41 @@ CrossSection CrossSection::Hull(
|
||||
SimplePolygon pts;
|
||||
pts.reserve(n);
|
||||
for (auto cs : crossSections) {
|
||||
auto paths = cs.GetPaths();
|
||||
auto paths = cs.GetPaths()->paths_;
|
||||
for (auto path : paths) {
|
||||
for (auto p : path) {
|
||||
pts.push_back(v2_of_pd(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
return CrossSection(C2::PathsD{HullImpl(pts)});
|
||||
return CrossSection(shared_paths(C2::PathsD{HullImpl(pts)}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the convex hull of this cross-section.
|
||||
*/
|
||||
CrossSection CrossSection::Hull() const {
|
||||
return Hull(std::vector<CrossSection>{*this});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the convex hull of a set of points. If the given points are fewer
|
||||
* than 3, an empty CrossSection will be returned.
|
||||
*
|
||||
* @param pts A vector of 2-dimensional points over which to compute a convex
|
||||
* hull.
|
||||
*/
|
||||
CrossSection CrossSection::Hull(SimplePolygon pts) {
|
||||
return CrossSection(C2::PathsD{HullImpl(pts)});
|
||||
return CrossSection(shared_paths(C2::PathsD{HullImpl(pts)}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the convex hull of a set of points/polygons. If the given points are
|
||||
* fewer than 3, an empty CrossSection will be returned.
|
||||
*
|
||||
* @param pts A vector of vectors of 2-dimensional points over which to compute
|
||||
* a convex hull.
|
||||
*/
|
||||
CrossSection CrossSection::Hull(const Polygons polys) {
|
||||
SimplePolygon pts;
|
||||
for (auto poly : polys) {
|
||||
@@ -651,14 +741,14 @@ CrossSection CrossSection::Hull(const Polygons polys) {
|
||||
* Return the total area covered by complex polygons making up the
|
||||
* CrossSection.
|
||||
*/
|
||||
double CrossSection::Area() const { return C2::Area(GetPaths()); }
|
||||
double CrossSection::Area() const { return C2::Area(GetPaths()->paths_); }
|
||||
|
||||
/**
|
||||
* Return the number of vertices in the CrossSection.
|
||||
*/
|
||||
int CrossSection::NumVert() const {
|
||||
int n = 0;
|
||||
auto paths = GetPaths();
|
||||
auto paths = GetPaths()->paths_;
|
||||
for (auto p : paths) {
|
||||
n += p.size();
|
||||
}
|
||||
@@ -669,19 +759,19 @@ int CrossSection::NumVert() const {
|
||||
* Return the number of contours (both outer and inner paths) in the
|
||||
* CrossSection.
|
||||
*/
|
||||
int CrossSection::NumContour() const { return GetPaths().size(); }
|
||||
int CrossSection::NumContour() const { return GetPaths()->paths_.size(); }
|
||||
|
||||
/**
|
||||
* Does the CrossSection contain any contours?
|
||||
*/
|
||||
bool CrossSection::IsEmpty() const { return GetPaths().empty(); }
|
||||
bool CrossSection::IsEmpty() const { return GetPaths()->paths_.empty(); }
|
||||
|
||||
/**
|
||||
* Returns the axis-aligned bounding rectangle of all the CrossSections'
|
||||
* vertices.
|
||||
*/
|
||||
Rect CrossSection::Bounds() const {
|
||||
auto r = C2::GetBounds(GetPaths());
|
||||
auto r = C2::GetBounds(GetPaths()->paths_);
|
||||
return Rect({r.left, r.bottom}, {r.right, r.top});
|
||||
}
|
||||
|
||||
@@ -690,7 +780,7 @@ Rect CrossSection::Bounds() const {
|
||||
*/
|
||||
Polygons CrossSection::ToPolygons() const {
|
||||
auto polys = Polygons();
|
||||
auto paths = GetPaths();
|
||||
auto paths = GetPaths()->paths_;
|
||||
polys.reserve(paths.size());
|
||||
for (auto p : paths) {
|
||||
auto sp = SimplePolygon();
|
||||
@@ -702,167 +792,4 @@ Polygons CrossSection::ToPolygons() const {
|
||||
}
|
||||
return polys;
|
||||
}
|
||||
|
||||
// Rect
|
||||
|
||||
/**
|
||||
* Default constructor is an empty rectangle..
|
||||
*/
|
||||
Rect::Rect() {}
|
||||
|
||||
Rect::~Rect() = default;
|
||||
Rect::Rect(Rect&&) noexcept = default;
|
||||
Rect& Rect::operator=(Rect&&) noexcept = default;
|
||||
Rect::Rect(const Rect& other) {
|
||||
min = glm::vec2(other.min);
|
||||
max = glm::vec2(other.max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rectangle that contains the two given points.
|
||||
*/
|
||||
Rect::Rect(const glm::vec2 a, const glm::vec2 b) {
|
||||
min = glm::min(a, b);
|
||||
max = glm::max(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dimensions of the rectangle.
|
||||
*/
|
||||
glm::vec2 Rect::Size() const { return max - min; }
|
||||
|
||||
/**
|
||||
* Return the area of the rectangle.
|
||||
*/
|
||||
float Rect::Area() const {
|
||||
auto sz = Size();
|
||||
return sz.x * sz.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute-largest coordinate value of any contained
|
||||
* point.
|
||||
*/
|
||||
float Rect::Scale() const {
|
||||
glm::vec2 absMax = glm::max(glm::abs(min), glm::abs(max));
|
||||
return glm::max(absMax.x, absMax.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the center point of the rectangle.
|
||||
*/
|
||||
glm::vec2 Rect::Center() const { return 0.5f * (max + min); }
|
||||
|
||||
/**
|
||||
* Does this rectangle contain (includes on border) the given point?
|
||||
*/
|
||||
bool Rect::Contains(const glm::vec2& p) const {
|
||||
return glm::all(glm::greaterThanEqual(p, min)) &&
|
||||
glm::all(glm::greaterThanEqual(max, p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this rectangle contain (includes equal) the given rectangle?
|
||||
*/
|
||||
bool Rect::Contains(const Rect& rect) const {
|
||||
return glm::all(glm::greaterThanEqual(rect.min, min)) &&
|
||||
glm::all(glm::greaterThanEqual(max, rect.max));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this rectangle overlap the one given (including equality)?
|
||||
*/
|
||||
bool Rect::DoesOverlap(const Rect& rect) const {
|
||||
return min.x <= rect.max.x && min.y <= rect.max.y && max.x >= rect.min.x &&
|
||||
max.y >= rect.min.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the rectangle empty (containing no space)?
|
||||
*/
|
||||
bool Rect::IsEmpty() const { return max.y <= min.y || max.x <= min.x; };
|
||||
|
||||
/**
|
||||
* Does this recangle have finite bounds?
|
||||
*/
|
||||
bool Rect::IsFinite() const {
|
||||
return glm::all(glm::isfinite(min)) && glm::all(glm::isfinite(max));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand this rectangle (in place) to include the given point.
|
||||
*/
|
||||
void Rect::Union(const glm::vec2 p) {
|
||||
min = glm::min(min, p);
|
||||
max = glm::max(max, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand this rectangle to include the given Rect.
|
||||
*/
|
||||
Rect Rect::Union(const Rect& rect) const {
|
||||
Rect out;
|
||||
out.min = glm::min(min, rect.min);
|
||||
out.max = glm::max(max, rect.max);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift this rectangle by the given vector.
|
||||
*/
|
||||
Rect Rect::operator+(const glm::vec2 shift) const {
|
||||
Rect out;
|
||||
out.min = min + shift;
|
||||
out.max = max + shift;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift this rectangle in-place by the given vector.
|
||||
*/
|
||||
Rect& Rect::operator+=(const glm::vec2 shift) {
|
||||
min += shift;
|
||||
max += shift;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale this rectangle by the given vector.
|
||||
*/
|
||||
Rect Rect::operator*(const glm::vec2 scale) const {
|
||||
Rect out;
|
||||
out.min = min * scale;
|
||||
out.max = max * scale;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale this rectangle in-place by the given vector.
|
||||
*/
|
||||
Rect& Rect::operator*=(const glm::vec2 scale) {
|
||||
min *= scale;
|
||||
max *= scale;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the rectangle by the given axis-aligned affine transform.
|
||||
*
|
||||
* Ensure the transform passed in is axis-aligned (rotations are all
|
||||
* multiples of 90 degrees), or else the resulting rectangle will no longer
|
||||
* bound properly.
|
||||
*/
|
||||
Rect Rect::Transform(const glm::mat3x2& m) const {
|
||||
Rect rect;
|
||||
rect.min = m * glm::vec3(min, 1);
|
||||
rect.max = m * glm::vec3(max, 1);
|
||||
return rect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a CrossSection with an outline defined by this axis-aligned
|
||||
* rectangle.
|
||||
*/
|
||||
CrossSection Rect::AsCrossSection() const { return CrossSection(*this); }
|
||||
|
||||
} // namespace manifold
|
||||
|
||||
@@ -17,17 +17,19 @@ project (manifold)
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} ${SOURCE_FILES})
|
||||
|
||||
if(MANIFOLD_USE_CUDA)
|
||||
set_source_files_properties(${SOURCE_FILES} PROPERTIES LANGUAGE CUDA)
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
|
||||
endif()
|
||||
|
||||
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} graphlite Clipper2 quickhull
|
||||
PUBLIC utilities cross_section sdf
|
||||
PRIVATE collider polygon ${MANIFOLD_INCLUDE} quickhull
|
||||
)
|
||||
|
||||
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/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.
|
||||
|
||||
@@ -27,39 +27,35 @@ namespace {
|
||||
// carefully designed to minimize rounding error and to eliminate it at edge
|
||||
// cases to ensure consistency.
|
||||
|
||||
__host__ __device__ 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);
|
||||
glm::vec2 Interpolate(glm::vec3 pL, glm::vec3 pR, float x) {
|
||||
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;
|
||||
}
|
||||
|
||||
__host__ __device__ 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;
|
||||
glm::vec4 Intersect(const glm::vec3 &pL, const glm::vec3 &pR,
|
||||
const glm::vec3 &qL, const glm::vec3 &qR) {
|
||||
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);
|
||||
@@ -67,60 +63,51 @@ __host__ __device__ glm::vec4 Intersect(const glm::vec3 &pL,
|
||||
return xyzz;
|
||||
}
|
||||
|
||||
template <const bool inverted>
|
||||
struct CopyFaceEdges {
|
||||
const SparseIndices &p1q1;
|
||||
// const int *p1q1;
|
||||
// x can be either vert or edge (0 or 1).
|
||||
thrust::pair<int *, int *> pXq1;
|
||||
const Halfedge *halfedgesQ;
|
||||
SparseIndices &pXq1;
|
||||
VecView<const Halfedge> halfedgesQ;
|
||||
|
||||
__host__ __device__ void operator()(thrust::tuple<int, int, int> in) {
|
||||
void operator()(thrust::tuple<size_t, size_t> in) {
|
||||
int idx = 3 * thrust::get<0>(in);
|
||||
const int pX = thrust::get<1>(in);
|
||||
const int q2 = thrust::get<2>(in);
|
||||
size_t i = thrust::get<1>(in);
|
||||
int pX = p1q1.Get(i, inverted);
|
||||
int q2 = p1q1.Get(i, !inverted);
|
||||
|
||||
for (const int i : {0, 1, 2}) {
|
||||
pXq1.first[idx + i] = pX;
|
||||
const int q1 = 3 * q2 + i;
|
||||
for (const int j : {0, 1, 2}) {
|
||||
const int q1 = 3 * q2 + j;
|
||||
const Halfedge edge = halfedgesQ[q1];
|
||||
pXq1.second[idx + i] = edge.IsForward() ? q1 : edge.pairedHalfedge;
|
||||
int a = pX;
|
||||
int b = edge.IsForward() ? q1 : edge.pairedHalfedge;
|
||||
if (inverted) std::swap(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,
|
||||
ExecutionPolicy policy) {
|
||||
const SparseIndices &p1q2, const SparseIndices &p2q1) {
|
||||
ZoneScoped;
|
||||
SparseIndices p1q1(3 * p1q2.size() + 3 * p2q1.size());
|
||||
for_each_n(policy, zip(countAt(0), p1q2.begin(0), p1q2.begin(1)), p1q2.size(),
|
||||
CopyFaceEdges({p1q1.ptrDpq(), inQ.halfedge_.cptrD()}));
|
||||
|
||||
p1q1.SwapPQ();
|
||||
for_each_n(policy, zip(countAt(p1q2.size()), p2q1.begin(1), p2q1.begin(0)),
|
||||
p2q1.size(),
|
||||
CopyFaceEdges({p1q1.ptrDpq(), inP.halfedge_.cptrD()}));
|
||||
p1q1.SwapPQ();
|
||||
p1q1.Unique(policy);
|
||||
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;
|
||||
}
|
||||
|
||||
__host__ __device__ bool Shadows(float p, float q, float dir) {
|
||||
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.
|
||||
*/
|
||||
__host__ __device__ thrust::pair<int, glm::vec2> Shadow01(
|
||||
const int p0, const int q1, const glm::vec3 *vertPosP,
|
||||
const glm::vec3 *vertPosQ, const Halfedge *halfedgeQ, const float expandP,
|
||||
const glm::vec3 *normalP, const bool reverse) {
|
||||
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,
|
||||
const float expandP, VecView<const glm::vec3> normalP, const bool reverse) {
|
||||
const int q1s = halfedgeQ[q1].startVert;
|
||||
const int q1e = halfedgeQ[q1].endVert;
|
||||
const float p0x = vertPosP[p0].x;
|
||||
@@ -148,43 +135,59 @@ __host__ __device__ thrust::pair<int, glm::vec2> Shadow01(
|
||||
return thrust::make_pair(s01, yz01);
|
||||
}
|
||||
|
||||
__host__ __device__ int BinarySearch(
|
||||
const thrust::pair<const int *, const int *> keys, const int size,
|
||||
const thrust::pair<int, int> key) {
|
||||
if (size <= 0) return -1;
|
||||
int left = 0;
|
||||
int right = size - 1;
|
||||
int m;
|
||||
thrust::pair<int, int> keyM;
|
||||
while (1) {
|
||||
m = right - (right - left) / 2;
|
||||
keyM = thrust::make_pair(keys.first[m], keys.second[m]);
|
||||
if (left == right) break;
|
||||
if (keyM > key)
|
||||
right = m - 1;
|
||||
else
|
||||
left = m;
|
||||
}
|
||||
if (keyM == key)
|
||||
return m;
|
||||
else
|
||||
// https://github.com/scandum/binary_search/blob/master/README.md
|
||||
// much faster than standard binary search on large arrays
|
||||
size_t monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
|
||||
if (array.size() == 0) {
|
||||
return -1;
|
||||
}
|
||||
size_t bot = 0;
|
||||
size_t top = array.size();
|
||||
while (top >= 65536) {
|
||||
size_t mid = top / 4;
|
||||
top -= mid * 3;
|
||||
if (key < array[bot + mid * 2]) {
|
||||
if (key >= array[bot + mid]) {
|
||||
bot += mid;
|
||||
}
|
||||
} else {
|
||||
bot += mid * 2;
|
||||
if (key >= array[bot + mid]) {
|
||||
bot += mid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (top > 3) {
|
||||
size_t mid = top / 2;
|
||||
if (key >= array[bot + mid]) {
|
||||
bot += mid;
|
||||
}
|
||||
top -= mid;
|
||||
}
|
||||
|
||||
while (top--) {
|
||||
if (key == array[bot + top]) {
|
||||
return bot + top;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct Kernel11 {
|
||||
const glm::vec3 *vertPosP;
|
||||
const glm::vec3 *vertPosQ;
|
||||
const Halfedge *halfedgeP;
|
||||
const Halfedge *halfedgeQ;
|
||||
VecView<const glm::vec3> vertPosP;
|
||||
VecView<const glm::vec3> vertPosQ;
|
||||
VecView<const Halfedge> halfedgeP;
|
||||
VecView<const Halfedge> halfedgeQ;
|
||||
float expandP;
|
||||
const glm::vec3 *normalP;
|
||||
VecView<const glm::vec3> normalP;
|
||||
const SparseIndices &p1q1;
|
||||
|
||||
__host__ __device__ void operator()(
|
||||
thrust::tuple<glm::vec4 &, int &, int, int> inout) {
|
||||
glm::vec4 &xyzz11 = thrust::get<0>(inout);
|
||||
int &s11 = thrust::get<1>(inout);
|
||||
const int p1 = thrust::get<2>(inout);
|
||||
const int q1 = thrust::get<3>(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);
|
||||
int &s11 = thrust::get<2>(inout);
|
||||
|
||||
// For pRL[k], qRL[k], k==0 is the left and k==1 is the right.
|
||||
int k = 0;
|
||||
@@ -233,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;
|
||||
@@ -254,20 +252,18 @@ struct Kernel11 {
|
||||
}
|
||||
};
|
||||
|
||||
std::tuple<VecDH<int>, VecDH<glm::vec4>> Shadow11(SparseIndices &p1q1,
|
||||
const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ,
|
||||
float expandP,
|
||||
ExecutionPolicy policy) {
|
||||
VecDH<int> s11(p1q1.size());
|
||||
VecDH<glm::vec4> xyzz11(p1q1.size());
|
||||
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(policy,
|
||||
zip(xyzz11.begin(), s11.begin(), p1q1.begin(0), p1q1.begin(1)),
|
||||
p1q1.size(),
|
||||
Kernel11({inP.vertPos_.cptrD(), inQ.vertPos_.cptrD(),
|
||||
inP.halfedge_.cptrD(), inQ.halfedge_.cptrD(), expandP,
|
||||
inP.vertNormal_.cptrD()}));
|
||||
for_each_n(autoPolicy(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}));
|
||||
|
||||
p1q1.KeepFinite(xyzz11, s11);
|
||||
|
||||
@@ -275,19 +271,19 @@ std::tuple<VecDH<int>, VecDH<glm::vec4>> Shadow11(SparseIndices &p1q1,
|
||||
};
|
||||
|
||||
struct Kernel02 {
|
||||
const glm::vec3 *vertPosP;
|
||||
const Halfedge *halfedgeQ;
|
||||
const glm::vec3 *vertPosQ;
|
||||
const bool forward;
|
||||
VecView<const glm::vec3> vertPosP;
|
||||
VecView<const Halfedge> halfedgeQ;
|
||||
VecView<const glm::vec3> vertPosQ;
|
||||
const float expandP;
|
||||
const glm::vec3 *vertNormalP;
|
||||
VecView<const glm::vec3> vertNormalP;
|
||||
const SparseIndices &p0q2;
|
||||
const bool forward;
|
||||
|
||||
__host__ __device__ void operator()(
|
||||
thrust::tuple<int &, float &, int, int> inout) {
|
||||
int &s02 = thrust::get<0>(inout);
|
||||
float &z02 = thrust::get<1>(inout);
|
||||
const int p0 = thrust::get<2>(inout);
|
||||
const int q2 = thrust::get<3>(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);
|
||||
float &z02 = thrust::get<2>(inout);
|
||||
|
||||
// For yzzLR[k], k==0 is the left and k==1 is the right.
|
||||
int k = 0;
|
||||
@@ -332,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) {
|
||||
@@ -351,22 +342,19 @@ struct Kernel02 {
|
||||
}
|
||||
};
|
||||
|
||||
std::tuple<VecDH<int>, VecDH<float>> Shadow02(const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ,
|
||||
SparseIndices &p0q2, bool forward,
|
||||
float expandP,
|
||||
ExecutionPolicy policy) {
|
||||
VecDH<int> s02(p0q2.size());
|
||||
VecDH<float> z02(p0q2.size());
|
||||
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_.cptrD() : inQ.vertNormal_.cptrD();
|
||||
for_each_n(
|
||||
policy,
|
||||
zip(s02.begin(), z02.begin(), p0q2.begin(!forward), p0q2.begin(forward)),
|
||||
p0q2.size(),
|
||||
Kernel02({inP.vertPos_.cptrD(), inQ.halfedge_.cptrD(),
|
||||
inQ.vertPos_.cptrD(), forward, expandP, vertNormalP}));
|
||||
auto vertNormalP = forward ? inP.vertNormal_ : inQ.vertNormal_;
|
||||
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}));
|
||||
|
||||
p0q2.KeepFinite(z02, s02);
|
||||
|
||||
@@ -374,25 +362,23 @@ std::tuple<VecDH<int>, VecDH<float>> Shadow02(const Manifold::Impl &inP,
|
||||
};
|
||||
|
||||
struct Kernel12 {
|
||||
const thrust::pair<const int *, const int *> p0q2;
|
||||
const int *s02;
|
||||
const float *z02;
|
||||
const int size02;
|
||||
const thrust::pair<const int *, const int *> p1q1;
|
||||
const int *s11;
|
||||
const glm::vec4 *xyzz11;
|
||||
const int size11;
|
||||
const Halfedge *halfedgesP;
|
||||
const Halfedge *halfedgesQ;
|
||||
const glm::vec3 *vertPosP;
|
||||
VecView<const int64_t> p0q2;
|
||||
VecView<const int> s02;
|
||||
VecView<const float> z02;
|
||||
VecView<const int64_t> p1q1;
|
||||
VecView<const int> s11;
|
||||
VecView<const glm::vec4> xyzz11;
|
||||
VecView<const Halfedge> halfedgesP;
|
||||
VecView<const Halfedge> halfedgesQ;
|
||||
VecView<const glm::vec3> vertPosP;
|
||||
const bool forward;
|
||||
const SparseIndices &p1q2;
|
||||
|
||||
__host__ __device__ void operator()(
|
||||
thrust::tuple<int &, glm::vec3 &, int, int> inout) {
|
||||
int &x12 = thrust::get<0>(inout);
|
||||
glm::vec3 &v12 = thrust::get<1>(inout);
|
||||
const int p1 = thrust::get<2>(inout);
|
||||
const int q2 = thrust::get<3>(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);
|
||||
glm::vec3 &v12 = thrust::get<2>(inout);
|
||||
|
||||
// For xzyLR-[k], k==0 is the left and k==1 is the right.
|
||||
int k = 0;
|
||||
@@ -406,9 +392,9 @@ struct Kernel12 {
|
||||
const Halfedge edge = halfedgesP[p1];
|
||||
|
||||
for (int vert : {edge.startVert, edge.endVert}) {
|
||||
const auto key =
|
||||
forward ? thrust::make_pair(vert, q2) : thrust::make_pair(q2, vert);
|
||||
const int idx = BinarySearch(p0q2, size02, key);
|
||||
const int64_t key = forward ? SparseIndices::EncodePQ(vert, q2)
|
||||
: SparseIndices::EncodePQ(q2, vert);
|
||||
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);
|
||||
@@ -427,9 +413,9 @@ struct Kernel12 {
|
||||
const int q1 = 3 * q2 + i;
|
||||
const Halfedge edge = halfedgesQ[q1];
|
||||
const int q1F = edge.IsForward() ? q1 : edge.pairedHalfedge;
|
||||
const auto key =
|
||||
forward ? thrust::make_pair(p1, q1F) : thrust::make_pair(q1F, p1);
|
||||
const int idx = BinarySearch(p1q1, size11, key);
|
||||
const int64_t key = forward ? SparseIndices::EncodePQ(p1, q1F)
|
||||
: SparseIndices::EncodePQ(q1F, p1);
|
||||
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);
|
||||
@@ -450,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];
|
||||
@@ -465,42 +446,46 @@ struct Kernel12 {
|
||||
}
|
||||
};
|
||||
|
||||
std::tuple<VecDH<int>, VecDH<glm::vec3>> Intersect12(
|
||||
const Manifold::Impl &inP, const Manifold::Impl &inQ, const VecDH<int> &s02,
|
||||
const SparseIndices &p0q2, const VecDH<int> &s11, const SparseIndices &p1q1,
|
||||
const VecDH<float> &z02, const VecDH<glm::vec4> &xyzz11,
|
||||
SparseIndices &p1q2, bool forward, ExecutionPolicy policy) {
|
||||
VecDH<int> x12(p1q2.size());
|
||||
VecDH<glm::vec3> v12(p1q2.size());
|
||||
std::tuple<Vec<int>, Vec<glm::vec3>> Intersect12(
|
||||
const Manifold::Impl &inP, const Manifold::Impl &inQ, const Vec<int> &s02,
|
||||
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(
|
||||
policy,
|
||||
zip(x12.begin(), v12.begin(), p1q2.begin(!forward), p1q2.begin(forward)),
|
||||
autoPolicy(p1q2.size()), zip(countAt(0_z), x12.begin(), v12.begin()),
|
||||
p1q2.size(),
|
||||
Kernel12({p0q2.ptrDpq(), s02.ptrD(), z02.cptrD(), p0q2.size(),
|
||||
p1q1.ptrDpq(), s11.ptrD(), xyzz11.cptrD(), p1q1.size(),
|
||||
inP.halfedge_.cptrD(), inQ.halfedge_.cptrD(),
|
||||
inP.vertPos_.cptrD(), forward}));
|
||||
Kernel12({p0q2.AsVec64(), s02, z02, p1q1.AsVec64(), s11, xyzz11,
|
||||
inP.halfedge_, inQ.halfedge_, inP.vertPos_, forward, p1q2}));
|
||||
|
||||
p1q2.KeepFinite(v12, x12);
|
||||
|
||||
return std::make_tuple(x12, v12);
|
||||
};
|
||||
|
||||
VecDH<int> Winding03(const Manifold::Impl &inP, SparseIndices &p0q2,
|
||||
VecDH<int> &s02, bool reverse, ExecutionPolicy policy) {
|
||||
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.
|
||||
VecDH<int> w03(inP.NumVert(), 0);
|
||||
|
||||
if (!is_sorted(policy, p0q2.begin(reverse), p0q2.end(reverse)))
|
||||
sort_by_key(policy, p0q2.begin(reverse), p0q2.end(reverse), s02.begin());
|
||||
VecDH<int> w03val(w03.size());
|
||||
VecDH<int> w03vert(w03.size());
|
||||
Vec<int> w03(inP.NumVert(), 0);
|
||||
// checking is slow, so just sort and reduce
|
||||
auto policy = autoPolicy(vertices.size());
|
||||
stable_sort(
|
||||
policy, zip(vertices.begin(), s02.begin()),
|
||||
zip(vertices.end(), s02.end()),
|
||||
[](const thrust::tuple<int, int> &a, const thrust::tuple<int, int> &b) {
|
||||
return thrust::get<0>(a) < thrust::get<0>(b);
|
||||
});
|
||||
Vec<int> w03val(w03.size());
|
||||
Vec<int> w03vert(w03.size());
|
||||
// sum known s02 values into w03 (winding number)
|
||||
auto endPair = reduce_by_key<
|
||||
thrust::pair<decltype(w03val.begin()), decltype(w03val.begin())>>(
|
||||
policy, p0q2.begin(reverse), p0q2.end(reverse), s02.begin(),
|
||||
w03vert.begin(), w03val.begin());
|
||||
policy, vertices.begin(), vertices.end(), s02.begin(), w03vert.begin(),
|
||||
w03val.begin());
|
||||
scatter(policy, w03val.begin(), endPair.second, w03vert.begin(), w03.begin());
|
||||
|
||||
if (reverse)
|
||||
@@ -513,10 +498,7 @@ VecDH<int> Winding03(const Manifold::Impl &inP, SparseIndices &p0q2,
|
||||
namespace manifold {
|
||||
Boolean3::Boolean3(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
OpType op)
|
||||
: inP_(inP),
|
||||
inQ_(inQ),
|
||||
expandP_(op == OpType::Add ? 1.0 : -1.0),
|
||||
policy_(autoPolicy(glm::max(inP.NumEdge(), inQ.NumEdge()))) {
|
||||
: inP_(inP), inQ_(inQ), expandP_(op == OpType::Add ? 1.0 : -1.0) {
|
||||
// Symbolic perturbation:
|
||||
// Union -> expand inP
|
||||
// Difference, Intersection -> contract inP
|
||||
@@ -536,29 +518,26 @@ Boolean3::Boolean3(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
// Level 3
|
||||
// Find edge-triangle overlaps (broad phase)
|
||||
p1q2_ = inQ_.EdgeCollisions(inP_);
|
||||
p2q1_ = inP_.EdgeCollisions(inQ_);
|
||||
p2q1_ = inP_.EdgeCollisions(inQ_, true); // inverted
|
||||
|
||||
policy_ = autoPolicy(glm::max(p1q2_.size(), p2q1_.size()));
|
||||
p1q2_.Sort(policy_);
|
||||
p1q2_.Sort();
|
||||
PRINT("p1q2 size = " << p1q2_.size());
|
||||
|
||||
p2q1_.SwapPQ();
|
||||
p2q1_.Sort(policy_);
|
||||
p2q1_.Sort();
|
||||
PRINT("p2q1 size = " << p2q1_.size());
|
||||
|
||||
// Level 2
|
||||
// Find vertices that overlap faces in XY-projection
|
||||
SparseIndices p0q2 = inQ.VertexCollisionsZ(inP.vertPos_);
|
||||
p0q2.Sort(policy_);
|
||||
p0q2.Sort();
|
||||
PRINT("p0q2 size = " << p0q2.size());
|
||||
|
||||
SparseIndices p2q0 = inP.VertexCollisionsZ(inQ.vertPos_);
|
||||
p2q0.SwapPQ();
|
||||
p2q0.Sort(policy_);
|
||||
SparseIndices p2q0 = inP.VertexCollisionsZ(inQ.vertPos_, true); // inverted
|
||||
p2q0.Sort();
|
||||
PRINT("p2q0 size = " << p2q0.size());
|
||||
|
||||
// Find involved edge pairs from Level 3
|
||||
SparseIndices p1q1 = Filter11(inP_, inQ_, p1q2_, p2q1_, policy_);
|
||||
SparseIndices p1q1 = Filter11(inP_, inQ_, p1q2_, p2q1_);
|
||||
PRINT("p1q1 size = " << p1q1.size());
|
||||
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
@@ -570,39 +549,46 @@ Boolean3::Boolean3(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
// Level 2
|
||||
// Build up XY-projection intersection of two edges, including the z-value for
|
||||
// each edge, keeping only those whose intersection exists.
|
||||
VecDH<int> s11;
|
||||
VecDH<glm::vec4> xyzz11;
|
||||
std::tie(s11, xyzz11) = Shadow11(p1q1, inP, inQ, expandP_, policy_);
|
||||
Vec<int> s11;
|
||||
Vec<glm::vec4> xyzz11;
|
||||
std::tie(s11, xyzz11) = Shadow11(p1q1, inP, inQ, expandP_);
|
||||
PRINT("s11 size = " << s11.size());
|
||||
|
||||
// Build up Z-projection of vertices onto triangles, keeping only those that
|
||||
// fall inside the triangle.
|
||||
VecDH<int> s02;
|
||||
VecDH<float> z02;
|
||||
std::tie(s02, z02) = Shadow02(inP, inQ, p0q2, true, expandP_, policy_);
|
||||
Vec<int> s02;
|
||||
Vec<float> z02;
|
||||
std::tie(s02, z02) = Shadow02(inP, inQ, p0q2, true, expandP_);
|
||||
PRINT("s02 size = " << s02.size());
|
||||
|
||||
VecDH<int> s20;
|
||||
VecDH<float> z20;
|
||||
std::tie(s20, z20) = Shadow02(inQ, inP, p2q0, false, expandP_, policy_);
|
||||
Vec<int> s20;
|
||||
Vec<float> z20;
|
||||
std::tie(s20, z20) = Shadow02(inQ, inP, p2q0, false, expandP_);
|
||||
PRINT("s20 size = " << s20.size());
|
||||
|
||||
// Level 3
|
||||
// Build up the intersection of the edges and triangles, keeping only those
|
||||
// that intersect, and record the direction the edge is passing through the
|
||||
// triangle.
|
||||
std::tie(x12_, v12_) = Intersect12(inP, inQ, s02, p0q2, s11, p1q1, z02,
|
||||
xyzz11, p1q2_, true, policy_);
|
||||
std::tie(x12_, v12_) =
|
||||
Intersect12(inP, inQ, s02, p0q2, s11, p1q1, z02, xyzz11, p1q2_, true);
|
||||
PRINT("x12 size = " << x12_.size());
|
||||
|
||||
std::tie(x21_, v21_) = Intersect12(inQ, inP, s20, p2q0, s11, p1q1, z20,
|
||||
xyzz11, p2q1_, false, policy_);
|
||||
std::tie(x21_, v21_) =
|
||||
Intersect12(inQ, inP, s20, p2q0, s11, p1q1, z20, xyzz11, p2q1_, false);
|
||||
PRINT("x21 size = " << x21_.size());
|
||||
|
||||
// Sum up the winding numbers of all vertices.
|
||||
w03_ = Winding03(inP, p0q2, s02, false, policy_);
|
||||
if (x12_.size() + x21_.size() >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
|
||||
w30_ = Winding03(inQ, p2q0, s20, true, policy_);
|
||||
Vec<int> p0 = p0q2.Copy(false);
|
||||
p0q2.Resize(0);
|
||||
Vec<int> q0 = p2q0.Copy(true);
|
||||
p2q0.Resize(0);
|
||||
// Sum up the winding numbers of all vertices.
|
||||
w03_ = Winding03(inP, p0, s02, false);
|
||||
|
||||
w30_ = Winding03(inQ, q0, s20, true);
|
||||
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
intersections.Stop();
|
||||
@@ -610,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
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ class Boolean3 {
|
||||
const Manifold::Impl &inP_, &inQ_;
|
||||
const float expandP_;
|
||||
SparseIndices p1q2_, p2q1_;
|
||||
VecDH<int> x12_, x21_, w03_, w30_;
|
||||
VecDH<glm::vec3> v12_, v21_;
|
||||
ExecutionPolicy policy_;
|
||||
Vec<int> x12_, x21_, w03_, w30_;
|
||||
Vec<glm::vec3> v12_, v21_;
|
||||
};
|
||||
} // namespace manifold
|
||||
|
||||