Compare commits

..
Author SHA1 Message Date
Emmett Lalish 6f42bc7406 forgot file 2023-04-04 11:49:52 -07:00
Emmett Lalish 804e659e6d added assimp as submodule again 2023-04-04 11:44:59 -07:00
155 changed files with 11553 additions and 17229 deletions
-56
View File
@@ -1,56 +0,0 @@
name: build_wheels
on: [release, workflow_dispatch]
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, windows-latest, macos-latest]
steps:
- run: |
git config --global submodule.fetchJobs 8
git config --global core.longpaths true
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build wheels
uses: pypa/cibuildwheel@v2.15.0
- uses: actions/upload-artifact@v3
with:
path: ./wheelhouse/*.whl
make_sdist:
name: Make SDist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
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@v3
with:
path: dist/*.tar.gz
pypi-publish-beta:
name: Upload release to PyPI
needs: [build_wheels, make_sdist]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/manifold3d
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/download-artifact@v3
with:
name: artifact
path: dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
with:
source: '.'
exclude: '*/third_party'
extensions: 'h,cpp,js,ts,html'
extensions: 'h,cpp'
clangFormatVersion: 12
+2 -1
View File
@@ -20,8 +20,9 @@ jobs:
mkdir public
mv ./samples ./public
mv ./docs ./public
mv ./bindings/wasm/examples/* ./public
- name: Download built examples
- name: Download WASM files
uses: dawidd6/action-download-artifact@v2
with:
workflow: manifold.yml
+64 -67
View File
@@ -12,45 +12,47 @@ on:
jobs:
build:
timeout-minutes: 45
timeout-minutes: 30
strategy:
matrix:
parallel_backend: [NONE, TBB]
cuda_support: [ON, OFF]
parallel_backend: [NONE, OMP, TBB]
runs-on: ubuntu-22.04
if: github.event.pull_request.draft == false
container:
image: docker://nvidia/cuda:12.0.1-devel-ubuntu22.04
steps:
- name: Install dependencies
run: |
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
apt-get -y update
DEBIAN_FRONTEND=noninteractive apt install -y libomp-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@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_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
- name: Test ${{matrix.parallel_backend}}
if: matrix.parallel_backend != 'NONE'
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} .. && make
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
# note that the test for CUDA backend does not really test CUDA, as we
# don't have CUDA GPU on GitHub Action
run: |
export PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python
cd build/test
./manifold_test
cd ../../
python3 bindings/python/examples/run_all.py -e
python3 bindings/python/examples/run_all.py
- name: Coverage Report
# only do code coverage for default sequential backend, it seems that TBB
# backend will cause failure
# perhaps issue related to invalid memory access?
if: matrix.parallel_backend == 'NONE'
if: matrix.parallel_backend == 'NONE' && matrix.cuda_support == 'OFF'
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DCODE_COVERAGE=ON .. && make
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} -DCODE_COVERAGE=ON .. && make
lcov --capture --initial --directory . --output-file ./code_coverage_init.info
cd test
./manifold_test
@@ -59,31 +61,35 @@ jobs:
lcov --add-tracefile ./code_coverage_init.info --add-tracefile ./code_coverage_test.info --output-file ./code_coverage_total.info
lcov --remove ./code_coverage_total.info '/usr/*' '*/third_party/*' '*/test/*' '*/extras/*' '*/bindings/*' --output-file ./code_coverage.info
- uses: codecov/codecov-action@v2
if: matrix.parallel_backend == 'NONE'
if: matrix.parallel_backend == 'NONE' && matrix.cuda_support == 'OFF'
with:
files: build/code_coverage.info
fail_ci_if_error: false
name: ${{matrix.parallel_backend}}
name: ${{matrix.parallel_backend}}-${{matrix.cuda_support}}
build_cbind:
timeout-minutes: 30
runs-on: ubuntu-22.04
if: github.event.pull_request.draft == false
container:
image: docker://nvidia/cuda:12.0.1-devel-ubuntu22.04
steps:
- name: Install dependencies
run: |
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
apt-get -y update
DEBIAN_FRONTEND=noninteractive apt install -y libomp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v1.12
- name: Build C bindings with TBB
- name: Build C bindings with OMP and CUDA
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=TBB .. && make
- name: Test ${{matrix.parallel_backend}}
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=OMP -DMANIFOLD_USE_CUDA=ON .. && make
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
# note that the test for CUDA backend does not really test CUDA, as we
# don't have CUDA GPU on GitHub Action
run: |
cd build/test
./manifold_test --gtest_filter=CBIND.*
@@ -96,7 +102,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs libglm-dev
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs
- uses: actions/checkout@v3
with:
submodules: recursive
@@ -118,24 +124,27 @@ jobs:
run: |
cd build/test
node ./manifold_test.js
- name: Test examples
run: |
cd bindings/wasm/examples
npm ci
npm run build
cd ../bindings/wasm
npm install
npm test
- name: Upload WASM files
uses: actions/upload-artifact@v3
with:
name: wasm
path: bindings/wasm/examples/dist/
path: |
build/bindings/wasm/manifold.js
build/bindings/wasm/manifold.wasm
build/bindings/wasm/manifold.d.ts
build/bindings/wasm/manifold-global-types.d.ts
build/bindings/wasm/manifold-encapsulated-types.d.ts
retention-days: 90
build_windows:
timeout-minutes: 30
strategy:
matrix:
parallel_backend: [NONE, TBB]
parallel_backend: [NONE]
cuda_support: [OFF]
max-parallel: 1
runs-on: windows-2019
if: github.event.pull_request.draft == false
@@ -145,40 +154,35 @@ jobs:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v1.12
- uses: ilammy/msvc-dev-cmd@v1
- name: Install CUDA
if: matrix.backend == 'CUDA'
env:
cuda: "11.3.0"
visual_studio: "Visual Studio 16 2019"
shell: powershell
run: |
Invoke-RestMethod https://raw.githubusercontent.com/ptheywood/cuda-cmake-github-actions/21811d232fc775fa8d44dddf7f8c5ce2cf8d2b81/scripts/actions/install_cuda_windows.ps1 -OutFile install_cuda_windows.ps1
.\install_cuda_windows.ps1
- name: nvcc check
if: matrix.backend == 'CUDA'
shell: powershell
run: |
nvcc -V
ls $env:CUDA_PATH
ls $env:CUDA_PATH\bin
ls $env:CUDA_PATH\include
- name: Build ${{matrix.backend}}
shell: powershell
run: |
cmake . -DCMAKE_BUILD_TYPE=Release -B build -DMANIFOLD_DEBUG=ON -DPYBIND11_FINDPYTHON=OFF -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64
cmake . -DCMAKE_BUILD_TYPE=Release -B build -DMANIFOLD_DEBUG=ON -DPYBIND11_FINDPYTHON=OFF -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} -A x64
cd build
cmake --build . --target ALL_BUILD --config Release
- name: Test ${{matrix.parallel_backend}}
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
shell: bash
run: |
cd build/test
./Release/manifold_test.exe
build_mxe:
timeout-minutes: 30
strategy:
matrix:
parallel_backend: [NONE, TBB]
max-parallel: 1
runs-on: ubuntu-latest
container: openscad/mxe-x86_64-gui:latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Build
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
- name: Test
run: |
cd build/test
./manifold_test
build_mac:
timeout-minutes: 30
strategy:
@@ -189,8 +193,11 @@ jobs:
steps:
- name: Install common dependencies
run: |
brew install pkg-config googletest assimp
brew install pkg-config
pip install trimesh
- name: Install OpenMP
if: matrix.parallel_backend == 'OMP'
run: brew install libomp
- name: Install TBB
if: matrix.parallel_backend == 'TBB'
run: brew install tbb
@@ -202,7 +209,7 @@ jobs:
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
- name: Test
run: |
cd build/test
@@ -212,23 +219,13 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
variant: [none, tbb, js]
variant: [none, omp, tbb, none-cuda, omp-cuda, tbb-cuda, js]
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: cachix/install-nix-action@v22
- run: nix build -L '.?submodules=1#manifold-${{matrix.variant}}'
build_nix_python:
timeout-minutes: 30
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: cachix/install-nix-action@v22
- run: nix build -L '.?submodules=1#manifold3d'
- uses: cachix/install-nix-action@v15
- run: |
nix build -L '.?submodules=1#manifold-${{matrix.variant}}'
+1 -7
View File
@@ -109,14 +109,8 @@ build
buildWASM
docs/html
node_modules/
bindings/wasm/examples/built
bindings/wasm/examples/dist
bindings/wasm/examples/public/manifold*
__pycache__
.vscode/c_cpp_properties.json
*.sublime-project
*.sublime-workspace
.cache
# PyCharm
.idea
.cache
+16 -6
View File
@@ -1,9 +1,19 @@
[submodule "third_party/thrust"]
path = src/third_party/thrust
url = https://github.com/NVIDIA/thrust
[submodule "third_party/glm"]
path = src/third_party/glm
url = https://github.com/g-truc/glm
[submodule "third_party/google_test"]
path = test/third_party/google_test
url = https://github.com/google/googletest.git
[submodule "third_party/pybind11"]
path = bindings/python/third_party/pybind11
url = https://github.com/pybind/pybind11
branch = stable
[submodule "src/third_party/clipper2"]
path = src/third_party/clipper2
url = https://github.com/AngusJohnson/clipper2
[submodule "src/third_party/quickhull"]
path = src/third_party/quickhull
url = https://github.com/akuukka/quickhull
[submodule "bindings/python/third_party/nanobind"]
path = bindings/python/third_party/nanobind
url = https://github.com/wjakob/nanobind
[submodule "meshIO/third_party/assimp"]
path = meshIO/third_party/assimp
url = https://github.com/assimp/assimp.git
+22 -22
View File
@@ -1,26 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "manifold test",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/test/manifold_test",
"args": [
"--gtest_break_on_failure",
"--gtest_filter=Manifold.GetMesh"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build/test",
"environment": [
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "MALLOC_CHECK_",
"value": "2"
"name": "manifold test",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/test/manifold_test",
"args": [
"--gtest_break_on_failure",
"--gtest_filter=Manifold.GetMesh"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build/test",
"environment": [
{
"name": "MALLOC_CHECK_",
"value": "2"
}
]
}
]
}
]
]
}
+2 -19
View File
@@ -98,27 +98,16 @@
"concepts": "cpp",
"queue": "cpp",
"stack": "cpp",
"__hash_table": "cpp",
"__std_stream": "cpp",
"__verbose_abort": "cpp",
"any": "cpp",
"barrier": "cpp",
"charconv": "cpp",
"coroutine": "cpp",
"latch": "cpp",
"semaphore": "cpp",
"span": "cpp",
"__sso_allocator": "cpp"
"__hash_table": "cpp"
},
"C_Cpp.clang_format_fallbackStyle": "google",
"editor.formatOnSave": true,
"cmake.configureArgs": [
"-DBUILD_SHARED_LIBS=ON",
"-DMANIFOLD_CBIND=ON",
"-DMANIFOLD_EXPORT=ON",
"-DMANIFOLD_DEBUG=ON",
"-DMANIFOLD_USE_CUDA=OFF",
"-DMANIFOLD_PAR=TBB",
"-DMANIFOLD_PAR=NONE",
"-DCODE_COVERAGE=OFF",
"-DCMAKE_CXX_FLAGS=''" //'-fsanitize=address,undefined'"
],
@@ -131,7 +120,6 @@
"editor.detectIndentation": false,
"cSpell.words": [
"Bary",
"Coplanarity",
"csaszar",
"Gyroid",
"halfedge",
@@ -140,9 +128,4 @@
"Verts"
],
"javascript.format.semicolons": "insert",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none",
"clang-format.executable": "clang-format",
}
+43 -44
View File
@@ -16,73 +16,72 @@ cmake_minimum_required(VERSION 3.18)
project(manifold LANGUAGES CXX)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(MANIFOLD_PAR "NONE" CACHE STRING "Parallel backend, either \"TBB\" or \"NONE\"")
set(MANIFOLD_FLAGS "" CACHE STRING "Manifold compiler flags")
option(MANIFOLD_EXPORT "Build mesh export (via assimp) utility library" OFF)
option(MANIFOLD_TEST "Enable testing suite" ON)
option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF)
option(MANIFOLD_USE_CUDA "Enable GPU support via CUDA" OFF)
option(MANIFOLD_PYBIND "Build python bindings" ON)
option(MANIFOLD_CBIND "Build C (FFI) bindings" OFF)
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)
option(BUILD_TEST_CGAL "Build CGAL performance comparisons" OFF)
if(TRACY_ENABLE)
option(CMAKE_BUILD_TYPE "Build type" RelWithDebInfo)
if(TRACY_MEMORY_USAGE)
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} -DTRACY_MEMORY_USAGE)
endif()
if(NOT MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer")
endif()
else()
option(CMAKE_BUILD_TYPE "Build type" Release)
endif()
set(MANIFOLD_PAR "NONE" CACHE STRING "Parallel backend, either \"TBB\" or \"OpenMP\" or \"NONE\"")
set(MANIFOLD_FLAGS -O3)
if(EMSCRIPTEN)
message("Building for Emscripten")
set(MANIFOLD_FLAGS -fexceptions -D_LIBCUDACXX_HAS_THREAD_API_EXTERNAL -D_LIBCUDACXX_HAS_THREAD_API_CUDA)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -sALLOW_MEMORY_GROWTH=1 -fexceptions -sDISABLE_EXCEPTION_CATCHING=0")
set(MANIFOLD_PYBIND OFF)
set(CMAKE_EXE_LINKER_FLAGS ${CMAKE_EXE_LINKER_FLAGS} -sALLOW_MEMORY_GROWTH=1)
endif()
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()
option(PYBIND11_FINDPYTHON "Enable PyBind to perform FindPython for you" ON)
option(BUILD_TEST_CGAL "Build CGAL performance comparisons" OFF)
option(BUILD_SHARED_LIBS "Build dynamic/shared libraries" OFF)
set(PYBIND11_DIR ${PROJECT_SOURCE_DIR}/bindings/python/third_party/pybind11)
set(THRUST_INC_DIR ${PROJECT_SOURCE_DIR}/src/third_party/thrust)
if(MANIFOLD_CBIND)
set(MANIFOLD_EXPORT ON)
endif()
if(CMAKE_EXPORT_COMPILE_COMMANDS AND NOT EMSCRIPTEN)
# for nixos
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES
${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
if(BUILD_SHARED_LIBS OR MANIFOLD_CBIND)
# Allow shared libraries to be relocatable (add Place Independent Code flag).
# Also include when statically linking C bindings to avoid issues with bundling
# artefacts in host languages using them for FFI.
add_compile_options(-fPIC)
endif()
if (MSVC)
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} /DNOMINMAX)
else()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused -Wno-array-bounds -Wno-stringop-overflow)
else()
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
endif()
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${WARNING_FLAGS} -O3)
if(MANIFOLD_USE_CUDA)
enable_language(CUDA)
find_package(CUDA REQUIRED)
# we cannot set THRUST_INC_DIR when building with CUDA, otherwise the
# compiler will not use the system CUDA headers which causes incompatibility
# clear THRUST_INC_DIR, we use the one from nvcc
set(THRUST_INC_DIR "")
set(MANIFOLD_NVCC_RELEASE_FLAGS -O3 -lineinfo)
set(MANIFOLD_NVCC_DEBUG_FLAGS -G)
set(MANIFOLD_NVCC_FLAGS -Xcudafe --diag_suppress=esa_on_defaulted_function_ignored --extended-lambda --expt-relaxed-constexpr
"$<$<CONFIG:RELEASE>:${MANIFOLD_NVCC_RELEASE_FLAGS}>"
"$<$<CONFIG:DEBUG>:${MANIFOLD_NVCC_DEBUG_FLAGS}>")
endif()
if(NOT MSVC)
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
add_compile_options(
"$<$<COMPILE_LANGUAGE:CXX>:${WARNING_FLAGS}>"
"$<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=${WARNING_FLAGS}>")
endif()
if(CODE_COVERAGE AND NOT MSVC)
set(COVERAGE_FLAGS -coverage -fno-inline-small-functions -fkeep-inline-functions -fkeep-static-functions)
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${COVERAGE_FLAGS})
add_compile_options(
"$<$<COMPILE_LANGUAGE:CXX>:${COVERAGE_FLAGS}>"
"$<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=-coverage>")
add_link_options("-coverage")
endif()
include(GNUInstallDirs)
add_subdirectory(src)
add_subdirectory(bindings)
+19 -48
View File
@@ -3,7 +3,7 @@
## 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.
[OpenSCAD](https://openscad.org/), [IFCjs](https://ifcjs.github.io/info/), [Grid.Space](https://grid.space/), and [OCADml](https://github.com/OCADml/OManifold) have all integrated our Manifold geometry kernel! Why? Because its reliability is guaranteed and it's 1,000 times faster than other libraries. See our [usage](https://github.com/elalish/manifold/discussions/340) and [performance](https://github.com/elalish/manifold/discussions/383) discussions for all the latest and to add your own projects & analyses.
For example, here is a log-log plot of Manifold's performance vs. earlier OpenSCAD geometry backends:
@@ -11,51 +11,37 @@ For example, here is a log-log plot of Manifold's performance vs. earlier OpenSC
## [ManifoldCAD.org](https://manifoldcad.org)
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.
If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid modelling web app. Our WASM is not GPU-accelerated, but it's still quite fast and a good way to test out our Manifold library.
![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/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, 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.
- [`Thrust`](https://github.com/NVIDIA/thrust): NVIDIA's parallel algorithms library (basically a superset of C++17 std::parallel_algorithms)
- [`tbb`](https://github.com/oneapi-src/oneTBB/): Intel's thread building blocks library. (only when `MANIFOLD_PAR=TBB` is enabled)
- [`gtest`](https://github.com/google/googletest/): Google test library (only when test is enabled, i.e. `MANIFOLD_TEST=ON`)
Other dependencies:
- [`Clipper2`](https://github.com/AngusJohnson/Clipper2): provides our 2D subsystem
- [`quickhull`](https://github.com/akuukka/quickhull): 3D convex hull algorithm.
We have four core dependencies, making use of submodules to ensure compatibility:
- `graphlite`: connected components algorithm
- `Clipper2`: provides our 2D subsystem
- `GLM`: a compact vector library
- `Thrust`: Nvidia's parallel algorithms library (basically a superset of C++17 std::parallel_algorithms)
## What's here
This library is fast with guaranteed manifold output. As such you need manifold meshes as input, which this library can create using constructors inspired by the OpenSCAD API, as well as more advanced features like smoothing and signed-distance function (SDF) level sets. You can also pass in your own mesh data, but you'll get an error status if the imported mesh isn't manifold. Various automated repair tools exist online for fixing non manifold models, usually for 3D printing.
This library is fast with guaranteed manifold output. As such you need manifold meshes as input, which this library can create using constructors inspired by the OpenSCAD API, as well as more advanced features like smoothing and signed-distance function (SDF) level sets. You can also pass in your own mesh data, but you'll get an error status if the imported mesh isn't manifold. Various automated repair tools exist online for fixing non manifold models, usually for 3D printing.
The most significant contribution here is a guaranteed-manifold [mesh Boolean](https://github.com/elalish/manifold/wiki/Manifold-Library#mesh-boolean) algorithm, which I believe is the first of its kind. If you know of another, please open a discussion - a mesh Boolean algorithm robust to edge cases has been an open problem for many years. Likewise, if the Boolean here ever fails you, please submit an issue! This Boolean forms the basis of a CAD kernel, as it allows simple shapes to be combined into more complex ones.
To aid in speed, this library makes extensive use of parallelization, generally through Nvidia's Thrust library. You can switch between the TBB, and serial C++ backends by setting a CMake flag. Not everything is so parallelizable, for instance a [polygon triangulation](https://github.com/elalish/manifold/wiki/Manifold-Library#polygon-triangulation) algorithm is included which is serial. Even if compiled with parallel backend, the code will still fall back to the serial version of the algorithms if the problem size is small. The WASM build is serial-only for now, but still fast.
> Note: OMP and CUDA backends are now removed
To aid in speed, this library makes extensive use of parallelization, generally through Nvidia's Thrust library. You can switch between the CUDA, OMP, TBB, and serial C++ backends by setting a CMake flag. Not everything is so parallelizable, for instance a [polygon triangulation](https://github.com/elalish/manifold/wiki/Manifold-Library#polygon-triangulation) algorithm is included which is serial. Even if compiled for CUDA, the code will still run without a GPU, falling back to the serial version of the algorithms. The WASM build is serial-only for now, but still fast.
Look in the [samples](https://github.com/elalish/manifold/tree/master/samples) directory for examples of how to use this library to make interesting 3D models. You may notice that some of these examples bare a certain resemblance to my OpenSCAD designs on [Thingiverse](https://www.thingiverse.com/emmett), which is no accident. Much as I love OpenSCAD, my library is dramatically faster and the code is more flexible.
## Building
Only CMake, a C++ compiler, and Python are required to be installed and set up to build this library (it has been tested with GCC, LLVM, MSVC). However, a variety of optional dependencies can bring in more functionality, see below.
Only CMake, a C++ compiler, and Python are required to be installed and set up to build this library (it has been tested with GCC, LLVM, MSVC). However, a variety of optional dependencies can bring in more functionality, see below.
Build and test (Ubuntu or similar):
```
@@ -67,23 +53,13 @@ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON .. && make
test/manifold_test
```
CMake flags (usage e.g. `-DMANIFOLD_DEBUG=ON`):
- `MANIFOLD_JSBIND=[OFF, <ON>]`: Build js binding when using emscripten.
- `MANIFOLD_CBIND=[<OFF>, ON]`: Build C FFI binding.
- `MANIFOLD_PYBIND=[OFF, <ON>]`: Build python binding.
- `MANIFOLD_PAR=[<NONE>, TBB]`: Provides multi-thread parallelization, requires `libtbb-dev` if `TBB` backend is selected.
- `MANIFOLD_EXPORT=[<OFF>, ON]`: Enables GLB export of 3D models from the tests, requires `libassimp-dev`.
CMake flags (usage e.g. `-DMANIFOLD_USE_CUDA=ON`):
- `MANIFOLD_USE_CUDA=[<OFF>, ON]`: Provides Nvidia GPU parallelization, requires CUDA Toolkit and its nvcc compiler.
- `MANIFOLD_PAR=[<NONE>, OMP, TBB]`: Provides multi-thread parallelization, requires `libomp-dev` or `libtbb-dev`.
- `MANIFOLD_EXPORT=[<OFF>, ON]`: Enables GLB export of 3D models from the tests, uses `assimp`.
- `MANIFOLD_DEBUG=[<OFF>, ON]`: Enables internal assertions and exceptions.
- `MANIFOLD_TEST=[OFF, <ON>]`: Build unittests.
- `TRACY_ENABLE=[<OFF>, ON]`: Enable integration with tracy profiler.
See profiling section below.
- `BUILD_TEST_CGAL=[<OFF>, ON]`: Builds a CGAL-based performance [comparison](https://github.com/elalish/manifold/tree/master/extras), requires `libcgal-dev`.
Offline building:
- `FETCHCONTENT_SOURCE_DIR_GLM`: path to glm source.
- `FETCHCONTENT_SOURCE_DIR_GOOGLETEST`: path to googletest source.
- `FETCHCONTENT_SOURCE_DIR_THRUST`: path to NVIDIA thrust source.
The build instructions used by our CI are in [manifold.yml](https://github.com/elalish/manifold/blob/master/.github/workflows/manifold.yml), which is a good source to check if something goes wrong and for instructions specific to other platforms, like Windows.
### WASM
@@ -114,8 +90,8 @@ node test/manifold_test.js
### Python
The CMake script will build the python binding `manifold3d` automatically. To
use the extension, please add `$BUILD_DIR/bindings/python` to your `PYTHONPATH`, where
The CMake script will build the python binding `pymanifold` automatically. To
use the extension, please add `$BUILD_DIR/tools` to your `PYTHONPATH`, where
`$BUILD_DIR` is the build directory for CMake. Examples using the python binding
can be found in `bindings/python/examples`. To see exported samples, run:
```
@@ -128,8 +104,8 @@ Run the following code in the interpreter for
python binding documentation:
```
>>> import manifold3d
>>> help(manifold3d)
>>> import pymanifold
>>> help(pymanifold)
```
For more detailed documentation, please refer to the C++ API.
@@ -138,11 +114,6 @@ For more detailed documentation, please refer to the C++ API.
Contributions are welcome! A lower barrier contribution is to simply make a PR that adds a test, especially if it repros an issue you've found. Simply name it prepended with DISABLED_, so that it passes the CI. That will be a very strong signal to me to fix your issue. However, if you know how to fix it yourself, then including the fix in your PR would be much appreciated!
## Profiling
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.
## About the author
This library was started by [Emmett Lalish](https://elalish.blogspot.com/). I am currently a Google employee and this is my 20% project, not an official Google project. At my day job I'm the maintainer of [\<model-viewer\>](https://modelviewer.dev/). I was the first employee at a 3D video startup, [Omnivor](https://www.omnivor.io/), and before that I worked on 3D printing at Microsoft, including [3D Builder](https://www.microsoft.com/en-us/p/3d-builder/9wzdncrfj3t6?activetab=pivot%3Aoverviewtab). Originally an aerospace engineer, I started at a small DARPA contractor doing seedling projects, one of which became [Sea Hunter](https://en.wikipedia.org/wiki/Sea_Hunter). I earned my doctorate from the University of Washington in control theory and published some [papers](https://www.researchgate.net/scientific-contributions/75011026_Emmett_Lalish).
+1 -1
View File
@@ -20,6 +20,6 @@ if(NOT EMSCRIPTEN AND MANIFOLD_PYBIND)
add_subdirectory(python)
endif()
if(EMSCRIPTEN AND MANIFOLD_JSBIND)
if(EMSCRIPTEN)
add_subdirectory(wasm)
endif()
+8 -23
View File
@@ -1,33 +1,18 @@
project(manifoldc)
add_library(${PROJECT_NAME} manifoldc.cpp conv.cpp)
add_library(manifoldc manifoldc.cpp conv.cpp)
if(MANIFOLD_USE_CUDA)
set_source_files_properties(manifoldc.cpp conv.cpp PROPERTIES LANGUAGE CUDA)
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
endif()
if(MANIFOLD_EXPORT)
target_link_libraries(${PROJECT_NAME} PRIVATE meshIO)
target_compile_options(${PROJECT_NAME} PUBLIC -DMANIFOLD_EXPORT)
set_property(TARGET manifoldc PROPERTY CUDA_ARCHITECTURES 61)
endif()
target_link_libraries(
${PROJECT_NAME}
PRIVATE manifold sdf cross_section
manifoldc
PRIVATE manifold sdf meshIO graphlite cross_section
)
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}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT bindings
)
install(
FILES include/manifoldc.h include/types.h
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
COMPONENT devel
)
target_include_directories(manifoldc PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_compile_options(manifoldc PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(manifoldc PRIVATE cxx_std_17)
+3 -16
View File
@@ -1,20 +1,7 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <conv.h>
#include <manifold.h>
#include <public.h>
#include "conv.h"
#include "manifold.h"
#include "public.h"
#include "types.h"
ManifoldBox *manifold_box(void *mem, float x1, float y1, float z1, float x2,
+29 -17
View File
@@ -1,25 +1,13 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "conv.h"
#include <conv.h>
#include <manifold.h>
#include <meshIO.h>
#include <sdf.h>
#include <vector>
#include "cross_section.h"
#include "manifold.h"
#include "include/types.h"
#include "public.h"
#include "sdf.h"
#include "types.h"
ManifoldManifold *to_c(manifold::Manifold *m) {
@@ -54,6 +42,10 @@ ManifoldMeshGL *to_c(manifold::MeshGL *m) {
return reinterpret_cast<ManifoldMeshGL *>(m);
}
ManifoldCurvature *to_c(manifold::Curvature *m) {
return reinterpret_cast<ManifoldCurvature *>(m);
}
ManifoldOpType to_c(manifold::OpType optype) {
ManifoldOpType op = MANIFOLD_ADD;
switch (optype) {
@@ -119,6 +111,14 @@ ManifoldRect *to_c(manifold::Rect *m) {
return reinterpret_cast<ManifoldRect *>(m);
}
ManifoldMaterial *to_c(manifold::Material *m) {
return reinterpret_cast<ManifoldMaterial *>(m);
}
ManifoldExportOptions *to_c(manifold::ExportOptions *m) {
return reinterpret_cast<ManifoldExportOptions *>(m);
}
ManifoldVec2 to_c(glm::vec2 v) { return {v.x, v.y}; }
ManifoldVec3 to_c(glm::vec3 v) { return {v.x, v.y, v.z}; }
@@ -161,6 +161,10 @@ const manifold::MeshGL *from_c(ManifoldMeshGL *m) {
return reinterpret_cast<manifold::MeshGL const *>(m);
}
const manifold::Curvature *from_c(ManifoldCurvature *c) {
return reinterpret_cast<manifold::Curvature const *>(c);
}
OpType from_c(ManifoldOpType optype) {
auto op = OpType::Add;
switch (optype) {
@@ -217,6 +221,14 @@ const manifold::Rect *from_c(ManifoldRect *m) {
return reinterpret_cast<manifold::Rect const *>(m);
}
manifold::Material *from_c(ManifoldMaterial *mat) {
return reinterpret_cast<manifold::Material *>(mat);
}
manifold::ExportOptions *from_c(ManifoldExportOptions *options) {
return reinterpret_cast<manifold::ExportOptions *>(options);
}
glm::vec2 from_c(ManifoldVec2 v) { return glm::vec2(v.x, v.y); }
glm::vec3 from_c(ManifoldVec3 v) { return glm::vec3(v.x, v.y, v.z); }
+10 -40
View File
@@ -1,23 +1,10 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <conv.h>
#include <manifold.h>
#include <public.h>
#include <vector>
#include "conv.h"
#include "cross_section.h"
#include "manifold.h"
#include "public.h"
#include "types.h"
ManifoldCrossSection *manifold_cross_section_empty(void *mem) {
@@ -119,28 +106,11 @@ ManifoldCrossSection *manifold_cross_section_intersection(
return to_c(new (mem) CrossSection(cs));
}
ManifoldCrossSection *manifold_cross_section_hull(void *mem,
ManifoldCrossSection *cs) {
auto hulled = from_c(cs)->Hull();
return to_c(new (mem) CrossSection(hulled));
}
ManifoldCrossSection *manifold_cross_section_batch_hull(
void *mem, ManifoldCrossSectionVec *css) {
auto hulled = CrossSection::Hull(*from_c(css));
return to_c(new (mem) CrossSection(hulled));
}
ManifoldCrossSection *manifold_cross_section_hull_simple_polygon(
void *mem, ManifoldSimplePolygon *ps) {
auto hulled = CrossSection::Hull(*from_c(ps));
return to_c(new (mem) CrossSection(hulled));
}
ManifoldCrossSection *manifold_cross_section_hull_polygons(
void *mem, ManifoldPolygons *ps) {
auto hulled = CrossSection::Hull(*from_c(ps));
return to_c(new (mem) CrossSection(hulled));
ManifoldCrossSection *manifold_cross_section_rect_clip(void *mem,
ManifoldCrossSection *cs,
ManifoldRect *r) {
auto clipped = from_c(cs)->RectClip(*from_c(r));
return to_c(new (mem) CrossSection(clipped));
}
ManifoldCrossSection *manifold_cross_section_translate(void *mem,
@@ -199,9 +169,9 @@ ManifoldCrossSection *manifold_cross_section_simplify(void *mem,
ManifoldCrossSection *manifold_cross_section_offset(
void *mem, ManifoldCrossSection *cs, double delta, ManifoldJoinType jt,
double miter_limit, int circular_segments) {
double miter_limit, double arc_tolerance) {
auto offset =
from_c(cs)->Offset(delta, from_c(jt), miter_limit, circular_segments);
from_c(cs)->Offset(delta, from_c(jt), miter_limit, arc_tolerance);
return to_c(new (mem) CrossSection(offset));
}
+12 -20
View File
@@ -1,28 +1,14 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstring>
#include <manifold.h>
#include <meshIO.h>
#include <public.h>
#include <sdf.h>
#include <types.h>
#include <vector>
#include "cross_section.h"
#include "manifold.h"
#include "public.h"
#include "sdf.h"
#include "types.h"
using namespace manifold;
using ManifoldVec = std::vector<Manifold>;
using CrossSectionVec = std::vector<CrossSection>;
@@ -36,7 +22,10 @@ ManifoldMesh *to_c(manifold::Mesh *m);
ManifoldMeshGL *to_c(manifold::MeshGL *m);
ManifoldBox *to_c(manifold::Box *m);
ManifoldRect *to_c(manifold::Rect *m);
ManifoldCurvature *to_c(manifold::Curvature *m);
ManifoldError to_c(manifold::Manifold::Error error);
ManifoldMaterial *to_c(manifold::Material *m);
ManifoldExportOptions *to_c(manifold::ExportOptions *m);
ManifoldVec2 to_c(glm::vec2 v);
ManifoldVec3 to_c(glm::vec3 v);
ManifoldIVec3 to_c(glm::ivec3 v);
@@ -55,6 +44,9 @@ CrossSection::FillRule from_c(ManifoldFillRule fillrule);
CrossSection::JoinType from_c(ManifoldJoinType jt);
const manifold::Box *from_c(ManifoldBox *m);
const manifold::Rect *from_c(ManifoldRect *r);
const manifold::Curvature *from_c(ManifoldCurvature *m);
manifold::Material *from_c(ManifoldMaterial *mat);
manifold::ExportOptions *from_c(ManifoldExportOptions *options);
glm::vec2 from_c(ManifoldVec2 v);
glm::vec3 from_c(ManifoldVec3 v);
glm::ivec3 from_c(ManifoldIVec3 v);
+46 -101
View File
@@ -1,29 +1,12 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stddef.h>
#include <stdint.h>
#include "types.h"
#include <types.h>
#ifdef __cplusplus
#include <cstdint>
extern "C" {
#endif
// Polygons
ManifoldSimplePolygon *manifold_simple_polygon(void *mem, ManifoldVec2 *ps,
size_t length);
ManifoldPolygons *manifold_polygons(void *mem, ManifoldSimplePolygon **ps,
@@ -40,7 +23,6 @@ ManifoldVec2 manifold_polygons_get_point(ManifoldPolygons *ps, int simple_idx,
int pt_idx);
// Mesh Construction
ManifoldMeshGL *manifold_meshgl(void *mem, float *vert_props, size_t n_verts,
size_t n_props, uint32_t *tri_verts,
size_t n_tris);
@@ -66,15 +48,6 @@ 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);
// Manifold Vectors
@@ -111,14 +84,7 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
float normal_x, float normal_y,
float normal_z, float offset);
// Convex Hulls
ManifoldManifold *manifold_hull(void *mem, ManifoldManifold *m);
ManifoldManifold *manifold_batch_hull(void *mem, ManifoldManifoldVec *ms);
ManifoldManifold *manifold_hull_pts(void *mem, ManifoldVec3 *ps, size_t length);
// Manifold Transformations
ManifoldManifold *manifold_translate(void *mem, ManifoldManifold *m, float x,
float y, float z);
ManifoldManifold *manifold_rotate(void *mem, ManifoldManifold *m, float x,
@@ -136,7 +102,6 @@ ManifoldManifold *manifold_warp(void *mem, ManifoldManifold *m,
ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine);
// Manifold Shapes / Constructors
ManifoldManifold *manifold_empty(void *mem);
ManifoldManifold *manifold_copy(void *mem, ManifoldManifold *m);
ManifoldManifold *manifold_tetrahedron(void *mem);
@@ -162,7 +127,6 @@ ManifoldManifoldVec *manifold_decompose(void *mem, ManifoldManifold *m);
ManifoldManifold *manifold_as_original(void *mem, ManifoldManifold *m);
// Manifold Info
int manifold_is_empty(ManifoldManifold *m);
ManifoldError manifold_status(ManifoldManifold *m);
int manifold_num_vert(ManifoldManifold *m);
@@ -172,14 +136,13 @@ ManifoldBox *manifold_bounding_box(void *mem, ManifoldManifold *m);
float manifold_precision(ManifoldManifold *m);
int manifold_genus(ManifoldManifold *m);
ManifoldProperties manifold_get_properties(ManifoldManifold *m);
ManifoldCurvature *manifold_get_curvature(void *mem, ManifoldManifold *m);
ManifoldCurvatureBounds manifold_curvature_bounds(ManifoldCurvature *curv);
size_t manifold_curvature_vert_length(ManifoldCurvature *curv);
float *manifold_curvature_vert_mean(void *mem, ManifoldCurvature *curv);
float *manifold_curvature_vert_gaussian(void *mem, ManifoldCurvature *curv);
int manifold_get_circular_segments(float radius);
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));
ManifoldManifold *manifold_calculate_curvature(void *mem, ManifoldManifold *m,
int gaussian_idx, int mean_idx);
// CrossSection Shapes/Constructors
@@ -229,17 +192,9 @@ ManifoldCrossSection *manifold_cross_section_difference(
void *mem, ManifoldCrossSection *a, ManifoldCrossSection *b);
ManifoldCrossSection *manifold_cross_section_intersection(
void *mem, ManifoldCrossSection *a, ManifoldCrossSection *b);
// CrossSection Convex Hulls
ManifoldCrossSection *manifold_cross_section_hull(void *mem,
ManifoldCrossSection *cs);
ManifoldCrossSection *manifold_cross_section_batch_hull(
void *mem, ManifoldCrossSectionVec *css);
ManifoldCrossSection *manifold_cross_section_hull_simple_polygon(
void *mem, ManifoldSimplePolygon *ps);
ManifoldCrossSection *manifold_cross_section_hull_polygons(
void *mem, ManifoldPolygons *ps);
ManifoldCrossSection *manifold_cross_section_rect_clip(void *mem,
ManifoldCrossSection *cs,
ManifoldRect *r);
// CrossSection Transformation
@@ -267,7 +222,7 @@ ManifoldCrossSection *manifold_cross_section_simplify(void *mem,
double epsilon);
ManifoldCrossSection *manifold_cross_section_offset(
void *mem, ManifoldCrossSection *cs, double delta, ManifoldJoinType jt,
double miter_limit, int circular_segments);
double miter_limit, double arc_tolerance);
// CrossSection Info
@@ -301,9 +256,10 @@ ManifoldRect *manifold_rect_mul(void *mem, ManifoldRect *r, float x, float y);
int manifold_rect_does_overlap_rect(ManifoldRect *a, ManifoldRect *r);
int manifold_rect_is_empty(ManifoldRect *r);
int manifold_rect_is_finite(ManifoldRect *r);
ManifoldCrossSection *manifold_rect_as_cross_section(void *mem,
ManifoldRect *r);
// Bounding Box
ManifoldBox *manifold_box(void *mem, float x1, float y1, float z1, float x2,
float y2, float z2);
ManifoldVec3 manifold_box_min(ManifoldBox *b);
@@ -328,13 +284,11 @@ int manifold_box_does_overlap_box(ManifoldBox *a, ManifoldBox *b);
int manifold_box_is_finite(ManifoldBox *b);
// Static Quality Globals
void manifold_set_min_circular_angle(float degrees);
void manifold_set_min_circular_edge_length(float length);
void manifold_set_circular_segments(int number);
// Manifold Mesh Extraction
int manifold_meshgl_num_prop(ManifoldMeshGL *m);
int manifold_meshgl_num_vert(ManifoldMeshGL *m);
int manifold_meshgl_num_tri(ManifoldMeshGL *m);
@@ -356,47 +310,7 @@ float *manifold_meshgl_run_transform(void *mem, ManifoldMeshGL *m);
uint32_t *manifold_meshgl_face_id(void *mem, ManifoldMeshGL *m);
float *manifold_meshgl_halfedge_tangent(void *mem, ManifoldMeshGL *m);
// memory size
size_t manifold_manifold_size();
size_t manifold_manifold_vec_size();
size_t manifold_cross_section_size();
size_t manifold_cross_section_vec_size();
size_t manifold_simple_polygon_size();
size_t manifold_polygons_size();
size_t manifold_manifold_pair_size();
size_t manifold_meshgl_size();
size_t manifold_box_size();
size_t manifold_rect_size();
size_t manifold_curvature_size();
// destruction
void manifold_destruct_manifold(ManifoldManifold *m);
void manifold_destruct_manifold_vec(ManifoldManifoldVec *ms);
void manifold_destruct_cross_section(ManifoldCrossSection *m);
void manifold_destruct_cross_section_vec(ManifoldCrossSectionVec *csv);
void manifold_destruct_simple_polygon(ManifoldSimplePolygon *p);
void manifold_destruct_polygons(ManifoldPolygons *p);
void manifold_destruct_meshgl(ManifoldMeshGL *m);
void manifold_destruct_box(ManifoldBox *b);
void manifold_destruct_rect(ManifoldRect *b);
// pointer free + destruction
void manifold_delete_manifold(ManifoldManifold *m);
void manifold_delete_manifold_vec(ManifoldManifoldVec *ms);
void manifold_delete_cross_section(ManifoldCrossSection *cs);
void manifold_delete_cross_section_vec(ManifoldCrossSectionVec *csv);
void manifold_delete_simple_polygon(ManifoldSimplePolygon *p);
void manifold_delete_polygons(ManifoldPolygons *p);
void manifold_delete_meshgl(ManifoldMeshGL *m);
void manifold_delete_box(ManifoldBox *b);
void manifold_delete_rect(ManifoldRect *b);
// MeshIO / Export
#ifdef MANIFOLD_EXPORT
ManifoldMaterial *manifold_material(void *mem);
void manifold_material_set_roughness(ManifoldMaterial *mat, float roughness);
void manifold_material_set_metalness(ManifoldMaterial *mat, float metalness);
@@ -410,18 +324,49 @@ void manifold_export_options_set_material(ManifoldExportOptions *options,
ManifoldMaterial *mat);
void manifold_export_meshgl(const char *filename, ManifoldMeshGL *mesh,
ManifoldExportOptions *options);
ManifoldMeshGL *manifold_import_meshgl(void *mem, const char *filename,
int force_cleanup);
// memory size
size_t manifold_manifold_size();
size_t manifold_manifold_vec_size();
size_t manifold_cross_section_size();
size_t manifold_cross_section_vec_size();
size_t manifold_simple_polygon_size();
size_t manifold_polygons_size();
size_t manifold_manifold_pair_size();
size_t manifold_meshgl_size();
size_t manifold_box_size();
size_t manifold_rect_size();
size_t manifold_curvature_size();
size_t manifold_material_size();
size_t manifold_export_options_size();
// destruction
void manifold_destruct_manifold(ManifoldManifold *m);
void manifold_destruct_manifold_vec(ManifoldManifoldVec *ms);
void manifold_destruct_cross_section(ManifoldCrossSection *m);
void manifold_destruct_cross_section_vec(ManifoldCrossSectionVec *csv);
void manifold_destruct_simple_polygon(ManifoldSimplePolygon *p);
void manifold_destruct_polygons(ManifoldPolygons *p);
void manifold_destruct_meshgl(ManifoldMeshGL *m);
void manifold_destruct_box(ManifoldBox *b);
void manifold_destruct_rect(ManifoldRect *b);
void manifold_destruct_curvature(ManifoldCurvature *c);
void manifold_destruct_material(ManifoldMaterial *m);
void manifold_destruct_export_options(ManifoldExportOptions *options);
// pointer free + destruction
void manifold_delete_manifold(ManifoldManifold *m);
void manifold_delete_manifold_vec(ManifoldManifoldVec *ms);
void manifold_delete_cross_section(ManifoldCrossSection *cs);
void manifold_delete_cross_section_vec(ManifoldCrossSectionVec *csv);
void manifold_delete_simple_polygon(ManifoldSimplePolygon *p);
void manifold_delete_polygons(ManifoldPolygons *p);
void manifold_delete_meshgl(ManifoldMeshGL *m);
void manifold_delete_box(ManifoldBox *b);
void manifold_delete_rect(ManifoldRect *b);
void manifold_delete_curvature(ManifoldCurvature *c);
void manifold_delete_material(ManifoldMaterial *m);
void manifold_delete_export_options(ManifoldExportOptions *options);
#endif
#ifdef __cplusplus
}
+7 -22
View File
@@ -1,22 +1,6 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <stddef.h>
// opaque pointers
typedef struct ManifoldManifold ManifoldManifold;
typedef struct ManifoldManifoldVec ManifoldManifoldVec;
typedef struct ManifoldCrossSection ManifoldCrossSection;
@@ -25,15 +9,11 @@ typedef struct ManifoldSimplePolygon ManifoldSimplePolygon;
typedef struct ManifoldPolygons ManifoldPolygons;
typedef struct ManifoldMesh ManifoldMesh;
typedef struct ManifoldMeshGL ManifoldMeshGL;
typedef struct ManifoldCurvature ManifoldCurvature;
typedef struct ManifoldBox ManifoldBox;
typedef struct ManifoldRect ManifoldRect;
#ifdef MANIFOLD_EXPORT
typedef struct ManifoldMaterial ManifoldMaterial;
typedef struct ManifoldExportOptions ManifoldExportOptions;
#endif
// structs
typedef struct ManifoldManifoldPair {
ManifoldManifold* first;
@@ -69,7 +49,12 @@ typedef struct ManifoldProperties {
float volume;
} ManifoldProperties;
// enums
typedef struct ManifoldCurvatureBounds {
float max_mean_curvature;
float min_mean_curvature;
float max_gaussian_curvature;
float min_gaussian_curvature;
} ManifoldCurvatureBounds;
typedef enum ManifoldOpType {
MANIFOLD_ADD,
+27 -81
View File
@@ -1,21 +1,8 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <conv.h>
#include <cross_section.h>
#include <manifold.h>
#include <manifoldc.h>
#include <meshIO.h>
#include <public.h>
#include <sdf.h>
@@ -25,13 +12,10 @@
#include "cross.cpp"
#include "include/conv.h"
#include "include/types.h"
#include "meshio.cpp"
#include "rect.cpp"
#include "types.h"
#ifdef MANIFOLD_EXPORT
#include "meshio.cpp"
#endif
using namespace manifold;
namespace {
@@ -43,20 +27,8 @@ ManifoldMeshGL *level_set(void *mem, float (*sdf)(float, float, float),
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 =
std::bind(sdf_context, _1, _2, _3, ctx);
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);
auto pol = seq ? std::make_optional(ExecutionPolicy::Seq) : std::nullopt;
auto mesh = LevelSet(fun, *from_c(bounds), edge_length, level, pol);
return to_c(new (mem) MeshGL(mesh));
}
} // namespace
@@ -202,26 +174,6 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
return to_c(new (mem) Manifold(trimmed));
}
ManifoldManifold *manifold_hull(void *mem, ManifoldManifold *m) {
auto hulled = from_c(m)->Hull();
return to_c(new (mem) Manifold(hulled));
}
ManifoldManifold *manifold_batch_hull(void *mem, ManifoldManifoldVec *ms) {
auto hulled = Manifold::Hull(*from_c(ms));
return to_c(new (mem) Manifold(hulled));
}
ManifoldManifold *manifold_hull_pts(void *mem, ManifoldVec3 *ps,
size_t length) {
std::vector<glm::vec3> vec(length);
for (int i = 0; i < length; ++i) {
vec[i] = {ps[i].x, ps[i].y, ps[i].z};
}
auto hulled = Manifold::Hull(vec);
return to_c(new (mem) Manifold(hulled));
}
ManifoldManifold *manifold_translate(void *mem, ManifoldManifold *m, float x,
float y, float z) {
auto v = glm::vec3(x, y, z);
@@ -279,18 +231,6 @@ ManifoldMeshGL *manifold_level_set_seq(void *mem,
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_context(mem, sdf, bounds, edge_length, level, false, ctx);
}
ManifoldMeshGL *manifold_level_set_seq_context(
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
float edge_length, float level, void *ctx) {
return level_set_context(mem, sdf, bounds, edge_length, level, true, ctx);
}
ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine) {
auto refined = from_c(m)->Refine(refine);
return to_c(new (mem) Manifold(refined));
@@ -506,26 +446,27 @@ ManifoldBox *manifold_bounding_box(void *mem, ManifoldManifold *m) {
}
float manifold_precision(ManifoldManifold *m) { return from_c(m)->Precision(); }
ManifoldCurvature *manifold_get_curvature(void *mem, ManifoldManifold *m) {
auto curv = from_c(m)->GetCurvature();
return to_c(new (mem) Curvature(curv));
}
uint32_t manifold_reserve_ids(uint32_t n) { return Manifold::ReserveIDs(n); }
ManifoldCurvatureBounds manifold_curvature_bounds(ManifoldCurvature *curv) {
auto c = *from_c(curv);
return {c.maxMeanCurvature, c.minMeanCurvature, c.maxGaussianCurvature,
c.minGaussianCurvature};
}
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 =
[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));
};
size_t manifold_curvature_vert_length(ManifoldCurvature *curv) {
return from_c(curv)->vertMeanCurvature.size();
}
ManifoldManifold *manifold_calculate_curvature(void *mem, ManifoldManifold *m,
int gaussian_idx, int mean_idx) {
auto man = from_c(m)->CalculateCurvature(gaussian_idx, mean_idx);
return to_c(new (mem) Manifold(man));
float *manifold_curvature_vert_mean(void *mem, ManifoldCurvature *curv) {
return copy_data(mem, from_c(curv)->vertMeanCurvature);
}
float *manifold_curvature_vert_gaussian(void *mem, ManifoldCurvature *curv) {
return copy_data(mem, from_c(curv)->vertGaussianCurvature);
}
// Static Quality Globals
@@ -559,6 +500,7 @@ size_t manifold_manifold_pair_size() { return sizeof(ManifoldManifoldPair); }
size_t manifold_meshgl_size() { return sizeof(MeshGL); }
size_t manifold_box_size() { return sizeof(Box); }
size_t manifold_rect_size() { return sizeof(Rect); }
size_t manifold_curvature_size() { return sizeof(Curvature); }
// pointer free + destruction
void manifold_delete_cross_section(ManifoldCrossSection *c) {
@@ -578,6 +520,7 @@ void manifold_delete_manifold_vec(ManifoldManifoldVec *ms) {
void manifold_delete_meshgl(ManifoldMeshGL *m) { delete from_c(m); }
void manifold_delete_box(ManifoldBox *b) { delete from_c(b); }
void manifold_delete_rect(ManifoldRect *r) { delete from_c(r); }
void manifold_delete_curvature(ManifoldCurvature *c) { delete from_c(c); }
// destruction
void manifold_destruct_cross_section(ManifoldCrossSection *cs) {
@@ -597,6 +540,9 @@ void manifold_destruct_manifold_vec(ManifoldManifoldVec *ms) {
void manifold_destruct_meshgl(ManifoldMeshGL *m) { from_c(m)->~MeshGL(); }
void manifold_destruct_box(ManifoldBox *b) { from_c(b)->~Box(); }
void manifold_destruct_rect(ManifoldRect *r) { from_c(r)->~Rect(); }
void manifold_destruct_curvature(ManifoldCurvature *c) {
from_c(c)->~Curvature();
}
#ifdef __cplusplus
}
+1 -46
View File
@@ -1,46 +1,11 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "meshIO.h"
#include "conv.h"
#include "types.h"
// C <-> C++ conversions
ManifoldMaterial *to_c(manifold::Material *m) {
return reinterpret_cast<ManifoldMaterial *>(m);
}
ManifoldExportOptions *to_c(manifold::ExportOptions *m) {
return reinterpret_cast<ManifoldExportOptions *>(m);
}
manifold::Material *from_c(ManifoldMaterial *mat) {
return reinterpret_cast<manifold::Material *>(mat);
}
manifold::ExportOptions *from_c(ManifoldExportOptions *options) {
return reinterpret_cast<manifold::ExportOptions *>(options);
}
#include <conv.h>
#ifdef __cplusplus
extern "C" {
#endif
// material
ManifoldMaterial *manifold_material(void *mem) {
return to_c(new (mem) manifold::Material());
}
@@ -62,8 +27,6 @@ void manifold_material_set_vert_color(ManifoldMaterial *mat,
from_c(mat)->vertColor = vector_of_vec_array(vert_color, n_vert);
}
// export options
ManifoldExportOptions *manifold_export_options(void *mem) {
return to_c(new (mem) manifold::ExportOptions());
}
@@ -78,19 +41,11 @@ void manifold_export_options_set_material(ManifoldExportOptions *options,
from_c(options)->mat = *from_c(mat);
}
// mesh IO
void manifold_export_meshgl(const char *filename, ManifoldMeshGL *mesh,
ManifoldExportOptions *options) {
manifold::ExportMesh(std::string(filename), *from_c(mesh), *from_c(options));
}
ManifoldMeshGL *manifold_import_meshgl(void *mem, const char *filename,
int force_cleanup) {
auto m = manifold::ImportMesh(std::string(filename), force_cleanup);
return to_c(new (mem) manifold::MeshGL(m));
}
// memory size
size_t manifold_material_size() { return sizeof(manifold::Material); }
+9 -16
View File
@@ -1,21 +1,8 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <conv.h>
#include <manifold.h>
#include <public.h>
#include "conv.h"
#include "cross_section.h"
#include "manifold.h"
#include "public.h"
#include "types.h"
ManifoldRect *manifold_rect(void *mem, float x1, float y1, float x2, float y2) {
@@ -98,3 +85,9 @@ int manifold_rect_does_overlap_rect(ManifoldRect *a, ManifoldRect *r) {
int manifold_rect_is_empty(ManifoldRect *r) { return from_c(r)->IsEmpty(); }
int manifold_rect_is_finite(ManifoldRect *r) { return from_c(r)->IsFinite(); }
ManifoldCrossSection *manifold_rect_as_cross_section(void *mem,
ManifoldRect *r) {
auto cs = from_c(r)->AsCrossSection();
return to_c(new (mem) CrossSection(cs));
}
+6 -21
View File
@@ -15,25 +15,10 @@
project(python)
add_subdirectory(third_party)
nanobind_add_module(
manifold3d
NB_STATIC STABLE_ABI LTO
manifold3d.cpp)
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")
if(SKBUILD)
install(
TARGETS manifold3d
LIBRARY DESTINATION ${SKBUILD_PLATLIB_DIR}
COMPONENT bindings
pybind11_add_module(pymanifold pymanifold.cpp)
target_link_libraries(pymanifold PRIVATE manifold)
target_compile_options(pymanifold PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(pymanifold PUBLIC cxx_std_17)
target_include_directories(pymanifold
PRIVATE ${PYBIND11_DIR}/include
)
else()
install(
TARGETS manifold3d
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT bindings
)
endif()
+1 -1
View File
@@ -14,7 +14,7 @@
limitations under the License.
"""
from manifold3d import Manifold
from pymanifold import Manifold
# https://gist.github.com/deckar01/ef11def51de7e71d9f288c6e5819fdb7
+1 -1
View File
@@ -14,7 +14,7 @@
limitations under the License.
"""
from manifold3d import Manifold
from pymanifold import Manifold
from functools import reduce
# https://gist.github.com/ochafik/2db96400e3c1f73558fcede990b8a355#file-cube-with-half-spheres-dents-scad
-38
View File
@@ -1,38 +0,0 @@
from manifold3d import CrossSection
def run():
# create a polygon
polygon_points = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
polygons_points = [polygon_points]
# create a cross-section
cross_section = CrossSection(polygons_points)
polygons = cross_section.to_polygons()
polygon = polygons[0]
if set(polygon) != set(polygon_points):
raise Exception(f"polygon={polygon} differs from polygon_points={polygon_points}")
# extrude a polygon to create a manifold
extruded_polygon = cross_section.extrude(10.0)
eps = 0.001
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.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}")
# get bounding box from manifold
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}")
return extruded_polygon
if __name__ == "__main__":
run()
+1 -1
View File
@@ -1,7 +1,7 @@
# One of the benchmarks in in https://gist.github.com/ochafik/2db96400e3c1f73558fcede990b8a355
# https://www.thingiverse.com/groups/openscad/forums/general/topic:34699
from manifold3d import Manifold
from pymanifold import Manifold
import numpy as np
n = 10
+2 -9
View File
@@ -18,7 +18,6 @@ import pathlib
import sys
import importlib
import trimesh
import numpy as np
from time import time
if __name__ == "__main__":
@@ -35,14 +34,8 @@ if __name__ == "__main__":
model = module.run()
mesh = model.to_mesh()
if export_models:
if mesh.vert_properties.shape[1] > 3:
vertices = mesh.vert_properties[:, :3]
colors = (mesh.vert_properties[:, 3:] * 255).astype(np.uint8)
else:
vertices = mesh.vert_properties
colors = None
meshOut = trimesh.Trimesh(vertices=vertices, faces=mesh.tri_verts,
vertex_colors=colors)
meshOut = trimesh.Trimesh(
vertices=mesh.vert_pos, faces=mesh.tri_verts)
trimesh.exchange.export.export_mesh(meshOut, f'{f}.glb', 'glb')
print(f'Exported model to {f}.glb')
t1 = time()
-40
View File
@@ -1,40 +0,0 @@
from manifold3d import Manifold
import numpy as np
def fractal(holes, hole, w, position, depth, maxDepth):
w /= 3
holes.append(hole.scale([w, w, 1.0]).translate(
[position[0], position[1], 0.0]))
if depth == maxDepth:
return
offsets = np.array([
[-w, -w],
[-w, 0.0],
[-w, w],
[0.0, w],
[w, w],
[w, 0.0],
[w, -w],
[0.0, -w],
])
for offset in offsets:
fractal(holes, hole, w, position + offset, depth + 1, maxDepth)
def posColors(pos, _):
return [1 - p / 2 for p in pos] + [1.0]
def run(n=1):
result = Manifold.cube([1, 1, 1], True)
holes = []
fractal(holes, result, 1.0, np.array([0.0, 0.0]), 1, n)
hole = Manifold.compose(holes)
result -= hole
result -= hole.rotate([90, 0, 0])
result -= hole.rotate([0, 90, 0])
return result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale(100)
+1 -1
View File
@@ -1,4 +1,4 @@
from manifold3d import Manifold
from pymanifold import Manifold
def run():
# for some reason this causes collider error
-989
View File
@@ -1,989 +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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <optional>
#include <string>
#include "cross_section.h"
#include "manifold.h"
#include "nanobind/nanobind.h"
#include "nanobind/ndarray.h"
#include "nanobind/operators.h"
#include "nanobind/stl/function.h"
#include "nanobind/stl/optional.h"
#include "nanobind/stl/tuple.h"
#include "nanobind/stl/vector.h"
template <>
struct nanobind::detail::type_caster<glm::vec3> {
NB_TYPE_CASTER(glm::vec3, const_name("Vec3"));
using Caster = make_caster<float>;
bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept {
size_t size;
PyObject *temp;
/* Will initialize 'size' and 'temp'. All return values and
return parameters are zero/NULL in the case of a failure. */
PyObject **o = seq_get(src.ptr(), &size, &temp);
if (size != 3) return false;
Caster caster;
if (o == nullptr) {
Py_XDECREF(temp);
return false;
}
bool success = true;
for (size_t i = 0; i < size; ++i) {
if (!caster.from_python(o[i], flags, cleanup)) {
success = false;
break;
}
value[i] = caster.value;
}
Py_XDECREF(temp);
return success;
}
static handle from_cpp(glm::vec3 vec, rv_policy policy,
cleanup_list *ls) noexcept {
std::vector<float> v{vec.x, vec.y, vec.z};
return make_caster<std::vector<float>>().from_cpp(&v, policy, ls);
}
};
namespace nb = nanobind;
using namespace manifold;
typedef std::tuple<float, float> Float2;
typedef std::tuple<float, float, float> Float3;
template <typename T>
std::vector<T> toVector(const T *arr, size_t size) {
return std::vector<T>(arr, arr + size);
}
NB_MODULE(manifold3d, m) {
m.doc() = "Python binding for the Manifold library.";
m.def("set_min_circular_angle", Quality::SetMinCircularAngle,
"Sets an angle constraint the default number of circular segments for "
"the CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), "
"and Manifold::Revolve() constructors. The number of segments will be "
"rounded up to the nearest factor of four."
"\n\n"
":param angle: The minimum angle in degrees between consecutive "
"segments. The angle will increase if the the segments hit the minimum "
"edge length.\n"
"Default is 10 degrees.");
m.def("set_min_circular_edge_length", Quality::SetMinCircularEdgeLength,
"Sets a length constraint the default number of circular segments for "
"the CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), "
"and Manifold::Revolve() constructors. The number of segments will be "
"rounded up to the nearest factor of four."
"\n\n"
":param length: The minimum length of segments. The length will "
"increase if the the segments hit the minimum angle. Default is 1.0.");
m.def("set_circular_segments", Quality::SetCircularSegments,
"Sets the default number of circular segments for the "
"CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), and "
"Manifold::Revolve() constructors. Overrides the edge length and angle "
"constraints and sets the number of segments to exactly this value."
"\n\n"
":param number: Number of circular segments. Default is 0, meaning no "
"constraint is applied.");
m.def("get_circular_segments", Quality::GetCircularSegments,
"Determine the result of the SetMinCircularAngle(), "
"SetMinCircularEdgeLength(), and SetCircularSegments() defaults."
"\n\n"
":param radius: For a given radius of circle, determine how many "
"default");
nb::class_<Manifold>(m, "Manifold")
.def(nb::init<>())
.def(
"__init__",
[](Manifold *self, std::vector<Manifold> &manifolds) {
if (manifolds.size() >= 1) {
// for some reason using Manifold() as the initial object
// will cause failure for python specifically
// unable to reproduce with c++ directly
Manifold first = manifolds[0];
for (int i = 1; i < manifolds.size(); i++) first += manifolds[i];
new (self) Manifold(first);
} else {
new (self) Manifold();
}
},
"Construct manifold as the union of a set of manifolds.")
.def(nb::self + nb::self, "Boolean union.")
.def(nb::self - nb::self, "Boolean difference.")
.def(nb::self ^ nb::self, "Boolean intersection.")
.def(
"hull", [](Manifold &self) { return self.Hull(); },
"Compute the convex hull of all points in this manifold.")
.def_static(
"batch_hull",
[](std::vector<Manifold> &ms) { return Manifold::Hull(ms); },
"Compute the convex hull enveloping a set of manifolds.")
.def_static(
"hull_points",
[](std::vector<Float3> &pts) {
std::vector<glm::vec3> vec(pts.size());
for (int i = 0; i < pts.size(); i++) {
vec[i] = {std::get<0>(pts[i]), std::get<1>(pts[i]),
std::get<2>(pts[i])};
}
return Manifold::Hull(vec);
},
"Compute the convex hull enveloping a set of 3d points.")
.def(
"transform",
[](Manifold &self, nb::ndarray<float, nb::shape<3, 4>> &mat) {
if (mat.ndim() != 2 || mat.shape(0) != 3 || mat.shape(1) != 4)
throw std::runtime_error("Invalid matrix shape, expected (3, 4)");
glm::mat4x3 mat_glm;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
mat_glm[j][i] = mat(i, j);
}
}
return self.Transform(mat_glm);
},
nb::arg("m"),
"Transform this Manifold in space. The first three columns form a "
"3x3 matrix transform and the last is a translation vector. This "
"operation can be chained. Transforms are combined and applied "
"lazily.\n"
"\n\n"
":param m: The affine transform matrix to apply to all the vertices.")
.def(
"translate",
[](Manifold &self, float x = 0.0f, float y = 0.0f, float z = 0.0f) {
return self.Translate(glm::vec3(x, y, z));
},
nb::arg("x") = 0.0f, nb::arg("y") = 0.0f, nb::arg("z") = 0.0f,
"Move this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param x: X axis translation. (default 0.0).\n"
":param y: Y axis translation. (default 0.0).\n"
":param z: Z axis translation. (default 0.0).")
.def("translate", &Manifold::Translate, nb::arg("t"),
"Move this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param v: The vector to add to every vertex.")
.def(
"scale",
[](Manifold &self, float scale) {
return self.Scale(glm::vec3(scale));
},
nb::arg("scale"),
"Scale this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param scale: The scalar multiplier for each component of every "
"vertices.")
.def("scale", &Manifold::Scale, nb::arg("v"),
"Scale this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param v: The vector to multiply every vertex by component.")
.def("mirror", &Manifold::Mirror, nb::arg("v"),
"Mirror this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param mirror: The vector defining the axis of mirroring.")
.def(
"rotate",
[](Manifold &self, glm::vec3 v) {
return self.Rotate(v[0], v[1], v[2]);
},
nb::arg("v"),
"Applies an Euler angle rotation to the manifold, first about the X "
"axis, then Y, then Z, in degrees. We use degrees so that we can "
"minimize rounding error, and eliminate it completely for any "
"multiples of 90 degrees. Additionally, more efficient code paths "
"are used to update the manifold when the transforms only rotate by "
"multiples of 90 degrees. This operation can be chained. Transforms "
"are combined and applied lazily."
"\n\n"
":param v: [X, Y, Z] rotation in degrees.")
.def(
"rotate",
[](Manifold &self, float xDegrees = 0.0f, float yDegrees = 0.0f,
float zDegrees = 0.0f) {
return self.Rotate(xDegrees, yDegrees, zDegrees);
},
nb::arg("x_degrees") = 0.0f, nb::arg("y_degrees") = 0.0f,
nb::arg("z_degrees") = 0.0f,
"Applies an Euler angle rotation to the manifold, first about the X "
"axis, then Y, then Z, in degrees. We use degrees so that we can "
"minimize rounding error, and eliminate it completely for any "
"multiples of 90 degrees. Additionally, more efficient code paths "
"are used to update the manifold when the transforms only rotate by "
"multiples of 90 degrees. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param x: X rotation in degrees. (default 0.0).\n"
":param y: Y rotation in degrees. (default 0.0).\n"
":param z: Z rotation in degrees. (default 0.0).")
.def(
"warp",
[](Manifold &self, const std::function<Float3(Float3)> &f) {
return self.Warp([&f](glm::vec3 &v) {
Float3 fv = f(std::make_tuple(v.x, v.y, v.z));
v.x = std::get<0>(fv);
v.y = std::get<1>(fv);
v.z = std::get<2>(fv);
});
},
nb::arg("f"),
"This function does not change the topology, but allows the vertices "
"to be moved according to any arbitrary input function. It is easy "
"to create a function that warps a geometrically valid object into "
"one which overlaps, but that is not checked here, so it is up to "
"the user to choose their function with discretion."
"\n\n"
":param warpFunc: A function that modifies a given vertex position.")
.def(
"set_properties",
[](Manifold &self, int newNumProp,
const std::function<nb::object(
Float3, const nb::ndarray<nb::numpy, const float, nb::c_contig>
&)> &f) {
const int oldNumProp = self.NumProp();
return self.SetProperties(newNumProp, [newNumProp, oldNumProp, &f](
float *newProps,
glm::vec3 v,
const float *oldProps) {
auto result =
f(std::make_tuple(v.x, v.y, v.z),
nb::ndarray<nb::numpy, const float, nb::c_contig>(
&oldProps, {static_cast<unsigned long>(oldNumProp)}));
nb::ndarray<float, nb::shape<nb::any>> array;
std::vector<float> vec;
if (nb::try_cast(result, array)) {
if (array.ndim() != 1 || array.shape(0) != newNumProp)
throw std::runtime_error("Invalid vector shape, expected (" +
std::to_string(newNumProp) + ")");
for (int i = 0; i < newNumProp; i++) newProps[i] = array(i);
} else if (nb::try_cast(result, vec)) {
for (int i = 0; i < newNumProp; i++) newProps[i] = vec[i];
} else {
throw std::runtime_error(
"Callback in set_properties should return an array");
}
});
},
nb::arg("new_num_prop"), nb::arg("f"),
"Create a new copy of this manifold with updated vertex properties "
"by supplying a function that takes the existing position and "
"properties as input. You may specify any number of output "
"properties, allowing creation and removal of channels. Note: "
"undefined behavior will result if you read past the number of input "
"properties or write past the number of output properties."
"\n\n"
":param numProp: The new number of properties per vertex."
":param propFunc: A function that modifies the properties of a given "
"vertex.")
.def(
"refine", [](Manifold &self, int n) { return self.Refine(n); },
nb::arg("n"),
"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 will all be coplanar (and will not be "
"immediately collapsed) unless the Mesh/Manifold has "
"halfedgeTangents specified (e.g. from the Smooth() constructor), "
"in which case the new vertices will be moved to the interpolated "
"surface according to their barycentric coordinates.\n"
"\n"
":param n: The number of pieces to split every edge into. Must be > "
"1.")
.def(
"to_mesh",
[](Manifold &self,
std::optional<nb::ndarray<uint32_t, nb::shape<3>>> &normalIdx) {
glm::ivec3 v(0);
if (normalIdx.has_value()) {
if (normalIdx->ndim() != 1 || normalIdx->shape(0) != 3)
throw std::runtime_error("Invalid vector shape, expected (3)");
auto value = *normalIdx;
v = glm::ivec3(value(0), value(1), value(2));
}
return self.GetMeshGL(v);
},
"The most complete output of this library, returning a MeshGL that "
"is designed to easily push into a renderer, including all "
"interleaved vertex properties that may have been input. It also "
"includes relations to all the input meshes that form a part of "
"this result and the transforms applied to each."
"\n\n"
":param normalIdx: If the original MeshGL inputs that formed this "
"manifold had properties corresponding to normal vectors, you can "
"specify which property channels these are (x, y, z), which will "
"cause this output MeshGL to automatically update these normals "
"according to the applied transforms and front/back side. Each "
"channel must be >= 3 and < numProp, and all original MeshGLs must "
"use the same channels for their normals.",
nb::arg("normalIdx") = nb::none())
.def("num_vert", &Manifold::NumVert,
"The number of vertices in the Manifold.")
.def("num_edge", &Manifold::NumEdge,
"The number of edges in the Manifold.")
.def("num_tri", &Manifold::NumTri,
"The number of triangles in the Manifold.")
.def("num_prop", &Manifold::NumProp,
"The number of properties per vertex in the Manifold")
.def("num_prop_vert", &Manifold::NumPropVert,
"The number of property vertices in the Manifold. This will always "
"be >= NumVert, as some physical vertices may be duplicated to "
"account for different properties on different neighboring "
"triangles.")
.def("precision", &Manifold::Precision,
"Returns the precision of this Manifold's vertices, which tracks "
"the approximate rounding error over all the transforms and "
"operations that have led to this state. Any triangles that are "
"colinear within this precision are considered degenerate and "
"removed. This is the value of &epsilon; defining "
"[&epsilon;-valid](https://github.com/elalish/manifold/wiki/"
"Manifold-Library#definition-of-%CE%B5-valid).")
.def("genus", &Manifold::Genus,
"The genus is a topological property of the manifold, representing "
"the number of \"handles\". A sphere is 0, torus 1, etc. It is only "
"meaningful for a single mesh, so it is best to call Decompose() "
"first.")
.def(
"get_volume",
[](Manifold &self) { return self.GetProperties().volume; },
"Get the volume of the manifold\n This is clamped to zero for a "
"given face if they are within the Precision().")
.def(
"get_surface_area",
[](Manifold &self) { return self.GetProperties().surfaceArea; },
"Get the surface area of the manifold\n This is clamped to zero for "
"a given face if they are within the Precision().")
.def("original_id", &Manifold::OriginalID,
"If this mesh is an original, this returns its meshID that can be "
"referenced by product manifolds' MeshRelation. If this manifold is "
"a product, this returns -1.")
.def("as_original", &Manifold::AsOriginal,
"This function condenses all coplanar faces in the relation, and "
"collapses those edges. In the process the relation to ancestor "
"meshes is lost and this new Manifold is marked an original. "
"Properties are preserved, so if they do not match across an edge, "
"that edge will be kept.")
.def("is_empty", &Manifold::IsEmpty,
"Does the Manifold have any triangles?")
.def(
"decompose", [](Manifold &self) { return self.Decompose(); },
"This operation returns a vector of Manifolds that are "
"topologically disconnected. If everything is connected, the vector "
"is length one, containing a copy of the original. It is the inverse "
"operation of Compose().")
.def("split", &Manifold::Split,
"Split cuts this manifold in two using the cutter manifold. The "
"first result is the intersection, second is the difference. This "
"is more efficient than doing them separately.")
.def(
"split_by_plane",
[](Manifold &self, Float3 normal, float originOffset) {
return self.SplitByPlane(
{std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)},
originOffset);
},
nb::arg("normal"), nb::arg("origin_offset"),
"Convenient version of Split() for a half-space."
"\n\n"
":param normal: This vector is normal to the cutting plane and its "
"length does not matter. The first result is in the direction of "
"this vector, the second result is on the opposite side.\n"
":param originOffset: The distance of the plane from the origin in "
"the direction of the normal vector.")
.def(
"trim_by_plane",
[](Manifold &self, Float3 normal, float originOffset) {
return self.TrimByPlane(
{std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)},
originOffset);
},
nb::arg("normal"), nb::arg("origin_offset"),
"Identical to SplitByPlane(), but calculating and returning only the "
"first result."
"\n\n"
":param normal: This vector is normal to the cutting plane and its "
"length does not matter. The result is in the direction of this "
"vector from the plane.\n"
":param originOffset: The distance of the plane from the origin in "
"the direction of the normal vector.")
.def("status", &Manifold::Status,
"Returns the reason for an input Mesh producing an empty Manifold. "
"This Status only applies to Manifolds newly-created from an input "
"Mesh - once they are combined into a new Manifold via operations, "
"the status reverts to NoError, simply processing the problem mesh "
"as empty. Likewise, empty meshes may still show NoError, for "
"instance if they are small enough relative to their precision to "
"be collapsed to nothing.")
.def_prop_ro(
"bounding_box",
[](Manifold &self) {
auto b = self.BoundingBox();
nb::tuple box = nb::make_tuple(b.min[0], b.min[1], b.min[2],
b.max[0], b.max[1], b.max[2]);
return box;
},
"Gets the manifold bounding box as a tuple "
"(xmin, ymin, zmin, xmax, ymax, zmax).")
.def_static(
"smooth", [](const Mesh &mesh) { return Manifold::Smooth(mesh); },
"Constructs a smooth version of the input mesh by creating tangents; "
"this method will throw if you have supplied tangents with your "
"mesh already. The actual triangle resolution is unchanged; use the "
"Refine() method to interpolate to a higher-resolution curve."
"\n\n"
"By default, every edge is calculated for maximum smoothness (very "
"much approximately), attempting to minimize the maximum mean "
"Curvature magnitude. No higher-order derivatives are considered, "
"as the interpolation is independent per triangle, only sharing "
"constraints on their boundaries."
"\n\n"
":param mesh: input Mesh.\n"
":param sharpenedEdges: If desired, you can supply a vector of "
"sharpened halfedges, which should in general be a small subset of "
"all halfedges. Order of entries doesn't matter, as each one "
"specifies the desired smoothness (between zero and one, with one "
"the default for all unspecified halfedges) and the halfedge index "
"(3 * triangle index + [0,1,2] where 0 is the edge between triVert 0 "
"and 1, etc)."
"\n\n"
"At a smoothness value of zero, a sharp crease is made. The "
"smoothness is interpolated along each edge, so the specified value "
"should be thought of as an average. Where exactly two sharpened "
"edges meet at a vertex, their tangents are rotated to be colinear "
"so that the sharpened edge can be continuous. Vertices with only "
"one sharpened edge are completely smooth, allowing sharpened edges "
"to smoothly vanish at termination. A single vertex can be sharpened "
"by sharping all edges that are incident on it, allowing cones to be "
"formed.")
.def_static(
"from_mesh", [](const MeshGL &mesh) { return Manifold(mesh); },
nb::arg("mesh"))
.def_static(
"compose",
[](const std::vector<Manifold> &list) {
return Manifold::Compose(list);
},
"combine several manifolds into one without checking for "
"intersections.")
.def_static(
"tetrahedron", []() { return Manifold::Tetrahedron(); },
"Constructs a tetrahedron centered at the origin with one vertex at "
"(1,1,1) and the rest at similarly symmetric points.")
.def_static(
"cube",
[](Float3 size, bool center = false) {
return Manifold::Cube(
glm::vec3(std::get<0>(size), std::get<1>(size),
std::get<2>(size)),
center);
},
nb::arg("size") = std::make_tuple(1.0f, 1.0f, 1.0f),
nb::arg("center") = false,
"Constructs a unit cube (edge lengths all one), by default in the "
"first octant, touching the origin."
"\n\n"
":param size: The X, Y, and Z dimensions of the box.\n"
":param center: Set to true to shift the center to the origin.")
.def_static(
"cube", &Manifold::Cube, nb::arg("size"), nb::arg("center") = false,
"Constructs a unit cube (edge lengths all one), by default in the "
"first octant, touching the origin."
"\n\n"
":param size: The X, Y, and Z dimensions of the box.\n"
":param center: Set to true to shift the center to the origin.")
.def_static(
"cube",
[](float x, float y, float z, bool center = false) {
return Manifold::Cube(glm::vec3(x, y, z), center);
},
nb::arg("x"), nb::arg("y"), nb::arg("z"), nb::arg("center") = false,
"Constructs a unit cube (edge lengths all one), by default in the "
"first octant, touching the origin."
"\n\n"
":param x: The X dimensions of the box.\n"
":param y: The Y dimensions of the box.\n"
":param z: The Z dimensions of the box.\n"
":param center: Set to true to shift the center to the origin.")
.def_static(
"cylinder",
[](float height, float radiusLow, float radiusHigh = -1.0f,
int circularSegments = 0, bool center = false) {
return Manifold::Cylinder(height, radiusLow, radiusHigh,
circularSegments, center);
},
nb::arg("height"), nb::arg("radius_low"),
nb::arg("radius_high") = -1.0f, nb::arg("circular_segments") = 0,
nb::arg("center") = false,
"A convenience constructor for the common case of extruding a "
"circle. Can also form cones if both radii are specified."
"\n\n"
":param height: Z-extent\n"
":param radiusLow: Radius of bottom circle. Must be positive.\n"
":param radiusHigh: Radius of top circle. Can equal zero. Default "
"(-1) is equal to radiusLow.\n"
":param circularSegments: How many line segments to use around the "
"circle. Default (-1) is calculated by the static Defaults.\n"
":param center: Set to true to shift the center to the origin. "
"Default is origin at the bottom.")
.def_static(
"sphere",
[](float radius, int circularSegments = 0) {
return Manifold::Sphere(radius, circularSegments);
},
nb::arg("radius"), nb::arg("circular_segments") = 0,
"Constructs a geodesic sphere of a given radius.\n"
"\n"
":param radius: Radius of the sphere. Must be positive.\n"
":param circularSegments: Number of segments along its diameter. "
"This number will always be rounded up to the nearest factor of "
"four, as this sphere is constructed by refining an octahedron. This "
"means there are a circle of vertices on all three of the axis "
"planes. Default is calculated by the static Defaults.")
.def_static("reserve_ids", Manifold::ReserveIDs, nb::arg("n"),
"Returns the first of n sequential new unique mesh IDs for "
"marking sets of triangles that can be looked up after "
"further operations. Assign to MeshGL.runOriginalID vector");
nb::class_<MeshGL>(m, "Mesh")
.def(
// note that reshape requires mutable ndarray, but this will not
// affect the original array passed into the function
"__init__",
[](MeshGL *self,
nb::ndarray<float, nb::shape<nb::any, nb::any>, nb::c_contig>
&vertProp,
nb::ndarray<uint32_t, nb::shape<nb::any, 3>, nb::c_contig>
&triVerts,
const std::optional<nb::ndarray<uint32_t, nb::shape<nb::any>,
nb::c_contig>> &mergeFromVert,
const std::optional<nb::ndarray<uint32_t, nb::shape<nb::any>,
nb::c_contig>> &mergeToVert,
const std::optional<nb::ndarray<uint32_t, nb::shape<nb::any>,
nb::c_contig>> &runIndex,
const std::optional<nb::ndarray<uint32_t, nb::shape<nb::any>,
nb::c_contig>> &runOriginalID,
std::optional<nb::ndarray<float, nb::shape<nb::any, 4, 3>,
nb::c_contig>> &runTransform,
const std::optional<nb::ndarray<uint32_t, nb::shape<nb::any>,
nb::c_contig>> &faceID,
const std::optional<nb::ndarray<float, nb::shape<nb::any, 3, 4>,
nb::c_contig>> &halfedgeTangent,
float precision) {
new (self) MeshGL();
MeshGL &out = *self;
out.numProp = vertProp.shape(1);
out.vertProperties =
toVector<float>(vertProp.data(), vertProp.size());
if (triVerts.ndim() != 2 || triVerts.shape(1) != 3)
throw std::runtime_error(
"Invalid tri_verts shape, expected (-1, 3)");
out.triVerts = toVector<uint32_t>(triVerts.data(), triVerts.size());
if (mergeFromVert.has_value())
out.mergeFromVert = toVector<uint32_t>(mergeFromVert->data(),
mergeFromVert->size());
if (mergeToVert.has_value())
out.mergeToVert =
toVector<uint32_t>(mergeToVert->data(), mergeToVert->size());
if (runIndex.has_value())
out.runIndex =
toVector<uint32_t>(runIndex->data(), runIndex->size());
if (runOriginalID.has_value())
out.runOriginalID = toVector<uint32_t>(runOriginalID->data(),
runOriginalID->size());
if (runTransform.has_value()) {
auto runTransform1 = *runTransform;
if (runTransform1.ndim() != 3 || runTransform1.shape(1) != 4 ||
runTransform1.shape(2) != 3)
throw std::runtime_error(
"Invalid run_transform shape, expected (-1, 4, 3)");
out.runTransform =
toVector<float>(runTransform1.data(), runTransform1.size());
}
if (faceID.has_value())
out.faceID = toVector<uint32_t>(faceID->data(), faceID->size());
if (halfedgeTangent.has_value()) {
auto halfedgeTangent1 = *halfedgeTangent;
if (halfedgeTangent1.ndim() != 3 ||
halfedgeTangent1.shape(1) != 3 ||
halfedgeTangent1.shape(2) != 4)
throw std::runtime_error(
"Invalid halfedge_tangent shape, expected (-1, 3, 4)");
out.halfedgeTangent = toVector<float>(halfedgeTangent1.data(),
halfedgeTangent1.size());
}
},
nb::arg("vert_properties"), nb::arg("tri_verts"),
nb::arg("merge_from_vert") = nb::none(),
nb::arg("merge_to_vert") = nb::none(),
nb::arg("run_index") = nb::none(),
nb::arg("run_original_id") = nb::none(),
nb::arg("run_transform") = nb::none(),
nb::arg("face_id") = nb::none(),
nb::arg("halfedge_tangent") = nb::none(), nb::arg("precision") = 0)
.def_prop_ro("vert_properties",
[](const MeshGL &self) {
return nb::ndarray<nb::numpy, const float, nb::c_contig>(
self.vertProperties.data(),
{self.vertProperties.size() / self.numProp,
self.numProp});
}, nb::rv_policy::reference_internal)
.def_prop_ro("tri_verts",
[](const MeshGL &self) {
return nb::ndarray<nb::numpy, const int, nb::c_contig>(
self.triVerts.data(), {self.triVerts.size() / 3, 3});
}, nb::rv_policy::reference_internal)
.def_prop_ro("run_transform",
[](const MeshGL &self) {
return nb::ndarray<nb::numpy, const float, nb::c_contig>(
self.runTransform.data(), {self.runTransform.size() / 12, 4, 3});
}, nb::rv_policy::reference_internal)
.def_prop_ro("halfedge_tangent",
[](const MeshGL &self) {
float *data = new float[self.halfedgeTangent.size()];
std::copy(self.halfedgeTangent.data(),
self.halfedgeTangent.data() +
self.halfedgeTangent.size(),
data);
return nb::ndarray<nb::numpy, const float, nb::c_contig>(
self.halfedgeTangent.data(), {self.halfedgeTangent.size() / 12, 3, 4});
}, nb::rv_policy::reference_internal)
.def_ro("merge_from_vert", &MeshGL::mergeFromVert)
.def_ro("merge_to_vert", &MeshGL::mergeToVert)
.def_ro("run_index", &MeshGL::runIndex)
.def_ro("run_original_id", &MeshGL::runOriginalID)
.def_ro("face_id", &MeshGL::faceID);
nb::enum_<Manifold::Error>(m, "Error")
.value("NoError", Manifold::Error::NoError)
.value("NonFiniteVertex", Manifold::Error::NonFiniteVertex)
.value("NotManifold", Manifold::Error::NotManifold)
.value("VertexOutOfBounds", Manifold::Error::VertexOutOfBounds)
.value("PropertiesWrongLength", Manifold::Error::PropertiesWrongLength)
.value("MissingPositionProperties",
Manifold::Error::MissingPositionProperties)
.value("MergeVectorsDifferentLengths",
Manifold::Error::MergeVectorsDifferentLengths)
.value("MergeIndexOutOfBounds", Manifold::Error::MergeIndexOutOfBounds)
.value("TransformWrongLength", Manifold::Error::TransformWrongLength)
.value("RunIndexWrongLength", Manifold::Error::RunIndexWrongLength)
.value("FaceIDWrongLength", Manifold::Error::FaceIDWrongLength)
.value("InvalidConstruction", Manifold::Error::InvalidConstruction);
nb::enum_<CrossSection::FillRule>(m, "FillRule")
.value("EvenOdd", CrossSection::FillRule::EvenOdd,
"Only odd numbered sub-regions are filled.")
.value("NonZero", CrossSection::FillRule::NonZero,
"Only non-zero sub-regions are filled.")
.value("Positive", CrossSection::FillRule::Positive,
"Only sub-regions with winding counts > 0 are filled.")
.value("Negative", CrossSection::FillRule::Negative,
"Only sub-regions with winding counts < 0 are filled.");
nb::enum_<CrossSection::JoinType>(m, "JoinType")
.value("Square", CrossSection::JoinType::Square,
"Squaring is applied uniformly at all joins where the internal "
"join angle is less that 90 degrees. The squared edge will be at "
"exactly the offset distance from the join vertex.")
.value(
"Round", CrossSection::JoinType::Square,
"Rounding is applied to all joins that have convex external angles, "
"and it maintains the exact offset distance from the join vertex.")
.value(
"Miter", CrossSection::JoinType::Miter,
"There's a necessary limit to mitered joins (to avoid narrow angled "
"joins producing excessively long and narrow "
"[spikes](http://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/"
"Classes/ClipperOffset/Properties/MiterLimit.htm)). So where mitered "
"joins would exceed a given maximum miter distance (relative to the "
"offset distance), these are 'squared' instead.");
nb::class_<CrossSection>(
m, "CrossSection",
"Two-dimensional cross sections guaranteed to be without "
"self-intersections, or overlaps between polygons (from construction "
"onwards). This class makes use of the "
"[Clipper2](http://www.angusj.com/clipper2/Docs/Overview.htm) library "
"for polygon clipping (boolean) and offsetting operations.")
.def(nb::init<>())
.def(
"__init__",
[](CrossSection *self, std::vector<std::vector<Float2>> &polygons,
CrossSection::FillRule fillrule) {
std::vector<SimplePolygon> simplePolygons(polygons.size());
for (int i = 0; i < polygons.size(); i++) {
simplePolygons[i] = std::vector<glm::vec2>(polygons[i].size());
for (int j = 0; j < polygons[i].size(); j++) {
simplePolygons[i][j] = {std::get<0>(polygons[i][j]),
std::get<1>(polygons[i][j])};
}
}
new (self) CrossSection(simplePolygons, fillrule);
},
nb::arg("polygons"),
nb::arg("fillrule") = CrossSection::FillRule::Positive,
"Create a 2d cross-section from a set of contours (complex "
"polygons). A boolean union operation (with Positive filling rule "
"by default) performed to combine overlapping polygons and ensure "
"the resulting CrossSection is free of intersections."
"\n\n"
":param contours: A set of closed paths describing zero or more "
"complex polygons.\n"
":param fillrule: The filling rule used to interpret polygon "
"sub-regions in contours.")
.def("area", &CrossSection::Area,
"Return the total area covered by complex polygons making up the "
"CrossSection.")
.def("num_vert", &CrossSection::NumVert,
"Return the number of vertices in the CrossSection.")
.def("num_contour", &CrossSection::NumContour,
"Return the number of contours (both outer and inner paths) in the "
"CrossSection.")
.def("is_empty", &CrossSection::IsEmpty,
"Does the CrossSection contain any contours?")
.def(
"translate",
[](CrossSection self, Float2 v) {
return self.Translate({std::get<0>(v), std::get<1>(v)});
},
"Move this CrossSection in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param v: The vector to add to every vertex.")
.def("rotate", &CrossSection::Rotate,
"Applies a (Z-axis) rotation to the CrossSection, in degrees. This "
"operation can be chained. Transforms are combined and applied "
"lazily."
"\n\n"
":param degrees: degrees about the Z-axis to rotate.")
.def(
"scale",
[](CrossSection self, Float2 s) {
return self.Scale({std::get<0>(s), std::get<1>(s)});
},
"Scale this CrossSection in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param v: The vector to multiply every vertex by per component.")
.def(
"mirror",
[](CrossSection self, Float2 ax) {
return self.Mirror({std::get<0>(ax), std::get<1>(ax)});
},
"Mirror this CrossSection over the arbitrary axis described by the "
"unit form of the given vector. If the length of the vector is zero, "
"an empty CrossSection is returned. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param ax: the axis to be mirrored over")
.def(
"transform",
[](CrossSection self, nb::ndarray<float, nb::shape<2, 3>> &mat) {
if (mat.ndim() != 2 || mat.shape(0) != 2 || mat.shape(1) != 3)
throw std::runtime_error("Invalid matrix shape, expected (2, 3)");
glm::mat3x2 mat_glm;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
mat_glm[j][i] = mat(i, j);
}
}
return self.Transform(mat_glm);
},
"Transform this CrossSection in space. The first two columns form a "
"2x2 matrix transform and the last is a translation vector. This "
"operation can be chained. Transforms are combined and applied "
"lazily."
"\n\n"
":param m: The affine transform matrix to apply to all the vertices.")
.def(
"warp",
[](CrossSection self, const std::function<Float2(Float2)> &f) {
return self.Warp([&f](glm::vec2 &v) {
Float2 fv = f(std::make_tuple(v.x, v.y));
v.x = std::get<0>(fv);
v.y = std::get<1>(fv);
});
},
nb::arg("f"),
"Move the vertices of this CrossSection (creating a new one) "
"according to any arbitrary input function, followed by a union "
"operation (with a Positive fill rule) that ensures any introduced "
"intersections are not included in the result."
"\n\n"
":param warpFunc: A function that modifies a given vertex position.")
.def("simplify", &CrossSection::Simplify,
"Remove vertices from the contours in this CrossSection that are "
"less than the specified distance epsilon from an imaginary line "
"that passes through its two adjacent vertices. Near duplicate "
"vertices and collinear points will be removed at lower epsilons, "
"with elimination of line segments becoming increasingly aggressive "
"with larger epsilons."
"\n\n"
"It is recommended to apply this function following Offset, in "
"order to clean up any spurious tiny line segments introduced that "
"do not improve quality in any meaningful way. This is particularly "
"important if further offseting operations are to be performed, "
"which would compound the issue.")
.def("offset", &CrossSection::Offset, nb::arg("delta"),
nb::arg("join_type"), nb::arg("miter_limit") = 2.0,
nb::arg("arc_tolerance") = 0.0,
"Inflate the contours in CrossSection by the specified delta, "
"handling corners according to the given JoinType."
"\n\n"
":param delta: Positive deltas will cause the expansion of "
"outlining contours to expand, and retraction of inner (hole) "
"contours. Negative deltas will have the opposite effect.\n"
":param jt: The join type specifying the treatment of contour joins "
"(corners).\n"
":param miter_limit: The maximum distance in multiples of delta "
"that vertices can be offset from their original positions with "
"before squaring is applied, <B>when the join type is Miter</B> "
"(default is 2, which is the minimum allowed). See the [Clipper2 "
"MiterLimit](http://www.angusj.com/clipper2/Docs/Units/"
"Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm) "
"page for a visual example.\n"
":param circularSegments: Number of segments per 360 degrees of "
"<B>JoinType::Round</B> corners (roughly, the number of vertices "
"that will be added to each contour). Default is calculated by the "
"static Quality defaults according to the radius.")
.def(nb::self + nb::self, "Boolean union.")
.def(nb::self - nb::self, "Boolean difference.")
.def(nb::self ^ nb::self, "Boolean intersection.")
.def(
"hull", [](CrossSection &self) { return self.Hull(); },
"Compute the convex hull of this cross-section.")
.def_static(
"batch_hull",
[](std::vector<CrossSection> &cs) { return CrossSection::Hull(cs); },
"Compute the convex hull enveloping a set of cross-sections.")
.def_static(
"hull_points",
[](std::vector<Float2> &pts) {
std::vector<glm::vec2> poly(pts.size());
for (int i = 0; i < pts.size(); i++) {
poly[i] = {std::get<0>(pts[i]), std::get<1>(pts[i])};
}
return CrossSection::Hull(poly);
},
"Compute the convex hull enveloping a set of 2d points.")
.def("decompose", &CrossSection::Decompose,
"This operation returns a vector of CrossSections that are "
"topologically disconnected, each containing one outline contour "
"with zero or more holes.")
.def(
"to_polygons",
[](CrossSection self) {
const Polygons &data = self.ToPolygons();
nb::list polygon_list;
for (int i = 0; i < data.size(); ++i) {
nb::list polygon;
for (int j = 0; j < data[i].size(); ++j) {
auto f = data[i][j];
nb::tuple vertex = nb::make_tuple(f[0], f[1]);
polygon.append(vertex);
}
polygon_list.append(polygon);
}
return polygon_list;
},
"Returns the vertices of the cross-section's polygons as a "
"List[List[Tuple[float, float]]].")
.def(
"extrude",
[](CrossSection self, float height, int nDivisions = 0,
float twistDegrees = 0.0f,
Float2 scaleTop = std::make_tuple(1.0f, 1.0f)) {
glm::vec2 scaleTopVec(std::get<0>(scaleTop), std::get<1>(scaleTop));
return Manifold::Extrude(self, height, nDivisions, twistDegrees,
scaleTopVec);
},
nb::arg("height"), nb::arg("n_divisions") = 0,
nb::arg("twist_degrees") = 0.0f,
nb::arg("scale_top") = std::make_tuple(1.0f, 1.0f),
"Constructs a manifold from the set of polygons by extruding them "
"along the Z-axis.\n"
"\n"
":param height: Z-extent of extrusion.\n"
":param nDivisions: Number of extra copies of the crossSection to "
"insert into the shape vertically; especially useful in combination "
"with twistDegrees to avoid interpolation artifacts. Default is "
"none.\n"
":param twistDegrees: Amount to twist the top crossSection relative "
"to the bottom, interpolated linearly for the divisions in between.\n"
":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. Default (1, 1).")
.def(
"revolve",
[](CrossSection self, int circularSegments = 0) {
return Manifold::Revolve(self, circularSegments);
},
nb::arg("circular_segments") = 0,
"Constructs a manifold from the set of polygons by revolving this "
"cross-section around its Y-axis and then setting this as the Z-axis "
"of the resulting manifold. If the polygons cross the Y-axis, only "
"the part on the positive X side is used. Geometrically valid input "
"will result in geometrically valid output.\n"
"\n"
":param circularSegments: Number of segments along its diameter. "
"Default is calculated by the static Defaults.")
.def_static(
"square",
[](Float2 dims, bool center) {
return CrossSection::Square({std::get<0>(dims), std::get<1>(dims)},
center);
},
nb::arg("dims"), nb::arg("center") = false,
"Constructs a square with the given XY dimensions. By default it is "
"positioned in the first quadrant, touching the origin. If any "
"dimensions in size are negative, or if all are zero, an empty "
"Manifold will be returned."
"\n\n"
":param size: The X, and Y dimensions of the square.\n"
":param center: Set to true to shift the center to the origin.")
.def_static(
"circle",
[](float radius, int circularSegments) {
return CrossSection::Circle(radius, circularSegments);
},
nb::arg("radius"), nb::arg("circularSegments") = 0,
"Constructs a circle of a given radius."
"\n\n"
":param radius: Radius of the circle. Must be positive.\n"
":param circularSegments: Number of segments along its diameter. "
"Default is calculated by the static Quality defaults according to "
"the radius.");
}
+738
View File
@@ -0,0 +1,738 @@
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cross_section.h"
#include "manifold.h"
#include "pybind11/functional.h"
#include "pybind11/numpy.h"
#include "pybind11/operators.h"
#include "pybind11/pybind11.h"
#include "pybind11/stl.h"
namespace py = pybind11;
using namespace manifold;
typedef std::tuple<float, float> Float2;
typedef std::tuple<float, float, float> Float3;
struct PolygonsWrapper {
std::unique_ptr<Polygons> polygons;
};
PYBIND11_MODULE(pymanifold, m) {
m.doc() =
"Python binding for the manifold library. Please check the C++ "
"documentation for APIs.\n"
"This binding will perform copying to make the API more familiar to "
"OpenSCAD users.";
m.def("set_min_circular_angle", Quality::SetMinCircularAngle,
"Sets an angle constraint the default number of circular segments for "
"the CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), "
"and Manifold::Revolve() constructors. The number of segments will be "
"rounded up to the nearest factor of four."
"\n\n"
"@param angle The minimum angle in degrees between consecutive "
"segments. The angle will increase if the the segments hit the minimum "
"edge length.\n"
"Default is 10 degrees.");
m.def("set_min_circular_edge_length", Quality::SetMinCircularEdgeLength,
"Sets a length constraint the default number of circular segments for "
"the CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), "
"and Manifold::Revolve() constructors. The number of segments will be "
"rounded up to the nearest factor of four."
"\n\n"
"@param length The minimum length of segments. The length will "
"increase if the the segments hit the minimum angle. Default is 1.0.");
m.def("set_circular_segments", Quality::SetCircularSegments,
"Sets the default number of circular segments for the "
"CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), and "
"Manifold::Revolve() constructors. Overrides the edge length and angle "
"constraints and sets the number of segments to exactly this value."
"\n\n"
"@param number Number of circular segments. Default is 0, meaning no "
"constraint is applied.");
m.def(
"get_circular_segments", Quality::GetCircularSegments,
"Determine the result of the SetMinCircularAngle(), "
"SetMinCircularEdgeLength(), and SetCircularSegments() defaults."
"\n\n"
"@param radius For a given radius of circle, determine how many default");
py::class_<Manifold>(m, "Manifold")
.def(py::init<>())
.def(py::init([](std::vector<Manifold> &manifolds) {
Manifold result;
if (manifolds.size() >= 1) {
// for some reason using Manifold() as the initial object
// will cause failure for python specifically
// unable to reproduce with c++ directly
Manifold first = manifolds[0];
for (int i = 1; i < manifolds.size(); i++) first += manifolds[i];
return first;
} else {
return Manifold();
}
}),
"Construct manifold as the union of a set of manifolds.")
.def(py::self + py::self, "Boolean union.")
.def(py::self - py::self, "Boolean difference.")
.def(py::self ^ py::self, "Boolean intersection.")
.def(
"transform",
[](Manifold self, py::array_t<float> &mat) {
auto mat_view = mat.unchecked<2>();
if (mat_view.shape(0) != 3 || mat_view.shape(1) != 4)
throw std::runtime_error("Invalid matrix shape");
glm::mat4x3 mat_glm;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
mat_glm[j][i] = mat_view(i, j);
}
}
return self.Transform(mat_glm);
},
py::arg("m"),
"Transform this Manifold in space. The first three columns form a "
"3x3 matrix transform and the last is a translation vector. This "
"operation can be chained. Transforms are combined and applied "
"lazily.\n"
"\n\n"
":param m: The affine transform matrix to apply to all the vertices.")
.def(
"translate",
[](Manifold self, float x = 0.0f, float y = 0.0f, float z = 0.0f) {
return self.Translate(glm::vec3(x, y, z));
},
py::arg("x") = 0.0f, py::arg("y") = 0.0f, py::arg("z") = 0.0f,
"Move this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param x: X axis translation. (default 0.0).\n"
":param y: Y axis translation. (default 0.0).\n"
":param z: Z axis translation. (default 0.0).")
.def(
"translate",
[](Manifold self, py::array_t<float> &t) {
auto t_view = t.unchecked<1>();
if (t.shape(0) != 3)
throw std::runtime_error("Invalid vector shape");
return self.Translate(glm::vec3(t_view(0), t_view(1), t_view(2)));
},
py::arg("t"),
"Move this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param v: The vector to add to every vertex.")
.def("scale",
static_cast<Manifold (*)(Manifold, float)>(
[](Manifold self, float scale) {
return self.Scale(glm::vec3(scale));
}),
py::arg("scale"),
"Scale this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param scale: The scalar multiplier for each component of every "
"vertices.")
.def(
"scale",
[](Manifold self, py::array_t<float> &scale) {
auto scale_view = scale.unchecked<1>();
if (scale_view.shape(0) != 3)
throw std::runtime_error("Invalid vector shape");
glm::vec3 v(scale_view(0), scale_view(1), scale_view(2));
return self.Scale(v);
},
py::arg("v"),
"Scale this Manifold in space. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param v: The vector to multiply every vertex by component.")
.def(
"rotate",
[](Manifold self, py::array_t<float> &v) {
auto v_view = v.unchecked<1>();
if (v_view.shape(0) != 3)
throw std::runtime_error("Invalid vector shape");
return self.Rotate(v_view(0), v_view(1), v_view(2));
},
py::arg("v"),
"Applies an Euler angle rotation to the manifold, first about the X "
"axis, then Y, then Z, in degrees. We use degrees so that we can "
"minimize rounding error, and eliminate it completely for any "
"multiples of 90 degrees. Additionally, more efficient code paths "
"are used to update the manifold when the transforms only rotate by "
"multiples of 90 degrees. This operation can be chained. Transforms "
"are combined and applied lazily."
"\n\n"
":param v: [X, Y, Z] rotation in degrees.")
.def(
"rotate",
[](Manifold self, float xDegrees = 0.0f, float yDegrees = 0.0f,
float zDegrees = 0.0f) {
return self.Rotate(xDegrees, yDegrees, zDegrees);
},
py::arg("x_degrees") = 0.0f, py::arg("y_degrees") = 0.0f,
py::arg("z_degrees") = 0.0f,
"Applies an Euler angle rotation to the manifold, first about the X "
"axis, then Y, then Z, in degrees. We use degrees so that we can "
"minimize rounding error, and eliminate it completely for any "
"multiples of 90 degrees. Additionally, more efficient code paths "
"are used to update the manifold when the transforms only rotate by "
"multiples of 90 degrees. This operation can be chained. "
"Transforms are combined and applied lazily."
"\n\n"
":param x: X rotation in degrees. (default 0.0).\n"
":param y: Y rotation in degrees. (default 0.0).\n"
":param z: Z rotation in degrees. (default 0.0).")
.def(
"warp",
[](Manifold self, const std::function<Float3(Float3)> &f) {
return self.Warp([&f](glm::vec3 &v) {
Float3 fv = f(std::make_tuple(v.x, v.y, v.z));
v.x = std::get<0>(fv);
v.y = std::get<1>(fv);
v.z = std::get<2>(fv);
});
},
py::arg("f"))
.def(
"refine", [](Manifold self, int n) { return self.Refine(n); },
py::arg("n"),
"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 will all be coplanar (and will not be "
"immediately collapsed) unless the Mesh/Manifold has "
"halfedgeTangents specified (e.g. from the Smooth() constructor), "
"in which case the new vertices will be moved to the interpolated "
"surface according to their barycentric coordinates.\n"
"\n"
":param n: The number of pieces to split every edge into. Must be > "
"1.")
.def("to_mesh", &Manifold::GetMesh)
.def("num_vert", &Manifold::NumVert,
"The number of vertices in the Manifold.")
.def("num_edge", &Manifold::NumEdge,
"The number of edges in the Manifold.")
.def("num_tri", &Manifold::NumTri,
"The number of triangles in the Manifold.")
.def("num_prop", &Manifold::NumProp,
"The number of properties per vertex in the Manifold")
.def("num_prop_vert", &Manifold::NumPropVert,
"The number of property vertices in the Manifold. This will always "
"be >= NumVert, as some physical vertices may be duplicated to "
"account for different properties on different neighboring "
"triangles.")
.def("precision", &Manifold::Precision,
"Returns the precision of this Manifold's vertices, which tracks "
"the approximate rounding error over all the transforms and "
"operations that have led to this state. Any triangles that are "
"colinear within this precision are considered degenerate and "
"removed. This is the value of &epsilon; defining "
"[&epsilon;-valid](https://github.com/elalish/manifold/wiki/"
"Manifold-Library#definition-of-%CE%B5-valid).")
.def("genus", &Manifold::Genus,
"The genus is a topological property of the manifold, representing "
"the number of \"handles\". A sphere is 0, torus 1, etc. It is only "
"meaningful for a single mesh, so it is best to call Decompose() "
"first.")
.def(
"get_volume",
[](Manifold self) { return self.GetProperties().volume; },
"Get the volume of the manifold\n This is clamped to zero for a "
"given face if they are within the Precision().")
.def(
"get_surface_area",
[](Manifold self) { return self.GetProperties().surfaceArea; },
"Get the surface area of the manifold\n This is clamped to zero for "
"a given face if they are within the Precision().")
.def("original_id", &Manifold::OriginalID,
"If this mesh is an original, this returns its meshID that can be "
"referenced by product manifolds' MeshRelation. If this manifold is "
"a product, this returns -1.")
.def("as_original", &Manifold::AsOriginal,
"This function condenses all coplanar faces in the relation, and "
"collapses those edges. In the process the relation to ancestor "
"meshes is lost and this new Manifold is marked an original. "
"Properties are preserved, so if they do not match across an edge, "
"that edge will be kept.")
.def("is_empty", &Manifold::IsEmpty,
"Does the Manifold have any triangles?")
.def(
"decompose", [](Manifold self) { return self.Decompose(); },
" This operation returns a vector of Manifolds that are "
"topologically disconnected. If everything is connected, the vector "
"is length one, containing a copy of the original. It is the inverse "
"operation of Compose().")
.def("split", &Manifold::Split,
"Split cuts this manifold in two using the cutter manifold. The "
"first result is the intersection, second is the difference. This "
"is more efficient than doing them separately.")
.def(
"split_by_plane",
[](Manifold self, Float3 normal, float originOffset) {
return self.SplitByPlane(
{std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)},
originOffset);
},
py::arg("normal"), py::arg("origin_offset"),
"Convenient version of Split() for a half-space."
"\n\n"
"@param normal This vector is normal to the cutting plane and its "
"length does not matter. The first result is in the direction of "
"this vector, the second result is on the opposite side.\n"
"@param originOffset The distance of the plane from the origin in "
"the direction of the normal vector.")
.def(
"trim_by_plane",
[](Manifold self, Float3 normal, float originOffset) {
return self.TrimByPlane(
{std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)},
originOffset);
},
py::arg("normal"), py::arg("origin_offset"),
"Identical to SplitByPlane(), but calculating and returning only the "
"first result."
"\n\n"
"@param normal This vector is normal to the cutting plane and its "
"length does not matter. The result is in the direction of this "
"vector from the plane.\n"
"@param originOffset The distance of the plane from the origin in "
"the direction of the normal vector.")
.def_static(
"smooth", [](const Mesh &mesh) { return Manifold::Smooth(mesh); },
"Constructs a smooth version of the input mesh by creating tangents; "
"this method will throw if you have supplied tangents with your "
"mesh already. The actual triangle resolution is unchanged; use the "
"Refine() method to interpolate to a higher-resolution curve."
"\n\n"
"By default, every edge is calculated for maximum smoothness (very "
"much approximately), attempting to minimize the maximum mean "
"Curvature magnitude. No higher-order derivatives are considered, "
"as the interpolation is independent per triangle, only sharing "
"constraints on their boundaries."
"\n\n"
":param mesh: input Mesh.\n"
":param sharpenedEdges: If desired, you can supply a vector of "
"sharpened halfedges, which should in general be a small subset of "
"all halfedges. Order of entries doesn't matter, as each one "
"specifies the desired smoothness (between zero and one, with one "
"the default for all unspecified halfedges) and the halfedge index "
"(3 * triangle index + [0,1,2] where 0 is the edge between triVert 0 "
"and 1, etc)."
"\n\n"
"At a smoothness value of zero, a sharp crease is made. The "
"smoothness is interpolated along each edge, so the specified value "
"should be thought of as an average. Where exactly two sharpened "
"edges meet at a vertex, their tangents are rotated to be colinear "
"so that the sharpened edge can be continuous. Vertices with only "
"one sharpened edge are completely smooth, allowing sharpened edges "
"to smoothly vanish at termination. A single vertex can be sharpened "
"by sharping all edges that are incident on it, allowing cones to be "
"formed.")
.def_static(
"from_mesh", [](const Mesh &mesh) { return Manifold(mesh); },
py::arg("mesh"))
.def_static(
"tetrahedron", []() { return Manifold::Tetrahedron(); },
"Constructs a tetrahedron centered at the origin with one vertex at "
"(1,1,1) and the rest at similarly symmetric points.")
.def_static(
"cube",
[](Float3 size, bool center = false) {
return Manifold::Cube(
glm::vec3(std::get<0>(size), std::get<1>(size),
std::get<2>(size)),
center);
},
py::arg("size") = std::make_tuple(1.0f, 1.0f, 1.0f),
py::arg("center") = false,
"Constructs a unit cube (edge lengths all one), by default in the "
"first octant, touching the origin."
"\n\n"
":param size: The X, Y, and Z dimensions of the box.\n"
":param center: Set to true to shift the center to the origin.")
.def_static(
"cube",
[](py::array_t<float> &size, bool center = false) {
auto size_view = size.unchecked<1>();
if (size_view.shape(0) != 3)
throw std::runtime_error("Invalid vector shape");
return Manifold::Cube(
glm::vec3(size_view(0), size_view(1), size_view(2)), center);
},
py::arg("size"), py::arg("center") = false,
"Constructs a unit cube (edge lengths all one), by default in the "
"first octant, touching the origin."
"\n\n"
":param size: The X, Y, and Z dimensions of the box.\n"
":param center: Set to true to shift the center to the origin.")
.def_static(
"cube",
[](float x, float y, float z, bool center = false) {
return Manifold::Cube(glm::vec3(x, y, z), center);
},
py::arg("x"), py::arg("y"), py::arg("z"), py::arg("center") = false,
"Constructs a unit cube (edge lengths all one), by default in the "
"first octant, touching the origin."
"\n\n"
":param x: The X dimensions of the box.\n"
":param y: The Y dimensions of the box.\n"
":param z: The Z dimensions of the box.\n"
":param center: Set to true to shift the center to the origin.")
.def_static(
"cylinder",
[](float height, float radiusLow, float radiusHigh = -1.0f,
int circularSegments = 0, bool center = false) {
return Manifold::Cylinder(height, radiusLow, radiusHigh,
circularSegments, center);
},
py::arg("height"), py::arg("radius_low"),
py::arg("radius_high") = -1.0f, py::arg("circular_segments") = 0,
py::arg("center") = false,
"A convenience constructor for the common case of extruding a "
"circle. Can also form cones if both radii are specified."
"\n\n"
":param height: Z-extent\n"
":param radiusLow: Radius of bottom circle. Must be positive.\n"
":param radiusHigh: Radius of top circle. Can equal zero. Default "
"(-1) is equal to radiusLow.\n"
":param circularSegments: How many line segments to use around the "
"circle. Default (-1) is calculated by the static Defaults.\n"
":param center: Set to true to shift the center to the origin. "
"Default is origin at the bottom.")
.def_static(
"sphere",
[](float radius, int circularSegments = 0) {
return Manifold::Sphere(radius, circularSegments);
},
py::arg("radius"), py::arg("circular_segments") = 0,
"Constructs a geodesic sphere of a given radius.\n"
"\n"
":param radius: Radius of the sphere. Must be positive.\n"
":param circularSegments: Number of segments along its diameter. "
"This number will always be rounded up to the nearest factor of "
"four, as this sphere is constructed by refining an octahedron. This "
"means there are a circle of vertices on all three of the axis "
"planes. Default is calculated by the static Defaults.")
.def_static("reserve_ids", Manifold::ReserveIDs, py::arg("n"),
"Returns the first of n sequential new unique mesh IDs for "
"marking sets of triangles that can be looked up after "
"further operations. Assign to MeshGL.runOriginalID vector");
py::class_<PolygonsWrapper>(m, "Polygons")
.def(py::init([](std::vector<std::vector<Float2>> &polygons) {
std::vector<SimplePolygon> simplePolygons(polygons.size());
for (int i = 0; i < polygons.size(); i++) {
std::vector<glm::vec2> vertices(polygons[i].size());
for (int j = 0; j < polygons[i].size(); j++) {
vertices[j] = {std::get<0>(polygons[i][j]),
std::get<1>(polygons[i][j])};
}
simplePolygons[i] = {vertices};
}
return PolygonsWrapper{std::make_unique<Polygons>(simplePolygons)};
}),
py::arg("polygons"),
"Construct the Polygons object from a list of simple polygon, "
"where each simple polygon is a list of points (pair of floats).")
.def(
"extrude",
[](PolygonsWrapper &self, float height, int nDivisions = 0,
float twistDegrees = 0.0f,
Float2 scaleTop = std::make_tuple(1.0f, 1.0f)) {
glm::vec2 scaleTopVec(std::get<0>(scaleTop), std::get<1>(scaleTop));
return Manifold::Extrude(*self.polygons, height, nDivisions,
twistDegrees, scaleTopVec);
},
py::arg("height"), py::arg("n_divisions") = 0,
py::arg("twist_degrees") = 0.0f,
py::arg("scale_top") = std::make_tuple(1.0f, 1.0f),
"Constructs a manifold from the set of polygons by extruding them "
"along the Z-axis.\n"
"\n"
":param height: Z-extent of extrusion.\n"
":param nDivisions: Number of extra copies of the crossSection to "
"insert into the shape vertically; especially useful in combination "
"with twistDegrees to avoid interpolation artifacts. Default is "
"none.\n"
":param twistDegrees: Amount to twist the top crossSection relative "
"to the bottom, interpolated linearly for the divisions in between.\n"
":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. Default (1, 1).")
.def(
"revolve",
[](PolygonsWrapper &self, int circularSegments = 0) {
return Manifold::Revolve(*self.polygons, circularSegments);
},
py::arg("circular_segments") = 0,
"Constructs a manifold from the set of polygons by revolving this "
"cross-section around its Y-axis and then setting this as the Z-axis "
"of the resulting manifold. If the polygons cross the Y-axis, only "
"the part on the positive X side is used. Geometrically valid input "
"will result in geometrically valid output.\n"
"\n"
":param circularSegments: Number of segments along its diameter. "
"Default is calculated by the static Defaults.");
py::class_<Mesh>(m, "Mesh")
.def(py::init([](py::array_t<float> &vertPos, py::array_t<int> &triVerts,
py::array_t<float> &vertNormal,
py::array_t<float> &halfedgeTangent) {
auto vertPos_view = vertPos.unchecked<2>();
auto triVerts_view = triVerts.unchecked<2>();
auto vertNormal_view = vertNormal.unchecked<2>();
auto halfedgeTangent_view = halfedgeTangent.unchecked<2>();
if (vertPos_view.shape(1) != 3)
throw std::runtime_error("Invalid vert_pos shape");
if (triVerts_view.shape(1) != 3)
throw std::runtime_error("Invalid tri_verts shape");
if (vertNormal_view.shape(0) != 0) {
if (vertNormal_view.shape(1) != 3)
throw std::runtime_error("Invalid vert_normal shape");
if (vertNormal_view.shape(0) != vertPos_view.shape(0))
throw std::runtime_error(
"vert_normal must have the same length as vert_pos");
}
if (halfedgeTangent_view.shape(0) != 0) {
if (halfedgeTangent_view.shape(1) != 4)
throw std::runtime_error("Invalid halfedge_tangent shape");
if (halfedgeTangent_view.shape(0) != triVerts_view.shape(0) * 3)
throw std::runtime_error(
"halfedge_tangent must be three times as long as "
"tri_verts");
}
std::vector<glm::vec3> vertPos_vec(vertPos_view.shape(0));
std::vector<glm::ivec3> triVerts_vec(triVerts_view.shape(0));
std::vector<glm::vec3> vertNormal_vec(vertNormal_view.shape(0));
std::vector<glm::vec4> halfedgeTangent_vec(
halfedgeTangent_view.shape(0));
for (int i = 0; i < vertPos_view.shape(0); i++)
for (const int j : {0, 1, 2})
vertPos_vec[i][j] = vertPos_view(i, j);
for (int i = 0; i < triVerts_view.shape(0); i++)
for (const int j : {0, 1, 2})
triVerts_vec[i][j] = triVerts_view(i, j);
for (int i = 0; i < vertNormal_view.shape(0); i++)
for (const int j : {0, 1, 2})
vertNormal_vec[i][j] = vertNormal_view(i, j);
for (int i = 0; i < halfedgeTangent_view.shape(0); i++)
for (const int j : {0, 1, 2, 3})
halfedgeTangent_vec[i][j] = halfedgeTangent_view(i, j);
return Mesh({vertPos_vec, triVerts_vec, vertNormal_vec,
halfedgeTangent_vec});
}),
py::arg("vert_pos"), py::arg("tri_verts"), py::arg("vert_normal"),
py::arg("halfedge_tangent"))
.def_property_readonly("vert_pos",
[](Mesh &self) {
const int numVert = self.vertPos.size();
py::array_t<float> vert_pos({numVert, 3});
auto vert_pos_view =
vert_pos.mutable_unchecked<2>();
for (int i = 0; i < numVert; ++i)
for (const int j : {0, 1, 2})
vert_pos_view(i, j) = self.vertPos[i][j];
return vert_pos;
})
.def_property_readonly("tri_verts",
[](Mesh &self) {
const int numTri = self.triVerts.size();
py::array_t<int> tri_verts({numTri, 3});
auto tri_verts_view =
tri_verts.mutable_unchecked<2>();
for (int i = 0; i < numTri; ++i)
for (const int j : {0, 1, 2})
tri_verts_view(i, j) = self.triVerts[i][j];
return tri_verts;
})
.def_property_readonly(
"vert_normal",
[](Mesh &self) {
const int numVert = self.vertNormal.size();
py::array_t<float> vert_normal({numVert, 3});
auto vert_normal_view = vert_normal.mutable_unchecked<2>();
for (int i = 0; i < numVert; ++i)
for (const int j : {0, 1, 2})
vert_normal_view(i, j) = self.vertNormal[i][j];
return vert_normal;
})
.def_property_readonly("halfedge_tangent", [](Mesh &self) {
const int numEdge = self.halfedgeTangent.size();
py::array_t<float> halfedge_tangent({numEdge, 4});
auto halfedge_tangent_view = halfedge_tangent.mutable_unchecked<2>();
for (int i = 0; i < numEdge; ++i)
for (const int j : {0, 1, 2, 3})
halfedge_tangent_view(i, j) = self.halfedgeTangent[i][j];
return halfedge_tangent;
});
py::enum_<CrossSection::FillRule>(m, "FillRule")
.value("EvenOdd", CrossSection::FillRule::EvenOdd,
"Only odd numbered sub-regions are filled.")
.value("NonZero", CrossSection::FillRule::NonZero,
"Only non-zero sub-regions are filled.")
.value("Positive", CrossSection::FillRule::Positive,
"Only sub-regions with winding counts > 0 are filled.")
.value("Negative", CrossSection::FillRule::Negative,
"Only sub-regions with winding counts < 0 are filled.");
py::enum_<CrossSection::JoinType>(m, "JoinType")
.value("Square", CrossSection::JoinType::Square,
"Squaring is applied uniformly at all joins where the internal "
"join angle is less that 90 degrees. The squared edge will be at "
"exactly the offset distance from the join vertex.")
.value(
"Round", CrossSection::JoinType::Square,
"Rounding is applied to all joins that have convex external angles, "
"and it maintains the exact offset distance from the join vertex.")
.value(
"Miter", CrossSection::JoinType::Miter,
"There's a necessary limit to mitered joins (to avoid narrow angled "
"joins producing excessively long and narrow "
"[spikes](http://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/"
"Classes/ClipperOffset/Properties/MiterLimit.htm)). So where mitered "
"joins would exceed a given maximum miter distance (relative to the "
"offset distance), these are 'squared' instead.");
py::class_<CrossSection>(
m, "CrossSection",
"Two-dimensional cross sections guaranteed to be without "
"self-intersections, or overlaps between polygons (from construction "
"onwards). This class makes use of the "
"[Clipper2](http://www.angusj.com/clipper2/Docs/Overview.htm) library "
"for polygon clipping (boolean) and offsetting operations.")
.def(py::init<>())
.def(py::init(
[](PolygonsWrapper &contours, CrossSection::FillRule fillrule) {
return CrossSection(*contours.polygons, fillrule);
}),
py::arg("contours"),
py::arg("fillrule") = CrossSection::FillRule::Positive)
.def("area", &CrossSection::Area)
.def("num_vert", &CrossSection::NumVert)
.def("num_contour", &CrossSection::NumContour)
.def("is_empty", &CrossSection::IsEmpty)
.def("translate",
[](CrossSection self, Float2 v) {
return self.Translate({std::get<0>(v), std::get<1>(v)});
})
.def("rotate", &CrossSection::Rotate)
.def("scale",
[](CrossSection self, Float2 s) {
return self.Scale({std::get<0>(s), std::get<1>(s)});
})
.def("mirror",
[](CrossSection self, Float2 ax) {
return self.Mirror({std::get<0>(ax), std::get<1>(ax)});
})
.def("transform",
[](CrossSection self, py::array_t<float> &mat) {
auto mat_view = mat.unchecked<2>();
if (mat_view.shape(0) != 2 || mat_view.shape(1) != 3)
throw std::runtime_error("Invalid matrix shape");
glm::mat3x2 mat_glm;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
mat_glm[j][i] = mat_view(i, j);
}
}
return self.Transform(mat_glm);
})
.def(
"warp",
[](CrossSection self, const std::function<Float2(Float2)> &f) {
return self.Warp([&f](glm::vec2 &v) {
Float2 fv = f(std::make_tuple(v.x, v.y));
v.x = std::get<0>(fv);
v.y = std::get<1>(fv);
});
},
py::arg("f"))
.def("simplify", &CrossSection::Simplify)
.def("offset", &CrossSection::Offset, py::arg("delta"),
py::arg("join_type"), py::arg("miter_limit") = 2.0,
py::arg("arc_tolerance") = 0.0)
.def(py::self + py::self, "Boolean union.")
.def(py::self - py::self, "Boolean difference.")
.def(py::self ^ py::self, "Boolean intersection.")
.def("decompose", &CrossSection::Decompose)
.def("to_polygons",
[](CrossSection self) {
return PolygonsWrapper{
std::make_unique<Polygons>(self.ToPolygons())};
})
.def(
"extrude",
[](CrossSection self, float height, int nDivisions = 0,
float twistDegrees = 0.0f,
Float2 scaleTop = std::make_tuple(1.0f, 1.0f)) {
glm::vec2 scaleTopVec(std::get<0>(scaleTop), std::get<1>(scaleTop));
return Manifold::Extrude(self, height, nDivisions, twistDegrees,
scaleTopVec);
},
py::arg("height"), py::arg("n_divisions") = 0,
py::arg("twist_degrees") = 0.0f,
py::arg("scale_top") = std::make_tuple(1.0f, 1.0f),
"Constructs a manifold from the set of polygons by extruding them "
"along the Z-axis.\n"
"\n"
":param height: Z-extent of extrusion.\n"
":param nDivisions: Number of extra copies of the crossSection to "
"insert into the shape vertically; especially useful in combination "
"with twistDegrees to avoid interpolation artifacts. Default is "
"none.\n"
":param twistDegrees: Amount to twist the top crossSection relative "
"to the bottom, interpolated linearly for the divisions in between.\n"
":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. Default (1, 1).")
.def(
"revolve",
[](CrossSection self, int circularSegments = 0) {
return Manifold::Revolve(self, circularSegments);
},
py::arg("circular_segments") = 0,
"Constructs a manifold from the set of polygons by revolving this "
"cross-section around its Y-axis and then setting this as the Z-axis "
"of the resulting manifold. If the polygons cross the Y-axis, only "
"the part on the positive X side is used. Geometrically valid input "
"will result in geometrically valid output.\n"
"\n"
":param circularSegments: Number of segments along its diameter. "
"Default is calculated by the static Defaults.")
.def_static(
"square",
[](Float2 dims, bool center) {
return CrossSection::Square({std::get<0>(dims), std::get<1>(dims)},
center);
},
py::arg("dims"), py::arg("center") = false)
.def_static(
"circle",
[](float radius, int circularSegments) {
return CrossSection::Circle(radius, circularSegments);
},
py::arg("radius"), py::arg("circularSegments") = 0);
}
+1 -1
View File
@@ -1 +1 @@
add_subdirectory(nanobind)
add_subdirectory(pybind11)
+10 -18
View File
@@ -16,31 +16,23 @@ project(wasm)
add_executable(manifoldjs bindings.cpp)
set_source_files_properties(bindings.cpp PROPERTIES OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js)
# make sure that we recompile the wasm when bindings.js is being modified
set_source_files_properties(bindings.cpp OBJECT_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/bindings.js)
target_link_libraries(manifoldjs manifold sdf cross_section polygon)
target_compile_options(manifoldjs PRIVATE ${MANIFOLD_FLAGS})
target_compile_options(manifoldjs PRIVATE ${MANIFOLD_FLAGS} -fexceptions)
target_link_options(manifoldjs PUBLIC --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js --bind -sALLOW_TABLE_GROWTH=1
-sEXPORTED_RUNTIME_METHODS=addFunction,removeFunction -sMODULARIZE=1 -sEXPORT_ES6=1)
target_compile_features(manifoldjs PUBLIC cxx_std_17)
set_target_properties(manifoldjs PROPERTIES OUTPUT_NAME "manifold")
file(MAKE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/examples/built)
# ensure that interface files are copied over when modified
add_custom_target(js_deps ALL
DEPENDS manifoldjs
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts)
file(COPY examples;test;. DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS examples;test)
add_custom_command(
TARGET js_deps POST_BUILD
TARGET manifoldjs POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_BINARY_DIR}/manifold.*
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts
${CMAKE_CURRENT_SOURCE_DIR}/examples/built/)
add_custom_command(
TARGET js_deps POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts
${CMAKE_CURRENT_SOURCE_DIR}/examples/public/)
${CMAKE_CURRENT_BINARY_DIR}/manifold.*
${CMAKE_CURRENT_BINARY_DIR}/manifold-*.d.ts
${CMAKE_CURRENT_BINARY_DIR}/examples/)
+1 -5
View File
@@ -20,11 +20,7 @@ The most significant contribution here is a guaranteed-manifold [mesh Boolean](h
## Note on memory management
Since Manifold is a WASM module, it does not automatically garbage-collect like regular JavaScript. You must manually `delete()` each manifold object constructed by your scripts, see [discussion](https://github.com/elalish/manifold/discussions/256#discussioncomment-3944287).
## Examples
Please see our usage [examples](https://github.com/elalish/manifold/tree/master/bindings/wasm/examples) to see how to interface this library with `three.js`, `<model-viewer>`, and `glTF`. Of particular note are the included libraries for lossless roundtrip of manifold meshes through glTF files, via a new extension: [EXT_manifold](https://github.com/KhronosGroup/glTF/pull/2286).
Since Manifold is a WASM module, it does not automatically garbage-collect like regular JavaScript. You must manually `delete()` each manifold object constructed by your scripts, see https://github.com/elalish/manifold/discussions/256#discussioncomment-3944287.
## About the author
+175 -85
View File
@@ -17,15 +17,157 @@
#include <vector>
#include "cross_section.h"
#include "helpers.cpp"
#include "manifold.h"
#include "polygon.h"
#include "sdf.h"
using namespace emscripten;
#include <manifold.h>
#include <polygon.h>
#include <sdf.h>
using namespace manifold;
Manifold Union(const Manifold& a, const Manifold& b) { return a + b; }
Manifold Difference(const Manifold& a, const Manifold& b) { return a - b; }
Manifold Intersection(const Manifold& a, const Manifold& b) { return a ^ b; }
Manifold UnionN(const std::vector<Manifold>& manifolds) {
return Manifold::BatchBoolean(manifolds, OpType::Add);
}
Manifold DifferenceN(const std::vector<Manifold>& manifolds) {
return Manifold::BatchBoolean(manifolds, OpType::Subtract);
}
Manifold IntersectionN(const std::vector<Manifold>& manifolds) {
return Manifold::BatchBoolean(manifolds, OpType::Intersect);
}
std::vector<SimplePolygon> ToPolygon(
std::vector<std::vector<glm::vec2>>& polygons) {
std::vector<SimplePolygon> simplePolygons(polygons.size());
for (int i = 0; i < polygons.size(); i++) {
std::vector<glm::vec2> vertices(polygons[i].size());
for (int j = 0; j < polygons[i].size(); j++) {
vertices[j] = polygons[i][j];
}
simplePolygons[i] = {vertices};
}
return simplePolygons;
}
val GetMeshJS(const Manifold& manifold, const glm::ivec3& normalIdx) {
MeshGL mesh = manifold.GetMeshGL(normalIdx);
val meshJS = val::object();
meshJS.set("numProp", mesh.numProp);
meshJS.set("triVerts",
val(typed_memory_view(mesh.triVerts.size(), mesh.triVerts.data()))
.call<val>("slice"));
meshJS.set("vertProperties",
val(typed_memory_view(mesh.vertProperties.size(),
mesh.vertProperties.data()))
.call<val>("slice"));
meshJS.set("mergeFromVert", val(typed_memory_view(mesh.mergeFromVert.size(),
mesh.mergeFromVert.data()))
.call<val>("slice"));
meshJS.set("mergeToVert", val(typed_memory_view(mesh.mergeToVert.size(),
mesh.mergeToVert.data()))
.call<val>("slice"));
meshJS.set("runIndex",
val(typed_memory_view(mesh.runIndex.size(), mesh.runIndex.data()))
.call<val>("slice"));
meshJS.set("runOriginalID", val(typed_memory_view(mesh.runOriginalID.size(),
mesh.runOriginalID.data()))
.call<val>("slice"));
meshJS.set("faceID",
val(typed_memory_view(mesh.faceID.size(), mesh.faceID.data()))
.call<val>("slice"));
meshJS.set("halfedgeTangent",
val(typed_memory_view(mesh.halfedgeTangent.size(),
mesh.halfedgeTangent.data()))
.call<val>("slice"));
meshJS.set("runTransform", val(typed_memory_view(mesh.runTransform.size(),
mesh.runTransform.data()))
.call<val>("slice"));
return meshJS;
}
MeshGL MeshJS2GL(const val& mesh) {
MeshGL out;
out.numProp = mesh["numProp"].as<int>();
out.triVerts = convertJSArrayToNumberVector<uint32_t>(mesh["triVerts"]);
out.vertProperties =
convertJSArrayToNumberVector<float>(mesh["vertProperties"]);
if (mesh["mergeFromVert"] != val::undefined()) {
out.mergeFromVert =
convertJSArrayToNumberVector<uint32_t>(mesh["mergeFromVert"]);
}
if (mesh["mergeToVert"] != val::undefined()) {
out.mergeToVert =
convertJSArrayToNumberVector<uint32_t>(mesh["mergeToVert"]);
}
if (mesh["runIndex"] != val::undefined()) {
out.runIndex = convertJSArrayToNumberVector<uint32_t>(mesh["runIndex"]);
}
if (mesh["runOriginalID"] != val::undefined()) {
out.runOriginalID =
convertJSArrayToNumberVector<uint32_t>(mesh["runOriginalID"]);
}
if (mesh["faceID"] != val::undefined()) {
out.faceID = convertJSArrayToNumberVector<uint32_t>(mesh["faceID"]);
}
if (mesh["halfedgeTangent"] != val::undefined()) {
out.halfedgeTangent =
convertJSArrayToNumberVector<float>(mesh["halfedgeTangent"]);
}
if (mesh["runTransform"] != val::undefined()) {
out.runTransform =
convertJSArrayToNumberVector<float>(mesh["runTransform"]);
}
return out;
}
Manifold FromMeshJS(const val& mesh) { return Manifold(MeshJS2GL(mesh)); }
Manifold Smooth(const val& mesh,
const std::vector<Smoothness>& sharpenedEdges = {}) {
return Manifold::Smooth(MeshJS2GL(mesh), sharpenedEdges);
}
Manifold Extrude(std::vector<std::vector<glm::vec2>>& polygons, float height,
int nDivisions, float twistDegrees, glm::vec2 scaleTop) {
return Manifold::Extrude(ToPolygon(polygons), height, nDivisions,
twistDegrees, scaleTop);
}
Manifold Revolve(std::vector<std::vector<glm::vec2>>& polygons,
int circularSegments) {
return Manifold::Revolve(ToPolygon(polygons), circularSegments);
}
Manifold Transform(Manifold& manifold, const val& mat) {
std::vector<float> array = convertJSArrayToNumberVector<float>(mat);
glm::mat4x3 matrix;
for (const int col : {0, 1, 2, 3})
for (const int row : {0, 1, 2}) matrix[col][row] = array[col * 4 + row];
return manifold.Transform(matrix);
}
Manifold Warp(Manifold& manifold, uintptr_t funcPtr) {
void (*f)(glm::vec3&) = reinterpret_cast<void (*)(glm::vec3&)>(funcPtr);
return manifold.Warp(f);
}
Manifold LevelSetJs(uintptr_t funcPtr, Box bounds, float edgeLength,
float level) {
float (*f)(const glm::vec3&) =
reinterpret_cast<float (*)(const glm::vec3&)>(funcPtr);
Mesh m = LevelSet(f, bounds, edgeLength, level);
return Manifold(m);
}
EMSCRIPTEN_BINDINGS(whatever) {
value_object<glm::vec2>("vec2")
.field("x", &glm::vec2::x)
@@ -63,18 +205,6 @@ EMSCRIPTEN_BINDINGS(whatever) {
.value("FaceIDWrongLength", Manifold::Error::FaceIDWrongLength)
.value("InvalidConstruction", Manifold::Error::InvalidConstruction);
enum_<CrossSection::FillRule>("fillrule")
.value("EvenOdd", CrossSection::FillRule::EvenOdd)
.value("NonZero", CrossSection::FillRule::NonZero)
.value("Positive", CrossSection::FillRule::Positive)
.value("Negative", CrossSection::FillRule::Negative);
enum_<CrossSection::JoinType>("jointype")
.value("Square", CrossSection::JoinType::Square)
.value("Round", CrossSection::JoinType::Round)
.value("Miter", CrossSection::JoinType::Miter);
value_object<Rect>("rect").field("min", &Rect::min).field("max", &Rect::max);
value_object<Box>("box").field("min", &Box::min).field("max", &Box::max);
value_object<Smoothness>("smoothness")
@@ -85,65 +215,33 @@ EMSCRIPTEN_BINDINGS(whatever) {
.field("surfaceArea", &Properties::surfaceArea)
.field("volume", &Properties::volume);
value_object<Curvature>("curvature")
.field("maxMeanCurvature", &Curvature::maxMeanCurvature)
.field("minMeanCurvature", &Curvature::minMeanCurvature)
.field("maxGaussianCurvature", &Curvature::maxGaussianCurvature)
.field("minGaussianCurvature", &Curvature::minGaussianCurvature)
.field("vertMeanCurvature", &Curvature::vertMeanCurvature)
.field("vertGaussianCurvature", &Curvature::vertGaussianCurvature);
register_vector<glm::ivec3>("Vector_ivec3");
register_vector<glm::vec3>("Vector_vec3");
register_vector<glm::vec2>("Vector_vec2");
register_vector<std::vector<glm::vec2>>("Vector2_vec2");
register_vector<float>("Vector_f32");
register_vector<CrossSection>("Vector_crossSection");
register_vector<Manifold>("Vector_manifold");
register_vector<Smoothness>("Vector_smoothness");
register_vector<glm::vec4>("Vector_vec4");
class_<CrossSection>("CrossSection")
.constructor(&cross_js::OfPolygons)
.function("_add", &cross_js::Union)
.function("_subtract", &cross_js::Difference)
.function("_intersect", &cross_js::Intersection)
.function("_Warp", &cross_js::Warp)
.function("transform", &cross_js::Transform)
.function("_Translate", &CrossSection::Translate)
.function("rotate", &CrossSection::Rotate)
.function("_Scale", &CrossSection::Scale)
.function("_Mirror", &CrossSection::Mirror)
.function("_Decompose", &CrossSection::Decompose)
.function("isEmpty", &CrossSection::IsEmpty)
.function("area", &CrossSection::Area)
.function("numVert", &CrossSection::NumVert)
.function("numContour", &CrossSection::NumContour)
.function("_Bounds", &CrossSection::Bounds)
.function("simplify", &CrossSection::Simplify)
.function("_Offset", &cross_js::Offset)
.function("_ToPolygons", &CrossSection::ToPolygons)
.function("hull",
select_overload<CrossSection() const>(&CrossSection::Hull));
// CrossSection Static Methods
function("_Square", &CrossSection::Square);
function("_Circle", &CrossSection::Circle);
function("_crossSectionCompose", &CrossSection::Compose);
function("_crossSectionUnionN", &cross_js::UnionN);
function("_crossSectionDifferenceN", &cross_js::DifferenceN);
function("_crossSectionIntersectionN", &cross_js::IntersectionN);
function("_crossSectionCollectVertices", &cross_js::CollectVertices);
function("_crossSectionHullPoints",
select_overload<CrossSection(std::vector<glm::vec2>)>(
&CrossSection::Hull));
class_<Manifold>("Manifold")
.constructor(&man_js::FromMeshJS)
.function("add", &man_js::Union)
.function("subtract", &man_js::Difference)
.function("intersect", &man_js::Intersection)
.function("_Split", &man_js::Split)
.function("_SplitByPlane", &man_js::SplitByPlane)
.constructor(&FromMeshJS)
.function("add", &Union)
.function("subtract", &Difference)
.function("intersect", &Intersection)
.function("_TrimByPlane", &Manifold::TrimByPlane)
.function("hull", select_overload<Manifold() const>(&Manifold::Hull))
.function("_GetMeshJS", &js::GetMeshJS)
.function("_GetMeshJS", &GetMeshJS)
.function("refine", &Manifold::Refine)
.function("_Warp", &man_js::Warp)
.function("_SetProperties", &man_js::SetProperties)
.function("transform", &man_js::Transform)
.function("_Warp", &Warp)
.function("transform", &Transform)
.function("_Translate", &Manifold::Translate)
.function("_Rotate", &Manifold::Rotate)
.function("_Scale", &Manifold::Scale)
@@ -155,40 +253,32 @@ EMSCRIPTEN_BINDINGS(whatever) {
.function("numVert", &Manifold::NumVert)
.function("numEdge", &Manifold::NumEdge)
.function("numTri", &Manifold::NumTri)
.function("numProp", &Manifold::NumProp)
.function("numPropVert", &Manifold::NumPropVert)
.function("_boundingBox", &Manifold::BoundingBox)
.function("precision", &Manifold::Precision)
.function("genus", &Manifold::Genus)
.function("getProperties", &Manifold::GetProperties)
.function("calculateCurvature", &Manifold::CalculateCurvature)
.function("_getCurvature", &Manifold::GetCurvature)
.function("originalID", &Manifold::OriginalID)
.function("asOriginal", &Manifold::AsOriginal);
// Manifold Static Methods
function("_Cube", &Manifold::Cube);
function("_Cylinder", &Manifold::Cylinder);
function("_Sphere", &Manifold::Sphere);
function("_Tetrahedron", &Manifold::Tetrahedron);
function("_Smooth", &js::Smooth);
function("_Extrude", &Manifold::Extrude);
function("tetrahedron", &Manifold::Tetrahedron);
function("_Smooth", &Smooth);
function("_Extrude", &Extrude);
function("_Triangulate", &Triangulate);
function("_Revolve", &Manifold::Revolve);
function("_LevelSet", &man_js::LevelSet);
function("_Merge", &js::Merge);
function("_ReserveIDs", &Manifold::ReserveIDs);
function("_manifoldCompose", &Manifold::Compose);
function("_manifoldUnionN", &man_js::UnionN);
function("_manifoldDifferenceN", &man_js::DifferenceN);
function("_manifoldIntersectionN", &man_js::IntersectionN);
function("_manifoldCollectVertices", &man_js::CollectVertices);
function("_manifoldHullPoints",
select_overload<Manifold(const std::vector<glm::vec3>&)>(
&Manifold::Hull));
function("_Revolve", &Revolve);
function("_LevelSet", &LevelSetJs);
function("_unionN", &UnionN);
function("_differenceN", &DifferenceN);
function("_intersectionN", &IntersectionN);
function("_Compose", &Manifold::Compose);
// Quality Globals
function("setMinCircularAngle", &Quality::SetMinCircularAngle);
function("setMinCircularEdgeLength", &Quality::SetMinCircularEdgeLength);
function("setCircularSegments", &Quality::SetCircularSegments);
function("getCircularSegments", &Quality::GetCircularSegments);
function("reserveIDs", &Manifold::ReserveIDs);
}
+64 -381
View File
@@ -17,8 +17,6 @@ Module.setup = function() {
if (_ManifoldInitialized) return;
_ManifoldInitialized = true;
// conversion utilities
function toVec(vec, list, f = x => x) {
if (list) {
for (let x of list) {
@@ -35,21 +33,6 @@ Module.setup = function() {
return result;
}
function vec2polygons(vec, f = x => x) {
const result = [];
const nPoly = vec.size();
for (let i = 0; i < nPoly; i++) {
const v = vec.get(i);
const nPts = v.size();
const poly = [];
for (let j = 0; j < nPts; j++) {
poly.push(f(v.get(j)));
}
result.push(poly);
}
return result;
}
function polygons2vec(polygons) {
if (polygons[0].length < 3) {
polygons = [polygons];
@@ -67,15 +50,7 @@ Module.setup = function() {
polygonsVec.delete();
}
function vararg2vec2(vec) {
if (vec[0] instanceof Array) return {x: vec[0][0], y: vec[0][1]};
if (typeof (vec[0]) == 'number')
// default to 0
return {x: vec[0] || 0, y: vec[1] || 0};
return vec[0];
}
function vararg2vec3(vec) {
function vararg2vec(vec) {
if (vec[0] instanceof Array)
return {x: vec[0][0], y: vec[0][1], z: vec[0][2]};
if (typeof (vec[0]) == 'number')
@@ -84,118 +59,6 @@ Module.setup = function() {
return vec[0];
}
function fillRuleToInt(fillRule) {
return fillRule == 'EvenOdd' ? 0 :
fillRule == 'NonZero' ? 1 :
fillRule == 'Negative' ? 3 :
/* Positive */ 2;
}
function joinTypeToInt(joinType) {
return joinType == 'Round' ? 1 : joinType == 'Miter' ? 2 : /* Square */ 0;
}
// CrossSection methods
const CrossSectionCtor = Module.CrossSection;
function cross(polygons, fillRule = 'Positive') {
if (polygons instanceof CrossSectionCtor) {
return polygons;
} else {
const polygonsVec = polygons2vec(polygons);
const cs = new CrossSectionCtor(polygonsVec, fillRuleToInt(fillRule));
disposePolygons(polygonsVec);
return cs;
}
};
Module.CrossSection.prototype.translate = function(...vec) {
return this._Translate(vararg2vec2(vec));
};
Module.CrossSection.prototype.scale = function(vec) {
// if only one factor provided, scale both x and y with it
if (typeof vec == 'number') {
return this._Scale({x: vec, y: vec});
}
return this._Scale(vararg2vec2([vec]));
};
Module.CrossSection.prototype.mirror = function(vec) {
return this._Mirror(vararg2vec2([vec]));
};
Module.CrossSection.prototype.warp = function(func) {
const wasmFuncPtr = addFunction(function(vec2Ptr) {
const x = getValue(vec2Ptr, 'float');
const y = getValue(vec2Ptr + 4, 'float');
const vert = [x, y];
func(vert);
setValue(vec2Ptr, vert[0], 'float');
setValue(vec2Ptr + 4, vert[1], 'float');
}, 'vi');
const out = this._Warp(wasmFuncPtr);
removeFunction(wasmFuncPtr);
return out;
};
Module.CrossSection.prototype.decompose = function() {
const vec = this._Decompose();
const result = fromVec(vec);
vec.delete();
return result;
};
Module.CrossSection.prototype.bounds = function() {
const result = this._Bounds();
return {
min: ['x', 'y'].map(f => result.min[f]),
max: ['x', 'y'].map(f => result.max[f]),
};
};
Module.CrossSection.prototype.offset = function(
delta, joinType = 'Square', miterLimit = 2.0, circularSegments = 0) {
return this._Offset(
delta, joinTypeToInt(joinType), miterLimit, circularSegments);
};
Module.CrossSection.prototype.extrude = function(
height, nDivisions = 0, twistDegrees = 0.0, scaleTop = [1.0, 1.0],
center = false) {
scaleTop = vararg2vec2([scaleTop]);
const man =
Module._Extrude(this, height, nDivisions, twistDegrees, scaleTop);
return (center ? man.translate([0., 0., -height / 2.]) : man);
};
Module.CrossSection.prototype.revolve = function(
circularSegments = 0, revolveDegrees = 360.0) {
return Module._Revolve(this, circularSegments, revolveDegrees);
};
Module.CrossSection.prototype.add = function(other) {
return this._add(cross(other));
};
Module.CrossSection.prototype.subtract = function(other) {
return this._subtract(cross(other));
};
Module.CrossSection.prototype.intersect = function(other) {
return this._intersect(cross(other));
};
Module.CrossSection.prototype.toPolygons = function() {
const vec = this._ToPolygons();
const result = vec2polygons(vec);
vec.delete();
return result;
};
// Manifold methods
Module.Manifold.prototype.warp = function(func) {
const wasmFuncPtr = addFunction(function(vec3Ptr) {
const x = getValue(vec3Ptr, 'float');
@@ -209,43 +72,11 @@ Module.setup = function() {
}, 'vi');
const out = this._Warp(wasmFuncPtr);
removeFunction(wasmFuncPtr);
const status = out.status();
if (status.value !== 0) {
throw new Module.ManifoldError(status.value);
}
return out;
};
Module.Manifold.prototype.setProperties = function(numProp, func) {
const oldNumProp = this.numProp();
const wasmFuncPtr = addFunction(function(newPtr, vec3Ptr, oldPtr) {
const newProp = [];
for (let i = 0; i < numProp; ++i) {
newProp[i] = getValue(newPtr + 4 * i, 'float');
}
const pos = [];
for (let i = 0; i < 3; ++i) {
pos[i] = getValue(vec3Ptr + 4 * i, 'float');
}
const oldProp = [];
for (let i = 0; i < oldNumProp; ++i) {
oldProp[i] = getValue(oldPtr + 4 * i, 'float');
}
func(newProp, pos, oldProp);
for (let i = 0; i < numProp; ++i) {
setValue(newPtr + 4 * i, newProp[i], 'float');
}
}, 'viii');
const out = this._SetProperties(numProp, wasmFuncPtr);
removeFunction(wasmFuncPtr);
return out;
};
Module.Manifold.prototype.translate = function(...vec) {
return this._Translate(vararg2vec3(vec));
return this._Translate(vararg2vec(vec));
};
Module.Manifold.prototype.rotate = function(vec) {
@@ -253,33 +84,18 @@ Module.setup = function() {
};
Module.Manifold.prototype.scale = function(vec) {
// if only one factor provided, scale all three dimensions (xyz) with it
if (typeof vec == 'number') {
return this._Scale({x: vec, y: vec, z: vec});
}
return this._Scale(vararg2vec3([vec]));
return this._Scale(vararg2vec([vec]));
};
Module.Manifold.prototype.mirror = function(vec) {
return this._Mirror(vararg2vec3([vec]));
return this._Mirror(vararg2vec([vec]));
};
Module.Manifold.prototype.trimByPlane = function(normal, offset = 0.) {
return this._TrimByPlane(vararg2vec3([normal]), offset);
};
Module.Manifold.prototype.split = function(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 result = fromVec(vec);
vec.delete();
return result;
Module.Manifold.prototype.trimByPlane = function(normal, offset) {
return this._TrimByPlane(vararg2vec([normal]), offset);
};
Module.Manifold.prototype.decompose = function() {
@@ -289,12 +105,15 @@ Module.setup = function() {
return result;
};
Module.Manifold.prototype.boundingBox = function() {
const result = this._boundingBox();
return {
min: ['x', 'y', 'z'].map(f => result.min[f]),
max: ['x', 'y', 'z'].map(f => result.max[f]),
};
Module.Manifold.prototype.getCurvature = function() {
const result = this._getCurvature();
const oldMeanCurvature = result.vertMeanCurvature;
const oldGaussianCurvature = result.vertGaussianCurvature;
result.vertMeanCurvature = fromVec(oldMeanCurvature);
result.vertGaussianCurvature = fromVec(oldGaussianCurvature);
oldMeanCurvature.delete();
oldGaussianCurvature.delete();
return result;
};
class Mesh {
@@ -334,12 +153,6 @@ Module.setup = function() {
return this.runOriginalID.length;
}
merge() {
const {changed, mesh} = Module._Merge(this);
Object.assign(this, {...mesh});
return changed;
}
verts(tri) {
return this.triVerts.subarray(3 * tri, 3 * (tri + 1));
}
@@ -377,6 +190,14 @@ Module.setup = function() {
return new Mesh(this._GetMeshJS(normalIdx));
};
Module.Manifold.prototype.boundingBox = function() {
const result = this._boundingBox();
return {
min: ['x', 'y', 'z'].map(f => result.min[f]),
max: ['x', 'y', 'z'].map(f => result.max[f]),
};
};
Module.ManifoldError = function ManifoldError(code, ...args) {
let message = 'Unknown error';
switch (code) {
@@ -425,95 +246,6 @@ Module.setup = function() {
{value: Module.ManifoldError, writable: true, configurable: true}
});
// CrossSection Constructors
Module.CrossSection = function(polygons, fillRule = 'Positive') {
const polygonsVec = polygons2vec(polygons);
const cs = new CrossSectionCtor(polygonsVec, fillRuleToInt(fillRule));
disposePolygons(polygonsVec);
return cs;
};
Module.CrossSection.ofPolygons = function(polygons, fillRule = 'Positive') {
return new Module.CrossSection(polygons, fillRule);
};
Module.CrossSection.square = function(...args) {
let size = undefined;
if (args.length == 0)
size = {x: 1, y: 1};
else if (typeof args[0] == 'number')
size = {x: args[0], y: args[0]};
else
size = vararg2vec2(args);
const center = args[1] || false;
return Module._Square(size, center);
};
Module.CrossSection.circle = function(radius, circularSegments = 0) {
return Module._Circle(radius, circularSegments);
};
// allows args to be either CrossSection or polygons (constructed with
// Positive fill)
function crossSectionBatchbool(name) {
return function(...args) {
if (args.length == 1) args = args[0];
const v = new Module.Vector_crossSection();
for (const cs of args) v.push_back(cross(cs));
const result = Module['_crossSection' + name](v);
v.delete();
return result;
};
}
Module.CrossSection.compose = crossSectionBatchbool('Compose');
Module.CrossSection.union = crossSectionBatchbool('UnionN');
Module.CrossSection.difference = crossSectionBatchbool('DifferenceN');
Module.CrossSection.intersection = crossSectionBatchbool('IntersectionN');
function pushVec2(vec, ps) {
toVec(vec, ps, p => {
if (p instanceof Array) return {x: p[0], y: p[1]};
return p;
})
}
Module.CrossSection.hull = function(...args) {
if (args.length == 1) args = args[0];
let pts = new Module.Vector_vec2();
for (const cs of args) {
if (cs instanceof CrossSectionCtor) {
Module._crossSectionCollectVertices(pts, cs);
} else if (
cs instanceof Array && cs.length == 2 && typeof cs[0] == 'number') {
pts.push_back({x: cs[0], y: cs[1]});
} else if (cs.x) {
pts.push_back(cs);
} else {
const wrap =
((cs[0].length == 2 && typeof cs[0][0] == 'number') || cs[0].x);
const polys = wrap ? [cs] : cs;
for (const poly of polys) pushVec2(pts, poly);
}
}
const result = Module._crossSectionHullPoints(pts);
pts.delete();
return result;
};
Module.CrossSection.prototype = Object.create(CrossSectionCtor.prototype);
// Because the constructor and prototype are being replaced, instanceof will
// not work as desired unless we refer back to the original like this
Object.defineProperty(Module.CrossSection, Symbol.hasInstance, {
get: () => (t) => {
return (t instanceof CrossSectionCtor);
}
});
// Manifold Constructors
const ManifoldCtor = Module.Manifold;
Module.Manifold = function(mesh) {
const manifold = new ManifoldCtor(mesh);
@@ -526,38 +258,32 @@ Module.setup = function() {
return manifold;
};
Module.Manifold.ofMesh = function(mesh) {
return new Module.Manifold(mesh);
};
Module.Manifold.prototype = Object.create(ManifoldCtor.prototype);
Module.Manifold.tetrahedron = function() {
return Module._Tetrahedron();
};
Module.Manifold.cube = function(...args) {
Module.cube = function(...args) {
let size = undefined;
if (args.length == 0)
size = {x: 1, y: 1, z: 1};
else if (typeof args[0] == 'number')
size = {x: args[0], y: args[0], z: args[0]};
else
size = vararg2vec3(args);
size = vararg2vec(args);
const center = args[1] || false;
return Module._Cube(size, center);
};
Module.Manifold.cylinder = function(
Module.cylinder = function(
height, radiusLow, radiusHigh = -1.0, circularSegments = 0,
center = false) {
return Module._Cylinder(
height, radiusLow, radiusHigh, circularSegments, center);
};
Module.Manifold.sphere = function(radius, circularSegments = 0) {
Module.sphere = function(radius, circularSegments = 0) {
return Module._Sphere(radius, circularSegments);
};
Module.Manifold.smooth = function(mesh, sharpenedEdges = []) {
Module.smooth = function(mesh, sharpenedEdges = []) {
const sharp = new Module.Vector_smoothness();
toVec(sharp, sharpenedEdges);
const result = Module._Smooth(mesh, sharp);
@@ -565,51 +291,40 @@ Module.setup = function() {
return result;
};
Module.Manifold.extrude = function(
Module.extrude = function(
polygons, height, nDivisions = 0, twistDegrees = 0.0,
scaleTop = [1.0, 1.0], center = false) {
const cs = (polygons instanceof CrossSectionCtor) ?
polygons :
Module.CrossSection(polygons, 'Positive');
return cs.extrude(height, nDivisions, twistDegrees, scaleTop, center);
scaleTop = [1.0, 1.0]) {
if (scaleTop instanceof Array) scaleTop = {x: scaleTop[0], y: scaleTop[1]};
const polygonsVec = polygons2vec(polygons);
const result = Module._Extrude(
polygonsVec, height, nDivisions, twistDegrees, scaleTop);
disposePolygons(polygonsVec);
return result;
};
Module.Manifold.revolve = function(
polygons, circularSegments = 0, revolveDegrees = 360.0) {
const cs = (polygons instanceof CrossSectionCtor) ?
polygons :
Module.CrossSection(polygons, 'Positive');
return cs.revolve(circularSegments, revolveDegrees);
Module.triangulate = function(polygons, precision = -1) {
const polygonsVec = polygons2vec(polygons);
const result = fromVec(Module._Triangulate(polygonsVec, precision), (x) => [x[0], x[1], x[2]]);
disposePolygons(polygonsVec);
return result;
}
Module.revolve = function(polygons, circularSegments = 0) {
const polygonsVec = polygons2vec(polygons);
const result = Module._Revolve(polygonsVec, circularSegments);
disposePolygons(polygonsVec);
return result;
};
Module.Manifold.reserveIDs = function(n) {
return Module._ReserveIDs(n);
};
Module.Manifold.compose = function(manifolds) {
Module.compose = function(manifolds) {
const vec = new Module.Vector_manifold();
toVec(vec, manifolds);
const result = Module._manifoldCompose(vec);
const result = Module._Compose(vec);
vec.delete();
return result;
};
function manifoldBatchbool(name) {
return function(...args) {
if (args.length == 1) args = args[0];
const v = new Module.Vector_manifold();
for (const m of args) v.push_back(m);
const result = Module['_manifold' + name + 'N'](v);
v.delete();
return result;
};
}
Module.Manifold.union = manifoldBatchbool('Union');
Module.Manifold.difference = manifoldBatchbool('Difference');
Module.Manifold.intersection = manifoldBatchbool('Intersection');
Module.Manifold.levelSet = function(sdf, bounds, edgeLength, level = 0) {
Module.levelSet = function(sdf, bounds, edgeLength, level = 0) {
const bounds2 = {
min: {x: bounds.min[0], y: bounds.min[1], z: bounds.min[2]},
max: {x: bounds.max[0], y: bounds.max[1], z: bounds.max[2]},
@@ -626,50 +341,18 @@ Module.setup = function() {
return out;
};
function pushVec3(vec, ps) {
toVec(vec, ps, p => {
if (p instanceof Array) return {x: p[0], y: p[1], z: p[2]};
return p;
})
function batchbool(name) {
return function(...args) {
if (args.length == 1) args = args[0];
const v = new Module.Vector_manifold();
for (const m of args) v.push_back(m);
const result = Module['_' + name + 'N'](v);
v.delete();
return result;
};
}
Module.Manifold.hull = function(...args) {
if (args.length == 1) args = args[0];
let pts = new Module.Vector_vec3();
for (const m of args) {
if (m instanceof ManifoldCtor) {
Module._manifoldCollectVertices(pts, m);
} else if (
m instanceof Array && m.length == 3 && typeof m[0] == 'number') {
pts.push_back({x: m[0], y: m[1], z: m[2]});
} else if (m.x) {
pts.push_back(m);
} else {
pushVec3(pts, m);
}
}
const result = Module._manifoldHullPoints(pts);
pts.delete();
return result;
};
Module.Manifold.prototype = Object.create(ManifoldCtor.prototype);
// Because the constructor and prototype are being replaced, instanceof will
// not work as desired unless we refer back to the original like this
Object.defineProperty(Module.Manifold, Symbol.hasInstance, {
get: () => (t) => {
return (t instanceof ManifoldCtor);
}
});
// Top-level functions
Module.triangulate = function(polygons, precision = -1) {
const polygonsVec = polygons2vec(polygons);
const result = fromVec(
Module._Triangulate(polygonsVec, precision), (x) => [x[0], x[1], x[2]]);
disposePolygons(polygonsVec);
return result;
};
Module.union = batchbool('union');
Module.difference = batchbool('difference');
Module.intersection = batchbool('intersection');
};
-1
View File
@@ -1 +0,0 @@
declare module '@jscadui/3mf-export';
-45
View File
@@ -1,45 +0,0 @@
# WASM/JS Examples
This is the home of our editor, [ManifoldCAD.org](https://manifoldcad.org/), as well as our other public examples of integrations with `three.js`, `<model-viewer>`, and `glTF`. Included are `manifold-gltf.js` and `gltf-io.js`, which are intended to be fairly general-purpose libraries for interfacing between manifolds and glTF. We should probably make them into their own npm package at some point.
## Local development
First, follow the directions in the root README to get your C++ build environment set up and working for WASM. From this directory (`bindings/wasm/examples/`) you can test the JS bindings by running:
```
npm install
npm test
```
To develop the manifoldCAD.org editor as well as our other example pages, run
```
npm run dev
```
which will serve the pages, watch for changes, and automatically rebuild and refresh. This build step doesn't do TS type-checking, so to verify everything is correct (beyond VSCode's TS linting), run
```
npm run build
```
See `package.json` for other useful scripts.
Note that the `emcmake` command automatically copies your WASM build into `built/`, (here, not just under the `buildWASM` directory) which is then packaged by Vite into `dist/assets/`.
To debug the WASM build directly in Chrome dev tools, simply build in debug mode:
```
emcmake cmake -DCMAKE_BUILD_TYPE=Debug .. && emmake make
```
and install the [DWARF](goo.gle/wasm-debugging-extension) Chrome extension.
When testing [ManifoldCAD.org](https://manifoldcad.org/) (either locally or the
deployed version) note that it uses a service worker for faster loading. This
means you need to open the page twice to see updates (the first time loads the
old version and caches the new one, the second time loads the new version from
cache). To see changes on each reload, open Chrome dev tools, go to the
Application tab and check "update on reload".
### Note for firefox users
To use the manifoldCAD.org editor (`npm run dev`), you'll likely have to set
`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.
+11 -46
View File
@@ -130,14 +130,18 @@ model-viewer {
.header .margin {
font-size: 16px;
margin: 1em 0.5em;
margin: 1em;
}
#file {
width: 250px;
}
.dropdown {
display: none;
position: absolute;
z-index: 1;
width: 100%;
width: 250px;
max-height: 80vh;
overflow-y: auto;
}
@@ -146,10 +150,6 @@ model-viewer {
display: block;
}
.hide {
display: none;
}
.uparrow {
display: inline-block;
position: relative;
@@ -174,26 +174,26 @@ model-viewer {
position: absolute;
width: 36px;
top: 0px;
right: 0px;
background-size: 20px;
background-repeat: no-repeat;
background-position: center;
}
.cancel {
background-image: url(/icons/close.png);
background-image: url(icons/close.png);
background-size: 20px;
background-repeat: no-repeat;
background-position: center;
}
.edit {
background-image: url(/icons/pencil.png);
background-image: url(icons/pencil.png);
right: 30px;
}
.trash {
background-image: url(/icons/trash.png);
background-image: url(icons/trash.png);
right: 0px;
}
.icon:hover {
@@ -212,15 +212,6 @@ model-viewer {
background-color: rgb(255, 177, 177);
}
#file-margin {
position: relative;
width: 250px;
}
#save {
width: 125px;
}
@media only screen and (max-width: 600px) {
.container {
flex-direction: column;
@@ -229,31 +220,5 @@ model-viewer {
.col {
width: 100%;
height: 50%;
}
#file-margin {
width: calc(100% - 32px);
}
p {
margin: .2em 1em;
text-align: center;
}
.header .margin {
margin: 0.2em 16px;
}
#button-container {
flex-direction: row;
margin-left: 8px;
margin-right: 8px;
width: calc(100% - 16px);
}
#button-container .margin {
width: calc(50% - 16px);
margin: 8px;
}
}
}
@@ -12,58 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Manifold, Mat4, Vec2, Vec3} from './manifold';
declare class GLTFNode {
manifold?: Manifold;
translation?: Vec3|((t: number) => Vec3);
rotation?: Vec3|((t: number) => Vec3);
scale?: Vec3|((t: number) => Vec3);
material?: GLTFMaterial;
name?: string;
constructor(parent?: GLTFNode);
clone(parent?: GLTFNode): GLTFNode;
}
declare class GLTFMaterial {
attributes?: string[];
roughness?: number;
metallic?: number;
baseColorFactor?: [number, number, number];
alpha?: number;
unlit?: boolean;
name?: string;
}
/**
* Set material properties on the input manifold. They will be carried along
* through operations.
* Wrap any object with this method to display it and any copies in transparent
* red. This is particularly useful for debugging subtract() as it will allow
* you find the object even if it doesn't currently intersect the result.
*
* @param manifold The object to add properties to - returned for chaining.
* @param material A set of material properties to apply to this manifold.
* @param manifold The object to show - returned for chaining.
*/
declare function setMaterial(
manifold: Manifold, material: GLTFMaterial): Manifold;
declare function show(manifold: Manifold): Manifold;
/**
* Wrap any shape object with this method to display it and any copies in
* transparent red. This is particularly useful for debugging subtract() as it
* will allow you find the object even if it doesn't currently intersect the
* result.
*
* @param shape The object to show - returned for chaining.
*/
declare function show(shape: CrossSection|Manifold): Manifold;
/**
* Wrap any shape object with this method to display it and any copies as the
* result, while ghosting out the final result in transparent gray. Helpful for
* Wrap any object with this method to display it and any copies as the result,
* while ghosting out the final result in transparent gray. Helpful for
* debugging as it allows you to see objects that may be hidden in the interior
* of the result. Multiple objects marked only() will all be shown.
*
* @param shape The object to show - returned for chaining.
* @param manifold The object to show - returned for chaining.
*/
declare function only(shape: CrossSection|Manifold): Manifold;
declare function only(manifold: Manifold): Manifold;
// Type definitions for gl-matrix 3.4.3 Project:
// https://github.com/toji/gl-matrix
+59 -96
View File
@@ -12,45 +12,31 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import ManifoldWorker from './worker?worker';
// Loaded globally by examples.js
const exampleFunctions = self.examples.functionBodies;
import {examples} from './examples.js';
const exampleFunctions = examples.functionBodies;
if (navigator.serviceWorker) {
navigator.serviceWorker.register(
'/service-worker.js', {scope: './index.html'});
navigator.serviceWorker.register('service-worker.js');
}
let editor = undefined;
// File UI ------------------------------------------------------------
const fileButton = document.querySelector('#file');
const currentFileElement = document.querySelector('#current');
const fileArrow = document.querySelector('#file .uparrow');
const fileDropdown = document.querySelector('#fileDropdown');
const saveContainer = document.querySelector('#save');
const saveDropdown = document.querySelector('#saveDropdown');
const saveArrow = document.querySelector('#save .uparrow');
const currentElement = document.querySelector('#current');
const arrow = document.querySelector('.uparrow');
const dropdown = document.querySelector('.dropdown');
const hideDropdown = function() {
fileDropdown.classList.remove('show');
saveDropdown.classList.remove('show');
fileArrow.classList.remove('down');
saveArrow.classList.remove('down');
dropdown.classList.remove('show');
arrow.classList.remove('down');
};
const toggleFileDropdown = function(event) {
const toggleDropdown = function(event) {
event.stopPropagation();
fileDropdown.classList.toggle('show');
fileArrow.classList.toggle('down');
dropdown.classList.toggle('show');
arrow.classList.toggle('down');
};
const toggleSaveDropdown = function(event) {
event.stopPropagation();
saveDropdown.classList.toggle('show');
saveArrow.classList.toggle('down');
};
fileButton.onclick = toggleFileDropdown;
saveArrow.parentElement.onclick = toggleSaveDropdown;
fileButton.onclick = toggleDropdown;
document.body.onclick = hideDropdown;
const prefix = 'ManifoldCAD';
@@ -73,7 +59,7 @@ function nthKey(n) {
function saveCurrent() {
if (editor) {
const currentName = currentFileElement.textContent;
const currentName = currentElement.textContent;
if (!exampleFunctions.get(currentName)) {
setScript(currentName, editor.getValue());
}
@@ -88,7 +74,7 @@ let isExample = true;
function switchTo(scriptName) {
if (editor) {
switching = true;
currentFileElement.textContent = scriptName;
currentElement.textContent = scriptName;
setScript('currentName', scriptName);
const code =
exampleFunctions.get(scriptName) ?? getScript(scriptName) ?? '';
@@ -112,10 +98,6 @@ function createDropdownItem(name) {
saveCurrent();
switchTo(label.textContent);
};
// Stop text input spaces from triggering the button
button.onkeyup = function(event) {
event.preventDefault();
};
return button;
}
@@ -159,8 +141,8 @@ function addEdit(button) {
if (!input) return;
const newName = uniqueName(input);
label.textContent = newName;
if (currentFileElement.textContent == oldName) {
currentFileElement.textContent = newName;
if (currentElement.textContent == oldName) {
currentElement.textContent = newName;
}
removeScript(oldName);
setScript(newName, code);
@@ -193,7 +175,7 @@ function addEdit(button) {
}, {once: true});
} else if (performance.now() - lastClick > 500) {
removeScript(label.textContent);
if (currentFileElement.textContent == label.textContent) {
if (currentElement.textContent == label.textContent) {
switchTo('Intro');
}
const container = button.parentElement;
@@ -233,44 +215,50 @@ function initializeRun() {
let tsWorker = undefined;
async function getManifoldDTS() {
const global = await fetch('/manifold-global-types.d.ts')
const global = await fetch('manifold-global-types.d.ts')
.then(response => response.text());
const encapsulated = await fetch('/manifold-encapsulated-types.d.ts')
const encapsulated = await fetch('manifold-encapsulated-types.d.ts')
.then(response => response.text());
return `
${global.replaceAll('export', '')}
${encapsulated.replace(/^import.*$/gm, '').replaceAll('export', 'declare')}
declare interface ManifoldToplevel {
CrossSection: typeof T.CrossSection;
Manifold: typeof T.Manifold;
Mesh: typeof T.Mesh;
triangulate: typeof T.triangulate;
setMinCircularAngle: typeof T.setMinCircularAngle;
setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength;
setCircularSegments: typeof T.setCircularSegments;
getCircularSegments: typeof T.getCircularSegments;
setup: () => void;
declare interface ManifoldStatic {
cube: typeof cube;
cylinder: typeof cylinder;
sphere: typeof sphere;
smooth: typeof smooth;
tetrahedron: typeof tetrahedron;
extrude: typeof extrude;
revolve: typeof revolve;
union: typeof union;
difference: typeof difference;
intersection: typeof intersection;
compose: typeof compose;
levelSet: typeof levelSet;
setMinCircularAngle: typeof setMinCircularAngle;
setMinCircularEdgeLength: typeof setMinCircularEdgeLength;
setCircularSegments: typeof setCircularSegments;
getCircularSegments: typeof getCircularSegments;
reserveIDs: typeof reserveIDs;
}
declare const module: ManifoldToplevel;
declare const module: ManifoldStatic;
`;
}
async function getEditorDTS() {
const global = await fetch('/editor.d.ts').then(response => response.text());
return `${global.replace(/^import.*$/gm, '')}`;
}
require.config({
paths:
{vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs'}
});
require(['vs/editor/editor.main'], async function() {
async function addTypes(uri) {
const content = await fetch(uri).then(response => response.text());
monaco.languages.typescript.typescriptDefaults.addExtraLib(content);
}
monaco.languages.typescript.typescriptDefaults.addExtraLib(
await getManifoldDTS());
monaco.languages.typescript.typescriptDefaults.addExtraLib(
await getEditorDTS());
await addTypes('editor.d.ts');
editor = monaco.editor.create(
document.getElementById('editor'),
{language: 'typescript', automaticLayout: true});
@@ -280,10 +268,10 @@ require(['vs/editor/editor.main'], async function() {
for (const [name] of exampleFunctions) {
const button = createDropdownItem(name);
fileDropdown.appendChild(button.parentElement);
dropdown.appendChild(button.parentElement);
}
let currentName = currentFileElement.textContent;
let currentName = currentElement.textContent;
for (let i = 0; i < window.localStorage.length; i++) {
const key = nthKey(i);
@@ -358,14 +346,11 @@ function finishRun() {
}
const mv = document.querySelector('model-viewer');
const output = {
glbURL: null,
threeMFURL: null
};
let objectURL = null;
let manifoldWorker = null;
function createWorker() {
manifoldWorker = new ManifoldWorker();
manifoldWorker = new Worker('worker.js', {type: 'module'});
manifoldWorker.onmessage = function(e) {
if (e.data == null) {
if (tsWorker != null && !manifoldInitialized) {
@@ -383,22 +368,14 @@ function createWorker() {
finishRun();
runButton.disabled = true;
setScript('safe', 'true');
if (output.threeMFURL != null) {
URL.revokeObjectURL(output.threeMFURL);
output.threeMFURL = null;
}
URL.revokeObjectURL(output.glbURL);
output.glbURL = e.data.glbURL;
output.threeMFURL = e.data.threeMFURL;
threemfButton.disabled = output.threeMFURL == null;
mv.src = output.glbURL;
if (output.glbURL == null) {
URL.revokeObjectURL(objectURL);
objectURL = e.data.objectURL;
mv.src = objectURL;
if (objectURL == null) {
mv.showPoster();
poster.textContent = 'Error';
createWorker();
} else {
setScript('safe', 'true');
}
}
}
@@ -431,24 +408,10 @@ runButton.onclick = function() {
}
};
function clickSave(saveButton, filename, outputName) {
const container = saveButton.parentElement;
return () => {
const oldSave = saveContainer.firstElementChild;
if (oldSave !== container) {
saveDropdown.insertBefore(oldSave, saveDropdown.firstElementChild);
saveContainer.insertBefore(container, saveDropdown);
container.appendChild(saveArrow.parentElement);
}
const link = document.createElement('a');
link.download = filename;
link.href = output[outputName];
link.click();
};
}
const glbButton = document.querySelector('#glb');
glbButton.onclick = clickSave(glbButton, 'manifold.glb', 'glbURL');
const threemfButton = document.querySelector('#threemf');
threemfButton.onclick = clickSave(threemfButton, 'manifold.3mf', 'threeMFURL');
const downloadButton = document.querySelector('#download');
downloadButton.onclick = function() {
const link = document.createElement('a');
link.download = 'manifold.glb';
link.href = objectURL;
link.click();
};
@@ -16,16 +16,14 @@ export const examples = {
functions: {
Intro: function() {
// Write code in JavaScript or TypeScript and this editor will show the
// API docs. Type e.g. "box." to see the Manifold API. Type
// "CrossSection." or "Manifold." to list the 2D and 3D constructors,
// respectively. Type "module." to see the static API - these functions
// can also be used bare. Use console.log() to print output (lower-right).
// This editor defines Z as up and units of mm.
const {cube, sphere} = Manifold;
// API docs. Type e.g. "box." to see the Manifold API. Type "module." to
// see the static API - these functions can also be used bare. Use
// console.log() to print output (lower-right). This editor defines Z as
// up and units of mm.
const box = cube([100, 100, 100], true);
const ball = sphere(60, 100);
// You must name your final output "result", or create at least one
// GLTFNode - see Menger Sponge and Gyroid Module examples.
// You must name your final output "result".
const result = box.subtract(ball);
// For visual debug, wrap any shape with show() and it and all of its
@@ -40,11 +38,6 @@ export const examples = {
// See the script drop-down above ("Intro") for usage examples. The
// gl-matrix package from npm is automatically imported for convenience -
// its API is available in the top-level glMatrix object.
// Use GLTFNode for disjoint manifolds rather than compose(), as this will
// keep them better organized in the GLB. This will also allow you to
// specify material properties, and even vertex colors via
// setProperties(). See Tetrahedron Puzzle example.
return result;
},
@@ -61,46 +54,20 @@ export const examples = {
const scale = edgeLength / (2 * Math.sqrt(2));
const tet = Manifold.tetrahedron().scale(scale);
const tet = tetrahedron().scale(scale);
const box = [];
box.push([1, -1], [1, 1]);
box.push([2, -2], [2, 2]);
for (let i = 0; i <= nDivisions; ++i) {
box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]);
box.push([gap / (2 * scale), 2 - i * 4 / nDivisions]);
}
const cyan = [0, 1, 1];
const magenta = [1, 0, 1];
const fade = (color, pos) => {
for (let i = 0; i < 3; ++i) {
color[i] = cyan[i] * pos[2] + magenta[i] * (1 - pos[2]);
}
};
const screw = extrude(box, 2, nDivisions, 270)
.rotate([0, 0, -45])
.translate([0, 0, -1])
.scale(scale);
// setProperties(3, fade) creates three channels of vertex properties
// according to the above fade function. setMaterial assigns these
// channels as colors, and sets the factor to white, since our default is
// yellow.
const screw = setMaterial(
Manifold.extrude(box, 1, nDivisions, 270).setProperties(3, fade),
{baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']});
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. Our default material:
// {
// roughness = 0.2,
// metallic = 1,
// baseColorFactor = [1, 1, 0],
// alpha = 1,
// unlit = false,
// name = ''
// }
const node = new GLTFNode();
node.manifold = result;
const result = tet.intersect(screw);
return result;
},
@@ -108,7 +75,6 @@ export const examples = {
// Demonstrates how at 90-degree intersections, the sphere and cylinder
// facets match up perfectly, for any choice of global resolution
// parameters.
const {sphere, cylinder, union} = Manifold;
function roundedFrame(edgeLength, radius, circularSegments = 0) {
const edge = cylinder(edgeLength, radius, -1, circularSegments);
@@ -173,7 +139,7 @@ export const examples = {
v[2] *= r;
};
const ball = Manifold.sphere(1, 200);
const ball = sphere(1, 200);
const heart = ball.warp(func);
const box = heart.boundingBox();
const result = heart.scale(100 / (box.max[0] - box.min[0]));
@@ -221,28 +187,7 @@ export const examples = {
const triVerts = Uint32Array.from(triangles);
const vertProperties = Float32Array.from(positions);
const scallop = new Mesh({numProp: 3, triVerts, vertProperties});
const colorCurvature = (color, pos, oldProp) => {
const a = Math.max(0, Math.min(1, oldProp[0] / 3 + 0.5));
const b = a * a * (3 - 2 * a);
const red = [1, 0, 0];
const blue = [0, 0, 1];
for (let i = 0; i < 3; ++i) {
color[i] = (1 - b) * blue[i] + b * red[i];
}
};
const result = Manifold.smooth(scallop, sharpenedEdges)
.refine(n)
.calculateCurvature(-1, 0)
.setProperties(3, colorCurvature);
const node = new GLTFNode();
node.manifold = result;
node.material = {
baseColorFactor: [1, 1, 1],
metallic: 0,
attributes: ['COLOR_0']
};
const result = smooth(scallop, sharpenedEdges).refine(n);
return result;
},
@@ -282,8 +227,12 @@ export const examples = {
const m = linearSegments > 2 ? linearSegments :
n * q * majorRadius / threadRadius;
const offset = 2
const circle = CrossSection.circle(1, n).translate([offset, 0]);
const circle = [];
const dPhi = 2 * 3.14159 / n;
const offset = 2;
for (let i = 0; i < n; ++i) {
circle.push([Math.cos(dPhi * i) + offset, Math.sin(dPhi * i)]);
}
const func = (v) => {
const psi = q * Math.atan2(v[0], v[1]);
@@ -301,14 +250,14 @@ export const examples = {
vec3.rotateZ(v, v, center, psi);
};
let knot = Manifold.revolve(circle, m).warp(func);
let knot = 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);
knot = compose(knots);
}
return knot;
@@ -344,35 +293,20 @@ export const examples = {
}
function mengerSponge(n) {
let result = Manifold.cube([1, 1, 1], true);
let result = cube([1, 1, 1], true);
const holes = [];
fractal(holes, result, 1.0, [0.0, 0.0], 1, n);
const hole = Manifold.compose(holes);
const hole = compose(holes);
result = difference(result, hole);
result = difference(result, hole.rotate([90, 0, 0]));
result = difference(result, hole.rotate([0, 90, 0]));
result = Manifold.difference(
result,
hole,
hole.rotate([90, 0, 0]),
hole.rotate([0, 90, 0]),
);
return result;
}
const posColors = (newProp, pos) => {
for (let i = 0; i < 3; ++i) {
newProp[i] = (1 - pos[i]) / 2;
}
};
const result = mengerSponge(3)
.trimByPlane([1, 1, 1], 0)
.setProperties(3, posColors)
.scale(100);
const node = new GLTFNode();
node.manifold = result;
node.material = {baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']};
const result = mengerSponge(3).trimByPlane([1, 1, 1], 0).scale(100);
return result;
},
@@ -384,7 +318,7 @@ export const examples = {
function base(
width, radius, decorRadius, twistRadius, nDecor, innerRadius,
outerRadius, cut, nCut, nDivision) {
let b = Manifold.cylinder(width, radius + twistRadius / 2);
let b = cylinder(width, radius + twistRadius / 2);
const circle = [];
const dPhiDeg = 180 / nDivision;
for (let i = 0; i < 2 * nDivision; ++i) {
@@ -393,7 +327,7 @@ export const examples = {
decorRadius * Math.sin(dPhiDeg * i * Math.PI / 180)
]);
}
let decor = Manifold.extrude(circle, width, nDivision, 180)
let decor = extrude(circle, width, nDivision, 180)
.scale([1, 0.5, 1])
.translate([0, radius, 0]);
for (let i = 0; i < nDecor; i++)
@@ -411,9 +345,8 @@ export const examples = {
stretch.push(vec2.rotate([0, 0], p2, o, dPhiRad * i));
stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i));
}
const result =
Manifold.intersection(Manifold.extrude(stretch, width), b);
return result;
b = intersection(extrude(stretch, width), b);
return b;
}
function stretchyBracelet(
@@ -426,7 +359,7 @@ export const examples = {
const cut = 0.5 * (Math.PI * 2 * innerRadius / nCut - thickness);
const adjThickness = 0.5 * thickness * height / cut;
return Manifold.difference(
return difference(
base(
width, radius, decorRadius, twistRadius, nDecor,
innerRadius + thickness, outerRadius + adjThickness,
@@ -448,13 +381,8 @@ export const examples = {
// manifolds.
const {vec3} = glMatrix;
// number of modules along pyramid edge (use 1 for print orientation)
const m = 4;
// module size
const size = 20;
// SDF resolution
const n = 20;
const pi = 3.14159;
function gyroid(p) {
@@ -471,41 +399,20 @@ export const examples = {
min: vec3.fromValues(-period, -period, -period),
max: vec3.fromValues(period, period, period)
};
return Manifold.levelSet(gyroid, box, period / n, level)
.scale(size / period);
return levelSet(gyroid, box, period / n, level).scale(size / period);
};
function rhombicDodecahedron() {
const box = Manifold.cube([1, 1, 2], true).scale(size * Math.sqrt(2));
const box = cube([1, 1, 2], true).scale(size * Math.sqrt(2));
const result =
box.rotate([90, 45, 0]).intersect(box.rotate([90, 45, 90]));
return result.intersect(box.rotate([0, 0, 45]));
}
const gyroidModule = rhombicDodecahedron()
.intersect(gyroidOffset(-0.4))
.subtract(gyroidOffset(0.4));
if (m > 1) {
for (let i = 0; i < m; ++i) {
for (let j = i; j < m; ++j) {
for (let k = j; k < m; ++k) {
const node = new GLTFNode();
node.manifold = gyroidModule;
node.translation =
[(k + i - j) * size, (k - i) * size, (-j) * size];
node.material = {
baseColorFactor:
[(k + i - j + 1) / m, (k - i + 1) / m, (j + 1) / m]
};
}
}
}
}
const result = gyroidModule.rotate([-45, 0, 90]).translate([
0, 0, size / Math.sqrt(2)
]);
let result = rhombicDodecahedron().intersect(gyroidOffset(-0.4));
result = result.subtract(gyroidOffset(0.4));
result =
result.rotate([-45, 0, 90]).translate([0, 0, size / Math.sqrt(2)]);
return result;
}
},
@@ -524,8 +431,4 @@ for (const [func, code] of Object.entries(examples.functions)) {
const name =
func.replace(/([a-z])([A-Z])/g, '$1 $2'); // Add spaces between words
examples.functionBodies.set(name, body);
};
if (typeof self !== 'undefined') {
self.examples = examples;
}
};
-285
View File
@@ -1,285 +0,0 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Accessor} from '@gltf-transform/core';
import {EXTManifold} from './manifold-gltf.js';
export const attributeDefs = {
'POSITION': {type: Accessor.Type.VEC3, components: 3},
'NORMAL': {type: Accessor.Type.VEC3, components: 3},
'TANGENT': {type: Accessor.Type.VEC4, components: 4},
'TEXCOORD_0': {type: Accessor.Type.VEC2, components: 2},
'TEXCOORD_1': {type: Accessor.Type.VEC2, components: 2},
'COLOR_0': {type: Accessor.Type.VEC3, components: 3},
'JOINTS_0': {type: Accessor.Type.VEC4, components: 4},
'WEIGHTS_0': {type: Accessor.Type.VEC4, components: 4},
'SKIP_1': {type: null, components: 1},
'SKIP_2': {type: null, components: 2},
'SKIP_3': {type: null, components: 3},
'SKIP_4': {type: null, components: 4},
};
export function setupIO(io) {
return io.registerExtensions([EXTManifold]);
}
function readPrimitive(primitive, numProp, attributes) {
const position = primitive.getAttribute('POSITION');
const numVert = position.getCount();
const vertProperties = [];
let offset = 0;
for (const attribute of attributes) {
const size = attributeDefs[attribute].components;
if (attribute === 'SKIP') {
offset += size;
continue;
}
const accessor = primitive.getAttribute(attribute);
if (accessor) {
const array = accessor.getArray();
for (let i = 0; i < numVert; ++i) {
for (let j = 0; j < size; ++j) {
vertProperties[numProp * i + offset + j] = array[i * size + j];
}
}
} else {
for (let i = 0; i < numVert; ++i) {
for (let j = 0; j < size; ++j) {
vertProperties[numProp * i + offset + j] = 0;
}
}
}
offset += size;
}
return vertProperties;
}
export function readMesh(mesh, attributes, materials) {
const primitives = mesh.listPrimitives();
if (primitives.length === 0) {
return {};
}
if (attributes.length === 0) {
const attributeSet = new Set();
for (const primitive of primitives) {
const semantics = primitive.listSemantics();
for (const semantic of semantics) {
attributeSet.add(semantic);
}
}
for (const semantic in attributeDefs) {
if (attributeSet.has(semantic)) {
attributes.push(semantic);
attributeSet.delete(semantic);
}
}
for (const semantic of attributeSet.keys()) {
attributes.push(semantic);
}
}
if (attributes.length < 1 || attributes[0] !== 'POSITION')
throw new Error('First attribute must be "POSITION".');
const numProp = attributes.map((def) => attributeDefs[def].components)
.reduce((a, b) => a + b);
const manifoldPrimitive = mesh.getExtension('EXT_manifold');
let vertPropArray = [];
let triVertArray = [];
const runIndexArray = [0];
const mergeFromVert = [];
const mergeToVert = [];
if (manifoldPrimitive != null) {
vertPropArray = readPrimitive(primitives[0], numProp, attributes);
for (const primitive of primitives) {
triVertArray = [...triVertArray, ...primitive.getIndices().getArray()];
runIndexArray.push(triVertArray.length);
materials.push(primitive.getMaterial());
}
const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? [];
const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? [];
const vert2merge = new Map();
for (const [i, idx] of mergeTriVert.entries()) {
vert2merge.set(triVertArray[idx], mergeTo[i]);
}
for (const [from, to] of vert2merge.entries()) {
mergeFromVert.push(from);
mergeToVert.push(to);
}
} else {
for (const primitive of primitives) {
const numVert = vertPropArray.length / numProp;
vertPropArray =
[...vertPropArray, ...readPrimitive(primitive, numProp, attributes)];
triVertArray = [
...triVertArray,
...primitive.getIndices().getArray().map((i) => i + numVert)
];
runIndexArray.push(triVertArray.length);
materials.push(primitive.getMaterial());
}
}
const vertProperties = new Float32Array(vertPropArray);
const triVerts = new Uint32Array(triVertArray);
const runIndex = new Uint32Array(runIndexArray);
return {
numProp,
triVerts,
vertProperties,
runIndex,
mergeFromVert,
mergeToVert
};
}
export function writeMesh(doc, manifoldMesh, attributes, materials) {
if (doc.getRoot().listBuffers().length === 0) {
doc.createBuffer();
}
const buffer = doc.getRoot().listBuffers()[0];
const manifoldExtension = doc.createExtension(EXTManifold);
const attributeUnion = [];
for (const matAttributes of attributes) {
matAttributes.forEach((attribute, i) => {
if (i >= attributeUnion.length) {
attributeUnion.push(attribute);
} else {
const size = attributeDefs[attribute].components;
const unionSize = attributeDefs[attributeUnion[i]].components;
if (size != unionSize) {
throw new Error(
'Attribute sizes do not correspond: ', attribute, ' and ',
attributeUnion[i]);
}
if (attributeDefs[attributeUnion[i]].type == null) {
attributeUnion[i] = attribute;
}
}
});
}
if (attributeUnion.length < 1 || attributeUnion[0] !== 'POSITION')
throw new Error('First attribute must be "POSITION".');
const mesh = doc.createMesh();
const numPrimitive = manifoldMesh.runIndex.length - 1;
for (let run = 0; run < numPrimitive; ++run) {
const indices = doc.createAccessor('indices')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array());
const primitive = doc.createPrimitive().setIndices(indices);
const material = materials[run];
if (material) {
primitive.setMaterial(material);
}
mesh.addPrimitive(primitive);
}
const numVert = manifoldMesh.numVert;
const numProp = manifoldMesh.numProp;
let offset = 0;
attributeUnion.forEach((attribute, aIdx) => {
if (attribute === 'SKIP') {
++offset;
return;
}
const def = attributeDefs[attribute];
if (def == null)
throw new Error(attribute + ' is not a recognized attribute.');
const n = def.components;
if (offset + n > numProp) throw new Error('Too many attribute channels.');
const array = new Float32Array(n * numVert);
for (let v = 0; v < numVert; ++v) {
for (let i = 0; i < n; ++i) {
array[n * v + i] =
manifoldMesh.vertProperties[numProp * v + offset + i];
}
}
const accessor =
doc.createAccessor().setBuffer(buffer).setType(def.type).setArray(
array);
mesh.listPrimitives().forEach((primitive, pIdx) => {
if (attributes[pIdx].length > aIdx &&
attributeDefs[attributes[pIdx][aIdx]].type != null) {
primitive.setAttribute(attribute, accessor);
}
});
offset += n;
});
const manifoldPrimitive = manifoldExtension.createManifoldPrimitive();
mesh.setExtension('EXT_manifold', manifoldPrimitive);
const indices = doc.createAccessor('indices')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(manifoldMesh.triVerts);
manifoldPrimitive.setIndices(indices);
manifoldPrimitive.setRunIndex(manifoldMesh.runIndex);
const vert2merge = [...Array(manifoldMesh.numVert).keys()];
for (const [i, from] of manifoldMesh.mergeFromVert.entries()) {
vert2merge[from] = manifoldMesh.mergeToVert[i];
}
const ind = [];
const val = [];
for (const [i, vert] of manifoldMesh.triVerts.entries()) {
const newVert = vert2merge[vert];
if (vert !== newVert) {
ind.push(i);
val.push(newVert);
}
}
const indicesAccessor = doc.createAccessor()
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(ind));
const valuesAccessor = doc.createAccessor()
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(val));
manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor);
return mesh;
}
export function disposeMesh(mesh) {
if (!mesh) return;
const primitives = mesh.listPrimitives();
for (const primitive of primitives) {
primitive.getIndices()?.dispose();
for (const accessor of primitive.listAttributes()) {
accessor.dispose();
}
}
mesh.dispose();
}
export async function loadTexture(texture, uri) {
const response = await fetch(uri);
const blob = await response.blob();
texture.setMimeType(blob.type);
texture.setImage(new Uint8Array(await blob.arrayBuffer()));
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 539 B

Before

Width:  |  Height:  |  Size: 704 KiB

After

Width:  |  Height:  |  Size: 704 KiB

Before

Width:  |  Height:  |  Size: 952 KiB

After

Width:  |  Height:  |  Size: 952 KiB

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+7 -24
View File
@@ -29,38 +29,21 @@
}
}
</script>
<script>
self.ModelViewerElement = self.ModelViewerElement || {};
self.ModelViewerElement.modelCacheSize = 0;
</script>
<script type="module" src="/examples.js"></script>
</head>
<body>
<div class="page">
<div class="container header" style="background-color: #A288B3;">
<p style="flex: 2;"><a target="_blank" href="https://github.com/elalish/manifold">Manifold</a>CAD </p>
<div class="margin" id="file-margin" >
<button type="button" id="file" class="blue item"><b class="uparrow"></b>&nbsp;&nbsp;<span
<div class="margin" style="position: relative;">
<button type="button" id="file" class="blue"><b class="uparrow"></b>&nbsp;&nbsp;<span
id="current">Intro</span></button>
<div class="dropdown" id="fileDropdown">
<div class="dropdown">
<button type="button" class="green item" id="new">New</button>
</div>
</div>
<div class="container" id="button-container">
<button type="button" id="compile" class="margin green" disabled><span>Run</span></button>
<div id="save" class="margin" style="position: relative; ">
<div class="item">
<button type="button" id="glb" class="blue item">Save GLB</button>
<button type="button" class="icon"><b class="uparrow" style="right: 2px;"></b></button>
</div>
<div class="dropdown" id="saveDropdown">
<div class="item">
<button type="button" id="threemf" class="blue item">Save 3MF</button>
</div>
</div>
</div>
</div>
<button type="button" id="compile" class="margin green" disabled><span>Run</span></button>
<button type="button" id="download" class="blue margin">Save GLB</button>
</div>
<div class="container" style="flex: 2;">
<div id="editor" class="col"></div>
@@ -84,6 +67,6 @@
integrity="sha512-iQEIc0rsSDujsfjtD+lfyJ1W23Bh/lbgriubKDAym6VlEIDRj9rrbSIyJRyshOrl8s0yRcQ0+gyrZfSLyjJGWQ=="
crossorigin="anonymous" referrerpolicy="no-referrer">
<script type="module" src="editor.js"></script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.1.1/model-viewer.min.js"></script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/2.0.1/model-viewer.min.js"></script>
</html>
</html>
-189
View File
@@ -1,189 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Make manifold glTF</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/png" href="https://elalish.github.io/manifold/samples/models/favicon.png" />
<style>
body {
background-color: #f7f7f7;
font-family: 'Rubik', sans-serif;
font-size: 16px;
line-height: 24px;
color: rgba(0, 0, 0, .87);
font-weight: 400;
-webkit-font-smoothing: antialiased;
}
p {
max-width: 700px;
margin: 1em;
text-align: left;
}
model-viewer {
display: block;
width: 100vw;
height: 100vw;
max-width: 600px;
max-height: 600px;
border: 3px solid black;
border-radius: 10px;
margin-top: 5px;
}
#poster {
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
}
</style>
</head>
<body>
<p>Load a glTF/GLB model and our <a href="https://github.com/elalish/manifold">Manifold</a> library will attempt to
merge it into a set of manifold objects. If the model is water-tight, you can download the new GLB which will have
our <a href="https://github.com/KhronosGroup/glTF/pull/2286">EXT_manifold</a> extension, thus preserving the
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
download button is still disabled, it means there were no manifold meshes found. </p>
<input type="file" id="input">
<button id="download" disabled>Download manifold GLB</button>
<input type="checkbox" id="viewFinal" disabled>
<label for="viewFinal">View Manifold GLB</label>
<model-viewer camera-controls shadow-intensity="1" alt="Loaded GLB model">
<span id="poster" slot="poster">Drop a GLB here</span>
</model-viewer>
</body>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/2.0.1/model-viewer.min.js"></script>
<script type="module">
import { WebIO } from '@gltf-transform/core';
import { prune } from '@gltf-transform/functions';
import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions';
import { SimpleDropzone } from 'simple-dropzone';
import { readMesh, writeMesh, setupIO } from './gltf-io.js';
import Module from './built/manifold.js';
const io = setupIO(new WebIO());
io.registerExtensions(KHRONOS_EXTENSIONS);
const wasm = await Module();
wasm.setup();
const { Manifold, Mesh } = wasm;
const mv = document.querySelector('model-viewer');
const inputEl = document.querySelector('#input');
const downloadButton = document.querySelector('#download');
const checkbox = document.querySelector('#viewFinal');
const dropCtrl = new SimpleDropzone(mv, inputEl);
let inputGLBurl = null;
let outputGLBurl = null;
let allManifold = true;
let anyManifold = false;
dropCtrl.on('drop', async ({ files }) => {
for (const [path, file] of files) {
const filename = file.name.toLowerCase();
if (filename.match(/\.(gltf|glb)$/)) {
URL.revokeObjectURL(inputGLBurl);
inputGLBurl = URL.createObjectURL(file);
await writeGLB(await readGLB(inputGLBurl));
updateUI();
break;
}
}
});
function updateUI() {
if (allManifold) {
checkbox.checked = true;
checkbox.disabled = true;
} else if (anyManifold) {
checkbox.checked = false;
checkbox.disabled = false;
} else {
checkbox.checked = false;
checkbox.disabled = true;
}
onClick();
}
checkbox.onclick = onClick;
function onClick() {
mv.src = checkbox.checked ? outputGLBurl : inputGLBurl;
downloadButton.disabled = !checkbox.checked;
};
downloadButton.onclick = function () {
const link = document.createElement('a');
link.download = 'manifold.glb';
link.href = outputGLBurl;
link.click();
};
async function writeGLB(doc) {
URL.revokeObjectURL(outputGLBurl);
if (!anyManifold) {
return;
}
const glb = await io.writeBinary(doc);
const blob = new Blob([glb], { type: 'application/octet-stream' });
outputGLBurl = URL.createObjectURL(blob);
}
async function readGLB(url) {
allManifold = false;
anyManifold = false;
updateUI();
allManifold = true;
const docIn = await io.read(url);
const nodes = docIn.getRoot().listNodes();
for (const node of nodes) {
const attributes = [];
const mesh = node.getMesh();
if (!mesh) {
continue;
}
const materials = [];
const tmpMesh = readMesh(mesh, attributes, materials);
tmpMesh.runOriginalID = [];
const firstID = Manifold.reserveIDs(materials.length);
for (let i = 0; i < materials.length; ++i) {
tmpMesh.runOriginalID.push(firstID + i);
}
const manifoldMesh = new Mesh(tmpMesh);
mesh.dispose();
const matAttributes = [];
materials.forEach(() => matAttributes.push(attributes));
manifoldMesh.merge();
try {
// Test manifoldness - will throw if not.
const manifold = Manifold(manifoldMesh);
node.setMesh(writeMesh(docIn, manifold.getMesh(), matAttributes, materials));
manifold.delete();
anyManifold = true;
} catch (e) {
console.log(mesh.getName(), e);
allManifold = false;
}
}
await docIn.transform(prune());
return docIn;
}
</script>
</html>
-180
View File
@@ -1,180 +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 {Extension, ExtensionProperty, PropertyType} from '@gltf-transform/core';
const NAME = 'EXT_manifold';
export class ManifoldPrimitive extends ExtensionProperty {
static EXTENSION_NAME = NAME;
init() {
this.EXTENSION_NAME = NAME;
this.propertyType = 'ManifoldPrimitive';
this.parentTypes = [PropertyType.MESH];
}
getDefaults() {
return Object.assign(
super.getDefaults(),
{manifoldPrimitive: null, mergeIndices: null, mergeValues: null});
}
getMergeIndices() {
return this.getRef('mergeIndices');
}
getMergeValues() {
return this.getRef('mergeValues');
}
setMerge(indicesAccessor, valuesAccessor) {
if (indicesAccessor.getCount() !== valuesAccessor.getCount())
throw new Error('merge vectors must be the same length.');
this.setRef('mergeIndices', indicesAccessor);
return this.setRef('mergeValues', valuesAccessor);
}
getRunIndex() {
return this.get('runIndex');
}
setRunIndex(runIndex) {
return this.set('runIndex', runIndex);
}
setIndices(indices) {
return this.setRef('indices', indices);
}
getIndices() {
return this.getRef('indices');
}
}
export class EXTManifold extends Extension {
extensionName = NAME;
static EXTENSION_NAME = NAME;
createManifoldPrimitive() {
return new ManifoldPrimitive(this.document.getGraph());
}
read(context) {
const {json} = context.jsonDoc;
const meshDefs = json.meshes || [];
meshDefs.forEach((meshDef, meshIndex) => {
if (!meshDef.extensions || !meshDef.extensions[NAME]) return;
const mesh = context.meshes[meshIndex];
const manifoldPrimitive = this.createManifoldPrimitive();
mesh.setExtension(NAME, manifoldPrimitive);
const manifoldDef = meshDef.extensions[NAME];
if (manifoldDef.manifoldPrimitive) {
let count = 0;
const runIndex = [];
runIndex.push(count);
for (const primitive of mesh.listPrimitives()) {
count += primitive.getIndices().getCount();
runIndex.push(count);
}
manifoldPrimitive.setRunIndex(runIndex);
}
if (manifoldDef.mergeIndices && manifoldDef.mergeValues) {
manifoldPrimitive.setMerge(
context.accessors[manifoldDef.mergeIndices],
context.accessors[manifoldDef.mergeValues]);
}
});
return this;
}
write(context) {
const {json} = context.jsonDoc;
this.document.getRoot().listMeshes().forEach((mesh) => {
const manifoldPrimitive = mesh.getExtension(NAME);
if (!manifoldPrimitive) return;
const meshIndex = context.meshIndexMap.get(mesh);
const meshDef = json.meshes[meshIndex];
const runIndex = manifoldPrimitive.getRunIndex();
const numPrimitive = runIndex.length - 1;
if (numPrimitive !== meshDef.primitives.length) {
throw new Error(
'The number of primitives must be exactly one less than the length of runIndex.');
}
const mergeIndicesIndex =
context.accessorIndexMap.get(manifoldPrimitive.getMergeIndices());
const 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()),
mode: existingPrimitive.mode,
attributes: {'POSITION': existingPrimitive.attributes['POSITION']}
};
const indices = json.accessors[primitive.indices];
if (!indices) {
return;
}
if (mergeIndices && mergeValues) {
indices.sparse = {
count: mergeIndices.count,
indices: {
bufferView: mergeIndices.bufferView,
byteOffset: mergeIndices.byteOffset,
componentType: mergeIndices.componentType
},
values: {
bufferView: mergeValues.bufferView,
byteOffset: mergeValues.byteOffset,
}
};
}
for (let i = 0; i < numPrimitive; ++i) {
const accessor = json.accessors[meshDef.primitives[i].indices];
accessor.bufferView = indices.bufferView;
accessor.byteOffset = indices.byteOffset + 4 * runIndex[i];
accessor.count = runIndex[i + 1] - runIndex[i];
}
meshDef.extensions = meshDef.extensions || {};
meshDef.extensions[NAME] = {
manifoldPrimitive: primitive,
mergeIndices: mergeIndicesIndex,
mergeValues: mergeValuesIndex
};
// Test the manifold primitive by replacing the material primitives
// meshDef.primitives = [primitive];
});
return this;
}
}
+183 -67
View File
@@ -45,66 +45,60 @@
<option value="intersection">Intersection</option>
<option value="union">Union</option>
</select>
<button id="download">Download manifold GLB</button>
<model-viewer camera-controls shadow-intensity="1" alt="Output of mesh Boolean operation"></model-viewer>
</body>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/2.0.1/model-viewer.min.js"></script>
<script type="module">
import { Document, WebIO } from '@gltf-transform/core';
import { clearNodeTransform, flatten, prune } from '@gltf-transform/functions';
import { loadTexture, readMesh, writeMesh, disposeMesh, setupIO } from './gltf-io.js';
import Module from './built/manifold.js';
import { Accessor, Document, WebIO } from 'https://cdn.skypack.dev/pin/@gltf-transform/core@v3.0.0-SfbIFhNPTRdr1UE2VSan/mode=imports,min/optimized/@gltf-transform/core.js';
import Module from '../manifold.js';
const io = setupIO(new WebIO());
const io = new WebIO();
const doc = new Document();
const buffer = doc.createBuffer();
const position = doc.createAccessor().setBuffer(buffer).setType(Accessor.Type.VEC3);
const uv = doc.createAccessor().setBuffer(buffer).setType(Accessor.Type.VEC2);
const moonIndices = doc.createAccessor().setBuffer(buffer).setType(Accessor.Type.SCALAR);
const spaceIndices = doc.createAccessor().setBuffer(buffer).setType(Accessor.Type.SCALAR);
const moonTexture = doc.createTexture('moon');
const spaceTexture = doc.createTexture('space');
const moonMaterial = doc.createMaterial('moon').setBaseColorTexture(moonTexture);
const spaceMaterial = doc.createMaterial('moon').setBaseColorTexture(spaceTexture);
const moonPrimitive = doc.createPrimitive().setMaterial(moonMaterial)
.setIndices(moonIndices).setAttribute('POSITION', position).setAttribute('TEXCOORD_0', uv);
const spacePrimitive = doc.createPrimitive().setMaterial(spaceMaterial)
.setIndices(spaceIndices).setAttribute('POSITION', position).setAttribute('TEXCOORD_0', uv);
const mesh = doc.createMesh('result').addPrimitive(moonPrimitive).addPrimitive(spacePrimitive);
const node = doc.createNode().setMesh(mesh);
const scene = doc.createScene().addChild(node);
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()));
}
async function loadTextures() {
const moonLoads = loadTexture(moonTexture, './moonAltitude.jpg');
const spaceLoads = loadTexture(spaceTexture, './hubbleDeepField.jpg');
await Promise.all([moonLoads, spaceLoads]);
}
const texturesLoad = loadTextures();
const materialMap = new Map();
const wasm = await Module();
wasm.setup();
const { Manifold, Mesh } = wasm;
const firstID = wasm.reserveIDs(2);
const space = cubeUV(firstID).scale(50);
const moon = sphereUV(firstID + 1).scale(60);
materialMap.set(firstID, spaceIndices);
materialMap.set(firstID + 1, moonIndices);
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));
const csg = function (operation) {
push2MV(wasm[operation](space, moon));
};
document.querySelector('select').onchange = function (event) {
@@ -116,28 +110,30 @@
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();
const mesh = 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);
for (const [i, id] of mesh.runOriginalID.entries()) {
const indices = materialMap.get(id);
indices.setArray(mesh.triVerts.subarray(mesh.runIndex[i], mesh.runIndex[i + 1]));
}
const mesh = writeMesh(doc, manifoldMesh, matAttributes, materials);
node.setMesh(mesh);
await doc.transform(prune());
const numVert = mesh.numVert;
const numProp = mesh.numProp;
const posArray = new Float32Array(3 * numVert);
const uvArray = new Float32Array(2 * numVert);
for (let i = 0; i < numVert; ++i) {
posArray[3 * i] = mesh.vertProperties[numProp * i];
posArray[3 * i + 1] = mesh.vertProperties[numProp * i + 1];
posArray[3 * i + 2] = mesh.vertProperties[numProp * i + 2];
uvArray[2 * i] = mesh.vertProperties[numProp * i + 3];
uvArray[2 * i + 1] = mesh.vertProperties[numProp * i + 4];
}
position.setArray(posArray);
uv.setArray(uvArray);
await texturesLoad;
const glb = await io.writeBinary(doc);
@@ -146,6 +142,126 @@
objectURL = URL.createObjectURL(blob);
mv.src = objectURL;
}
function cubeUV(id) {
const numProp = 5;
const numTri = 12;
const numVert = 8;
const numPropVert = 24;
const vertPos =
[-1, -1, -1,
1, -1, -1,
1, 1, -1,
-1, 1, -1,
-1, -1, 1,
1, -1, 1,
1, 1, 1,
-1, 1, 1];
const uvs = [0, 0, 0, 1, 1, 1, 1, 0];
const triVerts = new Uint32Array(
[0, 3, 2, 0, 2, 1,
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]);
const v4 = [0, 1, 2, 5];
const v6 = [0, 1, 2, 0, 2, 3];
const vertProperties = new Float32Array(numProp * numPropVert);
const mergeFromVert = new Uint32Array(numPropVert - numVert);
const mergeToVert = new Uint32Array(numPropVert - numVert);
const runOriginalID = new Uint32Array(1);
const meshUV = new wasm.Mesh({ numProp, triVerts, vertProperties, mergeFromVert, mergeToVert, runOriginalID });
runOriginalID[0] = id;
const vert2vert = [-1, -1, -1, -1, -1, -1, -1, -1];
let idx = 0;
for (let face = 0; face < 6; ++face) {
for (let v = 0; v < 4; ++v) {
const vIdx = 4 * face + v;
const vOld = triVerts[6 * face + v4[v]];
if (vert2vert[vOld] === -1) {
vert2vert[vOld] = vIdx;
} else {
mergeFromVert[idx] = vIdx;
mergeToVert[idx] = vert2vert[vOld];
++idx;
}
for (let i = 0; i < 3; ++i) {
vertProperties[numProp * vIdx + i] = vertPos[3 * vOld + i];
}
vertProperties[numProp * vIdx + 3] = uvs[2 * v];
vertProperties[numProp * vIdx + 4] = uvs[2 * v + 1];
}
for (let i = 0; i < 6; ++i) {
triVerts[6 * face + i] = 4 * face + v6[i];
}
}
return wasm.Manifold(meshUV);
}
function sphereUV(id) {
const n = 100;
const tol = 1 / (2 * n);
const mesh = wasm.sphere(1, n).getMesh();
const nMerge = n / 2 - 1;
const numVert = mesh.numVert;
const numTri = mesh.numTri;
const numPropIn = mesh.numProp;
const numProp = 5;
const triVerts = new Uint32Array(3 * numTri);
const vertProperties = new Float32Array(numProp * (numVert + nMerge));
const mergeFromVert = new Uint32Array(nMerge);
const mergeToVert = new Uint32Array(nMerge);
const runOriginalID = new Uint32Array(1);
const meshUV = new wasm.Mesh({ numProp, triVerts, vertProperties, mergeFromVert, mergeToVert, runOriginalID });
runOriginalID[0] = id;
const vert2vert = new Uint32Array(numVert);
let idx = 0;
for (let i = 0; i < numVert; ++i) {
const x = mesh.vertProperties[numPropIn * i];
const y = mesh.vertProperties[numPropIn * i + 1];
const z = mesh.vertProperties[numPropIn * i + 2];
vertProperties[numProp * i] = x;
vertProperties[numProp * i + 1] = y;
vertProperties[numProp * i + 2] = z;
const u = Math.atan2(y, x) / (2 * Math.PI) + 0.5;
const v = Math.acos(z) / Math.PI;
vertProperties[numProp * i + 4] = v;
if (Math.abs(y) < tol && x < -tol) {// Seam
const iNew = numVert + idx;
mergeFromVert[idx] = iNew;
mergeToVert[idx] = i;
vert2vert[i] = iNew;
vertProperties[numProp * iNew] = x;
vertProperties[numProp * iNew + 1] = y;
vertProperties[numProp * iNew + 2] = z;
vertProperties[numProp * iNew + 3] = 1;
vertProperties[numProp * iNew + 4] = v;
vertProperties[numProp * i + 3] = 0;
++idx;
} else {
vert2vert[i] = i;
vertProperties[numProp * i + 3] = u;
}
}
for (let tri = 0; tri < numTri; ++tri) {
const v0 = mesh.triVerts[3 * tri];
const v1 = mesh.triVerts[3 * tri + 1];
const v2 = mesh.triVerts[3 * tri + 2];
if ((vert2vert[v0] !== v0 || vert2vert[v1] !== v1 || vert2vert[v2] !== v2) &&
(vertProperties[numProp * v0 + 1] > tol || vertProperties[numProp * v1 + 1] > tol ||
vertProperties[numProp * v2 + 1] > tol)) {
triVerts[3 * tri] = vert2vert[v0];
triVerts[3 * tri + 1] = vert2vert[v1];
triVerts[3 * tri + 2] = vert2vert[v2];
} else {
triVerts[3 * tri] = v0;
triVerts[3 * tri + 1] = v1;
triVerts[3 * tri + 2] = v2;
}
}
return wasm.Manifold(meshUV);
}
</script>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
{
"name": "manifold-examples",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"build:watch": "vite build --watch",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run"
},
"dependencies": {
"@gltf-transform/core": "^3.2.1",
"@gltf-transform/extensions": "^3.2.1",
"@gltf-transform/functions": "^3.2.1",
"@jscadui/3mf-export": "^0.4.2",
"fflate": "^0.8.0",
"gl-matrix": "^3.4.3",
"simple-dropzone": "0.8.3",
"three": "0.151.2"
},
"devDependencies": {
"@vitest/ui": "^0.31.1",
"@vitest/web-worker": "^0.31.1",
"typescript": "5.0.4",
"vite": "^4.3.2",
"vitest": "^0.31.1"
}
}
Binary file not shown.
+14 -9
View File
@@ -44,16 +44,21 @@
</select><br />
<canvas id="output"></canvas>
</body>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import Module from './built/manifold.js';
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.module.js';
import { mergeVertices } from 'https://cdn.jsdelivr.net/npm/three@0.149.0/examples/jsm/utils/BufferGeometryUtils.js';
import Module from '../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;
@@ -66,12 +71,12 @@
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 manifold_1 = wasm.cube(0.2, 0.2, 0.2, true);
const manifold_2 = new wasm.Manifold(geometry2mesh(icosahedron));
const csg = function (operation) {
mesh.geometry?.dispose();
mesh.geometry = mesh2geometry(Manifold[operation](manifold_1, manifold_2).getMesh());
mesh.geometry = mesh2geometry(wasm[operation](manifold_1, manifold_2).getMesh());
};
document.querySelector('select').onchange = function (event) {
@@ -94,7 +99,7 @@
function geometry2mesh(geometry) {
const vertProperties = geometry.attributes.position.array;
const triVerts = geometry.index.array;
return new Mesh({ numProp: 3, vertProperties, triVerts });
return new wasm.Mesh({ numProp: 3, vertProperties, triVerts });
}
function mesh2geometry(mesh) {
-24
View File
@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "node",
"lib": [
"es2022",
"dom"
],
"sourceMap": true,
"inlineSources": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"skipLibCheck": true,
},
"include": [
"./*.ts"
],
"exclude": []
}
-26
View File
@@ -1,26 +0,0 @@
// vite.config.js
import {resolve} from 'path'
import {defineConfig} from 'vite'
export default defineConfig({
test: {testTimeout: 15000},
worker: {
format: 'es',
},
build: {
target: 'esnext',
rollupOptions: {
input: {
manifoldCAD: resolve(__dirname, 'index.html'),
makeManifold: resolve(__dirname, 'make-manifold.html'),
modelViewer: resolve(__dirname, 'model-viewer.html'),
three: resolve(__dirname, 'three.html'),
},
output: {
entryFileNames: `assets/[name].js`,
chunkFileNames: `assets/[name].js`,
assetFileNames: `assets/[name].[ext]`
}
},
},
})
+241
View File
@@ -0,0 +1,241 @@
// 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
//
// 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 * as glMatrix from 'https://cdn.jsdelivr.net/npm/gl-matrix@3.4.3/+esm';
import {Accessor, Document, Material, WebIO} from 'https://cdn.skypack.dev/pin/@gltf-transform/core@v3.0.0-SfbIFhNPTRdr1UE2VSan/mode=imports,min/optimized/@gltf-transform/core.js';
import Module from '../manifold.js';
const module = await Module();
module.setup();
// Faster on modern browsers than Float32Array
glMatrix.glMatrix.setMatrixArrayType(Array);
// Scene setup
const io = new WebIO();
const doc = new Document();
const buffer = doc.createBuffer();
const position =
doc.createAccessor().setBuffer(buffer).setType(Accessor.Type.VEC3);
const indices =
doc.createAccessor().setBuffer(buffer).setType(Accessor.Type.SCALAR);
const resultMaterial = doc.createMaterial()
.setBaseColorFactor([1, 1, 0, 1])
.setMetallicFactor(1)
.setRoughnessFactor(0.2);
const resultPrimitive = doc.createPrimitive()
.setMaterial(resultMaterial)
.setIndices(indices)
.setAttribute('POSITION', position);
const mesh = doc.createMesh().addPrimitive(resultPrimitive);
const node = doc.createNode('result').setMesh(mesh);
const scene = doc.createScene().addChild(node);
// Debug setup to show source meshes
const shown = new Map();
let ghost = false;
const debugMaterial = doc.createMaterial()
.setBaseColorFactor([1, 0, 0, 0.25])
.setAlphaMode(Material.AlphaMode.BLEND)
.setDoubleSided(true)
.setMetallicFactor(0);
const ghostMaterial = doc.createMaterial()
.setBaseColorFactor([0.5, 0.5, 0.5, 0.25])
.setAlphaMode(Material.AlphaMode.BLEND)
.setDoubleSided(true)
.setMetallicFactor(0);
function debug(manifold, material) {
const position =
doc.createAccessor().setBuffer(buffer).setType(Accessor.Type.VEC3);
const indices =
doc.createAccessor().setBuffer(buffer).setType(Accessor.Type.SCALAR);
const primitive = doc.createPrimitive()
.setMaterial(material)
.setIndices(indices)
.setAttribute('POSITION', position);
const mesh = doc.createMesh('debug').addPrimitive(primitive);
const result = manifold.asOriginal();
outputMesh(indices, position, result.getMesh());
shown.set(result.originalID(), mesh);
return result;
};
module.show = (manifold) => {
return debug(manifold, debugMaterial);
};
module.only = (manifold) => {
ghost = true;
return debug(manifold, resultMaterial);
};
function debugCleanup() {
for (const [id, mesh] of shown) {
const primitive = mesh.listPrimitives()[0];
primitive.getAttribute('POSITION').dispose();
primitive.getIndices().dispose();
primitive.dispose();
mesh.dispose();
}
scene.traverse((node) => {
if (node.getName() == 'debug') {
node.dispose();
}
});
shown.clear();
ghost = false;
}
// manifold member functions that returns a new manifold
const memberFunctions = [
'add', 'subtract', 'intersect', 'trimByPlane', 'refine', 'transform',
'translate', 'rotate', 'scale', 'mirror', 'asOriginal', 'decompose'
];
// top level functions that constructs a new manifold
const constructors = [
'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'union',
'difference', 'intersection', 'compose', 'levelSet', 'smooth', 'show', 'only'
];
const utils = [
'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments',
'getCircularSegments', 'Mesh'
];
const exposedFunctions = constructors.concat(utils);
// Setup memory management, such that users don't have to care about
// calling `delete` manually.
// Note that this only fixes memory leak across different runs: the memory
// will only be freed when the compilation finishes.
const manifoldRegistry = [];
for (const name of memberFunctions) {
const originalFn = module.Manifold.prototype[name];
module.Manifold.prototype['_' + name] = originalFn;
module.Manifold.prototype[name] = function(...args) {
const result = this['_' + name](...args);
manifoldRegistry.push(result);
return result;
};
}
for (const name of constructors) {
const originalFn = module[name];
module[name] = function(...args) {
const result = originalFn(...args);
manifoldRegistry.push(result);
return result;
};
}
module.cleanup = function() {
for (const obj of manifoldRegistry) {
// decompose result is an array of manifolds
if (obj instanceof Array)
for (const elem of obj) elem.delete();
else
obj.delete();
}
manifoldRegistry.length = 0;
};
// Setup complete
postMessage(null);
const oldLog = console.log;
console.log = function(...args) {
let message = '';
for (const arg of args) {
if (typeof arg == 'object') {
message += JSON.stringify(arg, null, 4);
} else {
message += arg.toString();
}
}
postMessage({log: message});
oldLog(...args);
};
onmessage = async (e) => {
const content = e.data + '\nreturn exportGLB(result);\n';
try {
const f = new Function(
'exportGLB', 'glMatrix', 'module', ...exposedFunctions, content);
await f(
exportGLB, glMatrix, module,
...exposedFunctions.map(name => module[name]));
} catch (error) {
console.log(error.toString());
postMessage({objectURL: null});
} finally {
module.cleanup();
debugCleanup();
}
};
function outputMesh(indices, position, mesh) {
indices.setArray(mesh.triVerts);
const numVert = mesh.numVert;
const numProp = mesh.numProp;
const posArray = new Float32Array(3 * numVert);
for (let i = 0; i < numVert; ++i) {
posArray[3 * i] = mesh.vertProperties[numProp * i];
posArray[3 * i + 1] = mesh.vertProperties[numProp * i + 1];
posArray[3 * i + 2] = mesh.vertProperties[numProp * i + 2];
}
position.setArray(posArray);
}
async function exportGLB(manifold) {
console.log(`Triangles: ${manifold.numTri().toLocaleString()}`);
const box = manifold.boundingBox();
const size = [0, 0, 0];
for (let i = 0; i < 3; i++) {
size[i] = Math.round((box.max[i] - box.min[i]) * 10) / 10;
}
console.log(`Bounding Box: X = ${size[0].toLocaleString()} mm, Y = ${
size[1].toLocaleString()} mm, Z = ${size[2].toLocaleString()} mm`);
const volume = Math.round(manifold.getProperties().volume / 10);
console.log(`Genus: ${manifold.genus().toLocaleString()}, Volume: ${
(volume / 100).toLocaleString()} cm^3`);
// From Z-up to Y-up (glTF)
const mesh = manifold.rotate([-90, 0, 0]).getMesh();
outputMesh(indices, position, mesh);
for (const [run, id] of mesh.runOriginalID.entries()) {
const outMesh = shown.get(id);
if (outMesh == null) {
continue;
}
const node =
doc.createNode('debug').setMesh(outMesh).setMatrix(mesh.transform(run));
scene.addChild(node);
}
resultPrimitive.setMaterial(ghost ? ghostMaterial : resultMaterial);
const glb = await io.writeBinary(doc);
const blob = new Blob([glb], {type: 'application/octet-stream'});
postMessage({objectURL: URL.createObjectURL(blob)});
}
-152
View File
@@ -1,152 +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
//
// 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 '@vitest/web-worker';
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 ManifoldWorker from './worker?worker';
const io = setupIO(new WebIO());
const wasm = await Module();
wasm.setup();
function initialized(worker) {
return new Promise((resolve) => {
worker.onmessage = function(e) {
if (e.data == null) {
resolve();
} else {
reject();
}
};
});
}
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 () => {
const result = await runExample('Intro');
expect(result.genus).to.equal(5, 'Genus');
expect(result.volume).to.be.closeTo(203164, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(62046, 1, 'Surface Area');
});
test('Tetrahedron Puzzle', async () => {
const result = await runExample('Tetrahedron Puzzle');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(7297, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(3303, 1, 'Surface Area');
});
test('Rounded Frame', async () => {
const result = await runExample('Rounded Frame');
expect(result.genus).to.equal(5, 'Genus');
expect(result.volume).to.be.closeTo(353706, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(68454, 1, 'Surface Area');
});
test('Heart', async () => {
const result = await runExample('Heart');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(282743, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(22187, 1, 'Surface Area');
});
test('Scallop', async () => {
const result = await runExample('Scallop');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(41284, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(7810, 1, 'Surface Area');
});
test('Torus Knot', async () => {
const result = await runExample('Torus Knot');
expect(result.genus).to.equal(1, 'Genus');
expect(result.volume).to.be.closeTo(20786, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(11177, 1, 'Surface Area');
});
test('Menger Sponge', async () => {
const result = await runExample('Menger Sponge');
expect(result.genus).to.equal(729, 'Genus');
expect(result.volume).to.be.closeTo(203222, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(130475, 10, 'Surface Area');
});
test('Stretchy Bracelet', async () => {
const result = await runExample('Stretchy Bracelet');
expect(result.genus).to.equal(1, 'Genus');
expect(result.volume).to.be.closeTo(3992, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(22267, 1, 'Surface Area');
});
test('Gyroid Module', async () => {
const result = await runExample('Gyroid Module');
expect(result.genus).to.equal(15, 'Genus');
expect(result.volume).to.be.closeTo(4167, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(5642, 1, 'Surface Area');
});
});
-566
View File
@@ -1,566 +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
//
// 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, mat4, Material, Node, WebIO} from '@gltf-transform/core';
import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
import {fileForContentTypes, to3dmodel} from '@jscadui/3mf-export';
import {strToU8, Zippable, zipSync} from 'fflate'
import * as glMatrix from 'gl-matrix';
import Module from './built/manifold';
//@ts-ignore
import {setupIO, writeMesh} from './gltf-io';
import type {GLTFMaterial, Quat} from './public/editor';
import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './public/manifold';
interface WorkerStatic extends ManifoldToplevel {
GLTFNode: typeof GLTFNode;
show(manifold: Manifold): Manifold;
only(manifold: Manifold): Manifold;
setMaterial(manifold: Manifold, material: GLTFMaterial): void;
cleanup(): void;
}
const module = await Module() as unknown as WorkerStatic;
module.setup();
// Faster on modern browsers than Float32Array
glMatrix.glMatrix.setMatrixArrayType(Array);
const io = setupIO(new WebIO());
io.registerExtensions(KHRONOS_EXTENSIONS);
// manifold static methods (that return a new manifold)
const manifoldStaticFunctions = [
'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'compose',
'union', 'difference', 'intersection', 'levelSet', 'smooth', 'ofMesh', 'hull'
];
// manifold member functions (that return a new manifold)
const manifoldMemberFunctions = [
'add', 'subtract', 'intersect', 'decompose', 'warp', 'transform', 'translate',
'rotate', 'scale', 'mirror', 'refine', 'setProperties', 'asOriginal',
'trimByPlane', 'split', 'splitByPlane', 'hull'
];
// CrossSection static methods (that return a new cross-section)
const crossSectionStaticFunctions = [
'square', 'circle', 'union', 'difference', 'intersection', 'compose',
'ofPolygons', 'hull'
];
// CrossSection member functions (that return a new cross-section)
const crossSectionMemberFunctions = [
'add', 'subtract', 'intersect', 'rectClip', 'decompose', 'transform',
'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset', 'hull'
];
// top level functions that construct a new manifold/mesh
const toplevelConstructors = ['show', 'only', 'setMaterial'];
const toplevel = [
'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments',
'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection'
];
const exposedFunctions = toplevelConstructors.concat(toplevel);
// Setup memory management, such that users don't have to care about
// calling `delete` manually.
// Note that this only fixes memory leak across different runs: the memory
// will only be freed when the compilation finishes.
const memoryRegistry = new Array<Manifold|CrossSection>();
function addMembers(
className: string, methodNames: Array<string>, areStatic: boolean) {
//@ts-ignore
const cls = module[className];
const obj = areStatic ? cls : cls.prototype;
for (const name of methodNames) {
if (name != 'cylinder') {
const originalFn = obj[name];
obj[name] = function(...args: any) {
//@ts-ignore
const result = originalFn(...args);
memoryRegistry.push(result);
return result;
};
}
}
}
addMembers('Manifold', manifoldMemberFunctions, false);
addMembers('Manifold', manifoldStaticFunctions, true);
addMembers('CrossSection', crossSectionMemberFunctions, false);
addMembers('CrossSection', crossSectionStaticFunctions, true);
for (const name of toplevelConstructors) {
//@ts-ignore
const originalFn = module[name];
//@ts-ignore
module[name] = function(...args: any) {
const result = originalFn(...args);
memoryRegistry.push(result);
return result;
};
}
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 SHOW = {
baseColorFactor: [1, 0, 0],
alpha: 0.25,
roughness: 1,
metallic: 0
} as GLTFMaterial;
const GHOST = {
baseColorFactor: [0.5, 0.5, 0.5],
alpha: 0.25,
roughness: 1,
metallic: 0
} as GLTFMaterial;
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>();
function cleanup() {
ghost = false;
shown.clear();
singles.clear();
nodes.length = 0;
id2material.clear();
materialCache.clear();
object2globalID.clear();
nextGlobalID = 0;
}
interface Mesh3MF {
id: string;
vertices: Float32Array;
indices: Uint32Array;
name?: string;
}
interface Child3MF {
objectID: string;
transform?: mat4;
}
interface Component3MF {
id: string;
children: Array<Child3MF>;
name?: string;
}
interface Header {
unit?: 'micron'|'millimeter'|'centimeter'|'inch'|'foot'|'meter';
title?: string;
author?: string;
description?: string;
application?: string;
creationDate?: string;
license?: string;
modificationDate?: string;
}
interface To3MF {
meshes: Array<Mesh3MF>;
components: Array<Component3MF>;
items: Array<Child3MF>;
precision: number;
header: Header;
}
class GLTFNode {
private _parent?: GLTFNode;
manifold?: Manifold;
translation?: Vec3;
rotation?: Vec3;
scale?: Vec3;
material?: GLTFMaterial;
name?: string;
constructor(parent?: GLTFNode) {
this._parent = parent;
nodes.push(this);
}
clone(parent?: GLTFNode) {
const copy = {...this};
copy._parent = parent;
nodes.push(copy);
return copy;
}
get parent() {
return this._parent;
}
}
module.GLTFNode = GLTFNode;
module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => {
const out = manifold.asOriginal();
id2material.set(out.originalID(), material);
return out;
};
function debug(manifold: Manifold, map: Map<number, Mesh>) {
let result = manifold.asOriginal();
map.set(result.originalID(), result.getMesh());
return result;
};
module.show = (manifold) => {
return debug(manifold, shown);
};
module.only = (manifold) => {
ghost = true;
return debug(manifold, singles);
};
// Setup complete
self.postMessage(null);
if (self.console) {
const oldLog = self.console.log;
self.console.log = function(...args) {
let message = '';
for (const arg of args) {
if (arg == null) {
message += 'undefined';
} else if (typeof arg == 'object') {
message += JSON.stringify(arg, null, 4);
} else {
message += arg.toString();
}
}
self.postMessage({log: message});
oldLog(...args);
};
}
// Swallow informational logs in testing framework
function log(...args: any[]) {
if (self.console) {
self.console.log(...args);
}
}
self.onmessage = async (e) => {
const content = e.data +
'\nreturn exportModels(typeof result === "undefined" ? undefined : result);\n';
try {
const f = new Function(
'exportModels', 'glMatrix', 'module', ...exposedFunctions, content);
await f(
exportModels, glMatrix, module, //@ts-ignore
...exposedFunctions.map(name => module[name]));
} catch (error: any) {
console.log(error.toString());
self.postMessage({objectURL: null});
} finally {
module.cleanup();
cleanup();
}
};
function createGLTFnode(doc: Document, node: GLTFNode) {
const out = doc.createNode(node.name);
if (node.translation) {
out.setTranslation(node.translation);
}
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);
}
if (node.scale) {
out.setScale(node.scale);
}
return out;
}
function getBackupMaterial(node?: GLTFNode): GLTFMaterial {
if (node == null) {
return {};
}
if (node.material == null) {
node.material = getBackupMaterial(node.parent);
}
return node.material;
}
function makeDefaultedMaterial(doc: Document, {
roughness = 0.2,
metallic = 1,
baseColorFactor = [1, 1, 0],
alpha = 1,
unlit = false,
name = ''
}: GLTFMaterial = {}): Material {
const material = doc.createMaterial(name);
if (unlit) {
const unlit = doc.createExtension(KHRMaterialsUnlit).createUnlit();
material.setExtension('KHR_materials_unlit', unlit);
}
if (alpha < 1) {
material.setAlphaMode(Material.AlphaMode.BLEND).setDoubleSided(true);
}
return material.setRoughnessFactor(roughness)
.setMetallicFactor(metallic)
.setBaseColorFactor([...baseColorFactor, alpha]);
}
function getCachedMaterial(doc: Document, matDef: GLTFMaterial): Material {
if (!materialCache.has(matDef)) {
materialCache.set(matDef, makeDefaultedMaterial(doc, matDef));
}
return materialCache.get(matDef)!;
}
function addMesh(
doc: Document, to3mf: To3MF, node: Node, manifold: Manifold,
backupMaterial: GLTFMaterial = {}) {
const numTri = manifold.numTri();
if (numTri == 0) {
log('Empty manifold, skipping.');
return;
}
log(`Triangles: ${numTri.toLocaleString()}`);
const box = manifold.boundingBox();
const size = [0, 0, 0];
for (let i = 0; i < 3; i++) {
size[i] = Math.round((box.max[i] - box.min[i]) * 10) / 10;
}
log(`Bounding Box: X = ${size[0].toLocaleString()} mm, Y = ${
size[1].toLocaleString()} mm, Z = ${size[2].toLocaleString()} mm`);
const volume = Math.round(manifold.getProperties().volume / 10);
log(`Genus: ${manifold.genus().toLocaleString()}, Volume: ${
(volume / 100).toLocaleString()} cm^3`);
// From Z-up to Y-up (glTF)
const manifoldMesh = manifold.getMesh();
const materials = new Array<GLTFMaterial>();
const attributes = new Array<Array<string>>();
for (const id of manifoldMesh.runOriginalID!) {
const material = id2material.get(id) || backupMaterial;
materials.push(material);
attributes.push(['POSITION', ...material.attributes ?? []]);
}
const gltfMaterials = materials.map((matDef) => {
return getCachedMaterial(doc, ghost ? GHOST : matDef);
});
node.setMesh(writeMesh(doc, manifoldMesh, attributes, gltfMaterials));
const vertices = manifoldMesh.numProp === 3 ?
manifoldMesh.vertProperties :
new Float32Array(manifoldMesh.numVert * 3);
if (manifoldMesh.numProp > 3) {
for (let i = 0; i < manifoldMesh.numVert; ++i) {
for (let j = 0; j < 3; ++j)
vertices[i * 3 + j] =
manifoldMesh.vertProperties[i * manifoldMesh.numProp + j];
}
}
object2globalID.set(manifold, nextGlobalID);
to3mf.meshes.push(
{vertices, indices: manifoldMesh.triVerts, id: `${nextGlobalID++}`});
for (const [run, id] of manifoldMesh.runOriginalID!.entries()) {
let inMesh = shown.get(id);
let single = false;
if (inMesh == null) {
single = true;
inMesh = singles.get(id);
}
if (inMesh == null) {
continue;
}
const mat = single ? materials[run] : SHOW;
const debugNode =
doc.createNode('debug')
.setMesh(writeMesh(
doc, inMesh, attributes, [getCachedMaterial(doc, mat)]))
.setMatrix(manifoldMesh.transform(run));
node.addChild(debugNode);
}
}
function cloneNode(toNode: Node, fromNode: Node) {
toNode.setMesh(fromNode.getMesh());
fromNode.listChildren().forEach((child) => {
const clone = child.clone();
toNode.addChild(clone);
});
}
function cloneNodeNewMaterial(
doc: Document, toNode: Node, fromNode: Node, backupMaterial: Material,
oldBackupMaterial: Material) {
cloneNode(toNode, fromNode);
const mesh = doc.createMesh();
toNode.setMesh(mesh);
fromNode.getMesh()!.listPrimitives().forEach((primitive) => {
const newPrimitive = primitive.clone();
if (primitive.getMaterial() === oldBackupMaterial) {
newPrimitive.setMaterial(backupMaterial);
}
mesh.addPrimitive(newPrimitive);
});
}
function createNodeFromCache(
doc: Document, to3MF: To3MF, nodeDef: GLTFNode,
manifold2node: Map<Manifold, Map<GLTFMaterial, Node>>): Node {
const node = createGLTFnode(doc, nodeDef);
const {manifold} = nodeDef;
if (manifold != null) {
const backupMaterial = getBackupMaterial(nodeDef);
const cachedNodes = manifold2node.get(manifold);
if (cachedNodes == null) {
addMesh(doc, to3MF, node, manifold, backupMaterial);
const cache = new Map<GLTFMaterial, Node>();
cache.set(backupMaterial, node);
manifold2node.set(manifold, cache);
} else {
const cachedNode = cachedNodes.get(backupMaterial);
if (cachedNode == null) {
const [oldBackupMaterial, oldNode] = cachedNodes.entries().next().value;
cloneNodeNewMaterial(
doc, node, oldNode, getCachedMaterial(doc, backupMaterial),
getCachedMaterial(doc, oldBackupMaterial));
cachedNodes.set(backupMaterial, node);
} else {
cloneNode(node, cachedNode);
}
}
}
object2globalID.set(nodeDef, nextGlobalID);
to3MF.components.push({
id: `${nextGlobalID++}`,
name: nodeDef.name,
children:
manifold == null ? [] : [{objectID: `${object2globalID.get(manifold)}`}]
});
return node;
}
async function exportModels(manifold?: Manifold) {
const doc = new Document();
const halfRoot2 = Math.sqrt(2) / 2;
const mm2m = 1 / 1000;
const wrapper = doc.createNode('wrapper')
.setRotation([-halfRoot2, 0, 0, halfRoot2])
.setScale([mm2m, mm2m, mm2m]);
doc.createScene().addChild(wrapper);
const to3mf = {
meshes: [],
components: [],
items: [],
precision: 7,
header: {
unit: 'millimeter',
title: 'ManifoldCAD.org model',
description: 'ManifoldCAD.org model',
application: 'ManifoldCAD.org',
}
} as To3MF;
if (nodes.length > 0) {
const node2gltf = new Map<GLTFNode, Node>();
const manifold2node = new Map<Manifold, Map<GLTFMaterial, Node>>();
let leafNodes = 0;
for (const nodeDef of nodes) {
node2gltf.set(
nodeDef, createNodeFromCache(doc, to3mf, nodeDef, manifold2node));
if (nodeDef.manifold) {
++leafNodes;
}
}
for (const nodeDef of nodes) {
const gltfNode = node2gltf.get(nodeDef)!;
const child = {
objectID: `${object2globalID.get(nodeDef)}`,
transform: gltfNode.getMatrix()
};
const {parent} = nodeDef;
if (parent == null) {
wrapper.addChild(gltfNode);
to3mf.items.push(child);
} else {
node2gltf.get(parent)!.addChild(gltfNode);
const parent3mf = to3mf.components.find(
(comp) => comp.id == `${object2globalID.get(parent)}`)!;
parent3mf.children.push(child);
}
}
log('Total glTF nodes: ', nodes.length,
', Total mesh references: ', leafNodes);
} else {
if (manifold == null) {
log('No output because "result" is undefined and no "GLTFNode"s were created.');
return;
}
const node = doc.createNode();
addMesh(doc, to3mf, node, manifold);
wrapper.addChild(node);
to3mf.items.push({objectID: `${object2globalID.get(manifold)}`});
}
const glb = await io.writeBinary(doc);
const blobGLB = new Blob([glb], {type: 'application/octet-stream'});
const model = to3dmodel(to3mf);
const files: Zippable = {};
files['3D/3dmodel.model'] = strToU8(model);
files[fileForContentTypes.name] = strToU8(fileForContentTypes.content);
const zipFile = zipSync(files);
const blob3MF = new Blob(
[zipFile],
{type: 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml'});
self.postMessage({
glbURL: URL.createObjectURL(blobGLB),
threeMFURL: URL.createObjectURL(blob3MF)
});
}
-232
View File
@@ -1,232 +0,0 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <vector>
#include "cross_section.h"
#include "manifold.h"
#include "polygon.h"
#include "sdf.h"
using namespace emscripten;
using namespace manifold;
namespace js {
val MeshGL2JS(const MeshGL& mesh) {
val meshJS = val::object();
meshJS.set("numProp", mesh.numProp);
meshJS.set("triVerts",
val(typed_memory_view(mesh.triVerts.size(), mesh.triVerts.data()))
.call<val>("slice"));
meshJS.set("vertProperties",
val(typed_memory_view(mesh.vertProperties.size(),
mesh.vertProperties.data()))
.call<val>("slice"));
meshJS.set("mergeFromVert", val(typed_memory_view(mesh.mergeFromVert.size(),
mesh.mergeFromVert.data()))
.call<val>("slice"));
meshJS.set("mergeToVert", val(typed_memory_view(mesh.mergeToVert.size(),
mesh.mergeToVert.data()))
.call<val>("slice"));
meshJS.set("runIndex",
val(typed_memory_view(mesh.runIndex.size(), mesh.runIndex.data()))
.call<val>("slice"));
meshJS.set("runOriginalID", val(typed_memory_view(mesh.runOriginalID.size(),
mesh.runOriginalID.data()))
.call<val>("slice"));
meshJS.set("faceID",
val(typed_memory_view(mesh.faceID.size(), mesh.faceID.data()))
.call<val>("slice"));
meshJS.set("halfedgeTangent",
val(typed_memory_view(mesh.halfedgeTangent.size(),
mesh.halfedgeTangent.data()))
.call<val>("slice"));
meshJS.set("runTransform", val(typed_memory_view(mesh.runTransform.size(),
mesh.runTransform.data()))
.call<val>("slice"));
return meshJS;
}
MeshGL MeshJS2GL(const val& mesh) {
MeshGL out;
out.numProp = mesh["numProp"].as<int>();
out.triVerts = convertJSArrayToNumberVector<uint32_t>(mesh["triVerts"]);
out.vertProperties =
convertJSArrayToNumberVector<float>(mesh["vertProperties"]);
if (mesh["mergeFromVert"] != val::undefined()) {
out.mergeFromVert =
convertJSArrayToNumberVector<uint32_t>(mesh["mergeFromVert"]);
}
if (mesh["mergeToVert"] != val::undefined()) {
out.mergeToVert =
convertJSArrayToNumberVector<uint32_t>(mesh["mergeToVert"]);
}
if (mesh["runIndex"] != val::undefined()) {
out.runIndex = convertJSArrayToNumberVector<uint32_t>(mesh["runIndex"]);
}
if (mesh["runOriginalID"] != val::undefined()) {
out.runOriginalID =
convertJSArrayToNumberVector<uint32_t>(mesh["runOriginalID"]);
}
if (mesh["faceID"] != val::undefined()) {
out.faceID = convertJSArrayToNumberVector<uint32_t>(mesh["faceID"]);
}
if (mesh["halfedgeTangent"] != val::undefined()) {
out.halfedgeTangent =
convertJSArrayToNumberVector<float>(mesh["halfedgeTangent"]);
}
if (mesh["runTransform"] != val::undefined()) {
out.runTransform =
convertJSArrayToNumberVector<float>(mesh["runTransform"]);
}
return out;
}
val GetMeshJS(const Manifold& manifold, const glm::ivec3& normalIdx) {
MeshGL mesh = manifold.GetMeshGL(normalIdx);
return MeshGL2JS(mesh);
}
val Merge(const val& mesh) {
val out = val::object();
MeshGL meshGL = MeshJS2GL(mesh);
bool changed = meshGL.Merge();
out.set("changed", changed);
out.set("mesh", changed ? MeshGL2JS(meshGL) : mesh);
return out;
}
Manifold Smooth(const val& mesh,
const std::vector<Smoothness>& sharpenedEdges = {}) {
return Manifold::Smooth(MeshJS2GL(mesh), sharpenedEdges);
}
} // namespace js
namespace cross_js {
CrossSection OfPolygons(std::vector<std::vector<glm::vec2>> polygons,
int fill_rule) {
auto fr = fill_rule == 0 ? CrossSection::FillRule::EvenOdd
: fill_rule == 1 ? CrossSection::FillRule::NonZero
: fill_rule == 2 ? CrossSection::FillRule::Positive
: CrossSection::FillRule::Negative;
return CrossSection(polygons, fr);
}
CrossSection Union(const CrossSection& a, const CrossSection& b) {
return a + b;
}
CrossSection Difference(const CrossSection& a, const CrossSection& b) {
return a - b;
}
CrossSection Intersection(const CrossSection& a, const CrossSection& b) {
return a ^ b;
}
CrossSection UnionN(const std::vector<CrossSection>& cross_sections) {
return CrossSection::BatchBoolean(cross_sections, OpType::Add);
}
CrossSection DifferenceN(const std::vector<CrossSection>& cross_sections) {
return CrossSection::BatchBoolean(cross_sections, OpType::Subtract);
}
CrossSection IntersectionN(const std::vector<CrossSection>& cross_sections) {
return CrossSection::BatchBoolean(cross_sections, OpType::Intersect);
}
CrossSection Transform(CrossSection& cross_section, const val& mat) {
std::vector<float> array = convertJSArrayToNumberVector<float>(mat);
glm::mat3x2 matrix;
for (const int col : {0, 1, 2})
for (const int row : {0, 1}) matrix[col][row] = array[col * 3 + row];
return cross_section.Transform(matrix);
}
CrossSection Warp(CrossSection& cross_section, uintptr_t funcPtr) {
void (*f)(glm::vec2&) = reinterpret_cast<void (*)(glm::vec2&)>(funcPtr);
return cross_section.Warp(f);
}
CrossSection Offset(CrossSection& cross_section, double delta, int join_type,
double miter_limit, double arc_tolerance) {
auto jt = join_type == 0 ? CrossSection::JoinType::Square
: join_type == 1 ? CrossSection::JoinType::Round
: CrossSection::JoinType::Miter;
return cross_section.Offset(delta, jt, miter_limit, arc_tolerance);
}
void CollectVertices(std::vector<glm::vec2>& verts, const CrossSection& cs) {
auto polys = cs.ToPolygons();
for (auto poly : polys) verts.insert(verts.end(), poly.begin(), poly.end());
}
} // namespace cross_js
namespace man_js {
Manifold FromMeshJS(const val& mesh) { return Manifold(js::MeshJS2GL(mesh)); }
Manifold Union(const Manifold& a, const Manifold& b) { return a + b; }
Manifold Difference(const Manifold& a, const Manifold& b) { return a - b; }
Manifold Intersection(const Manifold& a, const Manifold& b) { return a ^ b; }
Manifold UnionN(const std::vector<Manifold>& manifolds) {
return Manifold::BatchBoolean(manifolds, OpType::Add);
}
Manifold DifferenceN(const std::vector<Manifold>& manifolds) {
return Manifold::BatchBoolean(manifolds, OpType::Subtract);
}
Manifold IntersectionN(const std::vector<Manifold>& manifolds) {
return Manifold::BatchBoolean(manifolds, OpType::Intersect);
}
Manifold Transform(Manifold& manifold, const val& mat) {
std::vector<float> array = convertJSArrayToNumberVector<float>(mat);
glm::mat4x3 matrix;
for (const int col : {0, 1, 2, 3})
for (const int row : {0, 1, 2}) matrix[col][row] = array[col * 4 + row];
return manifold.Transform(matrix);
}
Manifold Warp(Manifold& manifold, uintptr_t funcPtr) {
void (*f)(glm::vec3&) = reinterpret_cast<void (*)(glm::vec3&)>(funcPtr);
return manifold.Warp(f);
}
Manifold SetProperties(Manifold& manifold, int numProp, uintptr_t funcPtr) {
void (*f)(float*, glm::vec3, const float*) =
reinterpret_cast<void (*)(float*, glm::vec3, const float*)>(funcPtr);
return manifold.SetProperties(numProp, f);
}
Manifold LevelSet(uintptr_t funcPtr, Box bounds, float edgeLength,
float level) {
float (*f)(const glm::vec3&) =
reinterpret_cast<float (*)(const glm::vec3&)>(funcPtr);
Mesh m = LevelSet(f, bounds, edgeLength, level);
return Manifold(m);
}
std::vector<Manifold> Split(Manifold& a, Manifold& b) {
auto [r1, r2] = a.Split(b);
return {r1, r2};
}
std::vector<Manifold> SplitByPlane(Manifold& m, glm::vec3 normal,
float originOffset) {
auto [a, b] = m.SplitByPlane(normal, originOffset);
return {a, b};
}
void CollectVertices(std::vector<glm::vec3>& verts, const Manifold& manifold) {
Mesh mesh = manifold.GetMesh();
verts.insert(verts.end(), mesh.vertPos.begin(), mesh.vertPos.end());
}
} // namespace man_js
+184 -614
View File
@@ -12,7 +12,101 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Box, FillRule, JoinType, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types';
import {Box, Curvature, Mat4, Polygons, Properties, SealedFloat32Array, SealedUint32Array, Smoothness, Vec2, Vec3} from './manifold-global-types';
/**
* Constructs a unit cube (edge lengths all one), by default in the first
* octant, touching the origin.
*
* @param size The X, Y, and Z dimensions of the box.
* @param center Set to true to shift the center to the origin.
*/
export function cube(size?: Vec3|number, center?: boolean): Manifold;
/**
* A convenience constructor for the common case of extruding a circle. Can also
* form cones if both radii are specified.
*
* @param height Z-extent
* @param radiusLow Radius of bottom circle. Must be positive.
* @param radiusHigh Radius of top circle. Can equal zero. Default is equal to
* radiusLow.
* @param circularSegments How many line segments to use around the circle.
* Default is calculated by the static Defaults.
* @param center Set to true to shift the center to the origin. Default is
* origin at the bottom.
*/
export function cylinder(
height: number, radiusLow: number, radiusHigh?: number,
circularSegments?: number, center?: boolean): Manifold;
/**
* Constructs a geodesic sphere of a given radius.
*
* @param radius Radius of the sphere. Must be positive.
* @param circularSegments Number of segments along its
* diameter. This number will always be rounded up to the nearest factor of
* four, as this sphere is constructed by refining an octahedron. This means
* there are a circle of vertices on all three of the axis planes. Default is
* calculated by the static Defaults.
*/
export function sphere(radius: number, circularSegments?: number): Manifold;
/**
* Constructs a smooth version of the input mesh by creating tangents; this
* method will throw if you have supplied tangents with your mesh already. The
* actual triangle resolution is unchanged; use the Refine() method to
* interpolate to a higher-resolution curve.
*
* By default, every edge is calculated for maximum smoothness (very much
* approximately), attempting to minimize the maximum mean Curvature magnitude.
* No higher-order derivatives are considered, as the interpolation is
* independent per triangle, only sharing constraints on their boundaries.
*
* @param mesh input Mesh.
* @param sharpenedEdges If desired, you can supply a vector of sharpened
* halfedges, which should in general be a small subset of all halfedges. Order
* of entries doesn't matter, as each one specifies the desired smoothness
* (between zero and one, with one the default for all unspecified halfedges)
* and the halfedge index (3 * triangle index + [0,1,2] where 0 is the edge
* between triVert 0 and 1, etc).
*
* At a smoothness value of zero, a sharp crease is made. The smoothness is
* interpolated along each edge, so the specified value should be thought of as
* an average. Where exactly two sharpened edges meet at a vertex, their
* tangents are rotated to be colinear so that the sharpened edge can be
* continuous. Vertices with only one sharpened edge are completely smooth,
* allowing sharpened edges to smoothly vanish at termination. A single vertex
* can be sharpened by sharping all edges that are incident on it, allowing
* cones to be formed.
*/
export function smooth(mesh: Mesh, sharpenedEdges?: Smoothness[]): Manifold;
/**
* Constructs a tetrahedron centered at the origin with one vertex at (1,1,1)
* and the rest at similarly symmetric points.
*/
export function tetrahedron(): Manifold;
/**
* Constructs a manifold from a set of polygons by extruding them along the
* Z-axis.
*
* @param crossSection A set of non-overlapping polygons to extrude.
* @param height Z-extent of extrusion.
* @param nDivisions Number of extra copies of the crossSection to insert into
* the shape vertically; especially useful in combination with twistDegrees to
* avoid interpolation artifacts. Default is none.
* @param twistDegrees Amount to twist the top crossSection relative to the
* 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.
* Default {1, 1}.
*/
export function extrude(
crossSection: Polygons, height: number, nDivisions?: number,
twistDegrees?: number, scaleTop?: Vec2): Manifold;
/**
* Triangulates a set of /epsilon-valid polygons.
@@ -24,6 +118,58 @@ import {Box, FillRule, JoinType, Mat4, Polygons, Properties, Rect, SealedFloat32
*/
export function triangulate(polygons: Polygons, precision?: number): Vec3[];
/**
* Constructs a manifold from a set of polygons by revolving this cross-section
* around its Y-axis and then setting this as the Z-axis of the resulting
* manifold. If the polygons cross the Y-axis, only the part on the positive X
* side is used. Geometrically valid input will result in geometrically valid
* output.
*
* @param crossSection A set of non-overlapping polygons to revolve.
* @param circularSegments Number of segments along its diameter. Default is
* calculated by the static Defaults.
*/
export function revolve(
crossSection: Polygons, circularSegments?: number): Manifold;
export function union(a: Manifold, b: Manifold): Manifold;
export function difference(a: Manifold, b: Manifold): Manifold;
export function intersection(a: Manifold, b: Manifold): Manifold;
export function union(manifolds: Manifold[]): Manifold;
export function difference(manifolds: Manifold[]): Manifold;
export function intersection(manifolds: Manifold[]): Manifold;
/**
* Constructs a new manifold from a list of other manifolds. This is a purely
* topological operation, so care should be taken to avoid creating
* overlapping results. It is the inverse operation of Decompose().
*
* @param manifolds A list of Manifolds to lazy-union together.
*/
export function compose(manifolds: Manifold[]): Manifold;
/**
* Constructs a level-set Mesh from the input Signed-Distance Function (SDF).
* This uses a form of Marching Tetrahedra (akin to Marching Cubes, but better
* for manifoldness). Instead of using a cubic grid, it uses a body-centered
* cubic grid (two shifted cubic grids). This means if your function's interior
* exceeds the given bounds, you will see a kind of egg-crate shape closing off
* the manifold, which is due to the underlying grid.
*
* @param sdf The signed-distance function which returns the signed distance of
* a given point in R^3. Positive values are inside, negative outside.
* @param bounds An axis-aligned box that defines the extent of the grid.
* @param edgeLength Approximate maximum edge length of the triangles in the
* final result. This affects grid spacing, and hence has a strong effect on
* performance.
* @param level You can inset your Mesh by using a positive value, or outset
* it with a negative value.
*/
export function levelSet(
sdf: (point: Vec3) => number, bounds: Box, edgeLength: number,
level?: number): Manifold;
/**
* @name Defaults
* These static properties control how circular shapes are quantized by
@@ -40,292 +186,12 @@ export function setCircularSegments(segments: number): void;
export function getCircularSegments(radius: number): number;
///@}
export class CrossSection {
/**
* Create a 2d cross-section from a set of contours (complex polygons). A
* boolean union operation (with Positive filling rule by default) is
* performed to combine overlapping polygons and ensure the resulting
* CrossSection is free of intersections.
*
* @param contours A set of closed paths describing zero or more complex
* polygons.
* @param fillRule The filling rule used to interpret polygon sub-regions in
* contours.
*/
constructor(polygons: Polygons, fillRule?: FillRule);
// Shapes
static square(size?: Vec2|number, center?: boolean): CrossSection;
static circle(radius: number, circularSegments?: number): CrossSection;
// Extrusions (2d to 3d manifold)
/**
* Constructs a manifold by extruding the cross-section along Z-axis.
*
* @param height Z-extent of extrusion.
* @param nDivisions Number of extra copies of the crossSection to insert into
* the shape vertically; especially useful in combination with twistDegrees to
* avoid interpolation artifacts. Default is none.
* @param twistDegrees Amount to twist the top crossSection relative to the
* 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. Default {1, 1}.
* @param center If true, the extrusion is centered on the z-axis through the
* origin
* as opposed to resting on the XY plane as is default.
*/
extrude(
height: number, nDivisions?: number, twistDegrees?: number,
scaleTop?: Vec2|number, center?: boolean): Manifold;
/**
* Constructs a manifold by revolving this cross-section around its Y-axis and
* then setting this as the Z-axis of the resulting manifold. If the contours
* cross the Y-axis, only the part on the positive X side is used.
* Geometrically valid input will result in geometrically valid output.
*
* @param circularSegments Number of segments along its diameter. Default is
* calculated by the static Defaults.
*/
revolve(circularSegments?: number, revolveDegrees?: number): Manifold;
// Transformations
/**
* Transform this CrossSection in space. Stored in column-major order. This
* operation can be chained. Transforms are combined and applied lazily.
*
* @param m The affine transformation matrix to apply to all the vertices. The
* last row is ignored.
*/
transform(m: Mat3): CrossSection;
/**
* Move this CrossSection in space. This operation can be chained. Transforms
* are combined and applied lazily.
*
* @param v The vector to add to every vertex.
*/
translate(v: Vec2): CrossSection;
translate(x: number, y?: number): CrossSection;
/**
* Applies a (Z-axis) rotation to the CrossSection, in degrees. This operation
* can be chained. Transforms are combined and applied lazily.
*
* @param degrees degrees about the Z-axis to rotate.
*/
rotate(v: number): CrossSection;
/**
* Scale this CrossSection in space. This operation can be chained. Transforms
* are combined and applied lazily.
*
* @param v The vector to multiply every vertex by per component.
*/
scale(v: Vec2|number): CrossSection;
/**
* Mirror this CrossSection over the arbitrary axis described by the unit form
* of the given vector. If the length of the vector is zero, an empty
* CrossSection is returned. This operation can be chained. Transforms are
* combined and applied lazily.
*
* @param ax the axis to be mirrored over
*/
mirror(v: Vec2): CrossSection;
/**
* Move the vertices of this CrossSection (creating a new one) according to
* any arbitrary input function, followed by a union operation (with a
* Positive fill rule) that ensures any introduced intersections are not
* included in the result.
*
* @param warpFunc A function that modifies a given vertex position.
*/
warp(warpFunc: (vert: Vec2) => void): CrossSection;
/**
* Inflate the contours in CrossSection by the specified delta, handling
* corners according to the given JoinType.
*
* @param delta Positive deltas will cause the expansion of outlining contours
* to expand, and retraction of inner (hole) contours. Negative deltas will
* have the opposite effect.
* @param joinType The join type specifying the treatment of contour joins
* (corners).
* @param miterLimit The maximum distance in multiples of delta that vertices
* can be offset from their original positions with before squaring is
* applied, **when the join type is Miter** (default is 2, which is the
* minimum allowed). See the [Clipper2
* MiterLimit](http://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm)
* page for a visual example.
* @param circularSegments Number of segments per 360 degrees of
* <B>JoinType::Round</B> corners (roughly, the number of vertices that
* will be added to each contour). Default is calculated by the static Quality
* defaults according to the radius.
*/
offset(
delta: number, joinType?: JoinType, miterLimit?: number,
circularSegments?: number): CrossSection;
/**
* Remove vertices from the contours in this CrossSection that are less than
* the specified distance epsilon from an imaginary line that passes through
* its two adjacent vertices. Near duplicate vertices and collinear points
* will be removed at lower epsilons, with elimination of line segments
* becoming increasingly aggressive with larger epsilons.
*
* It is recommended to apply this function following Offset, in order to
* clean up any spurious tiny line segments introduced that do not improve
* quality in any meaningful way. This is particularly important if further
* offseting operations are to be performed, which would compound the issue.
*
* @param epsilon minimum distance vertices must diverge from the hypothetical
* outline without them in order to be included in the output (default
* 1e-6)
*/
simplify(epsilon?: number): CrossSection;
// Clipping Operations
/**
* Boolean union
*/
add(other: CrossSection|Polygons): CrossSection;
/**
* Boolean difference
*/
subtract(other: CrossSection|Polygons): CrossSection;
/**
* Boolean intersection
*/
intersect(other: CrossSection|Polygons): CrossSection;
/**
* Boolean union of the cross-sections a and b
*/
static union(a: CrossSection|Polygons, b: CrossSection|Polygons):
CrossSection;
/**
* Boolean difference of the cross-section b from the cross-section a
*/
static difference(a: CrossSection|Polygons, b: CrossSection|Polygons):
CrossSection;
/**
* Boolean intersection of the cross-sections a and b
*/
static intersection(a: CrossSection|Polygons, b: CrossSection|Polygons):
CrossSection;
/**
* Boolean union of a list of cross-sections
*/
static union(polygons: (CrossSection|Polygons)[]): CrossSection;
/**
* Boolean difference of the tail of a list of cross-sections from its head
*/
static difference(polygons: (CrossSection|Polygons)[]): CrossSection;
/**
* Boolean intersection of a list of cross-sections
*/
static intersection(polygons: (CrossSection|Polygons)[]): CrossSection;
// Convex Hulls
/**
* Compute the convex hull of the contours in this CrossSection.
*/
hull(): CrossSection;
/**
* Compute the convex hull of all points in a list of polygons/cross-sections.
*/
static hull(polygons: (CrossSection|Polygons)[]): CrossSection;
// Topological Operations
/**
* Construct a CrossSection from a vector of other Polygons (batch
* boolean union).
*/
static compose(polygons: (CrossSection|Polygons)[]): CrossSection;
/**
* This operation returns a vector of CrossSections that are topologically
* disconnected, each containing one outline contour with zero or more
* holes.
*/
decompose(): CrossSection[];
// Polygon Conversion
/**
* Create a 2d cross-section from a set of contours (complex polygons). A
* boolean union operation (with Positive filling rule by default) is
* performed to combine overlapping polygons and ensure the resulting
* CrossSection is free of intersections.
*
* @param contours A set of closed paths describing zero or more complex
* polygons.
* @param fillRule The filling rule used to interpret polygon sub-regions in
* contours.
*/
static ofPolygons(polygons: Polygons, fillRule?: FillRule): CrossSection;
/**
* Return the contours of this CrossSection as a list of simple polygons.
*/
toPolygons(): SimplePolygon[];
// Properties
/**
* Return the total area covered by complex polygons making up the
* CrossSection.
*/
area(): number;
/**
* Does the CrossSection (not) have any contours?
*/
isEmpty(): boolean;
/**
* The number of vertices in the CrossSection.
*/
numVert(): number;
/**
* The number of contours in the CrossSection.
*/
numContour(): number;
/**
* Returns the axis-aligned bounding rectangle of all the CrossSection's
* vertices.
*/
bounds(): Rect;
// Memory
/**
* Frees the WASM memory of this CrossSection, since these cannot be
* garbage-collected automatically.
*/
delete(): void;
}
/**
* Returns the first of n sequential new unique mesh IDs for marking sets of
* triangles that can be looked up after further operations. Assign to
* Mesh.runOriginalID vector.
*/
export function reserveIDs(count: number): number;
export class Manifold {
/**
@@ -340,165 +206,6 @@ export class Manifold {
* materials into triangle runs.
*/
constructor(mesh: Mesh);
// Shapes
/**
* Constructs a tetrahedron centered at the origin with one vertex at (1,1,1)
* and the rest at similarly symmetric points.
*/
static tetrahedron(): Manifold;
/**
* Constructs a unit cube (edge lengths all one), by default in the first
* octant, touching the origin.
*
* @param size The X, Y, and Z dimensions of the box.
* @param center Set to true to shift the center to the origin.
*/
static cube(size?: Vec3|number, center?: boolean): Manifold;
/**
* A convenience constructor for the common case of extruding a circle. Can
* also form cones if both radii are specified.
*
* @param height Z-extent
* @param radiusLow Radius of bottom circle. Must be positive.
* @param radiusHigh Radius of top circle. Can equal zero. Default is equal to
* radiusLow.
* @param circularSegments How many line segments to use around the circle.
* Default is calculated by the static Defaults.
* @param center Set to true to shift the center to the origin. Default is
* origin at the bottom.
*/
static cylinder(
height: number, radiusLow: number, radiusHigh?: number,
circularSegments?: number, center?: boolean): Manifold;
/**
* Constructs a geodesic sphere of a given radius.
*
* @param radius Radius of the sphere. Must be positive.
* @param circularSegments Number of segments along its
* diameter. This number will always be rounded up to the nearest factor of
* four, as this sphere is constructed by refining an octahedron. This means
* there are a circle of vertices on all three of the axis planes. Default is
* calculated by the static Defaults.
*/
static sphere(radius: number, circularSegments?: number): Manifold;
// Extrusions from 2d shapes
/**
* Constructs a manifold from a set of polygons/cross-section by extruding
* them along the Z-axis.
*
* @param polygons A set of non-overlapping polygons to extrude.
* @param height Z-extent of extrusion.
* @param nDivisions Number of extra copies of the crossSection to insert into
* the shape vertically; especially useful in combination with twistDegrees to
* avoid interpolation artifacts. Default is none.
* @param twistDegrees Amount to twist the top crossSection relative to the
* 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. Default {1, 1}.
* @param center If true, the extrusion is centered on the z-axis through the
* origin
* as opposed to resting on the XY plane as is default.
*/
static extrude(
polygons: CrossSection|Polygons, height: number, nDivisions?: number,
twistDegrees?: number, scaleTop?: Vec2|number,
center?: boolean): Manifold;
/**
* Constructs a manifold from a set of polygons/cross-section by revolving
* them around the Y-axis and then setting this as the Z-axis of the resulting
* manifold. If the polygons cross the Y-axis, only the part on the positive X
* side is used. Geometrically valid input will result in geometrically valid
* output.
*
* @param polygons A set of non-overlapping polygons to revolve.
* @param circularSegments Number of segments along its diameter. Default is
* calculated by the static Defaults.
* @param revolveDegrees Number of degrees to revolve. Default is 360 degrees.
*/
static revolve(
polygons: CrossSection|Polygons, circularSegments?: number,
revolveDegrees?: number): Manifold;
// Mesh Conversion
/**
* Convert a Mesh into a Manifold, retaining its properties and merging only
* the positions according to the merge vectors. Will throw an error if the
* result is not an oriented 2-manifold. Will collapse degenerate triangles
* and unnecessary vertices.
*
* All fields are read, making this structure suitable for a lossless
* round-trip of data from getMesh(). For multi-material input, use
* reserveIDs() to set a unique originalID for each material, and sort the
* materials into triangle runs.
*/
static ofMesh(mesh: Mesh): Manifold;
/**
* Constructs a smooth version of the input mesh by creating tangents; this
* method will throw if you have supplied tangents with your mesh already. The
* actual triangle resolution is unchanged; use the Refine() method to
* interpolate to a higher-resolution curve.
*
* By default, every edge is calculated for maximum smoothness (very much
* approximately), attempting to minimize the maximum mean Curvature
* magnitude. No higher-order derivatives are considered, as the interpolation
* is independent per triangle, only sharing constraints on their boundaries.
*
* @param mesh input Mesh.
* @param sharpenedEdges If desired, you can supply a vector of sharpened
* halfedges, which should in general be a small subset of all halfedges.
* Order of entries doesn't matter, as each one specifies the desired
* smoothness (between zero and one, with one the default for all unspecified
* halfedges) and the halfedge index (3 * triangle index + [0,1,2] where 0 is
* the edge between triVert 0 and 1, etc).
*
* At a smoothness value of zero, a sharp crease is made. The smoothness is
* interpolated along each edge, so the specified value should be thought of
* as an average. Where exactly two sharpened edges meet at a vertex, their
* tangents are rotated to be colinear so that the sharpened edge can be
* continuous. Vertices with only one sharpened edge are completely smooth,
* allowing sharpened edges to smoothly vanish at termination. A single vertex
* can be sharpened by sharping all edges that are incident on it, allowing
* cones to be formed.
*/
static smooth(mesh: Mesh, sharpenedEdges?: Smoothness[]): Manifold;
// Signed Distance Functions
/**
* Constructs a level-set Mesh from the input Signed-Distance Function (SDF).
* This uses a form of Marching Tetrahedra (akin to Marching Cubes, but better
* for manifoldness). Instead of using a cubic grid, it uses a body-centered
* cubic grid (two shifted cubic grids). This means if your function's
* interior exceeds the given bounds, you will see a kind of egg-crate shape
* closing off the manifold, which is due to the underlying grid.
*
* @param sdf The signed-distance function which returns the signed distance
* of
* a given point in R^3. Positive values are inside, negative outside.
* @param bounds An axis-aligned box that defines the extent of the grid.
* @param edgeLength Approximate maximum edge length of the triangles in the
* final result. This affects grid spacing, and hence has a strong effect on
* performance.
* @param level You can inset your Mesh by using a positive value, or outset
* it with a negative value.
*/
static levelSet(
sdf: (point: Vec3) => number, bounds: Box, edgeLength: number,
level?: number): Manifold;
// Transformations
/**
* Transform this Manifold in space. Stored in column-major order. This
* operation can be chained. Transforms are combined and applied lazily.
@@ -515,7 +222,6 @@ export class Manifold {
* @param v The vector to add to every vertex.
*/
translate(v: Vec3): Manifold;
translate(x: number, y?: number, z?: number): Manifold;
/**
* Applies an Euler angle rotation to the manifold, first about the X axis,
@@ -528,7 +234,6 @@ export class Manifold {
* @param v [X, Y, Z] rotation in degrees.
*/
rotate(v: Vec3): Manifold;
rotate(x: number, y?: number, z?: number): Manifold;
/**
* Scale this Manifold in space. This operation can be chained. Transforms are
@@ -548,66 +253,6 @@ export class Manifold {
*/
mirror(v: Vec3): Manifold;
/**
* This function does not change the topology, but allows the vertices to be
* moved according to any arbitrary input function. It is easy to create a
* function that warps a geometrically valid object into one which overlaps,
* but that is not checked here, so it is up to the user to choose their
* function with discretion.
*
* @param warpFunc A function that modifies a given vertex position.
*/
warp(warpFunc: (vert: Vec3) => void): 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
* will all be coplanar (and will not be immediately collapsed) unless the
* Mesh/Manifold has halfedgeTangents specified (e.g. from the Smooth()
* constructor), in which case the new vertices will be moved to the
* interpolated surface according to their barycentric coordinates.
*
* @param n The number of pieces to split every edge into. Must be > 1.
*/
refine(n: 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
* input. You may specify any number of output properties, allowing creation
* and removal of channels. Note: undefined behavior will result if you read
* past the number of input properties or write past the number of output
* properties.
*
* @param numProp The new number of properties per vertex.
* @param propFunc A function that modifies the properties of a given vertex.
*/
setProperties(
numProp: number,
propFunc: (newProp: number[], position: Vec3, oldProp: number[]) => void):
Manifold;
/**
* Curvature is the inverse of the radius of curvature, and signed such that
* positive is convex and negative is concave. There are two orthogonal
* principal curvatures at any point on a manifold, with one maximum and the
* other minimum. Gaussian curvature is their product, while mean
* curvature is their sum. This approximates them for every vertex and assigns
* them as vertex properties on the given channels.
*
* @param gaussianIdx The property channel index in which to store the
* Gaussian curvature. An index < 0 will be ignored (stores nothing). The
* property set will be automatically expanded to include the channel
* index specified.
* @param meanIdx The property channel index in which to store the mean
* curvature. An index < 0 will be ignored (stores nothing). The property
* set will be automatically expanded to include the channel index
* specified.
*/
calculateCurvature(gaussianIdx: number, meanIdx: number): Manifold;
// Boolean Operations
/**
* Boolean union
*/
@@ -623,57 +268,6 @@ export class Manifold {
*/
intersect(other: Manifold): Manifold;
/**
* Boolean union of the manifolds a and b
*/
static union(a: Manifold, b: Manifold): Manifold;
/**
* Boolean difference of the manifold b from the manifold a
*/
static difference(a: Manifold, b: Manifold): Manifold;
/**
* Boolean intersection of the manifolds a and b
*/
static intersection(a: Manifold, b: Manifold): Manifold;
/**
* Boolean union of a list of manifolds
*/
static union(manifolds: Manifold[]): Manifold;
/**
* Boolean difference of the tail of a list of manifolds from its head
*/
static difference(manifolds: Manifold[]): Manifold;
/**
* Boolean intersection of a list of manifolds
*/
static intersection(manifolds: Manifold[]): Manifold;
/**
* Split cuts this manifold in two using the cutter manifold. The first result
* is the intersection, second is the difference. This is more efficient than
* doing them separately.
*
* @param cutter
*/
split(cutter: Manifold): Manifold[];
/**
* Convenient version of Split() for a half-space.
*
* @param normal This vector is normal to the cutting plane and its length
* does
* not matter. The first result is in the direction of this vector, the second
* result is on the opposite side.
* @param originOffset The distance of the plane from the origin in the
* direction of the normal vector.
*/
splitByPlane(normal: Vec3, originOffset: number): Manifold[];
/**
* Removes everything behind the given half-space plane.
*
@@ -683,31 +277,30 @@ export class Manifold {
* @param originOffset The distance of the plane from the origin in the
* direction of the normal vector.
*/
trimByPlane(normal: Vec3, originOffset: number): Manifold;
// Convex Hulls
trimByPlane(normal: vec3, originOffset: number): Manifold;
/**
* Compute the convex hull of all points in this Manifold.
*/
hull(): Manifold;
/**
* Compute the convex hull of all points contained within a set of Manifolds
* and point vectors.
*/
static hull(points: (Manifold|Vec3)[]): Manifold;
// Topological Operations
/**
* Constructs a new manifold from a list of other manifolds. This is a purely
* topological operation, so care should be taken to avoid creating
* overlapping results. It is the inverse operation of Decompose().
* 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
* will all be coplanar (and will not be immediately collapsed) unless the
* Mesh/Manifold has halfedgeTangents specified (e.g. from the Smooth()
* constructor), in which case the new vertices will be moved to the
* interpolated surface according to their barycentric coordinates.
*
* @param manifolds A list of Manifolds to lazy-union together.
* @param n The number of pieces to split every edge into. Must be > 1.
*/
static compose(manifolds: Manifold[]): Manifold;
refine(n: number): Manifold;
/**
* This function does not change the topology, but allows the vertices to be
* moved according to any arbitrary input function. It is easy to create a
* function that warps a geometrically valid object into one which overlaps,
* but that is not checked here, so it is up to the user to choose their
* function with discretion.
*
* @param warpFunc A function that modifies a given vertex position.
*/
warp(warpFunc: (vert: Vec3) => void): Manifold;
/**
* This operation returns a vector of Manifolds that are topologically
@@ -717,8 +310,6 @@ export class Manifold {
*/
decompose(): Manifold[];
// Property Access
/**
* Does the Manifold have any triangles?
*/
@@ -739,18 +330,6 @@ export class Manifold {
*/
numEdge(): number;
/**
* The number of properties per vertex in the Manifold.
*/
numProp(): number;
/**
* The number of property vertices in the Manifold. This will always be >=
* numVert, as some physical vertices may be duplicated to account for
* different properties on different neighboring triangles.
*/
numPropVert(): number
/**
* Returns the axis-aligned bounding box of all the Manifold's vertices.
*/
@@ -781,7 +360,16 @@ export class Manifold {
*/
getProperties(): Properties;
// Export
/**
* Curvature is the inverse of the radius of curvature, and signed such that
* positive is convex and negative is concave. There are two orthogonal
* principal curvatures at any point on a manifold, with one maximum and the
* other minimum. Gaussian curvature is their product, while mean
* curvature is their sum. This approximates them for every vertex (returned
* as vectors in the structure) and also returns their minimum and maximum
* values.
*/
getCurvature(): Curvature;
/**
* Returns a Mesh that is designed to easily push into a renderer, including
@@ -798,8 +386,6 @@ export class Manifold {
*/
getMesh(normalIdx?: Vec3): Mesh;
// ID Management
/**
* If you copy a manifold, but you want this new copy to have new properties
* (e.g. a different UV mapping), you can reset its IDs to a new original,
@@ -821,21 +407,6 @@ export class Manifold {
* returns -1.
*/
originalID(): number;
/**
* Returns the first of n sequential new unique mesh IDs for marking sets of
* triangles that can be looked up after further operations. Assign to
* Mesh.runOriginalID vector.
*/
static reserveIDs(count: number): number;
// Memory
/**
* Frees the WASM memory of this Manifold, since these cannot be
* garbage-collected automatically.
*/
delete(): void;
}
export class Mesh {
@@ -864,10 +435,9 @@ export class Mesh {
get numTri(): number;
get numVert(): number;
get numRun(): number;
merge(): boolean;
verts(tri: number): SealedUint32Array<3>;
position(vert: number): SealedFloat32Array<3>;
extras(vert: number): Float32Array;
tangent(halfedge: number): SealedFloat32Array<4>;
transform(run: number): Mat4;
}
}
+8 -6
View File
@@ -43,10 +43,6 @@ export type Mat4 = [
];
export type SimplePolygon = Vec2[];
export type Polygons = SimplePolygon|SimplePolygon[];
export type Rect = {
min: Vec2,
max: Vec2
};
export type Box = {
min: Vec3,
max: Vec3
@@ -59,5 +55,11 @@ export type Properties = {
surfaceArea: number,
volume: number
};
export type FillRule = 'EvenOdd'|'NonZero'|'Positive'|'Negative'
export type JoinType = 'Square'|'Round'|'Miter'
export type Curvature = {
maxMeanCurvature: number,
minMeanCurvature: number,
maxGaussianCurvature: number,
minGaussianCurvature: number,
vertMeanCurvature: number[],
vertGaussianCurvature: number[]
};
+17 -7
View File
@@ -15,21 +15,31 @@
import * as T from './manifold-encapsulated-types';
export * from './manifold-global-types';
export type CrossSection = T.CrossSection;
export type Manifold = T.Manifold;
export type Mesh = T.Mesh;
export interface ManifoldToplevel {
CrossSection: typeof T.CrossSection;
Manifold: typeof T.Manifold;
Mesh: typeof T.Mesh;
export interface ManifoldStatic {
cube: typeof T.cube;
cylinder: typeof T.cylinder;
sphere: typeof T.sphere;
smooth: typeof T.smooth;
tetrahedron: typeof T.tetrahedron;
extrude: typeof T.extrude;
triangulate: typeof T.triangulate;
revolve: typeof T.revolve;
union: typeof T.union;
difference: typeof T.difference;
intersection: typeof T.intersection;
compose: typeof T.compose;
levelSet: typeof T.levelSet;
setMinCircularAngle: typeof T.setMinCircularAngle;
setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength;
setCircularSegments: typeof T.setCircularSegments;
getCircularSegments: typeof T.getCircularSegments;
reserveIDs: typeof T.reserveIDs;
Manifold: typeof T.Manifold;
Mesh: typeof T.Mesh;
setup: () => void;
}
export default function Module(config?: {locateFile: () => string}):
Promise<ManifoldToplevel>;
export default function Module(config: {locateFile: () => string}): Promise<ManifoldStatic>;
+1693 -1
View File
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -8,7 +8,9 @@
"manifold.wasm",
"manifold.d.ts",
"manifold-encapsulated-types.d.ts",
"manifold-global-types.d.ts"
"manifold-global-types.d.ts",
"examples/examples.js",
"test/test.js"
],
"typings": "manifold.d.ts",
"types": "manifold.d.ts",
@@ -35,5 +37,13 @@
"bugs": {
"url": "https://github.com/elalish/manifold/issues"
},
"homepage": "https://github.com/elalish/manifold#readme"
"homepage": "https://github.com/elalish/manifold#readme",
"scripts": {
"test": "mocha -u tdd -t 30000"
},
"devDependencies": {
"chai": "*",
"mocha": "*",
"gl-matrix": "3.4.3"
}
}
+161
View File
@@ -0,0 +1,161 @@
// 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
//
// 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 {expect} from 'chai';
import glMatrix from 'gl-matrix';
import {examples} from '../examples/examples.js';
import Module from '../manifold.js';
const wasm = await Module();
wasm.setup();
// Faster on modern browsers than Float32Array
glMatrix.glMatrix.setMatrixArrayType(Array);
// manifold member functions that returns a new manifold
const memberFunctions = [
'add', 'subtract', 'intersect', 'trimByPlane', 'refine', 'transform',
'translate', 'rotate', 'scale', 'mirror', 'asOriginal', 'decompose'
];
// top level functions that constructs a new manifold
const constructors = [
'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'union',
'difference', 'intersection', 'compose', 'levelSet', 'smooth'
];
const utils = [
'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments',
'getCircularSegments', 'Mesh'
];
const exposedFunctions = constructors.concat(utils);
// Setup memory management, such that users don't have to care about
// calling `delete` manually.
// Note that this only fixes memory leak across different runs: the memory
// will only be freed when the compilation finishes.
let manifoldRegistry = [];
for (const name of memberFunctions) {
const originalFn = wasm.Manifold.prototype[name];
wasm.Manifold.prototype['_' + name] = originalFn;
wasm.Manifold.prototype[name] = function(...args) {
const result = this['_' + name](...args);
manifoldRegistry.push(result);
return result;
};
}
for (const name of constructors) {
const originalFn = wasm[name];
wasm[name] = function(...args) {
const result = originalFn(...args);
manifoldRegistry.push(result);
return result;
};
}
wasm.cleanup = function() {
for (const obj of manifoldRegistry) {
// decompose result is an array of manifolds
if (obj instanceof Array)
for (const elem of obj) elem.delete();
else
obj.delete();
}
manifoldRegistry = [];
};
function runExample(name) {
try {
const content = examples.functionBodies.get(name) + '\nreturn result;\n';
const f = new Function(...exposedFunctions, 'glMatrix', content);
const manifold = f(...exposedFunctions.map(name => wasm[name]), glMatrix);
const mesh = manifold.getMesh();
expect(mesh.mergeFromVert.length).to.equal(mesh.mergeToVert.length);
expect(mesh.mergeFromVert.length)
.to.equal(mesh.numVert - manifold.numVert());
const prop = manifold.getProperties();
const genus = manifold.genus();
return {...prop, genus};
} finally {
wasm.cleanup();
}
}
suite('Examples', () => {
test('Intro', () => {
const result = runExample('Intro');
expect(result.genus).to.equal(5, 'Genus');
expect(result.volume).to.be.closeTo(203164, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(62046, 1, 'Surface Area');
});
test('Tetrahedron Puzzle', () => {
const result = runExample('Tetrahedron Puzzle');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(7297, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(3303, 1, 'Surface Area');
});
test('Rounded Frame', () => {
const result = runExample('Rounded Frame');
expect(result.genus).to.equal(5, 'Genus');
expect(result.volume).to.be.closeTo(353706, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(68454, 1, 'Surface Area');
});
test('Heart', () => {
const result = runExample('Heart');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(282743, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(22187, 1, 'Surface Area');
});
test('Scallop', () => {
const result = runExample('Scallop');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(41284, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(7810, 1, 'Surface Area');
});
test('Torus Knot', () => {
const result = runExample('Torus Knot');
expect(result.genus).to.equal(1, 'Genus');
expect(result.volume).to.be.closeTo(20960, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(11202, 1, 'Surface Area');
});
test('Menger Sponge', () => {
const result = runExample('Menger Sponge');
expect(result.genus).to.equal(729, 'Genus');
expect(result.volume).to.be.closeTo(203222, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(130475, 10, 'Surface Area');
});
test('Stretchy Bracelet', () => {
const result = runExample('Stretchy Bracelet');
expect(result.genus).to.equal(1, 'Genus');
expect(result.volume).to.be.closeTo(3992, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(22267, 1, 'Surface Area');
});
test('Gyroid Module', () => {
const result = runExample('Gyroid Module');
expect(result.genus).to.equal(15, 'Genus');
expect(result.volume).to.be.closeTo(4167, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(5642, 1, 'Surface Area');
});
});
+1 -14
View File
@@ -24,24 +24,11 @@ target_link_libraries(largeSceneTest manifold)
target_compile_options(largeSceneTest PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(largeSceneTest PUBLIC cxx_std_17)
if(MANIFOLD_DEBUG)
add_executable(minimizeTestcase minimize_testcase.cpp)
target_link_libraries(minimizeTestcase polygon)
target_compile_options(minimizeTestcase PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(minimizeTestcase PUBLIC cxx_std_17)
endif()
if(MANIFOLD_EXPORT)
add_executable(convertFile convert_file.cpp)
target_link_libraries(convertFile manifold meshIO)
target_compile_options(convertFile PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(convertFile PUBLIC cxx_std_17)
endif()
if(BUILD_TEST_CGAL)
add_executable(perfTestCGAL perf_test_cgal.cpp)
find_package(CGAL REQUIRED COMPONENTS Core)
target_compile_definitions(perfTestCGAL PRIVATE CGAL_USE_GMPXX)
# target_compile_definitions(perfTestCGAL PRIVATE CGAL_DEBUG)
target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core)
target_compile_options(perfTestCGAL PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(perfTestCGAL PUBLIC cxx_std_17)
-67
View File
@@ -1,67 +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.
#include <iostream>
#include "manifold.h"
#include "meshIO.h"
using namespace manifold;
/*
*/
int main(int argc, char** argv) {
if (argc < 2) {
std::cout << "Specify an input filename." << std::endl;
return 1;
}
manifold::ManifoldParams().verbose = true;
const std::string filename(argv[1]);
MeshGL input = ImportMesh(filename);
std::cout << input.NumVert() << " vertices, " << input.NumTri()
<< " triangles" << std::endl;
if (input.Merge())
std::cout << filename << " is not manifold, attempting to fix."
<< std::endl;
const Manifold manifold(input);
if (manifold.Status() != Manifold::Error::NoError) {
std::cout << "Could not make a valid manifold, error: "
<< (int)manifold.Status() << std::endl;
return 2;
}
const std::vector<Manifold> parts = manifold.Decompose();
std::cout << parts.size() << " objects:" << std::endl;
for (const Manifold& part : parts) {
auto prop = part.GetProperties();
std::cout << part.NumVert() << " vertices, " << part.NumTri()
<< " triangles, volume = " << prop.volume
<< ", surface area = " << prop.surfaceArea << std::endl;
}
if (argc == 3) {
std::string outName = argv[2];
std::cout << "Writing " << outName << std::endl;
ExportMesh(outName, manifold.GetMeshGL(), {});
}
return 0;
}
-391
View File
@@ -1,391 +0,0 @@
#include <algorithm>
#include <fstream>
#include <string>
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
#include <execution>
#endif
#include "polygon.h"
#include "utils.h"
using namespace manifold;
inline float cross(glm::vec2 p, glm::vec2 q) { return p.x * q.y - p.y * q.x; }
// return true if p intersects with q
// note that we don't care about collinear, intersection in the ends etc.
// https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
inline bool intersect(glm::vec2 p0, glm::vec2 p1, glm::vec2 q0, glm::vec2 q1,
float precision) {
glm::vec2 r = p1 - p0;
glm::vec2 s = q1 - q0;
// allow some error in the intersection point
float epsilon_r = 2.0 * precision / glm::length(r);
float epsilon_s = 2.0 * precision / glm::length(s);
float rxs = cross(r, s);
// in case they are nearly collinear, ignore them...
// this is to avoid treating degenerate triangles as intersecting
//
// note that this does not correspond to a fixed angle,
// but seems to work well enough
if (rxs < kTolerance && rxs > -kTolerance) {
return false;
}
float u = cross(q0 - p0, r) / rxs;
float t = cross(q0 - p0, s) / rxs;
// we only care about intersection in the middle of both lines, excluding
// their ends
if (epsilon_r <= u && u <= (1 - epsilon_r) && //
epsilon_s <= t && t <= (1 - epsilon_s)) {
// there can be cases which r and s are close to collinear,
// so even though the lines are not intersecting,
// perturbation along r/s is not enough.
//
// in that case, apply perturbation perpendicular to r
glm::vec2 r_orth = glm::normalize(glm::vec2(-r.y, r.x)) * precision;
float u1 = cross(q0 - p0 + r_orth, r) / rxs;
float u2 = cross(q0 - p0 - r_orth, r) / rxs;
float t1 = cross(q0 - p0 + r_orth, s) / rxs;
float t2 = cross(q0 - p0 - r_orth, s) / rxs;
return 0 <= u1 && u1 <= 1 && //
0 <= t1 && t1 <= 1 && //
0 <= u2 && u2 <= 1 && //
0 <= t2 && t2 <= 1;
return true;
} else {
return false;
}
}
// check if removing point j in polygon i will introduce self-intersection
// this checks if the new edge j-1 <-> j+1 intersects with any other edges,
// assuming the original polygon does not contain self-intersection
bool safeToRemove(const Polygons &polys, int i, int j, float precision) {
if (polys[i].size() == 3) return false;
glm::vec2 prev = polys[i][j == 0 ? polys[i].size() - 1 : (j - 1)];
glm::vec2 next = polys[i][j == (polys[i].size() - 1) ? 0 : (j + 1)];
for (int k = 0; k < polys.size(); k++) {
auto ll = [&](size_t l) {
return l == (polys[k].size() - 1) ? 0 : (l + 1);
};
const glm::vec2 *polysk = polys[k].data();
if (!std::all_of(
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
std::execution::par,
#endif
countAt((size_t)0), countAt(polys[k].size()), [=](size_t l) {
if (i == k && (l == j || ll(l) == j)) return true;
return !intersect(prev, next, polysk[l], polysk[ll(l)],
precision);
}))
return false;
}
return true;
}
std::pair<int, int> findIndex(const Polygons &polys, int i) {
int outer = 0;
while (i >= polys[outer].size()) i -= polys[outer++].size();
return std::make_pair(outer, i);
}
void Dump(const Polygons &polys) {
for (const SimplePolygon &poly : polys) {
std::cout << "polys.push_back({" << std::setprecision(9) << std::endl;
for (const glm::vec2 v : poly) {
std::cout << " {" << v.x << ", " << v.y << "}, //" << std::endl;
}
std::cout << "});" << std::endl;
}
for (const SimplePolygon &poly : polys) {
std::cout << "show(array([" << std::endl;
for (const glm::vec2 v : poly) {
std::cout << " [" << v.x << ", " << v.y << "]," << std::endl;
}
std::cout << "]))" << std::endl;
}
}
void DumpTriangulation(const Polygons &polys, float precision) {
ExecutionParams oldParams = PolygonParams();
manifold::PolygonParams().processOverlaps = true;
std::vector<glm::ivec3> result = Triangulate(polys);
PolygonParams() = oldParams;
for (const glm::ivec3 &tri : result) {
std::pair<int, int> xid = findIndex(polys, tri.x);
std::pair<int, int> yid = findIndex(polys, tri.y);
std::pair<int, int> zid = findIndex(polys, tri.z);
glm::vec2 x = polys[xid.first][xid.second];
glm::vec2 y = polys[yid.first][yid.second];
glm::vec2 z = polys[zid.first][zid.second];
printf("show(array([[%.7f, %.7f], [%.7f, %.7f], [%.7f, %.7f]]))\n", x.x,
x.y, y.x, y.y, z.x, z.y);
if (CCW(x, y, z, precision) == -1) {
printf("^ not CCW\n");
}
}
}
// x direction ray
// idk which direction, but it is a fixed direction...
// https://stackoverflow.com/questions/11716268/point-in-polygon-algorithm
bool rayHit(glm::vec2 point, glm::vec2 q0, glm::vec2 q1) {
return ((q0.y >= point.y) != (q1.y >= point.y)) &&
(point.x <= (q1.x - q0.x) * (point.y - q0.y) / (q1.y - q0.y) + q0.x);
}
// get the list of polygons inside polygon i
// this enumerates over the first point in all other polygons,
// and check if they are inside the current polygon
// this algorithm assumes that polygons are non-intersecting
std::vector<int> getChildren(const Polygons &polys, int i) {
std::vector<int> results;
for (int j = 0; j < polys.size(); j++) {
if (i == j) continue;
glm::vec2 point = polys[j][0];
auto k1 = [&](size_t k) {
return k == (polys[i].size() - 1) ? 0 : (k + 1);
};
int count = std::count_if(
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
std::execution::par,
#endif
countAt((size_t)0), countAt(polys[i].size()),
[&](size_t k) { return rayHit(point, polys[i][k], polys[i][k1(k)]); });
if (count % 2) results.push_back(j);
}
return results;
}
// assuming polys is valid
// we try to remove vertices from polys such that
// 1. the updated polys is still valid (no overlapping edges, correct winding
// direction)
// 2. same error in triangulation (either geometryErr or overlapping triangles)
void simplify(Polygons &polys, float precision = -1) {
bool removedSomething = true;
std::string msg;
while (removedSomething) {
int points = 0;
for (const SimplePolygon &poly : polys) points += poly.size();
std::cout << "trying to simplify " << points << " points" << std::endl;
removedSomething = false;
// try to remove simple polygons
for (int i = 0; i < polys.size(); i++) {
std::vector<int> children = getChildren(polys, i);
// if there are children, we can't remove it or we will mess up with
// winding direction...
if (!children.empty()) continue;
SimplePolygon poly = std::move(polys[i]);
polys.erase(polys.begin() + i);
try {
std::vector<glm::ivec3> result = Triangulate(polys, precision);
polys.insert(polys.begin() + i, std::move(poly));
} catch (geometryErr &e) {
if (msg.size() > 0 && msg.compare(e.what()) != 0) {
polys.insert(polys.begin() + i, std::move(poly));
} else {
removedSomething = true;
msg = e.what();
}
} catch (topologyErr &e) {
polys.insert(polys.begin() + i, std::move(poly));
}
}
for (int i = 0; i < polys.size(); i++) {
std::vector<int> children = getChildren(polys, i);
for (int j = 0; j < polys[i].size(); j++) {
// removed vertex cannot change inclusion relation
// we just check if the vertex
// x: intersects with j0, j (original edge 1)
// y: intersects with j, j1 (original edge 2)
// z: intersects with j0, j1 (new edge)
// if (x ^ y ^ z) is true, it means that the count % 2 is changed,
// and we changed inclusion relation, so vertex j cannot be removed
int j0 = j == 0 ? polys[i].size() - 1 : j - 1;
int j1 = j == polys[i].size() - 1 ? 0 : j + 1;
if (std::any_of(children.begin(), children.end(), [&](int k) {
return rayHit(polys[k][0], polys[i][j0], polys[i][j]) ^
rayHit(polys[k][0], polys[i][j], polys[i][j1]) ^
rayHit(polys[k][0], polys[i][j0], polys[i][j1]);
}))
continue;
// removed vertex cannot introduce intersection
if (!safeToRemove(polys, i, j, precision)) continue;
glm::vec2 removed = polys[i][j];
polys[i].erase(polys[i].begin() + j);
try {
std::vector<glm::ivec3> result = Triangulate(polys, precision);
polys[i].insert(polys[i].begin() + j, removed);
} catch (geometryErr &e) {
if (msg.size() > 0 && msg.compare(e.what()) != 0) {
polys[i].insert(polys[i].begin() + j, removed);
} else {
removedSomething = true;
msg = e.what();
}
} catch (topologyErr &e) {
polys[i].insert(polys[i].begin() + j, removed);
}
}
}
}
std::cout << "Error message for triangulation:" << std::endl
<< msg.c_str() << std::endl;
}
struct Edge {
glm::vec2 west; // -x side vertex
glm::vec2 east; // +x side vertex
size_t i, j; // indices of origin vertex
bool operator<(const Edge &other) const {
return west.x < other.west.x ||
(west.x == other.west.x && east.x < other.east.x);
}
Edge(const Polygons &polys, size_t i, size_t j) : i(i), j(j) {
size_t j1 = j == polys[i].size() - 1 ? 0 : j + 1;
east = polys[i][j];
west = polys[i][j1];
if (west.x > east.x) std::swap(east, west);
}
};
int isValid(const Polygons &polys, float precision = -1) {
size_t numEdge = 0;
for (const SimplePolygon &poly : polys) numEdge += poly.size();
std::vector<Edge> edges;
edges.reserve(numEdge);
for (size_t i = 0; i < polys.size(); i++)
for (size_t j = 0; j < polys[i].size(); j++)
edges.push_back(Edge(polys, i, j));
std::sort(edges.begin(), edges.end());
// check intersection
std::mutex mutex;
std::vector<std::pair<Edge, Edge>> overlappingPairs;
std::for_each( //
countAt((size_t)0), countAt(edges.size()), [&](size_t i) {
// check all subsequent edges e' until e'.west.x > e.east.x
// this ensures we checked all edges e' where
// e.west.x < e'.west.x < e.east.x
Edge e = edges[i];
for (size_t j = i + 1; j < edges.size() && edges[j].west.x < e.east.x;
j++) {
if (intersect(e.west, e.east, edges[j].west, edges[j].east,
precision)) {
mutex.lock();
overlappingPairs.push_back(std::make_pair(e, edges[j]));
mutex.unlock();
}
}
});
if (!overlappingPairs.empty()) {
std::cout << "found " << overlappingPairs.size() << " self-intersection"
<< std::endl;
for (const std::pair<Edge, Edge> &pairs : overlappingPairs) {
std::cout << pairs.first.i << " " << pairs.first.j << "<->" //
<< pairs.second.i << " " << pairs.second.j << std::endl;
printf("array([[%.7f, %.7f], [%.7f, %.7f]])\n", pairs.first.west.x,
pairs.first.west.y, pairs.first.east.x, pairs.first.east.y);
printf("array([[%.7f, %.7f], [%.7f, %.7f]])\n", pairs.second.west.x,
pairs.second.west.y, pairs.second.east.x, pairs.second.east.y);
}
} else {
std::cout << "No self-intersection" << std::endl;
}
return 0;
}
int main(int argc, char **argv) {
PolygonParams().intermediateChecks = true;
PolygonParams().processOverlaps = false;
PolygonParams().suppressErrors = true;
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <input file>" << std::endl;
return 0;
}
std::ifstream fin(argv[1]);
std::string line;
float precision = -1;
Polygons polys;
SimplePolygon poly;
// search for precision first
while (std::getline(fin, line)) {
int index = line.find("Precision = ");
if (index != std::string::npos) {
std::istringstream iss(line.substr(index + 12));
if (!(iss >> precision)) {
std::cerr << "Error parsing precision" << std::endl;
return 1;
}
break;
}
}
std::cout << "Precision = " << precision << std::endl;
// fill polygons
while (std::getline(fin, line)) {
if (line.length() > 5 && line.substr(0, 5).compare(" {") == 0) {
std::istringstream iss(line.substr(5));
float x, y;
if (!(iss >> x)) {
std::cerr << "Error parsing coordinate:" << std::endl;
std::cerr << "Line: " << line << std::endl;
return 1;
}
iss.ignore(2);
if (!(iss >> y)) {
std::cerr << "Error parsing coordinate:" << std::endl;
std::cerr << "Line: " << line << std::endl;
return 1;
}
poly.push_back(glm::vec2(x, y));
} else if (line.length() >= 3 && line.substr(0, 3).compare("});") == 0) {
if (poly.size() < 3) {
std::cerr << "Error: empty/invalid polygon" << std::endl;
return 1;
}
polys.push_back(std::move(poly));
poly.clear();
} else if (line.length() >= 5 && line.substr(0, 5).compare("array") == 0) {
break;
}
}
if (polys.size() == 0) {
std::cerr << "Error: empty polygon" << std::endl;
return 1;
}
if (precision == -1) {
float bound = 0;
for (const SimplePolygon &poly : polys) {
for (const glm::vec2 &pt : poly) {
bound = glm::max(bound, glm::abs(pt.x));
bound = glm::max(bound, glm::abs(pt.y));
}
}
precision = bound * kTolerance;
}
std::cout << "------------" << std::endl;
isValid(polys, precision);
simplify(polys, precision);
std::cout << "------------" << std::endl;
std::cout << "Final polygon:" << std::endl;
Dump(polys);
std::cout << "------------" << std::endl;
std::cout << "Erroneous triangulation:" << std::endl;
DumpTriangulation(polys, precision);
return 0;
}
+3 -2
View File
@@ -57,9 +57,10 @@ void manifoldToCGALSurfaceMesh(Manifold &manifold, TriangleMesh &cgalMesh) {
int main(int argc, char **argv) {
for (int i = 0; i < 8; ++i) {
Manifold sphere = Manifold::Sphere(1, (8 << i) * 4);
Manifold sphere2 = sphere.Translate(glm::vec3(0.5));
Manifold sphere2 = sphere;
sphere2.Translate(glm::vec3(0.5));
TriangleMesh cgalSphere, cgalSphere2;
TriangleMesh cgalSphere, cgalSphere2, cgalOut;
manifoldToCGALSurfaceMesh(sphere, cgalSphere);
manifoldToCGALSurfaceMesh(sphere2, cgalSphere2);
Generated
+7 -43
View File
@@ -1,15 +1,12 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
"type": "github"
},
"original": {
@@ -18,59 +15,26 @@
"type": "github"
}
},
"gtest-src": {
"flake": false,
"locked": {
"lastModified": 1690989893,
"narHash": "sha256-t0RchAHTJbuI5YW4uyBPykTvcjy90JW9AOPNjIhwh6U=",
"owner": "google",
"repo": "googletest",
"rev": "f8d7d77c06936315286eb55f8de22cd23c188571",
"type": "github"
},
"original": {
"owner": "google",
"ref": "v1.14.0",
"repo": "googletest",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1692734709,
"narHash": "sha256-SCFnyHCyYjwEmgUsHDDuU0TsbVMKeU1vwkR+r7uS2Rg=",
"lastModified": 1657296039,
"narHash": "sha256-Ghh39+aS+pw5sTP/ZO8VIKE6sBhMadDaQZtf+3yu4Vc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b85ed9dcbf187b909ef7964774f8847d554fab3b",
"rev": "71d7a4c037dc4f3e98d5c4a81b941933cf5bf675",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"ref": "nixos-22.05",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"gtest-src": "gtest-src",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+52 -92
View File
@@ -1,11 +1,8 @@
{
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
inputs.gtest-src = {
url = "github:google/googletest/v1.14.0";
flake = false;
};
outputs = { self, nixpkgs, flake-utils, gtest-src }:
inputs.nixpkgs.url = "nixpkgs/nixos-22.05";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem
(system:
let
@@ -13,61 +10,27 @@
inherit system;
config.allowUnfree = true;
};
thrust-210 = pkgs.nvidia-thrust.overrideAttrs (_: _: {
version = "24486a1";
src = builtins.fetchGit {
url = "https://github.com/NVIDIA/thrust.git";
ref = "main";
rev = "24486a169a62a58ef8f824d3dc9613c006b6f5a7";
submodules = true;
};
cmakeFlags = [ "-DTHRUST_ENABLE_HEADER_TESTING=OFF" "-DTHRUST_ENABLE_TESTING=OFF" "-DTHRUST_ENABLE_EXAMPLES=OFF" "-DTHRUST_DEVICE_SYSTEM=CPP" ];
fixupPhase = ''
cat <<EOT > $out/lib/cmake/thrust/thrust-header-search.cmake
# Parse version information from version.h in source tree
set(_THRUST_VERSION_INCLUDE_DIR "$out/include")
if(EXISTS "\''${_THRUST_VERSION_INCLUDE_DIR}/thrust/version.h")
set(_THRUST_VERSION_INCLUDE_DIR "\''${_THRUST_VERSION_INCLUDE_DIR}" CACHE FILEPATH "" FORCE) # Clear old result
set_property(CACHE _THRUST_VERSION_INCLUDE_DIR PROPERTY TYPE INTERNAL)
endif()
EOT
cat <<EOT > $out/lib/cmake/libcudacxx/libcudacxx-header-search.cmake
# Parse version information from version header:
unset(_libcudacxx_VERSION_INCLUDE_DIR CACHE) # Clear old result to force search
find_path(_libcudacxx_VERSION_INCLUDE_DIR cuda/std/detail/__config
NO_DEFAULT_PATH # Only search explicit paths below:
PATHS
"\''${CMAKE_CURRENT_LIST_DIR}/../../../include" # Source tree
)
set_property(CACHE _libcudacxx_VERSION_INCLUDE_DIR PROPERTY TYPE INTERNAL)
EOT
'';
enableParallelBuilding = false;
});
manifold =
{ parallel-backend ? "none"
, cuda-support ? false
, doCheck ? true
, build-tools ? [ ]
, ...
}: pkgs.stdenv.mkDerivation {
inherit doCheck;
pname = "manifold-${parallel-backend}";
version = "2.2.0";
pname =
if cuda-support then
"manifold-${parallel-backend}-cuda"
else
"manifold-${parallel-backend}";
version = "beta";
src = self;
nativeBuildInputs = (with pkgs; [
cmake
thrust-210
glm
ninja
(python39.withPackages
(ps: with ps; [ trimesh ]))
gtest
]) ++ build-tools;
nativeBuildInputs = (with pkgs; [ cmake (python39.withPackages(ps: with ps; [trimesh])) ]) ++ build-tools ++
(if cuda-support then with pkgs.cudaPackages; [ cuda_nvcc cuda_cudart cuda_cccl pkgs.addOpenGLRunpath ] else [ ]);
cmakeFlags = [
"-DMANIFOLD_PYBIND=ON"
"-DMANIFOLD_CBIND=ON"
"-DBUILD_SHARED_LIBS=ON"
"-DMANIFOLD_PAR=${pkgs.lib.strings.toUpper parallel-backend}"
"-DMANIFOLD_USE_CUDA=${if cuda-support then "ON" else "OFF"}"
];
checkPhase = ''
cd test
@@ -76,20 +39,42 @@
PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python python3 bindings/python/examples/run_all.py
cd build
'';
installPhase = ''
mkdir -p $out
cp src/manifold/libmanifold.a $out/
cp extras/perfTest $out
cp bindings/python/pymanifold* $out
'';
};
parallelBackends = [
{ parallel-backend = "none"; }
{
parallel-backend = "omp";
build-tools = [ pkgs.llvmPackages_13.openmp ];
}
{
parallel-backend = "tbb";
build-tools = with pkgs; [ tbb_2021_8 pkg-config ];
build-tools = with pkgs; [ tbb pkg-config ];
}
];
buildMatrix = with pkgs; with lib; lists.flatten (map
(env: map
(x: x // env)
parallelBackends) [
{ cuda-support = false; }
{
cuda-support = true;
}
]);
devShell = { additional ? [ ] }: pkgs.mkShell {
buildInputs = with pkgs; [
cmake
tbb_2021_8
thrust-210
gtest
llvmPackages_13.openmp
clang-tools
clang_13
emscripten
tbb
lcov
] ++ additional;
};
in
@@ -97,10 +82,11 @@
packages = (builtins.listToAttrs
(map
(x: {
name = "manifold-" + x.parallel-backend;
name = "manifold-" + x.parallel-backend + (if
x.cuda-support then "-cuda" else "");
value = manifold x;
})
parallelBackends)) // {
buildMatrix)) // {
manifold-js = pkgs.buildEmscriptenPackage {
name = "manifold-js";
version = "beta";
@@ -112,10 +98,9 @@
export EM_CACHE=$(pwd)/.emscriptencache
mkdir build
cd build
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
-DFETCHCONTENT_SOURCE_DIR_GLM=${pkgs.glm.src} \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=${gtest-src} \
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-210.src} ..
mkdir cache
export EM_CACHE=$(pwd)/cache
emcmake cmake -DCMAKE_BUILD_TYPE=Release ..
'';
buildPhase = ''
emmake make -j''${NIX_BUILD_CORES}
@@ -131,40 +116,15 @@
cp {extras,wasm}/*.wasm $out/
'';
};
# but how should we make it work with other python versions?
manifold3d = with pkgs.python3Packages; buildPythonPackage {
pname = "manifold3d";
version = "2.2.0";
src = self;
propagatedBuildInputs = [
numpy
];
buildInputs = with pkgs; [
tbb_2021_8
thrust-210
glm
];
nativeBuildInputs = with pkgs; [
cmake
ninja
setuptools
scikit-build-core
pyproject-metadata
pathspec
pkg-config
];
checkInputs = [
trimesh
];
format = "pyproject";
dontUseCmakeConfigure = true;
doCheck = true;
checkPhase = ''
python3 bindings/python/examples/run_all.py
'';
};
};
devShell = devShell { };
devShells.cuda = devShell {
additional = with pkgs.cudaPackages; [
cuda_nvcc
cuda_cudart
cuda_cccl
];
};
}
);
}
+4 -15
View File
@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
project(meshIO)
add_subdirectory(third_party)
find_package(assimp REQUIRED)
project(meshIO)
add_library(${PROJECT_NAME} src/meshIO.cpp)
@@ -23,20 +23,9 @@ target_include_directories(${PROJECT_NAME}
)
target_link_libraries(${PROJECT_NAME}
PUBLIC manifold
PRIVATE assimp::assimp
PUBLIC utilities
PRIVATE assimp
)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
install(
TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT devel
)
install(
DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
COMPONENT devel
)
+6 -1
View File
@@ -15,10 +15,14 @@
#pragma once
#include <string>
#include "manifold.h"
#include "public.h"
namespace manifold {
/** @addtogroup Debug
* @{
*/
/** @defgroup MeshIO
* @brief 3D model file I/O based on Assimp
* @{
@@ -66,4 +70,5 @@ void ExportMesh(const std::string& filename, const Mesh& mesh,
void ExportMesh(const std::string& filename, const MeshGL& mesh,
const ExportOptions& options);
/** @} */
/** @} */
} // namespace manifold
-7
View File
@@ -79,13 +79,6 @@ void ExportScene(aiScene* scene, const std::string& filename) {
// std::cout << i << ", id = " << desc->id << ", " << desc->description
// << std::endl;
// }
auto ext = filename.substr(filename.length() - 4, 4);
if (ext == ".3mf") {
// Workaround https://github.com/assimp/assimp/issues/3816
aiNode* old_root = scene->mRootNode;
scene->mRootNode = new aiNode();
scene->mRootNode->addChildren(1, &old_root);
}
auto result = exporter.Export(scene, GetType(filename), filename);
+27
View File
@@ -0,0 +1,27 @@
# 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.
# I feel like there has to be a better way - I hate using the CMake cache.
set(ASSIMP_BUILD_TESTS OFF CACHE INTERNAL "" FORCE)
set(ASSIMP_WARNINGS_AS_ERRORS OFF CACHE INTERNAL "" FORCE)
set(ASSIMP_INSTALL OFF CACHE INTERNAL "" FORCE)
set(ASSIMP_BUILD_ASSIMP_VIEW OFF CACHE INTERNAL "" FORCE)
set(ASSIMP_BUILD_ALL_EXPORTERS_BY_DEFAULT OFF CACHE INTERNAL "" FORCE)
set(ASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT OFF CACHE INTERNAL "" FORCE)
set(ASSIMP_BUILD_GLTF_EXPORTER ON CACHE INTERNAL "" FORCE)
set(ASSIMP_BUILD_GLTF_IMPORTER ON CACHE INTERNAL "" FORCE)
add_subdirectory(assimp)
target_compile_options(assimp PRIVATE -Wno-error -Wno-deprecated-declarations)
Vendored Submodule
+1
-78
View File
@@ -1,78 +0,0 @@
[project]
name = "manifold3d"
version = "2.2.0"
authors = [
{ name="Emmett Lalish", email="elalish@gmail.com" },
]
description = "Library for geometric robustness"
readme = "README.md"
classifiers = [
"Development Status :: 4 - Beta",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: C++",
"Topic :: Multimedia :: Graphics :: 3D Modeling",
]
requires-python = ">=3.8"
dependencies = [
"numpy; python_version<'3.12'",
"numpy>=1.26.0b1; python_version>='3.12'"
]
[project.urls]
"Homepage" = "https://github.com/elalish/manifold"
"Bug Tracker" = "https://github.com/elalish/manifold/issues"
[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"
[tool.scikit-build]
cmake.minimum-version = "3.18"
sdist.exclude = [
".github",
"bindings/c",
"bindings/wasm",
"docs",
"extras",
"meshIO",
"samples",
"test",
"oneTBB", # we may have this when we build with cibuildwheel
"bindings/python/third_party/nanobind/tests/",
"bindings/python/third_party/nanobind/docs/",
"src/third_party/clipper2/CPP/Examples/",
"src/third_party/clipper2/CPP/Tests/",
"src/third_party/clipper2/CSharp/",
"src/third_party/clipper2/Delphi/",
"src/third_party/clipper2/DLL/",
"src/third_party/clipper2/Tests/",
]
wheel.packages = ["manifold3d"]
cmake.args = ["-DMANIFOLD_PAR=TBB", "-DMANIFOLD_TEST=OFF"]
install.components = ["bindings"]
[tool.cibuildwheel]
build-frontend = "build"
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"
musllinux-x86_64-image = "musllinux_1_2"
skip = ["*-win32", "*_i686", "pp*", "*-musllinux*"]
# only clone TBB once
before-all = "git clone --depth 1 --branch v2021.10.0 https://github.com/oneapi-src/oneTBB.git"
[tool.cibuildwheel.config-settings]
"cmake.define.FETCHCONTENT_SOURCE_DIR_TBB" = "./oneTBB"
"cmake.define.FETCHCONTENT_UPDATES_DISCONNECTED" = "ON"
[tool.cibuildwheel.macos]
archs = ["x86_64", "arm64"]
environment = "MACOSX_DEPLOYMENT_TARGET=10.14"
test-skip = "*-macosx_arm64 *-macosx_universal2:arm64"
[tool.cibuildwheel.windows]
before-build = "pip install delvewheel"
repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}"
+24 -4
View File
@@ -14,17 +14,37 @@
project(samples)
add_library(samples src/bracelet.cpp src/knot.cpp src/menger_sponge.cpp
src/rounded_frame.cpp src/scallop.cpp src/tet_puzzle.cpp
src/gyroid_module.cpp src/condensed_matter.cpp)
add_library(samples src/bracelet.cpp src/knot.cpp src/menger_sponge.cpp src/rounded_frame.cpp src/scallop.cpp src/tet_puzzle.cpp)
target_include_directories(samples
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(samples
PUBLIC manifold sdf
PUBLIC manifold
)
target_compile_options(samples PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(samples PUBLIC cxx_std_17)
project(samplesGPU)
add_library(samplesGPU src/gyroid_module.cpp)
if(MANIFOLD_USE_CUDA)
set_source_files_properties(src/gyroid_module.cpp PROPERTIES LANGUAGE CUDA)
set_property(TARGET samplesGPU PROPERTY CUDA_ARCHITECTURES 61)
endif()
target_include_directories(samplesGPU
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(samplesGPU
PUBLIC manifold sdf
)
target_compile_options(samplesGPU PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(samplesGPU
PUBLIC cxx_std_17
)
-2
View File
@@ -51,7 +51,5 @@ Manifold TetPuzzle(float edgeLength, float gap, int nDivisions);
Manifold Scallop();
Manifold GyroidModule(float size = 20, int n = 20);
Manifold CondensedMatter(int fn = 16);
/** @} */ // end of Samples
} // namespace manifold
-149
View File
@@ -1,149 +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.
// From
// https://gist.github.com/ochafik/70a6b15e982b7ccd5a79ff9afd99dbcf#file-condensed-matter-scad
#include "samples.h"
namespace {
using namespace manifold;
using namespace glm;
constexpr float AtomicRadiusN2 = 0.65;
constexpr float BondPairN2 = 1.197;
constexpr float AtomicRadiusSi = 1.1;
constexpr float LatticeCellSizeSi = 5.4309;
constexpr float fccOffset = 0.25;
constexpr float AtomicRadiusC = 0.7;
constexpr float LatticeCellSizeC = 3.65;
constexpr float cellLenA = 2.464;
constexpr float cellLenB = cellLenA;
constexpr float cellLenC = 6.711;
constexpr float cellAngleA = 90;
constexpr float cellAngleB = cellAngleA;
constexpr float cellAngleC = 120;
constexpr float LayerSeperationC = 3.364;
Manifold bond(int fn, vec3 p1 = {0, 0, 0}, vec3 p2 = {1, 1, 1}, float ar1 = 1.0,
float ar2 = 2.0) {
float cyR = std::min(ar1, ar2) / 5.0;
float dist = length(p1 - p2);
vec3 cyC = (p1 + p2) / 2.0f;
float beta = degrees(acos((p1.z - p2.z) / dist));
float gamma = degrees(atan2(p1.y - p2.y, p1.x - p2.x));
vec3 rot = {0.0, beta, gamma};
return Manifold::Cylinder(dist, cyR, -1, fn, true)
.Rotate(rot.x, rot.y, rot.z)
.Translate(cyC);
}
Manifold bondPair(int fn, float d = 0.0, float ar = 1.0) {
float axD = pow(d, 1.0 / 3.0);
vec3 p1 = {+axD, -axD, -axD};
vec3 p2 = {-axD, +axD, +axD};
Manifold sphere = Manifold::Sphere(ar, fn);
return sphere.Translate(p1) + sphere.Translate(p2) + bond(fn, p1, p2, ar, ar);
}
Manifold hexagonalClosePacked(int fn, vec3 dst = {1.0, 1.0, 1.0},
float ar = 1.0) {
std::vector<Manifold> parts;
vec3 p1 = {0, 0, 0};
parts.push_back(Manifold::Sphere(ar, fn));
float baseAg = 30;
vec3 ag = {baseAg, baseAg + 120, baseAg + 240};
vec3 points[] = {{cosd(ag.x) * dst.x, sind(ag.x) * dst.x, 0},
{cosd(ag.y) * dst.y, sind(ag.y) * dst.y, 0},
{cosd(ag.z) * dst.z, sind(ag.z) * dst.z, 0}};
for (vec3 p2 : points) {
parts.push_back(Manifold::Sphere(ar, fn).Translate(p2));
parts.push_back(bond(fn, p1, p2, ar, ar));
}
return Manifold::BatchBoolean(parts, OpType::Add);
}
Manifold fccDiamond(int fn, float ar = 1.0, float unitCell = 2.0,
float fccOffset = 0.25) {
std::vector<Manifold> parts;
float huc = unitCell / 2.0;
float od = fccOffset * unitCell;
vec3 interstitial[] = {
{+od, +od, +od}, {+od, -od, -od}, {-od, +od, -od}, {-od, -od, +od}};
vec3 corners[] = {{+huc, +huc, +huc},
{+huc, -huc, -huc},
{-huc, +huc, -huc},
{-huc, -huc, +huc}};
vec3 fcc[] = {{+huc, 0, 0}, {-huc, 0, 0}, {0, +huc, 0},
{0, -huc, 0}, {0, 0, +huc}, {0, 0, -huc}};
for (auto p : corners) parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
for (auto p : fcc) parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
for (auto p : interstitial)
parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
vec3 bonds[][2] = {{interstitial[0], corners[0]}, {interstitial[0], fcc[0]},
{interstitial[0], fcc[2]}, {interstitial[0], fcc[4]},
{interstitial[1], corners[1]}, {interstitial[1], fcc[0]},
{interstitial[1], fcc[3]}, {interstitial[1], fcc[5]},
{interstitial[2], corners[2]}, {interstitial[2], fcc[1]},
{interstitial[2], fcc[2]}, {interstitial[2], fcc[5]},
{interstitial[3], corners[3]}, {interstitial[3], fcc[1]},
{interstitial[3], fcc[3]}, {interstitial[3], fcc[4]}};
for (auto b : bonds) parts.push_back(bond(fn, b[0], b[1], ar, ar));
return Manifold::BatchBoolean(parts, OpType::Add);
}
Manifold SiCell(int fn, float x = 1.0, float y = 1.0, float z = 1.0) {
return fccDiamond(fn, AtomicRadiusSi, LatticeCellSizeSi, fccOffset)
.Translate({LatticeCellSizeSi * x, LatticeCellSizeSi * y,
LatticeCellSizeSi * z});
}
Manifold SiN2Cell(int fn, float x = 1.0, float y = 1.0, float z = 1.0) {
float n2Offset = LatticeCellSizeSi / 8;
return bondPair(fn, BondPairN2, AtomicRadiusN2)
.Translate({LatticeCellSizeSi * x - n2Offset,
LatticeCellSizeSi * y + n2Offset,
LatticeCellSizeSi * z + n2Offset}) +
SiCell(fn, x, y, z);
}
Manifold GraphiteCell(int fn, vec3 xyz = {1.0, 1.0, 1.0}) {
vec3 loc = {(cellLenA * xyz.x * cosd(30) * 2),
((cellLenB * sind(30)) + cellLenC) * xyz.y, xyz.z};
return hexagonalClosePacked(fn, {cellLenA, cellLenB, cellLenC}, AtomicRadiusC)
.Translate(loc);
}
} // namespace
namespace manifold {
Manifold CondensedMatter(int fn) {
std::vector<Manifold> parts;
float siOffset = 3.0 * LatticeCellSizeSi / 8.0;
for (int x = -3; x <= 3; x++)
for (int y = -1; y <= 2; y++)
parts.push_back(
GraphiteCell(fn, {x + (y % 2 == 0 ? 0.0 : 0.5), y,
LayerSeperationC * 0.5 + LatticeCellSizeSi * 1.5})
.Translate({0, -siOffset, 0})
.Rotate(0, 0, 45));
float xyPlane[] = {-2, -1, 0, +1, +2};
for (float x : xyPlane)
for (float y : xyPlane) parts.push_back(SiN2Cell(fn, x, y, 1));
return Manifold::BatchBoolean(parts, OpType::Add);
}
} // namespace manifold
+2 -4
View File
@@ -12,15 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "manifold.h"
#include "samples.h"
#include "sdf.h"
namespace {
using namespace manifold;
struct Gyroid {
float operator()(glm::vec3 p) const {
__host__ __device__ float operator()(glm::vec3 p) const {
p -= glm::pi<float>() / 4;
return cos(p.x) * sin(p.y) + cos(p.y) * sin(p.z) + cos(p.z) * sin(p.x);
}
@@ -56,4 +54,4 @@ Manifold GyroidModule(float size, int n) {
return result.Rotate(-45, 0, 90).Translate({0, 0, size / glm::sqrt(2.0f)});
}
} // namespace manifold
} // namespace manifold
+1 -2
View File
@@ -12,8 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# do not add third_party installations
add_subdirectory(third_party EXCLUDE_FROM_ALL)
add_subdirectory(third_party)
add_subdirectory(utilities)
add_subdirectory(collider)
add_subdirectory(cross_section)
+6 -8
View File
@@ -15,18 +15,16 @@
project (collider)
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
add_library(${PROJECT_NAME} ${SOURCE_FILES})
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
if(MANIFOLD_USE_CUDA)
set_source_files_properties(${SOURCE_FILES} PROPERTIES LANGUAGE CUDA)
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
endif()
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_link_libraries(${PROJECT_NAME} PUBLIC utilities)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME}
PUBLIC cxx_std_17
)
install(
TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT runtime
)
+11 -10
View File
@@ -15,7 +15,7 @@
#pragma once
#include "public.h"
#include "sparse.h"
#include "vec.h"
#include "vec_dh.h"
namespace manifold {
@@ -23,19 +23,20 @@ namespace manifold {
class Collider {
public:
Collider() {}
Collider(const VecView<const Box>& leafBB,
const VecView<const uint32_t>& leafMorton);
Collider(const VecDH<Box>& leafBB, const VecDH<uint32_t>& leafMorton);
// Aborts and returns false if transform is not axis aligned.
bool Transform(glm::mat4x3);
void UpdateBoxes(const VecView<const Box>& leafBB);
template <const bool selfCollision = false, const bool inverted = false,
typename T>
SparseIndices Collisions(const VecView<const T>& queriesIn) const;
void UpdateBoxes(const VecDH<Box>& leafBB);
// Collisions returns a sparse result, where i is the query index and j is
// the leaf index where their bounding boxes overlap.
template <typename T>
SparseIndices Collisions(const VecDH<T>& queriesIn) const;
private:
Vec<Box> nodeBBox_;
Vec<int> nodeParent_;
VecDH<Box> nodeBBox_;
VecDH<int> nodeParent_;
// even nodes are leaves, odd nodes are internal, root is 1
Vec<thrust::pair<int, int>> internalChildren_;
VecDH<thrust::pair<int, int>> internalChildren_;
int NumInternal() const { return internalChildren_.size(); };
int NumLeaves() const { return NumInternal() + 1; };
+76 -127
View File
@@ -24,7 +24,6 @@
// Adjustable parameters
constexpr int kInitialLength = 128;
constexpr int kLengthMultiple = 4;
constexpr int kSequentialThreshold = 512;
// Fundamental constants
constexpr int kRoot = 1;
@@ -60,30 +59,36 @@ uint32_t __inline clz(uint32_t value) {
namespace {
using namespace manifold;
bool IsLeaf(int node) { return node % 2 == 0; }
bool IsInternal(int node) { return node % 2 == 1; }
int Node2Internal(int node) { return (node - 1) / 2; }
int Internal2Node(int internal) { return internal * 2 + 1; }
int Node2Leaf(int node) { return node / 2; }
int Leaf2Node(int leaf) { return leaf * 2; }
__host__ __device__ bool IsLeaf(int node) { return node % 2 == 0; }
__host__ __device__ bool IsInternal(int node) { return node % 2 == 1; }
__host__ __device__ int Node2Internal(int node) { return (node - 1) / 2; }
__host__ __device__ int Internal2Node(int internal) { return internal * 2 + 1; }
__host__ __device__ int Node2Leaf(int node) { return node / 2; }
__host__ __device__ int Leaf2Node(int leaf) { return leaf * 2; }
struct CreateRadixTree {
VecView<int> nodeParent_;
VecView<thrust::pair<int, int>> internalChildren_;
const VecView<const uint32_t> leafMorton_;
int* nodeParent_;
thrust::pair<int, int>* internalChildren_;
const VecDc<uint32_t> leafMorton_;
int PrefixLength(uint32_t a, uint32_t b) const {
__host__ __device__ int PrefixLength(uint32_t a, uint32_t b) const {
// count-leading-zeros is used to find the number of identical highest-order
// bits
#ifdef __CUDA_ARCH__
return __clz(a ^ b);
#else
#ifdef _MSC_VER
// return __lzcnt(a ^ b);
return clz(a ^ b);
#else
return __builtin_clz(a ^ b);
#endif
#endif
}
int PrefixLength(int i, int j) const {
__host__ __device__ int PrefixLength(int i, int j) const {
if (j < 0 || j >= leafMorton_.size()) {
return -1;
} else {
@@ -98,7 +103,7 @@ struct CreateRadixTree {
}
}
int RangeEnd(int i) const {
__host__ __device__ int RangeEnd(int i) const {
// Determine direction of range (+1 or -1)
int dir = PrefixLength(i, i + 1) - PrefixLength(i, i - 1);
dir = (dir > 0) - (dir < 0);
@@ -116,7 +121,7 @@ struct CreateRadixTree {
return i + dir * length;
}
int FindSplit(int first, int last) const {
__host__ __device__ int FindSplit(int first, int last) const {
int commonPrefix = PrefixLength(first, last);
// Find the furthest object that shares more than commonPrefix bits with the
// first one, using binary search.
@@ -133,7 +138,7 @@ struct CreateRadixTree {
return split;
}
void operator()(int internal) {
__host__ __device__ void operator()(int internal) {
int first = internal;
// Find the range of objects with a common prefix
int last = RangeEnd(first);
@@ -152,27 +157,33 @@ struct CreateRadixTree {
}
};
template <typename T, const bool selfCollision, typename Recorder>
template <typename T, const bool allocateOnly>
struct FindCollisions {
VecView<const Box> nodeBBox_;
VecView<const thrust::pair<int, int>> internalChildren_;
Recorder recorder;
thrust::pair<int*, int*> queryTri_;
int* counts;
const Box* nodeBBox_;
const thrust::pair<int, int>* internalChildren_;
int RecordCollision(int node, thrust::tuple<T, int>& query) {
__host__ __device__ int RecordCollision(int node,
thrust::tuple<T, int>& query) {
const T& queryObj = thrust::get<0>(query);
const int queryIdx = thrust::get<1>(query);
int& count = counts[queryIdx];
bool overlaps = nodeBBox_[node].DoesOverlap(queryObj);
if (overlaps && IsLeaf(node)) {
int leafIdx = Node2Leaf(node);
if (!selfCollision || leafIdx != queryIdx) {
recorder.record(queryIdx, leafIdx);
if (allocateOnly) {
count++;
} else {
int pos = count++;
queryTri_.first[pos] = queryIdx;
queryTri_.second[pos] = Node2Leaf(node);
}
}
return overlaps && IsInternal(node); // Should traverse into node
}
void operator()(thrust::tuple<T, int> query) {
__host__ __device__ void operator()(thrust::tuple<T, int> query) {
// stack cannot overflow because radix tree has max depth 30 (Morton code) +
// 32 (index).
int stack[64];
@@ -181,7 +192,7 @@ struct FindCollisions {
int node = kRoot;
const int queryIdx = thrust::get<1>(query);
// same implies that this query do not have any collision
if (recorder.earlyexit(queryIdx)) return;
if (!allocateOnly && counts[queryIdx] == counts[queryIdx + 1]) return;
while (1) {
int internal = Node2Internal(node);
int child1 = internalChildren_[internal].first;
@@ -200,56 +211,16 @@ struct FindCollisions {
}
}
}
recorder.end(queryIdx);
}
};
struct CountCollisions {
VecView<int> counts;
VecView<char> empty;
void record(int queryIdx, int _leafIdx) { counts[queryIdx]++; }
bool earlyexit(int _queryIdx) { return false; }
void end(int queryIdx) {
if (counts[queryIdx] == 0) empty[queryIdx] = 1;
}
};
template <const bool inverted>
struct SeqCollisionRecorder {
SparseIndices& queryTri_;
void record(int queryIdx, int leafIdx) const {
if (inverted)
queryTri_.Add(leafIdx, queryIdx);
else
queryTri_.Add(queryIdx, leafIdx);
}
bool earlyexit(int queryIdx) const { return false; }
void end(int queryIdx) const {}
};
template <const bool inverted>
struct ParCollisionRecorder {
SparseIndices& queryTri;
VecView<int> counts;
VecView<char> empty;
void record(int queryIdx, int leafIdx) {
int pos = counts[queryIdx]++;
if (inverted)
queryTri.Set(pos, leafIdx, queryIdx);
else
queryTri.Set(pos, queryIdx, leafIdx);
}
bool earlyexit(int queryIdx) const { return empty[queryIdx] == 1; }
void end(int queryIdx) const {}
};
struct BuildInternalBoxes {
VecView<Box> nodeBBox_;
VecView<int> counter_;
const VecView<int> nodeParent_;
const VecView<thrust::pair<int, int>> internalChildren_;
Box* nodeBBox_;
int* counter_;
const int* nodeParent_;
const thrust::pair<int, int>* internalChildren_;
void operator()(int leaf) {
__host__ __device__ void operator()(int leaf) {
int node = Leaf2Node(leaf);
do {
node = nodeParent_[node];
@@ -263,7 +234,9 @@ struct BuildInternalBoxes {
struct TransformBox {
const glm::mat4x3 transform;
void operator()(Box& box) { box = box.Transform(transform); }
__host__ __device__ void operator()(Box& box) {
box = box.Transform(transform);
}
};
} // namespace
@@ -274,8 +247,8 @@ namespace manifold {
* bounding boxes and corresponding Morton codes. It is assumed these vectors
* are already sorted by increasing Morton code.
*/
Collider::Collider(const VecView<const Box>& leafBB,
const VecView<const uint32_t>& leafMorton) {
Collider::Collider(const VecDH<Box>& leafBB,
const VecDH<uint32_t>& leafMorton) {
ASSERT(leafBB.size() == leafMorton.size(), userErr,
"vectors must be the same length");
int num_nodes = 2 * leafBB.size() - 1;
@@ -285,7 +258,8 @@ Collider::Collider(const VecView<const Box>& leafBB,
internalChildren_.resize(leafBB.size() - 1, thrust::make_pair(-1, -1));
// organize tree
for_each_n(autoPolicy(NumInternal()), countAt(0), NumInternal(),
CreateRadixTree({nodeParent_, internalChildren_, leafMorton}));
CreateRadixTree(
{nodeParent_.ptrD(), internalChildren_.ptrD(), leafMorton}));
UpdateBoxes(leafBB);
}
@@ -293,61 +267,49 @@ Collider::Collider(const VecView<const Box>& leafBB,
* For a vector of query objects, this returns a sparse array of overlaps
* between the queries and the bounding boxes of the collider. Queries are
* normally axis-aligned bounding boxes. Points can also be used, and this case
* overlaps are defined as lying in the XY projection of the bounding box. If
* the query vector is the leaf vector, set selfCollision to true, which will
* then not report any collisions between an index and itself.
* overlaps are defined as lying in the XY projection of the bounding box.
*/
template <const bool selfCollision, const bool inverted, typename T>
SparseIndices Collider::Collisions(const VecView<const T>& queriesIn) const {
template <typename T>
SparseIndices Collider::Collisions(const VecDH<T>& queriesIn) const {
// 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) {
SparseIndices queryTri;
for_each_n(ExecutionPolicy::Seq, zip(queriesIn.cbegin(), countAt(0)),
queriesIn.size(),
FindCollisions<T, selfCollision, SeqCollisionRecorder<inverted>>{
nodeBBox_, internalChildren_, {queryTri}});
return queryTri;
} else {
// compute the number of collisions to determine the size for allocation and
// offset, this avoids the need for atomic
Vec<int> counts(queriesIn.size() + 1, 0);
Vec<char> empty(queriesIn.size(), 0);
for_each_n(ExecutionPolicy::Par, zip(queriesIn.cbegin(), countAt(0)),
queriesIn.size(),
FindCollisions<T, selfCollision, CountCollisions>{
nodeBBox_, internalChildren_, {counts, empty}});
// compute start index for each query and total count
exclusive_scan(ExecutionPolicy::Par, counts.begin(), counts.end(),
counts.begin(), 0, std::plus<int>());
if (counts.back() == 0) return SparseIndices(0);
SparseIndices queryTri(counts.back());
// actually recording collisions
for_each_n(ExecutionPolicy::Par, zip(queriesIn.cbegin(), countAt(0)),
queriesIn.size(),
FindCollisions<T, selfCollision, ParCollisionRecorder<inverted>>{
nodeBBox_, internalChildren_, {queryTri, counts, empty}});
return queryTri;
}
VecDH<int> counts(queriesIn.size() + 1, 0);
auto policy = autoPolicy(queriesIn.size());
// compute the number of collisions to determine the size for allocation and
// offset, this avoids the need for atomic
for_each_n(policy, zip(queriesIn.cbegin(), countAt(0)), queriesIn.size(),
FindCollisions<T, true>(
{thrust::pair<int*, int*>(nullptr, nullptr), counts.ptrD(),
nodeBBox_.ptrD(), internalChildren_.ptrD()}));
// compute start index for each query and total count
exclusive_scan(policy, counts.begin(), counts.end(), counts.begin());
SparseIndices queryTri(counts.back());
// actually recording collisions
for_each_n(
policy, zip(queriesIn.cbegin(), countAt(0)), queriesIn.size(),
FindCollisions<T, false>({queryTri.ptrDpq(), counts.ptrD(),
nodeBBox_.ptrD(), internalChildren_.ptrD()}));
return queryTri;
}
/**
* Recalculate the collider's internal bounding boxes without changing the
* hierarchy.
*/
void Collider::UpdateBoxes(const VecView<const Box>& leafBB) {
void Collider::UpdateBoxes(const VecDH<Box>& leafBB) {
ASSERT(leafBB.size() == NumLeaves(), userErr,
"must have the same number of updated boxes as original");
// copy in leaf node Boxes
strided_range<Vec<Box>::Iter> leaves(nodeBBox_.begin(), nodeBBox_.end(), 2);
strided_range<VecDH<Box>::Iter> leaves(nodeBBox_.begin(), nodeBBox_.end(), 2);
auto policy = autoPolicy(NumInternal());
copy(policy, leafBB.cbegin(), leafBB.cend(), leaves.begin());
// create global counters
Vec<int> counter(NumInternal(), 0);
VecDH<int> counter(NumInternal(), 0);
// kernel over leaves to save internal Boxes
for_each_n(
policy, countAt(0), NumLeaves(),
BuildInternalBoxes({nodeBBox_, counter, nodeParent_, internalChildren_}));
BuildInternalBoxes({nodeBBox_.ptrD(), counter.ptrD(), nodeParent_.ptrD(),
internalChildren_.ptrD()}));
}
/**
@@ -370,22 +332,9 @@ bool Collider::Transform(glm::mat4x3 transform) {
return axisAligned;
}
template SparseIndices Collider::Collisions<true, false, Box>(
const VecView<const Box>&) const;
template SparseIndices Collider::Collisions<Box>(const VecDH<Box>&) const;
template SparseIndices Collider::Collisions<false, false, Box>(
const VecView<const Box>&) const;
template SparseIndices Collider::Collisions<false, false, glm::vec3>(
const VecView<const glm::vec3>&) const;
template SparseIndices Collider::Collisions<true, true, Box>(
const VecView<const Box>&) const;
template SparseIndices Collider::Collisions<false, true, Box>(
const VecView<const Box>&) const;
template SparseIndices Collider::Collisions<false, true, glm::vec3>(
const VecView<const glm::vec3>&) const;
template SparseIndices Collider::Collisions<glm::vec3>(
const VecDH<glm::vec3>&) const;
} // namespace manifold
+1 -13
View File
@@ -15,7 +15,7 @@
project (cross_section)
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
add_library(${PROJECT_NAME} ${SOURCE_FILES})
add_library(${PROJECT_NAME} OBJECT ${SOURCE_FILES})
target_include_directories(${PROJECT_NAME}
PUBLIC ${PROJECT_SOURCE_DIR}/include
@@ -26,15 +26,3 @@ target_link_libraries( ${PROJECT_NAME}
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
install(
TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT runtime
)
install(
DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
COMPONENT devel
)
+72 -17
View File
@@ -14,22 +14,27 @@
#pragma once
#include <functional>
#include <clipper2/clipper.h>
#include <memory>
#include <vector>
#include "clipper2/clipper.core.h"
#include "clipper2/clipper.offset.h"
#include "glm/ext/matrix_float3x2.hpp"
#include "glm/ext/vector_float2.hpp"
#include "public.h"
namespace C2 = Clipper2Lib;
namespace manifold {
class Rect;
/** @addtogroup Core
* @{
*/
struct PathImpl;
/**
* Two-dimensional cross sections guaranteed to be without self-intersections,
* or overlaps between polygons (from construction onwards). This class makes
@@ -72,7 +77,6 @@ class CrossSection {
FillRule fillrule = FillRule::Positive);
CrossSection(const Polygons& contours,
FillRule fillrule = FillRule::Positive);
CrossSection(const Rect& rect);
static CrossSection Square(const glm::vec2 dims, bool center = false);
static CrossSection Circle(float radius, int circularSegments = 0);
///@}
@@ -123,7 +127,7 @@ class CrossSection {
};
CrossSection Offset(double delta, JoinType jt, double miter_limit = 2.0,
int circularSegments = 0) const;
double arc_tolerance = 0.0) const;
///@}
/** @name Boolean
@@ -139,6 +143,7 @@ class CrossSection {
CrossSection& operator-=(const CrossSection&);
CrossSection operator^(const CrossSection&) const;
CrossSection& operator^=(const CrossSection&);
CrossSection RectClip(const Rect& rect) const;
///@}
/** @name Topological
@@ -148,15 +153,6 @@ class CrossSection {
std::vector<CrossSection> Decompose() const;
///@}
/** @name Convex Hulling
*/
///@{
CrossSection Hull() const;
static CrossSection Hull(const std::vector<CrossSection>& crossSections);
static CrossSection Hull(const SimplePolygon poly);
static CrossSection Hull(const Polygons polys);
///@}
///
/** @name Conversion
*/
///@{
@@ -164,10 +160,69 @@ class CrossSection {
///@}
private:
mutable std::shared_ptr<const PathImpl> paths_;
mutable std::shared_ptr<const C2::PathsD> paths_;
mutable glm::mat3x2 transform_ = glm::mat3x2(1.0f);
CrossSection(std::shared_ptr<const PathImpl> paths);
std::shared_ptr<const PathImpl> GetPaths() const;
CrossSection(C2::PathsD paths);
C2::PathsD GetPaths() const;
};
/** @} */
/** @addtogroup Connections
* @{
*/
/**
* Axis-aligned rectangular bounds.
*/
class Rect {
public:
glm::vec2 min = glm::vec2(0);
glm::vec2 max = glm::vec2(0);
/** @name Creation
* Constructors
*/
///@{
Rect();
~Rect();
Rect(const Rect& other);
Rect& operator=(const Rect& other);
Rect(Rect&&) noexcept;
Rect& operator=(Rect&&) noexcept;
Rect(const glm::vec2 a, const glm::vec2 b);
///@}
/** @name Information
* Details of the rectangle
*/
///@{
glm::vec2 Size() const;
float Scale() const;
glm::vec2 Center() const;
bool Contains(const glm::vec2& pt) const;
bool Contains(const Rect& other) const;
bool DoesOverlap(const Rect& other) const;
bool IsEmpty() const;
bool IsFinite() const;
///@}
/** @name Modification
*/
///@{
void Union(const glm::vec2 p);
Rect Union(const Rect& other) const;
Rect operator+(const glm::vec2 shift) const;
Rect& operator+=(const glm::vec2 shift);
Rect operator*(const glm::vec2 scale) const;
Rect& operator*=(const glm::vec2 scale);
Rect Transform(const glm::mat3x2& m) const;
///@}
/** @name Conversion
*/
///@{
CrossSection AsCrossSection() const;
///@}
};
/** @} */
} // namespace manifold
+213 -191
View File
@@ -14,22 +14,8 @@
#include "cross_section.h"
#include "clipper2/clipper.core.h"
#include "clipper2/clipper.h"
#include "clipper2/clipper.offset.h"
namespace C2 = Clipper2Lib;
using namespace manifold;
namespace manifold {
struct PathImpl {
PathImpl(const C2::PathsD paths_) : paths_(paths_) {}
operator const C2::PathsD&() const { return paths_; }
const C2::PathsD paths_;
};
} // namespace manifold
namespace {
const int precision_ = 8;
@@ -110,8 +96,8 @@ C2::PathsD transform(const C2::PathsD ps, const glm::mat3x2 m) {
return transformed;
}
std::shared_ptr<const PathImpl> shared_paths(const C2::PathsD& ps) {
return std::make_shared<const PathImpl>(ps);
std::shared_ptr<const C2::PathsD> shared_paths(const C2::PathsD& ps) {
return std::make_shared<const C2::PathsD>(ps);
}
// forward declaration for mutual recursion
@@ -146,58 +132,6 @@ void decompose_hole(const C2::PolyTreeD* outline,
}
}
bool V2Lesser(glm::vec2 a, glm::vec2 b) {
if (a.x == b.x) return a.y < b.y;
return a.x < b.x;
}
double Cross(glm::vec2 a, glm::vec2 b) { return a.x * b.y - a.y * b.x; }
double SquaredDistance(glm::vec2 a, glm::vec2 b) {
auto d = a - b;
return glm::dot(d, d);
}
bool IsCw(glm::vec2 a, glm::vec2 b, glm::vec2 c) {
double lhs = Cross(a - c, b - c);
double rhs = 1e-18 * SquaredDistance(a, c) * SquaredDistance(b, c);
return lhs * std::abs(lhs) <= rhs;
}
void HullBacktrack(const SimplePolygon& pts, const int idx,
std::vector<int>& keep, const int hold) {
const int stop = keep.size() - hold;
int i = 0;
while (i < stop && !IsCw(pts[idx], pts[keep[keep.size() - 1]],
pts[keep[keep.size() - 2]])) {
keep.pop_back();
i++;
}
}
// Based on method described here:
// https://www.hackerearth.com/practice/math/geometry/line-sweep-technique/tutorial/
C2::PathD HullImpl(SimplePolygon& pts) {
int len = pts.size();
if (len < 3) return C2::PathD(); // not enough points to create a polygon
std::sort(pts.begin(), pts.end(), V2Lesser);
auto keep = std::vector<int>{0, 1};
for (int i = 2; i < len; i++) {
HullBacktrack(pts, i, keep, 1);
keep.push_back(i);
}
int nLower = keep.size();
for (int i = 0; i < len - 1; i++) {
int idx = len - 2 - i;
HullBacktrack(pts, idx, keep, nLower);
if (idx > 0) keep.push_back(idx);
}
auto path = C2::PathD(keep.size());
for (int i = 0; i < keep.size(); i++) {
path[i] = v2_to_pd(pts[keep[i]]);
}
return path;
}
} // namespace
namespace manifold {
@@ -205,9 +139,7 @@ namespace manifold {
/**
* The default constructor is an empty cross-section (containing no contours).
*/
CrossSection::CrossSection() {
paths_ = std::make_shared<const PathImpl>(C2::PathsD());
}
CrossSection::CrossSection() { paths_ = shared_paths(C2::PathsD()); }
CrossSection::~CrossSection() = default;
CrossSection::CrossSection(CrossSection&&) noexcept = default;
@@ -234,7 +166,7 @@ CrossSection& CrossSection::operator=(const CrossSection& other) {
};
// Private, skips unioning.
CrossSection::CrossSection(std::shared_ptr<const PathImpl> ps) { paths_ = ps; }
CrossSection::CrossSection(C2::PathsD ps) { paths_ = shared_paths(ps); }
/**
* Create a 2d cross-section from a single contour. A boolean union operation
@@ -270,30 +202,16 @@ CrossSection::CrossSection(const Polygons& contours, FillRule fillrule) {
paths_ = shared_paths(C2::Union(ps, fr(fillrule), precision_));
}
/**
* Create a 2d cross-section from an axis-aligned rectangle (bounding box).
*
* @param rect An axis-aligned rectangular bounding box.
*/
CrossSection::CrossSection(const Rect& rect) {
C2::PathD p(4);
p[0] = C2::PointD(rect.min.x, rect.min.y);
p[1] = C2::PointD(rect.max.x, rect.min.y);
p[2] = C2::PointD(rect.max.x, rect.max.y);
p[3] = C2::PointD(rect.min.x, rect.max.y);
paths_ = shared_paths(C2::PathsD{p});
}
// Private
// All access to paths_ should be done through the GetPaths() method, which
// applies the accumulated transform_
std::shared_ptr<const PathImpl> CrossSection::GetPaths() const {
C2::PathsD CrossSection::GetPaths() const {
if (transform_ == glm::mat3x2(1.0f)) {
return paths_;
return *paths_;
}
paths_ = shared_paths(::transform(paths_->paths_, transform_));
paths_ = shared_paths(transform(*paths_, transform_));
transform_ = glm::mat3x2(1.0f);
return paths_;
return *paths_;
}
/**
@@ -325,7 +243,7 @@ CrossSection CrossSection::Square(const glm::vec2 size, bool center) {
p[2] = C2::PointD(x, y);
p[3] = C2::PointD(0.0, y);
}
return CrossSection(shared_paths(C2::PathsD{p}));
return CrossSection(C2::PathsD{p});
}
/**
@@ -346,7 +264,7 @@ CrossSection CrossSection::Circle(float radius, int circularSegments) {
for (int i = 0; i < n; ++i) {
circle[i] = C2::PointD(radius * cosd(dPhi * i), radius * sind(dPhi * i));
}
return CrossSection(shared_paths(C2::PathsD{circle}));
return CrossSection(C2::PathsD{circle});
}
/**
@@ -355,9 +273,9 @@ CrossSection CrossSection::Circle(float radius, int circularSegments) {
CrossSection CrossSection::Boolean(const CrossSection& second,
OpType op) const {
auto ct = cliptype_of_op(op);
auto res = C2::BooleanOp(ct, C2::FillRule::Positive, GetPaths()->paths_,
second.GetPaths()->paths_, precision_);
return CrossSection(shared_paths(res));
auto res = C2::BooleanOp(ct, C2::FillRule::Positive, GetPaths(),
second.GetPaths(), precision_);
return CrossSection(res);
}
/**
@@ -374,19 +292,19 @@ CrossSection CrossSection::BatchBoolean(
auto subjs = crossSections[0].GetPaths();
int n_clips = 0;
for (int i = 1; i < crossSections.size(); ++i) {
n_clips += crossSections[i].GetPaths()->paths_.size();
n_clips += crossSections[i].GetPaths().size();
}
auto clips = C2::PathsD();
clips.reserve(n_clips);
for (int i = 1; i < crossSections.size(); ++i) {
auto ps = crossSections[i].GetPaths();
clips.insert(clips.end(), ps->paths_.begin(), ps->paths_.end());
clips.insert(clips.end(), ps.begin(), ps.end());
}
auto ct = cliptype_of_op(op);
auto res = C2::BooleanOp(ct, C2::FillRule::Positive, subjs->paths_, clips,
precision_);
return CrossSection(shared_paths(res));
auto res =
C2::BooleanOp(ct, C2::FillRule::Positive, subjs, clips, precision_);
return CrossSection(res);
}
/**
@@ -457,7 +375,7 @@ std::vector<CrossSection> CrossSection::Decompose() const {
}
C2::PolyTreeD tree;
C2::BooleanOp(C2::ClipType::Union, C2::FillRule::Positive, GetPaths()->paths_,
C2::BooleanOp(C2::ClipType::Union, C2::FillRule::Positive, GetPaths(),
C2::PathsD(), tree, precision_);
auto polys = std::vector<C2::PathsD>();
@@ -467,12 +385,24 @@ std::vector<CrossSection> CrossSection::Decompose() const {
auto comps = std::vector<CrossSection>(n_polys);
// reverse the stack while wrapping
for (int i = 0; i < n_polys; ++i) {
comps[n_polys - i - 1] = CrossSection(shared_paths(polys[i]));
comps[n_polys - i - 1] = CrossSection(polys[i]);
}
return comps;
}
/**
* Compute the intersection between a cross-section and an axis-aligned
* rectangle. This operation has much higher performance (<B>O(n)</B> vs
* <B>O(n<SUP>3</SUP>)</B>) than the general purpose intersection algorithm
* used for sets of cross-sections.
*/
CrossSection CrossSection::RectClip(const Rect& rect) const {
auto r = C2::RectD(rect.min.x, rect.min.y, rect.max.x, rect.max.y);
auto ps = C2::ExecuteRectClip(r, GetPaths(), false, precision_);
return CrossSection(ps);
}
/**
* Move this CrossSection in space. This operation can be chained. Transforms
* are combined and applied lazily.
@@ -557,8 +487,8 @@ CrossSection CrossSection::Warp(
std::function<void(glm::vec2&)> warpFunc) const {
auto paths = GetPaths();
auto warped = C2::PathsD();
warped.reserve(paths->paths_.size());
for (auto path : paths->paths_) {
warped.reserve(paths.size());
for (auto path : paths) {
auto sz = path.size();
auto s = C2::PathD(sz);
for (int i = 0; i < sz; ++i) {
@@ -568,8 +498,7 @@ CrossSection CrossSection::Warp(
}
warped.push_back(s);
}
return CrossSection(
shared_paths(C2::Union(warped, C2::FillRule::Positive, precision_)));
return CrossSection(C2::Union(warped, C2::FillRule::Positive, precision_));
}
/**
@@ -585,8 +514,8 @@ CrossSection CrossSection::Warp(
* offseting operations are to be performed, which would compound the issue.
*/
CrossSection CrossSection::Simplify(double epsilon) const {
auto ps = SimplifyPaths(GetPaths()->paths_, epsilon, false);
return CrossSection(shared_paths(ps));
auto ps = SimplifyPaths(GetPaths(), epsilon, false);
return CrossSection(ps);
}
/**
@@ -604,101 +533,32 @@ CrossSection CrossSection::Simplify(double epsilon) const {
* minimum allowed). See the [Clipper2
* MiterLimit](http://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm)
* page for a visual example.
* @param circularSegments Number of segments per 360 degrees of
* <B>JoinType::Round</B> corners (roughly, the number of vertices that
* will be added to each contour). Default is calculated by the static Quality
* defaults according to the radius.
* @param arc_tolerance The maximum acceptable imperfection for curves drawn
* (approximated with line segments) for Round joins (not relevant for other
* JoinTypes). By default (when undefined or =0), the allowable imprecision is
* scaled in inverse proportion to the offset delta.
*/
CrossSection CrossSection::Offset(double delta, JoinType jointype,
double miter_limit,
int circularSegments) const {
double arc_tol = 0.;
if (jointype == JoinType::Round) {
int n = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(delta);
// This calculates tolerance as a function of circular segments and delta
// (radius) in order to get back the same number of segments in Clipper2:
// steps_per_360 = PI / acos(1 - arc_tol / abs_delta)
const double abs_delta = std::fabs(delta);
const double scaled_delta = abs_delta * std::pow(10, precision_);
arc_tol = (std::cos(Clipper2Lib::PI / n) - 1) * -scaled_delta;
}
double arc_tolerance) const {
auto ps =
C2::InflatePaths(GetPaths()->paths_, delta, jt(jointype),
C2::EndType::Polygon, miter_limit, precision_, arc_tol);
return CrossSection(shared_paths(ps));
}
/**
* Compute the convex hull enveloping a set of cross-sections.
*
* @param crossSections A vector of cross-sections over which to compute a
* convex hull.
*/
CrossSection CrossSection::Hull(
const std::vector<CrossSection>& crossSections) {
int n = 0;
for (auto cs : crossSections) n += cs.NumVert();
SimplePolygon pts;
pts.reserve(n);
for (auto cs : crossSections) {
auto paths = cs.GetPaths()->paths_;
for (auto path : paths) {
for (auto p : path) {
pts.push_back(v2_of_pd(p));
}
}
}
return CrossSection(shared_paths(C2::PathsD{HullImpl(pts)}));
}
/**
* Compute the convex hull of this cross-section.
*/
CrossSection CrossSection::Hull() const {
return Hull(std::vector<CrossSection>{*this});
}
/**
* Compute the convex hull of a set of points. If the given points are fewer
* than 3, an empty CrossSection will be returned.
*
* @param pts A vector of 2-dimensional points over which to compute a convex
* hull.
*/
CrossSection CrossSection::Hull(SimplePolygon pts) {
return CrossSection(shared_paths(C2::PathsD{HullImpl(pts)}));
}
/**
* Compute the convex hull of a set of points/polygons. If the given points are
* fewer than 3, an empty CrossSection will be returned.
*
* @param pts A vector of vectors of 2-dimensional points over which to compute
* a convex hull.
*/
CrossSection CrossSection::Hull(const Polygons polys) {
SimplePolygon pts;
for (auto poly : polys) {
for (auto p : poly) {
pts.push_back(p);
}
}
return Hull(pts);
C2::InflatePaths(GetPaths(), delta, jt(jointype), C2::EndType::Polygon,
miter_limit, precision_, arc_tolerance);
return CrossSection(ps);
}
/**
* Return the total area covered by complex polygons making up the
* CrossSection.
*/
double CrossSection::Area() const { return C2::Area(GetPaths()->paths_); }
double CrossSection::Area() const { return C2::Area(GetPaths()); }
/**
* Return the number of vertices in the CrossSection.
*/
int CrossSection::NumVert() const {
int n = 0;
auto paths = GetPaths()->paths_;
auto paths = GetPaths();
for (auto p : paths) {
n += p.size();
}
@@ -709,19 +569,19 @@ int CrossSection::NumVert() const {
* Return the number of contours (both outer and inner paths) in the
* CrossSection.
*/
int CrossSection::NumContour() const { return GetPaths()->paths_.size(); }
int CrossSection::NumContour() const { return GetPaths().size(); }
/**
* Does the CrossSection contain any contours?
*/
bool CrossSection::IsEmpty() const { return GetPaths()->paths_.empty(); }
bool CrossSection::IsEmpty() const { return GetPaths().empty(); }
/**
* Returns the axis-aligned bounding rectangle of all the CrossSections'
* vertices.
*/
Rect CrossSection::Bounds() const {
auto r = C2::GetBounds(GetPaths()->paths_);
auto r = C2::GetBounds(GetPaths());
return Rect({r.left, r.bottom}, {r.right, r.top});
}
@@ -730,7 +590,7 @@ Rect CrossSection::Bounds() const {
*/
Polygons CrossSection::ToPolygons() const {
auto polys = Polygons();
auto paths = GetPaths()->paths_;
auto paths = GetPaths();
polys.reserve(paths.size());
for (auto p : paths) {
auto sp = SimplePolygon();
@@ -742,4 +602,166 @@ Polygons CrossSection::ToPolygons() const {
}
return polys;
}
// Rect
/**
* Default constructor is an empty rectangle..
*/
Rect::Rect() {}
Rect::~Rect() = default;
Rect::Rect(Rect&&) noexcept = default;
Rect& Rect::operator=(Rect&&) noexcept = default;
Rect::Rect(const Rect& other) {
min = glm::vec2(other.min);
max = glm::vec2(other.max);
}
/**
* Create a rectangle that contains the two given points.
*/
Rect::Rect(const glm::vec2 a, const glm::vec2 b) {
min = glm::min(a, b);
max = glm::max(a, b);
}
/**
* Return the dimensions of the rectangle.
*/
glm::vec2 Rect::Size() const { return max - min; }
/**
* Returns the absolute-largest coordinate value of any contained
* point.
*/
float Rect::Scale() const {
glm::vec2 absMax = glm::max(glm::abs(min), glm::abs(max));
return glm::max(absMax.x, absMax.y);
}
/**
* Returns the center point of the rectangle.
*/
glm::vec2 Rect::Center() const { return 0.5f * (max + min); }
/**
* Does this rectangle contain (includes equal) the given point?
*/
bool Rect::Contains(const glm::vec2& p) const {
return glm::all(glm::greaterThanEqual(p, min)) &&
glm::all(glm::greaterThanEqual(max, p));
}
/**
* Does this rectangle contain (includes equal) the given rectangle?
*/
bool Rect::Contains(const Rect& rect) const {
return glm::all(glm::greaterThanEqual(rect.min, min)) &&
glm::all(glm::greaterThanEqual(max, rect.max));
}
/**
* Does this rectangle overlap the one given (including equality)?
*/
bool Rect::DoesOverlap(const Rect& rect) const {
return min.x <= rect.max.x && min.y <= rect.max.y && max.x >= rect.min.x &&
max.y >= rect.min.y;
}
/**
* Is the rectangle empty (containing no space)?
*/
bool Rect::IsEmpty() const { return max.y <= min.y || max.x <= min.x; };
/**
* Does this recangle have finite bounds?
*/
bool Rect::IsFinite() const {
return glm::all(glm::isfinite(min)) && glm::all(glm::isfinite(max));
}
/**
* Expand this rectangle (in place) to include the given point.
*/
void Rect::Union(const glm::vec2 p) {
min = glm::min(min, p);
max = glm::max(max, p);
}
/**
* Expand this rectangle to include the given Rect.
*/
Rect Rect::Union(const Rect& rect) const {
Rect out;
out.min = glm::min(min, rect.min);
out.max = glm::max(max, rect.max);
return out;
}
/**
* Shift this rectangle by the given vector.
*/
Rect Rect::operator+(const glm::vec2 shift) const {
Rect out;
out.min = min + shift;
out.max = max + shift;
return out;
}
/**
* Shift this rectangle in-place by the given vector.
*/
Rect& Rect::operator+=(const glm::vec2 shift) {
min += shift;
max += shift;
return *this;
}
/**
* Scale this rectangle by the given vector.
*/
Rect Rect::operator*(const glm::vec2 scale) const {
Rect out;
out.min = min * scale;
out.max = max * scale;
return out;
}
/**
* Scale this rectangle in-place by the given vector.
*/
Rect& Rect::operator*=(const glm::vec2 scale) {
min *= scale;
max *= scale;
return *this;
}
/**
* Transform the rectangle by the given axis-aligned affine transform.
*
* Ensure the transform passed in is axis-aligned (rotations are all
* multiples of 90 degrees), or else the resulting rectangle will no longer
* bound properly.
*/
Rect Rect::Transform(const glm::mat3x2& m) const {
Rect rect;
rect.min = m * glm::vec3(min, 1);
rect.max = m * glm::vec3(max, 1);
return rect;
}
/**
* Return a CrossSection with an outline defined by this axis-aligned
* rectangle.
*/
CrossSection Rect::AsCrossSection() const {
SimplePolygon p(4);
p[0] = glm::vec2(min.x, max.y);
p[1] = glm::vec2(max.x, max.y);
p[2] = glm::vec2(max.x, min.y);
p[3] = glm::vec2(min.x, min.y);
return CrossSection(p);
}
} // namespace manifold
+6 -15
View File
@@ -17,26 +17,17 @@ project (manifold)
file(GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS *.cpp)
add_library(${PROJECT_NAME} ${SOURCE_FILES})
if(MANIFOLD_USE_CUDA)
set_source_files_properties(${SOURCE_FILES} PROPERTIES LANGUAGE CUDA)
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
endif()
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_link_libraries(${PROJECT_NAME}
PUBLIC utilities cross_section
PRIVATE collider polygon ${MANIFOLD_INCLUDE} Clipper2 quickhull
PRIVATE collider polygon ${MANIFOLD_INCLUDE} graphlite Clipper2
)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME}
PUBLIC cxx_std_17
)
install(
TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
COMPONENT runtime
)
install(
DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/manifold
COMPONENT devel
)

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