Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e7a576cea | ||
|
|
80a5f2e0c5 | ||
|
|
67c4647464 | ||
|
|
edd149e753 | ||
|
|
b5c175162a | ||
|
|
1df22bee48 | ||
|
|
d5c0418309 | ||
|
|
7a5d5421d4 | ||
|
|
69437096d0 | ||
|
|
3931b3296d | ||
|
|
62d0f25ce0 | ||
|
|
1bd16f4c02 | ||
|
|
3afe846570 | ||
|
|
12b9494fdd | ||
|
|
0577aabe7f | ||
|
|
bdae9dd11a | ||
|
|
5f4059ab1f | ||
|
|
9cb3969f70 | ||
|
|
4fb3520287 | ||
|
|
376d317f14 | ||
|
|
bf18c9f759 | ||
|
|
cdd1475fe1 | ||
|
|
55a0221f9b | ||
|
|
d78b1e2042 | ||
|
|
562ff02232 | ||
|
|
8e2b1dd485 | ||
|
|
5ef09daf45 | ||
|
|
6d932a2c7f | ||
|
|
fd81cd6172 | ||
|
|
6b4a4ed841 | ||
|
|
887f631b8c | ||
|
|
443863d09e | ||
|
|
5301542254 | ||
|
|
a122bde778 | ||
|
|
c9810fd517 | ||
|
|
4c57a56355 | ||
|
|
cdcbd329e6 | ||
|
|
c738f1eea1 | ||
|
|
a8b8bf71b1 | ||
|
|
c60df1b704 | ||
|
|
8d67ba3a3a | ||
|
|
3258152e2c | ||
|
|
78729599a5 | ||
|
|
32eefe9157 | ||
|
|
3e05284250 | ||
|
|
c9ec16969d | ||
|
|
9b1d255931 | ||
|
|
22d94d5688 | ||
|
|
e073141f14 | ||
|
|
f54e535c6a | ||
|
|
c6801c82ea | ||
|
|
a67aa13133 | ||
|
|
d43709715d | ||
|
|
2ddbd0a1a0 | ||
|
|
dc4afa950b | ||
|
|
c03a5ac3ad | ||
|
|
cae5a01af1 | ||
|
|
d59b9b93bd | ||
|
|
f3c5ada9f0 | ||
|
|
4395ab622c | ||
|
|
994956d7c3 | ||
|
|
79f735c59a | ||
|
|
dd67e992bd | ||
|
|
5bdb8bc233 | ||
|
|
c9ae400647 | ||
|
|
9c1517e54e | ||
|
|
3b8282e1d5 | ||
|
|
1635864cbc | ||
|
|
65b2ef8198 | ||
|
|
a9055a8abf | ||
|
|
0b7ee87489 | ||
|
|
9690305b8d | ||
|
|
aa88959899 | ||
|
|
dbb0ca76b4 | ||
|
|
9018b2ac45 | ||
|
|
f407e43e5b | ||
|
|
d7cbcb8c2e | ||
|
|
e27c9c100f | ||
|
|
8ad5f8e427 | ||
|
|
afc64f248a | ||
|
|
c0fb3766e4 | ||
|
|
887a5021af | ||
|
|
e83b9bd158 | ||
|
|
2d0c705839 | ||
|
|
b36ec2ce0e | ||
|
|
b8d143dd72 | ||
|
|
be022a58e4 | ||
|
|
30e0af942a | ||
|
|
8c77a6aa05 | ||
|
|
54fa822408 | ||
|
|
7d564cfbc1 | ||
|
|
bc0bded294 | ||
|
|
d52decc4f1 | ||
|
|
8060376e12 | ||
|
|
eba866d266 | ||
|
|
cc4966f062 | ||
|
|
3216a24be8 | ||
|
|
5e566c8c36 | ||
|
|
80edfee978 | ||
|
|
0e0d1c4f69 | ||
|
|
e15f4cf375 | ||
|
|
c9322ca63a | ||
|
|
45826d6c47 | ||
|
|
dd781cdbdf | ||
|
|
638badaf9b | ||
|
|
0149e482bc | ||
|
|
f683e8c9c0 | ||
|
|
1f8e8037f1 | ||
|
|
10a15c1dd4 | ||
|
|
6e1629ddce | ||
|
|
e273f6b09d | ||
|
|
67c468211d | ||
|
|
01c38e42ca | ||
|
|
1991232eb7 | ||
|
|
4551288f95 | ||
|
|
2e29784449 | ||
|
|
2a2cb6f88e | ||
|
|
1a91d8db2a | ||
|
|
82b98d9353 | ||
|
|
7273c9bf0f | ||
|
|
4ff3f7cf68 | ||
|
|
fafe4792da | ||
|
|
b439603eb9 | ||
|
|
b6eea01021 | ||
|
|
b9f7228064 | ||
|
|
0ef5f70434 | ||
|
|
bb6acf1f6e | ||
|
|
5930b5c09d | ||
|
|
a842de97c6 | ||
|
|
2942229b49 |
@@ -1,6 +1,9 @@
|
||||
name: build_wheels
|
||||
|
||||
on: [release, workflow_dispatch]
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_wheels:
|
||||
@@ -8,17 +11,49 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest]
|
||||
os: [ubuntu-22.04, windows-2019, macos-latest]
|
||||
steps:
|
||||
- run: |
|
||||
git config --global submodule.fetchJobs 8
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build wheels
|
||||
uses: pypa/cibuildwheel@v2.15.0
|
||||
- uses: actions/upload-artifact@v3
|
||||
uses: pypa/cibuildwheel@v2.19
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-${{ matrix.os }}
|
||||
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:
|
||||
name: python-sdist
|
||||
path: dist/*.tar.gz
|
||||
|
||||
pypi-publish:
|
||||
name: Upload release to PyPI
|
||||
needs: [build_wheels, make_sdist]
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
pattern: python-*
|
||||
merge-multiple: true
|
||||
path: dist
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
@@ -11,11 +11,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: DoozyX/clang-format-lint-action@v0.14
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DoozyX/clang-format-lint-action@v0.17
|
||||
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
|
||||
|
||||
@@ -16,24 +16,31 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
parallel_backend: [NONE, TBB]
|
||||
runs-on: ubuntu-22.04
|
||||
gcc: [13, 14]
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
CC: gcc-${{ matrix.gcc }}
|
||||
CXX: g++-${{ matrix.gcc }}
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y libglm-dev libgtest-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip lcov
|
||||
pip install trimesh
|
||||
- uses: actions/checkout@v3
|
||||
sudo apt-get update
|
||||
sudo apt-get install libglm-dev libgtest-dev libassimp-dev git libtbb-dev libthrust-dev pkg-config libpython3-dev lcov
|
||||
python -m pip install -U trimesh pytest
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- name: Build ${{matrix.backend}}
|
||||
if: matrix.parallel_backend != 'NONE'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
- name: Test ${{matrix.parallel_backend}}
|
||||
if: matrix.parallel_backend != 'NONE'
|
||||
run: |
|
||||
@@ -42,6 +49,7 @@ jobs:
|
||||
./manifold_test
|
||||
cd ../../
|
||||
python3 bindings/python/examples/run_all.py -e
|
||||
python3 -m pytest
|
||||
- name: Coverage Report
|
||||
# only do code coverage for default sequential backend, it seems that TBB
|
||||
# backend will cause failure
|
||||
@@ -51,19 +59,26 @@ jobs:
|
||||
mkdir build
|
||||
cd build
|
||||
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
|
||||
lcov --capture --gcov-tool gcov-${{ matrix.gcc }} --ignore-errors mismatch --initial --directory . --output-file ./code_coverage_init.info
|
||||
cd test
|
||||
./manifold_test
|
||||
cd ../
|
||||
lcov --capture --directory . --output-file ./code_coverage_test.info
|
||||
lcov --capture --gcov-tool gcov-${{ matrix.gcc }} --ignore-errors mismatch --directory . --output-file ./code_coverage_test.info
|
||||
lcov --add-tracefile ./code_coverage_init.info --add-tracefile ./code_coverage_test.info --output-file ./code_coverage_total.info
|
||||
lcov --remove ./code_coverage_total.info '/usr/*' '*/third_party/*' '*/test/*' '*/extras/*' '*/bindings/*' --output-file ./code_coverage.info
|
||||
- uses: codecov/codecov-action@v2
|
||||
cd ../
|
||||
- uses: codecov/codecov-action@v4
|
||||
if: matrix.parallel_backend == 'NONE'
|
||||
with:
|
||||
files: build/code_coverage.info
|
||||
fail_ci_if_error: false
|
||||
name: ${{matrix.parallel_backend}}
|
||||
- name: test cmake consumer
|
||||
run: |
|
||||
cd build
|
||||
sudo cmake --install .
|
||||
cd ..
|
||||
./test-cmake.sh
|
||||
|
||||
build_cbind:
|
||||
timeout-minutes: 30
|
||||
@@ -74,15 +89,15 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y libgtest-dev libglm-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- name: Build C bindings with TBB
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=TBB .. && make
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=TBB .. && make
|
||||
- name: Test ${{matrix.parallel_backend}}
|
||||
run: |
|
||||
cd build/test
|
||||
@@ -97,7 +112,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs libglm-dev
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup WASM
|
||||
@@ -105,9 +120,9 @@ jobs:
|
||||
# setup emscripten
|
||||
git clone https://github.com/emscripten-core/emsdk.git
|
||||
cd emsdk
|
||||
./emsdk install latest
|
||||
./emsdk activate latest
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
./emsdk install 3.1.45
|
||||
./emsdk activate 3.1.45
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- name: Build WASM
|
||||
run: |
|
||||
source ./emsdk/emsdk_env.sh
|
||||
@@ -124,12 +139,14 @@ jobs:
|
||||
npm ci
|
||||
npm run build
|
||||
npm test
|
||||
cp ../manifold.* ./dist/
|
||||
- name: Upload WASM files
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wasm
|
||||
path: bindings/wasm/examples/dist/
|
||||
retention-days: 90
|
||||
overwrite: true
|
||||
|
||||
build_windows:
|
||||
timeout-minutes: 30
|
||||
@@ -140,22 +157,29 @@ jobs:
|
||||
runs-on: windows-2019
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- name: Build ${{matrix.backend}}
|
||||
shell: powershell
|
||||
run: |
|
||||
cmake . -DCMAKE_BUILD_TYPE=Release -B build -DMANIFOLD_DEBUG=ON -DPYBIND11_FINDPYTHON=OFF -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64
|
||||
cmake . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64 -B build
|
||||
cd build
|
||||
cmake --build . --target ALL_BUILD --config Release
|
||||
- name: Test ${{matrix.parallel_backend}}
|
||||
shell: bash
|
||||
run: |
|
||||
cd build/test
|
||||
./Release/manifold_test.exe
|
||||
cd build/bin/Release
|
||||
./manifold_test.exe
|
||||
cd ../../
|
||||
- name: test cmake consumer
|
||||
run: |
|
||||
cd build
|
||||
cmake --install .
|
||||
cd ..
|
||||
./test-cmake.sh
|
||||
|
||||
build_mxe:
|
||||
timeout-minutes: 30
|
||||
@@ -166,7 +190,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container: openscad/mxe-x86_64-gui:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Build
|
||||
@@ -190,23 +214,30 @@ jobs:
|
||||
- name: Install common dependencies
|
||||
run: |
|
||||
brew install pkg-config googletest assimp
|
||||
pip install trimesh
|
||||
pip install trimesh pytest
|
||||
- name: Install TBB
|
||||
if: matrix.parallel_backend == 'TBB'
|
||||
run: brew install tbb
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: jwlawson/actions-setup-cmake@v1.12
|
||||
- uses: jwlawson/actions-setup-cmake@v2
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
|
||||
- name: Test
|
||||
run: |
|
||||
cd build/test
|
||||
./manifold_test
|
||||
cd ../../
|
||||
- name: test cmake consumer
|
||||
run: |
|
||||
cd build
|
||||
sudo cmake --install .
|
||||
cd ..
|
||||
./test-cmake.sh
|
||||
|
||||
build_nix:
|
||||
timeout-minutes: 30
|
||||
@@ -216,7 +247,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: cachix/install-nix-action@v22
|
||||
@@ -227,7 +258,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: cachix/install-nix-action@v22
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: publish_npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish_npm:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Download built examples
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: manifold.yml
|
||||
workflow_conclusion: completed
|
||||
name: wasm
|
||||
path: ./bindings/wasm/
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
cd ./bindings/wasm/
|
||||
npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Release File
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
upload_archive:
|
||||
name: Upload archive
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
ref=${{ github.ref_name }}
|
||||
echo "release_name=manifold-${ref#v}" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: ${{ env.release_name }}
|
||||
submodules: recursive
|
||||
- name: Build archive
|
||||
run: >
|
||||
tar --exclude=".git*" -cz ${{ env.release_name }} -f ${{ env.release_name }}.tar.gz
|
||||
- name: Log checksum
|
||||
run: >
|
||||
sha256sum ${{ env.release_name }}.tar.gz
|
||||
- name: Add file to release
|
||||
run: |
|
||||
cd ${{ env.release_name }} || exit 1
|
||||
gh release upload ${{ github.ref_name }} ../${{ env.release_name }}.tar.gz
|
||||
env:
|
||||
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
@@ -1,6 +1,3 @@
|
||||
[submodule "src/third_party/clipper2"]
|
||||
path = src/third_party/clipper2
|
||||
url = https://github.com/AngusJohnson/clipper2
|
||||
[submodule "src/third_party/quickhull"]
|
||||
path = src/third_party/quickhull
|
||||
url = https://github.com/akuukka/quickhull
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
{
|
||||
"name": "manifold test",
|
||||
"type": "cppdbg",
|
||||
"MIMode": "lldb",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/build/test/manifold_test",
|
||||
"args": [
|
||||
"--gtest_break_on_failure",
|
||||
"--gtest_filter=Manifold.GetMesh"
|
||||
"--gtest_filter=Smooth.RefineQuads"
|
||||
],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}/build/test",
|
||||
@@ -20,7 +21,8 @@
|
||||
"name": "MALLOC_CHECK_",
|
||||
"value": "2"
|
||||
}
|
||||
]
|
||||
],
|
||||
"externalConsole": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -108,16 +108,21 @@
|
||||
"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,
|
||||
"cmake.configureArgs": [
|
||||
"-DBUILD_SHARED_LIBS=ON",
|
||||
"-DMANIFOLD_CBIND=ON",
|
||||
"-DMANIFOLD_PYBIND=ON",
|
||||
"-DMANIFOLD_EXPORT=ON",
|
||||
"-DMANIFOLD_DEBUG=ON",
|
||||
"-DMANIFOLD_USE_CUDA=OFF",
|
||||
"-DMANIFOLD_PAR=TBB",
|
||||
"-DCODE_COVERAGE=OFF",
|
||||
"-DCMAKE_CXX_FLAGS=''" //'-fsanitize=address,undefined'"
|
||||
|
||||
@@ -21,11 +21,26 @@ set(MANIFOLD_PAR "NONE" CACHE STRING "Parallel backend, either \"TBB\" or \"NONE
|
||||
set(MANIFOLD_FLAGS "" CACHE STRING "Manifold compiler flags")
|
||||
option(MANIFOLD_EXPORT "Build mesh export (via assimp) utility library" OFF)
|
||||
option(MANIFOLD_TEST "Enable testing suite" ON)
|
||||
# fuzztest is a rather large dependency
|
||||
option(MANIFOLD_FUZZ "Enable fuzzing tests" OFF)
|
||||
option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF)
|
||||
|
||||
option(MANIFOLD_PYBIND "Build python bindings" ON)
|
||||
option(MANIFOLD_CBIND "Build C (FFI) bindings" OFF)
|
||||
option(MANIFOLD_PYBIND "Build python bindings" OFF)
|
||||
option(MANIFOLD_CBIND "Build C (FFI) bindings" ON)
|
||||
option(MANIFOLD_JSBIND "Build js binding" ${EMSCRIPTEN})
|
||||
option(BUILD_SHARED_LIBS "Build shared library" ON)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
if(MANIFOLD_FUZZ)
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
message(FATAL_ERROR "fuzztest only supports clang" )
|
||||
endif()
|
||||
# we should enable debug checks
|
||||
set(MANIFOLD_DEBUG ON)
|
||||
# enable fuzztest fuzzing mode
|
||||
set(FUZZTEST_FUZZING_MODE ON)
|
||||
# address sanitizer required
|
||||
set(CMAKE_CXX_FLAGS "$CMAKE_CXX_FLAGS -fsanitize=address")
|
||||
endif()
|
||||
|
||||
option(TRACY_ENABLE "Use tracy profiling" OFF)
|
||||
option(TRACY_MEMORY_USAGE "Track memory allocation with tracy (expensive)" OFF)
|
||||
@@ -47,15 +62,18 @@ if(EMSCRIPTEN)
|
||||
set(MANIFOLD_FLAGS -fexceptions -D_LIBCUDACXX_HAS_THREAD_API_EXTERNAL -D_LIBCUDACXX_HAS_THREAD_API_CUDA)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -sALLOW_MEMORY_GROWTH=1 -fexceptions -sDISABLE_EXCEPTION_CATCHING=0")
|
||||
set(MANIFOLD_PYBIND OFF)
|
||||
set(BUILD_SHARED_LIBS OFF)
|
||||
endif()
|
||||
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/manifoldDeps.cmake)
|
||||
|
||||
if(MANIFOLD_PYBIND)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if(Python_VERSION VERSION_LESS 3.12)
|
||||
find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
|
||||
else()
|
||||
find_package(Python COMPONENTS Interpreter Development.SABIModule REQUIRED)
|
||||
endif()
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
endif()
|
||||
|
||||
if(CMAKE_EXPORT_COMPILE_COMMANDS AND NOT EMSCRIPTEN)
|
||||
@@ -65,14 +83,15 @@ if(CMAKE_EXPORT_COMPILE_COMMANDS AND NOT EMSCRIPTEN)
|
||||
endif()
|
||||
|
||||
if (MSVC)
|
||||
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} /DNOMINMAX)
|
||||
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} /DNOMINMAX /bigobj)
|
||||
else()
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused -Wno-array-bounds -Wno-stringop-overflow)
|
||||
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused -Wno-array-bounds
|
||||
-Wno-stringop-overflow -Wno-alloc-size-larger-than)
|
||||
else()
|
||||
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
|
||||
endif()
|
||||
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${WARNING_FLAGS} -O3)
|
||||
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${WARNING_FLAGS})
|
||||
endif()
|
||||
|
||||
if(CODE_COVERAGE AND NOT MSVC)
|
||||
@@ -81,7 +100,16 @@ if(CODE_COVERAGE AND NOT MSVC)
|
||||
add_link_options("-coverage")
|
||||
endif()
|
||||
|
||||
include(GNUInstallDirs)
|
||||
# installation rpath
|
||||
set(CMAKE_SKIP_BUILD_RPATH FALSE)
|
||||
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
|
||||
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
|
||||
list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/lib" isSystemDir)
|
||||
if("${isSystemDir}" STREQUAL "-1")
|
||||
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
endif("${isSystemDir}" STREQUAL "-1")
|
||||
|
||||
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(bindings)
|
||||
@@ -95,3 +123,29 @@ if(MANIFOLD_TEST)
|
||||
add_subdirectory(test)
|
||||
add_subdirectory(extras)
|
||||
endif()
|
||||
|
||||
# installation related
|
||||
|
||||
set(MANIFOLD_VERSION_MAJOR 2)
|
||||
set(MANIFOLD_VERSION_MINOR 5)
|
||||
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,35 +1,36 @@
|
||||
[](https://codecov.io/github/elalish/manifold)
|
||||
[](https://badge.fury.io/py/manifold3d)
|
||||
[](https://badge.fury.io/js/manifold-3d)
|
||||
[](https://twitter.com/intent/follow?screen_name=manifoldcad)
|
||||
|
||||
## Users
|
||||
|
||||
[OpenSCAD](https://openscad.org/), [IFCjs](https://ifcjs.github.io/info/), [Grid.Space](https://grid.space/), and [OCADml](https://github.com/OCADml/OManifold) have all integrated our Manifold geometry kernel! Why? Because its reliability is guaranteed and it's 1,000 times faster than other libraries. See our [usage](https://github.com/elalish/manifold/discussions/340) and [performance](https://github.com/elalish/manifold/discussions/383) discussions for all the latest and to add your own projects & analyses.
|
||||
|
||||
For example, here is a log-log plot of Manifold's performance vs. earlier OpenSCAD geometry backends:
|
||||
## Manifold Frontend Sandboxes
|
||||
|
||||
<img src="https://elalish.github.io/manifold/samples/models/perfSpheres.png" width="350px"/>
|
||||
### [ManifoldCAD.org](https://manifoldcad.org)
|
||||
|
||||
## [ManifoldCAD.org](https://manifoldcad.org)
|
||||
If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid modelling web app where you script in JS/TS. This uses our npm package, [manifold-3d](https://www.npmjs.com/package/manifold-3d), built via WASM. It's not quite as fast as our raw C++, but it's hard to beat for interoperability.
|
||||
|
||||
If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid modelling web app. Our WASM is not multithreaded yet, but it's still quite fast and a good way to test out our Manifold library.
|
||||
*Note for Firefox users: If you find the editor is stuck on **Loading...**, setting
|
||||
`dom.workers.modules.enabled: true` in your `about:config`, as mentioned in
|
||||
[issue#328](https://github.com/elalish/manifold/issues/328#issuecomment-1473847102)
|
||||
may solve the problem.*
|
||||
|
||||
### [Python Colab Example](https://colab.research.google.com/drive/1VxrFYHPSHZgUbl9TeWzCeovlpXrPQ5J5?usp=sharing)
|
||||
|
||||
If you prefer Python to JS/TS, make your own copy of the example notebook above. It demonstrates interop between our [`manifold3d`](https://pypi.org/project/manifold3d/) PyPI library and the popular [`trimesh`](https://pypi.org/project/trimesh/) library, including showing the interactive model right in the notebook and saving 3D model output.
|
||||
|
||||

|
||||
|
||||
### Note for Firefox users
|
||||
|
||||
If you find the editor is stuck on **Loading...**, setting
|
||||
`dom.workers.modules.enabled: true` in your `about:config`, as mentioned in the
|
||||
discussion of the
|
||||
[issue#328](https://github.com/elalish/manifold/issues/328#issuecomment-1473847102)
|
||||
of this repository may solve the problem.
|
||||
|
||||
# Manifold
|
||||
|
||||
[**API Documentation**](https://elalish.github.io/manifold/docs/html/modules.html) | [**Algorithm Documentation**](https://github.com/elalish/manifold/wiki/Manifold-Library) | [**Blog Posts**](https://elalish.blogspot.com/search/label/Manifold) | [**Web Examples**](https://elalish.github.io/manifold/model-viewer.html)
|
||||
[**API Documentation**](https://elalish.github.io/manifold/docs/html/topics.html) | [**Algorithm Documentation**](https://github.com/elalish/manifold/wiki/Manifold-Library) | [**Blog Posts**](https://elalish.blogspot.com/search/label/Manifold) | [**Web Examples**](https://elalish.github.io/manifold/model-viewer.html)
|
||||
|
||||
[Manifold](https://github.com/elalish/manifold) is a geometry library dedicated to creating and operating on manifold triangle meshes. A [manifold mesh](https://github.com/elalish/manifold/wiki/Manifold-Library#manifoldness) is a mesh that represents a solid object, and so is very important in manufacturing, CAD, structural analysis, etc. Further information can be found on the [wiki](https://github.com/elalish/manifold/wiki/Manifold-Library).
|
||||
|
||||
This is a modern C++ library that Github's CI verifies builds and runs on a variety of platforms. Additionally, we build bindings for JavaScript ([manifold-3d](https://www.npmjs.com/package/manifold-3d) on npm), Python, and C to make this library more portable and easy to use.
|
||||
This is a modern C++ library that Github's CI verifies builds and runs on a variety of platforms. Additionally, we build bindings for JavaScript ([manifold-3d](https://www.npmjs.com/package/manifold-3d) on npm), Python ([manifold3d](https://pypi.org/project/manifold3d/)), and C to make this library more portable and easy to use.
|
||||
|
||||
System Dependencies (note that we will automatically download the dependency if there is no such package on the system):
|
||||
- [`GLM`](https://github.com/g-truc/glm/): A compact header-only vector library.
|
||||
@@ -87,6 +88,10 @@ Offline building:
|
||||
The build instructions used by our CI are in [manifold.yml](https://github.com/elalish/manifold/blob/master/.github/workflows/manifold.yml), which is a good source to check if something goes wrong and for instructions specific to other platforms, like Windows.
|
||||
|
||||
### WASM
|
||||
|
||||
> Note that we have only tested emscripten version 3.1.45. It is known that
|
||||
3.1.48 has some issues compiling manifold.
|
||||
|
||||
To build the JS WASM library, first install NodeJS and set up emscripten:
|
||||
|
||||
(on Mac):
|
||||
@@ -120,7 +125,7 @@ use the extension, please add `$BUILD_DIR/bindings/python` to your `PYTHONPATH`,
|
||||
can be found in `bindings/python/examples`. To see exported samples, run:
|
||||
```
|
||||
sudo apt install pkg-config libpython3-dev python3 python3-distutils python3-pip
|
||||
pip install trimesh
|
||||
pip install trimesh pytest
|
||||
python3 run_all.py -e
|
||||
```
|
||||
|
||||
@@ -134,14 +139,51 @@ python binding documentation:
|
||||
|
||||
For more detailed documentation, please refer to the C++ API.
|
||||
|
||||
### Java / Clojure
|
||||
|
||||
Unofficial java bindings are currently maintained in [a fork](https://github.com/SovereignShop/manifold).
|
||||
|
||||
There is also a Clojure [library](https://github.com/SovereignShop/clj-manifold3d).
|
||||
|
||||
### Windows Shenanigans
|
||||
|
||||
Windows users should build with `-DBUILD_SHARED_LIBS=OFF`, as enabling shared
|
||||
libraries in general makes things very complicated.
|
||||
|
||||
The DLL file for manifoldc (C FFI bindings) when built with msvc is in `${CMAKE_BINARY_DIR}/bin/${BUILD_TYPE}/manifoldc.dll`.
|
||||
For example, for the following command, the path relative to the project root directory is `build/bin/Release/manifoldc.dll`.
|
||||
```sh
|
||||
cmake . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64 -B build
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! A lower barrier contribution is to simply make a PR that adds a test, especially if it repros an issue you've found. Simply name it prepended with DISABLED_, so that it passes the CI. That will be a very strong signal to me to fix your issue. However, if you know how to fix it yourself, then including the fix in your PR would be much appreciated!
|
||||
|
||||
## Profiling
|
||||
### Formatting
|
||||
|
||||
There is a formatting script `format.sh` that automatically formats everything.
|
||||
It requires clang-format 11 and black formatter for python.
|
||||
|
||||
If you have clang-format installed but without clang-11, you can specify the
|
||||
clang-format executable by setting the `CLANG_FORMAT` environment variable.
|
||||
|
||||
### Profiling
|
||||
|
||||
There is now basic support for the [Tracy profiler](https://github.com/wolfpld/tracy) for our tests.
|
||||
To enable tracing, compile with `-DTRACY_ENABLE=on` cmake option, and run the test with Tracy server running.
|
||||
To enable memory profiling in addition to tracing, compile with `-DTRACY_MEMORY_USAGE=ON` in addition to `-DTRACY_ENABLE=ON`.
|
||||
|
||||
### Fuzzing Support
|
||||
|
||||
We use https://github.com/google/fuzztest for fuzzing the triangulator.
|
||||
|
||||
To enable fuzzing, make sure that you are using clang compiler (`-DCMAKE_CXX_COMPILER=clang -DCMAKE_C_COMPILER=clang`), running Linux, and enable fuzzing support by setting `-DMANIFOLD_FUZZ=ON`.
|
||||
|
||||
To run the fuzzer and minimize testcase, do
|
||||
```
|
||||
../minimizer.sh ./test/polygon_fuzz --fuzz=PolygonFuzz.TriangulationNoCrash
|
||||
```
|
||||
|
||||
## About the author
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
1. Go through Github issues to verify bugs have been fixed and closed.
|
||||
1. Verify [ManifoldCAD.org](https://manifoldcad.org) - check a few examples, run them, download a GLB and a 3MF.
|
||||
1. Verify our three.js [example](https://manifoldcad.org/three) is functional.
|
||||
1. Verify our model-viewer [example](https://manifoldcad.org/model-viewer) is functional - select Union and Intersection.
|
||||
1. Verify [make-manifold](https://manifoldcad.org/make-manifold) is functional. Try dropping [DragonAttenuation.glb](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/DragonAttenuation/glTF-Binary/DragonAttenuation.glb) in and verify you can select "View Manifold GLB" and that the dragon is still present while the backdrop is removed. Download the GLB.
|
||||
1. Make a new branch called the version, e.g. v2.3.0.
|
||||
1. Use VSCode to search and replace the old version with the new - so far in test-cmake.sh, flake.nix, pyproject.toml, and package.json.
|
||||
1. Also update CMakeLists.txt version by searching for "set(MANIFOLD_VERSION_".
|
||||
1. Commit, push, open a PR, verify tests pass, merge.
|
||||
1. On Github, draft a new release, make a new tag with the version number, add release notes, and publish.
|
||||
1. Check the Actions and verify that both PyPI and npm publishing actions ran successfully.
|
||||
1. Verify the npm [package](https://www.npmjs.com/package/manifold-3d?activeTab=code) looks good - unpacked size should be close to 1MB.
|
||||
1. Verify PyPI [package](https://pypi.org/project/manifold3d/#files) looks good - a bunch of built distributions ranging from ~600kB to ~1.1MB.
|
||||
1. If there's a problem with release deployment, the release workflows can be triggered separately, manually for any branch, under the Actions tab.
|
||||
@@ -1,33 +1,31 @@
|
||||
project(manifoldc)
|
||||
|
||||
add_library(${PROJECT_NAME} manifoldc.cpp conv.cpp)
|
||||
# this is a shared library for FFI bindings
|
||||
add_library(${PROJECT_NAME} SHARED manifoldc.cpp conv.cpp)
|
||||
|
||||
if(MANIFOLD_USE_CUDA)
|
||||
set_source_files_properties(manifoldc.cpp conv.cpp PROPERTIES LANGUAGE CUDA)
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
|
||||
if(MSVC)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
|
||||
${CMAKE_BINARY_DIR}/bin )
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
${CMAKE_BINARY_DIR}/bin )
|
||||
endif()
|
||||
|
||||
if(MANIFOLD_EXPORT)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE meshIO)
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC -DMANIFOLD_EXPORT)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE meshIO)
|
||||
target_compile_options(${PROJECT_NAME} PUBLIC -DMANIFOLD_EXPORT)
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE manifold sdf cross_section
|
||||
${PROJECT_NAME}
|
||||
PRIVATE manifold sdf cross_section
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT bindings
|
||||
)
|
||||
install(
|
||||
FILES include/manifoldc.h include/types.h
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/manifoldc.h include/types.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -182,9 +182,13 @@ ManifoldCrossSection *manifold_cross_section_transform(void *mem,
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_cross_section_warp(
|
||||
void *mem, ManifoldCrossSection *cs, ManifoldVec2 (*fun)(float, float)) {
|
||||
std::function<void(glm::vec2 & v)> warp = [fun](glm::vec2 &v) {
|
||||
v = from_c(fun(v.x, v.y));
|
||||
void *mem, ManifoldCrossSection *cs,
|
||||
ManifoldVec2 (*fun)(float, float, void *), void *ctx) {
|
||||
// Bind function with context argument to one without
|
||||
using namespace std::placeholders;
|
||||
std::function<ManifoldVec2(float, float)> f2 = std::bind(fun, _1, _2, ctx);
|
||||
std::function<void(glm::vec2 & v)> warp = [f2](glm::vec2 &v) {
|
||||
v = from_c(f2(v.x, v.y));
|
||||
};
|
||||
auto warped = from_c(cs)->Warp(warp);
|
||||
return to_c(new (mem) CrossSection(warped));
|
||||
|
||||
@@ -51,6 +51,7 @@ ManifoldMeshGL *manifold_meshgl_w_tangents(void *mem, float *vert_props,
|
||||
float *halfedge_tangent);
|
||||
ManifoldMeshGL *manifold_get_meshgl(void *mem, ManifoldManifold *m);
|
||||
ManifoldMeshGL *manifold_meshgl_copy(void *mem, ManifoldMeshGL *m);
|
||||
ManifoldMeshGL *manifold_meshgl_merge(void *mem, ManifoldMeshGL *m);
|
||||
|
||||
// SDF
|
||||
// By default, the execution policy (sequential or parallel) of
|
||||
@@ -59,20 +60,11 @@ ManifoldMeshGL *manifold_meshgl_copy(void *mem, ManifoldMeshGL *m);
|
||||
// using these bindings from a language that has a runtime lock preventing the
|
||||
// parallel execution of closures, then you should use manifold_level_set_seq to
|
||||
// force sequential execution.
|
||||
ManifoldMeshGL *manifold_level_set(void *mem, float (*sdf)(float, float, float),
|
||||
ManifoldMeshGL *manifold_level_set(void *mem,
|
||||
float (*sdf)(float, float, float, void *),
|
||||
ManifoldBox *bounds, float edge_length,
|
||||
float level);
|
||||
ManifoldMeshGL *manifold_level_set_seq(void *mem,
|
||||
float (*sdf)(float, float, float),
|
||||
ManifoldBox *bounds, float edge_length,
|
||||
float level);
|
||||
// The _context variants of manifold_level_set allow a pointer to be passed
|
||||
// back to each invocation of the sdf function pointer, for languages that
|
||||
// need additional data.
|
||||
ManifoldMeshGL *manifold_level_set_context(
|
||||
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
|
||||
float edge_length, float level, void *ctx);
|
||||
ManifoldMeshGL *manifold_level_set_seq_context(
|
||||
float level, void *ctx);
|
||||
ManifoldMeshGL *manifold_level_set_seq(
|
||||
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
|
||||
float edge_length, float level, void *ctx);
|
||||
|
||||
@@ -111,6 +103,12 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
|
||||
float normal_x, float normal_y,
|
||||
float normal_z, float offset);
|
||||
|
||||
// 3D to 2D
|
||||
|
||||
ManifoldCrossSection *manifold_slice(void *mem, ManifoldManifold *m,
|
||||
float height);
|
||||
ManifoldCrossSection *manifold_project(void *mem, ManifoldManifold *m);
|
||||
|
||||
// Convex Hulls
|
||||
|
||||
ManifoldManifold *manifold_hull(void *mem, ManifoldManifold *m);
|
||||
@@ -132,8 +130,16 @@ ManifoldManifold *manifold_transform(void *mem, ManifoldManifold *m, float x1,
|
||||
ManifoldManifold *manifold_mirror(void *mem, ManifoldManifold *m, float nx,
|
||||
float ny, float nz);
|
||||
ManifoldManifold *manifold_warp(void *mem, ManifoldManifold *m,
|
||||
ManifoldVec3 (*fun)(float, float, float));
|
||||
ManifoldVec3 (*fun)(float, float, float,
|
||||
void *),
|
||||
void *ctx);
|
||||
ManifoldManifold *manifold_smooth_by_normals(void *mem, ManifoldManifold *m,
|
||||
int normalIdx);
|
||||
ManifoldManifold *manifold_smooth_out(void *mem, ManifoldManifold *m,
|
||||
float minSharpAngle, float minSmoothness);
|
||||
ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine);
|
||||
ManifoldManifold *manifold_refine_to_length(void *mem, ManifoldManifold *m,
|
||||
float length);
|
||||
|
||||
// Manifold Shapes / Constructors
|
||||
|
||||
@@ -177,9 +183,16 @@ int manifold_original_id(ManifoldManifold *m);
|
||||
uint32_t manifold_reserve_ids(uint32_t n);
|
||||
ManifoldManifold *manifold_set_properties(
|
||||
void *mem, ManifoldManifold *m, int num_prop,
|
||||
void (*fun)(float *new_prop, ManifoldVec3 position, const float *old_prop));
|
||||
void (*fun)(float *new_prop, ManifoldVec3 position, const float *old_prop,
|
||||
void *ctx),
|
||||
void *ctx);
|
||||
ManifoldManifold *manifold_calculate_curvature(void *mem, ManifoldManifold *m,
|
||||
int gaussian_idx, int mean_idx);
|
||||
float manifold_min_gap(ManifoldManifold *m, ManifoldManifold *other,
|
||||
float searchLength);
|
||||
ManifoldManifold *manifold_calculate_normals(void *mem, ManifoldManifold *m,
|
||||
int normal_idx,
|
||||
int min_sharp_angle);
|
||||
|
||||
// CrossSection Shapes/Constructors
|
||||
|
||||
@@ -262,6 +275,9 @@ ManifoldCrossSection *manifold_cross_section_transform(void *mem,
|
||||
float x3, float y3);
|
||||
ManifoldCrossSection *manifold_cross_section_warp(
|
||||
void *mem, ManifoldCrossSection *cs, ManifoldVec2 (*fun)(float, float));
|
||||
ManifoldCrossSection *manifold_cross_section_warp_context(
|
||||
void *mem, ManifoldCrossSection *cs,
|
||||
ManifoldVec2 (*fun)(float, float, void *), void *ctx);
|
||||
ManifoldCrossSection *manifold_cross_section_simplify(void *mem,
|
||||
ManifoldCrossSection *cs,
|
||||
double epsilon);
|
||||
|
||||
@@ -35,20 +35,10 @@
|
||||
using namespace manifold;
|
||||
|
||||
namespace {
|
||||
ManifoldMeshGL *level_set(void *mem, float (*sdf)(float, float, float),
|
||||
ManifoldMeshGL *level_set(void *mem,
|
||||
float (*sdf_context)(float, float, float, void *),
|
||||
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 mesh = LevelSet(fun, *from_c(bounds), edge_length, level, !seq);
|
||||
return to_c(new (mem) MeshGL(mesh));
|
||||
}
|
||||
ManifoldMeshGL *level_set_context(
|
||||
void *mem, float (*sdf_context)(float, float, float, void *),
|
||||
ManifoldBox *bounds, float edge_length, float level, bool seq, void *ctx) {
|
||||
bool seq, void *ctx) {
|
||||
// Bind function with context argument to one without
|
||||
using namespace std::placeholders;
|
||||
std::function<float(float, float, float)> sdf =
|
||||
@@ -202,6 +192,17 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
|
||||
return to_c(new (mem) Manifold(trimmed));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_slice(void *mem, ManifoldManifold *m,
|
||||
float height) {
|
||||
auto poly = from_c(m)->Slice(height);
|
||||
return to_c(new (mem) CrossSection(poly));
|
||||
}
|
||||
|
||||
ManifoldCrossSection *manifold_project(void *mem, ManifoldManifold *m) {
|
||||
auto poly = from_c(m)->Project();
|
||||
return to_c(new (mem) CrossSection(poly));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_hull(void *mem, ManifoldManifold *m) {
|
||||
auto hulled = from_c(m)->Hull();
|
||||
return to_c(new (mem) Manifold(hulled));
|
||||
@@ -258,37 +259,44 @@ ManifoldManifold *manifold_mirror(void *mem, ManifoldManifold *m, float nx,
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_warp(void *mem, ManifoldManifold *m,
|
||||
ManifoldVec3 (*fun)(float, float, float)) {
|
||||
std::function<void(glm::vec3 & v)> warp = [fun](glm::vec3 &v) {
|
||||
v = from_c(fun(v.x, v.y, v.z));
|
||||
ManifoldVec3 (*fun)(float, float, float,
|
||||
void *),
|
||||
void *ctx) {
|
||||
// Bind function with context argument to one without
|
||||
using namespace std::placeholders;
|
||||
std::function<ManifoldVec3(float, float, float)> f3 =
|
||||
std::bind(fun, _1, _2, _3, ctx);
|
||||
std::function<void(glm::vec3 & v)> warp = [f3](glm::vec3 &v) {
|
||||
v = from_c(f3(v.x, v.y, v.z));
|
||||
};
|
||||
auto warped = from_c(m)->Warp(warp);
|
||||
return to_c(new (mem) Manifold(warped));
|
||||
}
|
||||
|
||||
ManifoldMeshGL *manifold_level_set(void *mem, float (*sdf)(float, float, float),
|
||||
ManifoldMeshGL *manifold_level_set(void *mem,
|
||||
float (*sdf)(float, float, float, void *),
|
||||
ManifoldBox *bounds, float edge_length,
|
||||
float level) {
|
||||
return level_set(mem, sdf, bounds, edge_length, level, false);
|
||||
float level, void *ctx) {
|
||||
return level_set(mem, sdf, bounds, edge_length, level, false, ctx);
|
||||
}
|
||||
|
||||
ManifoldMeshGL *manifold_level_set_seq(void *mem,
|
||||
float (*sdf)(float, float, float),
|
||||
ManifoldBox *bounds, float edge_length,
|
||||
float level) {
|
||||
return level_set(mem, sdf, bounds, edge_length, level, true);
|
||||
}
|
||||
|
||||
ManifoldMeshGL *manifold_level_set_context(
|
||||
ManifoldMeshGL *manifold_level_set_seq(
|
||||
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
|
||||
float edge_length, float level, void *ctx) {
|
||||
return level_set_context(mem, sdf, bounds, edge_length, level, false, ctx);
|
||||
return level_set(mem, sdf, bounds, edge_length, level, true, ctx);
|
||||
}
|
||||
|
||||
ManifoldMeshGL *manifold_level_set_seq_context(
|
||||
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
|
||||
float edge_length, float level, void *ctx) {
|
||||
return level_set_context(mem, sdf, bounds, edge_length, level, true, ctx);
|
||||
ManifoldManifold *manifold_smooth_by_normals(void *mem, ManifoldManifold *m,
|
||||
int normalIdx) {
|
||||
auto smoothed = from_c(m)->SmoothByNormals(normalIdx);
|
||||
return to_c(new (mem) Manifold(smoothed));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_smooth_out(void *mem, ManifoldManifold *m,
|
||||
float minSharpAngle,
|
||||
float minSmoothness) {
|
||||
auto smoothed = from_c(m)->SmoothOut(minSharpAngle, minSmoothness);
|
||||
return to_c(new (mem) Manifold(smoothed));
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine) {
|
||||
@@ -296,6 +304,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());
|
||||
}
|
||||
@@ -403,6 +417,15 @@ ManifoldMeshGL *manifold_meshgl_copy(void *mem, ManifoldMeshGL *m) {
|
||||
return to_c(new (mem) MeshGL(*from_c(m)));
|
||||
}
|
||||
|
||||
ManifoldMeshGL *manifold_meshgl_merge(void *mem, ManifoldMeshGL *m) {
|
||||
auto duplicate = new (mem) MeshGL(*from_c(m));
|
||||
if (duplicate->Merge()) {
|
||||
return to_c(duplicate);
|
||||
}
|
||||
delete duplicate;
|
||||
return m;
|
||||
}
|
||||
|
||||
int manifold_meshgl_num_prop(ManifoldMeshGL *m) { return from_c(m)->numProp; }
|
||||
int manifold_meshgl_num_vert(ManifoldMeshGL *m) { return from_c(m)->NumVert(); }
|
||||
int manifold_meshgl_num_tri(ManifoldMeshGL *m) { return from_c(m)->NumTri(); }
|
||||
@@ -509,14 +532,18 @@ float manifold_precision(ManifoldManifold *m) { return from_c(m)->Precision(); }
|
||||
|
||||
uint32_t manifold_reserve_ids(uint32_t n) { return Manifold::ReserveIDs(n); }
|
||||
|
||||
ManifoldManifold *manifold_set_properties(void *mem, ManifoldManifold *m,
|
||||
int num_prop,
|
||||
void (*fun)(float *new_prop,
|
||||
ManifoldVec3 position,
|
||||
const float *old_prop)) {
|
||||
ManifoldManifold *manifold_set_properties(
|
||||
void *mem, ManifoldManifold *m, int num_prop,
|
||||
void (*fun)(float *new_prop, ManifoldVec3 position, const float *old_prop,
|
||||
void *ctx),
|
||||
void *ctx) {
|
||||
// Bind function with context argument to one without
|
||||
using namespace std::placeholders;
|
||||
std::function<void(float *, ManifoldVec3, const float *)> f3 =
|
||||
std::bind(fun, _1, _2, _3, ctx);
|
||||
std::function<void(float *, glm::vec3, const float *)> f =
|
||||
[fun](float *new_prop, glm::vec3 v, const float *old_prop) {
|
||||
fun(new_prop, to_c(v), old_prop);
|
||||
[f3](float *new_prop, glm::vec3 v, const float *old_prop) {
|
||||
return (f3(new_prop, to_c(v), old_prop));
|
||||
};
|
||||
auto man = from_c(m)->SetProperties(num_prop, f);
|
||||
return to_c(new (mem) Manifold(man));
|
||||
@@ -528,6 +555,18 @@ ManifoldManifold *manifold_calculate_curvature(void *mem, ManifoldManifold *m,
|
||||
return to_c(new (mem) Manifold(man));
|
||||
}
|
||||
|
||||
float manifold_min_gap(ManifoldManifold *m, ManifoldManifold *other,
|
||||
float searchLength) {
|
||||
return from_c(m)->MinGap(*from_c(other), searchLength);
|
||||
}
|
||||
|
||||
ManifoldManifold *manifold_calculate_normals(void *mem, ManifoldManifold *m,
|
||||
int normal_idx,
|
||||
int min_sharp_angle) {
|
||||
auto man = from_c(m)->CalculateNormals(normal_idx, min_sharp_angle);
|
||||
return to_c(new (mem) Manifold(man));
|
||||
}
|
||||
|
||||
// Static Quality Globals
|
||||
|
||||
void manifold_set_min_circular_angle(float degrees) {
|
||||
|
||||
@@ -18,12 +18,31 @@ add_subdirectory(third_party)
|
||||
nanobind_add_module(
|
||||
manifold3d
|
||||
NB_STATIC STABLE_ABI LTO
|
||||
autogen_docstrings.inl
|
||||
manifold3d.cpp)
|
||||
target_link_libraries(manifold3d PRIVATE manifold)
|
||||
target_link_libraries(manifold3d PRIVATE manifold sdf polygon)
|
||||
target_compile_options(manifold3d PRIVATE ${MANIFOLD_FLAGS} -DMODULE_NAME=manifold3d)
|
||||
target_compile_features(manifold3d PUBLIC cxx_std_17)
|
||||
set_target_properties(manifold3d PROPERTIES OUTPUT_NAME "manifold3d")
|
||||
|
||||
message(Python_EXECUTABLE = ${Python_EXECUTABLE})
|
||||
# ideally we should generate a dependency file from python...
|
||||
set(DOCSTRING_DEPS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/manifold.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/constructors.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/sort.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/cross_section/src/cross_section.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/polygon/src/polygon.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/utilities/include/public.h
|
||||
)
|
||||
add_custom_command(
|
||||
OUTPUT autogen_docstrings.inl
|
||||
DEPENDS ${DOCSTRING_DEPS}
|
||||
COMMAND ${Python_EXECUTABLE} ARGS ${CMAKE_CURRENT_SOURCE_DIR}/gen_docs.py
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
target_include_directories(manifold3d PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
if(SKBUILD)
|
||||
install(
|
||||
TARGETS manifold3d
|
||||
@@ -33,7 +52,7 @@ install(
|
||||
else()
|
||||
install(
|
||||
TARGETS manifold3d
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
LIBRARY DESTINATION ${Python_SITEARCH}
|
||||
COMPONENT bindings
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Python Bindings
|
||||
|
||||
## Autogenerated Doc-Strings
|
||||
|
||||
Doc-strings for the python API wrapper are generated by gen_docs.py
|
||||
The script is run automatically during the build to keep python
|
||||
doc strings fully up-to-date with c++ sources.
|
||||
|
||||
It scrapes documentation comments and c++ function signatures
|
||||
from the manifold c++ sources, in order to generate a c++ header file
|
||||
exposing the comments as string variables named by function names.
|
||||
This allows python bindings to re-use the c++ comments directly.
|
||||
|
||||
Some snake-casing of params is applied for python use case.
|
||||
|
||||
---
|
||||
|
||||
When modifying the Manifold C++ sources, you may need to update
|
||||
gen_docs.py. For example, top-level free functions are white-listed,
|
||||
so if you add a new one, you will need to add it in gen_docs.py.
|
||||
|
||||
Similarly, the list of source files to parse is also white listed,
|
||||
so if you define functions in new files that need python wrappers,
|
||||
you will also need to up gen_docs.py.
|
||||
|
||||
To verify that python docs are correct after changes, you can
|
||||
run the following commends from the manifold repo root:
|
||||
```
|
||||
pip install .
|
||||
python -c 'import manifold3d; help(manifold3d)'
|
||||
```
|
||||
|
||||
Alternateively you could generate stubs with roughly the same info
|
||||
```
|
||||
pip install nanobind-stubgen
|
||||
pip install .
|
||||
nanobind-stubgen manifold3d
|
||||
```
|
||||
It will emit some warnings and write a file `manifold3d.pyi`
|
||||
which will show all the function signatures and docstrings.
|
||||
@@ -0,0 +1,128 @@
|
||||
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 = m.calculate_normals(0)
|
||||
m = m.smooth_by_normals(0)
|
||||
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)
|
||||
m = m.smooth_out()
|
||||
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])
|
||||
m = Manifold.cube()
|
||||
m2 = Manifold.cube().translate([2, 0, 0])
|
||||
d = m.min_gap(m2, 2)
|
||||
|
||||
|
||||
def run():
|
||||
all_root_level()
|
||||
all_cross_section()
|
||||
all_manifold()
|
||||
return Manifold()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -20,60 +20,82 @@ from manifold3d import Manifold
|
||||
|
||||
INCHES = 25.4
|
||||
|
||||
brick_depth = (3 + 5/8) * INCHES
|
||||
brick_height = (2 + 1/4) * INCHES
|
||||
brick_length = (7 + 5/8) * INCHES
|
||||
brick_depth = (3 + 5 / 8) * INCHES
|
||||
brick_height = (2 + 1 / 4) * INCHES
|
||||
brick_length = (7 + 5 / 8) * INCHES
|
||||
|
||||
mortar_gap = (3/8) * INCHES
|
||||
mortar_gap = (3 / 8) * INCHES
|
||||
|
||||
|
||||
def brick():
|
||||
return Manifold.cube(brick_length, brick_depth, brick_height)
|
||||
return Manifold.cube([brick_length, brick_depth, brick_height])
|
||||
|
||||
|
||||
def halfbrick():
|
||||
return Manifold.cube((brick_length - mortar_gap)/2, brick_depth, brick_height)
|
||||
return Manifold.cube([(brick_length - mortar_gap) / 2, brick_depth, brick_height])
|
||||
|
||||
|
||||
def row(length):
|
||||
bricks = [brick().translate((brick_length + mortar_gap)*x, 0, 0)
|
||||
for x in range(length)]
|
||||
return Manifold(bricks)
|
||||
bricks = [
|
||||
brick().translate([(brick_length + mortar_gap) * x, 0, 0])
|
||||
for x in range(length)
|
||||
]
|
||||
return sum(bricks, Manifold())
|
||||
|
||||
|
||||
def wall(length, height, alternate=0):
|
||||
bricks = [row(length).translate(
|
||||
((z+alternate) % 2)*(brick_depth+mortar_gap), 0,
|
||||
(brick_height+mortar_gap)*z) for z in range(height)]
|
||||
return Manifold(bricks)
|
||||
bricks = [
|
||||
row(length).translate(
|
||||
[
|
||||
((z + alternate) % 2) * (brick_depth + mortar_gap),
|
||||
0,
|
||||
(brick_height + mortar_gap) * z,
|
||||
]
|
||||
)
|
||||
for z in range(height)
|
||||
]
|
||||
return sum(bricks, Manifold())
|
||||
|
||||
|
||||
def walls(length, width, height):
|
||||
return Manifold([
|
||||
wall(length, height),
|
||||
wall(width, height, 1).rotate(0, 0, 90).translate(brick_depth, 0, 0),
|
||||
wall(length, height, 1).translate(
|
||||
0, (width)*(brick_length+mortar_gap), 0),
|
||||
wall(width, height).rotate(0, 0, 90)
|
||||
.translate((length+0.5)*(brick_length+mortar_gap) - mortar_gap, 0, 0),
|
||||
])
|
||||
return sum(
|
||||
[
|
||||
wall(length, height),
|
||||
wall(width, height, 1).rotate([0, 0, 90]).translate([brick_depth, 0, 0]),
|
||||
wall(length, height, 1).translate(
|
||||
[0, (width) * (brick_length + mortar_gap), 0]
|
||||
),
|
||||
wall(width, height)
|
||||
.rotate([0, 0, 90])
|
||||
.translate(
|
||||
[(length + 0.5) * (brick_length + mortar_gap) - mortar_gap, 0, 0]
|
||||
),
|
||||
],
|
||||
Manifold(),
|
||||
)
|
||||
|
||||
|
||||
def floor(length, width):
|
||||
results = [walls(length, width, 1)]
|
||||
if length > 1 and width > 1:
|
||||
results.append(floor(length-1, width-1).translate(brick_depth +
|
||||
mortar_gap, brick_depth+mortar_gap, 0))
|
||||
results.append(
|
||||
floor(length - 1, width - 1).translate(
|
||||
[brick_depth + mortar_gap, brick_depth + mortar_gap, 0]
|
||||
)
|
||||
)
|
||||
if length == 1 and width > 1:
|
||||
results.append(row(width-1).rotate(0, 0, 90))
|
||||
results.append(row(width - 1).rotate(0, 0, 90))
|
||||
if width == 1 and length > 1:
|
||||
results.append(row(length-1).translate(2*(brick_depth +
|
||||
mortar_gap), brick_depth+mortar_gap, 0))
|
||||
results.append(halfbrick().translate(
|
||||
brick_depth+mortar_gap, brick_depth+mortar_gap, 0))
|
||||
return Manifold(results)
|
||||
results.append(
|
||||
row(length - 1).translate(
|
||||
[2 * (brick_depth + mortar_gap), brick_depth + mortar_gap, 0]
|
||||
)
|
||||
)
|
||||
results.append(
|
||||
halfbrick().translate([brick_depth + mortar_gap, brick_depth + mortar_gap, 0])
|
||||
)
|
||||
return sum(results, Manifold())
|
||||
|
||||
|
||||
def run(width=10, length=10, height=10):
|
||||
return walls(length, width, height) + floor(length, width)
|
||||
|
||||
|
||||
@@ -13,18 +13,21 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
|
||||
from manifold3d import Manifold
|
||||
from functools import reduce
|
||||
|
||||
# https://gist.github.com/ochafik/2db96400e3c1f73558fcede990b8a355#file-cube-with-half-spheres-dents-scad
|
||||
|
||||
|
||||
def run(n=5, overlap=True):
|
||||
a = Manifold.cube(n, n, 0.5).translate(-0.5, -0.5, -0.5)
|
||||
a = Manifold.cube([n, n, 0.5]).translate([-0.5, -0.5, -0.5])
|
||||
|
||||
spheres = [Manifold.sphere(0.45 if overlap else 0.55, 50).translate(i, j, 0)
|
||||
for i in range(n) for j in range(n)]
|
||||
spheres = reduce(lambda a, b: a + b, spheres)
|
||||
|
||||
return a - spheres
|
||||
spheres = [
|
||||
Manifold.sphere(0.45 if overlap else 0.55, 50).translate([i, j, 0])
|
||||
for i in range(n)
|
||||
for j in range(n)
|
||||
]
|
||||
# spheres = reduce(lambda a, b: a + b, spheres)
|
||||
|
||||
return a - sum(spheres, Manifold())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from manifold3d import CrossSection
|
||||
from manifold3d import CrossSection, Manifold
|
||||
|
||||
|
||||
def run():
|
||||
@@ -10,26 +10,34 @@ def run():
|
||||
cross_section = CrossSection(polygons_points)
|
||||
polygons = cross_section.to_polygons()
|
||||
polygon = polygons[0]
|
||||
if set(polygon) != set(polygon_points):
|
||||
raise Exception(f"polygon={polygon} differs from polygon_points={polygon_points}")
|
||||
if set([tuple(p) for p in polygon]) != set(polygon_points):
|
||||
raise Exception(
|
||||
f"polygon={polygon} differs from polygon_points={polygon_points}"
|
||||
)
|
||||
|
||||
# extrude a polygon to create a manifold
|
||||
extruded_polygon = cross_section.extrude(10.0)
|
||||
extruded_polygon = Manifold.extrude(cross_section, 10.0)
|
||||
eps = 0.001
|
||||
observed_volume = extruded_polygon.get_volume()
|
||||
observed_volume = extruded_polygon.volume()
|
||||
expected_volume = 10.0
|
||||
if abs(observed_volume - expected_volume) > eps:
|
||||
raise Exception(f"observed_volume={observed_volume} differs from expected_volume={expected_volume}")
|
||||
observed_surface_area = extruded_polygon.get_surface_area()
|
||||
raise Exception(
|
||||
f"observed_volume={observed_volume} differs from expected_volume={expected_volume}"
|
||||
)
|
||||
observed_surface_area = extruded_polygon.surface_area()
|
||||
expected_surface_area = 42.0
|
||||
if abs(observed_surface_area - expected_surface_area) > eps:
|
||||
raise Exception(f"observed_surface_area={observed_surface_area} differs from expected_surface_area={expected_surface_area}")
|
||||
raise Exception(
|
||||
f"observed_surface_area={observed_surface_area} differs from expected_surface_area={expected_surface_area}"
|
||||
)
|
||||
|
||||
# get bounding box from manifold
|
||||
observed_bbox = extruded_polygon.bounding_box
|
||||
observed_bbox = extruded_polygon.bounding_box()
|
||||
expected_bbox = (0.0, 0.0, 0.0, 1.0, 1.0, 10.0)
|
||||
if observed_bbox != expected_bbox:
|
||||
raise Exception(f"observed_bbox={observed_bbox} differs from expected_bbox={expected_bbox}")
|
||||
raise Exception(
|
||||
f"observed_bbox={observed_bbox} differs from expected_bbox={expected_bbox}"
|
||||
)
|
||||
|
||||
return extruded_polygon
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Copyright 2023 The Manifold Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
from manifold3d import Mesh, Manifold
|
||||
|
||||
|
||||
def gyroid(x, y, z):
|
||||
xi = x - math.pi / 4.0
|
||||
yi = y - math.pi / 4.0
|
||||
zi = z - math.pi / 4.0
|
||||
return (
|
||||
math.cos(xi) * math.sin(yi)
|
||||
+ math.cos(yi) * math.sin(zi)
|
||||
+ math.cos(zi) * math.sin(xi)
|
||||
)
|
||||
|
||||
|
||||
def gyroid_levelset(level, period, size, n):
|
||||
return Manifold(
|
||||
Mesh.level_set(
|
||||
gyroid,
|
||||
[-period, -period, -period, period, period, period],
|
||||
period / n,
|
||||
level,
|
||||
)
|
||||
).scale([size / period] * 3)
|
||||
|
||||
|
||||
def rhombic_dodecahedron(size):
|
||||
box = Manifold.cube(size * math.sqrt(2.0) * np.array([1, 1, 2]), True)
|
||||
result = box.rotate([90, 45, 0]) ^ box.rotate([90, 45, 90])
|
||||
return result ^ box.rotate([0, 0, 45])
|
||||
|
||||
|
||||
def gyroid_module(size=20, n=15):
|
||||
period = math.pi * 2.0
|
||||
result = (
|
||||
gyroid_levelset(-0.4, period, size, n) ^ rhombic_dodecahedron(size)
|
||||
) - gyroid_levelset(0.4, period, size, n)
|
||||
return result.rotate([-45, 0, 90]).translate([0, 0, size / math.sqrt(2.0)])
|
||||
|
||||
|
||||
def run(size=20, n=15):
|
||||
return gyroid_module(size, n)
|
||||
@@ -24,10 +24,9 @@ from time import time
|
||||
if __name__ == "__main__":
|
||||
current_file = pathlib.Path(__file__)
|
||||
current_dir = current_file.parent
|
||||
files = [f.parts[-1][:-3]
|
||||
for f in current_dir.glob('*.py') if f != current_file]
|
||||
files = [f.parts[-1][:-3] for f in current_dir.glob("*.py") if f != current_file]
|
||||
|
||||
export_models = len(sys.argv) == 2 and sys.argv[-1] == '-e'
|
||||
export_models = len(sys.argv) == 2 and sys.argv[-1] == "-e"
|
||||
|
||||
for f in files:
|
||||
module = importlib.import_module(f)
|
||||
@@ -41,9 +40,10 @@ if __name__ == "__main__":
|
||||
else:
|
||||
vertices = mesh.vert_properties
|
||||
colors = None
|
||||
meshOut = trimesh.Trimesh(vertices=vertices, faces=mesh.tri_verts,
|
||||
vertex_colors=colors)
|
||||
trimesh.exchange.export.export_mesh(meshOut, f'{f}.glb', 'glb')
|
||||
print(f'Exported model to {f}.glb')
|
||||
meshOut = trimesh.Trimesh(
|
||||
vertices=vertices, faces=mesh.tri_verts, vertex_colors=colors
|
||||
)
|
||||
trimesh.exchange.export.export_mesh(meshOut, f"{f}.glb", "glb")
|
||||
print(f"Exported model to {f}.glb")
|
||||
t1 = time()
|
||||
print(f'Took {(t1-t0)*1000:.1f}ms for {f}')
|
||||
print(f"Took {(t1-t0)*1000:.1f}ms for {f}")
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Copyright 2023 The Manifold Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from manifold3d import Manifold, Mesh
|
||||
import numpy as np
|
||||
|
||||
|
||||
def run():
|
||||
# A smoothed manifold demonstrating selective edge sharpening with
|
||||
# smooth() and refine(), see more details at:
|
||||
# https://elalish.blogspot.com/2022/03/smoothing-triangle-meshes.html
|
||||
|
||||
height = 10
|
||||
radius = 30
|
||||
offset = 20
|
||||
wiggles = 12
|
||||
sharpness = 0.8
|
||||
n = 50
|
||||
|
||||
triangles = []
|
||||
positions = [[-offset, 0, height], [-offset, 0, -height]]
|
||||
sharpenedEdges = []
|
||||
|
||||
delta = np.pi / wiggles
|
||||
for i in range(2 * wiggles):
|
||||
theta = (i - wiggles) * delta
|
||||
amp = 0.5 * height * max(np.cos(0.8 * theta), 0)
|
||||
|
||||
positions.append(
|
||||
[
|
||||
radius * np.cos(theta),
|
||||
radius * np.sin(theta),
|
||||
amp * (1 if i % 2 == 0 else -1),
|
||||
]
|
||||
)
|
||||
|
||||
j = i + 1
|
||||
if j == 2 * wiggles:
|
||||
j = 0
|
||||
|
||||
smoothness = 1 - sharpness * np.cos((theta + delta / 2) / 2)
|
||||
halfedge = 3 * len(triangles) + 1
|
||||
sharpenedEdges.append((halfedge, smoothness))
|
||||
triangles.append([0, 2 + i, 2 + j])
|
||||
|
||||
halfedge = 3 * len(triangles) + 1
|
||||
sharpenedEdges.append((halfedge, smoothness))
|
||||
triangles.append([1, 2 + j, 2 + i])
|
||||
|
||||
scallop = Mesh(
|
||||
tri_verts=np.array(triangles, np.int32),
|
||||
vert_properties=np.array(positions, np.float32),
|
||||
)
|
||||
|
||||
def colorCurvature(_pos, oldProp):
|
||||
a = max(0, min(1, oldProp[0] / 3 + 0.5))
|
||||
b = a * a * (3 - 2 * a)
|
||||
red = [1, 0, 0]
|
||||
blue = [0, 0, 1]
|
||||
return [(1 - b) * blue[i] + b * red[i] for i in range(3)]
|
||||
|
||||
edges, smoothing = zip(*sharpenedEdges)
|
||||
return (
|
||||
Manifold.smooth(scallop, edges, smoothing)
|
||||
.refine(n)
|
||||
.calculate_curvature(-1, 0)
|
||||
.set_properties(3, colorCurvature)
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Copyright 2022 The Manifold Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from manifold3d import Manifold
|
||||
|
||||
|
||||
def run():
|
||||
a = Manifold.cube([1.0, 1.0, 1.0])
|
||||
b = Manifold.cube([1.0, 1.0, 1.0]).rotate([45.0, 45.0, 45.0])
|
||||
return a.split(b)[0]
|
||||
@@ -4,26 +4,27 @@ import numpy as np
|
||||
|
||||
def fractal(holes, hole, w, position, depth, maxDepth):
|
||||
w /= 3
|
||||
holes.append(hole.scale([w, w, 1.0]).translate(
|
||||
[position[0], position[1], 0.0]))
|
||||
holes.append(hole.scale([w, w, 1.0]).translate([position[0], position[1], 0.0]))
|
||||
if depth == maxDepth:
|
||||
return
|
||||
offsets = np.array([
|
||||
[-w, -w],
|
||||
[-w, 0.0],
|
||||
[-w, w],
|
||||
[0.0, w],
|
||||
[w, w],
|
||||
[w, 0.0],
|
||||
[w, -w],
|
||||
[0.0, -w],
|
||||
])
|
||||
offsets = np.array(
|
||||
[
|
||||
[-w, -w],
|
||||
[-w, 0.0],
|
||||
[-w, w],
|
||||
[0.0, w],
|
||||
[w, w],
|
||||
[w, 0.0],
|
||||
[w, -w],
|
||||
[0.0, -w],
|
||||
]
|
||||
)
|
||||
for offset in offsets:
|
||||
fractal(holes, hole, w, position + offset, depth + 1, maxDepth)
|
||||
|
||||
|
||||
def posColors(pos, _):
|
||||
return [1 - p / 2 for p in pos] + [1.0]
|
||||
return [-p + 0.5 for p in pos] + [1.0]
|
||||
|
||||
|
||||
def run(n=1):
|
||||
@@ -37,4 +38,6 @@ def run(n=1):
|
||||
result -= hole.rotate([90, 0, 0])
|
||||
result -= hole.rotate([0, 90, 0])
|
||||
|
||||
return result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale(100)
|
||||
return (
|
||||
result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale([100] * 3)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from manifold3d import *
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
# Creates a classic torus knot, defined as a string wrapping periodically
|
||||
# around the surface of an imaginary donut. If p and q have a common
|
||||
# factor then you will get multiple separate, interwoven knots. This is
|
||||
# an example of using the warp() method, thus avoiding any direct
|
||||
# handling of triangles.
|
||||
|
||||
|
||||
def run(warp_single=False):
|
||||
# The number of times the thread passes through the donut hole.
|
||||
p = 1
|
||||
# The number of times the thread circles the donut.
|
||||
q = 3
|
||||
# Radius of the interior of the imaginary donut.
|
||||
majorRadius = 25
|
||||
# Radius of the small cross-section of the imaginary donut.
|
||||
minorRadius = 10
|
||||
# Radius of the small cross-section of the actual object.
|
||||
threadRadius = 3.75
|
||||
# Number of linear segments making up the threadRadius circle. Default is
|
||||
# getCircularSegments(threadRadius).
|
||||
circularSegments = -1
|
||||
# Number of segments along the length of the knot. Default makes roughly
|
||||
# square facets.
|
||||
linearSegments = -1
|
||||
|
||||
# These default values recreate Matlab Knot by Emmett Lalish:
|
||||
# https://www.thingiverse.com/thing:7080
|
||||
|
||||
kLoops = np.gcd(p, q)
|
||||
pk = p / kLoops
|
||||
qk = q / kLoops
|
||||
n = (
|
||||
circularSegments
|
||||
if circularSegments > 2
|
||||
else get_circular_segments(threadRadius)
|
||||
)
|
||||
m = linearSegments if linearSegments > 2 else n * qk * majorRadius / threadRadius
|
||||
|
||||
offset = 2
|
||||
circle = CrossSection.circle(1, n).translate([offset, 0])
|
||||
|
||||
def ax_rotate(x, theta):
|
||||
a, b = (x + 1) % 3, (x + 2) % 3
|
||||
s, c = np.sin(theta), np.cos(theta)
|
||||
m = np.zeros((len(theta), 4, 4), dtype=np.float32)
|
||||
m[:, a, a], m[:, a, b] = c, s
|
||||
m[:, b, a], m[:, b, b] = -s, c
|
||||
m[:, x, x], m[:, 3, 3] = 1, 1
|
||||
return m
|
||||
|
||||
def func(pts):
|
||||
npts = pts.shape[0]
|
||||
x, y, z = pts[:, 0], pts[:, 1], pts[:, 2]
|
||||
psi = qk * np.arctan2(x, y)
|
||||
theta = psi * pk / qk
|
||||
x1 = np.sqrt(x * x + y * y)
|
||||
phi = np.arctan2(x1 - offset, z)
|
||||
|
||||
v = np.zeros((npts, 4), dtype=np.float32)
|
||||
v[:, 0] = threadRadius * np.cos(phi)
|
||||
v[:, 2] = threadRadius * np.sin(phi)
|
||||
v[:, 3] = 1
|
||||
r = majorRadius + minorRadius * np.cos(theta)
|
||||
|
||||
m1 = ax_rotate(0, -np.arctan2(pk * minorRadius, qk * r))
|
||||
m1[:, 3, 0] = minorRadius
|
||||
m2 = ax_rotate(1, theta)
|
||||
m2[:, 3, 0] = majorRadius
|
||||
m3 = ax_rotate(2, psi)
|
||||
|
||||
v = v[:, None, :] @ m1 @ m2 @ m3
|
||||
pts[:] = v[:, 0, :3]
|
||||
|
||||
def func_single(v):
|
||||
pts = np.array([v])
|
||||
func(pts)
|
||||
return pts[0]
|
||||
|
||||
if warp_single:
|
||||
return Manifold.revolve(circle, int(m)).warp(func_single)
|
||||
else:
|
||||
return Manifold.revolve(circle, int(m)).warp_batch(func)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("warp_single", [True, False])
|
||||
def test_warp(warp_single):
|
||||
m = run(warp_single=warp_single)
|
||||
assert m.volume() == pytest.approx(20785.76)
|
||||
assert m.surface_area() == pytest.approx(11176.8)
|
||||
assert m.genus() == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -1,7 +1,8 @@
|
||||
from manifold3d import Manifold
|
||||
|
||||
|
||||
def run():
|
||||
# for some reason this causes collider error
|
||||
obj = Manifold.cube()
|
||||
obj += Manifold([Manifold.cube()]).rotate(0, 0, 45+180)
|
||||
obj += Manifold.cube().rotate([0, 0, 45 + 180])
|
||||
return obj
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
from os.path import dirname
|
||||
import re
|
||||
|
||||
base = dirname(dirname(dirname(__file__)))
|
||||
|
||||
def snake_case(name):
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
|
||||
|
||||
|
||||
def python_param_modifier(comment):
|
||||
# p = f":{snake_case(m[0][1:])}:"
|
||||
comment = re.sub(r"@(param \w+)", lambda m: f':{snake_case(m[1])}:', comment)
|
||||
# python API renames `MeshGL` to `Mesh`
|
||||
comment = re.sub('mesh_gl', 'mesh', comment)
|
||||
comment = re.sub('MeshGL', 'Mesh', comment)
|
||||
return comment
|
||||
|
||||
|
||||
def method_key(name):
|
||||
name = re.sub(r"\+", "_plus", name)
|
||||
name = re.sub(r"\-", "_minus", name)
|
||||
name = re.sub(r"\^", "_xor", name)
|
||||
name = re.sub(r"\=", "_eq", name)
|
||||
name = re.sub(r"\:", "_", name)
|
||||
name = re.sub(r"\~", "destroy_", name)
|
||||
return name
|
||||
|
||||
|
||||
parens_re = re.compile(r"[^(]+\(([^(]*(\(.*\))*[^(]*\))", flags=re.DOTALL)
|
||||
args_re = re.compile(
|
||||
r"^[^,^\(^\)]*(\(.*\))*[^,^\(^\)]*[\s\&\*]([0-9\w]+)\s*[,\)]", flags=re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def parse_args(s):
|
||||
par = parens_re.match(s)
|
||||
if not par:
|
||||
return None
|
||||
out = []
|
||||
arg_str = par[1]
|
||||
while m := re.search(args_re, arg_str):
|
||||
out += [snake_case(m[2])]
|
||||
arg_str = arg_str[m.span()[1] :]
|
||||
return out
|
||||
|
||||
|
||||
def collect(fname, matcher, param_modifier=python_param_modifier):
|
||||
comment = ""
|
||||
with open(fname) as f:
|
||||
for line in f:
|
||||
line = line.lstrip()
|
||||
if line.startswith("/**"):
|
||||
comment = ""
|
||||
elif line.startswith("*/"):
|
||||
pass
|
||||
elif line.startswith("*") and comment is not None:
|
||||
comment += line[1:].lstrip()
|
||||
elif comment and (m := matcher(line)):
|
||||
while (args := parse_args(line)) is None:
|
||||
line += next(f)
|
||||
if len(line) > 500:
|
||||
break
|
||||
|
||||
method = method_key(snake_case(m[1]))
|
||||
# comment = re.sub(param_re, param_modifier, comment)
|
||||
comment = param_modifier(comment)
|
||||
method = "__".join([method, *args])
|
||||
assert method not in comments
|
||||
comments[method] = comment
|
||||
comment = ""
|
||||
|
||||
|
||||
comments = {}
|
||||
|
||||
method_re = re.compile(r"(\w+::[\w\-\+\^\=\:]+)\(")
|
||||
function_re = re.compile(r"([\w\-\+\^\=\:]+)\(")
|
||||
|
||||
|
||||
# we don't handle inline functions in classes properly
|
||||
# so instead just white-list functions we want
|
||||
def select_functions(s):
|
||||
m = function_re.search(s)
|
||||
if m and "Triangulate" in m[0]:
|
||||
return m
|
||||
if m and "Circular" in m[0]:
|
||||
return m
|
||||
return None
|
||||
|
||||
|
||||
collect(f"{base}/src/manifold/src/manifold.cpp", lambda s: method_re.search(s))
|
||||
collect(f"{base}/src/manifold/src/constructors.cpp", lambda s: method_re.search(s))
|
||||
collect(f"{base}/src/manifold/src/sort.cpp", lambda s: method_re.search(s))
|
||||
collect(
|
||||
f"{base}/src/cross_section/src/cross_section.cpp", lambda s: method_re.search(s)
|
||||
)
|
||||
collect(f"{base}/src/polygon/src/polygon.cpp", select_functions)
|
||||
collect(f"{base}/src/utilities/include/public.h", select_functions)
|
||||
|
||||
comments = dict(sorted(comments.items()))
|
||||
|
||||
gen_h = "autogen_docstrings.inl"
|
||||
with open(gen_h, "w") as f:
|
||||
f.write("#pragma once\n\n")
|
||||
f.write("// --- AUTO GENERATED ---\n")
|
||||
f.write("// gen_docs.py is run by cmake build\n\n")
|
||||
f.write("namespace manifold_docstrings {\n")
|
||||
for key, doc in comments.items():
|
||||
f.write(f'const char* {key} = R"___({doc.strip()})___";\n')
|
||||
f.write("} // namespace manifold_docs")
|
||||
@@ -0,0 +1,2 @@
|
||||
manifold.js
|
||||
manifold.wasm
|
||||
@@ -32,13 +32,21 @@ add_custom_target(js_deps ALL
|
||||
DEPENDS manifoldjs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts)
|
||||
|
||||
# copy WASM build back here for publishing to npm
|
||||
add_custom_command(
|
||||
TARGET js_deps POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_BINARY_DIR}/manifold.*
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts
|
||||
${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# copy WASM build and TS declarations for Vite to package into ManifoldCAD.org
|
||||
add_custom_command(
|
||||
TARGET js_deps POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/manifold*
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/examples/built/)
|
||||
|
||||
# copy TS declarations to public so they can be accessed by our editor
|
||||
add_custom_command(
|
||||
TARGET js_deps POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
|
||||
@@ -24,7 +24,7 @@ Since Manifold is a WASM module, it does not automatically garbage-collect like
|
||||
|
||||
## Examples
|
||||
|
||||
Please see our usage [examples](https://github.com/elalish/manifold/tree/master/bindings/wasm/examples) to see how to interface this library with `three.js`, `<model-viewer>`, and `glTF`. Of particular note are the included libraries for lossless roundtrip of manifold meshes through glTF files, via a new extension: [EXT_manifold](https://github.com/KhronosGroup/glTF/pull/2286).
|
||||
Please see our usage [examples](https://github.com/elalish/manifold/tree/master/bindings/wasm/examples) to see how to interface this library with `three.js`, `<model-viewer>`, and `glTF`. Of particular note are the included libraries for lossless roundtrip of manifold meshes through glTF files, via a new extension: [EXT_mesh_manifold](https://github.com/KhronosGroup/glTF/pull/2286).
|
||||
|
||||
## About the author
|
||||
|
||||
|
||||
@@ -138,9 +138,14 @@ 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("smoothByNormals", &Manifold::SmoothByNormals)
|
||||
.function("_SmoothOut", &Manifold::SmoothOut)
|
||||
.function("_Warp", &man_js::Warp)
|
||||
.function("_SetProperties", &man_js::SetProperties)
|
||||
.function("transform", &man_js::Transform)
|
||||
@@ -161,7 +166,9 @@ EMSCRIPTEN_BINDINGS(whatever) {
|
||||
.function("precision", &Manifold::Precision)
|
||||
.function("genus", &Manifold::Genus)
|
||||
.function("getProperties", &Manifold::GetProperties)
|
||||
.function("minGap", &Manifold::MinGap)
|
||||
.function("calculateCurvature", &Manifold::CalculateCurvature)
|
||||
.function("_CalculateNormals", &Manifold::CalculateNormals)
|
||||
.function("originalID", &Manifold::OriginalID)
|
||||
.function("asOriginal", &Manifold::AsOriginal);
|
||||
|
||||
@@ -191,4 +198,5 @@ EMSCRIPTEN_BINDINGS(whatever) {
|
||||
function("setMinCircularEdgeLength", &Quality::SetMinCircularEdgeLength);
|
||||
function("setCircularSegments", &Quality::SetCircularSegments);
|
||||
function("getCircularSegments", &Quality::GetCircularSegments);
|
||||
function("resetToCircularDefaults", &Quality::ResetToDefaults);
|
||||
}
|
||||
|
||||
@@ -196,6 +196,11 @@ Module.setup = function() {
|
||||
|
||||
// Manifold methods
|
||||
|
||||
Module.Manifold.prototype.smoothOut = function(
|
||||
minSharpAngle = 60, minSmoothness = 0) {
|
||||
return this._SmoothOut(minSharpAngle, minSmoothness);
|
||||
};
|
||||
|
||||
Module.Manifold.prototype.warp = function(func) {
|
||||
const wasmFuncPtr = addFunction(function(vec3Ptr) {
|
||||
const x = getValue(vec3Ptr, 'float');
|
||||
@@ -217,6 +222,11 @@ Module.setup = function() {
|
||||
return out;
|
||||
};
|
||||
|
||||
Module.Manifold.prototype.calculateNormals = function(
|
||||
normalIdx, minSharpAngle = 60) {
|
||||
return this._CalculateNormals(normalIdx, minSharpAngle);
|
||||
};
|
||||
|
||||
Module.Manifold.prototype.setProperties = function(numProp, func) {
|
||||
const oldNumProp = this.numProp();
|
||||
const wasmFuncPtr = addFunction(function(newPtr, vec3Ptr, oldPtr) {
|
||||
@@ -248,8 +258,12 @@ Module.setup = function() {
|
||||
return this._Translate(vararg2vec3(vec));
|
||||
};
|
||||
|
||||
Module.Manifold.prototype.rotate = function(vec) {
|
||||
return this._Rotate(...vec);
|
||||
Module.Manifold.prototype.rotate = function(xOrVec, y, z) {
|
||||
if (Array.isArray(xOrVec)) {
|
||||
return this._Rotate(...xOrVec);
|
||||
} else {
|
||||
return this._Rotate(xOrVec, y || 0, z || 0);
|
||||
}
|
||||
};
|
||||
|
||||
Module.Manifold.prototype.scale = function(vec) {
|
||||
@@ -268,15 +282,19 @@ Module.setup = function() {
|
||||
return this._TrimByPlane(vararg2vec3([normal]), offset);
|
||||
};
|
||||
|
||||
Module.Manifold.prototype.slice = function(height = 0.) {
|
||||
return this._Slice(height);
|
||||
};
|
||||
|
||||
Module.Manifold.prototype.split = function(manifold) {
|
||||
const vec = this._split(manifold);
|
||||
const vec = this._Split(manifold);
|
||||
const result = fromVec(vec);
|
||||
vec.delete();
|
||||
return result;
|
||||
};
|
||||
|
||||
Module.Manifold.prototype.splitByPlane = function(normal, offset = 0.) {
|
||||
const vec = this._splitByPlane(vararg2vec3([normal]), offset);
|
||||
const vec = this._SplitByPlane(vararg2vec3([normal]), offset);
|
||||
const result = fromVec(vec);
|
||||
vec.delete();
|
||||
return result;
|
||||
|
||||
@@ -65,7 +65,7 @@ button:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
input {
|
||||
.name {
|
||||
border: none;
|
||||
height: 36px;
|
||||
background-color: transparent;
|
||||
@@ -77,12 +77,12 @@ input {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
.name:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em;
|
||||
margin: 18px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ model-viewer {
|
||||
|
||||
.header .margin {
|
||||
font-size: 16px;
|
||||
margin: 1em 0.5em;
|
||||
margin: 18px 8px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@@ -168,6 +168,29 @@ model-viewer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top-edit {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 36px;
|
||||
top: 18px;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.top-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 36px;
|
||||
background-size: 24px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.squish {
|
||||
margin-right: -12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
@@ -180,6 +203,22 @@ model-viewer {
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.undo {
|
||||
background-image: url(/icons/undo.png);
|
||||
}
|
||||
|
||||
.redo {
|
||||
background-image: url(/icons/redo.png);
|
||||
}
|
||||
|
||||
.star {
|
||||
background-image: url(/icons/star.png);
|
||||
}
|
||||
|
||||
.share {
|
||||
background-image: url(/icons/share.png);
|
||||
}
|
||||
|
||||
.cancel {
|
||||
background-image: url(/icons/close.png);
|
||||
background-size: 20px;
|
||||
@@ -196,10 +235,22 @@ model-viewer {
|
||||
background-image: url(/icons/trash.png);
|
||||
}
|
||||
|
||||
.play {
|
||||
background-image: url(/icons/play.png);
|
||||
}
|
||||
|
||||
.pause {
|
||||
background-image: url(/icons/pause.png);
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.top-icon:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.blue {
|
||||
background-color: rgb(148, 221, 255);
|
||||
}
|
||||
@@ -221,7 +272,52 @@ model-viewer {
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
#animation {
|
||||
display: none;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#play {
|
||||
position: relative;
|
||||
background-color: #ccc;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
margin-right: 16px;
|
||||
background: #ffffff55;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 820px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
@@ -256,4 +352,4 @@ model-viewer {
|
||||
width: calc(50% - 16px);
|
||||
margin: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import ManifoldWorker from './worker?worker';
|
||||
import ManifoldWorker from './worker-wrapper?worker';
|
||||
|
||||
const CODE_START = '<code>';
|
||||
// Loaded globally by examples.js
|
||||
const exampleFunctions = self.examples.functionBodies;
|
||||
|
||||
@@ -24,6 +25,28 @@ if (navigator.serviceWorker) {
|
||||
|
||||
let editor = undefined;
|
||||
|
||||
// Edit UI ------------------------------------------------------------
|
||||
|
||||
const undoButton = document.querySelector('#undo');
|
||||
const redoButton = document.querySelector('#redo');
|
||||
const formatButton = document.querySelector('#format');
|
||||
const shareButton = document.querySelector('#share');
|
||||
|
||||
undoButton.onclick = () => editor.trigger('ignored', 'undo');
|
||||
redoButton.onclick = () => editor.trigger('ignored', 'redo');
|
||||
formatButton.onclick = () =>
|
||||
editor.trigger('ignored', 'editor.action.formatDocument');
|
||||
shareButton.onclick = () => {
|
||||
const url = new URL(window.location.toString());
|
||||
url.hash =
|
||||
'#' +
|
||||
encodeURIComponent(
|
||||
currentFileElement.textContent + CODE_START + editor.getValue());
|
||||
navigator.clipboard.writeText(url.toString());
|
||||
console.log('Shareable link copied to clipboard!');
|
||||
console.log('Consider shortening this URL using tinyURL.com');
|
||||
};
|
||||
|
||||
// File UI ------------------------------------------------------------
|
||||
const fileButton = document.querySelector('#file');
|
||||
const currentFileElement = document.querySelector('#current');
|
||||
@@ -90,9 +113,10 @@ function switchTo(scriptName) {
|
||||
switching = true;
|
||||
currentFileElement.textContent = scriptName;
|
||||
setScript('currentName', scriptName);
|
||||
const code =
|
||||
exampleFunctions.get(scriptName) ?? getScript(scriptName) ?? '';
|
||||
isExample = exampleFunctions.get(scriptName) != null;
|
||||
const code = isExample ? exampleFunctions.get(scriptName).substring(1) :
|
||||
getScript(scriptName) ?? '';
|
||||
window.location.hash = '#' + scriptName;
|
||||
editor.setValue(code);
|
||||
}
|
||||
}
|
||||
@@ -146,6 +170,7 @@ function addEdit(button) {
|
||||
const code = getScript(oldName);
|
||||
const form = document.createElement('form');
|
||||
const inputElement = document.createElement('input');
|
||||
inputElement.classList.add('name');
|
||||
inputElement.value = oldName;
|
||||
label.textContent = '';
|
||||
button.appendChild(form);
|
||||
@@ -203,16 +228,16 @@ function addEdit(button) {
|
||||
}
|
||||
|
||||
const newButton = document.querySelector('#new');
|
||||
function newItem(code) {
|
||||
const name = uniqueName('New Script');
|
||||
function newItem(code, scriptName = undefined) {
|
||||
const name = uniqueName(scriptName ?? 'New Script');
|
||||
setScript(name, code);
|
||||
const nextButton = createDropdownItem(name);
|
||||
newButton.insertAdjacentElement('afterend', nextButton.parentElement);
|
||||
addEdit(nextButton);
|
||||
nextButton.click();
|
||||
return {button: nextButton, name};
|
||||
};
|
||||
newButton.onclick = function() {
|
||||
newItem('');
|
||||
newItem('').button.click();
|
||||
};
|
||||
|
||||
const runButton = document.querySelector('#compile');
|
||||
@@ -225,7 +250,7 @@ function initializeRun() {
|
||||
if (autoExecute) {
|
||||
runButton.click();
|
||||
} else {
|
||||
poster.textContent = 'Auto-run disabled due to prior failure';
|
||||
poster.textContent = 'Auto-run disabled';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +276,7 @@ declare interface ManifoldToplevel {
|
||||
setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength;
|
||||
setCircularSegments: typeof T.setCircularSegments;
|
||||
getCircularSegments: typeof T.getCircularSegments;
|
||||
resetToCircularDefaults: typeof T.resetToCircularDefaults;
|
||||
setup: () => void;
|
||||
}
|
||||
declare const module: ManifoldToplevel;
|
||||
@@ -298,7 +324,24 @@ require(['vs/editor/editor.main'], async function() {
|
||||
addEdit(button);
|
||||
}
|
||||
}
|
||||
switchTo(currentName);
|
||||
|
||||
if (window.location.hash.length > 0) {
|
||||
const fragment = decodeURIComponent(window.location.hash.substring(1));
|
||||
const codeIdx = fragment.indexOf(CODE_START);
|
||||
if (codeIdx != -1) {
|
||||
autoExecute = true;
|
||||
const name = fragment.substring(0, codeIdx);
|
||||
switchTo(
|
||||
newItem(fragment.substring(codeIdx + CODE_START.length), name).name);
|
||||
} else {
|
||||
if (fragment != currentName) {
|
||||
autoExecute = true;
|
||||
}
|
||||
switchTo(fragment);
|
||||
}
|
||||
} else {
|
||||
switchTo(currentName);
|
||||
}
|
||||
|
||||
if (manifoldInitialized) {
|
||||
initializeRun();
|
||||
@@ -308,11 +351,12 @@ require(['vs/editor/editor.main'], async function() {
|
||||
runButton.disabled = false;
|
||||
if (switching) {
|
||||
switching = false;
|
||||
editor.setScrollTop(0);
|
||||
return;
|
||||
}
|
||||
if (isExample) {
|
||||
const cursor = editor.getPosition();
|
||||
newItem(editor.getValue());
|
||||
newItem(editor.getValue()).button.click();
|
||||
editor.setPosition(cursor);
|
||||
}
|
||||
});
|
||||
@@ -322,6 +366,51 @@ require(['vs/editor/editor.main'], async function() {
|
||||
};
|
||||
});
|
||||
|
||||
// Animation ------------------------------------------------------------
|
||||
const mv = document.querySelector('model-viewer');
|
||||
const animationContainer = document.querySelector('#animation');
|
||||
const playButton = document.querySelector('#play');
|
||||
const scrubber = document.querySelector('#scrubber');
|
||||
let paused = false;
|
||||
|
||||
mv.addEventListener('load', () => {
|
||||
const hasAnimation = mv.availableAnimations.length > 0;
|
||||
animationContainer.style.display = hasAnimation ? 'flex' : 'none';
|
||||
if (hasAnimation) {
|
||||
play();
|
||||
}
|
||||
});
|
||||
|
||||
function play() {
|
||||
mv.play();
|
||||
playButton.classList.remove('play');
|
||||
playButton.classList.add('pause');
|
||||
paused = false;
|
||||
scrubber.classList.add('hide');
|
||||
}
|
||||
|
||||
function pause() {
|
||||
mv.pause();
|
||||
playButton.classList.remove('pause');
|
||||
playButton.classList.add('play');
|
||||
paused = true;
|
||||
scrubber.max = mv.duration;
|
||||
scrubber.value = mv.currentTime;
|
||||
scrubber.classList.remove('hide');
|
||||
}
|
||||
|
||||
playButton.onclick = function() {
|
||||
if (paused) {
|
||||
play();
|
||||
} else {
|
||||
pause();
|
||||
}
|
||||
};
|
||||
|
||||
scrubber.oninput = function() {
|
||||
mv.currentTime = scrubber.value;
|
||||
};
|
||||
|
||||
// Execution ------------------------------------------------------------
|
||||
const consoleElement = document.querySelector('#console');
|
||||
const oldLog = console.log;
|
||||
@@ -357,7 +446,6 @@ function finishRun() {
|
||||
`Took ${(Math.round((t1 - t0) / 10) / 100).toLocaleString()} seconds`);
|
||||
}
|
||||
|
||||
const mv = document.querySelector('model-viewer');
|
||||
const output = {
|
||||
glbURL: null,
|
||||
threeMFURL: null
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
// Copyright 2023 The Manifold Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Accessor} from '@gltf-transform/core';
|
||||
|
||||
import {EXTManifold} from './manifold-gltf.js';
|
||||
|
||||
export const attributeDefs = {
|
||||
'POSITION': {type: Accessor.Type.VEC3, components: 3},
|
||||
'NORMAL': {type: Accessor.Type.VEC3, components: 3},
|
||||
'TANGENT': {type: Accessor.Type.VEC4, components: 4},
|
||||
'TEXCOORD_0': {type: Accessor.Type.VEC2, components: 2},
|
||||
'TEXCOORD_1': {type: Accessor.Type.VEC2, components: 2},
|
||||
'COLOR_0': {type: Accessor.Type.VEC3, components: 3},
|
||||
'JOINTS_0': {type: Accessor.Type.VEC4, components: 4},
|
||||
'WEIGHTS_0': {type: Accessor.Type.VEC4, components: 4},
|
||||
'SKIP_1': {type: null, components: 1},
|
||||
'SKIP_2': {type: null, components: 2},
|
||||
'SKIP_3': {type: null, components: 3},
|
||||
'SKIP_4': {type: null, components: 4},
|
||||
};
|
||||
|
||||
export function setupIO(io) {
|
||||
return io.registerExtensions([EXTManifold]);
|
||||
}
|
||||
|
||||
function readPrimitive(primitive, numProp, attributes) {
|
||||
const position = primitive.getAttribute('POSITION');
|
||||
const numVert = position.getCount();
|
||||
const vertProperties = [];
|
||||
let offset = 0;
|
||||
for (const attribute of attributes) {
|
||||
const size = attributeDefs[attribute].components;
|
||||
if (attribute === 'SKIP') {
|
||||
offset += size;
|
||||
continue;
|
||||
}
|
||||
const accessor = primitive.getAttribute(attribute);
|
||||
if (accessor) {
|
||||
const array = accessor.getArray();
|
||||
for (let i = 0; i < numVert; ++i) {
|
||||
for (let j = 0; j < size; ++j) {
|
||||
vertProperties[numProp * i + offset + j] = array[i * size + j];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < numVert; ++i) {
|
||||
for (let j = 0; j < size; ++j) {
|
||||
vertProperties[numProp * i + offset + j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
return vertProperties;
|
||||
}
|
||||
|
||||
export function readMesh(mesh, attributes, materials) {
|
||||
const primitives = mesh.listPrimitives();
|
||||
if (primitives.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (attributes.length === 0) {
|
||||
const attributeSet = new Set();
|
||||
for (const primitive of primitives) {
|
||||
const semantics = primitive.listSemantics();
|
||||
for (const semantic of semantics) {
|
||||
attributeSet.add(semantic);
|
||||
}
|
||||
}
|
||||
for (const semantic in attributeDefs) {
|
||||
if (attributeSet.has(semantic)) {
|
||||
attributes.push(semantic);
|
||||
attributeSet.delete(semantic);
|
||||
}
|
||||
}
|
||||
for (const semantic of attributeSet.keys()) {
|
||||
attributes.push(semantic);
|
||||
}
|
||||
}
|
||||
|
||||
if (attributes.length < 1 || attributes[0] !== 'POSITION')
|
||||
throw new Error('First attribute must be "POSITION".');
|
||||
|
||||
const numProp = attributes.map((def) => attributeDefs[def].components)
|
||||
.reduce((a, b) => a + b);
|
||||
|
||||
const manifoldPrimitive = mesh.getExtension('EXT_manifold');
|
||||
|
||||
let vertPropArray = [];
|
||||
let triVertArray = [];
|
||||
const runIndexArray = [0];
|
||||
const mergeFromVert = [];
|
||||
const mergeToVert = [];
|
||||
if (manifoldPrimitive != null) {
|
||||
vertPropArray = readPrimitive(primitives[0], numProp, attributes);
|
||||
for (const primitive of primitives) {
|
||||
triVertArray = [...triVertArray, ...primitive.getIndices().getArray()];
|
||||
runIndexArray.push(triVertArray.length);
|
||||
materials.push(primitive.getMaterial());
|
||||
}
|
||||
const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? [];
|
||||
const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? [];
|
||||
const vert2merge = new Map();
|
||||
for (const [i, idx] of mergeTriVert.entries()) {
|
||||
vert2merge.set(triVertArray[idx], mergeTo[i]);
|
||||
}
|
||||
for (const [from, to] of vert2merge.entries()) {
|
||||
mergeFromVert.push(from);
|
||||
mergeToVert.push(to);
|
||||
}
|
||||
} else {
|
||||
for (const primitive of primitives) {
|
||||
const numVert = vertPropArray.length / numProp;
|
||||
vertPropArray =
|
||||
[...vertPropArray, ...readPrimitive(primitive, numProp, attributes)];
|
||||
triVertArray = [
|
||||
...triVertArray,
|
||||
...primitive.getIndices().getArray().map((i) => i + numVert)
|
||||
];
|
||||
runIndexArray.push(triVertArray.length);
|
||||
materials.push(primitive.getMaterial());
|
||||
}
|
||||
}
|
||||
const vertProperties = new Float32Array(vertPropArray);
|
||||
const triVerts = new Uint32Array(triVertArray);
|
||||
const runIndex = new Uint32Array(runIndexArray);
|
||||
|
||||
return {
|
||||
numProp,
|
||||
triVerts,
|
||||
vertProperties,
|
||||
runIndex,
|
||||
mergeFromVert,
|
||||
mergeToVert
|
||||
};
|
||||
}
|
||||
|
||||
export function writeMesh(doc, manifoldMesh, attributes, materials) {
|
||||
if (doc.getRoot().listBuffers().length === 0) {
|
||||
doc.createBuffer();
|
||||
}
|
||||
const buffer = doc.getRoot().listBuffers()[0];
|
||||
const manifoldExtension = doc.createExtension(EXTManifold);
|
||||
|
||||
const attributeUnion = [];
|
||||
for (const matAttributes of attributes) {
|
||||
matAttributes.forEach((attribute, i) => {
|
||||
if (i >= attributeUnion.length) {
|
||||
attributeUnion.push(attribute);
|
||||
} else {
|
||||
const size = attributeDefs[attribute].components;
|
||||
const unionSize = attributeDefs[attributeUnion[i]].components;
|
||||
if (size != unionSize) {
|
||||
throw new Error(
|
||||
'Attribute sizes do not correspond: ', attribute, ' and ',
|
||||
attributeUnion[i]);
|
||||
}
|
||||
if (attributeDefs[attributeUnion[i]].type == null) {
|
||||
attributeUnion[i] = attribute;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (attributeUnion.length < 1 || attributeUnion[0] !== 'POSITION')
|
||||
throw new Error('First attribute must be "POSITION".');
|
||||
|
||||
const mesh = doc.createMesh();
|
||||
const numPrimitive = manifoldMesh.runIndex.length - 1;
|
||||
for (let run = 0; run < numPrimitive; ++run) {
|
||||
const indices = doc.createAccessor('indices')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array());
|
||||
const primitive = doc.createPrimitive().setIndices(indices);
|
||||
const material = materials[run];
|
||||
if (material) {
|
||||
primitive.setMaterial(material);
|
||||
}
|
||||
mesh.addPrimitive(primitive);
|
||||
}
|
||||
|
||||
const numVert = manifoldMesh.numVert;
|
||||
const numProp = manifoldMesh.numProp;
|
||||
let offset = 0;
|
||||
attributeUnion.forEach((attribute, aIdx) => {
|
||||
if (attribute === 'SKIP') {
|
||||
++offset;
|
||||
return;
|
||||
}
|
||||
const def = attributeDefs[attribute];
|
||||
if (def == null)
|
||||
throw new Error(attribute + ' is not a recognized attribute.');
|
||||
|
||||
const n = def.components;
|
||||
if (offset + n > numProp) throw new Error('Too many attribute channels.');
|
||||
|
||||
const array = new Float32Array(n * numVert);
|
||||
for (let v = 0; v < numVert; ++v) {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
array[n * v + i] =
|
||||
manifoldMesh.vertProperties[numProp * v + offset + i];
|
||||
}
|
||||
}
|
||||
|
||||
const accessor =
|
||||
doc.createAccessor().setBuffer(buffer).setType(def.type).setArray(
|
||||
array);
|
||||
|
||||
mesh.listPrimitives().forEach((primitive, pIdx) => {
|
||||
if (attributes[pIdx].length > aIdx &&
|
||||
attributeDefs[attributes[pIdx][aIdx]].type != null) {
|
||||
primitive.setAttribute(attribute, accessor);
|
||||
}
|
||||
});
|
||||
offset += n;
|
||||
});
|
||||
|
||||
const manifoldPrimitive = manifoldExtension.createManifoldPrimitive();
|
||||
mesh.setExtension('EXT_manifold', manifoldPrimitive);
|
||||
|
||||
const indices = doc.createAccessor('indices')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(manifoldMesh.triVerts);
|
||||
manifoldPrimitive.setIndices(indices);
|
||||
manifoldPrimitive.setRunIndex(manifoldMesh.runIndex);
|
||||
|
||||
const vert2merge = [...Array(manifoldMesh.numVert).keys()];
|
||||
for (const [i, from] of manifoldMesh.mergeFromVert.entries()) {
|
||||
vert2merge[from] = manifoldMesh.mergeToVert[i];
|
||||
}
|
||||
const ind = [];
|
||||
const val = [];
|
||||
for (const [i, vert] of manifoldMesh.triVerts.entries()) {
|
||||
const newVert = vert2merge[vert];
|
||||
if (vert !== newVert) {
|
||||
ind.push(i);
|
||||
val.push(newVert);
|
||||
}
|
||||
}
|
||||
const indicesAccessor = doc.createAccessor()
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(ind));
|
||||
const valuesAccessor = doc.createAccessor()
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(val));
|
||||
manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
export function disposeMesh(mesh) {
|
||||
if (!mesh) return;
|
||||
const primitives = mesh.listPrimitives();
|
||||
for (const primitive of primitives) {
|
||||
primitive.getIndices()?.dispose();
|
||||
for (const accessor of primitive.listAttributes()) {
|
||||
accessor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
mesh.dispose();
|
||||
}
|
||||
|
||||
export async function loadTexture(texture, uri) {
|
||||
const response = await fetch(uri);
|
||||
const blob = await response.blob();
|
||||
texture.setMimeType(blob.type);
|
||||
texture.setImage(new Uint8Array(await blob.arrayBuffer()));
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
// Copyright 2023 The Manifold Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Accessor, Document, Material, Mesh, Primitive, Texture, WebIO} from '@gltf-transform/core';
|
||||
|
||||
import {EXTManifold, ManifoldPrimitive} from './manifold-gltf';
|
||||
import {Mesh as ManifoldMesh, MeshOptions} from './public/manifold-encapsulated-types';
|
||||
|
||||
export const attributeDefs = {
|
||||
'POSITION': {type: Accessor.Type.VEC3, components: 3},
|
||||
'NORMAL': {type: Accessor.Type.VEC3, components: 3},
|
||||
'TANGENT': {type: Accessor.Type.VEC4, components: 4},
|
||||
'TEXCOORD_0': {type: Accessor.Type.VEC2, components: 2},
|
||||
'TEXCOORD_1': {type: Accessor.Type.VEC2, components: 2},
|
||||
'COLOR_0': {type: Accessor.Type.VEC3, components: 3},
|
||||
'JOINTS_0': {type: Accessor.Type.VEC4, components: 4},
|
||||
'WEIGHTS_0': {type: Accessor.Type.VEC4, components: 4},
|
||||
'SKIP_1': {type: null, components: 1},
|
||||
'SKIP_2': {type: null, components: 2},
|
||||
'SKIP_3': {type: null, components: 3},
|
||||
'SKIP_4': {type: null, components: 4},
|
||||
};
|
||||
|
||||
export type Attribute = keyof(typeof attributeDefs);
|
||||
|
||||
export interface Properties {
|
||||
material: Material;
|
||||
attributes: Attribute[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this first to register the manifold extension so that readMesh and
|
||||
* writeMesh will work.
|
||||
*/
|
||||
export function setupIO(io: WebIO) {
|
||||
return io.registerExtensions([EXTManifold]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an input mesh into Manifold-compatible data structures, whether it
|
||||
* contains the EXT_mesh_manifold extension or not.
|
||||
*
|
||||
* @param mesh The Mesh to read.
|
||||
* @param attributes An array of attributes representing the order of desired
|
||||
* properties returned in the vertProperties array of the output mesh. If
|
||||
* omitted, this will be populated with the union of all attributes defined
|
||||
* in the primitives of the input mesh. If present, the first entry must be
|
||||
* 'POSITION', and any attributes in the primitives that are not included in
|
||||
* this list will be ignored, while those in the list but not defined in a
|
||||
* primitive will be populated with zeros.
|
||||
* @returns The returned mesh is suitable for initializing a Manifold or Mesh of
|
||||
* the Manifold library if desired. See Manifold documentation if you prefer
|
||||
* to use these GL arrays in a different library. The runProperties array
|
||||
* gives the Material and attributes list associated with each triangle run,
|
||||
* which in turn corresponds to a primitive of the input mesh. These
|
||||
* attributes are the intersection of the attributes present on the
|
||||
* primitive and those requested in the attributes input.
|
||||
*/
|
||||
export function readMesh(mesh: Mesh, attributes: Attribute[] = []):
|
||||
{mesh: MeshOptions, runProperties: Properties[]}|null {
|
||||
const primitives = mesh.listPrimitives();
|
||||
if (primitives.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (attributes.length === 0) {
|
||||
const attributeSet = new Set<Attribute>();
|
||||
for (const primitive of primitives) {
|
||||
const semantics = primitive.listSemantics() as Attribute[];
|
||||
for (const semantic of semantics) {
|
||||
attributeSet.add(semantic);
|
||||
}
|
||||
}
|
||||
let semantic: Attribute;
|
||||
for (semantic in attributeDefs) {
|
||||
if (attributeSet.has(semantic)) {
|
||||
attributes.push(semantic);
|
||||
attributeSet.delete(semantic);
|
||||
}
|
||||
}
|
||||
for (const semantic of attributeSet.keys()) {
|
||||
attributes.push(semantic);
|
||||
}
|
||||
}
|
||||
|
||||
if (attributes.length < 1 || attributes[0] !== 'POSITION')
|
||||
throw new Error('First attribute must be "POSITION".');
|
||||
|
||||
let numProp = 0;
|
||||
const attributeOffsets = attributes.map((numProp = 0, def => {
|
||||
const last = numProp;
|
||||
numProp += attributeDefs[def].components;
|
||||
return last;
|
||||
}));
|
||||
|
||||
const manifoldPrimitive =
|
||||
mesh.getExtension('EXT_mesh_manifold') as ManifoldPrimitive;
|
||||
|
||||
let vertPropArray = Array<number>();
|
||||
let triVertArray = Array<number>();
|
||||
const runIndexArray = [0];
|
||||
const mergeFromVertArray = Array<number>();
|
||||
const mergeToVertArray = Array<number>();
|
||||
const runProperties = Array<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<number, number>();
|
||||
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 = Array<number>();
|
||||
const attributeUnion = Array<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 = Array<number>();
|
||||
const val = Array<number>();
|
||||
if (manifoldMesh.mergeFromVert && manifoldMesh.mergeToVert) {
|
||||
for (const [i, from] of manifoldMesh.mergeFromVert.entries()) {
|
||||
vert2merge[from] = manifoldMesh.mergeToVert[i];
|
||||
}
|
||||
|
||||
for (const [i, vert] of manifoldMesh.triVerts.entries()) {
|
||||
const newVert = vert2merge[vert];
|
||||
if (vert !== newVert) {
|
||||
ind.push(i);
|
||||
val.push(newVert);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ind.length > 0) {
|
||||
const indicesAccessor = doc.createAccessor('merge from')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(ind));
|
||||
const valuesAccessor = doc.createAccessor('merge to')
|
||||
.setBuffer(buffer)
|
||||
.setType(Accessor.Type.SCALAR)
|
||||
.setArray(new Uint32Array(val));
|
||||
manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor);
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to dispose of a Mesh, useful when replacing an existing Mesh
|
||||
* with one from writeMesh.
|
||||
*/
|
||||
export function disposeMesh(mesh: Mesh) {
|
||||
if (!mesh) return;
|
||||
const primitives = mesh.listPrimitives();
|
||||
for (const primitive of primitives) {
|
||||
primitive.getIndices()?.dispose();
|
||||
for (const accessor of primitive.listAttributes()) {
|
||||
accessor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
const manifoldPrimitive =
|
||||
mesh.getExtension('EXT_mesh_manifold') as ManifoldPrimitive;
|
||||
if (manifoldPrimitive) {
|
||||
manifoldPrimitive.getIndices()?.dispose();
|
||||
manifoldPrimitive.getMergeIndices()?.dispose();
|
||||
manifoldPrimitive.getMergeValues()?.dispose();
|
||||
}
|
||||
|
||||
mesh.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to download an image and apply it to the given texture.
|
||||
*
|
||||
* @param texture The texture to update
|
||||
* @param uri The location of the image to download
|
||||
*/
|
||||
export async function loadTexture(texture: Texture, uri: string) {
|
||||
const response = await fetch(uri);
|
||||
const blob = await response.blob();
|
||||
texture.setMimeType(blob.type);
|
||||
texture.setImage(new Uint8Array(await blob.arrayBuffer()));
|
||||
}
|
||||
|
||||
function writeProperties(
|
||||
vertProperties: number[], accessor: Accessor, numProp: number,
|
||||
offset: number) {
|
||||
const array = accessor.getArray()!;
|
||||
const size = accessor.getElementSize();
|
||||
const numVert = accessor.getCount();
|
||||
for (let i = 0; i < numVert; ++i) {
|
||||
for (let j = 0; j < size; ++j) {
|
||||
vertProperties[numProp * i + offset + j] = array[i * size + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readPrimitive(
|
||||
primitive: Primitive, numProp: number, attributes: Attribute[]) {
|
||||
const vertProperties: number[] = [];
|
||||
let offset = 0;
|
||||
for (const attribute of attributes) {
|
||||
const size = attributeDefs[attribute].components;
|
||||
if (attributeDefs[attribute].type == null) {
|
||||
offset += size;
|
||||
continue;
|
||||
}
|
||||
const accessor = primitive.getAttribute(attribute);
|
||||
if (accessor) {
|
||||
writeProperties(vertProperties, accessor, numProp, offset);
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
return vertProperties;
|
||||
}
|
||||
@@ -39,8 +39,15 @@
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container header" style="background-color: #A288B3;">
|
||||
<p style="flex: 2;"><a target="_blank" href="https://github.com/elalish/manifold">Manifold</a>CAD </p>
|
||||
<div class="margin" id="file-margin" >
|
||||
<p style="flex: 2;"><a target="_blank" href="https://github.com/elalish/manifold">Manifold</a>CAD
|
||||
<span class="top-edit blue">
|
||||
<button type="button" id="undo" class="top-icon undo squish"></button>
|
||||
<button type="button" id="redo" class="top-icon redo squish"></button>
|
||||
<button type="button" id="format" class="top-icon star squish"></button>
|
||||
<button type="button" id="share" class="top-icon share"></button>
|
||||
</span>
|
||||
</p>
|
||||
<div class="margin" id="file-margin">
|
||||
<button type="button" id="file" class="blue item"><b class="uparrow"></b> <span
|
||||
id="current">Intro</span></button>
|
||||
<div class="dropdown" id="fileDropdown">
|
||||
@@ -66,10 +73,14 @@
|
||||
<div id="editor" class="col"></div>
|
||||
<div class="col container" style="flex-direction: column;">
|
||||
<model-viewer camera-controls min-camera-orbit="auto 0deg auto" max-camera-orbit="auto 180deg auto"
|
||||
shadow-intensity="1" interaction-prompt="none" alt="Editor 3D output">
|
||||
shadow-intensity="1" tone-mapping="neutral" interaction-prompt="none" alt="Editor 3D output">
|
||||
<div class="center" slot="poster">
|
||||
<p id="poster" style="text-align: center; line-height: 36px;"> Loading... </p>
|
||||
</div>
|
||||
<div id="animation">
|
||||
<button type="button" id="play" class="icon pause"></button>
|
||||
<input type="range" min="0" max="1" step="0.01" value="0" class="slider" id="scrubber">
|
||||
</div>
|
||||
</model-viewer>
|
||||
<div id="console"></div>
|
||||
</div>
|
||||
@@ -84,6 +95,6 @@
|
||||
integrity="sha512-iQEIc0rsSDujsfjtD+lfyJ1W23Bh/lbgriubKDAym6VlEIDRj9rrbSIyJRyshOrl8s0yRcQ0+gyrZfSLyjJGWQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<script type="module" src="editor.js"></script>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.1.1/model-viewer.min.js"></script>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@
|
||||
<body>
|
||||
<p>Load a glTF/GLB model and our <a href="https://github.com/elalish/manifold">Manifold</a> library will attempt to
|
||||
merge it into a set of manifold objects. If the model is water-tight, you can download the new GLB which will have
|
||||
our <a href="https://github.com/KhronosGroup/glTF/pull/2286">EXT_manifold</a> extension, thus preserving the
|
||||
our <a href="https://github.com/KhronosGroup/glTF/pull/2286">EXT_mesh_manifold</a> extension, thus preserving the
|
||||
manifold data without losing any mesh properties.</p>
|
||||
<p> If the View Manifold GLB checkbox is enabled, then some meshes in the model have open edges and don't represent a
|
||||
solid object. Check this box to see only the manifold parts, which will also enable them to be downloaded. If the
|
||||
@@ -55,135 +55,11 @@
|
||||
<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="neutral" 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">
|
||||
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 Module from './built/manifold.js';
|
||||
|
||||
const io = setupIO(new WebIO());
|
||||
io.registerExtensions(KHRONOS_EXTENSIONS);
|
||||
|
||||
const wasm = await Module();
|
||||
wasm.setup();
|
||||
|
||||
const { Manifold, Mesh } = wasm;
|
||||
|
||||
const mv = document.querySelector('model-viewer');
|
||||
const inputEl = document.querySelector('#input');
|
||||
const downloadButton = document.querySelector('#download');
|
||||
const checkbox = document.querySelector('#viewFinal');
|
||||
const dropCtrl = new SimpleDropzone(mv, inputEl);
|
||||
|
||||
let inputGLBurl = null;
|
||||
let outputGLBurl = null;
|
||||
let allManifold = true;
|
||||
let anyManifold = false;
|
||||
|
||||
dropCtrl.on('drop', async ({ files }) => {
|
||||
for (const [path, file] of files) {
|
||||
const filename = file.name.toLowerCase();
|
||||
if (filename.match(/\.(gltf|glb)$/)) {
|
||||
URL.revokeObjectURL(inputGLBurl);
|
||||
inputGLBurl = URL.createObjectURL(file);
|
||||
await writeGLB(await readGLB(inputGLBurl));
|
||||
updateUI();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function updateUI() {
|
||||
if (allManifold) {
|
||||
checkbox.checked = true;
|
||||
checkbox.disabled = true;
|
||||
} else if (anyManifold) {
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = false;
|
||||
} else {
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = true;
|
||||
}
|
||||
onClick();
|
||||
}
|
||||
|
||||
checkbox.onclick = onClick;
|
||||
|
||||
function onClick() {
|
||||
mv.src = checkbox.checked ? outputGLBurl : inputGLBurl;
|
||||
downloadButton.disabled = !checkbox.checked;
|
||||
};
|
||||
|
||||
downloadButton.onclick = function () {
|
||||
const link = document.createElement('a');
|
||||
link.download = 'manifold.glb';
|
||||
link.href = outputGLBurl;
|
||||
link.click();
|
||||
};
|
||||
|
||||
async function writeGLB(doc) {
|
||||
URL.revokeObjectURL(outputGLBurl);
|
||||
if (!anyManifold) {
|
||||
return;
|
||||
}
|
||||
const glb = await io.writeBinary(doc);
|
||||
|
||||
const blob = new Blob([glb], { type: 'application/octet-stream' });
|
||||
outputGLBurl = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async function readGLB(url) {
|
||||
allManifold = false;
|
||||
anyManifold = false;
|
||||
updateUI();
|
||||
allManifold = true;
|
||||
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 matAttributes = [];
|
||||
materials.forEach(() => matAttributes.push(attributes));
|
||||
|
||||
manifoldMesh.merge();
|
||||
|
||||
try {
|
||||
// Test manifoldness - will throw if not.
|
||||
const manifold = Manifold(manifoldMesh);
|
||||
node.setMesh(writeMesh(docIn, manifold.getMesh(), matAttributes, materials));
|
||||
manifold.delete();
|
||||
anyManifold = true;
|
||||
} catch (e) {
|
||||
console.log(mesh.getName(), e);
|
||||
allManifold = false;
|
||||
}
|
||||
}
|
||||
|
||||
await docIn.transform(prune());
|
||||
|
||||
return docIn;
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
|
||||
<script type="module" src="make-manifold.ts"></script>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright 2024 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 {Document, WebIO} from '@gltf-transform/core';
|
||||
import {KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
|
||||
import {prune} from '@gltf-transform/functions';
|
||||
import {SimpleDropzone} from 'simple-dropzone';
|
||||
|
||||
import Module from './built/manifold.js';
|
||||
import {disposeMesh, Properties, readMesh, setupIO, writeMesh} from './gltf-io';
|
||||
|
||||
// Set up gltf-transform
|
||||
const io = setupIO(new WebIO());
|
||||
io.registerExtensions(KHRONOS_EXTENSIONS);
|
||||
|
||||
// Set up Manifold WASM library
|
||||
const wasm = await Module();
|
||||
wasm.setup();
|
||||
const {Manifold, Mesh} = wasm;
|
||||
|
||||
// UX elements
|
||||
const mv = document.querySelector('model-viewer');
|
||||
const inputEl = document.querySelector('#input') as HTMLInputElement;
|
||||
const downloadButton = document.querySelector('#download') as HTMLButtonElement;
|
||||
const checkbox = document.querySelector('#viewFinal') as HTMLInputElement;
|
||||
const dropCtrl = new SimpleDropzone(mv, inputEl) as any;
|
||||
|
||||
// glTF objects in memory
|
||||
let inputGLBurl = '';
|
||||
let outputGLBurl = '';
|
||||
// Status of manifoldness of all meshes
|
||||
let allManifold = true;
|
||||
let anyManifold = false;
|
||||
|
||||
// The processing is run when a glTF is drag-and-dropped onto this element.
|
||||
dropCtrl.on('drop', async ({files}: {files: Map<string, File>}) => {
|
||||
for (const [_path, file] of files) {
|
||||
const filename = file.name.toLowerCase();
|
||||
if (filename.match(/\.(gltf|glb)$/)) {
|
||||
URL.revokeObjectURL(inputGLBurl);
|
||||
inputGLBurl = URL.createObjectURL(file);
|
||||
await writeGLB(await readGLB(inputGLBurl));
|
||||
updateUI();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// UI functions
|
||||
|
||||
function updateUI() {
|
||||
if (allManifold) {
|
||||
checkbox.checked = true;
|
||||
checkbox.disabled = true;
|
||||
} else if (anyManifold) {
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = false;
|
||||
} else {
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = true;
|
||||
}
|
||||
onClick();
|
||||
}
|
||||
|
||||
checkbox.onclick = onClick;
|
||||
|
||||
function onClick() {
|
||||
(mv as any).src = checkbox.checked ? outputGLBurl : inputGLBurl;
|
||||
downloadButton.disabled = !checkbox.checked;
|
||||
};
|
||||
|
||||
downloadButton.onclick = () => {
|
||||
const link = document.createElement('a');
|
||||
link.download = 'manifold.glb';
|
||||
link.href = outputGLBurl;
|
||||
link.click();
|
||||
};
|
||||
|
||||
// Write output glTF using gltf-transform, which contains only the meshes that
|
||||
// are manifold, and using the EXT_mesh_manifold extension.
|
||||
async function writeGLB(doc: Document): Promise<void> {
|
||||
URL.revokeObjectURL(outputGLBurl);
|
||||
if (!anyManifold) {
|
||||
return;
|
||||
}
|
||||
const glb = await io.writeBinary(doc);
|
||||
|
||||
const blob = new Blob([glb], {type: 'application/octet-stream'});
|
||||
outputGLBurl = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
// Read the glTF ObjectURL and return a gltf-transform document with all the
|
||||
// non-manifold meshes stripped out.
|
||||
async function readGLB(url: string): Promise<Document> {
|
||||
allManifold = false;
|
||||
anyManifold = false;
|
||||
updateUI();
|
||||
allManifold = true;
|
||||
const docIn = await io.read(url);
|
||||
const nodes = docIn.getRoot().listNodes();
|
||||
for (const node of nodes) {
|
||||
const mesh = node.getMesh();
|
||||
if (!mesh) continue;
|
||||
|
||||
const tmp = readMesh(mesh);
|
||||
if (!tmp) continue;
|
||||
|
||||
const id2properties = new Map<number, Properties>();
|
||||
const numID = tmp.runProperties.length;
|
||||
const firstID = Manifold.reserveIDs(numID);
|
||||
tmp.mesh.runOriginalID = new Uint32Array(numID);
|
||||
for (let i = 0; i < numID; ++i) {
|
||||
tmp.mesh.runOriginalID[i] = firstID + i;
|
||||
id2properties.set(firstID + i, tmp.runProperties[i]);
|
||||
}
|
||||
const manifoldMesh = new Mesh(tmp.mesh);
|
||||
disposeMesh(mesh);
|
||||
// Make the mesh manifold if it's close.
|
||||
manifoldMesh.merge();
|
||||
|
||||
try {
|
||||
// Test manifoldness - will throw if not.
|
||||
const manifold = new Manifold(manifoldMesh);
|
||||
// Replace the mesh with a manifold version
|
||||
node.setMesh(writeMesh(docIn, manifold.getMesh(), id2properties));
|
||||
manifold.delete();
|
||||
anyManifold = true;
|
||||
} catch (e) {
|
||||
console.log(mesh.getName(), e);
|
||||
allManifold = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Prune the leftovers after non-manifold mesh removal.
|
||||
await docIn.transform(prune());
|
||||
|
||||
return docIn;
|
||||
}
|
||||
@@ -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 = [];
|
||||
const runIndex = Array<number>();
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2024 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 {Document, WebIO} from '@gltf-transform/core';
|
||||
import {clearNodeTransform, flatten, prune} from '@gltf-transform/functions';
|
||||
|
||||
import Module, {Manifold, Mesh} from './built/manifold';
|
||||
import {disposeMesh, Properties, readMesh, setupIO, writeMesh} from './gltf-io';
|
||||
|
||||
// Set up gltf-transform
|
||||
const io = setupIO(new WebIO());
|
||||
const doc = new Document();
|
||||
|
||||
|
||||
// Set up Manifold WASM library
|
||||
const wasm = await Module();
|
||||
wasm.setup();
|
||||
const {Manifold, Mesh} = wasm;
|
||||
|
||||
// Map of OriginalID to glTF material and attributes
|
||||
const id2properties = new Map<number, Properties>();
|
||||
|
||||
// Wrapper for gltf-io readMesh() that processes a whole glTF and stores the
|
||||
// properties in the above map. This function is simplified and intended only
|
||||
// for reading single-object glTFs, as it simply unions any extra meshes
|
||||
// together rather than returning a scene hierarchy.
|
||||
async function readGLB(url: string) {
|
||||
const manifolds = Array<Manifold>();
|
||||
const docIn = await io.read(url);
|
||||
await docIn.transform(flatten());
|
||||
const nodes = docIn.getRoot().listNodes();
|
||||
const ids = Array<number>();
|
||||
for (const node of nodes) {
|
||||
clearNodeTransform(node);
|
||||
const gltfMesh = node.getMesh();
|
||||
if (gltfMesh == null) continue;
|
||||
const tmp = readMesh(gltfMesh);
|
||||
if (tmp == null) continue;
|
||||
|
||||
const numID = tmp.runProperties.length;
|
||||
const firstID = Manifold.reserveIDs(numID);
|
||||
tmp.mesh.runOriginalID = new Uint32Array(numID);
|
||||
for (let i = 0; i < numID; ++i) {
|
||||
tmp.mesh.runOriginalID[i] = firstID + i;
|
||||
ids.push(firstID + i);
|
||||
id2properties.set(firstID + i, tmp.runProperties[i]);
|
||||
}
|
||||
manifolds.push(new 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];
|
||||
id2properties.get(id)!.material = material;
|
||||
}
|
||||
|
||||
return Manifold.union(manifolds);
|
||||
}
|
||||
|
||||
// Read static input glTFs
|
||||
const space = await readGLB('/models/space.glb');
|
||||
const moon = await readGLB('/models/moon.glb');
|
||||
|
||||
const node = doc.createNode();
|
||||
doc.createScene().addChild(node);
|
||||
|
||||
// Set up UI for operations
|
||||
type BooleanOp = 'union'|'difference'|'intersection';
|
||||
|
||||
function csg(operation: BooleanOp) {
|
||||
push2MV(Manifold[operation](space, moon));
|
||||
}
|
||||
|
||||
csg('difference');
|
||||
const selectElement = document.querySelector('select')!;
|
||||
selectElement.onchange = function() {
|
||||
csg(selectElement.value as BooleanOp);
|
||||
};
|
||||
|
||||
// The resulting glTF
|
||||
let objectURL = '';
|
||||
|
||||
// Set up download UI
|
||||
const downloadButton = document.querySelector('#download') as HTMLButtonElement;
|
||||
downloadButton.onclick = function() {
|
||||
const link = document.createElement('a');
|
||||
link.download = 'manifold.glb';
|
||||
link.href = objectURL;
|
||||
link.click();
|
||||
};
|
||||
|
||||
// <model-viewer> element for rendering resulting glTF.
|
||||
const mv = document.querySelector('model-viewer');
|
||||
|
||||
// Use gltf-io and gltf-transform to convert the resulting Manifold to a glTF
|
||||
// and display it with <model-viewer>.
|
||||
async function push2MV(manifold: Manifold) {
|
||||
disposeMesh(node.getMesh()!);
|
||||
const mesh = writeMesh(doc, manifold.getMesh(), id2properties);
|
||||
node.setMesh(mesh);
|
||||
await doc.transform(prune());
|
||||
|
||||
const glb = await io.writeBinary(doc);
|
||||
|
||||
const blob = new Blob([glb], {type: 'application/octet-stream'});
|
||||
URL.revokeObjectURL(objectURL);
|
||||
objectURL = URL.createObjectURL(blob);
|
||||
(mv as any).src = objectURL;
|
||||
}
|
||||
@@ -35,117 +35,24 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>This example demonstrates feeding Manifold's output into <a
|
||||
href="https://modelviewer.dev/"><model-viewer></a> via <a
|
||||
href="https://gltf-transform.donmccurdy.com/index.html">gltf-transform</a>. It also shows how to pass mesh
|
||||
properties like UV coordinates through Manifold and how to re-associate textures after operations. A more basic
|
||||
three.js example is <a href="three.html">here</a>. </p>
|
||||
<p>This example demonstrates reading and writing glTF models using <a
|
||||
href="https://gltf-transform.donmccurdy.com/index.html">gltf-transform</a> and using this to feed Manifold's
|
||||
output into <a href="https://modelviewer.dev/"><model-viewer></a>. It also shows how to pass mesh properties
|
||||
like UV coordinates through Manifold and how to re-associate textures and materials after operations, made simple
|
||||
using our `gltf-io.ts` library. The resulting glTF includes the EXT_mesh_manifold extension, using our
|
||||
`manifold-gltf.ts` extension to gltf-transform, which ensures lossless data transfer.</p>
|
||||
<p>Please open dev tools to inspect our source code - everything is source mapped back to our TS files and commented
|
||||
thoroughly. A more basic three.js example is <a href="three.html">here</a>. </p>
|
||||
<select>
|
||||
<option value="difference" selected>Difference</option>
|
||||
<option value="intersection">Intersection</option>
|
||||
<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="neutral"
|
||||
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">
|
||||
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 Module from './built/manifold.js';
|
||||
|
||||
const io = setupIO(new WebIO());
|
||||
const doc = new Document();
|
||||
|
||||
const wasm = await Module();
|
||||
wasm.setup();
|
||||
|
||||
const { Manifold, Mesh } = wasm;
|
||||
|
||||
const attributes = ['POSITION', 'TEXCOORD_0'];
|
||||
const materialMap = new Map();
|
||||
|
||||
async function readGLB(url, attributes, materialMap) {
|
||||
const manifolds = [];
|
||||
const docIn = await io.read(url);
|
||||
await docIn.transform(flatten());
|
||||
const nodes = docIn.getRoot().listNodes();
|
||||
const ids = [];
|
||||
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);
|
||||
ids.push(firstID + i);
|
||||
}
|
||||
manifolds.push(Manifold(new Mesh(manifoldMesh)));
|
||||
}
|
||||
// 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());
|
||||
for (const [i, id] of ids.entries()) {
|
||||
const material = doc.getRoot().listMaterials()[startIdx + i];
|
||||
materialMap.set(id, material);
|
||||
}
|
||||
|
||||
return Manifold.union(manifolds);
|
||||
}
|
||||
|
||||
const space = await readGLB('/models/space.glb', attributes, materialMap);
|
||||
const moon = await readGLB('/models/moon.glb', attributes, materialMap);
|
||||
|
||||
const node = doc.createNode();
|
||||
doc.createScene().addChild(node);
|
||||
|
||||
function csg(operation) {
|
||||
push2MV(Manifold[operation](space, moon));
|
||||
};
|
||||
|
||||
document.querySelector('select').onchange = function (event) {
|
||||
csg(event.target.value);
|
||||
};
|
||||
|
||||
csg('difference');
|
||||
|
||||
const mv = document.querySelector('model-viewer');
|
||||
let objectURL = null;
|
||||
|
||||
const downloadButton = document.querySelector('#download');
|
||||
downloadButton.onclick = function () {
|
||||
const link = document.createElement('a');
|
||||
link.download = 'manifold.glb';
|
||||
link.href = objectURL;
|
||||
link.click();
|
||||
};
|
||||
|
||||
async function push2MV(manifold) {
|
||||
// From Z-up to Y-up (glTF)
|
||||
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);
|
||||
node.setMesh(mesh);
|
||||
await doc.transform(prune());
|
||||
|
||||
const glb = await io.writeBinary(doc);
|
||||
|
||||
const blob = new Blob([glb], { type: 'application/octet-stream' });
|
||||
URL.revokeObjectURL(objectURL);
|
||||
objectURL = URL.createObjectURL(blob);
|
||||
mv.src = objectURL;
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
|
||||
<script type="module" src="model-viewer-script.ts"></script>
|
||||
|
||||
</html>
|
||||
@@ -8,25 +8,25 @@
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:run": "vitest run"
|
||||
"test:dev": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gltf-transform/core": "^3.2.1",
|
||||
"@gltf-transform/extensions": "^3.2.1",
|
||||
"@gltf-transform/functions": "^3.2.1",
|
||||
"@jscadui/3mf-export": "^0.4.2",
|
||||
"@gltf-transform/core": "^3.8.0",
|
||||
"@gltf-transform/extensions": "^3.8.0",
|
||||
"@gltf-transform/functions": "^3.8.0",
|
||||
"@jscadui/3mf-export": "^0.5.0",
|
||||
"@types/three": "^0.164.0",
|
||||
"fflate": "^0.8.0",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"simple-dropzone": "0.8.3",
|
||||
"three": "0.151.2"
|
||||
"three": "0.164.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^0.31.1",
|
||||
"@vitest/web-worker": "^0.31.1",
|
||||
"typescript": "5.0.4",
|
||||
"vite": "^4.3.2",
|
||||
"vitest": "^0.31.1"
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"typescript": "5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
"vitest": "^1.6.0"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/** @type {import("../../manifold-encapsulated-types")["Manifold"]} */
|
||||
const Manifold = globalThis.Manifold;
|
||||
|
||||
export const examples = {
|
||||
functions: {
|
||||
Intro: function() {
|
||||
@@ -50,9 +53,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 +64,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 +87,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;
|
||||
},
|
||||
|
||||
@@ -108,7 +131,7 @@ export const examples = {
|
||||
// Demonstrates how at 90-degree intersections, the sphere and cylinder
|
||||
// facets match up perfectly, for any choice of global resolution
|
||||
// parameters.
|
||||
const {sphere, cylinder, union} = Manifold;
|
||||
const {sphere, cylinder, union, cube} = Manifold;
|
||||
|
||||
function roundedFrame(edgeLength, radius, circularSegments = 0) {
|
||||
const edge = cylinder(edgeLength, radius, -1, circularSegments);
|
||||
@@ -132,6 +155,17 @@ export const examples = {
|
||||
setMinCircularAngle(3);
|
||||
setMinCircularEdgeLength(0.5);
|
||||
const result = roundedFrame(100, 10);
|
||||
// Demonstrate how you can use the .split method to perform
|
||||
// a subtraction and an intersection at once
|
||||
const [inside, outside] = result.split(cube(100, true));
|
||||
|
||||
const outsideNode = new GLTFNode();
|
||||
outsideNode.manifold = outside;
|
||||
|
||||
const insideNode = new GLTFNode();
|
||||
insideNode.manifold = inside;
|
||||
insideNode.material = {baseColorFactor: [0, 1, 1]};
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -139,6 +173,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 +209,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 +299,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 +393,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 |
@@ -0,0 +1 @@
|
||||
declare module 'simple-dropzone';
|
||||
@@ -35,83 +35,21 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>This demonstrates the minimum code to connect Manifold to a three.js rendering. A slightly more complex example
|
||||
involving GLTF export and <model-viewer> can be found <a href="model-viewer.html">here</a>. </p>
|
||||
<p>This example demonstrates interop between Manifold and Three.js with minimal code - please open dev tools to
|
||||
inspect the source. Here we generate two multi-material Three.js meshes and convert them to Manifolds while building
|
||||
a mapping from material to Manifold ID. Then Boolean operations are performed on the Manifolds and the result is
|
||||
converted back to a Three.js mesh using the material mapping. The input cube has half red faces and half
|
||||
normal-shaded, while the icosahedron has half blue faces and half normal-shaded. The resulting mesh has three
|
||||
materials, since one (normal-shaded) was common between the input models.</p>
|
||||
<p>A slightly more complex example involving our library for GLTF import/export and <model-viewer> can be found
|
||||
<a href="model-viewer.html">here</a>. </p>
|
||||
<select>
|
||||
<option value="difference" selected>Difference</option>
|
||||
<option value="union" selected>Union</option>
|
||||
<option value="difference">Difference</option>
|
||||
<option value="intersection">Intersection</option>
|
||||
<option value="union">Union</option>
|
||||
</select><br />
|
||||
<canvas id="output"></canvas>
|
||||
</body>
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
||||
import Module from './built/manifold.js';
|
||||
|
||||
const wasm = await Module();
|
||||
wasm.setup();
|
||||
|
||||
const { Manifold, Mesh } = wasm;
|
||||
|
||||
// we have manifold module, let's do some three.js
|
||||
const camera = new THREE.PerspectiveCamera(30, 1, 0.01, 10);
|
||||
camera.position.z = 1;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const mesh = new THREE.Mesh(undefined, new THREE.MeshNormalMaterial({
|
||||
flatShading: true
|
||||
}));
|
||||
scene.add(mesh);
|
||||
|
||||
const icosahedron = simplify(new THREE.IcosahedronGeometry(0.16));
|
||||
|
||||
const manifold_1 = Manifold.cube([0.2, 0.2, 0.2], true);
|
||||
const manifold_2 = new Manifold(geometry2mesh(icosahedron));
|
||||
|
||||
const csg = function (operation) {
|
||||
mesh.geometry?.dispose();
|
||||
mesh.geometry = mesh2geometry(Manifold[operation](manifold_1, manifold_2).getMesh());
|
||||
};
|
||||
|
||||
document.querySelector('select').onchange = function (event) {
|
||||
csg(event.target.value);
|
||||
};
|
||||
|
||||
csg('difference');
|
||||
|
||||
const output = document.querySelector("#output");
|
||||
const renderer = new THREE.WebGLRenderer({ canvas: output, antialias: true });
|
||||
const dim = output.getBoundingClientRect();
|
||||
renderer.setSize(dim.width, dim.height);
|
||||
renderer.setAnimationLoop(function (time) {
|
||||
mesh.rotation.x = time / 2000;
|
||||
mesh.rotation.y = time / 1000;
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
|
||||
// functions to convert between three.js and wasm
|
||||
function geometry2mesh(geometry) {
|
||||
const vertProperties = geometry.attributes.position.array;
|
||||
const triVerts = geometry.index.array;
|
||||
return new Mesh({ numProp: 3, vertProperties, triVerts });
|
||||
}
|
||||
|
||||
function mesh2geometry(mesh) {
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(mesh.vertProperties, 3));
|
||||
geometry.setIndex(new THREE.BufferAttribute(mesh.triVerts, 1));
|
||||
return geometry;
|
||||
}
|
||||
|
||||
// most of three.js geometries aren't manifolds, so...
|
||||
function simplify(geometry) {
|
||||
delete geometry.attributes.normal;
|
||||
delete geometry.attributes.uv;
|
||||
const simplified = mergeVertices(geometry);
|
||||
simplified.computeVertexNormals();
|
||||
return simplified;
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="three.ts"></script>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2024 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 {BoxGeometry, BufferAttribute, BufferGeometry, IcosahedronGeometry, Mesh as ThreeMesh, MeshLambertMaterial, MeshNormalMaterial, PerspectiveCamera, PointLight, Scene, WebGLRenderer} from 'three';
|
||||
|
||||
import Module, {Mesh} from './built/manifold';
|
||||
|
||||
// Load Manifold WASM library
|
||||
const wasm = await Module();
|
||||
wasm.setup();
|
||||
const {Manifold, Mesh} = wasm;
|
||||
|
||||
// Define our set of materials
|
||||
const materials = [
|
||||
new MeshNormalMaterial({flatShading: true}),
|
||||
new MeshLambertMaterial({color: 'red', flatShading: true}),
|
||||
new MeshLambertMaterial({color: 'blue', flatShading: true})
|
||||
];
|
||||
const result = new ThreeMesh(undefined, materials);
|
||||
|
||||
// Set up Manifold IDs corresponding to materials
|
||||
const firstID = Manifold.reserveIDs(materials.length);
|
||||
// ids vector is parallel to materials vector - same indexing
|
||||
const ids = [...Array<number>(materials.length)].map((_, idx) => firstID + idx);
|
||||
// Build a mapping to get back from ID to material index
|
||||
const id2matIndex = new Map();
|
||||
ids.forEach((id, idx) => id2matIndex.set(id, idx));
|
||||
|
||||
// Set up Three.js scene
|
||||
const scene = new Scene();
|
||||
const camera = new PerspectiveCamera(30, 1, 0.01, 10);
|
||||
camera.position.z = 1;
|
||||
camera.add(new PointLight(0xffffff, 1));
|
||||
scene.add(camera);
|
||||
scene.add(result);
|
||||
|
||||
// Set up Three.js renderer
|
||||
const output = document.querySelector('#output')!;
|
||||
const renderer = new WebGLRenderer({canvas: output, antialias: true});
|
||||
const dim = output.getBoundingClientRect();
|
||||
renderer.setSize(dim.width, dim.height);
|
||||
renderer.setAnimationLoop(function(time: number) {
|
||||
result.rotation.x = time / 2000;
|
||||
result.rotation.y = time / 1000;
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
|
||||
// Create input meshes in Three.js
|
||||
const cube = new BoxGeometry(0.2, 0.2, 0.2);
|
||||
cube.clearGroups();
|
||||
cube.addGroup(0, 18, 0); // First 6 faces colored by normal
|
||||
cube.addGroup(18, Infinity, 1); // Rest of faces are red
|
||||
|
||||
const icosahedron = new IcosahedronGeometry(0.16);
|
||||
icosahedron.clearGroups();
|
||||
icosahedron.addGroup(30, Infinity, 2); // Last faces are blue
|
||||
icosahedron.addGroup(0, 30, 0); // First 10 faces colored by normal
|
||||
// The above groups are in reversed order to demonstrate the need for sorting.
|
||||
|
||||
// Convert Three.js input meshes to Manifolds
|
||||
const manifoldCube = new Manifold(geometry2mesh(cube));
|
||||
const manifoldIcosahedron = new Manifold(geometry2mesh(icosahedron));
|
||||
|
||||
// Set up UI for operations
|
||||
type BooleanOp = 'union'|'difference'|'intersection';
|
||||
|
||||
function csg(operation: BooleanOp) {
|
||||
result.geometry?.dispose();
|
||||
result.geometry = mesh2geometry(
|
||||
Manifold[operation](manifoldCube, manifoldIcosahedron).getMesh());
|
||||
}
|
||||
|
||||
csg('union');
|
||||
const selectElement = document.querySelector('select')!;
|
||||
selectElement.onchange = function() {
|
||||
csg(selectElement.value as BooleanOp);
|
||||
};
|
||||
|
||||
// Convert Three.js BufferGeometry to Manifold Mesh
|
||||
function geometry2mesh(geometry: BufferGeometry) {
|
||||
// Only using position in this sample for simplicity. Can interleave any other
|
||||
// desired attributes here such as UV, normal, etc.
|
||||
const vertProperties = geometry.attributes.position.array as Float32Array;
|
||||
// Manifold only uses indexed geometry, so generate an index if necessary.
|
||||
const triVerts = geometry.index != null ?
|
||||
geometry.index.array as Uint32Array :
|
||||
new Uint32Array(vertProperties.length / 3).map((_, idx) => idx);
|
||||
// Create a triangle run for each group (material) - akin to a draw call.
|
||||
const starts = [...Array(geometry.groups.length)].map(
|
||||
(_, idx) => geometry.groups[idx].start);
|
||||
// Map the materials to ID.
|
||||
const originalIDs = [...Array(geometry.groups.length)].map(
|
||||
(_, idx) => ids[geometry.groups[idx].materialIndex!]);
|
||||
// List the runs in sequence.
|
||||
const indices = Array.from(starts.keys())
|
||||
indices.sort((a, b) => starts[a] - starts[b])
|
||||
const runIndex = new Uint32Array(indices.map(i => starts[i]));
|
||||
const runOriginalID = new Uint32Array(indices.map(i => originalIDs[i]));
|
||||
// Create the MeshGL for I/O with Manifold library.
|
||||
const mesh =
|
||||
new Mesh({numProp: 3, vertProperties, triVerts, runIndex, runOriginalID});
|
||||
// Automatically merge vertices with nearly identical positions to create a
|
||||
// Manifold. This only fills in the mergeFromVert and mergeToVert vectors -
|
||||
// these are automatically filled in for any mesh returned by Manifold. These
|
||||
// are necessary because GL drivers require duplicate verts when any
|
||||
// properties change, e.g. a UV boundary or sharp corner.
|
||||
mesh.merge();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// Convert Manifold Mesh to Three.js BufferGeometry
|
||||
function mesh2geometry(mesh: Mesh) {
|
||||
const geometry = new BufferGeometry();
|
||||
// Assign buffers
|
||||
geometry.setAttribute(
|
||||
'position', new BufferAttribute(mesh.vertProperties, 3));
|
||||
geometry.setIndex(new BufferAttribute(mesh.triVerts, 1));
|
||||
// Create a group (material) for each ID. Note that there may be multiple
|
||||
// triangle runs returned with the same ID, though these will always be
|
||||
// sequential since they are sorted by ID. In this example there are two runs
|
||||
// for the MeshNormalMaterial, one corresponding to each input mesh that had
|
||||
// this ID. This allows runTransform to return the total transformation matrix
|
||||
// applied to each triangle run from its input mesh - even after many
|
||||
// consecutive operations.
|
||||
let id = mesh.runOriginalID[0];
|
||||
let start = mesh.runIndex[0];
|
||||
for (let run = 0; run < mesh.numRun; ++run) {
|
||||
const nextID = mesh.runOriginalID[run + 1];
|
||||
if (nextID !== id) {
|
||||
const end = mesh.runIndex[run + 1];
|
||||
geometry.addGroup(start, end - start, id2matIndex.get(id));
|
||||
id = nextID;
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
return geometry;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": [
|
||||
"es2022",
|
||||
"dom"
|
||||
|
||||
@@ -9,6 +9,7 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
manifoldCAD: resolve(__dirname, 'index.html'),
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2024 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 {cleanup, evaluateCADToModel} from './worker';
|
||||
|
||||
// Setup complete
|
||||
self.postMessage(null);
|
||||
|
||||
if (self.console) {
|
||||
const oldLog = self.console.log;
|
||||
self.console.log = function(...args) {
|
||||
let message = '';
|
||||
for (const arg of args) {
|
||||
if (arg == null) {
|
||||
message += 'undefined';
|
||||
} else if (typeof arg == 'object') {
|
||||
message += JSON.stringify(arg, null, 4);
|
||||
} else {
|
||||
message += arg.toString();
|
||||
}
|
||||
}
|
||||
self.postMessage({log: message});
|
||||
oldLog(...args);
|
||||
};
|
||||
}
|
||||
|
||||
self.onmessage = async (e) => {
|
||||
try {
|
||||
const result = await evaluateCADToModel(e.data);
|
||||
self.postMessage(result);
|
||||
} catch (error: any) {
|
||||
console.log(error.toString());
|
||||
self.postMessage({objectURL: null});
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
@@ -12,79 +12,44 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '@vitest/web-worker';
|
||||
|
||||
import {WebIO} from '@gltf-transform/core';
|
||||
import {expect, suite, test} from 'vitest';
|
||||
import assert from 'node:assert';
|
||||
import {afterEach, expect, suite, test} from 'vitest';
|
||||
|
||||
import Module from './built/manifold.js';
|
||||
import {readMesh, setupIO} from './gltf-io.js';
|
||||
import {readMesh, setupIO} from './gltf-io';
|
||||
// @ts-ignore
|
||||
import {examples} from './public/examples.js';
|
||||
import ManifoldWorker from './worker?worker';
|
||||
import {Mesh} from './public/manifold';
|
||||
import {cleanup, evaluateCADToModel, module} from './worker';
|
||||
|
||||
const io = setupIO(new WebIO());
|
||||
|
||||
const wasm = await Module();
|
||||
wasm.setup();
|
||||
|
||||
function initialized(worker) {
|
||||
return new Promise((resolve) => {
|
||||
worker.onmessage = function(e) {
|
||||
if (e.data == null) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
};
|
||||
});
|
||||
async function runExample(name: string) {
|
||||
const code = examples.functionBodies.get(name);
|
||||
const result = await evaluateCADToModel(code);
|
||||
cleanup();
|
||||
assert.ok(result?.glbURL);
|
||||
const docIn = await io.read(result.glbURL);
|
||||
URL.revokeObjectURL(result.glbURL);
|
||||
const nodes = docIn.getRoot().listNodes();
|
||||
for (const node of nodes) {
|
||||
const docMesh = node.getMesh();
|
||||
if (!docMesh) {
|
||||
continue;
|
||||
}
|
||||
const {mesh} = readMesh(docMesh)!;
|
||||
const manifold = new module.Manifold(mesh as Mesh);
|
||||
const prop = manifold.getProperties();
|
||||
const genus = manifold.genus();
|
||||
manifold.delete();
|
||||
return {...prop, genus};
|
||||
}
|
||||
assert.ok(false);
|
||||
}
|
||||
|
||||
let glbURL = null;
|
||||
|
||||
async function runExample(name) {
|
||||
const worker = new ManifoldWorker();
|
||||
await initialized(worker);
|
||||
return new Promise((resolve, reject) => {
|
||||
worker.onmessageerror = function({data}) {
|
||||
reject(data);
|
||||
};
|
||||
|
||||
worker.onerror = function(e) {
|
||||
reject(e);
|
||||
};
|
||||
|
||||
worker.onmessage = async function(e) {
|
||||
try {
|
||||
URL.revokeObjectURL(glbURL);
|
||||
glbURL = e.data.glbURL;
|
||||
if (glbURL == null) {
|
||||
reject('no glbURL)');
|
||||
}
|
||||
const docIn = await io.read(glbURL);
|
||||
const nodes = docIn.getRoot().listNodes();
|
||||
for (const node of nodes) {
|
||||
const mesh = node.getMesh();
|
||||
if (!mesh) {
|
||||
continue;
|
||||
}
|
||||
const attributes = [];
|
||||
const materials = [];
|
||||
const manifoldMesh = readMesh(mesh, attributes, materials);
|
||||
const manifold = wasm.Manifold(manifoldMesh);
|
||||
const prop = manifold.getProperties();
|
||||
const genus = manifold.genus();
|
||||
console.log(genus);
|
||||
manifold.delete();
|
||||
resolve({...prop, genus});
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
worker.postMessage(examples.functionBodies.get(name));
|
||||
});
|
||||
}
|
||||
// allow vitest to report progress after each test
|
||||
// before going into heavy computation which blocks main thread
|
||||
afterEach(async () => {await new Promise(resolve => setTimeout(resolve, 500))})
|
||||
|
||||
suite('Examples', () => {
|
||||
test('Intro', async () => {
|
||||
@@ -97,29 +62,29 @@ 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 () => {
|
||||
const result = await runExample('Rounded Frame');
|
||||
expect(result.genus).to.equal(5, 'Genus');
|
||||
expect(result.volume).to.be.closeTo(353706, 10, 'Volume');
|
||||
expect(result.surfaceArea).to.be.closeTo(68454, 1, 'Surface Area');
|
||||
expect(result.volume).to.be.closeTo(270807, 10, 'Volume');
|
||||
expect(result.surfaceArea).to.be.closeTo(74599, 1, 'Surface Area');
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const result = await runExample('Scallop');
|
||||
expect(result.genus).to.equal(0, 'Genus');
|
||||
expect(result.volume).to.be.closeTo(41284, 1, 'Volume');
|
||||
expect(result.surfaceArea).to.be.closeTo(7810, 1, 'Surface Area');
|
||||
expect(result.volume).to.be.closeTo(39900, 100, 'Volume');
|
||||
expect(result.surfaceArea).to.be.closeTo(7930, 10, 'Surface Area');
|
||||
});
|
||||
|
||||
test('Torus Knot', async () => {
|
||||
@@ -12,27 +12,37 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Document, mat4, Material, Node, WebIO} from '@gltf-transform/core';
|
||||
import {Accessor, Animation, AnimationSampler, Document, mat4, Material, Mesh as GLTFMesh, Node, WebIO} from '@gltf-transform/core';
|
||||
import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
|
||||
import {fileForContentTypes, to3dmodel} from '@jscadui/3mf-export';
|
||||
import {fileForContentTypes, FileForRelThumbnail, to3dmodel} from '@jscadui/3mf-export';
|
||||
import {strToU8, Zippable, zipSync} from 'fflate'
|
||||
import * as glMatrix from 'gl-matrix';
|
||||
|
||||
import Module from './built/manifold';
|
||||
//@ts-ignore
|
||||
import {setupIO, writeMesh} from './gltf-io';
|
||||
import type {GLTFMaterial, Quat} from './public/editor';
|
||||
import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './public/manifold';
|
||||
import Module, {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './built/manifold';
|
||||
import {Properties, setupIO, writeMesh} from './gltf-io';
|
||||
import {GLTFMaterial, Quat} from './public/editor';
|
||||
|
||||
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;
|
||||
export const module = await Module() as WorkerStatic;
|
||||
module.setup();
|
||||
|
||||
// Faster on modern browsers than Float32Array
|
||||
@@ -48,9 +58,30 @@ const manifoldStaticFunctions = [
|
||||
];
|
||||
// manifold member functions (that return a new manifold)
|
||||
const manifoldMemberFunctions = [
|
||||
'add', 'subtract', 'intersect', 'decompose', 'warp', 'transform', 'translate',
|
||||
'rotate', 'scale', 'mirror', 'refine', 'setProperties', 'asOriginal',
|
||||
'trimByPlane', 'split', 'splitByPlane', 'hull'
|
||||
'add',
|
||||
'subtract',
|
||||
'intersect',
|
||||
'decompose',
|
||||
'warp',
|
||||
'transform',
|
||||
'translate',
|
||||
'rotate',
|
||||
'scale',
|
||||
'mirror',
|
||||
'calculateCurvature',
|
||||
'calculateNormals',
|
||||
'smoothByNormals',
|
||||
'smoothOut',
|
||||
'refine',
|
||||
'refineToLength',
|
||||
'setProperties',
|
||||
'asOriginal',
|
||||
'trimByPlane',
|
||||
'split',
|
||||
'splitByPlane',
|
||||
'slice',
|
||||
'project',
|
||||
'hull'
|
||||
];
|
||||
// CrossSection static methods (that return a new cross-section)
|
||||
const crossSectionStaticFunctions = [
|
||||
@@ -66,7 +97,8 @@ const crossSectionMemberFunctions = [
|
||||
const toplevelConstructors = ['show', 'only', 'setMaterial'];
|
||||
const toplevel = [
|
||||
'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments',
|
||||
'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection'
|
||||
'getCircularSegments', 'resetToCircularDefaults', 'Mesh', 'GLTFNode',
|
||||
'Manifold', 'CrossSection', 'setMorphStart', 'setMorphEnd'
|
||||
];
|
||||
const exposedFunctions = toplevelConstructors.concat(toplevel);
|
||||
|
||||
@@ -83,15 +115,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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,22 +141,23 @@ for (const name of toplevelConstructors) {
|
||||
};
|
||||
}
|
||||
|
||||
module.cleanup = function() {
|
||||
for (const obj of memoryRegistry) {
|
||||
// decompose result is an array of manifolds
|
||||
if (obj instanceof Array)
|
||||
for (const elem of obj) elem.delete();
|
||||
else
|
||||
obj.delete();
|
||||
}
|
||||
memoryRegistry.length = 0;
|
||||
};
|
||||
|
||||
// Debug setup to show source meshes
|
||||
let ghost = false;
|
||||
const shown = new Map<number, Mesh>();
|
||||
const singles = new Map<number, Mesh>();
|
||||
|
||||
const FPS = 30;
|
||||
|
||||
const GLOBAL_DEFAULTS = {
|
||||
roughness: 0.2,
|
||||
metallic: 1,
|
||||
baseColorFactor: [1, 1, 0] as [number, number, number],
|
||||
alpha: 1,
|
||||
unlit: false,
|
||||
animationLength: 1,
|
||||
animationMode: 'loop'
|
||||
};
|
||||
|
||||
const SHOW = {
|
||||
baseColorFactor: [1, 0, 0],
|
||||
alpha: 0.25,
|
||||
@@ -144,10 +175,26 @@ const GHOST = {
|
||||
const nodes = new Array<GLTFNode>();
|
||||
const id2material = new Map<number, GLTFMaterial>();
|
||||
const materialCache = new Map<GLTFMaterial, Material>();
|
||||
let nextGlobalID = 0;
|
||||
const object2globalID = new Map<GLTFNode|Manifold, number>();
|
||||
const manifold2morph = new Map<Manifold, Morph>();
|
||||
// lib3mf doesn't like objectid=0
|
||||
let nextGlobalID = 1;
|
||||
let animation: Animation;
|
||||
let timesAccessor: Accessor;
|
||||
let weightsAccessor: Accessor;
|
||||
let weightsSampler: AnimationSampler;
|
||||
let hasAnimation: boolean;
|
||||
|
||||
export function cleanup() {
|
||||
for (const obj of memoryRegistry) {
|
||||
// decompose result is an array of manifolds
|
||||
if (obj instanceof Array)
|
||||
for (const elem of obj) elem.delete();
|
||||
else
|
||||
obj.delete();
|
||||
}
|
||||
memoryRegistry.length = 0;
|
||||
|
||||
function cleanup() {
|
||||
ghost = false;
|
||||
shown.clear();
|
||||
singles.clear();
|
||||
@@ -155,7 +202,13 @@ function cleanup() {
|
||||
id2material.clear();
|
||||
materialCache.clear();
|
||||
object2globalID.clear();
|
||||
nextGlobalID = 0;
|
||||
manifold2morph.clear();
|
||||
nextGlobalID = 1;
|
||||
}
|
||||
|
||||
interface Morph {
|
||||
start?: (v: Vec3) => void;
|
||||
end?: (v: Vec3) => void;
|
||||
}
|
||||
|
||||
interface Mesh3MF {
|
||||
@@ -198,9 +251,9 @@ interface To3MF {
|
||||
class GLTFNode {
|
||||
private _parent?: GLTFNode;
|
||||
manifold?: Manifold;
|
||||
translation?: Vec3;
|
||||
rotation?: Vec3;
|
||||
scale?: Vec3;
|
||||
translation?: Vec3|((t: number) => Vec3);
|
||||
rotation?: Vec3|((t: number) => Vec3);
|
||||
scale?: Vec3|((t: number) => Vec3);
|
||||
material?: GLTFMaterial;
|
||||
name?: string;
|
||||
|
||||
@@ -221,12 +274,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());
|
||||
@@ -242,69 +315,153 @@ module.only = (manifold) => {
|
||||
return debug(manifold, singles);
|
||||
};
|
||||
|
||||
// Setup complete
|
||||
self.postMessage(null);
|
||||
|
||||
if (self.console) {
|
||||
const oldLog = self.console.log;
|
||||
self.console.log = function(...args) {
|
||||
let message = '';
|
||||
for (const arg of args) {
|
||||
if (arg == null) {
|
||||
message += 'undefined';
|
||||
} else if (typeof arg == 'object') {
|
||||
message += JSON.stringify(arg, null, 4);
|
||||
} else {
|
||||
message += arg.toString();
|
||||
}
|
||||
}
|
||||
self.postMessage({log: message});
|
||||
oldLog(...args);
|
||||
};
|
||||
}
|
||||
|
||||
// Swallow informational logs in testing framework
|
||||
function log(...args: any[]) {
|
||||
if (self.console) {
|
||||
if (typeof self !== 'undefined' && self.console) {
|
||||
self.console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
self.onmessage = async (e) => {
|
||||
const content = e.data +
|
||||
'\nreturn exportModels(typeof result === "undefined" ? undefined : result);\n';
|
||||
try {
|
||||
const f = new Function(
|
||||
'exportModels', 'glMatrix', 'module', ...exposedFunctions, content);
|
||||
await f(
|
||||
exportModels, glMatrix, module, //@ts-ignore
|
||||
...exposedFunctions.map(name => module[name]));
|
||||
} catch (error: any) {
|
||||
console.log(error.toString());
|
||||
self.postMessage({objectURL: null});
|
||||
} finally {
|
||||
module.cleanup();
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
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 createGLTFnode(doc: Document, node: GLTFNode) {
|
||||
function addMotion(
|
||||
doc: Document, type: 'translation'|'rotation'|'scale', node: GLTFNode,
|
||||
out: Node): Vec3|null {
|
||||
const motion = node[type];
|
||||
if (motion == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof motion !== 'function') {
|
||||
return motion;
|
||||
}
|
||||
|
||||
const nFrames = timesAccessor.getCount();
|
||||
const nEl = type == 'rotation' ? 4 : 3;
|
||||
const frames = new Float32Array(nEl * nFrames);
|
||||
for (let i = 0; i < nFrames; ++i) {
|
||||
const x = i / (nFrames - 1);
|
||||
const m = motion(
|
||||
globalDefaults.animationMode !== 'ping-pong' ?
|
||||
x :
|
||||
(1 - Math.cos(x * 2 * Math.PI)) / 2);
|
||||
frames.set(nEl === 4 ? euler2quat(m) : m, nEl * i);
|
||||
}
|
||||
|
||||
const framesAccessor =
|
||||
doc.createAccessor(node.name + ' ' + type + ' frames')
|
||||
.setBuffer(doc.getRoot().listBuffers()[0])
|
||||
.setArray(frames)
|
||||
.setType(nEl === 4 ? Accessor.Type.VEC4 : Accessor.Type.VEC3);
|
||||
const sampler = doc.createAnimationSampler()
|
||||
.setInput(timesAccessor)
|
||||
.setOutput(framesAccessor)
|
||||
.setInterpolation('LINEAR');
|
||||
const channel = doc.createAnimationChannel()
|
||||
.setTargetPath(type)
|
||||
.setTargetNode(out)
|
||||
.setSampler(sampler);
|
||||
animation.addSampler(sampler);
|
||||
animation.addChannel(channel);
|
||||
hasAnimation = true;
|
||||
return motion(0);
|
||||
}
|
||||
|
||||
function setMorph(doc: Document, node: Node, manifold: Manifold) {
|
||||
if (manifold2morph.has(manifold)) {
|
||||
const channel = doc.createAnimationChannel()
|
||||
.setTargetPath('weights')
|
||||
.setTargetNode(node)
|
||||
.setSampler(weightsSampler);
|
||||
animation.addChannel(channel);
|
||||
hasAnimation = true;
|
||||
}
|
||||
}
|
||||
|
||||
function morphStart(manifoldMesh: Mesh, morph?: Morph): number[] {
|
||||
const inputPositions: number[] = [];
|
||||
if (morph == null) {
|
||||
return inputPositions;
|
||||
}
|
||||
|
||||
for (let i = 0; i < manifoldMesh.numVert; ++i) {
|
||||
for (let j = 0; j < 3; ++j)
|
||||
inputPositions[i * 3 + j] =
|
||||
manifoldMesh.vertProperties[i * manifoldMesh.numProp + j];
|
||||
}
|
||||
if (morph.start) {
|
||||
for (let i = 0; i < manifoldMesh.numVert; ++i) {
|
||||
const vertProp = manifoldMesh.vertProperties;
|
||||
const offset = i * manifoldMesh.numProp;
|
||||
const pos = inputPositions.slice(offset, offset + 3) as Vec3;
|
||||
morph.start(pos);
|
||||
for (let j = 0; j < 3; ++j) vertProp[offset + j] = pos[j];
|
||||
}
|
||||
}
|
||||
return inputPositions;
|
||||
}
|
||||
|
||||
function morphEnd(
|
||||
doc: Document, manifoldMesh: Mesh, mesh: GLTFMesh, inputPositions: number[],
|
||||
morph?: Morph) {
|
||||
if (morph == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mesh.setWeights([0]);
|
||||
|
||||
mesh.listPrimitives().forEach((primitive, i) => {
|
||||
if (morph.end) {
|
||||
for (let i = 0; i < manifoldMesh.numVert; ++i) {
|
||||
const pos = inputPositions.slice(3 * i, 3 * (i + 1)) as Vec3;
|
||||
morph.end(pos);
|
||||
inputPositions.splice(3 * i, 3, ...pos);
|
||||
}
|
||||
}
|
||||
|
||||
const startPosition = primitive.getAttribute('POSITION')!.getArray()!;
|
||||
const array = new Float32Array(startPosition.length);
|
||||
|
||||
const offset = manifoldMesh.runIndex[i];
|
||||
for (let j = 0; j < array.length; ++j) {
|
||||
array[j] = inputPositions[offset + j] - startPosition[j];
|
||||
}
|
||||
|
||||
const morphAccessor = doc.createAccessor(mesh.getName() + ' morph target')
|
||||
.setBuffer(doc.getRoot().listBuffers()[0])
|
||||
.setArray(array)
|
||||
.setType(Accessor.Type.VEC3);
|
||||
const morphTarget =
|
||||
doc.createPrimitiveTarget().setAttribute('POSITION', morphAccessor);
|
||||
primitive.addTarget(morphTarget);
|
||||
});
|
||||
}
|
||||
|
||||
function createGLTFnode(doc: Document, node: GLTFNode): Node {
|
||||
const out = doc.createNode(node.name);
|
||||
if (node.translation) {
|
||||
out.setTranslation(node.translation);
|
||||
|
||||
const pos = addMotion(doc, 'translation', node, out);
|
||||
if (pos != null) {
|
||||
out.setTranslation(pos);
|
||||
}
|
||||
if (node.rotation) {
|
||||
const {quat} = glMatrix;
|
||||
const deg2rad = Math.PI / 180;
|
||||
const q = quat.create() as Quat;
|
||||
quat.rotateX(q, q, deg2rad * node.rotation[0]);
|
||||
quat.rotateY(q, q, deg2rad * node.rotation[1]);
|
||||
quat.rotateZ(q, q, deg2rad * node.rotation[2]);
|
||||
out.setRotation(q);
|
||||
|
||||
const rot = addMotion(doc, 'rotation', node, out);
|
||||
if (rot != null) {
|
||||
out.setRotation(euler2quat(rot));
|
||||
}
|
||||
if (node.scale) {
|
||||
out.setScale(node.scale);
|
||||
|
||||
const scale = addMotion(doc, 'scale', node, out);
|
||||
if (scale != null) {
|
||||
out.setScale(scale);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -318,15 +475,13 @@ function getBackupMaterial(node?: GLTFNode): GLTFMaterial {
|
||||
return node.material;
|
||||
}
|
||||
|
||||
function makeDefaultedMaterial(doc: Document, {
|
||||
roughness = 0.2,
|
||||
metallic = 1,
|
||||
baseColorFactor = [1, 1, 0],
|
||||
alpha = 1,
|
||||
unlit = false,
|
||||
name = ''
|
||||
}: GLTFMaterial = {}): Material {
|
||||
const material = doc.createMaterial(name);
|
||||
function makeDefaultedMaterial(
|
||||
doc: Document, matIn: GLTFMaterial = {}): Material {
|
||||
const defaults = {...globalDefaults};
|
||||
Object.assign(defaults, matIn);
|
||||
const {roughness, metallic, baseColorFactor, alpha, unlit} = defaults;
|
||||
|
||||
const material = doc.createMaterial(matIn.name ?? '');
|
||||
|
||||
if (unlit) {
|
||||
const unlit = doc.createExtension(KHRMaterialsUnlit).createUnlit();
|
||||
@@ -373,19 +528,23 @@ function addMesh(
|
||||
// From Z-up to Y-up (glTF)
|
||||
const manifoldMesh = manifold.getMesh();
|
||||
|
||||
const materials = new Array<GLTFMaterial>();
|
||||
const attributes = new Array<Array<string>>();
|
||||
const id2properties = new Map<number, Properties>();
|
||||
for (const id of manifoldMesh.runOriginalID!) {
|
||||
const material = id2material.get(id) || backupMaterial;
|
||||
materials.push(material);
|
||||
attributes.push(['POSITION', ...material.attributes ?? []]);
|
||||
id2properties.set(id, {
|
||||
material: getCachedMaterial(doc, ghost ? GHOST : material),
|
||||
attributes: ['POSITION', ...material.attributes ?? []]
|
||||
});
|
||||
}
|
||||
|
||||
const gltfMaterials = materials.map((matDef) => {
|
||||
return getCachedMaterial(doc, ghost ? GHOST : matDef);
|
||||
});
|
||||
const morph = manifold2morph.get(manifold);
|
||||
const inputPositions = morphStart(manifoldMesh, morph);
|
||||
|
||||
node.setMesh(writeMesh(doc, manifoldMesh, attributes, gltfMaterials));
|
||||
const mesh = writeMesh(doc, manifoldMesh, id2properties);
|
||||
mesh.setName(node.getName());
|
||||
node.setMesh(mesh);
|
||||
|
||||
morphEnd(doc, manifoldMesh, mesh, inputPositions, morph);
|
||||
|
||||
const vertices = manifoldMesh.numProp === 3 ?
|
||||
manifoldMesh.vertProperties :
|
||||
@@ -402,21 +561,18 @@ function addMesh(
|
||||
{vertices, indices: manifoldMesh.triVerts, id: `${nextGlobalID++}`});
|
||||
|
||||
for (const [run, id] of manifoldMesh.runOriginalID!.entries()) {
|
||||
let inMesh = shown.get(id);
|
||||
let single = false;
|
||||
if (inMesh == null) {
|
||||
single = true;
|
||||
inMesh = singles.get(id);
|
||||
}
|
||||
const show = shown.has(id);
|
||||
const inMesh = show ? shown.get(id) : singles.get(id);
|
||||
if (inMesh == null) {
|
||||
continue;
|
||||
}
|
||||
const mat = single ? materials[run] : SHOW;
|
||||
const debugNode =
|
||||
doc.createNode('debug')
|
||||
.setMesh(writeMesh(
|
||||
doc, inMesh, attributes, [getCachedMaterial(doc, mat)]))
|
||||
.setMatrix(manifoldMesh.transform(run));
|
||||
|
||||
id2properties.get(id)!.material = getCachedMaterial(
|
||||
doc, show ? SHOW : (id2material.get(id) || backupMaterial));
|
||||
|
||||
const debugNode = doc.createNode('debug')
|
||||
.setMesh(writeMesh(doc, inMesh, id2properties))
|
||||
.setMatrix(manifoldMesh.transform(run));
|
||||
node.addChild(debugNode);
|
||||
}
|
||||
}
|
||||
@@ -450,6 +606,7 @@ function createNodeFromCache(
|
||||
const node = createGLTFnode(doc, nodeDef);
|
||||
const {manifold} = nodeDef;
|
||||
if (manifold != null) {
|
||||
setMorph(doc, node, manifold);
|
||||
const backupMaterial = getBackupMaterial(nodeDef);
|
||||
const cachedNodes = manifold2node.get(manifold);
|
||||
if (cachedNodes == null) {
|
||||
@@ -481,7 +638,10 @@ function createNodeFromCache(
|
||||
return node;
|
||||
}
|
||||
|
||||
async function exportModels(manifold?: Manifold) {
|
||||
async function exportModels(defaults: GlobalDefaults, manifold?: Manifold) {
|
||||
Object.assign(globalDefaults, GLOBAL_DEFAULTS);
|
||||
Object.assign(globalDefaults, defaults);
|
||||
|
||||
const doc = new Document();
|
||||
const halfRoot2 = Math.sqrt(2) / 2;
|
||||
const mm2m = 1 / 1000;
|
||||
@@ -490,6 +650,32 @@ async function exportModels(manifold?: Manifold) {
|
||||
.setScale([mm2m, mm2m, mm2m]);
|
||||
doc.createScene().addChild(wrapper);
|
||||
|
||||
animation = doc.createAnimation('');
|
||||
hasAnimation = false;
|
||||
const nFrames = Math.round(globalDefaults.animationLength * FPS) + 1;
|
||||
const times = new Float32Array(nFrames);
|
||||
const weights = new Float32Array(nFrames);
|
||||
for (let i = 0; i < nFrames; ++i) {
|
||||
const x = i / (nFrames - 1);
|
||||
times[i] = x * globalDefaults.animationLength;
|
||||
weights[i] = globalDefaults.animationMode !== 'ping-pong' ?
|
||||
x :
|
||||
(1 - Math.cos(x * 2 * Math.PI)) / 2;
|
||||
}
|
||||
timesAccessor = doc.createAccessor('animation times')
|
||||
.setBuffer(doc.createBuffer())
|
||||
.setArray(times)
|
||||
.setType(Accessor.Type.SCALAR);
|
||||
weightsAccessor = doc.createAccessor('animation weights')
|
||||
.setBuffer(doc.getRoot().listBuffers()[0])
|
||||
.setArray(weights)
|
||||
.setType(Accessor.Type.SCALAR);
|
||||
weightsSampler = doc.createAnimationSampler()
|
||||
.setInput(timesAccessor)
|
||||
.setOutput(weightsAccessor)
|
||||
.setInterpolation('LINEAR');
|
||||
animation.addSampler(weightsSampler);
|
||||
|
||||
const to3mf = {
|
||||
meshes: [],
|
||||
components: [],
|
||||
@@ -547,20 +733,56 @@ async function exportModels(manifold?: Manifold) {
|
||||
to3mf.items.push({objectID: `${object2globalID.get(manifold)}`});
|
||||
}
|
||||
|
||||
if (!hasAnimation) {
|
||||
timesAccessor.dispose();
|
||||
weightsAccessor.dispose();
|
||||
weightsSampler.dispose();
|
||||
animation.dispose();
|
||||
}
|
||||
|
||||
const glb = await io.writeBinary(doc);
|
||||
const blobGLB = new Blob([glb], {type: 'application/octet-stream'});
|
||||
|
||||
const model = to3dmodel(to3mf);
|
||||
const fileForRelThumbnail = new FileForRelThumbnail();
|
||||
fileForRelThumbnail.add3dModel('3D/3dmodel.model')
|
||||
|
||||
const model = to3dmodel(to3mf as any);
|
||||
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({
|
||||
return ({
|
||||
glbURL: URL.createObjectURL(blobGLB),
|
||||
threeMFURL: URL.createObjectURL(blob3MF)
|
||||
});
|
||||
}
|
||||
|
||||
function evaluateCADToManifold(code: string) {
|
||||
const globalDefaults = {} as GlobalDefaults;
|
||||
const context = {
|
||||
globalDefaults,
|
||||
exportModels,
|
||||
glMatrix,
|
||||
module,
|
||||
...Object.fromEntries(
|
||||
exposedFunctions.map((name) => [name, (module as any)[name]]),
|
||||
),
|
||||
};
|
||||
const evalFn = new Function(
|
||||
...Object.keys(context),
|
||||
'resetToCircularDefaults();\n' + code +
|
||||
'\n return typeof result === "undefined" ? undefined : result;',
|
||||
);
|
||||
const manifold = evalFn(...Object.values(context));
|
||||
return {globalDefaults, manifold};
|
||||
}
|
||||
|
||||
export async function evaluateCADToModel(code: string) {
|
||||
const {globalDefaults, manifold} = evaluateCADToManifold(code);
|
||||
return await exportModels(globalDefaults, manifold);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -38,6 +38,7 @@ export function setMinCircularAngle(angle: number): void;
|
||||
export function setMinCircularEdgeLength(length: number): void;
|
||||
export function setCircularSegments(segments: number): void;
|
||||
export function getCircularSegments(radius: number): number;
|
||||
export function resetToCircularDefaults(): void;
|
||||
///@}
|
||||
|
||||
export class CrossSection {
|
||||
@@ -559,6 +560,37 @@ export class Manifold {
|
||||
*/
|
||||
warp(warpFunc: (vert: Vec3) => void): Manifold;
|
||||
|
||||
/**
|
||||
* Smooths out the Manifold by filling in the halfedgeTangent vectors. The
|
||||
* geometry will remain unchanged until Refine or RefineToLength is called to
|
||||
* interpolate the surface. This version uses the supplied vertex normal
|
||||
* properties to define the tangent vectors.
|
||||
*
|
||||
* @param normalIdx The first property channel of the normals. NumProp must be
|
||||
* at least normalIdx + 3. Any vertex where multiple normals exist and don't
|
||||
* agree will result in a sharp edge.
|
||||
*/
|
||||
smoothByNormals(normalIdx: number): Manifold;
|
||||
|
||||
/**
|
||||
* Smooths out the Manifold by filling in the halfedgeTangent vectors. The
|
||||
* geometry will remain unchanged until Refine or RefineToLength is called to
|
||||
* interpolate the surface. This version uses the geometry of the triangles
|
||||
* and pseudo-normals to define the tangent vectors.
|
||||
*
|
||||
* @param minSharpAngle degrees, default 60. Any edges with angles greater
|
||||
* than this value will remain sharp. The rest will be smoothed to G1
|
||||
* continuity, with the caveat that flat faces of three or more triangles will
|
||||
* always remain flat. With a value of zero, the model is faceted, but in this
|
||||
* case there is no point in smoothing.
|
||||
*
|
||||
* @param minSmoothness range: 0 - 1, default 0. The smoothness applied to
|
||||
* sharp angles. The default gives a hard edge, while values > 0 will give a
|
||||
* small fillet on these sharp edges. A value of 1 is equivalent to a
|
||||
* minSharpAngle of 180 - all edges will be smooth.
|
||||
*/
|
||||
smoothOut(minSharpAngle?: number, minSmoothness?: number): Manifold;
|
||||
|
||||
/**
|
||||
* Increase the density of the mesh by splitting every edge into n pieces. For
|
||||
* instance, with n = 2, each triangle will be split into 4 triangles. These
|
||||
@@ -571,6 +603,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
|
||||
@@ -606,6 +650,24 @@ export class Manifold {
|
||||
*/
|
||||
calculateCurvature(gaussianIdx: number, meanIdx: number): Manifold;
|
||||
|
||||
/**
|
||||
* Fills in vertex properties for normal vectors, calculated from the mesh
|
||||
* geometry. Flat faces composed of three or more triangles will remain flat.
|
||||
*
|
||||
* @param normalIdx The property channel in which to store the X
|
||||
* values of the normals. The X, Y, and Z channels will be sequential. The
|
||||
* property set will be automatically expanded to include up through normalIdx
|
||||
* + 2.
|
||||
*
|
||||
* @param minSharpAngle Any edges with angles greater than this value will
|
||||
* remain sharp, getting different normal vector properties on each side of
|
||||
* the edge. By default, no edges are sharp and all normals are shared. With a
|
||||
* value of zero, the model is faceted and all normals match their triangle
|
||||
* normals, but in this case it would be better not to calculate normals at
|
||||
* all.
|
||||
*/
|
||||
calculateNormals(normalIdx: number, minSharpAngle: number): Manifold;
|
||||
|
||||
// Boolean Operations
|
||||
|
||||
/**
|
||||
@@ -685,6 +747,22 @@ export class Manifold {
|
||||
*/
|
||||
trimByPlane(normal: Vec3, originOffset: number): Manifold;
|
||||
|
||||
/**
|
||||
* Returns the cross section of this object parallel to the X-Y plane at the
|
||||
* specified height. Using a height equal to the bottom
|
||||
* of the bounding box will return the bottom faces, while using a height
|
||||
* equal to the top of the bounding box will return empty.
|
||||
*
|
||||
* @param height Z-level of slice.
|
||||
*/
|
||||
slice(height: number): CrossSection;
|
||||
|
||||
/**
|
||||
* Returns a cross section representing the projected outline of this object
|
||||
* onto the X-Y plane.
|
||||
*/
|
||||
project(): CrossSection;
|
||||
|
||||
// Convex Hulls
|
||||
|
||||
/**
|
||||
@@ -781,6 +859,13 @@ export class Manifold {
|
||||
*/
|
||||
getProperties(): Properties;
|
||||
|
||||
|
||||
/*
|
||||
* Returns the minimum gap between two manifolds. Returns a float between
|
||||
* 0 and searchLength.
|
||||
*/
|
||||
minGap(other: Manifold, searchLength: number): number;
|
||||
|
||||
// Export
|
||||
|
||||
/**
|
||||
@@ -838,19 +923,7 @@ export class Manifold {
|
||||
delete(): void;
|
||||
}
|
||||
|
||||
export class Mesh {
|
||||
constructor(options: {
|
||||
numProp: number,
|
||||
vertProperties: Float32Array,
|
||||
triVerts: Uint32Array,
|
||||
mergeFromVert?: Uint32Array,
|
||||
mergeToVert?: Uint32Array,
|
||||
runIndex?: Uint32Array,
|
||||
runOriginalID?: Uint32Array,
|
||||
runTransform?: Float32Array,
|
||||
faceID?: Uint32Array,
|
||||
halfedgeTangent?: Float32Array
|
||||
});
|
||||
export interface MeshOptions {
|
||||
numProp: number;
|
||||
vertProperties: Float32Array;
|
||||
triVerts: Uint32Array;
|
||||
@@ -861,6 +934,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,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ManifoldToplevel {
|
||||
setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength;
|
||||
setCircularSegments: typeof T.setCircularSegments;
|
||||
getCircularSegments: typeof T.getCircularSegments;
|
||||
resetToCircularDefaults: typeof T.resetToCircularDefaults;
|
||||
setup: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "manifold-3d",
|
||||
"version": "2.1.0",
|
||||
"version": "2.5.1",
|
||||
"description": "Geometry library for topological robustness",
|
||||
"main": "manifold.js",
|
||||
"files": [
|
||||
|
||||
@@ -41,8 +41,9 @@ endif()
|
||||
if(BUILD_TEST_CGAL)
|
||||
add_executable(perfTestCGAL perf_test_cgal.cpp)
|
||||
find_package(CGAL REQUIRED COMPONENTS Core)
|
||||
find_package(Boost REQUIRED COMPONENTS thread)
|
||||
target_compile_definitions(perfTestCGAL PRIVATE CGAL_USE_GMPXX)
|
||||
target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core)
|
||||
target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core Boost::thread)
|
||||
target_compile_options(perfTestCGAL PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(perfTestCGAL PUBLIC cxx_std_17)
|
||||
endif()
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
{
|
||||
"nodes": {
|
||||
"clipper2-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1715688111,
|
||||
"narHash": "sha256-iy2QV8IVEEIPijWIpzIQtqIQeTU4bfiH41pC0X6m72U=",
|
||||
"owner": "AngusJohnson",
|
||||
"repo": "Clipper2",
|
||||
"rev": "ff378668baae3570e9d8070aa9eb339bdd5a6aba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "AngusJohnson",
|
||||
"repo": "Clipper2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -37,11 +53,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1692734709,
|
||||
"narHash": "sha256-SCFnyHCyYjwEmgUsHDDuU0TsbVMKeU1vwkR+r7uS2Rg=",
|
||||
"lastModified": 1704722960,
|
||||
"narHash": "sha256-mKGJ3sPsT6//s+Knglai5YflJUF2DGj7Ai6Ynopz0kI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b85ed9dcbf187b909ef7964774f8847d554fab3b",
|
||||
"rev": "317484b1ead87b9c1b8ac5261a8d2dd748a0492d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -52,9 +68,11 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"clipper2-src": "clipper2-src",
|
||||
"flake-utils": "flake-utils",
|
||||
"gtest-src": "gtest-src",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"thrust-src": "thrust-src"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
@@ -71,6 +89,24 @@
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"thrust-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696873248,
|
||||
"narHash": "sha256-mUhMXGPbO2t83EvI8YNDRLngvLiwfzs4EgRrcxKfhHs=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "756c5afc0750f1413da05bd2b6505180e84c53d4",
|
||||
"revCount": 4758,
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/NVIDIA/thrust.git"
|
||||
},
|
||||
"original": {
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/NVIDIA/thrust.git"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -5,7 +5,15 @@
|
||||
url = "github:google/googletest/v1.14.0";
|
||||
flake = false;
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, gtest-src }:
|
||||
inputs.thrust-src = {
|
||||
url = "git+https://github.com/NVIDIA/thrust.git?submodules=1";
|
||||
flake = false;
|
||||
};
|
||||
inputs.clipper2-src = {
|
||||
url = "github:AngusJohnson/Clipper2";
|
||||
flake = false;
|
||||
};
|
||||
outputs = { self, nixpkgs, flake-utils, gtest-src, thrust-src, clipper2-src }:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let
|
||||
@@ -13,36 +21,9 @@
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
thrust-210 = pkgs.nvidia-thrust.overrideAttrs (_: _: {
|
||||
version = "24486a1";
|
||||
src = builtins.fetchGit {
|
||||
url = "https://github.com/NVIDIA/thrust.git";
|
||||
ref = "main";
|
||||
rev = "24486a169a62a58ef8f824d3dc9613c006b6f5a7";
|
||||
submodules = true;
|
||||
};
|
||||
cmakeFlags = [ "-DTHRUST_ENABLE_HEADER_TESTING=OFF" "-DTHRUST_ENABLE_TESTING=OFF" "-DTHRUST_ENABLE_EXAMPLES=OFF" "-DTHRUST_DEVICE_SYSTEM=CPP" ];
|
||||
fixupPhase = ''
|
||||
cat <<EOT > $out/lib/cmake/thrust/thrust-header-search.cmake
|
||||
# Parse version information from version.h in source tree
|
||||
set(_THRUST_VERSION_INCLUDE_DIR "$out/include")
|
||||
if(EXISTS "\''${_THRUST_VERSION_INCLUDE_DIR}/thrust/version.h")
|
||||
set(_THRUST_VERSION_INCLUDE_DIR "\''${_THRUST_VERSION_INCLUDE_DIR}" CACHE FILEPATH "" FORCE) # Clear old result
|
||||
set_property(CACHE _THRUST_VERSION_INCLUDE_DIR PROPERTY TYPE INTERNAL)
|
||||
endif()
|
||||
EOT
|
||||
cat <<EOT > $out/lib/cmake/libcudacxx/libcudacxx-header-search.cmake
|
||||
# Parse version information from version header:
|
||||
unset(_libcudacxx_VERSION_INCLUDE_DIR CACHE) # Clear old result to force search
|
||||
find_path(_libcudacxx_VERSION_INCLUDE_DIR cuda/std/detail/__config
|
||||
NO_DEFAULT_PATH # Only search explicit paths below:
|
||||
PATHS
|
||||
"\''${CMAKE_CURRENT_LIST_DIR}/../../../include" # Source tree
|
||||
)
|
||||
set_property(CACHE _libcudacxx_VERSION_INCLUDE_DIR PROPERTY TYPE INTERNAL)
|
||||
EOT
|
||||
'';
|
||||
enableParallelBuilding = false;
|
||||
clipper2 = pkgs.clipper2.overrideAttrs (_: _: {
|
||||
version = "14052024";
|
||||
src = clipper2-src;
|
||||
});
|
||||
manifold =
|
||||
{ parallel-backend ? "none"
|
||||
@@ -52,28 +33,38 @@
|
||||
}: pkgs.stdenv.mkDerivation {
|
||||
inherit doCheck;
|
||||
pname = "manifold-${parallel-backend}";
|
||||
version = "2.2.0";
|
||||
version = "2.5.1";
|
||||
src = self;
|
||||
nativeBuildInputs = (with pkgs; [
|
||||
cmake
|
||||
thrust-210
|
||||
glm
|
||||
ninja
|
||||
(python39.withPackages
|
||||
(ps: with ps; [ trimesh ]))
|
||||
(python3.withPackages
|
||||
(ps: with ps; [ trimesh pytest ]))
|
||||
gtest
|
||||
pkg-config
|
||||
]) ++ build-tools;
|
||||
buildInputs = with pkgs; [
|
||||
glm
|
||||
clipper2
|
||||
assimp
|
||||
];
|
||||
cmakeFlags = [
|
||||
"-DMANIFOLD_PYBIND=ON"
|
||||
"-DMANIFOLD_CBIND=ON"
|
||||
"-DBUILD_SHARED_LIBS=ON"
|
||||
"-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src}"
|
||||
"-DMANIFOLD_PAR=${pkgs.lib.strings.toUpper parallel-backend}"
|
||||
];
|
||||
prePatch = ''
|
||||
substituteInPlace bindings/python/CMakeLists.txt \
|
||||
--replace 'DESTINATION ''${Python_SITEARCH}' 'DESTINATION "${placeholder "out"}/${pkgs.python3.sitePackages}"'
|
||||
'';
|
||||
checkPhase = ''
|
||||
cd test
|
||||
./manifold_test
|
||||
cd ../../
|
||||
PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python python3 bindings/python/examples/run_all.py
|
||||
PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python python3 -m pytest
|
||||
cd build
|
||||
'';
|
||||
};
|
||||
@@ -84,14 +75,6 @@
|
||||
build-tools = with pkgs; [ tbb_2021_8 pkg-config ];
|
||||
}
|
||||
];
|
||||
devShell = { additional ? [ ] }: pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cmake
|
||||
tbb_2021_8
|
||||
thrust-210
|
||||
gtest
|
||||
] ++ additional;
|
||||
};
|
||||
in
|
||||
{
|
||||
packages = (builtins.listToAttrs
|
||||
@@ -103,11 +86,13 @@
|
||||
parallelBackends)) // {
|
||||
manifold-js = pkgs.buildEmscriptenPackage {
|
||||
name = "manifold-js";
|
||||
version = "beta";
|
||||
version = "2.5.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
|
||||
@@ -115,7 +100,8 @@
|
||||
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
|
||||
-DFETCHCONTENT_SOURCE_DIR_GLM=${pkgs.glm.src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=${gtest-src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-210.src} ..
|
||||
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src} \
|
||||
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=../clipper2 ..
|
||||
'';
|
||||
buildPhase = ''
|
||||
emmake make -j''${NIX_BUILD_CORES}
|
||||
@@ -134,15 +120,15 @@
|
||||
# but how should we make it work with other python versions?
|
||||
manifold3d = with pkgs.python3Packages; buildPythonPackage {
|
||||
pname = "manifold3d";
|
||||
version = "2.2.0";
|
||||
version = "2.5.1";
|
||||
src = self;
|
||||
propagatedBuildInputs = [
|
||||
numpy
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
tbb_2021_8
|
||||
thrust-210
|
||||
glm
|
||||
clipper2
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
@@ -153,18 +139,27 @@
|
||||
pathspec
|
||||
pkg-config
|
||||
];
|
||||
SKBUILD_CMAKE_DEFINE = "FETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src}";
|
||||
checkInputs = [
|
||||
trimesh
|
||||
pytest
|
||||
];
|
||||
format = "pyproject";
|
||||
dontUseCmakeConfigure = true;
|
||||
doCheck = true;
|
||||
checkPhase = ''
|
||||
python3 bindings/python/examples/run_all.py
|
||||
python3 -m pytest
|
||||
'';
|
||||
};
|
||||
};
|
||||
devShell = devShell { };
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cmake
|
||||
tbb_2021_8
|
||||
gtest
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
shopt -s extglob
|
||||
if [ -z "$CLANG_FORMAT" ]; then
|
||||
CLANG_FORMAT=$(dirname $(which clang-11))/clang-format
|
||||
fi
|
||||
|
||||
$CLANG_FORMAT -i extras/*.cpp
|
||||
$CLANG_FORMAT -i meshIO/**/*.{h,cpp}
|
||||
$CLANG_FORMAT -i samples/**/*.{h,cpp}
|
||||
$CLANG_FORMAT -i test/*.{h,cpp}
|
||||
$CLANG_FORMAT -i bindings/*/*.cpp
|
||||
$CLANG_FORMAT -i bindings/c/include/*.h
|
||||
$CLANG_FORMAT -i bindings/wasm/**/*.{js,ts,html}
|
||||
$CLANG_FORMAT -i src/!(third_party)/*/*.{h,cpp}
|
||||
black bindings/python/examples/*.py
|
||||
@@ -0,0 +1,4 @@
|
||||
set(MANIFOLD_PAR "@MANIFOLD_PAR@")
|
||||
set(MANIFOLD_EXPORT "@MANIFOLD_EXPORT@")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/manifoldDeps.cmake")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/manifoldTargets.cmake")
|
||||
@@ -0,0 +1,79 @@
|
||||
include(FetchContent)
|
||||
include(GNUInstallDirs)
|
||||
find_package(PkgConfig QUIET)
|
||||
find_package(Clipper2)
|
||||
if(MANIFOLD_PAR STREQUAL "TBB")
|
||||
find_package(TBB QUIET)
|
||||
endif()
|
||||
if (PKG_CONFIG_FOUND)
|
||||
if (NOT Clipper2_FOUND)
|
||||
pkg_check_modules(Clipper2 Clipper2)
|
||||
endif()
|
||||
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})
|
||||
if(WIN32)
|
||||
set_property(TARGET Clipper2 PROPERTY
|
||||
IMPORTED_IMPLIB ${Clipper2_LINK_LIBRARIES})
|
||||
endif()
|
||||
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 ff378668baae3570e9d8070aa9eb339bdd5a6aba
|
||||
GIT_PROGRESS TRUE
|
||||
SOURCE_SUBDIR CPP
|
||||
)
|
||||
FetchContent_MakeAvailable(Clipper2)
|
||||
if(NOT EMSCRIPTEN)
|
||||
install(TARGETS Clipper2)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(glm QUIET)
|
||||
if(NOT glm_FOUND)
|
||||
message(STATUS "glm not found, downloading from source")
|
||||
set(GLM_BUILD_INSTALL "ON" CACHE STRING "")
|
||||
FetchContent_Declare(glm
|
||||
GIT_REPOSITORY https://github.com/g-truc/glm.git
|
||||
GIT_TAG 1.0.1
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(glm)
|
||||
if(NOT EMSCRIPTEN)
|
||||
install(TARGETS glm)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MANIFOLD_PAR STREQUAL "TBB" AND NOT TBB_FOUND)
|
||||
message(STATUS "tbb not found, downloading from source")
|
||||
include(FetchContent)
|
||||
set(TBB_TEST OFF CACHE INTERNAL "" FORCE)
|
||||
set(TBB_STRICT OFF CACHE INTERNAL "" FORCE)
|
||||
FetchContent_Declare(TBB
|
||||
GIT_REPOSITORY https://github.com/oneapi-src/oneTBB.git
|
||||
GIT_TAG v2021.11.0
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(TBB)
|
||||
set_property(DIRECTORY ${tbb_SOURCE_DIR} PROPERTY EXCLUDE_FROM_ALL YES)
|
||||
# note: we do want to install tbb to the user machine when built from
|
||||
# source
|
||||
if(NOT EMSCRIPTEN)
|
||||
install(TARGETS tbb)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MANIFOLD_EXPORT)
|
||||
find_package(assimp REQUIRED)
|
||||
endif()
|
||||
@@ -14,13 +14,11 @@
|
||||
|
||||
project(meshIO)
|
||||
|
||||
find_package(assimp REQUIRED)
|
||||
|
||||
add_library(${PROJECT_NAME} src/meshIO.cpp)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC manifold
|
||||
@@ -30,13 +28,5 @@ target_link_libraries(${PROJECT_NAME}
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT devel
|
||||
)
|
||||
install(
|
||||
DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/meshIO.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -234,6 +234,7 @@ void ExportMesh(const std::string& filename, const MeshGL& mesh,
|
||||
? 1
|
||||
: mesh.vertProperties[i * mesh.numProp +
|
||||
options.mat.colorChannels[j]];
|
||||
c = glm::saturate(c);
|
||||
mesh_out->mColors[0][i] = aiColor4D(c.r, c.g, c.b, c.a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Modified from https://github.com/google/fuzztest/blob/main/tools/minimizer.sh
|
||||
# If you want to use a specific reproducer, pass it in FUZZTEST_MINIMIZE_REPRODUCER
|
||||
# environment variable.
|
||||
# Otherwise, just run it as
|
||||
# $ ./minimizer.sh ./build/test/polygon_fuzz --fuzz=PolygonFuzz.TriangulationNoCrash
|
||||
# If you want to kill it, try SIGKILL, SIGTERM is not very useful
|
||||
# If you want to end the loop early, rename $(pwd)/reproducers into something
|
||||
# else...
|
||||
|
||||
if [ -z "${FUZZTEST_MINIMIZE_REPRODUCER}"]; then
|
||||
mkdir -p $(pwd)/reproducers
|
||||
FUZZTEST_REPRODUCERS_OUT_DIR=$(pwd)/reproducers "$@"
|
||||
wildcard="$(pwd)/reproducers/*"
|
||||
reproducers=($wildcard)
|
||||
FUZZTEST_MINIMIZE_REPRODUCER="${reproducers[0]}"
|
||||
fi
|
||||
readonly ORIGINAL_REPRODUCER="${FUZZTEST_MINIMIZE_REPRODUCER}"
|
||||
|
||||
for i in {0001..9999}; do
|
||||
echo
|
||||
echo "╔════════════════════════════════════════════════╗"
|
||||
echo "║ Minimization round: ${i} ║"
|
||||
echo "╚════════════════════════════════════════════════╝"
|
||||
echo "Note that to terminate early, simply move $ORIGINAL_REPRODUCER to somewhere else..."
|
||||
echo
|
||||
|
||||
if [ ! -f $ORIGINAL_REPRODUCER ]; then
|
||||
echo "Terminated by the user"
|
||||
break
|
||||
fi
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
FUZZTEST_REPRODUCERS_OUT_DIR="${TEMP_DIR}" \
|
||||
FUZZTEST_MINIMIZE_REPRODUCER="${FUZZTEST_MINIMIZE_REPRODUCER}" \
|
||||
"$@"
|
||||
|
||||
if [ $? -eq 130 ]; then
|
||||
echo
|
||||
echo "╔═══════════════════════════════════════════════╗"
|
||||
echo "║ Minimization terminated. ║"
|
||||
echo "╚═══════════════════════════════════════════════╝"
|
||||
echo
|
||||
echo "Find the smallest reproducer at:"
|
||||
echo
|
||||
echo "${FUZZTEST_MINIMIZE_REPRODUCER}"
|
||||
|
||||
rm -rf "${TEMP_DIR}"
|
||||
break
|
||||
fi
|
||||
|
||||
SMALLER_REPRODUCER=$(find "${TEMP_DIR}" -type f)
|
||||
NEW_NAME="${ORIGINAL_REPRODUCER}-min-${i}"
|
||||
mv "${SMALLER_REPRODUCER}" "${NEW_NAME}"
|
||||
FUZZTEST_MINIMIZE_REPRODUCER="${NEW_NAME}"
|
||||
|
||||
rm -rf "${TEMP_DIR}"
|
||||
done
|
||||
@@ -1,13 +1,13 @@
|
||||
[project]
|
||||
name = "manifold3d"
|
||||
version = "2.2.0"
|
||||
version = "2.5.1"
|
||||
authors = [
|
||||
{ name="Emmett Lalish", email="elalish@gmail.com" },
|
||||
]
|
||||
description = "Library for geometric robustness"
|
||||
readme = "README.md"
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: C++",
|
||||
@@ -42,20 +42,14 @@ sdist.exclude = [
|
||||
"oneTBB", # we may have this when we build with cibuildwheel
|
||||
"bindings/python/third_party/nanobind/tests/",
|
||||
"bindings/python/third_party/nanobind/docs/",
|
||||
"src/third_party/clipper2/CPP/Examples/",
|
||||
"src/third_party/clipper2/CPP/Tests/",
|
||||
"src/third_party/clipper2/CSharp/",
|
||||
"src/third_party/clipper2/Delphi/",
|
||||
"src/third_party/clipper2/DLL/",
|
||||
"src/third_party/clipper2/Tests/",
|
||||
]
|
||||
wheel.packages = ["manifold3d"]
|
||||
cmake.args = ["-DMANIFOLD_PAR=TBB", "-DMANIFOLD_TEST=OFF"]
|
||||
cmake.args = ["-DMANIFOLD_PYBIND=ON", "-DMANIFOLD_PAR=TBB", "-DMANIFOLD_TEST=OFF", "-DBUILD_SHARED_LIBS=OFF"]
|
||||
install.components = ["bindings"]
|
||||
|
||||
[tool.cibuildwheel]
|
||||
build-frontend = "build"
|
||||
test-requires = ["trimesh"]
|
||||
test-requires = ["trimesh", "pytest"]
|
||||
test-command = "python {project}/bindings/python/examples/run_all.py"
|
||||
# Setuptools bug causes collision between pypy and cpython artifacts
|
||||
manylinux-x86_64-image = "manylinux_2_28"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
testpaths =
|
||||
bindings/python/examples
|
||||
|
Before Width: | Height: | Size: 106 KiB |
@@ -15,18 +15,15 @@
|
||||
project (collider)
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} ${SOURCE_FILES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_link_libraries(${PROJECT_NAME} PUBLIC utilities)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
|
||||
target_compile_features(${PROJECT_NAME}
|
||||
PUBLIC cxx_std_17
|
||||
)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT runtime
|
||||
)
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/collider.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -30,6 +30,7 @@ class Collider {
|
||||
template <const bool selfCollision = false, const bool inverted = false,
|
||||
typename T>
|
||||
SparseIndices Collisions(const VecView<const T>& queriesIn) const;
|
||||
static uint32_t MortonCode(glm::vec3 position, Box bBox);
|
||||
|
||||
private:
|
||||
Vec<Box> nodeBBox_;
|
||||
@@ -38,7 +39,9 @@ class Collider {
|
||||
Vec<thrust::pair<int, int>> internalChildren_;
|
||||
|
||||
int NumInternal() const { return internalChildren_.size(); };
|
||||
int NumLeaves() const { return NumInternal() + 1; };
|
||||
int NumLeaves() const {
|
||||
return internalChildren_.empty() ? 0 : (NumInternal() + 1);
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace manifold
|
||||
|
||||
@@ -265,6 +265,14 @@ struct TransformBox {
|
||||
const glm::mat4x3 transform;
|
||||
void operator()(Box& box) { box = box.Transform(transform); }
|
||||
};
|
||||
|
||||
uint32_t SpreadBits3(uint32_t v) {
|
||||
v = 0xFF0000FFu & (v * 0x00010001u);
|
||||
v = 0x0F00F00Fu & (v * 0x00000101u);
|
||||
v = 0xC30C30C3u & (v * 0x00000011u);
|
||||
v = 0x49249249u & (v * 0x00000005u);
|
||||
return v;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace manifold {
|
||||
@@ -276,6 +284,7 @@ namespace manifold {
|
||||
*/
|
||||
Collider::Collider(const VecView<const Box>& leafBB,
|
||||
const VecView<const uint32_t>& leafMorton) {
|
||||
ZoneScoped;
|
||||
ASSERT(leafBB.size() == leafMorton.size(), userErr,
|
||||
"vectors must be the same length");
|
||||
int num_nodes = 2 * leafBB.size() - 1;
|
||||
@@ -299,6 +308,7 @@ Collider::Collider(const VecView<const Box>& leafBB,
|
||||
*/
|
||||
template <const bool selfCollision, const bool inverted, typename T>
|
||||
SparseIndices Collider::Collisions(const VecView<const T>& queriesIn) const {
|
||||
ZoneScoped;
|
||||
// note that the length is 1 larger than the number of queries so the last
|
||||
// element can store the sum when using exclusive scan
|
||||
if (queriesIn.size() < kSequentialThreshold) {
|
||||
@@ -336,6 +346,7 @@ SparseIndices Collider::Collisions(const VecView<const T>& queriesIn) const {
|
||||
* hierarchy.
|
||||
*/
|
||||
void Collider::UpdateBoxes(const VecView<const Box>& leafBB) {
|
||||
ZoneScoped;
|
||||
ASSERT(leafBB.size() == NumLeaves(), userErr,
|
||||
"must have the same number of updated boxes as original");
|
||||
// copy in leaf node Boxes
|
||||
@@ -355,6 +366,7 @@ void Collider::UpdateBoxes(const VecView<const Box>& leafBB) {
|
||||
* axis-aligned, abort and return false to indicate recalculation is necessary.
|
||||
*/
|
||||
bool Collider::Transform(glm::mat4x3 transform) {
|
||||
ZoneScoped;
|
||||
bool axisAligned = true;
|
||||
for (int row : {0, 1, 2}) {
|
||||
int count = 0;
|
||||
@@ -370,6 +382,18 @@ bool Collider::Transform(glm::mat4x3 transform) {
|
||||
return axisAligned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the 32-bit Morton code of a position within a bounding box.
|
||||
*/
|
||||
uint32_t Collider::MortonCode(glm::vec3 position, Box bBox) {
|
||||
glm::vec3 xyz = (position - bBox.min) / (bBox.max - bBox.min);
|
||||
xyz = glm::min(glm::vec3(1023.0f), glm::max(glm::vec3(0.0f), 1024.0f * xyz));
|
||||
uint32_t x = SpreadBits3(static_cast<uint32_t>(xyz.x));
|
||||
uint32_t y = SpreadBits3(static_cast<uint32_t>(xyz.y));
|
||||
uint32_t z = SpreadBits3(static_cast<uint32_t>(xyz.z));
|
||||
return x * 4 + y * 2 + z;
|
||||
}
|
||||
|
||||
template SparseIndices Collider::Collisions<true, false, Box>(
|
||||
const VecView<const Box>&) const;
|
||||
|
||||
@@ -387,5 +411,4 @@ template SparseIndices Collider::Collisions<false, true, Box>(
|
||||
|
||||
template SparseIndices Collider::Collisions<false, true, glm::vec3>(
|
||||
const VecView<const glm::vec3>&) const;
|
||||
|
||||
} // namespace manifold
|
||||
|
||||
@@ -13,28 +13,24 @@
|
||||
# limitations under the License.
|
||||
|
||||
project (cross_section)
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} ${SOURCE_FILES})
|
||||
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC ${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
target_link_libraries( ${PROJECT_NAME}
|
||||
PUBLIC utilities Clipper2
|
||||
)
|
||||
if(TARGET Clipper2::Clipper2)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC utilities
|
||||
PRIVATE Clipper2::Clipper2)
|
||||
else()
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC utilities
|
||||
PRIVATE Clipper2)
|
||||
endif()
|
||||
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/cross_section.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "glm/ext/matrix_float3x2.hpp"
|
||||
#include "glm/ext/vector_float2.hpp"
|
||||
#include "public.h"
|
||||
#include "vec_view.h"
|
||||
|
||||
namespace manifold {
|
||||
|
||||
@@ -97,6 +98,8 @@ class CrossSection {
|
||||
CrossSection Mirror(const glm::vec2 ax) const;
|
||||
CrossSection Transform(const glm::mat3x2& m) const;
|
||||
CrossSection Warp(std::function<void(glm::vec2&)> warpFunc) const;
|
||||
CrossSection WarpBatch(
|
||||
std::function<void(VecView<glm::vec2>)> warpFunc) const;
|
||||
CrossSection Simplify(double epsilon = 1e-6) const;
|
||||
|
||||
// Adapted from Clipper2 docs:
|
||||
|
||||
@@ -146,55 +146,63 @@ void decompose_hole(const C2::PolyTreeD* outline,
|
||||
}
|
||||
}
|
||||
|
||||
void flatten(const C2::PolyTreeD* tree, C2::PathsD& polys, int i) {
|
||||
auto n_outlines = tree->Count();
|
||||
if (i < n_outlines) {
|
||||
auto outline = tree->Child(i);
|
||||
flatten(outline, polys, 0);
|
||||
polys.push_back(outline->Polygon());
|
||||
if (i < n_outlines - 1) {
|
||||
flatten(tree, polys, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool V2Lesser(glm::vec2 a, glm::vec2 b) {
|
||||
if (a.x == b.x) return a.y < b.y;
|
||||
return a.x < b.x;
|
||||
}
|
||||
|
||||
double Cross(glm::vec2 a, glm::vec2 b) { return a.x * b.y - a.y * b.x; }
|
||||
|
||||
double SquaredDistance(glm::vec2 a, glm::vec2 b) {
|
||||
auto d = a - b;
|
||||
return glm::dot(d, d);
|
||||
}
|
||||
|
||||
bool IsCw(glm::vec2 a, glm::vec2 b, glm::vec2 c) {
|
||||
double lhs = Cross(a - c, b - c);
|
||||
double rhs = 1e-18 * SquaredDistance(a, c) * SquaredDistance(b, c);
|
||||
return lhs * std::abs(lhs) <= rhs;
|
||||
}
|
||||
|
||||
void HullBacktrack(const SimplePolygon& pts, const int idx,
|
||||
std::vector<int>& keep, const int hold) {
|
||||
const int stop = keep.size() - hold;
|
||||
int i = 0;
|
||||
while (i < stop && !IsCw(pts[idx], pts[keep[keep.size() - 1]],
|
||||
pts[keep[keep.size() - 2]])) {
|
||||
keep.pop_back();
|
||||
i++;
|
||||
void HullBacktrack(const glm::vec2& pt, std::vector<glm::vec2>& stack) {
|
||||
auto sz = stack.size();
|
||||
while (sz >= 2 && CCW(stack[sz - 2], stack[sz - 1], pt, 0.0f) <= 0.0f) {
|
||||
stack.pop_back();
|
||||
sz = stack.size();
|
||||
}
|
||||
}
|
||||
|
||||
// Based on method described here:
|
||||
// https://www.hackerearth.com/practice/math/geometry/line-sweep-technique/tutorial/
|
||||
// Changed to follow:
|
||||
// https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
|
||||
// This is the same algorithm (Andrew, also called Montone Chain).
|
||||
C2::PathD HullImpl(SimplePolygon& pts) {
|
||||
int len = pts.size();
|
||||
if (len < 3) return C2::PathD(); // not enough points to create a polygon
|
||||
std::sort(pts.begin(), pts.end(), V2Lesser);
|
||||
auto keep = std::vector<int>{0, 1};
|
||||
for (int i = 2; i < len; i++) {
|
||||
HullBacktrack(pts, i, keep, 1);
|
||||
keep.push_back(i);
|
||||
|
||||
auto lower = std::vector<glm::vec2>{};
|
||||
for (int i = 0; i < len; i++) {
|
||||
HullBacktrack(pts[i], lower);
|
||||
lower.push_back(pts[i]);
|
||||
}
|
||||
int nLower = keep.size();
|
||||
for (int i = 0; i < len - 1; i++) {
|
||||
int idx = len - 2 - i;
|
||||
HullBacktrack(pts, idx, keep, nLower);
|
||||
if (idx > 0) keep.push_back(idx);
|
||||
auto upper = std::vector<glm::vec2>{};
|
||||
for (int i = len - 1; i >= 0; i--) {
|
||||
HullBacktrack(pts[i], upper);
|
||||
upper.push_back(pts[i]);
|
||||
}
|
||||
auto path = C2::PathD(keep.size());
|
||||
for (int i = 0; i < keep.size(); i++) {
|
||||
path[i] = v2_to_pd(pts[keep[i]]);
|
||||
|
||||
upper.pop_back();
|
||||
lower.pop_back();
|
||||
|
||||
auto path = C2::PathD(lower.size() + upper.size());
|
||||
for (int i = 0; i < lower.size(); i++) {
|
||||
path[i] = v2_to_pd(lower[i]);
|
||||
}
|
||||
auto llen = lower.size();
|
||||
int sz = upper.size(); // "fix" -Waggressive-loop-optimizations warning.
|
||||
for (int i = 0; i < sz; i++) {
|
||||
path[i + llen] = v2_to_pd(upper[i]);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
@@ -555,21 +563,42 @@ CrossSection CrossSection::Transform(const glm::mat3x2& m) const {
|
||||
*/
|
||||
CrossSection CrossSection::Warp(
|
||||
std::function<void(glm::vec2&)> warpFunc) const {
|
||||
auto paths = GetPaths();
|
||||
auto warped = C2::PathsD();
|
||||
warped.reserve(paths->paths_.size());
|
||||
for (auto path : paths->paths_) {
|
||||
auto sz = path.size();
|
||||
auto s = C2::PathD(sz);
|
||||
for (int i = 0; i < sz; ++i) {
|
||||
auto v = v2_of_pd(path[i]);
|
||||
warpFunc(v);
|
||||
s[i] = v2_to_pd(v);
|
||||
return WarpBatch([&warpFunc](VecView<glm::vec2> vecs) {
|
||||
for (glm::vec2& p : vecs) {
|
||||
warpFunc(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as CrossSection::Warp but calls warpFunc with
|
||||
* a VecView which is roughly equivalent to std::span
|
||||
* pointing to all vec2 elements to be modified in-place
|
||||
*
|
||||
* @param warpFunc A function that modifies multiple vertex positions.
|
||||
*/
|
||||
CrossSection CrossSection::WarpBatch(
|
||||
std::function<void(VecView<glm::vec2>)> warpFunc) const {
|
||||
std::vector<glm::vec2> tmp_verts;
|
||||
C2::PathsD paths = GetPaths()->paths_; // deep copy
|
||||
for (C2::PathD const& path : paths) {
|
||||
for (C2::PointD const& p : path) {
|
||||
tmp_verts.push_back(v2_of_pd(p));
|
||||
}
|
||||
warped.push_back(s);
|
||||
}
|
||||
|
||||
warpFunc(VecView<glm::vec2>(tmp_verts.data(), tmp_verts.size()));
|
||||
|
||||
auto cursor = tmp_verts.begin();
|
||||
for (C2::PathD& path : paths) {
|
||||
for (C2::PointD& p : path) {
|
||||
p = v2_to_pd(*cursor);
|
||||
++cursor;
|
||||
}
|
||||
}
|
||||
|
||||
return CrossSection(
|
||||
shared_paths(C2::Union(warped, C2::FillRule::Positive, precision_)));
|
||||
shared_paths(C2::Union(paths, C2::FillRule::Positive, precision_)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -585,7 +614,28 @@ CrossSection CrossSection::Warp(
|
||||
* offseting operations are to be performed, which would compound the issue.
|
||||
*/
|
||||
CrossSection CrossSection::Simplify(double epsilon) const {
|
||||
auto ps = SimplifyPaths(GetPaths()->paths_, epsilon, false);
|
||||
C2::PolyTreeD tree;
|
||||
C2::BooleanOp(C2::ClipType::Union, C2::FillRule::Positive, GetPaths()->paths_,
|
||||
C2::PathsD(), tree, precision_);
|
||||
|
||||
C2::PathsD polys;
|
||||
flatten(&tree, polys, 0);
|
||||
|
||||
// Filter out contours less than epsilon wide.
|
||||
C2::PathsD filtered;
|
||||
for (C2::PathD poly : polys) {
|
||||
auto area = C2::Area(poly);
|
||||
Rect box;
|
||||
for (auto vert : poly) {
|
||||
box.Union(glm::vec2(vert.x, vert.y));
|
||||
}
|
||||
glm::vec2 size = box.Size();
|
||||
if (glm::abs(area) > glm::max(size.x, size.y) * epsilon) {
|
||||
filtered.push_back(poly);
|
||||
}
|
||||
}
|
||||
|
||||
auto ps = SimplifyPaths(filtered, epsilon, true);
|
||||
return CrossSection(shared_paths(ps));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,12 @@ project (manifold)
|
||||
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
|
||||
add_library(${PROJECT_NAME} ${SOURCE_FILES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
|
||||
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC utilities cross_section
|
||||
PRIVATE collider polygon ${MANIFOLD_INCLUDE} Clipper2 quickhull
|
||||
PUBLIC utilities cross_section sdf
|
||||
PRIVATE collider polygon ${MANIFOLD_INCLUDE} quickhull
|
||||
)
|
||||
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
|
||||
@@ -29,14 +31,5 @@ target_compile_features(${PROJECT_NAME}
|
||||
PUBLIC cxx_std_17
|
||||
)
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
COMPONENT runtime
|
||||
)
|
||||
install(
|
||||
DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
|
||||
COMPONENT devel
|
||||
)
|
||||
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
|
||||
install(FILES include/manifold.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "cross_section.h"
|
||||
#include "public.h"
|
||||
#include "vec_view.h"
|
||||
|
||||
namespace manifold {
|
||||
|
||||
@@ -45,9 +46,17 @@ class CsgLeafNode;
|
||||
*/
|
||||
struct MeshGL {
|
||||
/// Number of property vertices
|
||||
uint32_t NumVert() const { return vertProperties.size() / numProp; };
|
||||
uint32_t NumVert() const {
|
||||
if (vertProperties.size() / numProp >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
return vertProperties.size() / numProp;
|
||||
};
|
||||
/// Number of triangles
|
||||
uint32_t NumTri() const { return triVerts.size() / 3; };
|
||||
uint32_t NumTri() const {
|
||||
if (vertProperties.size() / numProp >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
return triVerts.size() / 3;
|
||||
};
|
||||
|
||||
/// Number of properties per vertex, always >= 3.
|
||||
uint32_t numProp = 3;
|
||||
@@ -68,7 +77,10 @@ struct MeshGL {
|
||||
/// Optional: Indicates runs of triangles that correspond to a particular
|
||||
/// input mesh instance. The runs encompass all of triVerts and are sorted
|
||||
/// by runOriginalID. Run i begins at triVerts[runIndex[i]] and ends at
|
||||
/// triVerts[runIndex[i+1]]. All runIndex values are divisible by 3.
|
||||
/// triVerts[runIndex[i+1]]. All runIndex values are divisible by 3. Returned
|
||||
/// runIndex will always be 1 longer than runOriginalID, but same length is
|
||||
/// also allowed as input: triVerts.size() will be automatically appended in
|
||||
/// this case.
|
||||
std::vector<uint32_t> runIndex;
|
||||
/// Optional: The OriginalID of the mesh this triangle run came from. This ID
|
||||
/// is ideal for reapplying materials to the output mesh. Multiple runs may
|
||||
@@ -111,6 +123,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. normals, UV
|
||||
* coordinates, colors, 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
|
||||
@@ -184,6 +221,7 @@ class Manifold {
|
||||
float Precision() const;
|
||||
int Genus() const;
|
||||
Properties GetProperties() const;
|
||||
float MinGap(const Manifold& other, float searchLength) const;
|
||||
///@}
|
||||
|
||||
/** @name Mesh ID
|
||||
@@ -206,11 +244,15 @@ 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 CalculateNormals(int normalIdx, float minSharpAngle = 60) const;
|
||||
Manifold SmoothByNormals(int normalIdx) const;
|
||||
Manifold SmoothOut(float minSharpAngle = 60, float minSmoothness = 0) const;
|
||||
Manifold Refine(int) const;
|
||||
// Manifold RefineToLength(float);
|
||||
Manifold RefineToLength(float) const;
|
||||
// Manifold RefineToPrecision(float);
|
||||
///@}
|
||||
|
||||
@@ -234,9 +276,20 @@ class Manifold {
|
||||
Manifold TrimByPlane(glm::vec3 normal, float originOffset) const;
|
||||
///@}
|
||||
|
||||
/** @name 2D from 3D
|
||||
*/
|
||||
///@{
|
||||
CrossSection Slice(float height = 0) const;
|
||||
CrossSection Project() const;
|
||||
///@}
|
||||
|
||||
/** @name Convex hull
|
||||
*/
|
||||
///@{
|
||||
Manifold Hull() const;
|
||||
static Manifold Hull(const std::vector<Manifold>& manifolds);
|
||||
static Manifold Hull(const std::vector<glm::vec3>& pts);
|
||||
///@}
|
||||
|
||||
/** @name Testing hooks
|
||||
* These are just for internal testing.
|
||||
|
||||
@@ -28,36 +28,34 @@ namespace {
|
||||
// cases to ensure consistency.
|
||||
|
||||
glm::vec2 Interpolate(glm::vec3 pL, glm::vec3 pR, float x) {
|
||||
float dxL = x - pL.x;
|
||||
float dxR = x - pR.x;
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
if (dxL * dxR > 0) printf("Not in domain!\n");
|
||||
#endif
|
||||
bool useL = fabs(dxL) < fabs(dxR);
|
||||
float lambda = (useL ? dxL : dxR) / (pR.x - pL.x);
|
||||
if (!isfinite(lambda)) return glm::vec2(pL.y, pL.z);
|
||||
const float dxL = x - pL.x;
|
||||
const float dxR = x - pR.x;
|
||||
ASSERT(dxL * dxR <= 0, logicErr, "Boolean manifold error: not in domain");
|
||||
const bool useL = fabs(dxL) < fabs(dxR);
|
||||
const glm::vec3 dLR = pR - pL;
|
||||
const float lambda = (useL ? dxL : dxR) / dLR.x;
|
||||
if (!isfinite(lambda) || !isfinite(dLR.y) || !isfinite(dLR.z))
|
||||
return glm::vec2(pL.y, pL.z);
|
||||
glm::vec2 yz;
|
||||
yz[0] = (useL ? pL.y : pR.y) + lambda * (pR.y - pL.y);
|
||||
yz[1] = (useL ? pL.z : pR.z) + lambda * (pR.z - pL.z);
|
||||
yz[0] = (useL ? pL.y : pR.y) + lambda * dLR.y;
|
||||
yz[1] = (useL ? pL.z : pR.z) + lambda * dLR.z;
|
||||
return yz;
|
||||
}
|
||||
|
||||
glm::vec4 Intersect(const glm::vec3 &pL, const glm::vec3 &pR,
|
||||
const glm::vec3 &qL, const glm::vec3 &qR) {
|
||||
float dyL = qL.y - pL.y;
|
||||
float dyR = qR.y - pR.y;
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
if (dyL * dyR > 0) printf("No intersection!\n");
|
||||
#endif
|
||||
bool useL = fabs(dyL) < fabs(dyR);
|
||||
float dx = pR.x - pL.x;
|
||||
const float dyL = qL.y - pL.y;
|
||||
const float dyR = qR.y - pR.y;
|
||||
ASSERT(dyL * dyR <= 0, logicErr, "Boolean manifold error: no intersection");
|
||||
const bool useL = fabs(dyL) < fabs(dyR);
|
||||
const float dx = pR.x - pL.x;
|
||||
float lambda = (useL ? dyL : dyR) / (dyL - dyR);
|
||||
if (!isfinite(lambda)) lambda = 0.0f;
|
||||
glm::vec4 xyzz;
|
||||
xyzz.x = (useL ? pL.x : pR.x) + lambda * dx;
|
||||
float pDy = pR.y - pL.y;
|
||||
float qDy = qR.y - qL.y;
|
||||
bool useP = fabs(pDy) < fabs(qDy);
|
||||
const float pDy = pR.y - pL.y;
|
||||
const float qDy = qR.y - qL.y;
|
||||
const bool useP = fabs(pDy) < fabs(qDy);
|
||||
xyzz.y = (useL ? (useP ? pL.y : qL.y) : (useP ? pR.y : qR.y)) +
|
||||
lambda * (useP ? pDy : qDy);
|
||||
xyzz.z = (useL ? pL.z : pR.z) + lambda * (pR.z - pL.z);
|
||||
@@ -73,9 +71,9 @@ struct CopyFaceEdges {
|
||||
SparseIndices &pXq1;
|
||||
VecView<const Halfedge> halfedgesQ;
|
||||
|
||||
void operator()(thrust::tuple<int, int> in) {
|
||||
void operator()(thrust::tuple<size_t, size_t> in) {
|
||||
int idx = 3 * thrust::get<0>(in);
|
||||
int i = thrust::get<1>(in);
|
||||
size_t i = thrust::get<1>(in);
|
||||
int pX = p1q1.Get(i, inverted);
|
||||
int q2 = p1q1.Get(i, !inverted);
|
||||
|
||||
@@ -85,17 +83,18 @@ struct CopyFaceEdges {
|
||||
int a = pX;
|
||||
int b = edge.IsForward() ? q1 : edge.pairedHalfedge;
|
||||
if (inverted) std::swap(a, b);
|
||||
pXq1.Set(idx + j, a, b);
|
||||
pXq1.Set(idx + static_cast<size_t>(j), a, b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SparseIndices Filter11(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
const SparseIndices &p1q2, const SparseIndices &p2q1) {
|
||||
ZoneScoped;
|
||||
SparseIndices p1q1(3 * p1q2.size() + 3 * p2q1.size());
|
||||
for_each_n(autoPolicy(p1q2.size()), zip(countAt(0), countAt(0)), p1q2.size(),
|
||||
CopyFaceEdges<false>({p1q2, p1q1, inQ.halfedge_}));
|
||||
for_each_n(autoPolicy(p2q1.size()), zip(countAt(p1q2.size()), countAt(0)),
|
||||
for_each_n(autoPolicy(p1q2.size()), zip(countAt(0_z), countAt(0_z)),
|
||||
p1q2.size(), CopyFaceEdges<false>({p1q2, p1q1, inQ.halfedge_}));
|
||||
for_each_n(autoPolicy(p2q1.size()), zip(countAt(p1q2.size()), countAt(0_z)),
|
||||
p2q1.size(), CopyFaceEdges<true>({p2q1, p1q1, inP.halfedge_}));
|
||||
p1q1.Unique();
|
||||
return p1q1;
|
||||
@@ -105,17 +104,6 @@ inline bool Shadows(float p, float q, float dir) {
|
||||
return p == q ? dir < 0 : p < q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since this function is called from two different places, it is necessary that
|
||||
* it returns identical results for identical input to keep consistency.
|
||||
* Normally this is trivial as computers make floating-point errors, but are
|
||||
* at least deterministic. However, in the case of CUDA, these functions can be
|
||||
* compiled by two different compilers (for the CPU and GPU). We have found that
|
||||
* the different compilers can cause slightly different rounding errors, so it
|
||||
* is critical that the two places this function is called both use the same
|
||||
* compiled function (they must agree on CPU or GPU). This is now taken care of
|
||||
* by the shared policy_ member.
|
||||
*/
|
||||
inline thrust::pair<int, glm::vec2> Shadow01(
|
||||
const int p0, const int q1, VecView<const glm::vec3> vertPosP,
|
||||
VecView<const glm::vec3> vertPosQ, VecView<const Halfedge> halfedgeQ,
|
||||
@@ -149,14 +137,14 @@ inline thrust::pair<int, glm::vec2> Shadow01(
|
||||
|
||||
// https://github.com/scandum/binary_search/blob/master/README.md
|
||||
// much faster than standard binary search on large arrays
|
||||
int monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
|
||||
size_t monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
|
||||
if (array.size() == 0) {
|
||||
return -1;
|
||||
}
|
||||
unsigned int bot = 0;
|
||||
unsigned int top = array.size();
|
||||
size_t bot = 0;
|
||||
size_t top = array.size();
|
||||
while (top >= 65536) {
|
||||
unsigned int mid = top / 4;
|
||||
size_t mid = top / 4;
|
||||
top -= mid * 3;
|
||||
if (key < array[bot + mid * 2]) {
|
||||
if (key >= array[bot + mid]) {
|
||||
@@ -171,7 +159,7 @@ int monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
|
||||
}
|
||||
|
||||
while (top > 3) {
|
||||
unsigned int mid = top / 2;
|
||||
size_t mid = top / 2;
|
||||
if (key >= array[bot + mid]) {
|
||||
bot += mid;
|
||||
}
|
||||
@@ -195,7 +183,7 @@ struct Kernel11 {
|
||||
VecView<const glm::vec3> normalP;
|
||||
const SparseIndices &p1q1;
|
||||
|
||||
void operator()(thrust::tuple<int, glm::vec4 &, int &> inout) {
|
||||
void operator()(thrust::tuple<size_t, glm::vec4 &, int &> inout) {
|
||||
const int p1 = p1q1.Get(thrust::get<0>(inout), false);
|
||||
const int q1 = p1q1.Get(thrust::get<0>(inout), true);
|
||||
glm::vec4 &xyzz11 = thrust::get<1>(inout);
|
||||
@@ -248,12 +236,7 @@ struct Kernel11 {
|
||||
if (s11 == 0) { // No intersection
|
||||
xyzz11 = glm::vec4(NAN);
|
||||
} else {
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
// Assert left and right were both found
|
||||
if (k != 2) {
|
||||
printf("k = %d\n", k);
|
||||
}
|
||||
#endif
|
||||
ASSERT(k == 2, logicErr, "Boolean manifold error: s11");
|
||||
xyzz11 = Intersect(pRL[0], pRL[1], qRL[0], qRL[1]);
|
||||
|
||||
const int p1s = halfedgeP[p1].startVert;
|
||||
@@ -273,11 +256,12 @@ std::tuple<Vec<int>, Vec<glm::vec4>> Shadow11(SparseIndices &p1q1,
|
||||
const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ,
|
||||
float expandP) {
|
||||
ZoneScoped;
|
||||
Vec<int> s11(p1q1.size());
|
||||
Vec<glm::vec4> xyzz11(p1q1.size());
|
||||
|
||||
for_each_n(autoPolicy(p1q1.size()),
|
||||
zip(countAt(0), xyzz11.begin(), s11.begin()), p1q1.size(),
|
||||
zip(countAt(0_z), xyzz11.begin(), s11.begin()), p1q1.size(),
|
||||
Kernel11({inP.vertPos_, inQ.vertPos_, inP.halfedge_, inQ.halfedge_,
|
||||
expandP, inP.vertNormal_, p1q1}));
|
||||
|
||||
@@ -295,7 +279,7 @@ struct Kernel02 {
|
||||
const SparseIndices &p0q2;
|
||||
const bool forward;
|
||||
|
||||
void operator()(thrust::tuple<int, int &, float &> inout) {
|
||||
void operator()(thrust::tuple<size_t, int &, float &> inout) {
|
||||
const int p0 = p0q2.Get(thrust::get<0>(inout), !forward);
|
||||
const int q2 = p0q2.Get(thrust::get<0>(inout), forward);
|
||||
int &s02 = thrust::get<1>(inout);
|
||||
@@ -344,12 +328,7 @@ struct Kernel02 {
|
||||
if (s02 == 0) { // No intersection
|
||||
z02 = NAN;
|
||||
} else {
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
// Assert left and right were both found
|
||||
if (k != 2) {
|
||||
printf("k = %d\n", k);
|
||||
}
|
||||
#endif
|
||||
ASSERT(k == 2, logicErr, "Boolean manifold error: s02");
|
||||
glm::vec3 vertPos = vertPosP[p0];
|
||||
z02 = Interpolate(yzzRL[0], yzzRL[1], vertPos.y)[1];
|
||||
if (forward) {
|
||||
@@ -367,12 +346,13 @@ std::tuple<Vec<int>, Vec<float>> Shadow02(const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ,
|
||||
SparseIndices &p0q2, bool forward,
|
||||
float expandP) {
|
||||
ZoneScoped;
|
||||
Vec<int> s02(p0q2.size());
|
||||
Vec<float> z02(p0q2.size());
|
||||
|
||||
auto vertNormalP = forward ? inP.vertNormal_ : inQ.vertNormal_;
|
||||
for_each_n(autoPolicy(p0q2.size()), zip(countAt(0), s02.begin(), z02.begin()),
|
||||
p0q2.size(),
|
||||
for_each_n(autoPolicy(p0q2.size()),
|
||||
zip(countAt(0_z), s02.begin(), z02.begin()), p0q2.size(),
|
||||
Kernel02({inP.vertPos_, inQ.halfedge_, inQ.vertPos_, expandP,
|
||||
vertNormalP, p0q2, forward}));
|
||||
|
||||
@@ -394,7 +374,7 @@ struct Kernel12 {
|
||||
const bool forward;
|
||||
const SparseIndices &p1q2;
|
||||
|
||||
void operator()(thrust::tuple<int, int &, glm::vec3 &> inout) {
|
||||
void operator()(thrust::tuple<size_t, int &, glm::vec3 &> inout) {
|
||||
int p1 = p1q2.Get(thrust::get<0>(inout), !forward);
|
||||
int q2 = p1q2.Get(thrust::get<0>(inout), forward);
|
||||
int &x12 = thrust::get<1>(inout);
|
||||
@@ -414,7 +394,7 @@ struct Kernel12 {
|
||||
for (int vert : {edge.startVert, edge.endVert}) {
|
||||
const int64_t key = forward ? SparseIndices::EncodePQ(vert, q2)
|
||||
: SparseIndices::EncodePQ(q2, vert);
|
||||
const int idx = monobound_quaternary_search(p0q2, key);
|
||||
const size_t idx = monobound_quaternary_search(p0q2, key);
|
||||
if (idx != -1) {
|
||||
const int s = s02[idx];
|
||||
x12 += s * ((vert == edge.startVert) == forward ? 1 : -1);
|
||||
@@ -435,7 +415,7 @@ struct Kernel12 {
|
||||
const int q1F = edge.IsForward() ? q1 : edge.pairedHalfedge;
|
||||
const int64_t key = forward ? SparseIndices::EncodePQ(p1, q1F)
|
||||
: SparseIndices::EncodePQ(q1F, p1);
|
||||
const int idx = monobound_quaternary_search(p1q1, key);
|
||||
const size_t idx = monobound_quaternary_search(p1q1, key);
|
||||
if (idx != -1) { // s is implicitly zero for anything not found
|
||||
const int s = s11[idx];
|
||||
x12 -= s * (edge.IsForward() ? 1 : -1);
|
||||
@@ -456,12 +436,7 @@ struct Kernel12 {
|
||||
if (x12 == 0) { // No intersection
|
||||
v12 = glm::vec3(NAN);
|
||||
} else {
|
||||
#ifdef MANIFOLD_DEBUG
|
||||
// Assert left and right were both found
|
||||
if (k != 2) {
|
||||
printf("k = %d\n", k);
|
||||
}
|
||||
#endif
|
||||
ASSERT(k == 2, logicErr, "Boolean manifold error: v12");
|
||||
const glm::vec4 xzyy =
|
||||
Intersect(xzyLR0[0], xzyLR0[1], xzyLR1[0], xzyLR1[1]);
|
||||
v12.x = xzyy[0];
|
||||
@@ -476,11 +451,12 @@ std::tuple<Vec<int>, Vec<glm::vec3>> Intersect12(
|
||||
const SparseIndices &p0q2, const Vec<int> &s11, const SparseIndices &p1q1,
|
||||
const Vec<float> &z02, const Vec<glm::vec4> &xyzz11, SparseIndices &p1q2,
|
||||
bool forward) {
|
||||
ZoneScoped;
|
||||
Vec<int> x12(p1q2.size());
|
||||
Vec<glm::vec3> v12(p1q2.size());
|
||||
|
||||
for_each_n(
|
||||
autoPolicy(p1q2.size()), zip(countAt(0), x12.begin(), v12.begin()),
|
||||
autoPolicy(p1q2.size()), zip(countAt(0_z), x12.begin(), v12.begin()),
|
||||
p1q2.size(),
|
||||
Kernel12({p0q2.AsVec64(), s02, z02, p1q1.AsVec64(), s11, xyzz11,
|
||||
inP.halfedge_, inQ.halfedge_, inP.vertPos_, forward, p1q2}));
|
||||
@@ -492,6 +468,7 @@ std::tuple<Vec<int>, Vec<glm::vec3>> Intersect12(
|
||||
|
||||
Vec<int> Winding03(const Manifold::Impl &inP, Vec<int> &vertices, Vec<int> &s02,
|
||||
bool reverse) {
|
||||
ZoneScoped;
|
||||
// verts that are not shadowed (not in p0q2) have winding number zero.
|
||||
Vec<int> w03(inP.NumVert(), 0);
|
||||
// checking is slow, so just sort and reduce
|
||||
@@ -601,6 +578,9 @@ Boolean3::Boolean3(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
Intersect12(inQ, inP, s20, p2q0, s11, p1q1, z20, xyzz11, p2q1_, false);
|
||||
PRINT("x21 size = " << x21_.size());
|
||||
|
||||
if (x12_.size() + x21_.size() >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
|
||||
Vec<int> p0 = p0q2.Copy(false);
|
||||
p0q2.Resize(0);
|
||||
Vec<int> q0 = p2q0.Copy(true);
|
||||
@@ -616,7 +596,6 @@ Boolean3::Boolean3(const Manifold::Impl &inP, const Manifold::Impl &inQ,
|
||||
if (ManifoldParams().verbose) {
|
||||
broad.Print("Broad phase");
|
||||
intersections.Print("Intersections");
|
||||
MemUsage();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -101,7 +101,12 @@ std::tuple<Vec<int>, Vec<int>> SizeOutput(
|
||||
const Vec<int> &i03, const Vec<int> &i30, const Vec<int> &i12,
|
||||
const Vec<int> &i21, const SparseIndices &p1q2, const SparseIndices &p2q1,
|
||||
bool invertQ) {
|
||||
ZoneScoped;
|
||||
Vec<int> sidesPerFacePQ(inP.NumTri() + inQ.NumTri(), 0);
|
||||
// note: numFaceR <= facePQ2R.size() = sidesPerFacePQ.size() + 1
|
||||
if (sidesPerFacePQ.size() + 1 >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("boolean result too large");
|
||||
|
||||
auto sidesPerFaceP = sidesPerFacePQ.view(0, inP.NumTri());
|
||||
auto sidesPerFaceQ = sidesPerFacePQ.view(inP.NumTri(), inQ.NumTri());
|
||||
|
||||
@@ -167,13 +172,14 @@ void AddNewEdgeVerts(
|
||||
concurrent_map<std::pair<int, int>, std::vector<EdgePos>> &edgesNew,
|
||||
const SparseIndices &p1q2, const Vec<int> &i12, const Vec<int> &v12R,
|
||||
const Vec<Halfedge> &halfedgeP, bool forward) {
|
||||
ZoneScoped;
|
||||
// For each edge of P that intersects a face of Q (p1q2), add this vertex to
|
||||
// P's corresponding edge vector and to the two new edges, which are
|
||||
// intersections between the face of Q and the two faces of P attached to the
|
||||
// edge. The direction and duplicity are given by i12, while v12R remaps to
|
||||
// the output vert index. When forward is false, all is reversed.
|
||||
auto process = [&](std::function<void(size_t)> lock,
|
||||
std::function<void(size_t)> unlock, int i) {
|
||||
std::function<void(size_t)> unlock, size_t i) {
|
||||
const int edgeP = p1q2.Get(i, !forward);
|
||||
const int faceQ = p1q2.Get(i, forward);
|
||||
const int vert = v12R[i];
|
||||
@@ -216,9 +222,9 @@ void AddNewEdgeVerts(
|
||||
[&](size_t hash) { mutexes[hash % mutexes.size()].unlock(); },
|
||||
std::placeholders::_1);
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<int>(0, p1q2.size(), 32),
|
||||
[&](const tbb::blocked_range<int> &range) {
|
||||
for (int i = range.begin(); i != range.end(); i++) processFun(i);
|
||||
tbb::blocked_range<size_t>(0_z, p1q2.size(), 32),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t i = range.begin(); i != range.end(); i++) processFun(i);
|
||||
},
|
||||
ap);
|
||||
return;
|
||||
@@ -226,7 +232,7 @@ void AddNewEdgeVerts(
|
||||
#endif
|
||||
auto processFun = std::bind(
|
||||
process, [](size_t _) {}, [](size_t _) {}, std::placeholders::_1);
|
||||
for (int i = 0; i < p1q2.size(); ++i) processFun(i);
|
||||
for (size_t i = 0; i < p1q2.size(); ++i) processFun(i);
|
||||
}
|
||||
|
||||
std::vector<Halfedge> PairUp(std::vector<EdgePos> &edgePos) {
|
||||
@@ -237,7 +243,7 @@ std::vector<Halfedge> PairUp(std::vector<EdgePos> &edgePos) {
|
||||
// a heuristic.
|
||||
ASSERT(edgePos.size() % 2 == 0, topologyErr,
|
||||
"Non-manifold edge! Not an even number of points.");
|
||||
int nEdges = edgePos.size() / 2;
|
||||
size_t nEdges = edgePos.size() / 2;
|
||||
auto middle = std::partition(edgePos.begin(), edgePos.end(),
|
||||
[](EdgePos x) { return x.isStart; });
|
||||
ASSERT(middle - edgePos.begin() == nEdges, topologyErr, "Non-manifold edge!");
|
||||
@@ -245,7 +251,7 @@ std::vector<Halfedge> PairUp(std::vector<EdgePos> &edgePos) {
|
||||
std::stable_sort(edgePos.begin(), middle, cmp);
|
||||
std::stable_sort(middle, edgePos.end(), cmp);
|
||||
std::vector<Halfedge> edges;
|
||||
for (int i = 0; i < nEdges; ++i)
|
||||
for (size_t i = 0; i < nEdges; ++i)
|
||||
edges.push_back({edgePos[i].vert, edgePos[i + nEdges].vert, -1, -1});
|
||||
return edges;
|
||||
}
|
||||
@@ -256,6 +262,7 @@ void AppendPartialEdges(Manifold::Impl &outR, Vec<char> &wholeHalfedgeP,
|
||||
Vec<TriRef> &halfedgeRef, const Manifold::Impl &inP,
|
||||
const Vec<int> &i03, const Vec<int> &vP2R,
|
||||
const Vec<int>::IterC faceP2R, bool forward) {
|
||||
ZoneScoped;
|
||||
// Each edge in the map is partially retained; for each of these, look up
|
||||
// their original verts and include them based on their winding number (i03),
|
||||
// while remapping them to the output using vP2R. Use the verts position
|
||||
@@ -282,7 +289,6 @@ void AppendPartialEdges(Manifold::Impl &outR, Vec<char> &wholeHalfedgeP,
|
||||
}
|
||||
|
||||
int inclusion = i03[vStart];
|
||||
bool reversed = inclusion < 0;
|
||||
EdgePos edgePos = {vP2R[vStart],
|
||||
glm::dot(outR.vertPos_[vP2R[vStart]], edgeVec),
|
||||
inclusion > 0};
|
||||
@@ -292,7 +298,6 @@ void AppendPartialEdges(Manifold::Impl &outR, Vec<char> &wholeHalfedgeP,
|
||||
}
|
||||
|
||||
inclusion = i03[vEnd];
|
||||
reversed |= inclusion < 0;
|
||||
edgePos = {vP2R[vEnd], glm::dot(outR.vertPos_[vP2R[vEnd]], edgeVec),
|
||||
inclusion < 0};
|
||||
for (int j = 0; j < glm::abs(inclusion); ++j) {
|
||||
@@ -337,6 +342,7 @@ void AppendNewEdges(
|
||||
Manifold::Impl &outR, Vec<int> &facePtrR,
|
||||
concurrent_map<std::pair<int, int>, std::vector<EdgePos>> &edgesNew,
|
||||
Vec<TriRef> &halfedgeRef, const Vec<int> &facePQ2R, const int numFaceP) {
|
||||
ZoneScoped;
|
||||
// Pair up each edge's verts and distribute to faces based on indices in key.
|
||||
Vec<Halfedge> &halfedgeR = outR.halfedge_;
|
||||
Vec<glm::vec3> &vertPosR = outR.vertPos_;
|
||||
@@ -399,7 +405,6 @@ struct DuplicateHalfedges {
|
||||
if (!thrust::get<0>(in)) return;
|
||||
Halfedge halfedge = thrust::get<1>(in);
|
||||
if (!halfedge.IsForward()) return;
|
||||
const int edgeP = thrust::get<2>(in);
|
||||
|
||||
const int inclusion = i03[halfedge.startVert];
|
||||
if (inclusion == 0) return;
|
||||
@@ -442,6 +447,7 @@ void AppendWholeEdges(Manifold::Impl &outR, Vec<int> &facePtrR,
|
||||
const Vec<char> wholeHalfedgeP, const Vec<int> &i03,
|
||||
const Vec<int> &vP2R, VecView<const int> faceP2R,
|
||||
bool forward) {
|
||||
ZoneScoped;
|
||||
for_each_n(ManifoldParams().deterministic ? ExecutionPolicy::Seq
|
||||
: autoPolicy(inP.halfedge_.size()),
|
||||
zip(wholeHalfedgeP.begin(), inP.halfedge_.begin(), countAt(0)),
|
||||
@@ -463,9 +469,8 @@ struct MapTriRef {
|
||||
}
|
||||
};
|
||||
|
||||
Vec<TriRef> UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ, bool invertQ) {
|
||||
Vec<TriRef> refPQ = outR.meshRelation_.triRef;
|
||||
void UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ, bool invertQ) {
|
||||
const int offsetQ = Manifold::Impl::meshIDCounter_;
|
||||
for_each_n(
|
||||
autoPolicy(outR.NumTri()), outR.meshRelation_.triRef.begin(),
|
||||
@@ -480,7 +485,6 @@ Vec<TriRef> UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
|
||||
outR.meshRelation_.meshIDtransform[pair.first + offsetQ].backSide ^=
|
||||
invertQ;
|
||||
}
|
||||
return refPQ;
|
||||
}
|
||||
|
||||
struct Barycentric {
|
||||
@@ -514,8 +518,9 @@ struct Barycentric {
|
||||
}
|
||||
};
|
||||
|
||||
void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
const Manifold::Impl &inP, const Manifold::Impl &inQ) {
|
||||
void CreateProperties(Manifold::Impl &outR, const Manifold::Impl &inP,
|
||||
const Manifold::Impl &inQ) {
|
||||
ZoneScoped;
|
||||
const int numPropP = inP.NumProp();
|
||||
const int numPropQ = inQ.NumProp();
|
||||
const int numProp = glm::max(numPropP, numPropQ);
|
||||
@@ -526,7 +531,8 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
outR.meshRelation_.triProperties.resize(numTri);
|
||||
|
||||
Vec<glm::vec3> bary(outR.halfedge_.size());
|
||||
for_each_n(autoPolicy(numTri), zip(countAt(0), refPQ.cbegin()), numTri,
|
||||
for_each_n(autoPolicy(numTri),
|
||||
zip(countAt(0), outR.meshRelation_.triRef.cbegin()), numTri,
|
||||
Barycentric({bary, inP.vertPos_, inQ.vertPos_, outR.vertPos_,
|
||||
inP.halfedge_, inQ.halfedge_, outR.halfedge_,
|
||||
outR.precision_}));
|
||||
@@ -534,6 +540,14 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
using Entry = std::pair<glm::ivec3, int>;
|
||||
int idMissProp = outR.NumVert();
|
||||
std::vector<std::vector<Entry>> propIdx(outR.NumVert() + 1);
|
||||
std::vector<int> propMissIdx[2];
|
||||
propMissIdx[0].resize(inQ.NumPropVert(), -1);
|
||||
propMissIdx[1].resize(inP.NumPropVert(), -1);
|
||||
|
||||
if (static_cast<size_t>(outR.NumVert()) * static_cast<size_t>(numProp) >=
|
||||
std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("too many vertices");
|
||||
|
||||
outR.meshRelation_.properties.reserve(outR.NumVert() * numProp);
|
||||
int idx = 0;
|
||||
|
||||
@@ -541,14 +555,14 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
// Skip collapsed triangles
|
||||
if (outR.halfedge_[3 * tri].startVert < 0) continue;
|
||||
|
||||
const int triPQ = refPQ[tri].tri;
|
||||
const bool PQ = refPQ[tri].meshID == 0;
|
||||
const TriRef ref = outR.meshRelation_.triRef[tri];
|
||||
const bool PQ = ref.meshID == 0;
|
||||
const int oldNumProp = PQ ? numPropP : numPropQ;
|
||||
const auto &properties =
|
||||
PQ ? inP.meshRelation_.properties : inQ.meshRelation_.properties;
|
||||
const glm::ivec3 &triProp = oldNumProp == 0 ? glm::ivec3(-1)
|
||||
: PQ ? inP.meshRelation_.triProperties[triPQ]
|
||||
: inQ.meshRelation_.triProperties[triPQ];
|
||||
: PQ ? inP.meshRelation_.triProperties[ref.tri]
|
||||
: inQ.meshRelation_.triProperties[ref.tri];
|
||||
|
||||
for (const int i : {0, 1, 2}) {
|
||||
const int vert = outR.halfedge_[3 * tri + i].startVert;
|
||||
@@ -556,8 +570,7 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
|
||||
glm::ivec4 key(PQ, idMissProp, -1, -1);
|
||||
if (oldNumProp > 0) {
|
||||
key[1] = vert;
|
||||
int edge = -1;
|
||||
int edge = -2;
|
||||
for (const int j : {0, 1, 2}) {
|
||||
if (uvw[j] == 1) {
|
||||
// On a retained vert, the propVert must also match
|
||||
@@ -571,24 +584,37 @@ void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
|
||||
// On an edge, both propVerts must match
|
||||
const int p0 = triProp[Next3(edge)];
|
||||
const int p1 = triProp[Prev3(edge)];
|
||||
key[1] = vert;
|
||||
key[2] = glm::min(p0, p1);
|
||||
key[3] = glm::max(p0, p1);
|
||||
} else if (edge == -2) {
|
||||
key[1] = vert;
|
||||
}
|
||||
}
|
||||
|
||||
auto &bin = propIdx[key.y];
|
||||
bool bFound = false;
|
||||
for (int k = 0; k < bin.size(); ++k) {
|
||||
if (bin[k].first == glm::ivec3(key.x, key.z, key.w)) {
|
||||
bFound = true;
|
||||
outR.meshRelation_.triProperties[tri][i] = bin[k].second;
|
||||
break;
|
||||
if (key.y == idMissProp && key.z >= 0) {
|
||||
// only key.x/key.z matters
|
||||
auto &entry = propMissIdx[key.x][key.z];
|
||||
if (entry >= 0) {
|
||||
outR.meshRelation_.triProperties[tri][i] = entry;
|
||||
continue;
|
||||
}
|
||||
entry = idx;
|
||||
} else {
|
||||
auto &bin = propIdx[key.y];
|
||||
bool bFound = false;
|
||||
for (int k = 0; k < bin.size(); ++k) {
|
||||
if (bin[k].first == glm::ivec3(key.x, key.z, key.w)) {
|
||||
bFound = true;
|
||||
outR.meshRelation_.triProperties[tri][i] = bin[k].second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bFound) continue;
|
||||
bin.push_back(std::make_pair(glm::ivec3(key.x, key.z, key.w), idx));
|
||||
}
|
||||
if (bFound) continue;
|
||||
bin.push_back(std::make_pair(glm::ivec3(key.x, key.z, key.w), idx));
|
||||
|
||||
outR.meshRelation_.triProperties[tri][i] = idx++;
|
||||
|
||||
for (int p = 0; p < numProp; ++p) {
|
||||
if (p < oldNumProp) {
|
||||
glm::vec3 oldProps;
|
||||
@@ -724,7 +750,6 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
std::tie(faceEdge, facePQ2R) =
|
||||
SizeOutput(outR, inP_, inQ_, i03, i30, i12, i21, p1q2_, p2q1_, invertQ);
|
||||
|
||||
const int numFaceR = faceEdge.size() - 1;
|
||||
// This gets incremented for each halfedge that's added to a face so that the
|
||||
// next one knows where to slot in.
|
||||
Vec<int> facePtrR = faceEdge;
|
||||
@@ -770,12 +795,12 @@ Manifold::Impl Boolean3::Result(OpType op) const {
|
||||
if (ManifoldParams().intermediateChecks)
|
||||
ASSERT(outR.IsManifold(), logicErr, "triangulated mesh is not manifold!");
|
||||
|
||||
Vec<TriRef> refPQ = UpdateReference(outR, inP_, inQ_, invertQ);
|
||||
CreateProperties(outR, inP_, inQ_);
|
||||
|
||||
UpdateReference(outR, inP_, inQ_, invertQ);
|
||||
|
||||
outR.SimplifyTopology();
|
||||
|
||||
CreateProperties(outR, refPQ, inP_, inQ_);
|
||||
|
||||
if (ManifoldParams().intermediateChecks)
|
||||
ASSERT(outR.Is2Manifold(), logicErr, "simplified mesh is not 2-manifold!");
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ Manifold Manifold::Smooth(const MeshGL& meshGL,
|
||||
std::vector<float> propertyTolerance(meshGL.numProp - 3, -1);
|
||||
std::shared_ptr<Impl> impl =
|
||||
std::make_shared<Impl>(meshGL, propertyTolerance);
|
||||
impl->CreateTangents(sharpenedEdges);
|
||||
impl->CreateTangents(impl->UpdateSharpenedEdges(sharpenedEdges));
|
||||
return Manifold(impl);
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ Manifold Manifold::Smooth(const Mesh& mesh,
|
||||
|
||||
Impl::MeshRelationD relation = {(int)ReserveIDs(1)};
|
||||
std::shared_ptr<Impl> impl = std::make_shared<Impl>(mesh, relation);
|
||||
impl->CreateTangents(sharpenedEdges);
|
||||
impl->CreateTangents(impl->UpdateSharpenedEdges(sharpenedEdges));
|
||||
return Manifold(impl);
|
||||
}
|
||||
|
||||
@@ -153,10 +153,9 @@ Manifold Manifold::Cube(glm::vec3 size, bool center) {
|
||||
glm::length(size) == 0.) {
|
||||
return Invalid();
|
||||
}
|
||||
auto cube = Manifold(std::make_shared<Impl>(Impl::Shape::Cube));
|
||||
cube = cube.Scale(size);
|
||||
if (center) cube = cube.Translate(-size / 2.0f);
|
||||
return cube.AsOriginal();
|
||||
glm::mat4x3 m =
|
||||
glm::translate(center ? (-size / 2.0f) : glm::vec3(0)) * glm::scale(size);
|
||||
return Manifold(std::make_shared<Impl>(Manifold::Impl::Shape::Cube, m));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +207,7 @@ Manifold Manifold::Sphere(float radius, int circularSegments) {
|
||||
int n = circularSegments > 0 ? (circularSegments + 3) / 4
|
||||
: Quality::GetCircularSegments(radius) / 4;
|
||||
auto pImpl_ = std::make_shared<Impl>(Impl::Shape::Octahedron);
|
||||
pImpl_->Subdivide(n);
|
||||
pImpl_->Subdivide([n](glm::vec3 edge) { return n - 1; });
|
||||
for_each_n(autoPolicy(pImpl_->NumVert()), pImpl_->vertPos_.begin(),
|
||||
pImpl_->NumVert(), ToSphere({radius}));
|
||||
pImpl_->Finish();
|
||||
@@ -220,6 +219,9 @@ Manifold Manifold::Sphere(float radius, int circularSegments) {
|
||||
/**
|
||||
* Constructs a manifold from a set of polygons by extruding them along the
|
||||
* Z-axis.
|
||||
* Note that high twistDegrees with small nDivisions may cause
|
||||
* self-intersection. This is not checked here and it is up to the user to
|
||||
* choose the correct parameters.
|
||||
*
|
||||
* @param crossSection A set of non-overlapping polygons to extrude.
|
||||
* @param height Z-extent of extrusion.
|
||||
@@ -230,11 +232,13 @@ Manifold Manifold::Sphere(float radius, int circularSegments) {
|
||||
* bottom, interpolated linearly for the divisions in between.
|
||||
* @param scaleTop Amount to scale the top (independently in X and Y). If the
|
||||
* scale is {0, 0}, a pure cone is formed with only a single vertex at the top.
|
||||
* Note that scale is applied after twist.
|
||||
* Default {1, 1}.
|
||||
*/
|
||||
Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
|
||||
int nDivisions, float twistDegrees,
|
||||
glm::vec2 scaleTop) {
|
||||
ZoneScoped;
|
||||
auto polygons = crossSection.ToPolygons();
|
||||
if (polygons.size() == 0 || height <= 0.0f) {
|
||||
return Invalid();
|
||||
@@ -264,9 +268,9 @@ Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
|
||||
for (int i = 1; i < nDivisions + 1; ++i) {
|
||||
float alpha = i / float(nDivisions);
|
||||
float phi = alpha * twistDegrees;
|
||||
glm::mat2 transform(cosd(phi), sind(phi), -sind(phi), cosd(phi));
|
||||
glm::vec2 scale = glm::mix(glm::vec2(1.0f), scaleTop, alpha);
|
||||
transform = transform * glm::mat2(scale.x, 0.0f, 0.0f, scale.y);
|
||||
glm::mat2 rotation(cosd(phi), sind(phi), -sind(phi), cosd(phi));
|
||||
glm::mat2 transform = glm::mat2(scale.x, 0.0f, 0.0f, scale.y) * rotation;
|
||||
int j = 0;
|
||||
int idx = 0;
|
||||
for (const auto& poly : polygons) {
|
||||
@@ -320,6 +324,7 @@ Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
|
||||
*/
|
||||
Manifold Manifold::Revolve(const CrossSection& crossSection,
|
||||
int circularSegments, float revolveDegrees) {
|
||||
ZoneScoped;
|
||||
Polygons polygons = crossSection.ToPolygons();
|
||||
|
||||
if (polygons.size() == 0) {
|
||||
@@ -327,8 +332,9 @@ Manifold Manifold::Revolve(const CrossSection& crossSection,
|
||||
}
|
||||
|
||||
const Rect bounds = crossSection.Bounds();
|
||||
const float radius = bounds.max.x;
|
||||
|
||||
if (bounds.max.x <= 0) {
|
||||
if (radius <= 0) {
|
||||
return Invalid();
|
||||
} else if (bounds.min.x < 0) {
|
||||
// Take the x>=0 slice.
|
||||
@@ -340,21 +346,15 @@ Manifold Manifold::Revolve(const CrossSection& crossSection,
|
||||
polygons = (crossSection ^ posBoundingBox).ToPolygons();
|
||||
}
|
||||
|
||||
float radius = 0.0f;
|
||||
for (const auto& poly : polygons) {
|
||||
for (const auto& vert : poly) {
|
||||
radius = fmax(radius, vert.x);
|
||||
}
|
||||
}
|
||||
|
||||
if (revolveDegrees > 360.0f) {
|
||||
revolveDegrees = 360.0f;
|
||||
}
|
||||
const bool isFullRevolution = revolveDegrees == 360.0f;
|
||||
|
||||
const int nDivisions = circularSegments > 2
|
||||
? circularSegments
|
||||
: Quality::GetCircularSegments(radius);
|
||||
const int nDivisions =
|
||||
circularSegments > 2
|
||||
? circularSegments
|
||||
: Quality::GetCircularSegments(radius) * revolveDegrees / 360;
|
||||
|
||||
auto pImpl_ = std::make_shared<Impl>();
|
||||
auto& vertPos = pImpl_->vertPos_;
|
||||
@@ -384,14 +384,14 @@ Manifold Manifold::Revolve(const CrossSection& crossSection,
|
||||
|
||||
if (!isFullRevolution) startPoses.push_back(startPosIndex);
|
||||
|
||||
const glm::vec2 currPolyVertex = poly[polyVert];
|
||||
const glm::vec2 prevPolyVertex =
|
||||
poly[polyVert == 0 ? poly.size() - 1 : polyVert - 1];
|
||||
|
||||
const int prevStartPosIndex =
|
||||
startPosIndex +
|
||||
(polyVert == 0 ? nRevolveAxisVerts + (nSlices * nPosVerts) - nSlices
|
||||
: poly[polyVert - 1].x == 0.0 ? -1
|
||||
: -nSlices);
|
||||
glm::vec2 currPolyVertex = poly[polyVert];
|
||||
glm::vec2 prevPolyVertex =
|
||||
poly[polyVert == 0 ? poly.size() - 1 : polyVert - 1];
|
||||
(polyVert == 0 ? nRevolveAxisVerts + (nSlices * nPosVerts) : 0) +
|
||||
(prevPolyVertex.x == 0.0 ? -1 : -nSlices);
|
||||
|
||||
for (int slice = 0; slice < nSlices; ++slice) {
|
||||
const float phi = slice * dPhi;
|
||||
@@ -464,6 +464,7 @@ Manifold Manifold::Compose(const std::vector<Manifold>& manifolds) {
|
||||
* containing a copy of the original. It is the inverse operation of Compose().
|
||||
*/
|
||||
std::vector<Manifold> Manifold::Decompose() const {
|
||||
ZoneScoped;
|
||||
UnionFind<> uf(NumVert());
|
||||
// Graph graph;
|
||||
auto pImpl_ = GetCsgLeafNode().GetImpl();
|
||||
@@ -508,7 +509,6 @@ std::vector<Manifold> Manifold::Decompose() const {
|
||||
|
||||
impl->GatherFaces(*pImpl_, faceNew2Old);
|
||||
impl->ReindexVerts(vertNew2Old, pImpl_->NumVert());
|
||||
|
||||
impl->Finish();
|
||||
|
||||
meshes.push_back(Manifold(impl));
|
||||
|
||||
@@ -172,6 +172,7 @@ CsgNodeType CsgLeafNode::GetNodeType() const { return CsgNodeType::Leaf; }
|
||||
*/
|
||||
Manifold::Impl CsgLeafNode::Compose(
|
||||
const std::vector<std::shared_ptr<CsgLeafNode>> &nodes) {
|
||||
ZoneScoped;
|
||||
float precision = -1;
|
||||
int numVert = 0;
|
||||
int numEdge = 0;
|
||||
@@ -316,7 +317,7 @@ Manifold::Impl CsgLeafNode::Compose(
|
||||
auto &node = nodes[i];
|
||||
const int offset = i * Manifold::Impl::meshIDCounter_;
|
||||
|
||||
for (const auto pair : node->pImpl_->meshRelation_.meshIDtransform) {
|
||||
for (const auto &pair : node->pImpl_->meshRelation_.meshIDtransform) {
|
||||
combined.meshRelation_.meshIDtransform[pair.first + offset] = pair.second;
|
||||
}
|
||||
}
|
||||
@@ -454,6 +455,7 @@ std::shared_ptr<CsgLeafNode> CsgOpNode::ToLeafNode() const {
|
||||
std::shared_ptr<Manifold::Impl> CsgOpNode::BatchBoolean(
|
||||
OpType operation,
|
||||
std::vector<std::shared_ptr<const Manifold::Impl>> &results) {
|
||||
ZoneScoped;
|
||||
auto getImplPtr = GetImplPtr();
|
||||
ASSERT(operation != OpType::Subtract, logicErr,
|
||||
"BatchBoolean doesn't support Difference.");
|
||||
@@ -529,6 +531,7 @@ std::shared_ptr<Manifold::Impl> CsgOpNode::BatchBoolean(
|
||||
* `BatchBoolean` instead of using `Compose` for non-intersecting manifolds.
|
||||
*/
|
||||
void CsgOpNode::BatchUnion() const {
|
||||
ZoneScoped;
|
||||
// INVARIANT: children_ is a vector of leaf nodes
|
||||
// this kMaxUnionSize is a heuristic to avoid the pairwise disjoint check
|
||||
// with O(n^2) complexity to take too long.
|
||||
|
||||
@@ -73,9 +73,7 @@ struct FlagEdge {
|
||||
current = NextHalfedge(halfedge[current].pairedHalfedge);
|
||||
int tri = current / 3;
|
||||
const TriRef ref = triRef[tri];
|
||||
if ((ref.meshID != ref0.meshID || ref.tri != ref0.tri) &&
|
||||
(ref.meshID != ref1.meshID || ref.tri != ref1.tri))
|
||||
return false;
|
||||
if (!ref.SameFace(ref0) && !ref.SameFace(ref1)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -145,29 +143,32 @@ namespace manifold {
|
||||
void Manifold::Impl::SimplifyTopology() {
|
||||
if (!halfedge_.size()) return;
|
||||
|
||||
const int nbEdges = halfedge_.size();
|
||||
const size_t nbEdges = halfedge_.size();
|
||||
auto policy = autoPolicy(nbEdges);
|
||||
int numFlagged = 0;
|
||||
size_t numFlagged = 0;
|
||||
Vec<uint8_t> bflags(nbEdges);
|
||||
|
||||
// In the case of a very bad triangulation, it is possible to create pinched
|
||||
// verts. They must be removed before edge collapse.
|
||||
SplitPinchedVerts();
|
||||
|
||||
Vec<SortEntry> entries;
|
||||
entries.reserve(nbEdges / 2);
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
if (halfedge_[i].IsForward()) {
|
||||
entries.push_back({halfedge_[i].startVert, halfedge_[i].endVert, i});
|
||||
{
|
||||
ZoneScopedN("DedupeEdge");
|
||||
Vec<SortEntry> entries;
|
||||
entries.reserve(nbEdges / 2);
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
if (halfedge_[i].IsForward()) {
|
||||
entries.push_back({halfedge_[i].startVert, halfedge_[i].endVert, i});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stable_sort(policy, entries.begin(), entries.end());
|
||||
for (int i = 0; i < entries.size() - 1; ++i) {
|
||||
if (entries[i].start == entries[i + 1].start &&
|
||||
entries[i].end == entries[i + 1].end) {
|
||||
DedupeEdge(entries[i].index);
|
||||
numFlagged++;
|
||||
stable_sort(policy, entries.begin(), entries.end());
|
||||
for (int i = 0; i < entries.size() - 1; ++i) {
|
||||
if (entries[i].start == entries[i + 1].start &&
|
||||
entries[i].end == entries[i + 1].end) {
|
||||
DedupeEdge(entries[i].index);
|
||||
numFlagged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,13 +179,19 @@ void Manifold::Impl::SimplifyTopology() {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ManifoldParams().cleanupTriangles) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<int> scratchBuffer;
|
||||
scratchBuffer.reserve(10);
|
||||
{
|
||||
ZoneScopedN("CollapseShortEdge");
|
||||
numFlagged = 0;
|
||||
ShortEdge se{halfedge_, vertPos_, precision_};
|
||||
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
for_each_n(policy, countAt(0_z), nbEdges,
|
||||
[&](size_t i) { bflags[i] = se(i); });
|
||||
for (size_t i = 0; i < nbEdges; ++i) {
|
||||
if (bflags[i]) {
|
||||
CollapseEdge(i, scratchBuffer);
|
||||
scratchBuffer.resize(0);
|
||||
@@ -201,10 +208,12 @@ void Manifold::Impl::SimplifyTopology() {
|
||||
#endif
|
||||
|
||||
{
|
||||
ZoneScopedN("CollapseFlaggedEdge");
|
||||
numFlagged = 0;
|
||||
FlagEdge se{halfedge_, meshRelation_.triRef};
|
||||
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
for_each_n(policy, countAt(0_z), nbEdges,
|
||||
[&](size_t i) { bflags[i] = se(i); });
|
||||
for (size_t i = 0; i < nbEdges; ++i) {
|
||||
if (bflags[i]) {
|
||||
CollapseEdge(i, scratchBuffer);
|
||||
scratchBuffer.resize(0);
|
||||
@@ -221,13 +230,15 @@ void Manifold::Impl::SimplifyTopology() {
|
||||
#endif
|
||||
|
||||
{
|
||||
ZoneScopedN("RecursiveEdgeSwap");
|
||||
numFlagged = 0;
|
||||
SwappableEdge se{halfedge_, vertPos_, faceNormal_, precision_};
|
||||
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
|
||||
for_each_n(policy, countAt(0_z), nbEdges,
|
||||
[&](size_t i) { bflags[i] = se(i); });
|
||||
std::vector<int> edgeSwapStack;
|
||||
std::vector<int> visited(halfedge_.size(), -1);
|
||||
int tag = 0;
|
||||
for (int i = 0; i < nbEdges; ++i) {
|
||||
for (size_t i = 0; i < nbEdges; ++i) {
|
||||
if (bflags[i]) {
|
||||
numFlagged++;
|
||||
tag++;
|
||||
@@ -312,12 +323,10 @@ void Manifold::Impl::DedupeEdge(const int edge) {
|
||||
vertPos_.push_back(vertPos_[endVert]);
|
||||
if (vertNormal_.size() > 0) vertNormal_.push_back(vertNormal_[endVert]);
|
||||
|
||||
do {
|
||||
halfedge_[current].endVert = newVert;
|
||||
current = NextHalfedge(current);
|
||||
halfedge_[current].startVert = newVert;
|
||||
current = halfedge_[current].pairedHalfedge;
|
||||
} while (current != edge);
|
||||
ForVert(NextHalfedge(current), [this, newVert](int e) {
|
||||
halfedge_[e].startVert = newVert;
|
||||
halfedge_[halfedge_[e].pairedHalfedge].endVert = newVert;
|
||||
});
|
||||
}
|
||||
|
||||
// Orbit startVert
|
||||
@@ -337,12 +346,10 @@ void Manifold::Impl::DedupeEdge(const int edge) {
|
||||
vertPos_.push_back(vertPos_[endVert]);
|
||||
if (vertNormal_.size() > 0) vertNormal_.push_back(vertNormal_[endVert]);
|
||||
|
||||
do {
|
||||
halfedge_[current].endVert = newVert;
|
||||
current = NextHalfedge(current);
|
||||
halfedge_[current].startVert = newVert;
|
||||
current = halfedge_[current].pairedHalfedge;
|
||||
} while (current != pair);
|
||||
ForVert(NextHalfedge(current), [this, newVert](int e) {
|
||||
halfedge_[e].startVert = newVert;
|
||||
halfedge_[halfedge_[e].pairedHalfedge].endVert = newVert;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,9 +476,9 @@ void Manifold::Impl::CollapseEdge(const int edge, std::vector<int>& edges) {
|
||||
const glm::mat3x2 projection = GetAxisAlignedProjection(faceNormal_[tri]);
|
||||
// Don't collapse if the edge is not redundant (this may have changed due
|
||||
// to the collapse of neighbors).
|
||||
if (ref.meshID != refCheck.meshID || ref.tri != refCheck.tri) {
|
||||
if (!ref.SameFace(refCheck)) {
|
||||
refCheck = triRef[edge / 3];
|
||||
if (ref.meshID != refCheck.meshID || ref.tri != refCheck.tri) {
|
||||
if (!ref.SameFace(refCheck)) {
|
||||
return;
|
||||
} else {
|
||||
// Don't collapse if the edges separating the faces are not colinear
|
||||
@@ -501,25 +508,18 @@ void Manifold::Impl::CollapseEdge(const int edge, std::vector<int>& edges) {
|
||||
const int tri1 = toRemove.pairedHalfedge / 3;
|
||||
const int triVert0 = (edge + 1) % 3;
|
||||
const int triVert1 = toRemove.pairedHalfedge % 3;
|
||||
const int prop0 = triProp.size() > 0 ? triProp[tri0][edge % 3] : -1;
|
||||
const int prop1 = triProp.size() > 0
|
||||
? triProp[tri1][(toRemove.pairedHalfedge + 1) % 3]
|
||||
: -1;
|
||||
current = start;
|
||||
while (current != tri0edge[2]) {
|
||||
current = NextHalfedge(current);
|
||||
|
||||
// Update the shifted triangles to the vertBary of endVert
|
||||
const int tri = current / 3;
|
||||
const int vIdx = current - 3 * tri;
|
||||
|
||||
if (!shortEdge) {
|
||||
if (triProp.size() > 0) {
|
||||
if (triProp[tri][vIdx] == prop0) {
|
||||
triProp[tri][vIdx] = triProp[tri0][triVert0];
|
||||
} else if (triProp[tri][vIdx] == prop1) {
|
||||
triProp[tri][vIdx] = triProp[tri1][triVert1];
|
||||
}
|
||||
if (triProp.size() > 0) {
|
||||
// Update the shifted triangles to the vertBary of endVert
|
||||
const int tri = current / 3;
|
||||
const int vIdx = current - 3 * tri;
|
||||
if (triRef[tri].SameFace(triRef[tri0])) {
|
||||
triProp[tri][vIdx] = triProp[tri0][triVert0];
|
||||
} else if (triRef[tri].SameFace(triRef[tri1])) {
|
||||
triProp[tri][vIdx] = triProp[tri1][triVert1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,6 +656,7 @@ void Manifold::Impl::RecursiveEdgeSwap(const int edge, int& tag,
|
||||
}
|
||||
|
||||
void Manifold::Impl::SplitPinchedVerts() {
|
||||
ZoneScoped;
|
||||
std::vector<bool> vertProcessed(NumVert(), false);
|
||||
std::vector<bool> halfedgeProcessed(halfedge_.size(), false);
|
||||
for (int i = 0; i < halfedge_.size(); ++i) {
|
||||
@@ -667,14 +668,11 @@ void Manifold::Impl::SplitPinchedVerts() {
|
||||
} else {
|
||||
vertProcessed[vert] = true;
|
||||
}
|
||||
int current = i;
|
||||
do {
|
||||
ForVert(i, [this, &halfedgeProcessed, vert](int current) {
|
||||
halfedgeProcessed[current] = true;
|
||||
halfedge_[current].startVert = vert;
|
||||
current = halfedge_[current].pairedHalfedge;
|
||||
halfedge_[current].endVert = vert;
|
||||
current = NextHalfedge(current);
|
||||
} while (current != i);
|
||||
halfedge_[halfedge_[current].pairedHalfedge].endVert = vert;
|
||||
});
|
||||
}
|
||||
}
|
||||
} // namespace manifold
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#define TBB_PREVIEW_CONCURRENT_ORDERED_CONTAINERS 1
|
||||
#include <tbb/concurrent_map.h>
|
||||
#endif
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "impl.h"
|
||||
#include "polygon.h"
|
||||
@@ -39,6 +39,7 @@ using AddTriangle = std::function<void(int, glm::ivec3, glm::vec3, TriRef)>;
|
||||
*/
|
||||
void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
const Vec<TriRef>& halfedgeRef) {
|
||||
ZoneScoped;
|
||||
Vec<glm::ivec3> triVerts;
|
||||
Vec<glm::vec3> triNormal;
|
||||
Vec<TriRef>& triRef = meshRelation_.triRef;
|
||||
@@ -126,19 +127,21 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
auto generalTriangulation = [&](int face) {
|
||||
const glm::vec3 normal = faceNormal_[face];
|
||||
const glm::mat3x2 projection = GetAxisAlignedProjection(normal);
|
||||
const PolygonsIdx polys = Face2Polygons(face, projection, faceEdge);
|
||||
const PolygonsIdx polys =
|
||||
Face2Polygons(halfedge_.cbegin() + faceEdge[face],
|
||||
halfedge_.cbegin() + faceEdge[face + 1], projection);
|
||||
return TriangulateIdx(polys, precision_);
|
||||
};
|
||||
#if MANIFOLD_PAR == 'T' && __has_include(<tbb/tbb.h>)
|
||||
tbb::task_group group;
|
||||
// map from face to triangle
|
||||
tbb::concurrent_unordered_map<int, std::vector<glm::ivec3>> results;
|
||||
Vec<int> triCount(faceEdge.size());
|
||||
Vec<size_t> triCount(faceEdge.size());
|
||||
triCount.back() = 0;
|
||||
// precompute number of triangles per face, and launch async tasks to
|
||||
// triangulate complex faces
|
||||
for_each(autoPolicy(faceEdge.size()), countAt(0),
|
||||
countAt(faceEdge.size() - 1), [&](int face) {
|
||||
for_each(autoPolicy(faceEdge.size()), countAt(0_z),
|
||||
countAt(faceEdge.size() - 1), [&](size_t face) {
|
||||
triCount[face] = faceEdge[face + 1] - faceEdge[face] - 2;
|
||||
ASSERT(triCount[face] >= 1, topologyErr,
|
||||
"face has less than three edges.");
|
||||
@@ -152,13 +155,15 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
group.wait();
|
||||
// prefix sum computation (assign unique index to each face) and preallocation
|
||||
exclusive_scan(autoPolicy(triCount.size()), triCount.begin(), triCount.end(),
|
||||
triCount.begin(), 0);
|
||||
triCount.begin(), 0_z);
|
||||
if (triCount.back() >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("too many triangles");
|
||||
triVerts.resize(triCount.back());
|
||||
triNormal.resize(triCount.back());
|
||||
triRef.resize(triCount.back());
|
||||
|
||||
auto processFace2 = std::bind(
|
||||
processFace, [&](int face) { return std::move(results[face]); },
|
||||
processFace, [&](size_t face) { return std::move(results[face]); },
|
||||
[&](int face, glm::ivec3 tri, glm::vec3 normal, TriRef r) {
|
||||
triVerts[triCount[face]] = tri;
|
||||
triNormal[triCount[face]] = normal;
|
||||
@@ -167,7 +172,7 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
},
|
||||
std::placeholders::_1);
|
||||
// set triangles in parallel
|
||||
for_each(autoPolicy(faceEdge.size()), countAt(0),
|
||||
for_each(autoPolicy(faceEdge.size()), countAt(0_z),
|
||||
countAt(faceEdge.size() - 1), processFace2);
|
||||
#else
|
||||
triVerts.reserve(faceEdge.size());
|
||||
@@ -191,20 +196,16 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
|
||||
}
|
||||
|
||||
/**
|
||||
* For the input face index, return a set of 2D polygons formed by the input
|
||||
* projection of the vertices.
|
||||
* Returns a set of 2D polygons formed by the input projection of the vertices
|
||||
* of the list of Halfedges, which must be an even-manifold, meaning each vert
|
||||
* must be referenced the same number of times as a startVert and endVert.
|
||||
*/
|
||||
PolygonsIdx Manifold::Impl::Face2Polygons(int face, glm::mat3x2 projection,
|
||||
const Vec<int>& faceEdge) const {
|
||||
const int firstEdge = faceEdge[face];
|
||||
const int lastEdge = faceEdge[face + 1];
|
||||
|
||||
std::map<int, int> vert_edge;
|
||||
for (int edge = firstEdge; edge < lastEdge; ++edge) {
|
||||
const bool inserted =
|
||||
vert_edge.emplace(std::make_pair(halfedge_[edge].startVert, edge))
|
||||
.second;
|
||||
ASSERT(inserted, topologyErr, "face has duplicate vertices.");
|
||||
PolygonsIdx Manifold::Impl::Face2Polygons(VecView<Halfedge>::IterC start,
|
||||
VecView<Halfedge>::IterC end,
|
||||
glm::mat3x2 projection) const {
|
||||
std::multimap<int, int> vert_edge;
|
||||
for (auto edge = start; edge != end; ++edge) {
|
||||
vert_edge.emplace(std::make_pair(edge->startVert, edge - start));
|
||||
}
|
||||
|
||||
PolygonsIdx polys;
|
||||
@@ -217,13 +218,104 @@ PolygonsIdx Manifold::Impl::Face2Polygons(int face, glm::mat3x2 projection,
|
||||
thisEdge = startEdge;
|
||||
polys.push_back({});
|
||||
}
|
||||
int vert = halfedge_[thisEdge].startVert;
|
||||
int vert = (start + thisEdge)->startVert;
|
||||
polys.back().push_back({projection * vertPos_[vert], vert});
|
||||
const auto result = vert_edge.find(halfedge_[thisEdge].endVert);
|
||||
const auto result = vert_edge.find((start + thisEdge)->endVert);
|
||||
ASSERT(result != vert_edge.end(), topologyErr, "non-manifold edge");
|
||||
thisEdge = result->second;
|
||||
vert_edge.erase(result);
|
||||
}
|
||||
return polys;
|
||||
}
|
||||
|
||||
CrossSection Manifold::Impl::Slice(float height) const {
|
||||
Box plane = bBox_;
|
||||
plane.min.z = plane.max.z = height;
|
||||
Vec<Box> query;
|
||||
query.push_back(plane);
|
||||
const SparseIndices collisions =
|
||||
collider_.Collisions<false, false>(query.cview());
|
||||
|
||||
std::unordered_set<int> tris;
|
||||
for (int i = 0; i < collisions.size(); ++i) {
|
||||
const int tri = collisions.Get(i, 1);
|
||||
float min = std::numeric_limits<float>::infinity();
|
||||
float max = -std::numeric_limits<float>::infinity();
|
||||
for (const int j : {0, 1, 2}) {
|
||||
const float z = vertPos_[halfedge_[3 * tri + j].startVert].z;
|
||||
min = glm::min(min, z);
|
||||
max = glm::max(max, z);
|
||||
}
|
||||
|
||||
if (min <= height && max > height) {
|
||||
tris.insert(tri);
|
||||
}
|
||||
}
|
||||
|
||||
Polygons polys;
|
||||
while (!tris.empty()) {
|
||||
const int startTri = *tris.begin();
|
||||
SimplePolygon poly;
|
||||
|
||||
int k = 0;
|
||||
for (const int j : {0, 1, 2}) {
|
||||
if (vertPos_[halfedge_[3 * startTri + j].startVert].z > height &&
|
||||
vertPos_[halfedge_[3 * startTri + Next3(j)].startVert].z <= height) {
|
||||
k = Next3(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int tri = startTri;
|
||||
do {
|
||||
tris.erase(tris.find(tri));
|
||||
if (vertPos_[halfedge_[3 * tri + k].endVert].z <= height) {
|
||||
k = Next3(k);
|
||||
}
|
||||
|
||||
Halfedge up = halfedge_[3 * tri + k];
|
||||
const glm::vec3 below = vertPos_[up.startVert];
|
||||
const glm::vec3 above = vertPos_[up.endVert];
|
||||
const float a = (height - below.z) / (above.z - below.z);
|
||||
poly.push_back(glm::vec2(glm::mix(below, above, a)));
|
||||
|
||||
const int pair = up.pairedHalfedge;
|
||||
tri = pair / 3;
|
||||
k = Next3(pair % 3);
|
||||
} while (tri != startTri);
|
||||
|
||||
polys.push_back(poly);
|
||||
}
|
||||
|
||||
return CrossSection(polys);
|
||||
}
|
||||
|
||||
CrossSection Manifold::Impl::Project() const {
|
||||
const glm::mat3x2 projection = GetAxisAlignedProjection({0, 0, 1});
|
||||
auto policy = autoPolicy(halfedge_.size());
|
||||
|
||||
Vec<Halfedge> cusps(NumEdge());
|
||||
cusps.resize(copy_if<decltype(cusps.begin())>(
|
||||
policy, halfedge_.cbegin(), halfedge_.cend(), cusps.begin(),
|
||||
[&](Halfedge edge) {
|
||||
return faceNormal_[edge.face].z >= 0 &&
|
||||
faceNormal_[halfedge_[edge.pairedHalfedge].face].z <
|
||||
0;
|
||||
}) -
|
||||
cusps.begin());
|
||||
|
||||
PolygonsIdx polysIndexed =
|
||||
Face2Polygons(cusps.cbegin(), cusps.cend(), projection);
|
||||
|
||||
Polygons polys;
|
||||
for (const auto& poly : polysIndexed) {
|
||||
SimplePolygon simple;
|
||||
for (const PolyVert& polyVert : poly) {
|
||||
simple.push_back(polyVert.pos);
|
||||
}
|
||||
polys.push_back(simple);
|
||||
}
|
||||
|
||||
return CrossSection(polys).Simplify(precision_);
|
||||
}
|
||||
} // namespace manifold
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "hashtable.h"
|
||||
#include "mesh_fixes.h"
|
||||
#include "par.h"
|
||||
#include "svd.h"
|
||||
#include "tri_dist.h"
|
||||
|
||||
namespace {
|
||||
using namespace manifold;
|
||||
@@ -320,6 +322,7 @@ int GetLabels(std::vector<int>& components,
|
||||
|
||||
void DedupePropVerts(manifold::Vec<glm::ivec3>& triProp,
|
||||
const Vec<thrust::pair<int, int>>& vert2vert) {
|
||||
ZoneScoped;
|
||||
std::vector<int> vertLabels;
|
||||
const int numLabels = GetLabels(vertLabels, vert2vert, vert2vert.size());
|
||||
|
||||
@@ -349,6 +352,11 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
const int numVert = meshGL.NumVert();
|
||||
const int numTri = meshGL.NumTri();
|
||||
|
||||
if (meshGL.numProp > 3 &&
|
||||
static_cast<size_t>(numVert) * static_cast<size_t>(meshGL.numProp - 3) >=
|
||||
std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
|
||||
if (meshGL.numProp < 3) {
|
||||
MarkFailure(Error::MissingPositionProperties);
|
||||
return;
|
||||
@@ -367,7 +375,8 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
}
|
||||
|
||||
if (!meshGL.runOriginalID.empty() && !meshGL.runIndex.empty() &&
|
||||
meshGL.runOriginalID.size() + 1 != meshGL.runIndex.size()) {
|
||||
meshGL.runOriginalID.size() + 1 != meshGL.runIndex.size() &&
|
||||
meshGL.runOriginalID.size() != meshGL.runIndex.size()) {
|
||||
MarkFailure(Error::RunIndexWrongLength);
|
||||
return;
|
||||
}
|
||||
@@ -379,7 +388,7 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
|
||||
std::vector<int> prop2vert(numVert);
|
||||
std::iota(prop2vert.begin(), prop2vert.end(), 0);
|
||||
for (int i = 0; i < meshGL.mergeFromVert.size(); ++i) {
|
||||
for (size_t i = 0; i < meshGL.mergeFromVert.size(); ++i) {
|
||||
const int from = meshGL.mergeFromVert[i];
|
||||
const int to = meshGL.mergeToVert[i];
|
||||
if (from >= numVert || to >= numVert) {
|
||||
@@ -388,8 +397,8 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
}
|
||||
prop2vert[from] = to;
|
||||
}
|
||||
for (int i = 0; i < numTri; ++i) {
|
||||
for (const int j : {0, 1, 2}) {
|
||||
for (size_t i = 0; i < numTri; ++i) {
|
||||
for (const size_t j : {0, 1, 2}) {
|
||||
const int vert = meshGL.triVerts[3 * i + j];
|
||||
if (vert < 0 || vert >= numVert) {
|
||||
MarkFailure(Error::VertexOutOfBounds);
|
||||
@@ -403,8 +412,8 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
|
||||
if (meshGL.numProp > 3) {
|
||||
relation.triProperties.resize(numTri);
|
||||
for (int i = 0; i < numTri; ++i) {
|
||||
for (const int j : {0, 1, 2}) {
|
||||
for (size_t i = 0; i < numTri; ++i) {
|
||||
for (const size_t j : {0, 1, 2}) {
|
||||
relation.triProperties[i][j] = meshGL.triVerts[3 * i + j];
|
||||
}
|
||||
}
|
||||
@@ -435,8 +444,11 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
|
||||
relation.originalID = Impl::ReserveIDs(1);
|
||||
} else {
|
||||
std::vector<uint32_t> runIndex = meshGL.runIndex;
|
||||
const uint32_t runEnd = meshGL.triVerts.size();
|
||||
if (runIndex.empty()) {
|
||||
runIndex = {0, 3 * meshGL.NumTri()};
|
||||
runIndex = {0, runEnd};
|
||||
} else if (runIndex.size() == meshGL.runOriginalID.size()) {
|
||||
runIndex.push_back(runEnd);
|
||||
}
|
||||
relation.triRef.resize(meshGL.NumTri());
|
||||
const int startID = Impl::ReserveIDs(meshGL.runOriginalID.size());
|
||||
@@ -478,11 +490,13 @@ Manifold::Impl::Impl(const Mesh& mesh, const MeshRelationD& relation,
|
||||
const std::vector<float>& propertyTolerance,
|
||||
bool hasFaceIDs)
|
||||
: vertPos_(mesh.vertPos), halfedgeTangent_(mesh.halfedgeTangent) {
|
||||
if (mesh.triVerts.size() >= std::numeric_limits<int>::max())
|
||||
throw std::out_of_range("mesh too large");
|
||||
meshRelation_ = {relation.originalID, relation.numProp, relation.properties,
|
||||
relation.meshIDtransform};
|
||||
|
||||
Vec<glm::ivec3> triVerts;
|
||||
for (int i = 0; i < mesh.triVerts.size(); ++i) {
|
||||
for (size_t i = 0; i < mesh.triVerts.size(); ++i) {
|
||||
const glm::ivec3 tri = mesh.triVerts[i];
|
||||
// Remove topological degenerates
|
||||
if (tri[0] != tri[1] && tri[1] != tri[2] && tri[2] != tri[0]) {
|
||||
@@ -532,7 +546,7 @@ Manifold::Impl::Impl(const Mesh& mesh, const MeshRelationD& relation,
|
||||
* Create either a unit tetrahedron, cube or octahedron. The cube is in the
|
||||
* first octant, while the others are symmetric about the origin.
|
||||
*/
|
||||
Manifold::Impl::Impl(Shape shape) {
|
||||
Manifold::Impl::Impl(Shape shape, const glm::mat4x3 m) {
|
||||
std::vector<glm::vec3> vertPos;
|
||||
std::vector<glm::ivec3> triVerts;
|
||||
switch (shape) {
|
||||
@@ -545,19 +559,19 @@ Manifold::Impl::Impl(Shape shape) {
|
||||
break;
|
||||
case Shape::Cube:
|
||||
vertPos = {{0.0f, 0.0f, 0.0f}, //
|
||||
{1.0f, 0.0f, 0.0f}, //
|
||||
{1.0f, 1.0f, 0.0f}, //
|
||||
{0.0f, 1.0f, 0.0f}, //
|
||||
{0.0f, 0.0f, 1.0f}, //
|
||||
{0.0f, 1.0f, 0.0f}, //
|
||||
{0.0f, 1.0f, 1.0f}, //
|
||||
{1.0f, 0.0f, 0.0f}, //
|
||||
{1.0f, 0.0f, 1.0f}, //
|
||||
{1.0f, 1.0f, 1.0f}, //
|
||||
{0.0f, 1.0f, 1.0f}};
|
||||
triVerts = {{0, 2, 1}, {0, 3, 2}, //
|
||||
{4, 5, 6}, {4, 6, 7}, //
|
||||
{0, 1, 5}, {0, 5, 4}, //
|
||||
{1, 2, 6}, {1, 6, 5}, //
|
||||
{2, 3, 7}, {2, 7, 6}, //
|
||||
{3, 0, 4}, {3, 4, 7}};
|
||||
{1.0f, 1.0f, 0.0f}, //
|
||||
{1.0f, 1.0f, 1.0f}};
|
||||
triVerts = {{1, 0, 4}, {2, 4, 0}, //
|
||||
{1, 3, 0}, {3, 1, 5}, //
|
||||
{3, 2, 0}, {3, 7, 2}, //
|
||||
{5, 4, 6}, {5, 1, 4}, //
|
||||
{6, 4, 2}, {7, 6, 2}, //
|
||||
{7, 3, 5}, {7, 5, 6}};
|
||||
break;
|
||||
case Shape::Octahedron:
|
||||
vertPos = {{1.0f, 0.0f, 0.0f}, //
|
||||
@@ -573,6 +587,7 @@ Manifold::Impl::Impl(Shape shape) {
|
||||
break;
|
||||
}
|
||||
vertPos_ = vertPos;
|
||||
for (auto& v : vertPos_) v = m * glm::vec4(v, 1.0f);
|
||||
CreateHalfedges(triVerts);
|
||||
Finish();
|
||||
meshRelation_.originalID = ReserveIDs(1);
|
||||
@@ -581,6 +596,7 @@ Manifold::Impl::Impl(Shape shape) {
|
||||
}
|
||||
|
||||
void Manifold::Impl::RemoveUnreferencedVerts(Vec<glm::ivec3>& triVerts) {
|
||||
ZoneScoped;
|
||||
Vec<int> vertOld2New(NumVert() + 1, 0);
|
||||
auto policy = autoPolicy(NumVert());
|
||||
for_each(policy, triVerts.cbegin(), triVerts.cend(),
|
||||
@@ -613,6 +629,7 @@ void Manifold::Impl::InitializeOriginal() {
|
||||
}
|
||||
|
||||
void Manifold::Impl::CreateFaces(const std::vector<float>& propertyTolerance) {
|
||||
ZoneScoped;
|
||||
Vec<float> propertyToleranceD =
|
||||
propertyTolerance.empty() ? Vec<float>(meshRelation_.numProp, kTolerance)
|
||||
: propertyTolerance;
|
||||
@@ -661,6 +678,7 @@ void Manifold::Impl::CreateFaces(const std::vector<float>& propertyTolerance) {
|
||||
* Create the halfedge_ data structure from an input triVerts array like Mesh.
|
||||
*/
|
||||
void Manifold::Impl::CreateHalfedges(const Vec<glm::ivec3>& triVerts) {
|
||||
ZoneScoped;
|
||||
const int numTri = triVerts.size();
|
||||
const int numHalfedge = 3 * numTri;
|
||||
// drop the old value first to avoid copy
|
||||
@@ -714,7 +732,14 @@ void Manifold::Impl::MarkFailure(Error status) {
|
||||
}
|
||||
|
||||
void Manifold::Impl::Warp(std::function<void(glm::vec3&)> warpFunc) {
|
||||
thrust::for_each_n(thrust::host, vertPos_.begin(), NumVert(), warpFunc);
|
||||
WarpBatch([&warpFunc](VecView<glm::vec3> vecs) {
|
||||
thrust::for_each(thrust::host, vecs.begin(), vecs.end(), warpFunc);
|
||||
});
|
||||
}
|
||||
|
||||
void Manifold::Impl::WarpBatch(
|
||||
std::function<void(VecView<glm::vec3>)> warpFunc) {
|
||||
warpFunc(vertPos_.view());
|
||||
CalculateBBox();
|
||||
if (!IsFinite()) {
|
||||
MarkFailure(Error::NonFiniteVertex);
|
||||
@@ -725,11 +750,11 @@ void Manifold::Impl::Warp(std::function<void(glm::vec3&)> warpFunc) {
|
||||
CalculateNormals();
|
||||
SetPrecision();
|
||||
CreateFaces();
|
||||
SimplifyTopology();
|
||||
Finish();
|
||||
}
|
||||
|
||||
Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3& transform_) const {
|
||||
ZoneScoped;
|
||||
if (transform_ == glm::mat4x3(1.0f)) return *this;
|
||||
auto policy = autoPolicy(NumVert());
|
||||
Impl result;
|
||||
@@ -776,11 +801,8 @@ Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3& transform_) const {
|
||||
if (!result.collider_.Transform(transform_)) result.Update();
|
||||
|
||||
result.CalculateBBox();
|
||||
float scale = 0;
|
||||
for (int i : {0, 1, 2})
|
||||
scale =
|
||||
glm::max(scale, transform_[0][i] + transform_[1][i] + transform_[2][i]);
|
||||
result.precision_ *= scale;
|
||||
// Scale the precision by the norm of the 3x3 portion of the transform.
|
||||
result.precision_ *= SpectralNorm(glm::mat3(transform_));
|
||||
// Maximum of inherited precision loss and translational precision loss.
|
||||
result.SetPrecision(result.precision_);
|
||||
return result;
|
||||
@@ -807,6 +829,7 @@ void Manifold::Impl::SetPrecision(float minPrecision) {
|
||||
* recalculation.
|
||||
*/
|
||||
void Manifold::Impl::CalculateNormals() {
|
||||
ZoneScoped;
|
||||
vertNormal_.resize(NumVert());
|
||||
auto policy = autoPolicy(NumTri());
|
||||
fill(policy, vertNormal_.begin(), vertNormal_.end(), glm::vec3(0));
|
||||
@@ -849,6 +872,7 @@ void Manifold::Impl::IncrementMeshIDs() {
|
||||
*/
|
||||
SparseIndices Manifold::Impl::EdgeCollisions(const Impl& Q,
|
||||
bool inverted) const {
|
||||
ZoneScoped;
|
||||
Vec<TmpEdge> edges = CreateTmpEdges(Q.halfedge_);
|
||||
const int numEdge = edges.size();
|
||||
Vec<Box> QedgeBB(numEdge);
|
||||
@@ -863,10 +887,10 @@ SparseIndices Manifold::Impl::EdgeCollisions(const Impl& Q,
|
||||
q1p2 = collider_.Collisions<false, false>(QedgeBB.cview());
|
||||
|
||||
if (inverted)
|
||||
for_each(policy, countAt(0), countAt(q1p2.size()),
|
||||
for_each(policy, countAt(0_z), countAt(q1p2.size()),
|
||||
ReindexEdge<true>({edges, q1p2}));
|
||||
else
|
||||
for_each(policy, countAt(0), countAt(q1p2.size()),
|
||||
for_each(policy, countAt(0_z), countAt(q1p2.size()),
|
||||
ReindexEdge<false>({edges, q1p2}));
|
||||
return q1p2;
|
||||
}
|
||||
@@ -877,9 +901,11 @@ SparseIndices Manifold::Impl::EdgeCollisions(const Impl& Q,
|
||||
*/
|
||||
SparseIndices Manifold::Impl::VertexCollisionsZ(
|
||||
VecView<const glm::vec3> vertsIn, bool inverted) const {
|
||||
ZoneScoped;
|
||||
if (inverted)
|
||||
return collider_.Collisions<false, true>(vertsIn);
|
||||
else
|
||||
return collider_.Collisions<false, false>(vertsIn);
|
||||
}
|
||||
|
||||
} // namespace manifold
|
||||
|
||||
@@ -42,6 +42,9 @@ struct Manifold::Impl {
|
||||
Vec<TriRef> triRef;
|
||||
Vec<glm::ivec3> triProperties;
|
||||
};
|
||||
struct BaryIndices {
|
||||
int tri, start4, end4;
|
||||
};
|
||||
|
||||
Box bBox_;
|
||||
float precision_ = -1;
|
||||
@@ -59,13 +62,36 @@ struct Manifold::Impl {
|
||||
|
||||
Impl() {}
|
||||
enum class Shape { Tetrahedron, Cube, Octahedron };
|
||||
Impl(Shape);
|
||||
Impl(Shape, const glm::mat4x3 = glm::mat4x3(1));
|
||||
|
||||
Impl(const MeshGL&, std::vector<float> propertyTolerance = {});
|
||||
Impl(const Mesh&, const MeshRelationD& relation,
|
||||
const std::vector<float>& propertyTolerance = {},
|
||||
bool hasFaceIDs = false);
|
||||
|
||||
inline void ForVert(int halfedge, std::function<void(int halfedge)> func) {
|
||||
int current = halfedge;
|
||||
do {
|
||||
current = NextHalfedge(halfedge_[current].pairedHalfedge);
|
||||
func(current);
|
||||
} while (current != halfedge);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void ForVert(
|
||||
int halfedge, std::function<T(int halfedge)> transform,
|
||||
std::function<void(int halfedge, const T& here, T& next)> binaryOp) {
|
||||
T here = transform(halfedge);
|
||||
int current = halfedge;
|
||||
do {
|
||||
const int nextHalfedge = NextHalfedge(halfedge_[current].pairedHalfedge);
|
||||
T next = transform(nextHalfedge);
|
||||
binaryOp(current, here, next);
|
||||
here = next;
|
||||
current = nextHalfedge;
|
||||
} while (current != halfedge);
|
||||
}
|
||||
|
||||
void CreateFaces(const std::vector<float>& propertyTolerance = {});
|
||||
void RemoveUnreferencedVerts(Vec<glm::ivec3>& triVerts);
|
||||
void InitializeOriginal();
|
||||
@@ -76,6 +102,7 @@ struct Manifold::Impl {
|
||||
void Update();
|
||||
void MarkFailure(Error status);
|
||||
void Warp(std::function<void(glm::vec3&)> warpFunc);
|
||||
void WarpBatch(std::function<void(VecView<glm::vec3>)> warpFunc);
|
||||
Impl Transform(const glm::mat4x3& transform) const;
|
||||
SparseIndices EdgeCollisions(const Impl& B, bool inverted = false) const;
|
||||
SparseIndices VertexCollisionsZ(VecView<const glm::vec3> vertsIn,
|
||||
@@ -102,6 +129,7 @@ struct Manifold::Impl {
|
||||
bool Is2Manifold() const;
|
||||
bool MatchesTriNormals() const;
|
||||
int NumDegenerateTris() const;
|
||||
float MinGap(const Impl& other, float searchLength) const;
|
||||
|
||||
// sort.cu
|
||||
void Finish();
|
||||
@@ -115,8 +143,11 @@ struct Manifold::Impl {
|
||||
|
||||
// face_op.cu
|
||||
void Face2Tri(const Vec<int>& faceEdge, const Vec<TriRef>& halfedgeRef);
|
||||
PolygonsIdx Face2Polygons(int face, glm::mat3x2 projection,
|
||||
const Vec<int>& faceEdge) const;
|
||||
PolygonsIdx Face2Polygons(VecView<Halfedge>::IterC start,
|
||||
VecView<Halfedge>::IterC end,
|
||||
glm::mat3x2 projection) const;
|
||||
CrossSection Slice(float height) const;
|
||||
CrossSection Project() const;
|
||||
|
||||
// edge_op.cu
|
||||
void SimplifyTopology();
|
||||
@@ -132,9 +163,31 @@ struct Manifold::Impl {
|
||||
void CollapseTri(const glm::ivec3& triEdge);
|
||||
void SplitPinchedVerts();
|
||||
|
||||
// smoothing.cu
|
||||
void CreateTangents(const std::vector<Smoothness>&);
|
||||
Vec<Barycentric> Subdivide(int n);
|
||||
void Refine(int n);
|
||||
// subdivision.cpp
|
||||
int GetNeighbor(int tri) const;
|
||||
glm::ivec4 GetHalfedges(int tri) const;
|
||||
BaryIndices GetIndices(int halfedge) const;
|
||||
void FillRetainedVerts(Vec<Barycentric>& vertBary) const;
|
||||
Vec<Barycentric> Subdivide(std::function<int(glm::vec3)>);
|
||||
|
||||
// smoothing.cpp
|
||||
bool IsInsideQuad(int halfedge) const;
|
||||
bool IsMarkedInsideQuad(int halfedge) const;
|
||||
glm::vec3 GetNormal(int halfedge, int normalIdx) const;
|
||||
glm::vec4 TangentFromNormal(const glm::vec3& normal, int halfedge) const;
|
||||
std::vector<Smoothness> UpdateSharpenedEdges(
|
||||
const std::vector<Smoothness>&) const;
|
||||
Vec<bool> FlatFaces() const;
|
||||
Vec<int> VertFlatFace(const Vec<bool>&) const;
|
||||
Vec<int> VertHalfedge() const;
|
||||
std::vector<Smoothness> SharpenEdges(float minSharpAngle,
|
||||
float minSmoothness) const;
|
||||
void SharpenTangent(int halfedge, float smoothness);
|
||||
void SetNormals(int normalIdx, float minSharpAngle);
|
||||
void LinearizeFlatTangents();
|
||||
void DistributeTangents(const Vec<bool>& fixedHalfedges);
|
||||
void CreateTangents(int normalIdx);
|
||||
void CreateTangents(std::vector<Smoothness>);
|
||||
void Refine(std::function<int(glm::vec3)>);
|
||||
};
|
||||
} // namespace manifold
|
||||
|
||||