Compare commits

..
1 Commits
Author SHA1 Message Date
pca006132 324ff3f065 push to pypi 2023-10-30 17:57:34 +08:00
150 changed files with 11679 additions and 82022 deletions
+12 -15
View File
@@ -1,9 +1,6 @@
name: build_wheels
on:
release:
types: [published]
workflow_dispatch:
on: [release, workflow_dispatch]
jobs:
build_wheels:
@@ -16,14 +13,13 @@ jobs:
- run: |
git config --global submodule.fetchJobs 8
git config --global core.longpaths true
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build wheels
uses: pypa/cibuildwheel@v2.16
- uses: actions/upload-artifact@v4
uses: pypa/cibuildwheel@v2.15.0
- uses: actions/upload-artifact@v3
with:
name: python-${{ matrix.os }}
path: ./wheelhouse/*.whl
make_sdist:
@@ -32,15 +28,17 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0 # Optional, use if you use setuptools_scm
submodules: true # Optional, use if you have submodules
- name: Build SDist
run: pipx run build --sdist
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: python-sdist
path: dist/*.tar.gz
pypi-publish:
pypi-publish-beta:
name: Upload release to PyPI
needs: [build_wheels, make_sdist]
runs-on: ubuntu-latest
@@ -50,10 +48,9 @@ jobs:
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
pattern: python-*
merge-multiple: true
name: artifact
path: dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+4 -7
View File
@@ -11,14 +11,11 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: DoozyX/clang-format-lint-action@v0.17
- uses: actions/checkout@v3
- uses: DoozyX/clang-format-lint-action@v0.14
with:
source: '.'
exclude: '*/third_party'
extensions: 'h,cpp,js,ts,html'
clangFormatVersion: 11
- uses: psf/black@stable
with:
options: "--check --verbose"
src: "./bindings/python/examples"
clangFormatVersion: 12
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- 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@v3
uses: dawidd6/action-download-artifact@v2
with:
workflow: manifold.yml
workflow_conclusion: completed
+30 -61
View File
@@ -16,31 +16,24 @@ jobs:
strategy:
matrix:
parallel_backend: [NONE, TBB]
gcc: [13, 14]
runs-on: ubuntu-24.04
env:
CC: gcc-${{ matrix.gcc }}
CXX: g++-${{ matrix.gcc }}
runs-on: ubuntu-22.04
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 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
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
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v2
- uses: jwlawson/actions-setup-cmake@v1.12
- name: Build ${{matrix.backend}}
if: matrix.parallel_backend != 'NONE'
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=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_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
- name: Test ${{matrix.parallel_backend}}
if: matrix.parallel_backend != 'NONE'
run: |
@@ -49,7 +42,6 @@ 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
@@ -59,26 +51,19 @@ 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 --gcov-tool gcov-${{ matrix.gcc }} --ignore-errors mismatch --initial --directory . --output-file ./code_coverage_init.info
lcov --capture --initial --directory . --output-file ./code_coverage_init.info
cd test
./manifold_test
cd ../
lcov --capture --gcov-tool gcov-${{ matrix.gcc }} --ignore-errors mismatch --directory . --output-file ./code_coverage_test.info
lcov --capture --directory . --output-file ./code_coverage_test.info
lcov --add-tracefile ./code_coverage_init.info --add-tracefile ./code_coverage_test.info --output-file ./code_coverage_total.info
lcov --remove ./code_coverage_total.info '/usr/*' '*/third_party/*' '*/test/*' '*/extras/*' '*/bindings/*' --output-file ./code_coverage.info
cd ../
- uses: codecov/codecov-action@v4
- uses: codecov/codecov-action@v2
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
@@ -89,15 +74,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@v4
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v2
- uses: jwlawson/actions-setup-cmake@v1.12
- name: Build C bindings with TBB
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=TBB .. && make
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=TBB .. && make
- name: Test ${{matrix.parallel_backend}}
run: |
cd build/test
@@ -112,7 +97,7 @@ jobs:
run: |
sudo apt-get -y update
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs libglm-dev
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup WASM
@@ -120,9 +105,9 @@ jobs:
# setup emscripten
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install 3.1.45
./emsdk activate 3.1.45
- uses: jwlawson/actions-setup-cmake@v2
./emsdk install latest
./emsdk activate latest
- uses: jwlawson/actions-setup-cmake@v1.12
- name: Build WASM
run: |
source ./emsdk/emsdk_env.sh
@@ -139,14 +124,12 @@ jobs:
npm ci
npm run build
npm test
cp ../manifold.* ./dist/
- name: Upload WASM files
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: wasm
path: bindings/wasm/examples/dist/
retention-days: 90
overwrite: true
build_windows:
timeout-minutes: 30
@@ -157,29 +140,22 @@ jobs:
runs-on: windows-2019
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v2
- uses: jwlawson/actions-setup-cmake@v1.12
- uses: ilammy/msvc-dev-cmd@v1
- name: Build ${{matrix.backend}}
shell: powershell
run: |
cmake . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64 -B build
cmake . -DCMAKE_BUILD_TYPE=Release -B build -DMANIFOLD_DEBUG=ON -DPYBIND11_FINDPYTHON=OFF -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64
cd build
cmake --build . --target ALL_BUILD --config Release
- name: Test ${{matrix.parallel_backend}}
shell: bash
run: |
cd build/bin/Release
./manifold_test.exe
cd ../../
- name: test cmake consumer
run: |
cd build
cmake --install .
cd ..
./test-cmake.sh
cd build/test
./Release/manifold_test.exe
build_mxe:
timeout-minutes: 30
@@ -190,7 +166,7 @@ jobs:
runs-on: ubuntu-latest
container: openscad/mxe-x86_64-gui:latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build
@@ -214,30 +190,23 @@ jobs:
- name: Install common dependencies
run: |
brew install pkg-config googletest assimp
pip install trimesh pytest
pip install trimesh
- name: Install TBB
if: matrix.parallel_backend == 'TBB'
run: brew install tbb
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v2
- uses: jwlawson/actions-setup-cmake@v1.12
- name: Build
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=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
@@ -247,7 +216,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: cachix/install-nix-action@v22
@@ -258,7 +227,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: cachix/install-nix-action@v22
-34
View File
@@ -1,34 +0,0 @@
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}}
-31
View File
@@ -1,31 +0,0 @@
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}}
+3
View File
@@ -1,3 +1,6 @@
[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
+2 -4
View File
@@ -7,12 +7,11 @@
{
"name": "manifold test",
"type": "cppdbg",
"MIMode": "lldb",
"request": "launch",
"program": "${workspaceFolder}/build/test/manifold_test",
"args": [
"--gtest_break_on_failure",
"--gtest_filter=Smooth.TruncatedCone"
"--gtest_filter=Manifold.GetMesh"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build/test",
@@ -21,8 +20,7 @@
"name": "MALLOC_CHECK_",
"value": "2"
}
],
"externalConsole": false
]
}
]
}
+2 -7
View File
@@ -108,21 +108,16 @@
"latch": "cpp",
"semaphore": "cpp",
"span": "cpp",
"__sso_allocator": "cpp",
"source_location": "cpp",
"format": "cpp",
"numbers": "cpp",
"ranges": "cpp",
"stop_token": "cpp"
"__sso_allocator": "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'"
+8 -62
View File
@@ -21,26 +21,11 @@ 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" 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(MANIFOLD_PYBIND "Build python bindings" ON)
option(MANIFOLD_CBIND "Build C (FFI) bindings" OFF)
option(MANIFOLD_JSBIND "Build js binding" ${EMSCRIPTEN})
option(TRACY_ENABLE "Use tracy profiling" OFF)
option(TRACY_MEMORY_USAGE "Track memory allocation with tracy (expensive)" OFF)
@@ -62,18 +47,15 @@ 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)
@@ -83,15 +65,14 @@ if(CMAKE_EXPORT_COMPILE_COMMANDS AND NOT EMSCRIPTEN)
endif()
if (MSVC)
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} /DNOMINMAX /bigobj)
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} /DNOMINMAX)
else()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused -Wno-array-bounds
-Wno-stringop-overflow -Wno-alloc-size-larger-than)
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused -Wno-array-bounds -Wno-stringop-overflow)
else()
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
endif()
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${WARNING_FLAGS})
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${WARNING_FLAGS} -O3)
endif()
if(CODE_COVERAGE AND NOT MSVC)
@@ -100,16 +81,7 @@ if(CODE_COVERAGE AND NOT MSVC)
add_link_options("-coverage")
endif()
# installation rpath
set(CMAKE_SKIP_BUILD_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/lib" isSystemDir)
if("${isSystemDir}" STREQUAL "-1")
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
endif("${isSystemDir}" STREQUAL "-1")
include(GNUInstallDirs)
add_subdirectory(src)
add_subdirectory(bindings)
@@ -123,29 +95,3 @@ 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 0)
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()
+16 -58
View File
@@ -1,36 +1,35 @@
[![codecov](https://codecov.io/github/elalish/manifold/branch/master/graph/badge.svg?token=IIA8G5HVS7)](https://codecov.io/github/elalish/manifold)
[![PyPI version](https://badge.fury.io/py/manifold3d.svg)](https://badge.fury.io/py/manifold3d)
[![npm version](https://badge.fury.io/js/manifold-3d.svg)](https://badge.fury.io/js/manifold-3d)
[![twitter](https://img.shields.io/twitter/follow/manifoldcad?style=social&logo=twitter)](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.
## Manifold Frontend Sandboxes
For example, here is a log-log plot of Manifold's performance vs. earlier OpenSCAD geometry backends:
### [ManifoldCAD.org](https://manifoldcad.org)
<img src="https://elalish.github.io/manifold/samples/models/perfSpheres.png" width="350px"/>
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.
## [ManifoldCAD.org](https://manifoldcad.org)
*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.
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.
![A metallic Menger sponge](https://elalish.github.io/manifold/samples/models/mengerSponge3.webp "A metallic Menger sponge")
### 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/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)
[**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)
[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 ([manifold3d](https://pypi.org/project/manifold3d/)), 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, 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.
@@ -88,10 +87,6 @@ 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):
@@ -125,7 +120,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 pytest
pip install trimesh
python3 run_all.py -e
```
@@ -139,51 +134,14 @@ 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!
### 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
## 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
-13
View File
@@ -1,13 +0,0 @@
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.
+19 -17
View File
@@ -1,31 +1,33 @@
project(manifoldc)
# this is a shared library for FFI bindings
add_library(${PROJECT_NAME} SHARED manifoldc.cpp conv.cpp)
add_library(${PROJECT_NAME} manifoldc.cpp conv.cpp)
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 )
if(MANIFOLD_USE_CUDA)
set_source_files_properties(manifoldc.cpp conv.cpp PROPERTIES LANGUAGE CUDA)
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
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
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
install(FILES include/manifoldc.h include/types.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
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
)
+3 -7
View File
@@ -182,13 +182,9 @@ ManifoldCrossSection *manifold_cross_section_transform(void *mem,
}
ManifoldCrossSection *manifold_cross_section_warp(
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));
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));
};
auto warped = from_c(cs)->Warp(warp);
return to_c(new (mem) CrossSection(warped));
+15 -31
View File
@@ -51,7 +51,6 @@ 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
@@ -60,11 +59,20 @@ ManifoldMeshGL *manifold_meshgl_merge(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, void *),
ManifoldMeshGL *manifold_level_set(void *mem, float (*sdf)(float, float, float),
ManifoldBox *bounds, float edge_length,
float level, void *ctx);
ManifoldMeshGL *manifold_level_set_seq(
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(
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
float edge_length, float level, void *ctx);
@@ -103,12 +111,6 @@ 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);
@@ -130,16 +132,8 @@ 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,
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);
ManifoldVec3 (*fun)(float, float, float));
ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine);
ManifoldManifold *manifold_refine_to_length(void *mem, ManifoldManifold *m,
float length);
// Manifold Shapes / Constructors
@@ -183,16 +177,9 @@ 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 *ctx),
void *ctx);
void (*fun)(float *new_prop, ManifoldVec3 position, const float *old_prop));
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
@@ -275,9 +262,6 @@ 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);
+39 -78
View File
@@ -35,10 +35,20 @@
using namespace manifold;
namespace {
ManifoldMeshGL *level_set(void *mem,
float (*sdf_context)(float, float, float, void *),
ManifoldMeshGL *level_set(void *mem, float (*sdf)(float, float, float),
ManifoldBox *bounds, float edge_length, float level,
bool seq, void *ctx) {
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) {
// Bind function with context argument to one without
using namespace std::placeholders;
std::function<float(float, float, float)> sdf =
@@ -192,17 +202,6 @@ 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));
@@ -259,44 +258,37 @@ ManifoldManifold *manifold_mirror(void *mem, ManifoldManifold *m, float nx,
}
ManifoldManifold *manifold_warp(void *mem, ManifoldManifold *m,
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));
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));
};
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, void *),
ManifoldMeshGL *manifold_level_set(void *mem, float (*sdf)(float, float, float),
ManifoldBox *bounds, float edge_length,
float level, void *ctx) {
return level_set(mem, sdf, bounds, edge_length, level, false, ctx);
float level) {
return level_set(mem, sdf, bounds, edge_length, level, false);
}
ManifoldMeshGL *manifold_level_set_seq(
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(
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
float edge_length, float level, void *ctx) {
return level_set(mem, sdf, bounds, edge_length, level, true, ctx);
return level_set_context(mem, sdf, bounds, edge_length, level, false, 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));
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_refine(void *mem, ManifoldManifold *m, int refine) {
@@ -304,12 +296,6 @@ 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());
}
@@ -417,15 +403,6 @@ 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(); }
@@ -532,18 +509,14 @@ 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,
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);
ManifoldManifold *manifold_set_properties(void *mem, ManifoldManifold *m,
int num_prop,
void (*fun)(float *new_prop,
ManifoldVec3 position,
const float *old_prop)) {
std::function<void(float *, glm::vec3, const float *)> f =
[f3](float *new_prop, glm::vec3 v, const float *old_prop) {
return (f3(new_prop, to_c(v), old_prop));
[fun](float *new_prop, glm::vec3 v, const float *old_prop) {
fun(new_prop, to_c(v), old_prop);
};
auto man = from_c(m)->SetProperties(num_prop, f);
return to_c(new (mem) Manifold(man));
@@ -555,18 +528,6 @@ 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) {
+2 -21
View File
@@ -18,31 +18,12 @@ add_subdirectory(third_party)
nanobind_add_module(
manifold3d
NB_STATIC STABLE_ABI LTO
autogen_docstrings.inl
manifold3d.cpp)
target_link_libraries(manifold3d PRIVATE manifold sdf polygon)
target_link_libraries(manifold3d PRIVATE manifold)
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
@@ -52,7 +33,7 @@ install(
else()
install(
TARGETS manifold3d
LIBRARY DESTINATION ${Python_SITEARCH}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT bindings
)
endif()
-40
View File
@@ -1,40 +0,0 @@
# 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.
-128
View File
@@ -1,128 +0,0 @@
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()
+30 -52
View File
@@ -20,82 +20,60 @@ 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 sum(bricks, Manifold())
bricks = [brick().translate((brick_length + mortar_gap)*x, 0, 0)
for x in range(length)]
return Manifold(bricks)
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 sum(bricks, Manifold())
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)
def walls(length, width, height):
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(),
)
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),
])
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 sum(results, Manifold())
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)
def run(width=10, length=10, height=10):
return walls(length, width, height) + floor(length, width)
+7 -10
View File
@@ -13,21 +13,18 @@
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)
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
return a - sum(spheres, Manifold())
+10 -18
View File
@@ -1,4 +1,4 @@
from manifold3d import CrossSection, Manifold
from manifold3d import CrossSection
def run():
@@ -10,34 +10,26 @@ def run():
cross_section = CrossSection(polygons_points)
polygons = cross_section.to_polygons()
polygon = polygons[0]
if set([tuple(p) for p in polygon]) != set(polygon_points):
raise Exception(
f"polygon={polygon} differs from polygon_points={polygon_points}"
)
if set(polygon) != set(polygon_points):
raise Exception(f"polygon={polygon} differs from polygon_points={polygon_points}")
# extrude a polygon to create a manifold
extruded_polygon = Manifold.extrude(cross_section, 10.0)
extruded_polygon = cross_section.extrude(10.0)
eps = 0.001
observed_volume = extruded_polygon.volume()
observed_volume = extruded_polygon.get_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.surface_area()
raise Exception(f"observed_volume={observed_volume} differs from expected_volume={expected_volume}")
observed_surface_area = extruded_polygon.get_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
-59
View File
@@ -1,59 +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
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)
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -24,9 +24,10 @@ 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)
@@ -40,10 +41,9 @@ 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}')
-81
View File
@@ -1,81 +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
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)
)
-23
View File
@@ -1,23 +0,0 @@
"""
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]
+14 -17
View File
@@ -4,27 +4,26 @@ 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 [-p + 0.5 for p in pos] + [1.0]
return [1 - p / 2 for p in pos] + [1.0]
def run(n=1):
@@ -38,6 +37,4 @@ 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] * 3)
)
return result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale(100)
@@ -1,98 +0,0 @@
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 -2
View File
@@ -1,8 +1,7 @@
from manifold3d import Manifold
def run():
# for some reason this causes collider error
obj = Manifold.cube()
obj += Manifold.cube().rotate([0, 0, 45 + 180])
obj += Manifold([Manifold.cube()]).rotate(0, 0, 45+180)
return obj
-109
View File
@@ -1,109 +0,0 @@
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")
File diff suppressed because it is too large Load Diff
-2
View File
@@ -1,2 +0,0 @@
manifold.js
manifold.wasm
+1 -9
View File
@@ -32,21 +32,13 @@ 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})
# 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}/manifold*.d.ts
${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
+1 -1
View File
@@ -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_mesh_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_manifold](https://github.com/KhronosGroup/glTF/pull/2286).
## About the author
-8
View File
@@ -138,14 +138,9 @@ 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)
@@ -166,9 +161,7 @@ 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);
@@ -198,5 +191,4 @@ EMSCRIPTEN_BINDINGS(whatever) {
function("setMinCircularEdgeLength", &Quality::SetMinCircularEdgeLength);
function("setCircularSegments", &Quality::SetCircularSegments);
function("getCircularSegments", &Quality::GetCircularSegments);
function("resetToCircularDefaults", &Quality::ResetToDefaults);
}
+4 -8
View File
@@ -248,12 +248,8 @@ Module.setup = function() {
return this._Translate(vararg2vec3(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.rotate = function(vec) {
return this._Rotate(...vec);
};
Module.Manifold.prototype.scale = function(vec) {
@@ -273,14 +269,14 @@ Module.setup = function() {
};
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;
+6 -102
View File
@@ -65,7 +65,7 @@ button:hover {
filter: brightness(0.9);
}
.name {
input {
border: none;
height: 36px;
background-color: transparent;
@@ -77,12 +77,12 @@ button:hover {
-webkit-font-smoothing: antialiased;
}
.name:focus {
input:focus {
outline: none;
}
p {
margin: 18px;
margin: 1em;
text-align: left;
}
@@ -130,7 +130,7 @@ model-viewer {
.header .margin {
font-size: 16px;
margin: 18px 8px;
margin: 1em 0.5em;
}
.dropdown {
@@ -168,29 +168,6 @@ 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;
@@ -203,22 +180,6 @@ 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;
@@ -235,22 +196,10 @@ 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);
}
@@ -272,52 +221,7 @@ model-viewer {
width: 125px;
}
#animation {
display: none;
align-items: center;
position: absolute;
width: 100%;
bottom: 0px;
margin: 2px;
}
#play {
position: relative;
background-color: #ccc;
margin: 8px;
}
.slider {
position: relative;
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 10px;
border-radius: 5px;
margin-right: 16px;
background: #ffffff55;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
border-radius: 50%;
background: #ccc;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 25px;
height: 25px;
border-radius: 50%;
background: #ccc;
cursor: pointer;
}
@media only screen and (max-width: 820px) {
@media only screen and (max-width: 600px) {
.container {
flex-direction: column;
width: 100%;
@@ -352,4 +256,4 @@ model-viewer {
width: calc(50% - 16px);
margin: 8px;
}
}
}
+11 -99
View File
@@ -12,9 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import ManifoldWorker from './worker-wrapper?worker';
import ManifoldWorker from './worker?worker';
const CODE_START = '<code>';
// Loaded globally by examples.js
const exampleFunctions = self.examples.functionBodies;
@@ -25,28 +24,6 @@ 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');
@@ -113,10 +90,9 @@ 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);
}
}
@@ -170,7 +146,6 @@ 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);
@@ -228,16 +203,16 @@ function addEdit(button) {
}
const newButton = document.querySelector('#new');
function newItem(code, scriptName = undefined) {
const name = uniqueName(scriptName ?? 'New Script');
function newItem(code) {
const name = uniqueName('New Script');
setScript(name, code);
const nextButton = createDropdownItem(name);
newButton.insertAdjacentElement('afterend', nextButton.parentElement);
addEdit(nextButton);
return {button: nextButton, name};
nextButton.click();
};
newButton.onclick = function() {
newItem('').button.click();
newItem('');
};
const runButton = document.querySelector('#compile');
@@ -250,7 +225,7 @@ function initializeRun() {
if (autoExecute) {
runButton.click();
} else {
poster.textContent = 'Auto-run disabled';
poster.textContent = 'Auto-run disabled due to prior failure';
}
}
@@ -276,7 +251,6 @@ 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;
@@ -324,24 +298,7 @@ require(['vs/editor/editor.main'], async function() {
addEdit(button);
}
}
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);
}
switchTo(currentName);
if (manifoldInitialized) {
initializeRun();
@@ -351,12 +308,11 @@ 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()).button.click();
newItem(editor.getValue());
editor.setPosition(cursor);
}
});
@@ -366,51 +322,6 @@ 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;
@@ -446,6 +357,7 @@ function finishRun() {
`Took ${(Math.round((t1 - t0) / 10) / 100).toLocaleString()} seconds`);
}
const mv = document.querySelector('model-viewer');
const output = {
glbURL: null,
threeMFURL: null
+285
View File
@@ -0,0 +1,285 @@
// 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()));
}
-424
View File
@@ -1,424 +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, 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;
}
+5 -16
View File
@@ -39,15 +39,8 @@
<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
&nbsp;&nbsp;<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">
<p style="flex: 2;"><a target="_blank" href="https://github.com/elalish/manifold">Manifold</a>CAD </p>
<div class="margin" id="file-margin" >
<button type="button" id="file" class="blue item"><b class="uparrow"></b>&nbsp;&nbsp;<span
id="current">Intro</span></button>
<div class="dropdown" id="fileDropdown">
@@ -73,14 +66,10 @@
<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" tone-mapping="neutral" interaction-prompt="none" alt="Editor 3D output">
shadow-intensity="1" 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>
@@ -95,6 +84,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.5.0/model-viewer.min.js"></script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.1.1/model-viewer.min.js"></script>
</html>
</html>
+128 -4
View File
@@ -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_mesh_manifold</a> extension, thus preserving the
our <a href="https://github.com/KhronosGroup/glTF/pull/2286">EXT_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,11 +55,135 @@
<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" tone-mapping="neutral" alt="Loaded GLB model">
<model-viewer camera-controls shadow-intensity="1" 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/3.5.0/model-viewer.min.js"></script>
<script type="module" src="make-manifold.ts"></script>
<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>
</html>
-149
View File
@@ -1,149 +0,0 @@
// 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,32 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Accessor, Extension, ExtensionProperty, GLTF, IProperty, PropertyType, ReaderContext, WriterContext} from '@gltf-transform/core';
import {Extension, ExtensionProperty, PropertyType} from '@gltf-transform/core';
const NAME = 'EXT_mesh_manifold';
const MERGE = 'MERGE';
const NAME = 'EXT_manifold';
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> {
export class ManifoldPrimitive extends ExtensionProperty {
static EXTENSION_NAME = NAME;
public declare extensionName: typeof NAME;
public declare propertyType: 'ManifoldPrimitive';
public declare parentTypes: [PropertyType.MESH];
init() {
ManifoldPrimitive.EXTENSION_NAME = NAME;
this.EXTENSION_NAME = NAME;
this.propertyType = 'ManifoldPrimitive';
this.parentTypes = [PropertyType.MESH];
}
@@ -56,7 +39,7 @@ export class ManifoldPrimitive extends ExtensionProperty<IManifoldPrimitive> {
return this.getRef('mergeValues');
}
setMerge(indicesAccessor: Accessor, valuesAccessor: Accessor) {
setMerge(indicesAccessor, valuesAccessor) {
if (indicesAccessor.getCount() !== valuesAccessor.getCount())
throw new Error('merge vectors must be the same length.');
this.setRef('mergeIndices', indicesAccessor);
@@ -67,29 +50,28 @@ export class ManifoldPrimitive extends ExtensionProperty<IManifoldPrimitive> {
return this.get('runIndex');
}
setRunIndex(runIndex: number[]|Uint32Array) {
setRunIndex(runIndex) {
return this.set('runIndex', runIndex);
}
setIndices(indices: Accessor) {
setIndices(indices) {
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: ReaderContext) {
read(context) {
const {json} = context.jsonDoc;
const meshDefs = json.meshes || [];
@@ -100,27 +82,20 @@ export class EXTManifold extends Extension {
const manifoldPrimitive = this.createManifoldPrimitive();
mesh.setExtension(NAME, manifoldPrimitive);
const manifoldDef = meshDef.extensions[NAME] as ManifoldDef;
const manifoldDef = meshDef.extensions[NAME];
if (manifoldDef.manifoldPrimitive) {
let count = 0;
const runIndex = Array<number>();
const runIndex = [];
runIndex.push(count);
for (const primitive of mesh.listPrimitives()) {
const indices = primitive.getIndices();
if (!indices) {
console.log('Skipping non-indexed primitive ', primitive.getName());
continue;
}
count += indices.getCount();
count += primitive.getIndices().getCount();
runIndex.push(count);
}
manifoldPrimitive.setRunIndex(runIndex);
manifoldPrimitive.setIndices(
context.accessors[manifoldDef.manifoldPrimitive.indices!]);
}
if (manifoldDef.mergeIndices != null && manifoldDef.mergeValues != null) {
if (manifoldDef.mergeIndices && manifoldDef.mergeValues) {
manifoldPrimitive.setMerge(
context.accessors[manifoldDef.mergeIndices],
context.accessors[manifoldDef.mergeValues]);
@@ -130,34 +105,15 @@ export class EXTManifold extends Extension {
return this;
}
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) {
write(context) {
const {json} = context.jsonDoc;
this.document.getRoot().listMeshes().forEach((mesh) => {
const manifoldPrimitive = mesh.getExtension(NAME) as ManifoldPrimitive;
const manifoldPrimitive = mesh.getExtension(NAME);
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;
@@ -168,20 +124,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;
}
@@ -190,21 +146,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];
}
@@ -1,124 +0,0 @@
// 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;
}
+105 -12
View File
@@ -35,24 +35,117 @@
</head>
<body>
<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/">&lt;model-viewer&gt;</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>
<p>This example demonstrates feeding Manifold's output into <a
href="https://modelviewer.dev/">&lt;model-viewer&gt;</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>
<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" tone-mapping="neutral"
alt="Output of mesh Boolean operation"></model-viewer>
<model-viewer camera-controls shadow-intensity="1" alt="Output of mesh Boolean operation"></model-viewer>
</body>
<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>
<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>
</html>
+1032 -2286
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -8,25 +8,25 @@
"build": "tsc --noEmit && vite build",
"build:watch": "vite build --watch",
"preview": "vite preview",
"test": "vitest run",
"test": "vitest",
"test:ui": "vitest --ui",
"test:dev": "vitest"
"test:run": "vitest run"
},
"dependencies": {
"@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",
"@gltf-transform/core": "^3.2.1",
"@gltf-transform/extensions": "^3.2.1",
"@gltf-transform/functions": "^3.2.1",
"@jscadui/3mf-export": "^0.4.2",
"fflate": "^0.8.0",
"gl-matrix": "^3.4.3",
"simple-dropzone": "0.8.3",
"three": "0.164.1"
"three": "0.151.2"
},
"devDependencies": {
"@vitest/ui": "^1.6.0",
"typescript": "5.2.2",
"vite": "^4.5.0",
"vitest": "^1.6.0"
"@vitest/ui": "^0.31.1",
"@vitest/web-worker": "^0.31.1",
"typescript": "5.0.4",
"vite": "^4.3.2",
"vitest": "^0.31.1"
}
}
+6 -42
View File
@@ -25,11 +25,8 @@ 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?: Attribute[];
attributes?: string[];
roughness?: number;
metallic?: number;
baseColorFactor?: [number, number, number];
@@ -38,48 +35,15 @@ 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';
}
/**
* Returns a shallow copy of the input manifold with the given material
* properties applied. They will be carried along through operations.
* Set material properties on the input manifold. They will be carried along
* through operations.
*
* @param manifold The input object.
* @param manifold The object to add properties to - returned for chaining.
* @param material A set of material properties to apply to this 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;
declare function setMaterial(
manifold: Manifold, material: GLTFMaterial): Manifold;
/**
* Wrap any shape object with this method to display it and any copies in
+91 -147
View File
@@ -12,9 +12,6 @@
// 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() {
@@ -53,9 +50,9 @@ export const examples = {
TetrahedronPuzzle: function() {
// A tetrahedron cut into two identical halves that can screw together as
// 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:
// 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:
// https://www.thingiverse.com/thing:186372
const edgeLength = 50; // Length of each edge of the overall tetrahedron.
@@ -64,21 +61,19 @@ export const examples = {
const scale = edgeLength / (2 * Math.sqrt(2));
const tet = Manifold.tetrahedron().intersect(
Manifold.tetrahedron().rotate([0, 0, 90]).scale(2.5));
const tet = Manifold.tetrahedron().scale(scale);
const box = [];
box.push([2, -2], [2, 2]);
box.push([1, -1], [1, 1]);
for (let i = 0; i <= nDivisions; ++i) {
box.push([gap / (2 * scale), 2 - i * 4 / nDivisions]);
box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]);
}
const cyan = [0, 1, 1];
const magenta = [1, 0, 1];
const fade = (color, pos) => {
for (let i = 0; i < 3; ++i) {
const x = pos[2] / 2;
color[i] = cyan[i] * x + magenta[i] * (1 - x);
color[i] = cyan[i] * pos[2] + magenta[i] * (1 - pos[2]);
}
};
@@ -87,43 +82,25 @@ export const examples = {
// channels as colors, and sets the factor to white, since our default is
// yellow.
const screw = setMaterial(
Manifold.extrude(box, 2, nDivisions, 270).setProperties(3, fade),
Manifold.extrude(box, 1, nDivisions, 270).setProperties(3, fade),
{baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']});
const result =
tet.intersect(screw.rotate([0, 0, -45]).translate([0, 0, -1]))
.scale(scale);
const result = tet.intersect(
screw.rotate([0, 0, -45]).translate([0, 0, -0.5]).scale(2 * 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. 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';
// 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;
return result;
},
@@ -131,7 +108,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, cube} = Manifold;
const {sphere, cylinder, union} = Manifold;
function roundedFrame(edgeLength, radius, circularSegments = 0) {
const edge = cylinder(edgeLength, radius, -1, circularSegments);
@@ -155,30 +132,6 @@ export const examples = {
setMinCircularAngle(3);
setMinCircularEdgeLength(0.5);
const result = roundedFrame(100, 10);
// Demonstrate how you can use the .split method to performs
// a subtraction and an intersection at once
const [inside, outside] = result.split(cube(100, true));
const rootNode = new GLTFNode();
const outsideNode = new GLTFNode(rootNode);
outsideNode.manifold = outside;
const insideNode = new GLTFNode(rootNode);
insideNode.scale = (t) => {
const isInsideVisible = t < 0.5;
return isInsideVisible ? [1, 1, 1] : [0, 0, 0];
};
insideNode.material = {
metallic: 1,
baseColorFactor: [1, 1, .3],
roughness: .5,
};
insideNode.manifold = inside;
globalDefaults.animationLength = 3; // seconds
globalDefaults.animationMode = 'ping-pong';
return result;
},
@@ -186,7 +139,6 @@ 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];
@@ -222,20 +174,9 @@ export const examples = {
};
const ball = Manifold.sphere(1, 200);
const heart = ball.warp(func);
const box = heart.boundingBox();
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);
const result = heart.scale(100 / (box.max[0] - box.min[0]));
return result;
},
@@ -312,68 +253,71 @@ export const examples = {
// an example of using the warp() method, thus avoiding any direct
// handling of triangles.
// 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;
// @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.
// These default values recreate Matlab Knot by Emmett Lalish:
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:
// https://www.thingiverse.com/thing:7080
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]];
}
const result = torusKnot(1, 3, 25, 10, 3.75);
return result;
},
@@ -406,12 +350,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;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 471 B

Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
declare module 'simple-dropzone';
+73 -11
View File
@@ -35,21 +35,83 @@
</head>
<body>
<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 &lt;model-viewer&gt; can be found
<a href="model-viewer.html">here</a>. </p>
<p>This demonstrates the minimum code to connect Manifold to a three.js rendering. A slightly more complex example
involving GLTF export and &lt;model-viewer&gt; can be found <a href="model-viewer.html">here</a>. </p>
<select>
<option value="union" selected>Union</option>
<option value="difference">Difference</option>
<option value="difference" selected>Difference</option>
<option value="intersection">Intersection</option>
<option value="union">Union</option>
</select><br />
<canvas id="output"></canvas>
</body>
<script type="module" src="three.ts"></script>
<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>
</html>
-148
View File
@@ -1,148 +0,0 @@
// 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;
}
+1 -1
View File
@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "Bundler",
"moduleResolution": "node",
"lib": [
"es2022",
"dom"
-1
View File
@@ -9,7 +9,6 @@ export default defineConfig({
},
build: {
target: 'esnext',
sourcemap: true,
rollupOptions: {
input: {
manifoldCAD: resolve(__dirname, 'index.html'),
-48
View File
@@ -1,48 +0,0 @@
// 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,44 +12,79 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {WebIO} from '@gltf-transform/core';
import assert from 'node:assert';
import {afterEach, expect, suite, test} from 'vitest';
import '@vitest/web-worker';
import {readMesh, setupIO} from './gltf-io';
// @ts-ignore
import {WebIO} from '@gltf-transform/core';
import {expect, suite, test} from 'vitest';
import Module from './built/manifold.js';
import {readMesh, setupIO} from './gltf-io.js';
import {examples} from './public/examples.js';
import {Mesh} from './public/manifold';
import {cleanup, evaluateCADToModel, module} from './worker';
import ManifoldWorker from './worker?worker';
const io = setupIO(new WebIO());
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);
const wasm = await Module();
wasm.setup();
function initialized(worker) {
return new Promise((resolve) => {
worker.onmessage = function(e) {
if (e.data == null) {
resolve();
} else {
reject();
}
};
});
}
// 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))})
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));
});
}
suite('Examples', () => {
test('Intro', async () => {
@@ -62,29 +97,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(7240, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(3235, 1, 'Surface Area');
expect(result.volume).to.be.closeTo(7297, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(3303, 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(270807, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(74599, 1, 'Surface Area');
expect(result.volume).to.be.closeTo(353706, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(68454, 1, 'Surface Area');
});
test('Heart', async () => {
const result = await runExample('Heart');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(3.342, 0.001, 'Volume');
expect(result.surfaceArea).to.be.closeTo(11.51, 0.01, 'Surface Area');
expect(result.volume).to.be.closeTo(282743, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(22187, 1, 'Surface Area');
});
test('Scallop', async () => {
const result = await runExample('Scallop');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(39900, 100, 'Volume');
expect(result.surfaceArea).to.be.closeTo(7930, 10, 'Surface Area');
expect(result.volume).to.be.closeTo(41284, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(7810, 1, 'Surface Area');
});
test('Torus Knot', async () => {
+122 -344
View File
@@ -12,37 +12,27 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Accessor, Animation, AnimationSampler, Document, mat4, Material, Mesh as GLTFMesh, Node, WebIO} from '@gltf-transform/core';
import {Document, mat4, Material, Node, WebIO} from '@gltf-transform/core';
import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
import {fileForContentTypes, FileForRelThumbnail, to3dmodel} from '@jscadui/3mf-export';
import {fileForContentTypes, to3dmodel} from '@jscadui/3mf-export';
import {strToU8, Zippable, zipSync} from 'fflate'
import * as glMatrix from 'gl-matrix';
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';
}
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';
interface WorkerStatic extends ManifoldToplevel {
GLTFNode: typeof GLTFNode;
show(manifold: Manifold): Manifold;
only(manifold: Manifold): Manifold;
setMaterial(manifold: Manifold, material: GLTFMaterial): Manifold;
setMorphStart(manifold: Manifold, func: (v: Vec3) => void): void;
setMorphEnd(manifold: Manifold, func: (v: Vec3) => void): void;
setMaterial(manifold: Manifold, material: GLTFMaterial): void;
cleanup(): void;
}
export const module = await Module() as WorkerStatic;
const module = await Module() as unknown as WorkerStatic;
module.setup();
// Faster on modern browsers than Float32Array
@@ -58,30 +48,9 @@ const manifoldStaticFunctions = [
];
// manifold member functions (that return a new manifold)
const manifoldMemberFunctions = [
'add',
'subtract',
'intersect',
'decompose',
'warp',
'transform',
'translate',
'rotate',
'scale',
'mirror',
'calculateCurvature',
'calculateNormals',
'smoothByNormals',
'smoothOut',
'refine',
'refineToLength',
'setProperties',
'asOriginal',
'trimByPlane',
'split',
'splitByPlane',
'slice',
'project',
'hull'
'add', 'subtract', 'intersect', 'decompose', 'warp', 'transform', 'translate',
'rotate', 'scale', 'mirror', 'refine', 'setProperties', 'asOriginal',
'trimByPlane', 'split', 'splitByPlane', 'hull'
];
// CrossSection static methods (that return a new cross-section)
const crossSectionStaticFunctions = [
@@ -97,8 +66,7 @@ const crossSectionMemberFunctions = [
const toplevelConstructors = ['show', 'only', 'setMaterial'];
const toplevel = [
'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments',
'getCircularSegments', 'resetToCircularDefaults', 'Mesh', 'GLTFNode',
'Manifold', 'CrossSection', 'setMorphStart', 'setMorphEnd'
'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection'
];
const exposedFunctions = toplevelConstructors.concat(toplevel);
@@ -115,13 +83,15 @@ function addMembers(
const cls = module[className];
const obj = areStatic ? cls : cls.prototype;
for (const name of methodNames) {
const originalFn = obj[name];
obj[name] = function(...args: any) {
//@ts-ignore
const result = originalFn(...args);
memoryRegistry.push(result);
return result;
};
if (name != 'cylinder') {
const originalFn = obj[name];
obj[name] = function(...args: any) {
//@ts-ignore
const result = originalFn(...args);
memoryRegistry.push(result);
return result;
};
}
}
}
@@ -141,23 +111,22 @@ 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,
@@ -175,26 +144,10 @@ 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();
@@ -202,13 +155,7 @@ export function cleanup() {
id2material.clear();
materialCache.clear();
object2globalID.clear();
manifold2morph.clear();
nextGlobalID = 1;
}
interface Morph {
start?: (v: Vec3) => void;
end?: (v: Vec3) => void;
nextGlobalID = 0;
}
interface Mesh3MF {
@@ -251,9 +198,9 @@ interface To3MF {
class GLTFNode {
private _parent?: GLTFNode;
manifold?: Manifold;
translation?: Vec3|((t: number) => Vec3);
rotation?: Vec3|((t: number) => Vec3);
scale?: Vec3|((t: number) => Vec3);
translation?: Vec3;
rotation?: Vec3;
scale?: Vec3;
material?: GLTFMaterial;
name?: string;
@@ -274,32 +221,12 @@ 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());
@@ -315,153 +242,69 @@ 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 (typeof self !== 'undefined' && self.console) {
if (self.console) {
self.console.log(...args);
}
}
function euler2quat(rotation: Vec3): Quat {
const {quat} = glMatrix;
const deg2rad = Math.PI / 180;
const q = quat.create() as Quat;
quat.rotateZ(q, q, deg2rad * rotation[2]);
quat.rotateY(q, q, deg2rad * rotation[1]);
quat.rotateX(q, q, deg2rad * rotation[0]);
return q;
}
function addMotion(
doc: Document, type: 'translation'|'rotation'|'scale', node: GLTFNode,
out: Node): Vec3|null {
const motion = node[type];
if (motion == null) {
return null;
}
if (typeof motion !== 'function') {
return motion;
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();
}
};
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 {
function createGLTFnode(doc: Document, node: GLTFNode) {
const out = doc.createNode(node.name);
const pos = addMotion(doc, 'translation', node, out);
if (pos != null) {
out.setTranslation(pos);
if (node.translation) {
out.setTranslation(node.translation);
}
const rot = addMotion(doc, 'rotation', node, out);
if (rot != null) {
out.setRotation(euler2quat(rot));
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 scale = addMotion(doc, 'scale', node, out);
if (scale != null) {
out.setScale(scale);
if (node.scale) {
out.setScale(node.scale);
}
return out;
}
@@ -475,13 +318,15 @@ function getBackupMaterial(node?: GLTFNode): GLTFMaterial {
return node.material;
}
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 ?? '');
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);
if (unlit) {
const unlit = doc.createExtension(KHRMaterialsUnlit).createUnlit();
@@ -528,23 +373,19 @@ function addMesh(
// From Z-up to Y-up (glTF)
const manifoldMesh = manifold.getMesh();
const id2properties = new Map<number, Properties>();
const materials = new Array<GLTFMaterial>();
const attributes = new Array<Array<string>>();
for (const id of manifoldMesh.runOriginalID!) {
const material = id2material.get(id) || backupMaterial;
id2properties.set(id, {
material: getCachedMaterial(doc, ghost ? GHOST : material),
attributes: ['POSITION', ...material.attributes ?? []]
});
materials.push(material);
attributes.push(['POSITION', ...material.attributes ?? []]);
}
const morph = manifold2morph.get(manifold);
const inputPositions = morphStart(manifoldMesh, morph);
const gltfMaterials = materials.map((matDef) => {
return getCachedMaterial(doc, ghost ? GHOST : matDef);
});
const mesh = writeMesh(doc, manifoldMesh, id2properties);
mesh.setName(node.getName());
node.setMesh(mesh);
morphEnd(doc, manifoldMesh, mesh, inputPositions, morph);
node.setMesh(writeMesh(doc, manifoldMesh, attributes, gltfMaterials));
const vertices = manifoldMesh.numProp === 3 ?
manifoldMesh.vertProperties :
@@ -561,18 +402,21 @@ function addMesh(
{vertices, indices: manifoldMesh.triVerts, id: `${nextGlobalID++}`});
for (const [run, id] of manifoldMesh.runOriginalID!.entries()) {
const show = shown.has(id);
const inMesh = show ? shown.get(id) : singles.get(id);
let inMesh = shown.get(id);
let single = false;
if (inMesh == null) {
single = true;
inMesh = singles.get(id);
}
if (inMesh == null) {
continue;
}
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));
const mat = single ? materials[run] : SHOW;
const debugNode =
doc.createNode('debug')
.setMesh(writeMesh(
doc, inMesh, attributes, [getCachedMaterial(doc, mat)]))
.setMatrix(manifoldMesh.transform(run));
node.addChild(debugNode);
}
}
@@ -606,7 +450,6 @@ 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) {
@@ -638,10 +481,7 @@ function createNodeFromCache(
return node;
}
async function exportModels(defaults: GlobalDefaults, manifold?: Manifold) {
Object.assign(globalDefaults, GLOBAL_DEFAULTS);
Object.assign(globalDefaults, defaults);
async function exportModels(manifold?: Manifold) {
const doc = new Document();
const halfRoot2 = Math.sqrt(2) / 2;
const mm2m = 1 / 1000;
@@ -650,32 +490,6 @@ async function exportModels(defaults: GlobalDefaults, 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: [],
@@ -733,56 +547,20 @@ async function exportModels(defaults: GlobalDefaults, manifold?: Manifold) {
to3mf.items.push({objectID: `${object2globalID.get(manifold)}`});
}
if (!hasAnimation) {
timesAccessor.dispose();
weightsAccessor.dispose();
weightsSampler.dispose();
animation.dispose();
}
const glb = await io.writeBinary(doc);
const blobGLB = new Blob([glb], {type: 'application/octet-stream'});
const fileForRelThumbnail = new FileForRelThumbnail();
fileForRelThumbnail.add3dModel('3D/3dmodel.model')
const model = to3dmodel(to3mf as any);
const model = to3dmodel(to3mf);
const files: Zippable = {};
files['3D/3dmodel.model'] = strToU8(model);
files[fileForContentTypes.name] = strToU8(fileForContentTypes.content);
files[fileForRelThumbnail.name] = strToU8(fileForRelThumbnail.content);
const zipFile = zipSync(files);
const blob3MF = new Blob(
[zipFile],
{type: 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml'});
return ({
self.postMessage({
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);
}
+14 -101
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Box, FillRule, JoinType, Mat3, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types';
import {Box, FillRule, JoinType, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types';
/**
* Triangulates a set of /epsilon-valid polygons.
@@ -38,7 +38,6 @@ 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 {
@@ -560,37 +559,6 @@ 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
@@ -603,18 +571,6 @@ 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
@@ -650,24 +606,6 @@ 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
/**
@@ -747,22 +685,6 @@ 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
/**
@@ -859,13 +781,6 @@ 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
/**
@@ -923,7 +838,19 @@ export class Manifold {
delete(): void;
}
export interface MeshOptions {
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
});
numProp: number;
vertProperties: Float32Array;
triVerts: Uint32Array;
@@ -934,20 +861,6 @@ export interface MeshOptions {
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;
-12
View File
@@ -22,18 +22,6 @@ export interface SealedFloat32Array<N extends number> extends Float32Array {
export type Vec2 = [number, number];
export type Vec3 = [number, number, number];
// 3x3 matrix stored in column-major order
export type Mat3 = [
number,
number,
number,
number,
number,
number,
number,
number,
number,
];
// 4x4 matrix stored in column-major order
export type Mat4 = [
number,
-1
View File
@@ -28,7 +28,6 @@ export interface ManifoldToplevel {
setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength;
setCircularSegments: typeof T.setCircularSegments;
getCircularSegments: typeof T.getCircularSegments;
resetToCircularDefaults: typeof T.resetToCircularDefaults;
setup: () => void;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "manifold-3d",
"version": "2.5.0",
"version": "2.1.0",
"description": "Geometry library for topological robustness",
"main": "manifold.js",
"files": [
+1 -2
View File
@@ -41,9 +41,8 @@ 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 Boost::thread)
target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core)
target_compile_options(perfTestCGAL PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(perfTestCGAL PUBLIC cxx_std_17)
endif()
Generated
+7 -43
View File
@@ -1,31 +1,15 @@
{
"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": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github"
},
"original": {
@@ -53,11 +37,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1704722960,
"narHash": "sha256-mKGJ3sPsT6//s+Knglai5YflJUF2DGj7Ai6Ynopz0kI=",
"lastModified": 1692734709,
"narHash": "sha256-SCFnyHCyYjwEmgUsHDDuU0TsbVMKeU1vwkR+r7uS2Rg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "317484b1ead87b9c1b8ac5261a8d2dd748a0492d",
"rev": "b85ed9dcbf187b909ef7964774f8847d554fab3b",
"type": "github"
},
"original": {
@@ -68,11 +52,9 @@
},
"root": {
"inputs": {
"clipper2-src": "clipper2-src",
"flake-utils": "flake-utils",
"gtest-src": "gtest-src",
"nixpkgs": "nixpkgs",
"thrust-src": "thrust-src"
"nixpkgs": "nixpkgs"
}
},
"systems": {
@@ -89,24 +71,6 @@
"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",
+51 -46
View File
@@ -5,15 +5,7 @@
url = "github:google/googletest/v1.14.0";
flake = false;
};
inputs.thrust-src = {
url = "git+https://github.com/NVIDIA/thrust.git?submodules=1";
flake = false;
};
inputs.clipper2-src = {
url = "github:AngusJohnson/Clipper2";
flake = false;
};
outputs = { self, nixpkgs, flake-utils, gtest-src, thrust-src, clipper2-src }:
outputs = { self, nixpkgs, flake-utils, gtest-src }:
flake-utils.lib.eachDefaultSystem
(system:
let
@@ -21,9 +13,36 @@
inherit system;
config.allowUnfree = true;
};
clipper2 = pkgs.clipper2.overrideAttrs (_: _: {
version = "14052024";
src = clipper2-src;
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;
});
manifold =
{ parallel-backend ? "none"
@@ -33,38 +52,28 @@
}: pkgs.stdenv.mkDerivation {
inherit doCheck;
pname = "manifold-${parallel-backend}";
version = "2.5.0";
version = "2.2.0";
src = self;
nativeBuildInputs = (with pkgs; [
cmake
ninja
(python3.withPackages
(ps: with ps; [ trimesh pytest ]))
gtest
pkg-config
]) ++ build-tools;
buildInputs = with pkgs; [
thrust-210
glm
clipper2
assimp
];
ninja
(python39.withPackages
(ps: with ps; [ trimesh ]))
gtest
]) ++ build-tools;
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
'';
};
@@ -75,6 +84,14 @@
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
@@ -86,13 +103,11 @@
parallelBackends)) // {
manifold-js = pkgs.buildEmscriptenPackage {
name = "manifold-js";
version = "2.5.0";
version = "beta";
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
@@ -100,8 +115,7 @@
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
-DFETCHCONTENT_SOURCE_DIR_GLM=${pkgs.glm.src} \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=${gtest-src} \
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src} \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=../clipper2 ..
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-210.src} ..
'';
buildPhase = ''
emmake make -j''${NIX_BUILD_CORES}
@@ -120,15 +134,15 @@
# but how should we make it work with other python versions?
manifold3d = with pkgs.python3Packages; buildPythonPackage {
pname = "manifold3d";
version = "2.5.0";
version = "2.2.0";
src = self;
propagatedBuildInputs = [
numpy
];
buildInputs = with pkgs; [
tbb_2021_8
thrust-210
glm
clipper2
];
nativeBuildInputs = with pkgs; [
cmake
@@ -139,27 +153,18 @@
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 = pkgs.mkShell {
buildInputs = with pkgs; [
cmake
tbb_2021_8
gtest
];
};
devShell = devShell { };
}
);
}
-16
View File
@@ -1,16 +0,0 @@
#!/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
-4
View File
@@ -1,4 +0,0 @@
set(MANIFOLD_PAR "@MANIFOLD_PAR@")
set(MANIFOLD_EXPORT "@MANIFOLD_EXPORT@")
include("${CMAKE_CURRENT_LIST_DIR}/manifoldDeps.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/manifoldTargets.cmake")
-79
View File
@@ -1,79 +0,0 @@
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()
+15 -5
View File
@@ -14,11 +14,13 @@
project(meshIO)
find_package(assimp REQUIRED)
add_library(${PROJECT_NAME} src/meshIO.cpp)
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 manifold
@@ -28,5 +30,13 @@ target_link_libraries(${PROJECT_NAME}
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
install(FILES include/meshIO.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
install(
TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT devel
)
install(
DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
COMPONENT devel
)
-1
View File
@@ -234,7 +234,6 @@ 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);
}
}
-59
View File
@@ -1,59 +0,0 @@
#!/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
+10 -4
View File
@@ -1,13 +1,13 @@
[project]
name = "manifold3d"
version = "2.5.0"
version = "2.2.0"
authors = [
{ name="Emmett Lalish", email="elalish@gmail.com" },
]
description = "Library for geometric robustness"
readme = "README.md"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Development Status :: 4 - Beta",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: C++",
@@ -42,14 +42,20 @@ 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_PYBIND=ON", "-DMANIFOLD_PAR=TBB", "-DMANIFOLD_TEST=OFF", "-DBUILD_SHARED_LIBS=OFF"]
cmake.args = ["-DMANIFOLD_PAR=TBB", "-DMANIFOLD_TEST=OFF"]
install.components = ["bindings"]
[tool.cibuildwheel]
build-frontend = "build"
test-requires = ["trimesh", "pytest"]
test-requires = ["trimesh"]
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"
-3
View File
@@ -1,3 +0,0 @@
[pytest]
testpaths =
bindings/python/examples
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+9 -6
View File
@@ -15,15 +15,18 @@
project (collider)
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
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>)
add_library(${PROJECT_NAME} ${SOURCE_FILES})
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_link_libraries(${PROJECT_NAME} PUBLIC utilities)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME}
PUBLIC cxx_std_17
)
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
install(FILES include/collider.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
install(
TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT runtime
)
+1 -4
View File
@@ -30,7 +30,6 @@ 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_;
@@ -39,9 +38,7 @@ class Collider {
Vec<thrust::pair<int, int>> internalChildren_;
int NumInternal() const { return internalChildren_.size(); };
int NumLeaves() const {
return internalChildren_.empty() ? 0 : (NumInternal() + 1);
};
int NumLeaves() const { return NumInternal() + 1; };
};
} // namespace manifold
+1 -24
View File
@@ -265,14 +265,6 @@ 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 {
@@ -284,7 +276,6 @@ 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;
@@ -308,7 +299,6 @@ 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) {
@@ -346,7 +336,6 @@ 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
@@ -366,7 +355,6 @@ 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;
@@ -382,18 +370,6 @@ 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;
@@ -411,4 +387,5 @@ template SparseIndices Collider::Collisions<false, true, Box>(
template SparseIndices Collider::Collisions<false, true, glm::vec3>(
const VecView<const glm::vec3>&) const;
} // namespace manifold
+20 -16
View File
@@ -13,24 +13,28 @@
# limitations under the License.
project (cross_section)
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
target_include_directories(${PROJECT_NAME} PUBLIC
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
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()
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
add_library(${PROJECT_NAME} ${SOURCE_FILES})
target_include_directories(${PROJECT_NAME}
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
target_link_libraries( ${PROJECT_NAME}
PUBLIC utilities Clipper2
)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
install(FILES include/cross_section.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
install(
TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT runtime
)
install(
DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
COMPONENT devel
)
@@ -21,7 +21,6 @@
#include "glm/ext/matrix_float3x2.hpp"
#include "glm/ext/vector_float2.hpp"
#include "public.h"
#include "vec_view.h"
namespace manifold {
@@ -98,8 +97,6 @@ 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:
+46 -96
View File
@@ -146,63 +146,55 @@ 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;
}
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();
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++;
}
}
// 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 lower = std::vector<glm::vec2>{};
for (int i = 0; i < len; i++) {
HullBacktrack(pts[i], lower);
lower.push_back(pts[i]);
auto keep = std::vector<int>{0, 1};
for (int i = 2; i < len; i++) {
HullBacktrack(pts, i, keep, 1);
keep.push_back(i);
}
auto upper = std::vector<glm::vec2>{};
for (int i = len - 1; i >= 0; i--) {
HullBacktrack(pts[i], upper);
upper.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);
}
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]);
auto path = C2::PathD(keep.size());
for (int i = 0; i < keep.size(); i++) {
path[i] = v2_to_pd(pts[keep[i]]);
}
return path;
}
@@ -563,42 +555,21 @@ CrossSection CrossSection::Transform(const glm::mat3x2& m) const {
*/
CrossSection CrossSection::Warp(
std::function<void(glm::vec2&)> warpFunc) const {
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));
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);
}
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(paths, C2::FillRule::Positive, precision_)));
shared_paths(C2::Union(warped, C2::FillRule::Positive, precision_)));
}
/**
@@ -614,28 +585,7 @@ CrossSection CrossSection::WarpBatch(
* offseting operations are to be performed, which would compound the issue.
*/
CrossSection CrossSection::Simplify(double epsilon) const {
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);
auto ps = SimplifyPaths(GetPaths()->paths_, epsilon, false);
return CrossSection(shared_paths(ps));
}
+14 -7
View File
@@ -17,12 +17,10 @@ project (manifold)
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
add_library(${PROJECT_NAME} ${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 cross_section sdf
PRIVATE collider polygon ${MANIFOLD_INCLUDE} quickhull
PUBLIC utilities cross_section
PRIVATE collider polygon ${MANIFOLD_INCLUDE} Clipper2 quickhull
)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
@@ -31,5 +29,14 @@ target_compile_features(${PROJECT_NAME}
PUBLIC cxx_std_17
)
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
install(FILES include/manifold.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
install(
TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT runtime
)
install(
DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
COMPONENT devel
)
+4 -57
View File
@@ -18,7 +18,6 @@
#include "cross_section.h"
#include "public.h"
#include "vec_view.h"
namespace manifold {
@@ -46,17 +45,9 @@ class CsgLeafNode;
*/
struct MeshGL {
/// Number of property vertices
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;
};
uint32_t NumVert() const { return vertProperties.size() / numProp; };
/// Number of triangles
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;
};
uint32_t NumTri() const { return triVerts.size() / 3; };
/// Number of properties per vertex, always >= 3.
uint32_t numProp = 3;
@@ -77,10 +68,7 @@ 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. 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.
/// triVerts[runIndex[i+1]]. All runIndex values are divisible by 3.
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
@@ -123,31 +111,6 @@ 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
@@ -221,7 +184,6 @@ class Manifold {
float Precision() const;
int Genus() const;
Properties GetProperties() const;
float MinGap(const Manifold& other, float searchLength) const;
///@}
/** @name Mesh ID
@@ -244,15 +206,11 @@ 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) const;
// Manifold RefineToLength(float);
// Manifold RefineToPrecision(float);
///@}
@@ -276,20 +234,9 @@ 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.
+70 -49
View File
@@ -28,34 +28,36 @@ namespace {
// cases to ensure consistency.
glm::vec2 Interpolate(glm::vec3 pL, glm::vec3 pR, float x) {
const float dxL = x - pL.x;
const float dxR = x - pR.x;
ASSERT(dxL * dxR <= 0, logicErr, "Boolean manifold error: not in domain");
const bool useL = fabs(dxL) < fabs(dxR);
const glm::vec3 dLR = pR - pL;
const float lambda = (useL ? dxL : dxR) / dLR.x;
if (!isfinite(lambda) || !isfinite(dLR.y) || !isfinite(dLR.z))
return glm::vec2(pL.y, pL.z);
float dxL = x - pL.x;
float dxR = x - pR.x;
#ifdef MANIFOLD_DEBUG
if (dxL * dxR > 0) printf("Not in domain!\n");
#endif
bool useL = fabs(dxL) < fabs(dxR);
float lambda = (useL ? dxL : dxR) / (pR.x - pL.x);
if (!isfinite(lambda)) return glm::vec2(pL.y, pL.z);
glm::vec2 yz;
yz[0] = (useL ? pL.y : pR.y) + lambda * dLR.y;
yz[1] = (useL ? pL.z : pR.z) + lambda * dLR.z;
yz[0] = (useL ? pL.y : pR.y) + lambda * (pR.y - pL.y);
yz[1] = (useL ? pL.z : pR.z) + lambda * (pR.z - pL.z);
return yz;
}
glm::vec4 Intersect(const glm::vec3 &pL, const glm::vec3 &pR,
const glm::vec3 &qL, const glm::vec3 &qR) {
const float dyL = qL.y - pL.y;
const float dyR = qR.y - pR.y;
ASSERT(dyL * dyR <= 0, logicErr, "Boolean manifold error: no intersection");
const bool useL = fabs(dyL) < fabs(dyR);
const float dx = pR.x - pL.x;
float 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;
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;
const float pDy = pR.y - pL.y;
const float qDy = qR.y - qL.y;
const bool useP = fabs(pDy) < fabs(qDy);
float pDy = pR.y - pL.y;
float qDy = qR.y - qL.y;
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);
@@ -71,9 +73,9 @@ struct CopyFaceEdges {
SparseIndices &pXq1;
VecView<const Halfedge> halfedgesQ;
void operator()(thrust::tuple<size_t, size_t> in) {
void operator()(thrust::tuple<int, int> in) {
int idx = 3 * thrust::get<0>(in);
size_t i = thrust::get<1>(in);
int i = thrust::get<1>(in);
int pX = p1q1.Get(i, inverted);
int q2 = p1q1.Get(i, !inverted);
@@ -83,18 +85,17 @@ struct CopyFaceEdges {
int a = pX;
int b = edge.IsForward() ? q1 : edge.pairedHalfedge;
if (inverted) std::swap(a, b);
pXq1.Set(idx + static_cast<size_t>(j), a, b);
pXq1.Set(idx + 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_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)),
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)),
p2q1.size(), CopyFaceEdges<true>({p2q1, p1q1, inP.halfedge_}));
p1q1.Unique();
return p1q1;
@@ -104,6 +105,17 @@ 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,
@@ -137,14 +149,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
size_t monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
int monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
if (array.size() == 0) {
return -1;
}
size_t bot = 0;
size_t top = array.size();
unsigned int bot = 0;
unsigned int top = array.size();
while (top >= 65536) {
size_t mid = top / 4;
unsigned int mid = top / 4;
top -= mid * 3;
if (key < array[bot + mid * 2]) {
if (key >= array[bot + mid]) {
@@ -159,7 +171,7 @@ size_t monobound_quaternary_search(VecView<const int64_t> array, int64_t key) {
}
while (top > 3) {
size_t mid = top / 2;
unsigned int mid = top / 2;
if (key >= array[bot + mid]) {
bot += mid;
}
@@ -183,7 +195,7 @@ struct Kernel11 {
VecView<const glm::vec3> normalP;
const SparseIndices &p1q1;
void operator()(thrust::tuple<size_t, glm::vec4 &, int &> inout) {
void operator()(thrust::tuple<int, 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);
@@ -236,7 +248,12 @@ struct Kernel11 {
if (s11 == 0) { // No intersection
xyzz11 = glm::vec4(NAN);
} else {
ASSERT(k == 2, logicErr, "Boolean manifold error: s11");
#ifdef MANIFOLD_DEBUG
// Assert left and right were both found
if (k != 2) {
printf("k = %d\n", k);
}
#endif
xyzz11 = Intersect(pRL[0], pRL[1], qRL[0], qRL[1]);
const int p1s = halfedgeP[p1].startVert;
@@ -256,12 +273,11 @@ 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_z), xyzz11.begin(), s11.begin()), p1q1.size(),
zip(countAt(0), xyzz11.begin(), s11.begin()), p1q1.size(),
Kernel11({inP.vertPos_, inQ.vertPos_, inP.halfedge_, inQ.halfedge_,
expandP, inP.vertNormal_, p1q1}));
@@ -279,7 +295,7 @@ struct Kernel02 {
const SparseIndices &p0q2;
const bool forward;
void operator()(thrust::tuple<size_t, int &, float &> inout) {
void operator()(thrust::tuple<int, 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);
@@ -328,7 +344,12 @@ struct Kernel02 {
if (s02 == 0) { // No intersection
z02 = NAN;
} else {
ASSERT(k == 2, logicErr, "Boolean manifold error: s02");
#ifdef MANIFOLD_DEBUG
// Assert left and right were both found
if (k != 2) {
printf("k = %d\n", k);
}
#endif
glm::vec3 vertPos = vertPosP[p0];
z02 = Interpolate(yzzRL[0], yzzRL[1], vertPos.y)[1];
if (forward) {
@@ -346,13 +367,12 @@ 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_z), s02.begin(), z02.begin()), p0q2.size(),
for_each_n(autoPolicy(p0q2.size()), zip(countAt(0), s02.begin(), z02.begin()),
p0q2.size(),
Kernel02({inP.vertPos_, inQ.halfedge_, inQ.vertPos_, expandP,
vertNormalP, p0q2, forward}));
@@ -374,7 +394,7 @@ struct Kernel12 {
const bool forward;
const SparseIndices &p1q2;
void operator()(thrust::tuple<size_t, int &, glm::vec3 &> inout) {
void operator()(thrust::tuple<int, 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);
@@ -394,7 +414,7 @@ struct Kernel12 {
for (int vert : {edge.startVert, edge.endVert}) {
const int64_t key = forward ? SparseIndices::EncodePQ(vert, q2)
: SparseIndices::EncodePQ(q2, vert);
const size_t idx = monobound_quaternary_search(p0q2, key);
const int idx = monobound_quaternary_search(p0q2, key);
if (idx != -1) {
const int s = s02[idx];
x12 += s * ((vert == edge.startVert) == forward ? 1 : -1);
@@ -415,7 +435,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 size_t idx = monobound_quaternary_search(p1q1, key);
const int 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);
@@ -436,7 +456,12 @@ struct Kernel12 {
if (x12 == 0) { // No intersection
v12 = glm::vec3(NAN);
} else {
ASSERT(k == 2, logicErr, "Boolean manifold error: v12");
#ifdef MANIFOLD_DEBUG
// Assert left and right were both found
if (k != 2) {
printf("k = %d\n", k);
}
#endif
const glm::vec4 xzyy =
Intersect(xzyLR0[0], xzyLR0[1], xzyLR1[0], xzyLR1[1]);
v12.x = xzyy[0];
@@ -451,12 +476,11 @@ 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_z), x12.begin(), v12.begin()),
autoPolicy(p1q2.size()), zip(countAt(0), x12.begin(), v12.begin()),
p1q2.size(),
Kernel12({p0q2.AsVec64(), s02, z02, p1q1.AsVec64(), s11, xyzz11,
inP.halfedge_, inQ.halfedge_, inP.vertPos_, forward, p1q2}));
@@ -468,7 +492,6 @@ 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
@@ -578,9 +601,6 @@ 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);
@@ -596,6 +616,7 @@ Boolean3::Boolean3(const Manifold::Impl &inP, const Manifold::Impl &inQ,
if (ManifoldParams().verbose) {
broad.Print("Broad phase");
intersections.Print("Intersections");
MemUsage();
}
#endif
}
+37 -62
View File
@@ -101,12 +101,7 @@ 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());
@@ -172,14 +167,13 @@ 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, size_t i) {
std::function<void(size_t)> unlock, int i) {
const int edgeP = p1q2.Get(i, !forward);
const int faceQ = p1q2.Get(i, forward);
const int vert = v12R[i];
@@ -222,9 +216,9 @@ void AddNewEdgeVerts(
[&](size_t hash) { mutexes[hash % mutexes.size()].unlock(); },
std::placeholders::_1);
tbb::parallel_for(
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);
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);
},
ap);
return;
@@ -232,7 +226,7 @@ void AddNewEdgeVerts(
#endif
auto processFun = std::bind(
process, [](size_t _) {}, [](size_t _) {}, std::placeholders::_1);
for (size_t i = 0; i < p1q2.size(); ++i) processFun(i);
for (int i = 0; i < p1q2.size(); ++i) processFun(i);
}
std::vector<Halfedge> PairUp(std::vector<EdgePos> &edgePos) {
@@ -243,7 +237,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.");
size_t nEdges = edgePos.size() / 2;
int 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!");
@@ -251,7 +245,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 (size_t i = 0; i < nEdges; ++i)
for (int i = 0; i < nEdges; ++i)
edges.push_back({edgePos[i].vert, edgePos[i + nEdges].vert, -1, -1});
return edges;
}
@@ -262,7 +256,6 @@ 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
@@ -289,6 +282,7 @@ 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};
@@ -298,6 +292,7 @@ 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) {
@@ -342,7 +337,6 @@ 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_;
@@ -405,6 +399,7 @@ 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;
@@ -447,7 +442,6 @@ 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)),
@@ -469,8 +463,9 @@ struct MapTriRef {
}
};
void UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
const Manifold::Impl &inQ, bool invertQ) {
Vec<TriRef> UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
const Manifold::Impl &inQ, bool invertQ) {
Vec<TriRef> refPQ = outR.meshRelation_.triRef;
const int offsetQ = Manifold::Impl::meshIDCounter_;
for_each_n(
autoPolicy(outR.NumTri()), outR.meshRelation_.triRef.begin(),
@@ -485,6 +480,7 @@ void UpdateReference(Manifold::Impl &outR, const Manifold::Impl &inP,
outR.meshRelation_.meshIDtransform[pair.first + offsetQ].backSide ^=
invertQ;
}
return refPQ;
}
struct Barycentric {
@@ -518,9 +514,8 @@ struct Barycentric {
}
};
void CreateProperties(Manifold::Impl &outR, const Manifold::Impl &inP,
const Manifold::Impl &inQ) {
ZoneScoped;
void CreateProperties(Manifold::Impl &outR, const Vec<TriRef> &refPQ,
const Manifold::Impl &inP, const Manifold::Impl &inQ) {
const int numPropP = inP.NumProp();
const int numPropQ = inQ.NumProp();
const int numProp = glm::max(numPropP, numPropQ);
@@ -531,8 +526,7 @@ void CreateProperties(Manifold::Impl &outR, const Manifold::Impl &inP,
outR.meshRelation_.triProperties.resize(numTri);
Vec<glm::vec3> bary(outR.halfedge_.size());
for_each_n(autoPolicy(numTri),
zip(countAt(0), outR.meshRelation_.triRef.cbegin()), numTri,
for_each_n(autoPolicy(numTri), zip(countAt(0), refPQ.cbegin()), numTri,
Barycentric({bary, inP.vertPos_, inQ.vertPos_, outR.vertPos_,
inP.halfedge_, inQ.halfedge_, outR.halfedge_,
outR.precision_}));
@@ -540,14 +534,6 @@ void CreateProperties(Manifold::Impl &outR, const Manifold::Impl &inP,
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;
@@ -555,14 +541,14 @@ void CreateProperties(Manifold::Impl &outR, const Manifold::Impl &inP,
// Skip collapsed triangles
if (outR.halfedge_[3 * tri].startVert < 0) continue;
const TriRef ref = outR.meshRelation_.triRef[tri];
const bool PQ = ref.meshID == 0;
const int triPQ = refPQ[tri].tri;
const bool PQ = refPQ[tri].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[ref.tri]
: inQ.meshRelation_.triProperties[ref.tri];
: PQ ? inP.meshRelation_.triProperties[triPQ]
: inQ.meshRelation_.triProperties[triPQ];
for (const int i : {0, 1, 2}) {
const int vert = outR.halfedge_[3 * tri + i].startVert;
@@ -570,7 +556,8 @@ void CreateProperties(Manifold::Impl &outR, const Manifold::Impl &inP,
glm::ivec4 key(PQ, idMissProp, -1, -1);
if (oldNumProp > 0) {
int edge = -2;
key[1] = vert;
int edge = -1;
for (const int j : {0, 1, 2}) {
if (uvw[j] == 1) {
// On a retained vert, the propVert must also match
@@ -584,37 +571,24 @@ void CreateProperties(Manifold::Impl &outR, const Manifold::Impl &inP,
// 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;
}
}
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;
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;
}
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;
@@ -750,6 +724,7 @@ 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;
@@ -795,12 +770,12 @@ Manifold::Impl Boolean3::Result(OpType op) const {
if (ManifoldParams().intermediateChecks)
ASSERT(outR.IsManifold(), logicErr, "triangulated mesh is not manifold!");
CreateProperties(outR, inP_, inQ_);
UpdateReference(outR, inP_, inQ_, invertQ);
Vec<TriRef> refPQ = 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!");
+27 -27
View File
@@ -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(impl->UpdateSharpenedEdges(sharpenedEdges));
impl->CreateTangents(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(impl->UpdateSharpenedEdges(sharpenedEdges));
impl->CreateTangents(sharpenedEdges);
return Manifold(impl);
}
@@ -153,9 +153,10 @@ Manifold Manifold::Cube(glm::vec3 size, bool center) {
glm::length(size) == 0.) {
return Invalid();
}
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));
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();
}
/**
@@ -207,7 +208,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](glm::vec3 edge) { return n - 1; });
pImpl_->Subdivide(n);
for_each_n(autoPolicy(pImpl_->NumVert()), pImpl_->vertPos_.begin(),
pImpl_->NumVert(), ToSphere({radius}));
pImpl_->Finish();
@@ -219,9 +220,6 @@ 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.
@@ -232,13 +230,11 @@ 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();
@@ -268,9 +264,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);
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;
transform = transform * glm::mat2(scale.x, 0.0f, 0.0f, scale.y);
int j = 0;
int idx = 0;
for (const auto& poly : polygons) {
@@ -324,7 +320,6 @@ 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) {
@@ -332,9 +327,8 @@ Manifold Manifold::Revolve(const CrossSection& crossSection,
}
const Rect bounds = crossSection.Bounds();
const float radius = bounds.max.x;
if (radius <= 0) {
if (bounds.max.x <= 0) {
return Invalid();
} else if (bounds.min.x < 0) {
// Take the x>=0 slice.
@@ -346,15 +340,21 @@ 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) * revolveDegrees / 360;
const int nDivisions = circularSegments > 2
? circularSegments
: Quality::GetCircularSegments(radius);
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) : 0) +
(prevPolyVertex.x == 0.0 ? -1 : -nSlices);
(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];
for (int slice = 0; slice < nSlices; ++slice) {
const float phi = slice * dPhi;
@@ -464,7 +464,6 @@ 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();
@@ -509,6 +508,7 @@ std::vector<Manifold> Manifold::Decompose() const {
impl->GatherFaces(*pImpl_, faceNew2Old);
impl->ReindexVerts(vertNew2Old, pImpl_->NumVert());
impl->Finish();
meshes.push_back(Manifold(impl));
-3
View File
@@ -172,7 +172,6 @@ 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;
@@ -455,7 +454,6 @@ 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.");
@@ -531,7 +529,6 @@ 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.
+58 -56
View File
@@ -73,7 +73,9 @@ struct FlagEdge {
current = NextHalfedge(halfedge[current].pairedHalfedge);
int tri = current / 3;
const TriRef ref = triRef[tri];
if (!ref.SameFace(ref0) && !ref.SameFace(ref1)) return false;
if ((ref.meshID != ref0.meshID || ref.tri != ref0.tri) &&
(ref.meshID != ref1.meshID || ref.tri != ref1.tri))
return false;
}
return true;
}
@@ -143,32 +145,29 @@ namespace manifold {
void Manifold::Impl::SimplifyTopology() {
if (!halfedge_.size()) return;
const size_t nbEdges = halfedge_.size();
const int nbEdges = halfedge_.size();
auto policy = autoPolicy(nbEdges);
size_t numFlagged = 0;
int 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();
{
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});
}
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++;
}
}
@@ -179,19 +178,13 @@ 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_z), nbEdges,
[&](size_t i) { bflags[i] = se(i); });
for (size_t i = 0; i < nbEdges; ++i) {
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
for (int i = 0; i < nbEdges; ++i) {
if (bflags[i]) {
CollapseEdge(i, scratchBuffer);
scratchBuffer.resize(0);
@@ -208,12 +201,10 @@ void Manifold::Impl::SimplifyTopology() {
#endif
{
ZoneScopedN("CollapseFlaggedEdge");
numFlagged = 0;
FlagEdge se{halfedge_, meshRelation_.triRef};
for_each_n(policy, countAt(0_z), nbEdges,
[&](size_t i) { bflags[i] = se(i); });
for (size_t i = 0; i < nbEdges; ++i) {
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
for (int i = 0; i < nbEdges; ++i) {
if (bflags[i]) {
CollapseEdge(i, scratchBuffer);
scratchBuffer.resize(0);
@@ -230,15 +221,13 @@ void Manifold::Impl::SimplifyTopology() {
#endif
{
ZoneScopedN("RecursiveEdgeSwap");
numFlagged = 0;
SwappableEdge se{halfedge_, vertPos_, faceNormal_, precision_};
for_each_n(policy, countAt(0_z), nbEdges,
[&](size_t i) { bflags[i] = se(i); });
for_each_n(policy, countAt(0), nbEdges, [&](int i) { bflags[i] = se(i); });
std::vector<int> edgeSwapStack;
std::vector<int> visited(halfedge_.size(), -1);
int tag = 0;
for (size_t i = 0; i < nbEdges; ++i) {
for (int i = 0; i < nbEdges; ++i) {
if (bflags[i]) {
numFlagged++;
tag++;
@@ -323,10 +312,12 @@ void Manifold::Impl::DedupeEdge(const int edge) {
vertPos_.push_back(vertPos_[endVert]);
if (vertNormal_.size() > 0) vertNormal_.push_back(vertNormal_[endVert]);
ForVert(NextHalfedge(current), [this, newVert](int e) {
halfedge_[e].startVert = newVert;
halfedge_[halfedge_[e].pairedHalfedge].endVert = newVert;
});
do {
halfedge_[current].endVert = newVert;
current = NextHalfedge(current);
halfedge_[current].startVert = newVert;
current = halfedge_[current].pairedHalfedge;
} while (current != edge);
}
// Orbit startVert
@@ -346,10 +337,12 @@ void Manifold::Impl::DedupeEdge(const int edge) {
vertPos_.push_back(vertPos_[endVert]);
if (vertNormal_.size() > 0) vertNormal_.push_back(vertNormal_[endVert]);
ForVert(NextHalfedge(current), [this, newVert](int e) {
halfedge_[e].startVert = newVert;
halfedge_[halfedge_[e].pairedHalfedge].endVert = newVert;
});
do {
halfedge_[current].endVert = newVert;
current = NextHalfedge(current);
halfedge_[current].startVert = newVert;
current = halfedge_[current].pairedHalfedge;
} while (current != pair);
}
}
@@ -476,9 +469,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.SameFace(refCheck)) {
if (ref.meshID != refCheck.meshID || ref.tri != refCheck.tri) {
refCheck = triRef[edge / 3];
if (!ref.SameFace(refCheck)) {
if (ref.meshID != refCheck.meshID || ref.tri != refCheck.tri) {
return;
} else {
// Don't collapse if the edges separating the faces are not colinear
@@ -508,18 +501,25 @@ 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);
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];
// 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];
}
}
}
@@ -656,7 +656,6 @@ 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) {
@@ -668,11 +667,14 @@ void Manifold::Impl::SplitPinchedVerts() {
} else {
vertProcessed[vert] = true;
}
ForVert(i, [this, &halfedgeProcessed, vert](int current) {
int current = i;
do {
halfedgeProcessed[current] = true;
halfedge_[current].startVert = vert;
halfedge_[halfedge_[current].pairedHalfedge].endVert = vert;
});
current = halfedge_[current].pairedHalfedge;
halfedge_[current].endVert = vert;
current = NextHalfedge(current);
} while (current != i);
}
}
} // namespace manifold
+23 -115
View File
@@ -17,7 +17,7 @@
#define TBB_PREVIEW_CONCURRENT_ORDERED_CONTAINERS 1
#include <tbb/concurrent_map.h>
#endif
#include <unordered_set>
#include <map>
#include "impl.h"
#include "polygon.h"
@@ -39,7 +39,6 @@ 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;
@@ -127,21 +126,19 @@ 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(halfedge_.cbegin() + faceEdge[face],
halfedge_.cbegin() + faceEdge[face + 1], projection);
const PolygonsIdx polys = Face2Polygons(face, projection, faceEdge);
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<size_t> triCount(faceEdge.size());
Vec<int> 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_z),
countAt(faceEdge.size() - 1), [&](size_t face) {
for_each(autoPolicy(faceEdge.size()), countAt(0),
countAt(faceEdge.size() - 1), [&](int face) {
triCount[face] = faceEdge[face + 1] - faceEdge[face] - 2;
ASSERT(triCount[face] >= 1, topologyErr,
"face has less than three edges.");
@@ -155,15 +152,13 @@ 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_z);
if (triCount.back() >= std::numeric_limits<int>::max())
throw std::out_of_range("too many triangles");
triCount.begin(), 0);
triVerts.resize(triCount.back());
triNormal.resize(triCount.back());
triRef.resize(triCount.back());
auto processFace2 = std::bind(
processFace, [&](size_t face) { return std::move(results[face]); },
processFace, [&](int face) { return std::move(results[face]); },
[&](int face, glm::ivec3 tri, glm::vec3 normal, TriRef r) {
triVerts[triCount[face]] = tri;
triNormal[triCount[face]] = normal;
@@ -172,7 +167,7 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
},
std::placeholders::_1);
// set triangles in parallel
for_each(autoPolicy(faceEdge.size()), countAt(0_z),
for_each(autoPolicy(faceEdge.size()), countAt(0),
countAt(faceEdge.size() - 1), processFace2);
#else
triVerts.reserve(faceEdge.size());
@@ -196,16 +191,20 @@ void Manifold::Impl::Face2Tri(const Vec<int>& faceEdge,
}
/**
* 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.
* For the input face index, return a set of 2D polygons formed by the input
* projection of the 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 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 polys;
@@ -218,104 +217,13 @@ PolygonsIdx Manifold::Impl::Face2Polygons(VecView<Halfedge>::IterC start,
thisEdge = startEdge;
polys.push_back({});
}
int vert = (start + thisEdge)->startVert;
int vert = halfedge_[thisEdge].startVert;
polys.back().push_back({projection * vertPos_[vert], vert});
const auto result = vert_edge.find((start + thisEdge)->endVert);
const auto result = vert_edge.find(halfedge_[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
+29 -55
View File
@@ -22,8 +22,6 @@
#include "hashtable.h"
#include "mesh_fixes.h"
#include "par.h"
#include "svd.h"
#include "tri_dist.h"
namespace {
using namespace manifold;
@@ -322,7 +320,6 @@ 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());
@@ -352,11 +349,6 @@ 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;
@@ -375,8 +367,7 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
}
if (!meshGL.runOriginalID.empty() && !meshGL.runIndex.empty() &&
meshGL.runOriginalID.size() + 1 != meshGL.runIndex.size() &&
meshGL.runOriginalID.size() != meshGL.runIndex.size()) {
meshGL.runOriginalID.size() + 1 != meshGL.runIndex.size()) {
MarkFailure(Error::RunIndexWrongLength);
return;
}
@@ -388,7 +379,7 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
std::vector<int> prop2vert(numVert);
std::iota(prop2vert.begin(), prop2vert.end(), 0);
for (size_t i = 0; i < meshGL.mergeFromVert.size(); ++i) {
for (int i = 0; i < meshGL.mergeFromVert.size(); ++i) {
const int from = meshGL.mergeFromVert[i];
const int to = meshGL.mergeToVert[i];
if (from >= numVert || to >= numVert) {
@@ -397,8 +388,8 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
}
prop2vert[from] = to;
}
for (size_t i = 0; i < numTri; ++i) {
for (const size_t j : {0, 1, 2}) {
for (int i = 0; i < numTri; ++i) {
for (const int j : {0, 1, 2}) {
const int vert = meshGL.triVerts[3 * i + j];
if (vert < 0 || vert >= numVert) {
MarkFailure(Error::VertexOutOfBounds);
@@ -412,8 +403,8 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
if (meshGL.numProp > 3) {
relation.triProperties.resize(numTri);
for (size_t i = 0; i < numTri; ++i) {
for (const size_t j : {0, 1, 2}) {
for (int i = 0; i < numTri; ++i) {
for (const int j : {0, 1, 2}) {
relation.triProperties[i][j] = meshGL.triVerts[3 * i + j];
}
}
@@ -444,11 +435,8 @@ 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, runEnd};
} else if (runIndex.size() == meshGL.runOriginalID.size()) {
runIndex.push_back(runEnd);
runIndex = {0, 3 * meshGL.NumTri()};
}
relation.triRef.resize(meshGL.NumTri());
const int startID = Impl::ReserveIDs(meshGL.runOriginalID.size());
@@ -490,13 +478,11 @@ 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 (size_t i = 0; i < mesh.triVerts.size(); ++i) {
for (int 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]) {
@@ -546,7 +532,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, const glm::mat4x3 m) {
Manifold::Impl::Impl(Shape shape) {
std::vector<glm::vec3> vertPos;
std::vector<glm::ivec3> triVerts;
switch (shape) {
@@ -559,19 +545,19 @@ Manifold::Impl::Impl(Shape shape, const glm::mat4x3 m) {
break;
case Shape::Cube:
vertPos = {{0.0f, 0.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, 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}};
{0.0f, 1.0f, 0.0f}, //
{0.0f, 0.0f, 1.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}};
break;
case Shape::Octahedron:
vertPos = {{1.0f, 0.0f, 0.0f}, //
@@ -587,7 +573,6 @@ Manifold::Impl::Impl(Shape shape, const glm::mat4x3 m) {
break;
}
vertPos_ = vertPos;
for (auto& v : vertPos_) v = m * glm::vec4(v, 1.0f);
CreateHalfedges(triVerts);
Finish();
meshRelation_.originalID = ReserveIDs(1);
@@ -596,7 +581,6 @@ Manifold::Impl::Impl(Shape shape, const glm::mat4x3 m) {
}
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(),
@@ -629,7 +613,6 @@ 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;
@@ -678,7 +661,6 @@ 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
@@ -732,14 +714,7 @@ void Manifold::Impl::MarkFailure(Error status) {
}
void Manifold::Impl::Warp(std::function<void(glm::vec3&)> 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());
thrust::for_each_n(thrust::host, vertPos_.begin(), NumVert(), warpFunc);
CalculateBBox();
if (!IsFinite()) {
MarkFailure(Error::NonFiniteVertex);
@@ -750,11 +725,11 @@ void Manifold::Impl::WarpBatch(
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;
@@ -801,8 +776,11 @@ Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3& transform_) const {
if (!result.collider_.Transform(transform_)) result.Update();
result.CalculateBBox();
// Scale the precision by the norm of the 3x3 portion of the transform.
result.precision_ *= SpectralNorm(glm::mat3(transform_));
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;
// Maximum of inherited precision loss and translational precision loss.
result.SetPrecision(result.precision_);
return result;
@@ -829,7 +807,6 @@ 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));
@@ -872,7 +849,6 @@ 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);
@@ -887,10 +863,10 @@ SparseIndices Manifold::Impl::EdgeCollisions(const Impl& Q,
q1p2 = collider_.Collisions<false, false>(QedgeBB.cview());
if (inverted)
for_each(policy, countAt(0_z), countAt(q1p2.size()),
for_each(policy, countAt(0), countAt(q1p2.size()),
ReindexEdge<true>({edges, q1p2}));
else
for_each(policy, countAt(0_z), countAt(q1p2.size()),
for_each(policy, countAt(0), countAt(q1p2.size()),
ReindexEdge<false>({edges, q1p2}));
return q1p2;
}
@@ -901,11 +877,9 @@ 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
+7 -59
View File
@@ -42,9 +42,6 @@ struct Manifold::Impl {
Vec<TriRef> triRef;
Vec<glm::ivec3> triProperties;
};
struct BaryIndices {
int tri, start4, end4;
};
Box bBox_;
float precision_ = -1;
@@ -62,36 +59,13 @@ struct Manifold::Impl {
Impl() {}
enum class Shape { Tetrahedron, Cube, Octahedron };
Impl(Shape, const glm::mat4x3 = glm::mat4x3(1));
Impl(Shape);
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();
@@ -102,7 +76,6 @@ 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,
@@ -129,7 +102,6 @@ struct Manifold::Impl {
bool Is2Manifold() const;
bool MatchesTriNormals() const;
int NumDegenerateTris() const;
float MinGap(const Impl& other, float searchLength) const;
// sort.cu
void Finish();
@@ -143,11 +115,8 @@ struct Manifold::Impl {
// face_op.cu
void Face2Tri(const Vec<int>& faceEdge, const Vec<TriRef>& halfedgeRef);
PolygonsIdx Face2Polygons(VecView<Halfedge>::IterC start,
VecView<Halfedge>::IterC end,
glm::mat3x2 projection) const;
CrossSection Slice(float height) const;
CrossSection Project() const;
PolygonsIdx Face2Polygons(int face, glm::mat3x2 projection,
const Vec<int>& faceEdge) const;
// edge_op.cu
void SimplifyTopology();
@@ -163,30 +132,9 @@ struct Manifold::Impl {
void CollapseTri(const glm::ivec3& triEdge);
void SplitPinchedVerts();
// 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;
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)>);
// smoothing.cu
void CreateTangents(const std::vector<Smoothness>&);
Vec<Barycentric> Subdivide(int n);
void Refine(int n);
};
} // namespace manifold

Some files were not shown because too many files have changed in this diff Show More