Compare commits

..
Author SHA1 Message Date
pca006132 311d25a8cf fixes 2023-07-23 14:34:42 +08:00
pca006132 6fa0e7becb make to_mesh accept MeshGL 2023-07-21 13:33:35 +08:00
pca006132 e425761d8e fix according to comments 2023-07-21 12:56:06 +08:00
pca006132 a4cc9ee54b fix hsplit 2023-07-20 00:52:56 +08:00
pca006132 2a7f9cf48f clean up array related stuff 2023-07-20 00:48:23 +08:00
pca006132 99740dafcb Merge remote-tracking branch 'upstream/master' into pythonMeshGL 2023-07-19 22:40:48 +08:00
pca006132 9157350a11 updated python meshgl 2023-07-19 22:40:27 +08:00
pca006132 a8626f907c please gcc (#496)
* please gcc

* added documentation for mallocManaged
2023-07-19 10:58:28 +08:00
Geoff deRosenroll 2f22b92352 3d Convex Hull via Akuukka QuickHull (#489)
Add convex hull methods to Manifold using newly vendored https://github.com/akuukka/quickhull
2023-07-17 17:09:59 -07:00
Stéphane GINIER a3d7991563 Optimize sort_by_key (SortFaces/SortVerts) (#494)
* Optimize sort_by_key (SortFaces/SortVerts)

Remap boxes separately

* Fix build validation
2023-07-17 10:36:26 +08:00
Stéphane GINIER 2aef6e4728 Optimise SortEntry memory footprint (#493) 2023-07-15 22:21:26 +08:00
pca006132 c167a48d36 optimization in #483 (#491)
* remove extra prefetch

this is in the hot path and causes performance issues

* implemented optimization proposed by @stephomi in #483

* formatting

* fix cuda build

* combine the loop
2023-07-15 19:01:32 +08:00
Emmett Lalish f6cf8d23d3 updated python mesh 2023-07-14 23:01:28 -07:00
Peter Harman da3f323113 (C bindings) Pass additional data to level set SDF function (#427)
Allows a developer to pass some context to the level set function in the C FFI bindings, such as a pointer to an object in the host language, so that parallel calls can be threadsafe.
2023-07-13 12:50:40 -07:00
Emmett Lalish 8b7bf4231b updated python doc strings (#488)
* updated doc strings

* pymanifold -> manifold3d
2023-07-13 11:34:31 -07:00
Stéphane GINIER ac2cfd857f Replace map with unordered_map (#484)
Replace map usage with vector
2023-07-13 07:15:08 -07:00
pca006132 285bb0754b fix nixos js build (#473)
* fix nix js build

* fix js CrossSection::rotate binding

* add wasm hull operation
2023-07-12 12:03:19 +08:00
Geoff deRosenroll ba5fead6d5 Add 2d hull operation (#486)
This adds a 2d convex hull operation to CrossSection (ported from OCADml, which I ported based on BOSL2).
2023-07-10 23:43:47 -07:00
Emmett Lalish 96c57c0591 Fix Merge manifoldness issues (#481)
* test passes

* fixed merge properly

* fixed relation

* fixed pinched verts

* added WASM debug instructions
2023-07-07 09:40:10 -07:00
Emmett Lalish 4b65ff3afe Fix operations with different numbers of properties (#480)
* added tests and fixed a bug

* more testing, fixed compose
2023-07-06 13:12:33 -07:00
Geoff deRosenroll 709021d9d2 Remove RectClip API + Rect::AsCrossSection fix (#478)
Following discussion on #455, this removes RectClip from our API, as it does not provide the same guarantees that Clipper2's regular boolean clipping operations give us (that make sure that CrossSections can be extruded successfully).

Also, this fixes AsCrossSection which was returning empty CrossSections due to incorrect winding followed by a Positive fill rule union operation. Now it is working as advertised, while skipping over the needless clipping by implementing it as a CrossSection constructor (building a PathsD directly).
2023-07-05 10:41:03 -07:00
John Collins 6cec951aed Partial Revolve (#455)
* handle verts on revolve axis

* Tests working

* Const declares, test cycle start vert

* clang-format

* clang-format test code

* fix header declaration

* WASM bindings

* format bindings.js

* clang format

* Address comments
2023-07-04 15:52:48 -07:00
Emmett Lalish dd3445ddb1 fixed WASM segfault (#474) 2023-06-30 08:55:02 -07:00
Axel Angel 37db0d0182 python bindings: mirror and Mesh with 2 args (#459)
* python bindings: Add mirror

* python bindings: fix Mesh constructor optional arguments
2023-06-27 07:32:44 -07:00
Emmett Lalish 1b9243df2a Update HTML examples for WASM API changes (#470)
updated for WASM API changes
2023-06-25 20:40:16 -07:00
Emmett Lalish 8b4c79c485 added Fill Rule test (#464) 2023-06-22 16:04:19 -07:00
Geoff deRosenroll 7a197af83d Update Clipper2 to latest commit (#463)
* Update to latest clipper2 commit

* Update RectClip binding - rename and dropped param
2023-06-22 15:02:05 -07:00
Geoff deRosenroll a65fedb2f9 Clean up CrossSection constructor in pymanifold (#461)
Remove vertices intermediary and {vertices} assignment
2023-06-21 14:33:13 -07:00
Geoff deRosenroll 6ae9dee640 Workaround assimp exporting empty 3mfs (#454)
Workaround assimp producing meshless 3mf files by nesting the node holding the mesh into a child of the root node if the extension is .3mf
2023-06-15 10:45:01 -07:00
Geoff deRosenroll 79daba1200 Fix oversized malloc error (gcc inlining related) (#453)
Fixes #450

This breaks the allocation size calculation in ManagedVec::shrink_to_fit onto a separate line in order to work around spooky inlining (I think) by GCC that trips up an excessive allocation compilation error in malloc.
2023-06-15 10:42:49 -07:00
Geoff deRosenroll 2f8e3174f7 Update C bindings to curvature and set properites additions (#451)
Add C bindings to `Manifold::ReserveIDs`, `Manifold::SetProperties`, and `Manifold::CalculateCurvature`.
2023-06-14 23:08:53 -07:00
Emmett Lalish cce35aa5a0 Fix RemoveIfFolded() in the decimator (#447)
* enabled test

* remove test

* test infinite loops

* test infinite loops

* reduced test

* update volume

* fixed RemoveIfFolded

* rename test

* allow degenerates
2023-06-10 16:44:25 -07:00
Emmett Lalish e727b115c0 Change curvature API (#448)
* new curvature API compiles

* tests pass

* update JS example and fix bindings
2023-06-09 22:36:15 -07:00
Johnny Bigert f6431112a8 Update Python bindings (#445)
* Added more Python bindings (CrossSection) and some clarifying Python examples on how to create polygons etc
* Returned extrude from Python example and renamed accordingly.
* Removed PolygonWrapper class from Python bindings
2023-06-08 10:52:10 -07:00
Geoff deRosenroll 2643c291ed Use quality globals instead of arc tolerance for offset (#444) 2023-06-01 11:52:55 -07:00
Emmett Lalish e024edf103 ManifoldCAD docs update (#446)
* fix back compat

* updated docs

* Revert "fix back compat"

This reverts commit 5f102e567577ab8fcd7f2215801f5dcea9d13e8e.
2023-06-01 10:31:05 -07:00
Geoff deRosenroll cbfeed1672 Update JS bindings with CrossSection + reorganize (#440)
- add CrossSection class
- move static method (constructors) under their respective classes
-add Manifold.{split, splitByPlane}
- enable usage of instanceof on Manifold (and now CrossSection) objects simplify registry indirection in worker.ts, enabling method de-structuring (opening static function names out of class module name-spaces)

Build changes:
- break C++ helpers out from bindings.cpp to helpers.cpp
- ensure that ts files are copied during builds even when nothing is recompiled

Note that in addition to the new bindings, the most of the existing toplevel functions have been moved under their respective classes as static methods (breaking change!). I haven't added any new examples yet, but the existing ones are passing and do make use of CrossSection now (some transparently, and some directly), but I think enough of this is together that review is worthwhile.
2023-06-01 00:31:57 -07:00
Emmett Lalish 9dac47a76a Fix editor warp crash (#442)
* reset errored worker

* catch non-finite warp errors
2023-05-26 22:37:10 -07:00
Emmett Lalish eb41b9d36a Add CI check for formatting more languages (#441)
* add format files

* some fixes
2023-05-26 08:19:02 -07:00
Emmett Lalish 72935213e8 Editor scene (#438)
* SetProperties working

* cleanup

* added Node

* switched to Vite

* scene export working

* vertex color output works

* added unlit

* fixed rename bug

* switch worker to TS

* don't fail on empty manifolds

* reusing meshes

* cleanup, updated gyroid example

* change signature of setPropFunc

* updated examples

* add dist for githack testing

* Revert "add dist for githack testing"

This reverts commit 8769d7ab08dfaab9fe7824726f7a7229bf553bb3.

* remove duplicate files

* updated docs

* fixed some bugs

* multi-material working

* switched to vitest

* tests pass

* fixed examples

* fix CI

* update deployment

* tweak logging

* cleanup

* fix copy

* updated READMEs

* extend timeout
2023-05-25 09:40:02 -07:00
Geoff deRosenroll e1ebc66425 (C) Only link MeshIO when MANIFOLD_EXPORT is on (#436)
Only link MeshIO in C bindings when `MANIFOLD_EXPORT` is defined. MeshIO related code has been better consolidated to limit the number of `#ifdef` blocks required.
2023-05-21 00:14:36 -07:00
Emmett Lalish ef52b0cce2 Add SetProperties() (#433)
* SetProperties working

* cleanup
2023-05-16 21:22:49 -07:00
Geoff deRosenroll 812ca5c30f Add manifold_import_meshgl (#434)
Adds missing C binding to MeshIO::ImportMesh
2023-05-16 16:48:40 -07:00
Emmett Lalish f064b2a5fd make-manifold (#428)
* make-manifold working

* prettier

* fixed stack exceeded

* download only manifolds

* keep any meshes that are manifold

* fixed extension accessor writer

* fix read-write roundtrip
2023-05-15 07:50:28 -07:00
pca006132 18abd3ffcf fix compose index error (#430)
* fix compose index error

https://github.com/elalish/manifold/issues/425#issuecomment-1540356917

* simplify test case
2023-05-13 00:36:07 +08:00
Loosetooth 1615b72e83 add unionError test case (#429) 2023-05-11 08:06:09 -07:00
Emmett Lalish 58fb7e3300 EXT_manifold for glTF (#420)
* extension export works

* reading GLBs

* trying to merge documents

* add await

* removed extra materials

* added builds

* several fixes

* fix versions

* move to built

* fixed test.js

* updated README

* disable nix JS test for now

* fix codecov timeout

* fix test
2023-05-04 20:49:39 -07:00
David Braun 1959a30560 Fix bug in three.html example. (#419)
Fix bug in three.html.

The bug didn't change the output but made it harder for newcomers to learn the JavaScript API.
2023-04-21 15:24:11 -07:00
Emmett Lalish 4bdb761d5e fixed warp (#418)
* fixed warp

* fix pi

* ease up test tolerance

* improve cbind test

* fix build

* better bind testing
2023-04-21 17:02:16 +08:00
Emmett Lalish ba042ee8db fix warp simplification (#416)
* fixed warp simplification

* fix test
2023-04-20 11:52:47 -07:00
Emmett Lalish 97317888bc added global coplanarity check (#408)
* added global coplanarity check

* fix early-out

* added test, fixed precision

* one more precision
2023-04-12 08:03:47 -07:00
Emmett Lalish a9bf836574 Added file conversion and debug utility (#407)
* added file conversion utility

* added verbose output

* fix build
2023-04-11 11:38:35 -07:00
Emmett Lalish 29dc753513 Fix triangulator assertion (#406)
fixed debug check
2023-04-11 10:10:17 -07:00
Emmett Lalish 3a517a490e Update polygon tests (#405)
* simplify polygon tests

* fix polygon Dump output

* updated docs

* removed IsManifold
2023-04-11 09:10:34 -07:00
Emmett Lalish aeba26760b Add Merge function (#394)
* start Merge

* started Merge function

* added collider

* refactor now builds

* connect vectors

* finished Merge, updated Collider

* added tests

* added sorting, updated graph_lite

* revert submodule change

* fixed indexing

* fixed prop dedupe bug

* fixed test

* update meshIO linking

* fix MSVC

* use assimp on Mac

* add precision handling

* improved tests

* revert test tweak

* update collider

* clean up multiset

* updated docs
2023-04-09 17:16:40 -07:00
pca006132 85d74b4ccf Flip triangles in csg_tree (#404)
* flip triangles in csg_tree

* added test
2023-04-08 01:11:56 +08:00
Jiří Hon a5f92e2522 fix typo in typescript types (#401) 2023-04-04 13:28:43 -07:00
109 changed files with 10528 additions and 6340 deletions
+1 -1
View File
@@ -16,6 +16,6 @@ jobs:
with:
source: '.'
exclude: '*/third_party'
extensions: 'h,cpp'
extensions: 'h,cpp,js,ts,html'
clangFormatVersion: 12
+1 -2
View File
@@ -20,9 +20,8 @@ jobs:
mkdir public
mv ./samples ./public
mv ./docs ./public
mv ./bindings/wasm/examples/* ./public
- name: Download WASM files
- name: Download built examples
uses: dawidd6/action-download-artifact@v2
with:
workflow: manifold.yml
+15 -15
View File
@@ -12,7 +12,7 @@ on:
jobs:
build:
timeout-minutes: 30
timeout-minutes: 45
strategy:
matrix:
cuda_support: [ON, OFF]
@@ -25,13 +25,14 @@ jobs:
- name: Install dependencies
run: |
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
DEBIAN_FRONTEND=noninteractive apt install -y libomp-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip lcov
pip install trimesh
- uses: actions/checkout@v3
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v1.12
- name: Build ${{matrix.backend}}
if: matrix.parallel_backend != 'NONE' || matrix.cuda_support != 'OFF'
run: |
mkdir build
cd build
@@ -39,6 +40,7 @@ jobs:
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
# note that the test for CUDA backend does not really test CUDA, as we
# don't have CUDA GPU on GitHub Action
if: matrix.parallel_backend != 'NONE' || matrix.cuda_support != 'OFF'
run: |
export PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python
cd build/test
@@ -51,6 +53,7 @@ jobs:
# perhaps issue related to invalid memory access?
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}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} -DCODE_COVERAGE=ON .. && make
lcov --capture --initial --directory . --output-file ./code_coverage_init.info
@@ -77,7 +80,7 @@ jobs:
- name: Install dependencies
run: |
apt-get -y update
DEBIAN_FRONTEND=noninteractive apt install -y libomp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip
DEBIAN_FRONTEND=noninteractive apt install -y libomp-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip
- uses: actions/checkout@v3
with:
submodules: recursive
@@ -124,19 +127,17 @@ jobs:
run: |
cd build/test
node ./manifold_test.js
cd ../bindings/wasm
npm install
- name: Test examples
run: |
cd bindings/wasm/examples
npm ci
npm run build
npm test
- name: Upload WASM files
uses: actions/upload-artifact@v3
with:
name: wasm
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
path: bindings/wasm/examples/dist/
retention-days: 90
build_windows:
@@ -193,7 +194,7 @@ jobs:
steps:
- name: Install common dependencies
run: |
brew install pkg-config
brew install pkg-config assimp
pip install trimesh
- name: Install OpenMP
if: matrix.parallel_backend == 'OMP'
@@ -209,7 +210,7 @@ jobs:
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
- name: Test
run: |
cd build/test
@@ -227,5 +228,4 @@ jobs:
with:
submodules: recursive
- uses: cachix/install-nix-action@v15
- run: |
nix build -L '.?submodules=1#manifold-${{matrix.variant}}'
- run: nix build -L '.?submodules=1#manifold-${{matrix.variant}}'
+7 -1
View File
@@ -109,8 +109,14 @@ 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
.cache
# PyCharm
.idea
+3 -3
View File
@@ -14,6 +14,6 @@
[submodule "src/third_party/clipper2"]
path = src/third_party/clipper2
url = https://github.com/AngusJohnson/clipper2
[submodule "meshIO/third_party/assimp"]
path = meshIO/third_party/assimp
url = https://github.com/assimp/assimp.git
[submodule "src/third_party/quickhull"]
path = src/third_party/quickhull
url = https://github.com/akuukka/quickhull
+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": [
// 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": [
{
"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"
}
]
"name": "MALLOC_CHECK_",
"value": "2"
}
]
]
}
]
}
+13 -1
View File
@@ -98,12 +98,23 @@
"concepts": "cpp",
"queue": "cpp",
"stack": "cpp",
"__hash_table": "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"
},
"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",
@@ -120,6 +131,7 @@
"editor.detectIndentation": false,
"cSpell.words": [
"Bary",
"Coplanarity",
"csaszar",
"Gyroid",
"halfedge",
-4
View File
@@ -41,10 +41,6 @@ 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(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
+17 -9
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:
@@ -15,15 +15,23 @@ If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid mo
![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.
We have four core dependencies, making use of submodules to ensure compatibility:
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
@@ -31,7 +39,7 @@ We have four core dependencies, making use of submodules to ensure compatibility
## 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.
@@ -41,7 +49,7 @@ Look in the [samples](https://github.com/elalish/manifold/tree/master/samples) d
## 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):
```
@@ -56,7 +64,7 @@ test/manifold_test
CMake flags (usage e.g. `-DMANIFOLD_USE_CUDA=ON`):
- `MANIFOLD_USE_CUDA=[<OFF>, ON]`: Provides Nvidia GPU parallelization, requires CUDA Toolkit and its nvcc compiler.
- `MANIFOLD_PAR=[<NONE>, OMP, TBB]`: Provides multi-thread parallelization, requires `libomp-dev` or `libtbb-dev`.
- `MANIFOLD_EXPORT=[<OFF>, ON]`: Enables GLB export of 3D models from the tests, uses `assimp`.
- `MANIFOLD_EXPORT=[<OFF>, ON]`: Enables GLB export of 3D models from the tests, requires `libassimp-dev`.
- `MANIFOLD_DEBUG=[<OFF>, ON]`: Enables internal assertions and exceptions.
- `BUILD_TEST_CGAL=[<OFF>, ON]`: Builds a CGAL-based performance [comparison](https://github.com/elalish/manifold/tree/master/extras), requires `libcgal-dev`.
@@ -90,7 +98,7 @@ node test/manifold_test.js
### Python
The CMake script will build the python binding `pymanifold` automatically. To
The CMake script will build the python binding `manifold3d` 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:
@@ -104,8 +112,8 @@ Run the following code in the interpreter for
python binding documentation:
```
>>> import pymanifold
>>> help(pymanifold)
>>> import manifold3d
>>> help(manifold3d)
```
For more detailed documentation, please refer to the C++ API.
+12 -8
View File
@@ -1,18 +1,22 @@
project(manifoldc)
add_library(manifoldc manifoldc.cpp conv.cpp)
add_library(${PROJECT_NAME} manifoldc.cpp conv.cpp)
if(MANIFOLD_USE_CUDA)
set_source_files_properties(manifoldc.cpp conv.cpp PROPERTIES LANGUAGE CUDA)
set_property(TARGET manifoldc PROPERTY CUDA_ARCHITECTURES 61)
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)
endif()
target_link_libraries(
manifoldc
PRIVATE manifold sdf meshIO graphlite cross_section
${PROJECT_NAME}
PRIVATE manifold sdf graphlite cross_section
)
target_include_directories(manifoldc PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_compile_options(manifoldc PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(manifoldc PRIVATE cxx_std_17)
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)
-25
View File
@@ -1,6 +1,5 @@
#include <conv.h>
#include <manifold.h>
#include <meshIO.h>
#include <sdf.h>
#include <vector>
@@ -42,10 +41,6 @@ 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) {
@@ -111,14 +106,6 @@ 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,10 +148,6 @@ 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) {
@@ -221,14 +204,6 @@ 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); }
+2 -9
View File
@@ -106,13 +106,6 @@ ManifoldCrossSection *manifold_cross_section_intersection(
return to_c(new (mem) CrossSection(cs));
}
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,
ManifoldCrossSection *cs,
float x, float y) {
@@ -169,9 +162,9 @@ ManifoldCrossSection *manifold_cross_section_simplify(void *mem,
ManifoldCrossSection *manifold_cross_section_offset(
void *mem, ManifoldCrossSection *cs, double delta, ManifoldJoinType jt,
double miter_limit, double arc_tolerance) {
double miter_limit, int circular_segments) {
auto offset =
from_c(cs)->Offset(delta, from_c(jt), miter_limit, arc_tolerance);
from_c(cs)->Offset(delta, from_c(jt), miter_limit, circular_segments);
return to_c(new (mem) CrossSection(offset));
}
-7
View File
@@ -1,6 +1,5 @@
#pragma once
#include <manifold.h>
#include <meshIO.h>
#include <public.h>
#include <sdf.h>
#include <types.h>
@@ -22,10 +21,7 @@ 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);
@@ -44,9 +40,6 @@ 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);
+56 -43
View File
@@ -3,6 +3,7 @@
#include <types.h>
#ifdef __cplusplus
#include <cstdint>
extern "C" {
#endif
@@ -48,6 +49,15 @@ 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
@@ -136,13 +146,14 @@ 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
@@ -192,9 +203,6 @@ ManifoldCrossSection *manifold_cross_section_difference(
void *mem, ManifoldCrossSection *a, ManifoldCrossSection *b);
ManifoldCrossSection *manifold_cross_section_intersection(
void *mem, ManifoldCrossSection *a, ManifoldCrossSection *b);
ManifoldCrossSection *manifold_cross_section_rect_clip(void *mem,
ManifoldCrossSection *cs,
ManifoldRect *r);
// CrossSection Transformation
@@ -222,7 +230,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, double arc_tolerance);
double miter_limit, int circular_segments);
// CrossSection Info
@@ -310,7 +318,43 @@ 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);
@@ -324,49 +368,18 @@ 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
}
+8 -7
View File
@@ -1,6 +1,8 @@
#pragma once
#include <stddef.h>
// opaque pointers
typedef struct ManifoldManifold ManifoldManifold;
typedef struct ManifoldManifoldVec ManifoldManifoldVec;
typedef struct ManifoldCrossSection ManifoldCrossSection;
@@ -9,11 +11,15 @@ 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;
@@ -49,12 +55,7 @@ typedef struct ManifoldProperties {
float volume;
} ManifoldProperties;
typedef struct ManifoldCurvatureBounds {
float max_mean_curvature;
float min_mean_curvature;
float max_gaussian_curvature;
float min_gaussian_curvature;
} ManifoldCurvatureBounds;
// enums
typedef enum ManifoldOpType {
MANIFOLD_ADD,
+47 -25
View File
@@ -2,7 +2,6 @@
#include <cross_section.h>
#include <manifold.h>
#include <manifoldc.h>
#include <meshIO.h>
#include <public.h>
#include <sdf.h>
@@ -12,10 +11,13 @@
#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 {
@@ -31,6 +33,20 @@ ManifoldMeshGL *level_set(void *mem, float (*sdf)(float, float, float),
auto mesh = LevelSet(fun, *from_c(bounds), edge_length, level, pol);
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 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
#ifdef __cplusplus
@@ -231,6 +247,18 @@ 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));
@@ -446,27 +474,26 @@ 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));
}
ManifoldCurvatureBounds manifold_curvature_bounds(ManifoldCurvature *curv) {
auto c = *from_c(curv);
return {c.maxMeanCurvature, c.minMeanCurvature, c.maxGaussianCurvature,
c.minGaussianCurvature};
}
uint32_t manifold_reserve_ids(uint32_t n) { return Manifold::ReserveIDs(n); }
size_t manifold_curvature_vert_length(ManifoldCurvature *curv) {
return from_c(curv)->vertMeanCurvature.size();
}
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));
};
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);
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));
}
// Static Quality Globals
@@ -500,7 +527,6 @@ 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) {
@@ -520,7 +546,6 @@ 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) {
@@ -540,9 +565,6 @@ 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
}
+32
View File
@@ -2,10 +2,32 @@
#include <conv.h>
#include "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);
}
#ifdef __cplusplus
extern "C" {
#endif
// material
ManifoldMaterial *manifold_material(void *mem) {
return to_c(new (mem) manifold::Material());
}
@@ -27,6 +49,8 @@ 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());
}
@@ -41,11 +65,19 @@ 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); }
+5 -5
View File
@@ -15,10 +15,10 @@
project(python)
add_subdirectory(third_party)
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
pybind11_add_module(manifold3d manifold3d.cpp)
target_link_libraries(manifold3d PRIVATE manifold)
target_compile_options(manifold3d PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(manifold3d PUBLIC cxx_std_17)
target_include_directories(manifold3d
PRIVATE ${PYBIND11_DIR}/include
)
+1 -1
View File
@@ -14,7 +14,7 @@
limitations under the License.
"""
from pymanifold import Manifold
from manifold3d import Manifold
# https://gist.github.com/deckar01/ef11def51de7e71d9f288c6e5819fdb7
+1 -1
View File
@@ -14,7 +14,7 @@
limitations under the License.
"""
from pymanifold import Manifold
from manifold3d import Manifold
from functools import reduce
# https://gist.github.com/ochafik/2db96400e3c1f73558fcede990b8a355#file-cube-with-half-spheres-dents-scad
+38
View File
@@ -0,0 +1,38 @@
from manifold3d import CrossSection, FillRule
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=} differs from {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=} differs from {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=} differs from {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=} differs from {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 pymanifold import Manifold
from manifold3d import Manifold
import numpy as np
n = 10
+6 -2
View File
@@ -18,6 +18,7 @@ import pathlib
import sys
import importlib
import trimesh
import numpy as np
from time import time
if __name__ == "__main__":
@@ -34,8 +35,11 @@ if __name__ == "__main__":
model = module.run()
mesh = model.to_mesh()
if export_models:
meshOut = trimesh.Trimesh(
vertices=mesh.vert_pos, faces=mesh.tri_verts)
if mesh.vert_properties.shape[1] > 3:
vertices = np.hsplit(mesh.vert_properties, 3)[0]
else:
vertices = mesh.vert_properties
meshOut = trimesh.Trimesh(vertices=vertices, faces=mesh.tri_verts)
trimesh.exchange.export.export_mesh(meshOut, f'{f}.glb', 'glb')
print(f'Exported model to {f}.glb')
t1 = time()
+1 -1
View File
@@ -1,4 +1,4 @@
from pymanifold import Manifold
from manifold3d import Manifold
def run():
# for some reason this causes collider error
@@ -27,16 +27,19 @@ using namespace manifold;
typedef std::tuple<float, float> Float2;
typedef std::tuple<float, float, float> Float3;
struct PolygonsWrapper {
std::unique_ptr<Polygons> polygons;
};
constexpr auto pyArrayFlags = py::array::c_style | py::array::forcecast;
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.";
PYBIND11_MAKE_OPAQUE(std::vector<int>);
PYBIND11_MAKE_OPAQUE(std::vector<uint32_t>);
PYBIND11_MAKE_OPAQUE(std::vector<float>);
template <typename T>
std::vector<T> toVector(const py::array_t<T> &arr) {
return std::vector<T>(arr.data(), arr.data() + arr.size());
}
PYBIND11_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 "
@@ -44,7 +47,7 @@ PYBIND11_MODULE(pymanifold, m) {
"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 "
":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.");
@@ -55,7 +58,7 @@ PYBIND11_MODULE(pymanifold, m) {
"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 "
":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,
@@ -64,15 +67,15 @@ PYBIND11_MODULE(pymanifold, m) {
"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 "
":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");
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<>())
@@ -95,10 +98,10 @@ PYBIND11_MODULE(pymanifold, m) {
.def(py::self ^ py::self, "Boolean intersection.")
.def(
"transform",
[](Manifold self, py::array_t<float> &mat) {
[](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");
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++) {
@@ -116,7 +119,7 @@ PYBIND11_MODULE(pymanifold, m) {
":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) {
[](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,
@@ -128,9 +131,9 @@ PYBIND11_MODULE(pymanifold, m) {
":param z: Z axis translation. (default 0.0).")
.def(
"translate",
[](Manifold self, py::array_t<float> &t) {
[](Manifold &self, py::array_t<float> &t) {
auto t_view = t.unchecked<1>();
if (t.shape(0) != 3)
if (t.ndim() != 1 || 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)));
},
@@ -139,23 +142,23 @@ PYBIND11_MODULE(pymanifold, m) {
"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) {
[](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");
if (scale.ndim() != 1 || scale.shape(0) != 3)
throw std::runtime_error("Invalid vector shape, expected (3)");
glm::vec3 v(scale_view(0), scale_view(1), scale_view(2));
return self.Scale(v);
},
@@ -164,12 +167,26 @@ PYBIND11_MODULE(pymanifold, m) {
"Transforms are combined and applied lazily."
"\n\n"
":param v: The vector to multiply every vertex by component.")
.def(
"mirror",
[](Manifold &self, py::array_t<float> &mirror) {
auto v_view = mirror.unchecked<1>();
if (mirror.ndim() != 1 || mirror.shape(0) != 3)
throw std::runtime_error("Invalid vector shape, expected (3)");
glm::vec3 v(v_view(0), v_view(1), v_view(2));
return self.Mirror(v);
},
py::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, py::array_t<float> &v) {
[](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");
if (v.ndim() != 1 || v.shape(0) != 3)
throw std::runtime_error("Invalid vector shape, expected (3)");
return self.Rotate(v_view(0), v_view(1), v_view(2));
},
py::arg("v"),
@@ -184,7 +201,7 @@ PYBIND11_MODULE(pymanifold, m) {
":param v: [X, Y, Z] rotation in degrees.")
.def(
"rotate",
[](Manifold self, float xDegrees = 0.0f, float yDegrees = 0.0f,
[](Manifold &self, float xDegrees = 0.0f, float yDegrees = 0.0f,
float zDegrees = 0.0f) {
return self.Rotate(xDegrees, yDegrees, zDegrees);
},
@@ -203,7 +220,7 @@ PYBIND11_MODULE(pymanifold, m) {
":param z: Z rotation in degrees. (default 0.0).")
.def(
"warp",
[](Manifold self, const std::function<Float3(Float3)> &f) {
[](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);
@@ -211,9 +228,16 @@ PYBIND11_MODULE(pymanifold, m) {
v.z = std::get<2>(fv);
});
},
py::arg("f"))
py::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(
"refine", [](Manifold self, int n) { return self.Refine(n); },
"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 "
@@ -225,7 +249,34 @@ PYBIND11_MODULE(pymanifold, m) {
"\n"
":param n: The number of pieces to split every edge into. Must be > "
"1.")
.def("to_mesh", &Manifold::GetMesh)
.def(
"to_mesh",
[](Manifold &self, std::optional<py::array_t<uint32_t>> &normalIdx) {
glm::ivec3 v(0);
if (normalIdx.has_value()) {
if (normalIdx.value().ndim() != 1 ||
normalIdx.value().shape(0) != 3)
throw std::runtime_error("Invalid vector shape, expected (3)");
auto normalIdx_view = normalIdx.value().unchecked<1>();
v = glm::ivec3(normalIdx_view(0), normalIdx_view(1),
normalIdx_view(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.",
py::arg("normalIdx") = py::none())
.def("num_vert", &Manifold::NumVert,
"The number of vertices in the Manifold.")
.def("num_edge", &Manifold::NumEdge,
@@ -254,12 +305,12 @@ PYBIND11_MODULE(pymanifold, m) {
"first.")
.def(
"get_volume",
[](Manifold self) { return self.GetProperties().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; },
[](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,
@@ -275,8 +326,8 @@ PYBIND11_MODULE(pymanifold, m) {
.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 "
"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().")
@@ -286,7 +337,7 @@ PYBIND11_MODULE(pymanifold, m) {
"is more efficient than doing them separately.")
.def(
"split_by_plane",
[](Manifold self, Float3 normal, float originOffset) {
[](Manifold &self, Float3 normal, float originOffset) {
return self.SplitByPlane(
{std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)},
originOffset);
@@ -294,14 +345,14 @@ PYBIND11_MODULE(pymanifold, m) {
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 "
":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 "
":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) {
[](Manifold &self, Float3 normal, float originOffset) {
return self.TrimByPlane(
{std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)},
originOffset);
@@ -310,11 +361,21 @@ PYBIND11_MODULE(pymanifold, m) {
"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 "
":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 "
":param originOffset: The distance of the plane from the origin in "
"the direction of the normal vector.")
.def_property_readonly(
"bounding_box",
[](Manifold &self) {
auto b = self.BoundingBox();
py::tuple box = py::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; "
@@ -347,7 +408,7 @@ PYBIND11_MODULE(pymanifold, m) {
"by sharping all edges that are incident on it, allowing cones to be "
"formed.")
.def_static(
"from_mesh", [](const Mesh &mesh) { return Manifold(mesh); },
"from_mesh", [](const MeshGL &mesh) { return Manifold(mesh); },
py::arg("mesh"))
.def_static(
"tetrahedron", []() { return Manifold::Tetrahedron(); },
@@ -372,8 +433,8 @@ PYBIND11_MODULE(pymanifold, m) {
"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");
if (size.ndim() != 1 || size.shape(0) != 3)
throw std::runtime_error("Invalid vector shape, expected (3)");
return Manifold::Cube(
glm::vec3(size_view(0), size_view(1), size_view(2)), center);
},
@@ -436,157 +497,116 @@ PYBIND11_MODULE(pymanifold, m) {
"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).")
py::class_<MeshGL>(m, "Mesh")
.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.");
// note that reshape requires mutable array_t, but this will not
// affect the original array passed into the function
py::init(
[](py::array_t<float, pyArrayFlags> &vertProp,
py::array_t<int, pyArrayFlags> &triVerts,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&mergeFromVert,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&mergeToVert,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&runIndex,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&runOriginalID,
std::optional<py::array_t<float, pyArrayFlags>> &runTransform,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&faceID,
const std::optional<py::array_t<float, pyArrayFlags>>
&halfedgeTangent,
float precision) {
MeshGL out;
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>();
out.numProp = vertProp.shape(1);
vertProp = vertProp.reshape({-1});
out.vertProperties = toVector<float>(vertProp);
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>();
if (triVerts.ndim() != 2 || triVerts.shape(1) != 3)
throw std::runtime_error(
"Invalid tri_verts shape, expected (-1, 3)");
triVerts = triVerts.reshape({-1});
out.triVerts = toVector<uint32_t>(triVerts);
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;
})
if (mergeFromVert.has_value())
out.mergeFromVert = toVector<uint32_t>(mergeFromVert.value());
if (mergeToVert.has_value())
out.mergeToVert = toVector<uint32_t>(mergeToVert.value());
if (runIndex.has_value())
out.runIndex = toVector<uint32_t>(runIndex.value());
if (runOriginalID.has_value())
out.runOriginalID = toVector<uint32_t>(runOriginalID.value());
if (runTransform.has_value()) {
auto runTransform1 = runTransform.value();
if (runTransform1.ndim() != 3 ||
runTransform1.shape(1) != 4 ||
runTransform1.shape(2) != 3)
throw std::runtime_error(
"Invalid run_transform shape, expected (-1, 4, 3)");
runTransform1 = runTransform1.reshape({-1});
out.runTransform = toVector<float>(runTransform1);
}
if (faceID.has_value())
out.faceID = toVector<uint32_t>(faceID.value());
if (halfedgeTangent.has_value()) {
auto halfedgeTangent1 = halfedgeTangent.value();
if (halfedgeTangent1.ndim() != 3 ||
halfedgeTangent1.shape(1) != 3 ||
halfedgeTangent1.shape(2) != 4)
throw std::runtime_error(
"Invalid halfedge_tangent shape, expected (-1, 3, 4)");
halfedgeTangent1 = halfedgeTangent1.reshape({-1});
out.halfedgeTangent = toVector<float>(halfedgeTangent1);
}
return out;
}),
py::arg("vert_properties"), py::arg("tri_verts"),
py::arg("merge_from_vert") = py::none(),
py::arg("merge_to_vert") = py::none(),
py::arg("run_index") = py::none(),
py::arg("run_original_id") = py::none(),
py::arg("run_transform") = py::none(),
py::arg("face_id") = py::none(),
py::arg("halfedge_tangent") = py::none(), py::arg("precision") = 0)
.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;
"vert_properties",
[](const MeshGL &self) {
return py::array(self.vertProperties.size(),
self.vertProperties.data())
.reshape(std::array<int, 2>{-1, (int)self.numProp});
})
.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;
});
.def_property_readonly("tri_verts",
[](const MeshGL &self) {
return py::array(self.triVerts.size(),
self.triVerts.data())
.reshape(std::array<int, 2>{-1, 3});
})
.def_property_readonly("run_transform",
[](const MeshGL &self) {
return py::array(self.runTransform.size(),
self.runTransform.data())
.reshape(std::array<int, 3>{-1, 4, 3});
})
.def_property_readonly("halfedge_tangent",
[](const MeshGL &self) {
return py::array(self.halfedgeTangent.size(),
self.halfedgeTangent.data())
.reshape(std::array<int, 3>{-1, 3, 4});
})
.def_readonly("merge_from_vert", &MeshGL::mergeFromVert)
.def_readonly("merge_to_vert", &MeshGL::mergeToVert)
.def_readonly("run_index", &MeshGL::runIndex)
.def_readonly("run_original_id", &MeshGL::runOriginalID)
.def_readonly("face_id", &MeshGL::faceID);
py::enum_<CrossSection::FillRule>(m, "FillRule")
.value("EvenOdd", CrossSection::FillRule::EvenOdd,
@@ -624,42 +644,94 @@ PYBIND11_MODULE(pymanifold, m) {
"[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);
.def(py::init([](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])};
}
}
return self.Transform(mat_glm);
})
return CrossSection(simplePolygons, fillrule);
}),
py::arg("polygons"),
py::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, py::array_t<float> &mat) {
auto mat_view = mat.unchecked<2>();
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_view(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) {
@@ -669,20 +741,73 @@ PYBIND11_MODULE(pymanifold, m) {
v.y = std::get<1>(fv);
});
},
py::arg("f"))
.def("simplify", &CrossSection::Simplify)
py::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, py::arg("delta"),
py::arg("join_type"), py::arg("miter_limit") = 2.0,
py::arg("arc_tolerance") = 0.0)
py::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(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("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();
py::list polygon_list;
for (int i = 0; i < data.size(); ++i) {
py::list polygon;
for (int j = 0; j < data[i].size(); ++j) {
auto f = data[i][j];
py::tuple vertex = py::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,
@@ -728,11 +853,24 @@ PYBIND11_MODULE(pymanifold, m) {
return CrossSection::Square({std::get<0>(dims), std::get<1>(dims)},
center);
},
py::arg("dims"), py::arg("center") = false)
py::arg("dims"), py::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);
},
py::arg("radius"), py::arg("circularSegments") = 0);
py::arg("radius"), py::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.");
}
+18 -10
View File
@@ -16,23 +16,31 @@ project(wasm)
add_executable(manifoldjs bindings.cpp)
# 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)
set_source_files_properties(bindings.cpp PROPERTIES OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js)
target_link_libraries(manifoldjs manifold sdf cross_section polygon)
target_compile_options(manifoldjs PRIVATE ${MANIFOLD_FLAGS} -fexceptions)
target_compile_options(manifoldjs PRIVATE ${MANIFOLD_FLAGS})
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(COPY examples;test;. DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS examples;test)
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)
add_custom_command(
TARGET manifoldjs POST_BUILD
TARGET js_deps POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_BINARY_DIR}/manifold.*
${CMAKE_CURRENT_BINARY_DIR}/manifold-*.d.ts
${CMAKE_CURRENT_BINARY_DIR}/examples/)
${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/)
+5 -1
View File
@@ -20,7 +20,11 @@ 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 https://github.com/elalish/manifold/discussions/256#discussioncomment-3944287.
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).
## About the author
+76 -175
View File
@@ -14,160 +14,18 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <vector>
using namespace emscripten;
#include <manifold.h>
#include <polygon.h>
#include <sdf.h>
#include <vector>
#include "cross_section.h"
#include "helpers.cpp"
using namespace emscripten;
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)
@@ -205,6 +63,18 @@ 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")
@@ -215,33 +85,60 @@ 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);
class_<Manifold>("Manifold")
.constructor(&FromMeshJS)
.function("add", &Union)
.function("subtract", &Difference)
.function("intersect", &Intersection)
.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)
.function("_TrimByPlane", &Manifold::TrimByPlane)
.function("_GetMeshJS", &GetMeshJS)
.function("_GetMeshJS", &js::GetMeshJS)
.function("refine", &Manifold::Refine)
.function("_Warp", &Warp)
.function("transform", &Transform)
.function("_Warp", &man_js::Warp)
.function("_SetProperties", &man_js::SetProperties)
.function("transform", &man_js::Transform)
.function("_Translate", &Manifold::Translate)
.function("_Rotate", &Manifold::Rotate)
.function("_Scale", &Manifold::Scale)
@@ -253,32 +150,36 @@ 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("_getCurvature", &Manifold::GetCurvature)
.function("calculateCurvature", &Manifold::CalculateCurvature)
.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", &Smooth);
function("_Extrude", &Extrude);
function("_Tetrahedron", &Manifold::Tetrahedron);
function("_Smooth", &js::Smooth);
function("_Extrude", &Manifold::Extrude);
function("_Triangulate", &Triangulate);
function("_Revolve", &Revolve);
function("_LevelSet", &LevelSetJs);
function("_unionN", &UnionN);
function("_differenceN", &DifferenceN);
function("_intersectionN", &IntersectionN);
function("_Compose", &Manifold::Compose);
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);
// Quality Globals
function("setMinCircularAngle", &Quality::SetMinCircularAngle);
function("setMinCircularEdgeLength", &Quality::SetMinCircularEdgeLength);
function("setCircularSegments", &Quality::SetCircularSegments);
function("getCircularSegments", &Quality::GetCircularSegments);
function("reserveIDs", &Manifold::ReserveIDs);
}
+325 -65
View File
@@ -17,6 +17,8 @@ Module.setup = function() {
if (_ManifoldInitialized) return;
_ManifoldInitialized = true;
// conversion utilities
function toVec(vec, list, f = x => x) {
if (list) {
for (let x of list) {
@@ -33,6 +35,21 @@ 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];
@@ -50,7 +67,15 @@ Module.setup = function() {
polygonsVec.delete();
}
function vararg2vec(vec) {
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) {
if (vec[0] instanceof Array)
return {x: vec[0][0], y: vec[0][1], z: vec[0][2]};
if (typeof (vec[0]) == 'number')
@@ -59,6 +84,118 @@ 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');
@@ -72,11 +209,43 @@ 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(vararg2vec(vec));
return this._Translate(vararg2vec3(vec));
};
Module.Manifold.prototype.rotate = function(vec) {
@@ -84,18 +253,33 @@ 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(vararg2vec([vec]));
return this._Scale(vararg2vec3([vec]));
};
Module.Manifold.prototype.mirror = function(vec) {
return this._Mirror(vararg2vec([vec]));
return this._Mirror(vararg2vec3([vec]));
};
Module.Manifold.prototype.trimByPlane = function(normal, offset) {
return this._TrimByPlane(vararg2vec([normal]), offset);
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.decompose = function() {
@@ -105,15 +289,12 @@ Module.setup = function() {
return result;
};
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;
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]),
};
};
class Mesh {
@@ -153,6 +334,12 @@ 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));
}
@@ -190,14 +377,6 @@ 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) {
@@ -246,6 +425,65 @@ 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');
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);
@@ -258,32 +496,38 @@ Module.setup = function() {
return manifold;
};
Module.Manifold.prototype = Object.create(ManifoldCtor.prototype);
Module.Manifold.ofMesh = function(mesh) {
return new Module.Manifold(mesh);
};
Module.cube = function(...args) {
Module.Manifold.tetrahedron = function() {
return Module._Tetrahedron();
};
Module.Manifold.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 = vararg2vec(args);
size = vararg2vec3(args);
const center = args[1] || false;
return Module._Cube(size, center);
};
Module.cylinder = function(
Module.Manifold.cylinder = function(
height, radiusLow, radiusHigh = -1.0, circularSegments = 0,
center = false) {
return Module._Cylinder(
height, radiusLow, radiusHigh, circularSegments, center);
};
Module.sphere = function(radius, circularSegments = 0) {
Module.Manifold.sphere = function(radius, circularSegments = 0) {
return Module._Sphere(radius, circularSegments);
};
Module.smooth = function(mesh, sharpenedEdges = []) {
Module.Manifold.smooth = function(mesh, sharpenedEdges = []) {
const sharp = new Module.Vector_smoothness();
toVec(sharp, sharpenedEdges);
const result = Module._Smooth(mesh, sharp);
@@ -291,40 +535,51 @@ Module.setup = function() {
return result;
};
Module.extrude = function(
Module.Manifold.extrude = function(
polygons, height, nDivisions = 0, twistDegrees = 0.0,
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;
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);
};
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.revolve = function(
polygons, circularSegments = 0, revolveDegrees = 360.0) {
const cs = (polygons instanceof CrossSectionCtor) ?
polygons :
Module.CrossSection(polygons, 'Positive');
return cs.revolve(circularSegments, revolveDegrees);
};
Module.compose = function(manifolds) {
Module.Manifold.reserveIDs = function(n) {
return Module._ReserveIDs(n);
};
Module.Manifold.compose = function(manifolds) {
const vec = new Module.Vector_manifold();
toVec(vec, manifolds);
const result = Module._Compose(vec);
const result = Module._manifoldCompose(vec);
vec.delete();
return result;
};
Module.levelSet = function(sdf, bounds, edgeLength, level = 0) {
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) {
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]},
@@ -341,18 +596,23 @@ Module.setup = function() {
return out;
};
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.prototype = Object.create(ManifoldCtor.prototype);
Module.union = batchbool('union');
Module.difference = batchbool('difference');
Module.intersection = batchbool('intersection');
// 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;
};
};
+45
View File
@@ -0,0 +1,45 @@
# 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.
+3 -3
View File
@@ -180,19 +180,19 @@ model-viewer {
}
.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;
}
+34 -31
View File
@@ -12,11 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {examples} from './examples.js';
const exampleFunctions = examples.functionBodies;
import ManifoldWorker from './worker?worker';
// Loaded globally by examples.js
const exampleFunctions = self.examples.functionBodies;
if (navigator.serviceWorker) {
navigator.serviceWorker.register('service-worker.js');
navigator.serviceWorker.register(
'/service-worker.js', {scope: './index.html'});
}
let editor = undefined;
@@ -98,6 +101,10 @@ function createDropdownItem(name) {
saveCurrent();
switchTo(label.textContent);
};
// Stop text input spaces from triggering the button
button.onkeyup = function(event) {
event.preventDefault();
};
return button;
}
@@ -215,50 +222,44 @@ 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 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 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 const module: ManifoldStatic;
declare const module: ManifoldToplevel;
`;
}
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());
await addTypes('editor.d.ts');
monaco.languages.typescript.typescriptDefaults.addExtraLib(
await getEditorDTS());
editor = monaco.editor.create(
document.getElementById('editor'),
{language: 'typescript', automaticLayout: true});
@@ -350,7 +351,7 @@ let objectURL = null;
let manifoldWorker = null;
function createWorker() {
manifoldWorker = new Worker('worker.js', {type: 'module'});
manifoldWorker = new ManifoldWorker();
manifoldWorker.onmessage = function(e) {
if (e.data == null) {
if (tsWorker != null && !manifoldInitialized) {
@@ -368,7 +369,6 @@ function createWorker() {
finishRun();
runButton.disabled = true;
setScript('safe', 'true');
URL.revokeObjectURL(objectURL);
objectURL = e.data.objectURL;
@@ -376,6 +376,9 @@ function createWorker() {
if (objectURL == null) {
mv.showPoster();
poster.textContent = 'Error';
createWorker();
} else {
setScript('safe', 'true');
}
}
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Accessor} from '@gltf-transform/core';
import {EXTManifold} from './manifold-gltf.js';
export const attributeDefs = {
'POSITION': {type: Accessor.Type.VEC3, components: 3},
'NORMAL': {type: Accessor.Type.VEC3, components: 3},
'TANGENT': {type: Accessor.Type.VEC4, components: 4},
'TEXCOORD_0': {type: Accessor.Type.VEC2, components: 2},
'TEXCOORD_1': {type: Accessor.Type.VEC2, components: 2},
'COLOR_0': {type: Accessor.Type.VEC3, components: 3},
'JOINTS_0': {type: Accessor.Type.VEC4, components: 4},
'WEIGHTS_0': {type: Accessor.Type.VEC4, components: 4},
'SKIP_1': {type: null, components: 1},
'SKIP_2': {type: null, components: 2},
'SKIP_3': {type: null, components: 3},
'SKIP_4': {type: null, components: 4},
};
export function setupIO(io) {
return io.registerExtensions([EXTManifold]);
}
function readPrimitive(primitive, numProp, attributes) {
const position = primitive.getAttribute('POSITION');
const numVert = position.getCount();
const vertProperties = [];
let offset = 0;
for (const attribute of attributes) {
const size = attributeDefs[attribute].components;
if (attribute === 'SKIP') {
offset += size;
continue;
}
const accessor = primitive.getAttribute(attribute);
if (accessor) {
const array = accessor.getArray();
for (let i = 0; i < numVert; ++i) {
for (let j = 0; j < size; ++j) {
vertProperties[numProp * i + offset + j] = array[i * size + j];
}
}
} else {
for (let i = 0; i < numVert; ++i) {
for (let j = 0; j < size; ++j) {
vertProperties[numProp * i + offset + j] = 0;
}
}
}
offset += size;
}
return vertProperties;
}
export function readMesh(mesh, attributes, materials) {
const primitives = mesh.listPrimitives();
if (primitives.length === 0) {
return {};
}
if (attributes.length === 0) {
const attributeSet = new Set();
for (const primitive of primitives) {
const semantics = primitive.listSemantics();
for (const semantic of semantics) {
attributeSet.add(semantic);
}
}
for (const semantic in attributeDefs) {
if (attributeSet.has(semantic)) {
attributes.push(semantic);
attributeSet.delete(semantic);
}
}
for (const semantic of attributeSet.keys()) {
attributes.push(semantic);
}
}
if (attributes.length < 1 || attributes[0] !== 'POSITION')
throw new Error('First attribute must be "POSITION".');
const numProp = attributes.map((def) => attributeDefs[def].components)
.reduce((a, b) => a + b);
const manifoldPrimitive = mesh.getExtension('EXT_manifold');
let vertPropArray = [];
let triVertArray = [];
const runIndexArray = [0];
const mergeFromVert = [];
const mergeToVert = [];
if (manifoldPrimitive != null) {
vertPropArray = readPrimitive(primitives[0], numProp, attributes);
for (const primitive of primitives) {
triVertArray = [...triVertArray, ...primitive.getIndices().getArray()];
runIndexArray.push(triVertArray.length);
materials.push(primitive.getMaterial());
}
const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? [];
const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? [];
const vert2merge = new Map();
for (const [i, idx] of mergeTriVert.entries()) {
vert2merge.set(triVertArray[idx], mergeTo[i]);
}
for (const [from, to] of vert2merge.entries()) {
mergeFromVert.push(from);
mergeToVert.push(to);
}
} else {
for (const primitive of primitives) {
const numVert = vertPropArray.length / numProp;
vertPropArray =
[...vertPropArray, ...readPrimitive(primitive, numProp, attributes)];
triVertArray = [
...triVertArray,
...primitive.getIndices().getArray().map((i) => i + numVert)
];
runIndexArray.push(triVertArray.length);
materials.push(primitive.getMaterial());
}
}
const vertProperties = new Float32Array(vertPropArray);
const triVerts = new Uint32Array(triVertArray);
const runIndex = new Uint32Array(runIndexArray);
return {
numProp,
triVerts,
vertProperties,
runIndex,
mergeFromVert,
mergeToVert
};
}
export function writeMesh(doc, manifoldMesh, attributes, materials) {
if (doc.getRoot().listBuffers().length === 0) {
doc.createBuffer();
}
const buffer = doc.getRoot().listBuffers()[0];
const manifoldExtension = doc.createExtension(EXTManifold);
const attributeUnion = [];
for (const matAttributes of attributes) {
matAttributes.forEach((attribute, i) => {
if (i >= attributeUnion.length) {
attributeUnion.push(attribute);
} else {
const size = attributeDefs[attribute].components;
const unionSize = attributeDefs[attributeUnion[i]].components;
if (size != unionSize) {
throw new Error(
'Attribute sizes do not correspond: ', attribute, ' and ',
attributeUnion[i]);
}
if (attributeDefs[attributeUnion[i]].type == null) {
attributeUnion[i] = attribute;
}
}
});
}
if (attributeUnion.length < 1 || attributeUnion[0] !== 'POSITION')
throw new Error('First attribute must be "POSITION".');
const mesh = doc.createMesh();
const numPrimitive = manifoldMesh.runIndex.length - 1;
for (let run = 0; run < numPrimitive; ++run) {
const indices = doc.createAccessor('indices')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array());
const primitive = doc.createPrimitive().setIndices(indices);
const material = materials[run];
if (material) {
primitive.setMaterial(material);
}
mesh.addPrimitive(primitive);
}
const numVert = manifoldMesh.numVert;
const numProp = manifoldMesh.numProp;
let offset = 0;
attributeUnion.forEach((attribute, aIdx) => {
if (attribute === 'SKIP') {
++offset;
return;
}
const def = attributeDefs[attribute];
if (def == null)
throw new Error(attribute + ' is not a recognized attribute.');
const n = def.components;
if (offset + n > numProp) throw new Error('Too many attribute channels.');
const array = new Float32Array(n * numVert);
for (let v = 0; v < numVert; ++v) {
for (let i = 0; i < n; ++i) {
array[n * v + i] =
manifoldMesh.vertProperties[numProp * v + offset + i];
}
}
const accessor =
doc.createAccessor().setBuffer(buffer).setType(def.type).setArray(
array);
mesh.listPrimitives().forEach((primitive, pIdx) => {
if (attributes[pIdx].length > aIdx &&
attributeDefs[attributes[pIdx][aIdx]].type != null) {
primitive.setAttribute(attribute, accessor);
}
});
offset += n;
});
const manifoldPrimitive = manifoldExtension.createManifoldPrimitive();
mesh.setExtension('EXT_manifold', manifoldPrimitive);
const indices = doc.createAccessor('indices')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(manifoldMesh.triVerts);
manifoldPrimitive.setIndices(indices);
manifoldPrimitive.setRunIndex(manifoldMesh.runIndex);
const vert2merge = [...Array(manifoldMesh.numVert).keys()];
for (const [i, from] of manifoldMesh.mergeFromVert.entries()) {
vert2merge[from] = manifoldMesh.mergeToVert[i];
}
const ind = [];
const val = [];
for (const [i, vert] of manifoldMesh.triVerts.entries()) {
const newVert = vert2merge[vert];
if (vert !== newVert) {
ind.push(i);
val.push(newVert);
}
}
const indicesAccessor = doc.createAccessor()
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(ind));
const valuesAccessor = doc.createAccessor()
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(val));
manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor);
return mesh;
}
export function disposeMesh(mesh) {
if (!mesh) return;
const primitives = mesh.listPrimitives();
for (const primitive of primitives) {
primitive.getIndices()?.dispose();
for (const accessor of primitive.listAttributes()) {
accessor.dispose();
}
}
mesh.dispose();
}
export async function loadTexture(texture, uri) {
const response = await fetch(uri);
const blob = await response.blob();
texture.setMimeType(blob.type);
texture.setImage(new Uint8Array(await blob.arrayBuffer()));
}
+6 -1
View File
@@ -29,6 +29,11 @@
}
}
</script>
<script>
self.ModelViewerElement = self.ModelViewerElement || {};
self.ModelViewerElement.modelCacheSize = 0;
</script>
<script type="module" src="/examples.js"></script>
</head>
<body>
@@ -67,6 +72,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/2.0.1/model-viewer.min.js"></script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.1.1/model-viewer.min.js"></script>
</html>
+189
View File
@@ -0,0 +1,189 @@
<!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
@@ -0,0 +1,180 @@
// 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;
}
}
+67 -183
View File
@@ -45,60 +45,66 @@
<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 { 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';
import { Document, WebIO } from '@gltf-transform/core';
import { clearNodeTransform, flatten, prune } from '@gltf-transform/functions';
import { loadTexture, readMesh, writeMesh, disposeMesh, setupIO } from './gltf-io.js';
import Module from './built/manifold.js';
const io = new WebIO();
const io = setupIO(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 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 { Manifold, Mesh } = wasm;
const csg = function (operation) {
push2MV(wasm[operation](space, moon));
const attributes = ['POSITION', 'TEXCOORD_0'];
const materialMap = new Map();
async function readGLB(url, attributes, materialMap) {
const manifolds = [];
const docIn = await io.read(url);
await docIn.transform(flatten());
const nodes = docIn.getRoot().listNodes();
const ids = [];
for (const node of nodes) {
clearNodeTransform(node);
const materials = [];
const manifoldMesh = readMesh(node.getMesh(), attributes, materials);
manifoldMesh.runOriginalID = [];
const firstID = Manifold.reserveIDs(materials.length);
for (let i = 0; i < materials.length; ++i) {
manifoldMesh.runOriginalID.push(firstID + i);
ids.push(firstID + i);
}
manifolds.push(Manifold(new Mesh(manifoldMesh)));
}
// pull in materials, TODO: replace with transfer() when available
const startIdx = doc.getRoot().listMaterials().length;
doc.merge(docIn);
doc.getRoot().listScenes().forEach((s) => s.dispose());
doc.getRoot().listBuffers().forEach((s) => s.dispose());
for (const [i, id] of ids.entries()) {
const material = doc.getRoot().listMaterials()[startIdx + i];
materialMap.set(id, material);
}
return Manifold.union(manifolds);
}
const space = await readGLB('/models/space.glb', attributes, materialMap);
const moon = await readGLB('/models/moon.glb', attributes, materialMap);
const node = doc.createNode();
doc.createScene().addChild(node);
function csg(operation) {
push2MV(Manifold[operation](space, moon));
};
document.querySelector('select').onchange = function (event) {
@@ -110,30 +116,28 @@
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 mesh = manifold.rotate([-90, 0, 0]).getMesh();
const manifoldMesh = manifold.rotate([-90, 0, 0]).getMesh();
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]));
disposeMesh(node.getMesh());
const materials = [];
const matAttributes = [];
for (const id of manifoldMesh.runOriginalID) {
materials.push(materialMap.get(id));
matAttributes.push(attributes);
}
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 mesh = writeMesh(doc, manifoldMesh, matAttributes, materials);
node.setMesh(mesh);
await doc.transform(prune());
const glb = await io.writeBinary(doc);
@@ -142,126 +146,6 @@
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.

Before

Width:  |  Height:  |  Size: 109 KiB

File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"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",
"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"
}
}
@@ -12,24 +12,58 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* 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 show - returned for chaining.
*/
declare function show(manifold: Manifold): Manifold;
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;
}
/**
* 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
* Set material properties on the input manifold. They will be carried along
* through operations.
*
* @param manifold The object to add properties to - returned for chaining.
* @param material A set of material properties to apply to this manifold.
*/
declare function setMaterial(
manifold: Manifold, material: GLTFMaterial): Manifold;
/**
* 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
* 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 manifold The object to show - returned for chaining.
* @param shape The object to show - returned for chaining.
*/
declare function only(manifold: Manifold): Manifold;
declare function only(shape: CrossSection|Manifold): Manifold;
// Type definitions for gl-matrix 3.4.3 Project:
// https://github.com/toji/gl-matrix
@@ -16,14 +16,16 @@ 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 "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.
// 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;
const box = cube([100, 100, 100], true);
const ball = sphere(60, 100);
// You must name your final output "result".
// You must name your final output "result", or create at least one
// GLTFNode - see Menger Sponge and Gyroid Module examples.
const result = box.subtract(ball);
// For visual debug, wrap any shape with show() and it and all of its
@@ -38,6 +40,11 @@ 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;
},
@@ -54,20 +61,46 @@ export const examples = {
const scale = edgeLength / (2 * Math.sqrt(2));
const tet = tetrahedron().scale(scale);
const tet = Manifold.tetrahedron().scale(scale);
const box = [];
box.push([2, -2], [2, 2]);
box.push([1, -1], [1, 1]);
for (let i = 0; i <= nDivisions; ++i) {
box.push([gap / (2 * scale), 2 - i * 4 / nDivisions]);
box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]);
}
const screw = extrude(box, 2, nDivisions, 270)
.rotate([0, 0, -45])
.translate([0, 0, -1])
.scale(scale);
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 result = tet.intersect(screw);
// 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;
return result;
},
@@ -75,6 +108,7 @@ export const examples = {
// Demonstrates how at 90-degree intersections, the sphere and cylinder
// facets match up perfectly, for any choice of global resolution
// parameters.
const {sphere, cylinder, union} = Manifold;
function roundedFrame(edgeLength, radius, circularSegments = 0) {
const edge = cylinder(edgeLength, radius, -1, circularSegments);
@@ -139,7 +173,7 @@ export const examples = {
v[2] *= r;
};
const ball = sphere(1, 200);
const ball = Manifold.sphere(1, 200);
const heart = ball.warp(func);
const box = heart.boundingBox();
const result = heart.scale(100 / (box.max[0] - box.min[0]));
@@ -187,7 +221,28 @@ export const examples = {
const triVerts = Uint32Array.from(triangles);
const vertProperties = Float32Array.from(positions);
const scallop = new Mesh({numProp: 3, triVerts, vertProperties});
const result = smooth(scallop, sharpenedEdges).refine(n);
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']
};
return result;
},
@@ -227,12 +282,8 @@ export const examples = {
const m = linearSegments > 2 ? linearSegments :
n * q * majorRadius / threadRadius;
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 offset = 2
const circle = CrossSection.circle(1, n).translate([offset, 0]);
const func = (v) => {
const psi = q * Math.atan2(v[0], v[1]);
@@ -250,14 +301,14 @@ export const examples = {
vec3.rotateZ(v, v, center, psi);
};
let knot = revolve(circle, m).warp(func);
let knot = Manifold.revolve(circle, m).warp(func);
if (kLoops > 1) {
const knots = [];
for (let k = 0; k < kLoops; ++k) {
knots.push(knot.rotate([0, 0, 360 * (k / kLoops) * (q / p)]));
}
knot = compose(knots);
knot = Manifold.compose(knots);
}
return knot;
@@ -293,20 +344,35 @@ export const examples = {
}
function mengerSponge(n) {
let result = cube([1, 1, 1], true);
let result = Manifold.cube([1, 1, 1], true);
const holes = [];
fractal(holes, result, 1.0, [0.0, 0.0], 1, n);
const hole = compose(holes);
result = difference(result, hole);
result = difference(result, hole.rotate([90, 0, 0]));
result = difference(result, hole.rotate([0, 90, 0]));
const hole = Manifold.compose(holes);
result = Manifold.difference(
result,
hole,
hole.rotate([90, 0, 0]),
hole.rotate([0, 90, 0]),
);
return result;
}
const result = mengerSponge(3).trimByPlane([1, 1, 1], 0).scale(100);
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']};
return result;
},
@@ -318,7 +384,7 @@ export const examples = {
function base(
width, radius, decorRadius, twistRadius, nDecor, innerRadius,
outerRadius, cut, nCut, nDivision) {
let b = cylinder(width, radius + twistRadius / 2);
let b = Manifold.cylinder(width, radius + twistRadius / 2);
const circle = [];
const dPhiDeg = 180 / nDivision;
for (let i = 0; i < 2 * nDivision; ++i) {
@@ -327,7 +393,7 @@ export const examples = {
decorRadius * Math.sin(dPhiDeg * i * Math.PI / 180)
]);
}
let decor = extrude(circle, width, nDivision, 180)
let decor = Manifold.extrude(circle, width, nDivision, 180)
.scale([1, 0.5, 1])
.translate([0, radius, 0]);
for (let i = 0; i < nDecor; i++)
@@ -345,8 +411,9 @@ export const examples = {
stretch.push(vec2.rotate([0, 0], p2, o, dPhiRad * i));
stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i));
}
b = intersection(extrude(stretch, width), b);
return b;
const result =
Manifold.intersection(Manifold.extrude(stretch, width), b);
return result;
}
function stretchyBracelet(
@@ -359,7 +426,7 @@ export const examples = {
const cut = 0.5 * (Math.PI * 2 * innerRadius / nCut - thickness);
const adjThickness = 0.5 * thickness * height / cut;
return difference(
return Manifold.difference(
base(
width, radius, decorRadius, twistRadius, nDecor,
innerRadius + thickness, outerRadius + adjThickness,
@@ -381,8 +448,13 @@ 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) {
@@ -399,20 +471,41 @@ export const examples = {
min: vec3.fromValues(-period, -period, -period),
max: vec3.fromValues(period, period, period)
};
return levelSet(gyroid, box, period / n, level).scale(size / period);
return Manifold.levelSet(gyroid, box, period / n, level)
.scale(size / period);
};
function rhombicDodecahedron() {
const box = cube([1, 1, 2], true).scale(size * Math.sqrt(2));
const box = Manifold.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]));
}
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)]);
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)
]);
return result;
}
},
@@ -431,4 +524,8 @@ 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;
}

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

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 110 KiB

+9 -14
View File
@@ -44,21 +44,16 @@
</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 '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';
import * as THREE from 'three';
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import Module from './built/manifold.js';
const wasm = await Module();
wasm.setup();
const { Manifold, Mesh } = wasm;
// we have manifold module, let's do some three.js
const camera = new THREE.PerspectiveCamera(30, 1, 0.01, 10);
camera.position.z = 1;
@@ -71,12 +66,12 @@
const icosahedron = simplify(new THREE.IcosahedronGeometry(0.16));
const manifold_1 = wasm.cube(0.2, 0.2, 0.2, true);
const manifold_2 = new wasm.Manifold(geometry2mesh(icosahedron));
const manifold_1 = Manifold.cube([0.2, 0.2, 0.2], true);
const manifold_2 = new Manifold(geometry2mesh(icosahedron));
const csg = function (operation) {
mesh.geometry?.dispose();
mesh.geometry = mesh2geometry(wasm[operation](manifold_1, manifold_2).getMesh());
mesh.geometry = mesh2geometry(Manifold[operation](manifold_1, manifold_2).getMesh());
};
document.querySelector('select').onchange = function (event) {
@@ -99,7 +94,7 @@
function geometry2mesh(geometry) {
const vertProperties = geometry.attributes.position.array;
const triVerts = geometry.index.array;
return new wasm.Mesh({ numProp: 3, vertProperties, triVerts });
return new Mesh({ numProp: 3, vertProperties, triVerts });
}
function mesh2geometry(mesh) {
+24
View File
@@ -0,0 +1,24 @@
{
"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
@@ -0,0 +1,26 @@
// 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
@@ -1,241 +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 * 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
@@ -0,0 +1,152 @@
// 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 objectURL = 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(objectURL);
objectURL = e.data.objectURL;
if (objectURL == null) {
reject('no objectURL');
}
const docIn = await io.read(objectURL);
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');
});
});
+464
View File
@@ -0,0 +1,464 @@
// 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, Material, Node, WebIO} from '@gltf-transform/core';
import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
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'
];
// 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'
];
// CrossSection static methods (that return a new cross-section)
const crossSectionStaticFunctions = [
'square', 'circle', 'union', 'difference', 'intersection', 'compose',
'ofPolygons'
];
// CrossSection member functions (that return a new cross-section)
const crossSectionMemberFunctions = [
'add', 'subtract', 'intersect', 'rectClip', 'decompose', 'transform',
'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset'
];
// 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>();
function cleanup() {
ghost = false;
shown.clear();
singles.clear();
nodes.length = 0;
id2material.clear();
materialCache.clear();
}
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 exportGLB(typeof result === "undefined" ? undefined : result);\n';
try {
const f = new Function(
'exportGLB', 'glMatrix', 'module', ...exposedFunctions, content);
await f(
exportGLB, 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, 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));
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, nodeDef: GLTFNode,
manifold2node: Map<Manifold, Map<GLTFMaterial, Node>>): Node {
const node = createGLTFnode(doc, nodeDef);
if (nodeDef.manifold != null) {
const backupMaterial = getBackupMaterial(nodeDef);
const cachedNodes = manifold2node.get(nodeDef.manifold);
if (cachedNodes == null) {
addMesh(doc, node, nodeDef.manifold, backupMaterial);
const cache = new Map<GLTFMaterial, Node>();
cache.set(backupMaterial, node);
manifold2node.set(nodeDef.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);
}
}
}
return node;
}
async function exportGLB(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);
if (nodes.length > 0) {
const node2gltf = new Map<GLTFNode, Node>();
const manifold2node = new Map<Manifold, Map<GLTFMaterial, Node>>();
let leafNodes = 0;
for (const nodeDef of nodes) {
node2gltf.set(nodeDef, createNodeFromCache(doc, nodeDef, manifold2node));
if (nodeDef.manifold) {
++leafNodes;
}
}
for (const nodeDef of nodes) {
const gltfNode = node2gltf.get(nodeDef)!;
if (nodeDef.parent == null) {
wrapper.addChild(gltfNode);
} else {
node2gltf.get(nodeDef.parent)!.addChild(gltfNode);
}
}
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, node, manifold);
wrapper.addChild(node);
}
const glb = await io.writeBinary(doc);
const blob = new Blob([glb], {type: 'application/octet-stream'});
self.postMessage({objectURL: URL.createObjectURL(blob)});
}
+222
View File
@@ -0,0 +1,222 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <manifold.h>
#include <polygon.h>
#include <sdf.h>
#include <vector>
#include "cross_section.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);
}
} // 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};
}
} // namespace man_js
+591 -184
View File
@@ -12,101 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
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;
import {Box, FillRule, JoinType, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types';
/**
* Triangulates a set of /epsilon-valid polygons.
@@ -118,58 +24,6 @@ export function extrude(
*/
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
@@ -186,12 +40,284 @@ export function setCircularSegments(segments: number): void;
export function getCircularSegments(radius: number): 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.
*/
export function reserveIDs(count: 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;
/**
* 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;
/**
* Compute the convex hull of the contours in this CrossSection.
*/
hull(): CrossSection;
/**
* Remove vertices from the contours in this CrossSection that are less than
* the specified distance epsilon from an imaginary line that passes through
* 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;
// 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;
}
export class Manifold {
/**
@@ -206,6 +332,165 @@ 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.
@@ -253,6 +538,66 @@ 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
*/
@@ -268,6 +613,57 @@ 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.
*
@@ -277,30 +673,18 @@ 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;
trimByPlane(normal: Vec3, originOffset: number): Manifold;
// Topological Operations
/**
* 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.
* 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 n The number of pieces to split every edge into. Must be > 1.
* @param manifolds A list of Manifolds to lazy-union together.
*/
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;
static compose(manifolds: Manifold[]): Manifold;
/**
* This operation returns a vector of Manifolds that are topologically
@@ -310,6 +694,8 @@ export class Manifold {
*/
decompose(): Manifold[];
// Property Access
/**
* Does the Manifold have any triangles?
*/
@@ -330,6 +716,18 @@ 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.
*/
@@ -360,16 +758,7 @@ export class Manifold {
*/
getProperties(): Properties;
/**
* 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;
// Export
/**
* Returns a Mesh that is designed to easily push into a renderer, including
@@ -386,6 +775,8 @@ 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,
@@ -407,6 +798,21 @@ 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 {
@@ -435,9 +841,10 @@ 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;
}
}
+6 -8
View File
@@ -43,6 +43,10 @@ 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
@@ -55,11 +59,5 @@ export type Properties = {
surfaceArea: number,
volume: number
};
export type Curvature = {
maxMeanCurvature: number,
minMeanCurvature: number,
maxGaussianCurvature: number,
minGaussianCurvature: number,
vertMeanCurvature: number[],
vertGaussianCurvature: number[]
};
export type FillRule = 'EvenOdd'|'NonZero'|'Positive'|'Negative'
export type JoinType = 'Square'|'Round'|'Miter'
+7 -17
View File
@@ -15,31 +15,21 @@
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 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;
export interface ManifoldToplevel {
CrossSection: typeof T.CrossSection;
Manifold: typeof T.Manifold;
Mesh: typeof T.Mesh;
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<ManifoldStatic>;
export default function Module(config?: {locateFile: () => string}):
Promise<ManifoldToplevel>;
+1 -1693
View File
File diff suppressed because it is too large Load Diff
+2 -12
View File
@@ -8,9 +8,7 @@
"manifold.wasm",
"manifold.d.ts",
"manifold-encapsulated-types.d.ts",
"manifold-global-types.d.ts",
"examples/examples.js",
"test/test.js"
"manifold-global-types.d.ts"
],
"typings": "manifold.d.ts",
"types": "manifold.d.ts",
@@ -37,13 +35,5 @@
"bugs": {
"url": "https://github.com/elalish/manifold/issues"
},
"homepage": "https://github.com/elalish/manifold#readme",
"scripts": {
"test": "mocha -u tdd -t 30000"
},
"devDependencies": {
"chai": "*",
"mocha": "*",
"gl-matrix": "3.4.3"
}
"homepage": "https://github.com/elalish/manifold#readme"
}
-161
View File
@@ -1,161 +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 {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');
});
});
+7
View File
@@ -24,6 +24,13 @@ target_link_libraries(largeSceneTest manifold)
target_compile_options(largeSceneTest PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(largeSceneTest PUBLIC cxx_std_17)
if(MANIFOLD_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)
+67
View File
@@ -0,0 +1,67 @@
// 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;
}
Generated
+4 -4
View File
@@ -17,16 +17,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1657296039,
"narHash": "sha256-Ghh39+aS+pw5sTP/ZO8VIKE6sBhMadDaQZtf+3yu4Vc=",
"lastModified": 1689008574,
"narHash": "sha256-VFMgyHDiqsGDkRg73alv6OdHJAqhybryWHv77bSCGIw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "71d7a4c037dc4f3e98d5c4a81b941933cf5bf675",
"rev": "4a729ce4b1fe5ec4fffc71c67c96aa5184ebb462",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.05",
"ref": "nixos-unstable",
"type": "indirect"
}
},
+3 -3
View File
@@ -1,6 +1,6 @@
{
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.nixpkgs.url = "nixpkgs/nixos-22.05";
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem
@@ -25,7 +25,7 @@
"manifold-${parallel-backend}";
version = "beta";
src = self;
nativeBuildInputs = (with pkgs; [ cmake (python39.withPackages(ps: with ps; [trimesh])) ]) ++ 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"
@@ -43,7 +43,7 @@
mkdir -p $out
cp src/manifold/libmanifold.a $out/
cp extras/perfTest $out
cp bindings/python/pymanifold* $out
cp bindings/python/manifold3d* $out
'';
};
parallelBackends = [
+4 -4
View File
@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
add_subdirectory(third_party)
project(meshIO)
find_package(assimp REQUIRED)
add_library(${PROJECT_NAME} src/meshIO.cpp)
target_include_directories(${PROJECT_NAME}
@@ -23,8 +23,8 @@ target_include_directories(${PROJECT_NAME}
)
target_link_libraries(${PROJECT_NAME}
PUBLIC utilities
PRIVATE assimp
PUBLIC manifold
PRIVATE assimp::assimp
)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
+1 -6
View File
@@ -15,14 +15,10 @@
#pragma once
#include <string>
#include "public.h"
#include "manifold.h"
namespace manifold {
/** @addtogroup Debug
* @{
*/
/** @defgroup MeshIO
* @brief 3D model file I/O based on Assimp
* @{
@@ -70,5 +66,4 @@ 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,6 +79,13 @@ 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
@@ -1,27 +0,0 @@
# Copyright 2023 The Manifold Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# 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)
+1 -4
View File
@@ -24,12 +24,9 @@ class Collider {
public:
Collider() {}
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 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>
template <const bool selfCollision = false, typename T>
SparseIndices Collisions(const VecDH<T>& queriesIn) const;
private:
+26 -18
View File
@@ -157,7 +157,7 @@ struct CreateRadixTree {
}
};
template <typename T, const bool allocateOnly>
template <typename T, const bool allocateOnly, const bool selfCollision>
struct FindCollisions {
thrust::pair<int*, int*> queryTri_;
int* counts;
@@ -172,12 +172,15 @@ struct FindCollisions {
bool overlaps = nodeBBox_[node].DoesOverlap(queryObj);
if (overlaps && IsLeaf(node)) {
if (allocateOnly) {
count++;
} else {
int pos = count++;
queryTri_.first[pos] = queryIdx;
queryTri_.second[pos] = Node2Leaf(node);
const int leafIdx = Node2Leaf(node);
if (!selfCollision || leafIdx != queryIdx) {
if (allocateOnly) {
count++;
} else {
int pos = count++;
queryTri_.first[pos] = queryIdx;
queryTri_.second[pos] = leafIdx;
}
}
}
return overlaps && IsInternal(node); // Should traverse into node
@@ -267,9 +270,11 @@ Collider::Collider(const VecDH<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.
* 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.
*/
template <typename T>
template <const bool selfCollision, 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
@@ -278,17 +283,17 @@ SparseIndices Collider::Collisions(const VecDH<T>& queriesIn) const {
// 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>(
FindCollisions<T, true, selfCollision>(
{thrust::pair<int*, int*>(nullptr, nullptr), counts.ptrD(),
nodeBBox_.ptrD(), internalChildren_.ptrD()}));
// compute start index for each query and total count
exclusive_scan(policy, counts.begin(), counts.end(), counts.begin());
SparseIndices queryTri(counts.back());
// actually recording collisions
for_each_n(
policy, zip(queriesIn.cbegin(), countAt(0)), queriesIn.size(),
FindCollisions<T, false>({queryTri.ptrDpq(), counts.ptrD(),
nodeBBox_.ptrD(), internalChildren_.ptrD()}));
for_each_n(policy, zip(queriesIn.cbegin(), countAt(0)), queriesIn.size(),
FindCollisions<T, false, selfCollision>(
{queryTri.ptrDpq(), counts.ptrD(), nodeBBox_.ptrD(),
internalChildren_.ptrD()}));
return queryTri;
}
@@ -308,8 +313,8 @@ void Collider::UpdateBoxes(const VecDH<Box>& leafBB) {
// kernel over leaves to save internal Boxes
for_each_n(
policy, countAt(0), NumLeaves(),
BuildInternalBoxes({nodeBBox_.ptrD(), counter.ptrD(), nodeParent_.ptrD(),
internalChildren_.ptrD()}));
BuildInternalBoxes({nodeBBox_.ptrD(), counter.ptrD(), nodeParent_.cptrD(),
internalChildren_.cptrD()}));
}
/**
@@ -332,9 +337,12 @@ bool Collider::Transform(glm::mat4x3 transform) {
return axisAligned;
}
template SparseIndices Collider::Collisions<Box>(const VecDH<Box>&) const;
template SparseIndices Collider::Collisions<true, Box>(const VecDH<Box>&) const;
template SparseIndices Collider::Collisions<glm::vec3>(
template SparseIndices Collider::Collisions<false, Box>(
const VecDH<Box>&) const;
template SparseIndices Collider::Collisions<false, glm::vec3>(
const VecDH<glm::vec3>&) const;
} // namespace manifold
+12 -2
View File
@@ -77,6 +77,7 @@ 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);
///@}
@@ -127,7 +128,7 @@ class CrossSection {
};
CrossSection Offset(double delta, JoinType jt, double miter_limit = 2.0,
double arc_tolerance = 0.0) const;
int circularSegments = 0) const;
///@}
/** @name Boolean
@@ -143,7 +144,6 @@ class CrossSection {
CrossSection& operator-=(const CrossSection&);
CrossSection operator^(const CrossSection&) const;
CrossSection& operator^=(const CrossSection&);
CrossSection RectClip(const Rect& rect) const;
///@}
/** @name Topological
@@ -153,6 +153,15 @@ 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
*/
///@{
@@ -197,6 +206,7 @@ class Rect {
*/
///@{
glm::vec2 Size() const;
float Area() const;
float Scale() const;
glm::vec2 Center() const;
bool Contains(const glm::vec2& pt) const;
+128 -27
View File
@@ -132,6 +132,58 @@ 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 {
@@ -202,6 +254,20 @@ 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_
@@ -391,18 +457,6 @@ std::vector<CrossSection> CrossSection::Decompose() const {
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.
@@ -533,20 +587,66 @@ 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 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.
* @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.
*/
CrossSection CrossSection::Offset(double delta, JoinType jointype,
double miter_limit,
double arc_tolerance) const {
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;
}
auto ps =
C2::InflatePaths(GetPaths(), delta, jt(jointype), C2::EndType::Polygon,
miter_limit, precision_, arc_tolerance);
miter_limit, precision_, arc_tol);
return CrossSection(ps);
}
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();
for (auto path : paths) {
for (auto p : path) {
pts.push_back(v2_of_pd(p));
}
}
}
return CrossSection(C2::PathsD{HullImpl(pts)});
}
CrossSection CrossSection::Hull() const {
return Hull(std::vector<CrossSection>{*this});
}
CrossSection CrossSection::Hull(SimplePolygon pts) {
return CrossSection(C2::PathsD{HullImpl(pts)});
}
CrossSection CrossSection::Hull(const Polygons polys) {
SimplePolygon pts;
for (auto poly : polys) {
for (auto p : poly) {
pts.push_back(p);
}
}
return Hull(pts);
}
/**
* Return the total area covered by complex polygons making up the
* CrossSection.
@@ -631,6 +731,14 @@ Rect::Rect(const glm::vec2 a, const glm::vec2 b) {
*/
glm::vec2 Rect::Size() const { return max - min; }
/**
* Return the area of the rectangle.
*/
float Rect::Area() const {
auto sz = Size();
return sz.x * sz.y;
}
/**
* Returns the absolute-largest coordinate value of any contained
* point.
@@ -646,7 +754,7 @@ float Rect::Scale() const {
glm::vec2 Rect::Center() const { return 0.5f * (max + min); }
/**
* Does this rectangle contain (includes equal) the given point?
* Does this rectangle contain (includes on border) the given point?
*/
bool Rect::Contains(const glm::vec2& p) const {
return glm::all(glm::greaterThanEqual(p, min)) &&
@@ -755,13 +863,6 @@ Rect Rect::Transform(const glm::mat3x2& m) const {
* 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);
}
CrossSection Rect::AsCrossSection() const { return CrossSection(*this); }
} // namespace manifold
+1 -1
View File
@@ -25,7 +25,7 @@ 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} graphlite Clipper2
PRIVATE collider polygon ${MANIFOLD_INCLUDE} graphlite Clipper2 quickhull
)
target_compile_features(${PROJECT_NAME}
+83 -4
View File
@@ -33,6 +33,80 @@ ExecutionParams& ManifoldParams();
class CsgNode;
class CsgLeafNode;
/** @ingroup Connections
* @{
*/
/**
* An alternative to Mesh for output suitable for pushing into graphics
* libraries directly. This may not be manifold since the verts are duplicated
* along property boundaries that do not match. The additional merge vectors
* store this missing information, allowing the manifold to be reconstructed.
*/
struct MeshGL {
/// Number of property vertices
uint32_t NumVert() const { return vertProperties.size() / numProp; };
/// Number of triangles
uint32_t NumTri() const { return triVerts.size() / 3; };
/// Number of properties per vertex, always >= 3.
uint32_t numProp = 3;
/// Flat, GL-style interleaved list of all vertex properties: propVal =
/// vertProperties[vert * numProp + propIdx]. The first three properties are
/// always the position x, y, z.
std::vector<float> vertProperties;
/// The vertex indices of the three triangle corners in CCW (from the outside)
/// order, for each triangle.
std::vector<uint32_t> triVerts;
/// Optional: A list of only the vertex indicies that need to be merged to
/// reconstruct the manifold.
std::vector<uint32_t> mergeFromVert;
/// Optional: The same length as mergeFromVert, and the corresponding value
/// contains the vertex to merge with. It will have an identical position, but
/// the other properties may differ.
std::vector<uint32_t> mergeToVert;
/// Optional: Indicates runs of triangles that correspond to a particular
/// input mesh instance. The runs encompass all of triVerts and are sorted
/// by runOriginalID. Run i begins at triVerts[runIndex[i]] and ends at
/// triVerts[runIndex[i+1]]. All runIndex values are divisible by 3.
std::vector<uint32_t> runIndex;
/// Optional: The OriginalID of the mesh this triangle run came from. This ID
/// is ideal for reapplying materials to the output mesh. Multiple runs may
/// have the same ID, e.g. representing different copies of the same input
/// mesh. If you create an input MeshGL that you want to be able to reference
/// as one or more originals, be sure to set unique values from ReserveIDs().
std::vector<uint32_t> runOriginalID;
/// Optional: For each run, a 3x4 transform is stored representing how the
/// corresponding original mesh was transformed to create this triangle run.
/// This matrix is stored in column-major order and the length of the overall
/// vector is 12 * runOriginalID.size().
std::vector<float> runTransform;
/// Optional: Length NumTri, contains an ID of the source face this triangle
/// comes from. When auto-generated, this ID will be a triangle index into the
/// original mesh. All neighboring coplanar triangles from that input mesh
/// will refer to a single triangle of that group as the faceID. When
/// supplying faceIDs, ensure that triangles with the same ID are in fact
/// coplanar and have consistent properties (within some tolerance) or the
/// output will be surprising.
std::vector<uint32_t> faceID;
/// Optional: The X-Y-Z-W weighted tangent vectors for smooth Refine(). If
/// non-empty, must be exactly four times as long as Mesh.triVerts. Indexed
/// as 4 * (3 * tri + i) + j, i < 3, j < 4, representing the tangent value
/// Mesh.triVerts[tri][i] along the CCW edge. If empty, mesh is faceted.
std::vector<float> halfedgeTangent;
/// The absolute precision of the vertex positions, based on accrued rounding
/// errors. When creating a Manifold, the precision used will be the maximum
/// of this and a baseline precision from the size of the bounding box. Any
/// edge shorter than precision may be collapsed.
float precision = 0;
MeshGL() = default;
MeshGL(const Mesh& mesh);
bool Merge();
};
/** @} */
/** @defgroup Core
* @brief The central classes of the library
* @{
@@ -67,7 +141,8 @@ class Manifold {
int nDivisions = 0, float twistDegrees = 0.0f,
glm::vec2 scaleTop = glm::vec2(1.0f));
static Manifold Revolve(const CrossSection& crossSection,
int circularSegments = 0);
int circularSegments = 0,
float revolveDegrees = 360.0f);
///@}
/** @name Topological
@@ -109,7 +184,6 @@ class Manifold {
float Precision() const;
int Genus() const;
Properties GetProperties() const;
Curvature GetCurvature() const;
///@}
/** @name Mesh ID
@@ -132,6 +206,9 @@ class Manifold {
Manifold Transform(const glm::mat4x3&) const;
Manifold Mirror(glm::vec3) const;
Manifold Warp(std::function<void(glm::vec3&)>) const;
Manifold SetProperties(
int, std::function<void(float*, glm::vec3, const float*)>) const;
Manifold CalculateCurvature(int gaussianIdx, int meanIdx) const;
Manifold Refine(int) const;
// Manifold RefineToLength(float);
// Manifold RefineToPrecision(float);
@@ -157,11 +234,14 @@ class Manifold {
Manifold TrimByPlane(glm::vec3 normal, float originOffset) const;
///@}
Manifold Hull() const;
static Manifold Hull(const std::vector<Manifold>& manifolds);
static Manifold Hull(const std::vector<glm::vec3>& pts);
/** @name Testing hooks
* These are just for internal testing.
*/
///@{
bool IsManifold() const;
bool MatchesTriNormals() const;
int NumDegenerateTris() const;
int NumOverlaps(const Manifold& second) const;
@@ -173,7 +253,6 @@ class Manifold {
Manifold(std::shared_ptr<CsgNode> pNode_);
Manifold(std::shared_ptr<Impl> pImpl_);
static Manifold Invalid();
mutable std::shared_ptr<CsgNode> pNode_;
CsgLeafNode& GetCsgLeafNode() const;
+51 -34
View File
@@ -481,7 +481,10 @@ void CreateProperties(Manifold::Impl &outR, const VecDH<TriRef> &refPQ,
inP.halfedge_.cptrD(), inQ.halfedge_.cptrD(),
outR.halfedge_.cptrD(), outR.precision_}));
std::map<std::tuple<int, int, int, int>, int> propIdx;
using Entry = std::pair<glm::ivec3, int>;
int idMissProp = outR.NumVert();
std::vector<std::vector<Entry>> propIdx(outR.NumVert() + 1);
outR.meshRelation_.properties.reserve(outR.NumVert() * numProp);
int idx = 0;
for (int tri = 0; tri < numTri; ++tri) {
@@ -490,47 +493,61 @@ void CreateProperties(Manifold::Impl &outR, const VecDH<TriRef> &refPQ,
const int triPQ = refPQ[tri].tri;
const bool PQ = refPQ[tri].meshID == 0;
const int oldNumProp = PQ ? numPropP : numPropQ;
const auto &properties =
PQ ? inP.meshRelation_.properties : inQ.meshRelation_.properties;
const auto &triProp = PQ ? inP.meshRelation_.triProperties[triPQ]
: inQ.meshRelation_.triProperties[triPQ];
const glm::ivec3 &triProp = oldNumProp == 0 ? glm::ivec3(-1)
: PQ ? inP.meshRelation_.triProperties[triPQ]
: inQ.meshRelation_.triProperties[triPQ];
for (const int i : {0, 1, 2}) {
const int vert = outR.halfedge_[3 * tri + i].startVert;
const glm::vec3 uvw = bary[3 * tri + i];
const glm::vec3 &uvw = bary[3 * tri + i];
auto key = std::make_tuple(PQ, vert, -1, -1);
int edge = -1;
for (const int j : {0, 1, 2}) {
if (uvw[j] == 1) {
// On a retained vert, the propVert must also match
std::get<2>(key) = triProp[j];
edge = -1;
glm::ivec4 key(PQ, idMissProp, -1, -1);
if (oldNumProp > 0) {
key[1] = vert;
int edge = -1;
for (const int j : {0, 1, 2}) {
if (uvw[j] == 1) {
// On a retained vert, the propVert must also match
key[2] = triProp[j];
edge = -1;
break;
}
if (uvw[j] == 0) edge = j;
}
if (edge >= 0) {
// On an edge, both propVerts must match
const int p0 = triProp[Next3(edge)];
const int p1 = triProp[Prev3(edge)];
key[2] = glm::min(p0, p1);
key[3] = glm::max(p0, p1);
}
}
auto &bin = propIdx[key.y];
bool bFound = false;
for (int k = 0; k < bin.size(); ++k) {
if (bin[k].first == glm::ivec3(key.x, key.z, key.w)) {
bFound = true;
outR.meshRelation_.triProperties[tri][i] = bin[k].second;
break;
}
if (uvw[j] == 0) edge = j;
}
if (edge >= 0) {
// On an edge, both propVerts must match
const int p0 = triProp[Next3(edge)];
const int p1 = triProp[Prev3(edge)];
std::get<2>(key) = glm::min(p0, p1);
std::get<3>(key) = glm::max(p0, p1);
}
const auto it = propIdx.find(key);
if (it != propIdx.end()) {
outR.meshRelation_.triProperties[tri][i] = it->second;
continue;
}
outR.meshRelation_.triProperties[tri][i] = idx;
propIdx.insert({key, idx++});
if (bFound) continue;
bin.push_back(std::make_pair(glm::ivec3(key.x, key.z, key.w), idx));
outR.meshRelation_.triProperties[tri][i] = idx++;
for (int p = 0; p < numProp; ++p) {
glm::vec3 oldProps;
for (const int j : {0, 1, 2})
oldProps[j] = properties[numProp * triProp[j] + p];
outR.meshRelation_.properties.push_back(glm::dot(uvw, oldProps));
if (p < oldNumProp) {
glm::vec3 oldProps;
for (const int j : {0, 1, 2})
oldProps[j] = properties[oldNumProp * triProp[j] + p];
outR.meshRelation_.properties.push_back(glm::dot(uvw, oldProps));
} else {
outR.meshRelation_.properties.push_back(0);
}
}
}
}
@@ -551,12 +568,12 @@ Manifold::Impl Boolean3::Result(OpType op) const {
const int c2 = op == OpType::Add ? 1 : 0;
const int c3 = op == OpType::Intersect ? 1 : -1;
if (w03_.size() == 0) {
if (w30_.size() != 0 && op == OpType::Add) {
if (inP_.IsEmpty()) {
if (!inQ_.IsEmpty() && op == OpType::Add) {
return inQ_;
}
return Manifold::Impl();
} else if (w30_.size() == 0) {
} else if (inQ_.IsEmpty()) {
if (op == OpType::Intersect) {
return Manifold::Impl();
}
+95 -65
View File
@@ -317,92 +317,122 @@ Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
* @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.
* @param revolveDegrees Number of degrees to revolve. Default is 360 degrees.
*/
Manifold Manifold::Revolve(const CrossSection& crossSection,
int circularSegments) {
auto polygons = crossSection.ToPolygons();
int circularSegments, float revolveDegrees) {
Polygons polygons = crossSection.ToPolygons();
if (polygons.size() == 0) {
return Invalid();
}
const Rect bounds = crossSection.Bounds();
if (bounds.max.x <= 0) {
return Invalid();
} else if (bounds.min.x < 0) {
// Take the x>=0 slice.
glm::vec2 min = bounds.min;
glm::vec2 max = bounds.max;
CrossSection posBoundingBox = CrossSection(
{{0.0, min.y}, {max.x, min.y}, {max.x, max.y}, {0.0, max.y}});
polygons = (crossSection ^ posBoundingBox).ToPolygons();
}
float radius = 0.0f;
for (const auto& poly : polygons) {
for (const auto& vert : poly) {
radius = fmax(radius, vert.x);
}
}
int nDivisions = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(radius);
if (revolveDegrees > 360.0f) {
revolveDegrees = 360.0f;
}
const bool isFullRevolution = revolveDegrees == 360.0f;
const int nDivisions = circularSegments > 2
? circularSegments
: Quality::GetCircularSegments(radius);
auto pImpl_ = std::make_shared<Impl>();
auto& vertPos = pImpl_->vertPos_;
VecDH<glm::ivec3> triVertsDH;
auto& triVerts = triVertsDH;
float dPhi = 360.0f / nDivisions;
std::vector<int> startPoses;
std::vector<int> endPoses;
const float dPhi = revolveDegrees / nDivisions;
// first and last slice are distinguished if not a full revolution.
const int nSlices = isFullRevolution ? nDivisions : nDivisions + 1;
for (const auto& poly : polygons) {
int start = -1;
for (int polyVert = 0; polyVert < poly.size(); ++polyVert) {
if (poly[polyVert].x <= 0) {
start = polyVert;
break;
std::size_t nPosVerts = 0;
std::size_t nRevolveAxisVerts = 0;
for (auto& pt : poly) {
if (pt.x > 0) {
nPosVerts++;
} else {
nRevolveAxisVerts++;
}
}
if (start == -1) { // poly all positive
for (int polyVert = 0; polyVert < poly.size(); ++polyVert) {
int startVert = vertPos.size();
int lastStart =
startVert +
(polyVert == 0 ? nDivisions * (poly.size() - 1) : -nDivisions);
for (int slice = 0; slice < nDivisions; ++slice) {
int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
float phi = slice * dPhi;
glm::vec2 pos = poly[polyVert];
vertPos.push_back({pos.x * cosd(phi), pos.x * sind(phi), pos.y});
triVerts.push_back({startVert + slice, startVert + lastSlice,
lastStart + lastSlice});
triVerts.push_back(
{lastStart + lastSlice, lastStart + slice, startVert + slice});
for (int polyVert = 0; polyVert < poly.size(); ++polyVert) {
const int startPosIndex = vertPos.size();
if (!isFullRevolution) startPoses.push_back(startPosIndex);
const int prevStartPosIndex =
startPosIndex +
(polyVert == 0 ? nRevolveAxisVerts + (nSlices * nPosVerts) - nSlices
: poly[polyVert - 1].x == 0.0 ? -1
: -nSlices);
glm::vec2 currPolyVertex = poly[polyVert];
glm::vec2 prevPolyVertex =
poly[polyVert == 0 ? poly.size() - 1 : polyVert - 1];
for (int slice = 0; slice < nSlices; ++slice) {
const float phi = slice * dPhi;
if (slice == 0 || currPolyVertex.x > 0) {
vertPos.push_back({currPolyVertex.x * cosd(phi),
currPolyVertex.x * sind(phi), currPolyVertex.y});
}
if (isFullRevolution || slice > 0) {
const int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
if (currPolyVertex.x > 0.0) {
triVerts.push_back(
{startPosIndex + slice, startPosIndex + lastSlice,
// "Reuse" vertex of first slice if it lies on the revolve axis
(prevPolyVertex.x == 0.0 ? prevStartPosIndex
: prevStartPosIndex + lastSlice)});
}
if (prevPolyVertex.x > 0.0) {
triVerts.push_back(
{prevStartPosIndex + lastSlice, prevStartPosIndex + slice,
(currPolyVertex.x == 0.0 ? startPosIndex
: startPosIndex + slice)});
}
}
}
} else { // poly crosses zero
int polyVert = start;
glm::vec2 pos = poly[polyVert];
do {
glm::vec2 lastPos = pos;
polyVert = (polyVert + 1) % poly.size();
pos = poly[polyVert];
if (pos.x > 0) {
if (lastPos.x <= 0) {
float a = pos.x / (pos.x - lastPos.x);
vertPos.push_back({0.0f, 0.0f, glm::mix(pos.y, lastPos.y, a)});
}
int startVert = vertPos.size();
for (int slice = 0; slice < nDivisions; ++slice) {
int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
float phi = slice * dPhi;
glm::vec2 pos = poly[polyVert];
vertPos.push_back({pos.x * cosd(phi), pos.x * sind(phi), pos.y});
if (lastPos.x > 0) {
triVerts.push_back({startVert + slice, startVert + lastSlice,
startVert - nDivisions + lastSlice});
triVerts.push_back({startVert - nDivisions + lastSlice,
startVert - nDivisions + slice,
startVert + slice});
} else {
triVerts.push_back(
{startVert - 1, startVert + slice, startVert + lastSlice});
}
}
} else if (lastPos.x > 0) {
int startVert = vertPos.size();
float a = pos.x / (pos.x - lastPos.x);
vertPos.push_back({0.0f, 0.0f, glm::mix(pos.y, lastPos.y, a)});
for (int slice = 0; slice < nDivisions; ++slice) {
int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
triVerts.push_back({startVert, startVert - nDivisions + lastSlice,
startVert - nDivisions + slice});
}
}
} while (polyVert != start);
if (!isFullRevolution) endPoses.push_back(vertPos.size() - 1);
}
}
// Add front and back triangles if not a full revolution.
if (!isFullRevolution) {
std::vector<glm::ivec3> frontTriangles =
Triangulate(polygons, pImpl_->precision_);
for (auto& t : frontTriangles) {
triVerts.push_back({startPoses[t.x], startPoses[t.y], startPoses[t.z]});
}
for (auto& t : frontTriangles) {
triVerts.push_back({endPoses[t.z], endPoses[t.y], endPoses[t.x]});
}
}
+76 -20
View File
@@ -18,6 +18,7 @@
#include "boolean3.h"
#include "impl.h"
#include "mesh_fixes.h"
#include "par.h"
namespace {
@@ -30,16 +31,6 @@ struct Transform4x3 {
}
};
struct TransformNormals {
const glm::mat3 transform;
__host__ __device__ glm::vec3 operator()(glm::vec3 normal) {
normal = glm::normalize(transform * normal);
if (isnan(normal.x)) normal = glm::vec3(0.0f);
return normal;
}
};
struct UpdateHalfedge {
const int nextVert;
const int nextEdge;
@@ -54,6 +45,15 @@ struct UpdateHalfedge {
}
};
struct UpdateTriProp {
const int nextProp;
__host__ __device__ glm::ivec3 operator()(glm::ivec3 tri) {
tri += nextProp;
return tri;
}
};
struct UpdateMeshIDs {
const int offset;
@@ -151,9 +151,12 @@ Manifold::Impl CsgLeafNode::Compose(
int numVert = 0;
int numEdge = 0;
int numTri = 0;
int numPropVert = 0;
std::vector<int> vertIndices;
std::vector<int> edgeIndices;
std::vector<int> triIndices;
std::vector<int> propVertIndices;
int numPropOut = 0;
for (auto &node : nodes) {
float nodeOldScale = node->pImpl_->bBox_.Scale();
float nodeNewScale =
@@ -167,9 +170,15 @@ Manifold::Impl CsgLeafNode::Compose(
vertIndices.push_back(numVert);
edgeIndices.push_back(numEdge * 2);
triIndices.push_back(numTri);
propVertIndices.push_back(numPropVert);
numVert += node->pImpl_->NumVert();
numEdge += node->pImpl_->NumEdge();
numTri += node->pImpl_->NumTri();
const int numProp = node->pImpl_->NumProp();
numPropOut = glm::max(numPropOut, numProp);
numPropVert +=
numProp == 0 ? 1
: node->pImpl_->meshRelation_.properties.size() / numProp;
}
Manifold::Impl combined;
@@ -179,6 +188,11 @@ Manifold::Impl CsgLeafNode::Compose(
combined.faceNormal_.resize(numTri);
combined.halfedgeTangent_.resize(2 * numEdge);
combined.meshRelation_.triRef.resize(numTri);
if (numPropOut > 0) {
combined.meshRelation_.numProp = numPropOut;
combined.meshRelation_.properties.resize(numPropOut * numPropVert, 0);
combined.meshRelation_.triProperties.resize(numTri);
}
auto policy = autoPolicy(numTri);
// if we are already parallelizing for each node, do not perform multithreaded
@@ -191,9 +205,44 @@ Manifold::Impl CsgLeafNode::Compose(
for_each_n_host(
nodes.size() > 1 ? ExecutionPolicy::Par : ExecutionPolicy::Seq,
countAt(0), nodes.size(),
[&nodes, &vertIndices, &edgeIndices, &triIndices, &combined,
policy](int i) {
[&nodes, &vertIndices, &edgeIndices, &triIndices, &propVertIndices,
numPropOut, &combined, policy](int i) {
auto &node = nodes[i];
copy(policy, node->pImpl_->halfedgeTangent_.begin(),
node->pImpl_->halfedgeTangent_.end(),
combined.halfedgeTangent_.begin() + edgeIndices[i]);
transform(
policy, node->pImpl_->halfedge_.begin(),
node->pImpl_->halfedge_.end(),
combined.halfedge_.begin() + edgeIndices[i],
UpdateHalfedge({vertIndices[i], edgeIndices[i], triIndices[i]}));
if (numPropOut > 0) {
auto start =
combined.meshRelation_.triProperties.begin() + triIndices[i];
if (node->pImpl_->NumProp() > 0) {
auto &triProp = node->pImpl_->meshRelation_.triProperties;
transform(policy, triProp.begin(), triProp.end(), start,
UpdateTriProp({propVertIndices[i]}));
const int numProp = node->pImpl_->NumProp();
auto &oldProp = node->pImpl_->meshRelation_.properties;
auto &newProp = combined.meshRelation_.properties;
for (int p = 0; p < numProp; ++p) {
strided_range<VecDH<float>::IterC> oldRange(
oldProp.begin() + p, oldProp.end(), numProp);
strided_range<VecDH<float>::Iter> newRange(
newProp.begin() + numPropOut * propVertIndices[i] + p,
newProp.end(), numPropOut);
copy(policy, oldRange.begin(), oldRange.end(), newRange.begin());
}
} else {
// point all triangles at single new property of zeros.
fill(policy, start, start + node->pImpl_->NumTri(),
glm::ivec3(propVertIndices[i]));
}
}
if (node->transform_ == glm::mat4x3(1.0f)) {
copy(policy, node->pImpl_->vertPos_.begin(),
node->pImpl_->vertPos_.end(),
@@ -215,15 +264,22 @@ Manifold::Impl CsgLeafNode::Compose(
combined.vertPos_.begin() + vertIndices[i]);
copy_n(policy, faceNormalBegin, node->pImpl_->faceNormal_.size(),
combined.faceNormal_.begin() + triIndices[i]);
const bool invert = glm::determinant(glm::mat3(node->transform_)) < 0;
for_each_n(policy,
zip(combined.halfedgeTangent_.begin() + edgeIndices[i],
countAt(0)),
node->pImpl_->halfedgeTangent_.size(),
TransformTangents{glm::mat3(node->transform_), invert,
node->pImpl_->halfedgeTangent_.cptrD(),
node->pImpl_->halfedge_.cptrD()});
if (invert)
for_each_n(policy,
zip(combined.meshRelation_.triRef.begin(),
countAt(triIndices[i])),
node->pImpl_->NumTri(),
FlipTris({combined.halfedge_.ptrD()}));
}
copy(policy, node->pImpl_->halfedgeTangent_.begin(),
node->pImpl_->halfedgeTangent_.end(),
combined.halfedgeTangent_.begin() + edgeIndices[i]);
transform(
policy, node->pImpl_->halfedge_.begin(),
node->pImpl_->halfedge_.end(),
combined.halfedge_.begin() + edgeIndices[i],
UpdateHalfedge({vertIndices[i], edgeIndices[i], triIndices[i]}));
// Since the nodes may be copies containing the same meshIDs, it is
// important to add an offset so that each node instance gets
// unique meshIDs.
+145 -56
View File
@@ -49,7 +49,7 @@ struct ShortEdge {
const glm::vec3* vertPos;
const float precision;
__host__ __device__ bool operator()(int edge) {
__host__ __device__ bool operator()(int edge) const {
if (halfedge[edge].pairedHalfedge < 0) return false;
// Flag short edges
const glm::vec3 delta =
@@ -62,7 +62,7 @@ struct FlagEdge {
const Halfedge* halfedge;
const TriRef* triRef;
__host__ __device__ bool operator()(int edge) {
__host__ __device__ bool operator()(int edge) const {
if (halfedge[edge].pairedHalfedge < 0) return false;
// Flag redundant edges - those where the startVert is surrounded by only
// two original triangles.
@@ -87,7 +87,7 @@ struct SwappableEdge {
const glm::vec3* triNormal;
const float precision;
__host__ __device__ bool operator()(int edge) {
__host__ __device__ bool operator()(int edge) const {
if (halfedge[edge].pairedHalfedge < 0) return false;
int tri = halfedge[edge].face;
@@ -110,6 +110,15 @@ struct SwappableEdge {
Is01Longest(v[0], v[1], v[2]);
}
};
struct SortEntry {
int start;
int end;
int index;
inline bool operator<(const SortEntry& other) const {
return start == other.start ? end < other.end : start < other.start;
}
};
} // namespace
namespace manifold {
@@ -136,56 +145,96 @@ namespace manifold {
void Manifold::Impl::SimplifyTopology() {
if (!halfedge_.size()) return;
int nbEdges = halfedge_.size();
int numFlagged = 0;
VecDH<uint8_t> bflags(nbEdges);
uint8_t* bflagsPtr = bflags.ptrD();
VecDH<SortEntry> entries(nbEdges);
SortEntry* entriesPtr = entries.ptrD();
auto policy = autoPolicy(halfedge_.size());
for_each_n(policy, countAt(0), nbEdges, [=] __host__ __device__(int i) {
entriesPtr[i].start = halfedge_[i].startVert;
entriesPtr[i].end = halfedge_[i].endVert;
entriesPtr[i].index = i;
});
VecDH<Halfedge> halfedge(halfedge_);
VecDH<int> idx(halfedge_.size());
sequence(policy, idx.begin(), idx.end());
sort_by_key(policy, halfedge.begin(), halfedge.end(), idx.begin());
VecDH<int> flaggedEdges(halfedge_.size());
int numFlagged =
copy_if<decltype(flaggedEdges.begin())>(
policy, idx.begin(), idx.end() - 1, countAt(0), flaggedEdges.begin(),
DuplicateEdge({halfedge.cptrD()})) -
flaggedEdges.begin();
flaggedEdges.resize(numFlagged);
for (const int edge : flaggedEdges) DedupeEdge(edge);
flaggedEdges.resize(halfedge_.size());
numFlagged =
copy_if<decltype(flaggedEdges.begin())>(
policy, countAt(0), countAt(halfedge_.size()), flaggedEdges.begin(),
ShortEdge({halfedge_.cptrD(), vertPos_.cptrD(), precision_})) -
flaggedEdges.begin();
flaggedEdges.resize(numFlagged);
for (const int edge : flaggedEdges) CollapseEdge(edge);
flaggedEdges.resize(halfedge_.size());
numFlagged =
copy_if<decltype(flaggedEdges.begin())>(
policy, countAt(0), countAt(halfedge_.size()), flaggedEdges.begin(),
FlagEdge({halfedge_.cptrD(), meshRelation_.triRef.cptrD()})) -
flaggedEdges.begin();
flaggedEdges.resize(numFlagged);
for (const int edge : flaggedEdges) CollapseEdge(edge);
flaggedEdges.resize(halfedge_.size());
numFlagged =
copy_if<decltype(flaggedEdges.begin())>(
policy, countAt(0), countAt(halfedge_.size()), flaggedEdges.begin(),
SwappableEdge({halfedge_.cptrD(), vertPos_.cptrD(),
faceNormal_.cptrD(), precision_})) -
flaggedEdges.begin();
flaggedEdges.resize(numFlagged);
for (const int edge : flaggedEdges) {
RecursiveEdgeSwap(edge);
sort(policy, entries.begin(), entries.end());
for (int i = 0; i < nbEdges - 1; ++i) {
if (entries[i].start == entries[i + 1].start &&
entries[i].end == entries[i + 1].end) {
DedupeEdge(entries[i].index);
numFlagged++;
}
}
#ifdef MANIFOLD_DEBUG
if (ManifoldParams().verbose && numFlagged > 0) {
std::cout << "found " << numFlagged << " duplicate edges to split"
<< std::endl;
}
#endif
{
numFlagged = 0;
ShortEdge se{halfedge_.cptrD(), vertPos_.cptrD(), precision_};
for_each_n(policy, countAt(0), nbEdges,
[=] __host__ __device__(int i) { bflagsPtr[i] = se(i); });
for (int i = 0; i < nbEdges; ++i) {
if (bflags[i]) {
CollapseEdge(i);
numFlagged++;
}
}
}
#ifdef MANIFOLD_DEBUG
if (ManifoldParams().verbose && numFlagged > 0) {
std::cout << "found " << numFlagged << " short edges to collapse"
<< std::endl;
}
#endif
{
numFlagged = 0;
FlagEdge se{halfedge_.cptrD(), meshRelation_.triRef.cptrD()};
for_each_n(policy, countAt(0), nbEdges,
[=] __host__ __device__(int i) { bflagsPtr[i] = se(i); });
for (int i = 0; i < nbEdges; ++i) {
if (bflags[i]) {
CollapseEdge(i);
numFlagged++;
}
}
}
#ifdef MANIFOLD_DEBUG
if (ManifoldParams().verbose && numFlagged > 0) {
std::cout << "found " << numFlagged << " colinear edges to collapse"
<< std::endl;
}
#endif
{
numFlagged = 0;
SwappableEdge se{halfedge_.cptrD(), vertPos_.cptrD(), faceNormal_.cptrD(),
precision_};
for_each_n(policy, countAt(0), nbEdges,
[=] __host__ __device__(int i) { bflagsPtr[i] = se(i); });
for (int i = 0; i < nbEdges; ++i) {
if (bflags[i]) {
RecursiveEdgeSwap(i);
numFlagged++;
}
}
}
#ifdef MANIFOLD_DEBUG
if (ManifoldParams().verbose && numFlagged > 0) {
std::cout << "found " << numFlagged << " edges to swap" << std::endl;
}
#endif
}
void Manifold::Impl::DedupeEdge(const int edge) {
@@ -253,11 +302,13 @@ void Manifold::Impl::PairUp(int edge0, int edge1) {
// (edgeEdge.endVert must == startEdge.endVert), updating each edge to point
// to vert instead.
void Manifold::Impl::UpdateVert(int vert, int startEdge, int endEdge) {
while (startEdge != endEdge) {
halfedge_[startEdge].endVert = vert;
startEdge = NextHalfedge(startEdge);
halfedge_[startEdge].startVert = vert;
startEdge = halfedge_[startEdge].pairedHalfedge;
int current = startEdge;
while (current != endEdge) {
halfedge_[current].endVert = vert;
current = NextHalfedge(current);
halfedge_[current].startVert = vert;
current = halfedge_[current].pairedHalfedge;
ASSERT(current != startEdge, logicErr, "infinite loop in decimator!");
}
}
@@ -298,8 +349,23 @@ void Manifold::Impl::RemoveIfFolded(int edge) {
const glm::ivec3 tri0edge = TriOf(edge);
const glm::ivec3 tri1edge = TriOf(halfedge_[edge].pairedHalfedge);
if (halfedge_[tri0edge[1]].endVert == halfedge_[tri1edge[1]].endVert) {
if (halfedge_[tri0edge[1]].pairedHalfedge == tri1edge[2]) {
if (halfedge_[tri0edge[2]].pairedHalfedge == tri1edge[1]) {
for (int i : {0, 1, 2})
vertPos_[halfedge_[tri0edge[i]].startVert] = glm::vec3(NAN);
} else {
vertPos_[halfedge_[tri0edge[1]].startVert] = glm::vec3(NAN);
}
} else {
if (halfedge_[tri0edge[2]].pairedHalfedge == tri1edge[1]) {
vertPos_[halfedge_[tri1edge[1]].startVert] = glm::vec3(NAN);
}
}
PairUp(halfedge_[tri0edge[1]].pairedHalfedge,
halfedge_[tri1edge[2]].pairedHalfedge);
PairUp(halfedge_[tri0edge[2]].pairedHalfedge,
halfedge_[tri1edge[1]].pairedHalfedge);
for (int i : {0, 1, 2}) {
vertPos_[halfedge_[tri0edge[i]].startVert] = glm::vec3(NAN);
halfedge_[tri0edge[i]] = {-1, -1, -1, -1};
halfedge_[tri1edge[i]] = {-1, -1, -1, -1};
}
@@ -519,4 +585,27 @@ void Manifold::Impl::RecursiveEdgeSwap(const int edge) {
RecursiveEdgeSwap(halfedge_[tri0edge[1]].pairedHalfedge);
RecursiveEdgeSwap(halfedge_[tri1edge[0]].pairedHalfedge);
}
void Manifold::Impl::SplitPinchedVerts() {
std::vector<bool> vertProcessed(NumVert(), false);
std::vector<bool> halfedgeProcessed(halfedge_.size(), false);
for (int i = 0; i < halfedge_.size(); ++i) {
if (halfedgeProcessed[i]) continue;
int vert = halfedge_[i].startVert;
if (vertProcessed[vert]) {
vertPos_.push_back(vertPos_[vert]);
vert = NumVert() - 1;
} else {
vertProcessed[vert] = true;
}
int current = i;
do {
halfedgeProcessed[current] = true;
halfedge_[current].startVert = vert;
current = halfedge_[current].pairedHalfedge;
halfedge_[current].endVert = vert;
current = NextHalfedge(current);
} while (current != i);
}
}
} // namespace manifold
+99 -72
View File
@@ -23,11 +23,14 @@
#include "graph.h"
#include "hashtable.h"
#include "mesh_fixes.h"
#include "par.h"
namespace {
using namespace manifold;
constexpr uint64_t kRemove = std::numeric_limits<uint64_t>::max();
__host__ __device__ void AtomicAddVec3(glm::vec3& target,
const glm::vec3& add) {
for (int i : {0, 1, 2}) {
@@ -43,12 +46,6 @@ __host__ __device__ void AtomicAddVec3(glm::vec3& target,
}
}
__host__ __device__ int FlipHalfedge(int halfedge) {
const int tri = halfedge / 3;
const int vert = 2 - (halfedge - 3 * tri);
return 3 * tri + vert;
}
struct Normalize {
__host__ __device__ void operator()(glm::vec3& v) { v = SafeNormalize(v); }
};
@@ -61,52 +58,6 @@ struct Transform4x3 {
}
};
struct TransformNormals {
const glm::mat3 transform;
__host__ __device__ glm::vec3 operator()(glm::vec3 normal) {
normal = glm::normalize(transform * normal);
if (isnan(normal.x)) normal = glm::vec3(0.0f);
return normal;
}
};
struct TransformTangents {
const glm::mat3 transform;
const bool invert;
const glm::vec4* oldTangents;
const Halfedge* halfedge;
__host__ __device__ void operator()(thrust::tuple<glm::vec4&, int> inOut) {
glm::vec4& tangent = thrust::get<0>(inOut);
int edge = thrust::get<1>(inOut);
if (invert) {
edge = halfedge[FlipHalfedge(edge)].pairedHalfedge;
}
tangent = glm::vec4(transform * glm::vec3(oldTangents[edge]),
oldTangents[edge].w);
}
};
struct FlipTris {
Halfedge* halfedge;
__host__ __device__ void operator()(thrust::tuple<TriRef&, int> inOut) {
TriRef& bary = thrust::get<0>(inOut);
const int tri = thrust::get<1>(inOut);
thrust::swap(halfedge[3 * tri], halfedge[3 * tri + 2]);
for (const int i : {0, 1, 2}) {
thrust::swap(halfedge[3 * tri + i].startVert,
halfedge[3 * tri + i].endVert);
halfedge[3 * tri + i].pairedHalfedge =
FlipHalfedge(halfedge[3 * tri + i].pairedHalfedge);
}
}
};
struct AssignNormals {
glm::vec3* vertNormal;
const glm::vec3* vertPos;
@@ -242,20 +193,17 @@ struct CoplanarEdge {
const int edgeIdx = thrust::get<2>(inOut);
const Halfedge edge = halfedge[edgeIdx];
if (!edge.IsForward()) return;
const Halfedge pair = halfedge[edge.pairedHalfedge];
if (triRef[edge.face].meshID != triRef[pair.face].meshID) return;
const glm::vec3 base = vertPos[edge.startVert];
const int baseNum = edgeIdx - 3 * edge.face;
const int jointNum = edge.pairedHalfedge - 3 * pair.face;
const int edgeNum = baseNum == 0 ? 2 : baseNum - 1;
const int pairNum = jointNum == 0 ? 2 : jointNum - 1;
if (numProp > 0) {
const int prop0 = triProp[edge.face][baseNum];
const int prop1 = triProp[edge.face][edgeNum];
const int prop1 = triProp[pair.face][jointNum == 2 ? 0 : jointNum + 1];
bool propEqual = true;
for (int p = 0; p < numProp; ++p) {
if (glm::abs(prop[numProp * prop0 + p] - prop[numProp * prop1 + p]) >
@@ -270,6 +218,10 @@ struct CoplanarEdge {
}
}
if (!edge.IsForward()) return;
const int edgeNum = baseNum == 0 ? 2 : baseNum - 1;
const int pairNum = jointNum == 0 ? 2 : jointNum - 1;
const glm::vec3 jointVec = vertPos[pair.startVert] - base;
const glm::vec3 edgeVec =
vertPos[halfedge[3 * edge.face + edgeNum].startVert] - base;
@@ -324,6 +276,36 @@ struct CoplanarEdge {
}
};
struct CheckCoplanarity {
int* comp2tri;
const Halfedge* halfedge;
const glm::vec3* vertPos;
const int* components;
const float precision;
__host__ __device__ void operator()(int tri) {
const int component = components[tri];
const int referenceTri = comp2tri[component];
if (referenceTri < 0 || referenceTri == tri) return;
const glm::vec3 origin = vertPos[halfedge[3 * referenceTri].startVert];
const glm::vec3 normal = glm::normalize(
glm::cross(vertPos[halfedge[3 * referenceTri + 1].startVert] - origin,
vertPos[halfedge[3 * referenceTri + 2].startVert] - origin));
for (const int i : {0, 1, 2}) {
const glm::vec3 vert = vertPos[halfedge[3 * tri + i].startVert];
// If any component vertex is not coplanar with the component's reference
// triangle, unmark the entire component so that none of its triangles are
// marked coplanar.
if (glm::abs(glm::dot(normal, vert - origin)) > precision) {
comp2tri[component] = -1;
break;
}
}
}
};
struct EdgeBox {
const glm::vec3* vertPos;
@@ -376,6 +358,7 @@ uint32_t Manifold::Impl::ReserveIDs(uint32_t n) {
Manifold::Impl::Impl(const MeshGL& meshGL,
std::vector<float> propertyTolerance) {
Mesh mesh;
mesh.precision = meshGL.precision;
const int numVert = meshGL.NumVert();
const int numTri = meshGL.NumTri();
@@ -507,10 +490,25 @@ Manifold::Impl::Impl(const MeshGL& meshGL,
Manifold::Impl::Impl(const Mesh& mesh, const MeshRelationD& relation,
const std::vector<float>& propertyTolerance,
bool hasFaceIDs)
: vertPos_(mesh.vertPos),
halfedgeTangent_(mesh.halfedgeTangent),
meshRelation_(relation) {
VecDH<glm::ivec3> triVerts = mesh.triVerts;
: vertPos_(mesh.vertPos), halfedgeTangent_(mesh.halfedgeTangent) {
meshRelation_ = {relation.originalID, relation.numProp, relation.properties,
relation.meshIDtransform};
VecDH<glm::ivec3> triVerts;
for (int i = 0; i < mesh.triVerts.size(); ++i) {
const glm::ivec3 tri = mesh.triVerts[i];
// Remove topological degenerates
if (tri[0] != tri[1] && tri[1] != tri[2] && tri[2] != tri[0]) {
triVerts.push_back(tri);
if (relation.triRef.size() > 0) {
meshRelation_.triRef.push_back(relation.triRef[i]);
}
if (relation.triProperties.size() > 0) {
meshRelation_.triProperties.push_back(relation.triProperties[i]);
}
}
}
if (!IsIndexInBounds(triVerts)) {
MarkFailure(Error::VertexOutOfBounds);
return;
@@ -522,13 +520,16 @@ Manifold::Impl::Impl(const Mesh& mesh, const MeshRelationD& relation,
MarkFailure(Error::NonFiniteVertex);
return;
}
SetPrecision();
SetPrecision(mesh.precision);
CreateHalfedges(triVerts);
if (!IsManifold()) {
MarkFailure(Error::NotManifold);
return;
}
SplitPinchedVerts();
CalculateNormals();
InitializeOriginal();
@@ -659,9 +660,20 @@ void Manifold::Impl::CreateFaces(const std::vector<float>& propertyTolerance) {
}
}
VecDH<int> componentsD(components);
VecDH<int> comp2triD(comp2tri);
for_each_n(
autoPolicy(halfedge_.size()), countAt(0), NumTri(),
CheckCoplanarity({comp2triD.ptrD(), halfedge_.cptrD(), vertPos_.cptrD(),
componentsD.cptrD(), precision_}));
VecDH<TriRef>& triRef = meshRelation_.triRef;
for (int tri = 0; tri < NumTri(); ++tri)
triRef[tri].tri = comp2tri[components[tri]];
for (int tri = 0; tri < NumTri(); ++tri) {
const int referenceTri = comp2triD[components[tri]];
if (referenceTri >= 0) {
triRef[tri].tri = referenceTri;
}
}
}
/**
@@ -669,12 +681,12 @@ void Manifold::Impl::CreateFaces(const std::vector<float>& propertyTolerance) {
*/
void Manifold::Impl::CreateHalfedges(const VecDH<glm::ivec3>& triVerts) {
const int numTri = triVerts.size();
const int numEdge = 3 * numTri / 2;
const int numHalfedge = 3 * numTri;
// drop the old value first to avoid copy
halfedge_.resize(0);
halfedge_.resize(2 * numEdge);
VecDH<uint64_t> edge(2 * numEdge);
VecDH<int> ids(2 * numEdge);
halfedge_.resize(numHalfedge);
VecDH<uint64_t> edge(numHalfedge);
VecDH<int> ids(numHalfedge);
auto policy = autoPolicy(numTri);
sequence(policy, ids.begin(), ids.end());
for_each_n(policy, zip(countAt(0), triVerts.begin()), numTri,
@@ -688,8 +700,8 @@ void Manifold::Impl::CreateHalfedges(const VecDH<glm::ivec3>& triVerts) {
// Once sorted, the first half of the range is the forward halfedges, which
// correspond to their backward pair at the same offset in the second half
// of the range.
for_each_n(policy, countAt(0), numEdge,
LinkHalfedges({halfedge_.ptrD(), ids.ptrD(), numEdge}));
for_each_n(policy, countAt(0), numHalfedge / 2,
LinkHalfedges({halfedge_.ptrD(), ids.ptrD(), numHalfedge / 2}));
}
/**
@@ -715,6 +727,22 @@ void Manifold::Impl::MarkFailure(Error status) {
status_ = status;
}
void Manifold::Impl::Warp(std::function<void(glm::vec3&)> warpFunc) {
thrust::for_each_n(thrust::host, vertPos_.begin(), NumVert(), warpFunc);
CalculateBBox();
if (!IsFinite()) {
MarkFailure(Error::NonFiniteVertex);
return;
}
Update();
faceNormal_.resize(0); // force recalculation of triNormal
CalculateNormals();
SetPrecision();
CreateFaces();
SimplifyTopology();
Finish();
}
Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3& transform_) const {
if (transform_ == glm::mat4x3(1.0f)) return *this;
auto policy = autoPolicy(NumVert());
@@ -778,8 +806,7 @@ Manifold::Impl Manifold::Impl::Transform(const glm::mat4x3& transform_) const {
* by the optional input.
*/
void Manifold::Impl::SetPrecision(float minPrecision) {
precision_ = glm::max(minPrecision, kTolerance * bBox_.Scale());
if (!glm::isfinite(precision_)) precision_ = -1;
precision_ = MaxPrecision(minPrecision, bBox_);
}
/**
+5 -3
View File
@@ -37,10 +37,10 @@ struct Manifold::Impl {
/// The originalID of this Manifold if it is an original; -1 otherwise.
int originalID = -1;
int numProp = 0;
VecDH<TriRef> triRef;
VecDH<glm::ivec3> triProperties;
VecDH<float> properties;
std::map<int, Relation> meshIDtransform;
VecDH<TriRef> triRef;
VecDH<glm::ivec3> triProperties;
};
Box bBox_;
@@ -75,6 +75,7 @@ struct Manifold::Impl {
void Update();
void MarkFailure(Error status);
void Warp(std::function<void(glm::vec3&)> warpFunc);
Impl Transform(const glm::mat4x3& transform) const;
SparseIndices EdgeCollisions(const Impl& B) const;
SparseIndices VertexCollisionsZ(const VecDH<glm::vec3>& vertsIn) const;
@@ -91,7 +92,7 @@ struct Manifold::Impl {
// properties.cu
Properties GetProperties() const;
Curvature GetCurvature() const;
void CalculateCurvature(int gaussianIdx, int meanIdx);
void CalculateBBox();
bool IsFinite() const;
bool IsIndexInBounds(const VecDH<glm::ivec3>& triVerts) const;
@@ -126,6 +127,7 @@ struct Manifold::Impl {
void UpdateVert(int vert, int startEdge, int endEdge);
void FormLoop(int current, int end);
void CollapseTri(const glm::ivec3& triEdge);
void SplitPinchedVerts();
// smoothing.cu
void CreateTangents(const std::vector<Smoothness>&);
+159 -35
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include <QuickHull.hpp>
#include <algorithm>
#include <map>
#include <numeric>
@@ -40,6 +41,26 @@ struct MakeTri {
}
};
struct UpdateProperties {
float* properties;
const int numProp;
const float* oldProperties;
const int numOldProp;
const glm::vec3* vertPos;
const glm::ivec3* triProperties;
const Halfedge* halfedges;
std::function<void(float*, glm::vec3, const float*)> propFunc;
__host__ __device__ void operator()(int tri) {
for (int i : {0, 1, 2}) {
const int vert = halfedges[3 * tri + i].startVert;
const int propVert = triProperties[tri][i];
propFunc(properties + numProp * propVert, vertPos[vert],
oldProperties + numOldProp * propVert);
}
}
};
Manifold Halfspace(Box bBox, glm::vec3 normal, float originOffset) {
normal = glm::normalize(normal);
Manifold cutter =
@@ -137,6 +158,7 @@ Mesh Manifold::GetMesh() const {
const Impl& impl = *GetCsgLeafNode().GetImpl();
Mesh result;
result.precision = Precision();
result.vertPos.insert(result.vertPos.end(), impl.vertPos_.begin(),
impl.vertPos_.end());
result.vertNormal.insert(result.vertNormal.end(), impl.vertNormal_.begin(),
@@ -178,6 +200,7 @@ MeshGL Manifold::GetMeshGL(glm::ivec3 normalIdx) const {
!isOriginal && glm::all(glm::greaterThan(normalIdx, glm::ivec3(2)));
MeshGL out;
out.precision = Precision();
out.numProp = 3 + numProp;
out.triVerts.resize(3 * numTri);
@@ -262,7 +285,9 @@ MeshGL Manifold::GetMeshGL(glm::ivec3 normalIdx) const {
// Duplicate verts with different props
std::vector<int> vert2idx(impl.NumVert(), -1);
std::map<std::pair<int, int>, int> vertPropPair;
std::vector<std::vector<glm::ivec2>> vertPropPair(impl.NumVert());
out.vertProperties.reserve(numVert * out.numProp);
for (int run = 0; run < out.runOriginalID.size(); ++run) {
for (int tri = out.runIndex[run] / 3; tri < out.runIndex[run + 1] / 3;
++tri) {
@@ -272,14 +297,19 @@ MeshGL Manifold::GetMeshGL(glm::ivec3 normalIdx) const {
const int prop = triProp[i];
const int vert = out.triVerts[3 * tri + i];
const auto it = vertPropPair.find({vert, prop});
if (it != vertPropPair.end()) {
out.triVerts[3 * tri + i] = it->second;
continue;
auto& bin = vertPropPair[vert];
bool bFound = false;
for (int k = 0; k < bin.size(); ++k) {
if (bin[k].x == prop) {
bFound = true;
out.triVerts[3 * tri + i] = bin[k].y;
break;
}
}
if (bFound) continue;
const int idx = out.vertProperties.size() / out.numProp;
vertPropPair.insert({{vert, prop}, idx});
out.triVerts[3 * tri + i] = idx;
bin.push_back({prop, idx});
for (int p : {0, 1, 2}) {
out.vertProperties.push_back(impl.vertPos_[vert][p]);
@@ -288,6 +318,7 @@ MeshGL Manifold::GetMeshGL(glm::ivec3 normalIdx) const {
out.vertProperties.push_back(
impl.meshRelation_.properties[prop * numProp + p]);
}
if (updateNormals) {
glm::vec3 normal;
const int start = out.vertProperties.size() - out.numProp;
@@ -379,27 +410,12 @@ int Manifold::Genus() const {
}
/**
* Returns the surface area and volume of the manifold. These properties are
* clamped to zero for a given face if they are within the Precision(). This
* means degenerate manifolds can by identified by testing these properties as
* == 0.
* Returns the surface area and volume of the manifold.
*/
Properties Manifold::GetProperties() const {
return GetCsgLeafNode().GetImpl()->GetProperties();
}
/**
* 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.
*/
Curvature Manifold::GetCurvature() const {
return GetCsgLeafNode().GetImpl()->GetCurvature();
}
/**
* 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
@@ -434,13 +450,6 @@ uint32_t Manifold::ReserveIDs(uint32_t n) {
return Manifold::Impl::ReserveIDs(n);
}
/**
* Should always be true. Also checks saneness of the internal data structures.
*/
bool Manifold::IsManifold() const {
return GetCsgLeafNode().GetImpl()->Is2Manifold();
}
/**
* The triangle normal vectors are saved over the course of operations rather
* than recalculated to avoid rounding error. This checks that triangles still
@@ -551,12 +560,81 @@ Manifold Manifold::Mirror(glm::vec3 normal) const {
*/
Manifold Manifold::Warp(std::function<void(glm::vec3&)> warpFunc) const {
auto pImpl = std::make_shared<Impl>(*GetCsgLeafNode().GetImpl());
thrust::for_each_n(thrust::host, pImpl->vertPos_.begin(), NumVert(),
warpFunc);
pImpl->Update();
pImpl->faceNormal_.resize(0); // force recalculation of triNormal
pImpl->CalculateNormals();
pImpl->SetPrecision();
pImpl->Warp(warpFunc);
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
}
/**
* Create a new copy of this manifold with updated vertex properties by
* supplying a function that takes the existing position and properties as
* 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.
*/
Manifold Manifold::SetProperties(
int numProp, std::function<void(float* newProp, glm::vec3 position,
const float* oldProp)>
propFunc) const {
auto pImpl = std::make_shared<Impl>(*GetCsgLeafNode().GetImpl());
const int oldNumProp = NumProp();
const VecDH<float> oldProperties = pImpl->meshRelation_.properties;
auto& triProperties = pImpl->meshRelation_.triProperties;
if (numProp == 0) {
triProperties.resize(0);
pImpl->meshRelation_.properties.resize(0);
} else {
if (triProperties.size() == 0) {
const int numTri = NumTri();
triProperties.resize(numTri);
int idx = 0;
for (int i = 0; i < numTri; ++i) {
for (const int j : {0, 1, 2}) {
triProperties[i][j] = idx++;
}
}
pImpl->meshRelation_.properties = VecDH<float>(numProp * idx, 0);
} else {
pImpl->meshRelation_.properties =
VecDH<float>(numProp * NumPropVert(), 0);
}
thrust::for_each_n(
thrust::host, countAt(0), NumTri(),
UpdateProperties({pImpl->meshRelation_.properties.ptrH(), numProp,
oldProperties.ptrH(), oldNumProp,
pImpl->vertPos_.ptrH(), triProperties.ptrH(),
pImpl->halfedge_.ptrH(), propFunc}));
}
pImpl->meshRelation_.numProp = numProp;
pImpl->CreateFaces();
pImpl->SimplifyTopology();
pImpl->Finish();
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
}
/**
* 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.
*/
Manifold Manifold::CalculateCurvature(int gaussianIdx, int meanIdx) const {
auto pImpl = std::make_shared<Impl>(*GetCsgLeafNode().GetImpl());
pImpl->CalculateCurvature(gaussianIdx, meanIdx);
return Manifold(std::make_shared<CsgLeafNode>(pImpl));
}
@@ -698,4 +776,50 @@ Manifold Manifold::TrimByPlane(glm::vec3 normal, float originOffset) const {
}
ExecutionParams& ManifoldParams() { return params; }
/**
* Compute the convex hull of a set of points. If the given points are fewer
* than 4, or they are all coplanar, an empty Manifold will be returned.
*
* @param pts A vector of 3-dimensional points over which to compute a convex
* hull.
*/
Manifold Manifold::Hull(const std::vector<glm::vec3>& pts) {
const int numVert = pts.size();
if (numVert < 4) return Manifold();
std::vector<quickhull::Vector3<double>> vertices(numVert);
for (int i = 0; i < numVert; i++) {
vertices[i] = {pts[i].x, pts[i].y, pts[i].z};
}
quickhull::QuickHull<double> qh;
// bools: correct triangle winding, and use original indices
auto hull = qh.getConvexHull(vertices, false, true);
const auto& triangles = hull.getIndexBuffer();
const int numTris = triangles.size() / 3;
Mesh mesh;
mesh.vertPos = pts;
mesh.triVerts.reserve(numTris);
for (int i = 0; i < numTris; i++) {
const int j = i * 3;
mesh.triVerts.push_back({triangles[j], triangles[j + 1], triangles[j + 2]});
}
return Manifold(mesh);
}
/**
* Compute the convex hull of this manifold.
*/
Manifold Manifold::Hull() const { return Hull(GetMesh().vertPos); }
/**
* Compute the convex hull enveloping a set of manifolds.
*
* @param manifolds A vector of manifolds over which to compute a convex hull.
*/
Manifold Manifold::Hull(const std::vector<Manifold>& manifolds) {
return Compose(manifolds).Hull();
}
} // namespace manifold
+57
View File
@@ -0,0 +1,57 @@
#include "impl.h"
namespace {
using namespace manifold;
__host__ __device__ inline int FlipHalfedge(int halfedge) {
const int tri = halfedge / 3;
const int vert = 2 - (halfedge - 3 * tri);
return 3 * tri + vert;
}
struct TransformNormals {
const glm::mat3 transform;
__host__ __device__ glm::vec3 operator()(glm::vec3 normal) {
normal = glm::normalize(transform * normal);
if (isnan(normal.x)) normal = glm::vec3(0.0f);
return normal;
}
};
struct TransformTangents {
const glm::mat3 transform;
const bool invert;
const glm::vec4* oldTangents;
const Halfedge* halfedge;
__host__ __device__ void operator()(thrust::tuple<glm::vec4&, int> inOut) {
glm::vec4& tangent = thrust::get<0>(inOut);
int edge = thrust::get<1>(inOut);
if (invert) {
edge = halfedge[FlipHalfedge(edge)].pairedHalfedge;
}
tangent = glm::vec4(transform * glm::vec3(oldTangents[edge]),
oldTangents[edge].w);
}
};
struct FlipTris {
Halfedge* halfedge;
__host__ __device__ void operator()(thrust::tuple<TriRef&, int> inOut) {
TriRef& bary = thrust::get<0>(inOut);
const int tri = thrust::get<1>(inOut);
thrust::swap(halfedge[3 * tri], halfedge[3 * tri + 2]);
for (const int i : {0, 1, 2}) {
thrust::swap(halfedge[3 * tri + i].startVert,
halfedge[3 * tri + i].endVert);
halfedge[3 * tri + i].pairedHalfedge =
FlipHalfedge(halfedge[3 * tri + i].pairedHalfedge);
}
}
};
} // namespace
+66 -30
View File
@@ -43,9 +43,7 @@ struct FaceAreaVolume {
float area = glm::length(crossP);
float volume = glm::dot(crossP, vertPos[halfedges[3 * face].startVert]);
return area > perimeter * precision
? thrust::make_pair(area / 2.0f, volume / 6.0f)
: thrust::make_pair(0.0f, 0.0f);
return thrust::make_pair(area / 2.0f, volume / 6.0f);
}
};
@@ -156,14 +154,51 @@ struct NormalizeCurvature {
}
};
struct CheckManifold {
struct UpdateProperties {
float* properties;
const float* oldProperties;
const Halfedge* halfedge;
const float* meanCurvature;
const float* gaussianCurvature;
const int oldNumProp;
const int numProp;
const int gaussianIdx;
const int meanIdx;
__host__ __device__ void operator()(thrust::tuple<glm::ivec3&, int> inOut) {
glm::ivec3& triProp = thrust::get<0>(inOut);
const int tri = thrust::get<1>(inOut);
for (const int i : {0, 1, 2}) {
const int vert = halfedge[3 * tri + i].startVert;
if (oldNumProp == 0) {
triProp[i] = vert;
}
const int propVert = triProp[i];
for (int p = 0; p < oldNumProp; ++p) {
properties[numProp * propVert + p] =
oldProperties[oldNumProp * propVert + p];
}
if (gaussianIdx >= 0) {
properties[numProp * propVert + gaussianIdx] = gaussianCurvature[vert];
}
if (meanIdx >= 0) {
properties[numProp * propVert + meanIdx] = meanCurvature[vert];
}
}
}
};
struct CheckHalfedges {
const Halfedge* halfedges;
__host__ __device__ bool operator()(int edge) {
const Halfedge halfedge = halfedges[edge];
if (halfedge.startVert == -1 && halfedge.endVert == -1 &&
halfedge.pairedHalfedge == -1)
return true;
if (halfedge.startVert == -1 && halfedge.endVert == -1) return true;
if (halfedge.pairedHalfedge == -1) return false;
const Halfedge paired = halfedges[halfedge.pairedHalfedge];
bool good = true;
@@ -243,7 +278,7 @@ bool Manifold::Impl::IsManifold() const {
auto policy = autoPolicy(halfedge_.size());
return all_of(policy, countAt(0), countAt(halfedge_.size()),
CheckManifold({halfedge_.cptrD()}));
CheckHalfedges({halfedge_.cptrD()}));
}
/**
@@ -292,9 +327,9 @@ Properties Manifold::Impl::GetProperties() const {
return {areaVolume.first, areaVolume.second};
}
Curvature Manifold::Impl::GetCurvature() const {
Curvature result;
if (IsEmpty()) return result;
void Manifold::Impl::CalculateCurvature(int gaussianIdx, int meanIdx) {
if (IsEmpty()) return;
if (gaussianIdx < 0 && meanIdx < 0) return;
VecDH<float> vertMeanCurvature(NumVert(), 0);
VecDH<float> vertGaussianCurvature(NumVert(), glm::two_pi<float>());
VecDH<float> vertArea(NumVert(), 0);
@@ -309,25 +344,26 @@ Curvature Manifold::Impl::GetCurvature() const {
zip(vertMeanCurvature.begin(), vertGaussianCurvature.begin(),
vertArea.begin(), degree.begin()),
NumVert(), NormalizeCurvature());
result.minMeanCurvature = reduce<float>(
policy, vertMeanCurvature.begin(), vertMeanCurvature.end(),
std::numeric_limits<float>::infinity(), thrust::minimum<float>());
result.maxMeanCurvature = reduce<float>(
policy, vertMeanCurvature.begin(), vertMeanCurvature.end(),
-std::numeric_limits<float>::infinity(), thrust::maximum<float>());
result.minGaussianCurvature = reduce<float>(
policy, vertGaussianCurvature.begin(), vertGaussianCurvature.end(),
std::numeric_limits<float>::infinity(), thrust::minimum<float>());
result.maxGaussianCurvature = reduce<float>(
policy, vertGaussianCurvature.begin(), vertGaussianCurvature.end(),
-std::numeric_limits<float>::infinity(), thrust::maximum<float>());
result.vertMeanCurvature.insert(result.vertMeanCurvature.end(),
vertMeanCurvature.begin(),
vertMeanCurvature.end());
result.vertGaussianCurvature.insert(result.vertGaussianCurvature.end(),
vertGaussianCurvature.begin(),
vertGaussianCurvature.end());
return result;
const int oldNumProp = NumProp();
const int numProp = glm::max(oldNumProp, glm::max(gaussianIdx, meanIdx) + 1);
const VecDH<float> oldProperties = meshRelation_.properties;
meshRelation_.properties = VecDH<float>(numProp * NumPropVert(), 0);
meshRelation_.numProp = numProp;
if (meshRelation_.triProperties.size() == 0) {
meshRelation_.triProperties.resize(NumTri());
}
for_each_n(
policy, zip(meshRelation_.triProperties.begin(), countAt(0)), NumTri(),
UpdateProperties({meshRelation_.properties.ptrD(), oldProperties.cptrD(),
halfedge_.cptrD(), vertMeanCurvature.cptrD(),
vertGaussianCurvature.cptrD(), oldNumProp, numProp,
gaussianIdx, meanIdx}));
CreateFaces();
SimplifyTopology();
Finish();
}
/**
+6
View File
@@ -28,6 +28,12 @@ __host__ __device__ inline glm::vec3 SafeNormalize(glm::vec3 v) {
return glm::isfinite(v.x) ? v : glm::vec3(0);
}
__host__ __device__ inline float MaxPrecision(float minPrecision,
const Box& bBox) {
float precision = glm::max(minPrecision, kTolerance * bBox.Scale());
return glm::isfinite(precision) ? precision : -1;
}
__host__ __device__ inline int NextHalfedge(int current) {
++current;
if (current % 3 == 0) current -= 3;
+185 -5
View File
@@ -14,6 +14,10 @@
#include <thrust/sequence.h>
#include <numeric>
#include <set>
#include "graph.h"
#include "impl.h"
#include "par.h"
@@ -192,6 +196,44 @@ struct ReindexFace {
}
};
struct VertMortonBox {
const float* vertProperties;
const uint32_t numProp;
const float tol;
const Box bBox;
__host__ __device__ void operator()(
thrust::tuple<uint32_t&, Box&, int> inout) {
uint32_t& mortonCode = thrust::get<0>(inout);
Box& vertBox = thrust::get<1>(inout);
int vert = thrust::get<2>(inout);
const glm::vec3 center(vertProperties[numProp * vert],
vertProperties[numProp * vert + 1],
vertProperties[numProp * vert + 2]);
vertBox.min = center - tol / 2;
vertBox.max = center + tol / 2;
mortonCode = MortonCode(center, bBox);
}
};
struct Duplicate {
__host__ __device__ thrust::pair<float, float> operator()(float x) {
return thrust::make_pair(x, x);
}
};
struct MinMax : public thrust::binary_function<thrust::pair<float, float>,
thrust::pair<float, float>,
thrust::pair<float, float>> {
__host__ __device__ thrust::pair<float, float> operator()(
thrust::pair<float, float> a, thrust::pair<float, float> b) {
return thrust::make_pair(glm::min(a.first, b.first),
glm::max(a.second, b.second));
}
};
} // namespace
namespace manifold {
@@ -254,6 +296,8 @@ void Manifold::Impl::Finish() {
CalculateNormals();
collider_ = Collider(faceBox, faceMorton);
ASSERT(Is2Manifold(), logicErr, "mesh is not 2-manifold!");
}
/**
@@ -268,8 +312,9 @@ void Manifold::Impl::SortVerts() {
VecDH<int> vertNew2Old(numVert);
sequence(policy, vertNew2Old.begin(), vertNew2Old.end());
sort_by_key(policy, vertMorton.begin(), vertMorton.end(),
zip(vertPos_.begin(), vertNew2Old.begin()));
vertNew2Old.begin());
ReindexVerts(vertNew2Old, numVert);
@@ -279,10 +324,12 @@ void Manifold::Impl::SortVerts() {
lower_bound<decltype(vertMorton.begin())>(policy, vertMorton.begin(),
vertMorton.end(), kNoCode) -
vertMorton.begin();
vertPos_.resize(newNumVert);
vertNew2Old.resize(newNumVert);
Permute(vertPos_, vertNew2Old);
if (vertNormal_.size() == numVert) {
Permute(vertNormal_, vertNew2Old);
vertNormal_.resize(newNumVert);
}
}
@@ -351,7 +398,7 @@ void Manifold::Impl::SortFaces(VecDH<Box>& faceBox,
sequence(policy, faceNew2Old.begin(), faceNew2Old.end());
sort_by_key(policy, faceMorton.begin(), faceMorton.end(),
zip(faceBox.begin(), faceNew2Old.begin()));
faceNew2Old.begin());
// Tris were flagged for removal with pairedHalfedge = -1 and assigned kNoCode
// to sort them to the end, which allows them to be removed.
@@ -359,10 +406,10 @@ void Manifold::Impl::SortFaces(VecDH<Box>& faceBox,
find<decltype(faceMorton.begin())>(policy, faceMorton.begin(),
faceMorton.end(), kNoCode) -
faceMorton.begin();
faceBox.resize(newNumTri);
faceMorton.resize(newNumTri);
faceNew2Old.resize(newNumTri);
Permute(faceBox, faceNew2Old);
GatherFaces(faceNew2Old);
}
@@ -432,4 +479,137 @@ void Manifold::Impl::GatherFaces(const Impl& old,
old.halfedge_.cptrD(), old.halfedgeTangent_.cptrD(),
faceNew2Old.cptrD(), faceOld2New.cptrD()}));
}
/// Constructs a position-only MeshGL from the input Mesh.
MeshGL::MeshGL(const Mesh& mesh) {
numProp = 3;
precision = mesh.precision;
vertProperties.resize(numProp * mesh.vertPos.size());
for (int i = 0; i < mesh.vertPos.size(); ++i) {
for (int j : {0, 1, 2}) vertProperties[3 * i + j] = mesh.vertPos[i][j];
}
triVerts.resize(3 * mesh.triVerts.size());
for (int i = 0; i < mesh.triVerts.size(); ++i) {
for (int j : {0, 1, 2}) triVerts[3 * i + j] = mesh.triVerts[i][j];
}
halfedgeTangent.resize(4 * mesh.halfedgeTangent.size());
for (int i = 0; i < mesh.halfedgeTangent.size(); ++i) {
for (int j : {0, 1, 2, 3})
halfedgeTangent[4 * i + j] = mesh.halfedgeTangent[i][j];
}
}
/**
* Updates the mergeFromVert and mergeToVert vectors in order to create a
* manifold solid. If the MeshGL is already manifold, no change will occur and
* the function will return false. Otherwise, this will merge verts along open
* edges within precision (the maximum of the MeshGL precision and the baseline
* bounding-box precision), keeping any from the existing merge vectors.
*
* There is no guarantee the result will be manifold - this is a best-effort
* helper function designed primarily to aid in the case where a manifold
* multi-material MeshGL was produced, but its merge vectors were lost due to a
* round-trip through a file format. Constructing a Manifold from the result
* will report a Status if it is not manifold.
*/
bool MeshGL::Merge() {
std::multiset<std::pair<int, int>> openEdges;
std::vector<int> merge(NumVert());
std::iota(merge.begin(), merge.end(), 0);
for (int i = 0; i < mergeFromVert.size(); ++i) {
merge[mergeFromVert[i]] = mergeToVert[i];
}
const int numVert = NumVert();
const int numTri = NumTri();
const int next[3] = {1, 2, 0};
for (int tri = 0; tri < numTri; ++tri) {
for (int i : {0, 1, 2}) {
auto edge = std::make_pair(merge[triVerts[3 * tri + next[i]]],
merge[triVerts[3 * tri + i]]);
auto it = openEdges.find(edge);
if (it == openEdges.end()) {
std::swap(edge.first, edge.second);
openEdges.insert(edge);
} else {
openEdges.erase(it);
}
}
}
if (openEdges.empty()) {
return false;
}
const int numOpenVert = openEdges.size();
VecDH<int> openVerts(numOpenVert);
int i = 0;
for (const auto& edge : openEdges) {
const int vert = edge.first;
openVerts[i++] = vert;
}
VecDH<float> vertPropD(vertProperties);
Box bBox;
for (const int i : {0, 1, 2}) {
strided_range<VecDH<float>::Iter> iPos(vertPropD.begin() + i,
vertPropD.end(), numProp);
auto minMax = transform_reduce<thrust::pair<float, float>>(
autoPolicy(numVert), iPos.begin(), iPos.end(), Duplicate(),
thrust::make_pair(std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity()),
MinMax());
bBox.min[i] = minMax.first;
bBox.max[i] = minMax.second;
}
precision = MaxPrecision(precision, bBox);
if (precision < 0) return false;
auto policy = autoPolicy(numOpenVert);
VecDH<Box> vertBox(numOpenVert);
VecDH<uint32_t> vertMorton(numOpenVert);
for_each_n(policy,
zip(vertMorton.begin(), vertBox.begin(), openVerts.cbegin()),
numOpenVert,
VertMortonBox({vertPropD.cptrD(), numProp, precision, bBox}));
sort_by_key(policy, vertMorton.begin(), vertMorton.end(),
zip(vertBox.begin(), openVerts.begin()));
Collider collider(vertBox, vertMorton);
SparseIndices toMerge = collider.Collisions<true>(vertBox);
Graph graph;
for (int i = 0; i < numVert; ++i) {
graph.add_nodes(i);
}
for (int i = 0; i < mergeFromVert.size(); ++i) {
graph.add_edge(static_cast<int>(mergeFromVert[i]),
static_cast<int>(mergeToVert[i]));
}
for (int i = 0; i < toMerge.size(); ++i) {
graph.add_edge(openVerts[toMerge.Get(0)[i]], openVerts[toMerge.Get(1)[i]]);
}
std::vector<int> vertLabels;
const int numLabels = ConnectedComponents(vertLabels, graph);
std::vector<int> label2vert(numLabels);
for (int v = 0; v < numVert; ++v) {
label2vert[vertLabels[v]] = v;
}
mergeToVert.clear();
mergeFromVert.clear();
for (int v = 0; v < numVert; ++v) {
const int mergeTo = label2vert[vertLabels[v]];
if (mergeTo != v) {
mergeFromVert.push_back(v);
mergeToVert.push_back(mergeTo);
}
}
return true;
}
} // namespace manifold
+6 -2
View File
@@ -38,9 +38,13 @@ using PolygonsIdx = std::vector<SimplePolygonIdx>;
std::vector<glm::ivec3> TriangulateIdx(const PolygonsIdx &polys,
float precision = -1);
/** @} */
std::vector<glm::ivec3> Triangulate(
std::vector<std::vector<glm::vec2>> &polygons, float precision = -1);
/** @ingroup Connections
* @{
*/
std::vector<glm::ivec3> Triangulate(const Polygons &polygons,
float precision = -1);
ExecutionParams &PolygonParams();
/** @} */
+17 -11
View File
@@ -155,8 +155,8 @@ void Dump(const PolygonsIdx &polys) {
for (auto poly : polys) {
std::cout << "polys.push_back({" << std::setprecision(9) << std::endl;
for (auto v : poly) {
std::cout << " {glm::vec2(" << v.pos.x << ", " << v.pos.y << "), "
<< v.idx << "}, //" << std::endl;
std::cout << " {" << v.pos.x << ", " << v.pos.y << "}, //"
<< std::endl;
}
std::cout << "});" << std::endl;
}
@@ -947,7 +947,8 @@ class Monotones {
OVERLAP_ASSERT(type != Skip, "Skip should not happen on reverse sweep!");
PairItr westPair = GetPair(vert, type);
OVERLAP_ASSERT(westPair != activePairs_.end(), "No active pair!");
OVERLAP_ASSERT(type == Start || westPair != activePairs_.end(),
"No active pair!");
switch (type) {
case Merge: {
@@ -1033,12 +1034,15 @@ class Monotones {
namespace manifold {
/**
* @brief Triangulates a set of /epsilon-valid polygons.
* @brief Triangulates a set of &epsilon;-valid polygons. If the input is not
* &epsilon;-valid, the triangulation may overlap, but will always return a
* manifold result that matches the input edge directions.
*
* @param polys The set of polygons, wound CCW and representing multiple
* polygons and/or holes. These have 2D-projected positions as well as
* references back to the original vertices
* @param precision The value of epsilon, bounding the uncertainty of the input
* references back to the original vertices.
* @param precision The value of &epsilon;, bounding the uncertainty of the
* input.
* @return std::vector<glm::ivec3> The triangles, referencing the original
* vertex indicies.
*/
@@ -1071,19 +1075,21 @@ std::vector<glm::ivec3> TriangulateIdx(const PolygonsIdx &polys,
}
/**
* @brief Triangulates a set of /epsilon-valid polygons.
* @brief Triangulates a set of &epsilon;-valid polygons. If the input is not
* &epsilon;-valid, the triangulation may overlap, but will always return a
* manifold result that matches the input edge directions.
*
* @param polygons The set of polygons, wound CCW and representing multiple
* polygons and/or holes.
* @param precision The value of epsilon, bounding the uncertainty of the input
* @param precision The value of &epsilon;, bounding the uncertainty of the
* input.
* @return std::vector<glm::ivec3> The triangles, referencing the original
* polygon points in order.
*/
std::vector<glm::ivec3> Triangulate(
std::vector<std::vector<glm::vec2>> &polygons, float precision) {
std::vector<glm::ivec3> Triangulate(const Polygons &polygons, float precision) {
int idx = 0;
PolygonsIdx polygonsIndexed;
for (auto &poly : polygons) {
for (const auto &poly : polygons) {
SimplePolygonIdx simpleIndexed;
for (const glm::vec2 &polyVert : poly) {
simpleIndexed.push_back({polyVert, idx++});
+4 -15
View File
@@ -1,17 +1,3 @@
# 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.
add_subdirectory(glm)
add_subdirectory(graphlite)
@@ -20,5 +6,8 @@ set(CLIPPER2_EXAMPLES OFF)
set(CLIPPER2_TESTS OFF)
set(CLIPPER2_USINGZ "OFF" CACHE STRING "Preempt cache default of USINGZ (we only use 2d)")
add_definitions(-D_HAS_EXCEPTIONS=0) # disable exceptions for STL
add_subdirectory(clipper2/CPP)
add_library(quickhull quickhull/QuickHull.cpp)
target_include_directories(quickhull PUBLIC quickhull)
target_compile_features(quickhull PUBLIC cxx_std_17)
File diff suppressed because it is too large Load Diff
Vendored Submodule
+1
+2
View File
@@ -62,3 +62,5 @@ if(MANIFOLD_DEBUG)
target_compile_options(${PROJECT_NAME}
PUBLIC -DMANIFOLD_DEBUG)
endif()
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
+20 -87
View File
@@ -145,83 +145,11 @@ struct Mesh {
/// as 3 * tri + i, representing the tangent from Mesh.triVerts[tri][i] along
/// the CCW edge. If empty, mesh is faceted.
std::vector<glm::vec4> halfedgeTangent;
};
/**
* An alternative to Mesh for output suitable for pushing into graphics
* libraries directly. This may not be manifold since the verts are duplicated
* along property boundaries that do not match. The additional merge vectors
* store this missing information, allowing the manifold to be reconstructed.
*/
struct MeshGL {
/// Number of property vertices
uint32_t NumVert() const { return vertProperties.size() / numProp; };
/// Number of triangles
uint32_t NumTri() const { return triVerts.size() / 3; };
/// Number of properties per vertex, always >= 3.
uint32_t numProp = 3;
/// Flat, GL-style interleaved list of all vertex properties: propVal =
/// vertProperties[vert * numProp + propIdx]. The first three properties are
/// always the position x, y, z.
std::vector<float> vertProperties;
/// The vertex indices of the three triangle corners in CCW (from the outside)
/// order, for each triangle.
std::vector<uint32_t> triVerts;
/// Optional: A list of only the vertex indicies that need to be merged to
/// reconstruct the manifold.
std::vector<uint32_t> mergeFromVert;
/// Optional: The same length as mergeFromVert, and the corresponding value
/// contains the vertex to merge with. It will have an identical position, but
/// the other properties may differ.
std::vector<uint32_t> mergeToVert;
/// Optional: Indicates runs of triangles that correspond to a particular
/// input mesh instance. The runs encompass all of triVerts and are sorted
/// by runOriginalID. Run i begins at triVerts[runIndex[i]] and ends at
/// triVerts[runIndex[i+1]]. All runIndex values are divisible by 3.
std::vector<uint32_t> runIndex;
/// Optional: The OriginalID of the mesh this triangle run came from. This ID
/// is ideal for reapplying materials to the output mesh. Multiple runs may
/// have the same ID, e.g. representing different copies of the same input
/// mesh. If you create an input MeshGL that you want to be able to reference
/// as one or more originals, be sure to set unique values from ReserveIDs().
std::vector<uint32_t> runOriginalID;
/// Optional: For each run, a 3x4 transform is stored representing how the
/// corresponding original mesh was transformed to create this triangle run.
/// This matrix is stored in column-major order and the length of the overall
/// vector is 12 * runOriginalID.size().
std::vector<float> runTransform;
/// Optional: Length NumTri, contains an ID of the source face this triangle
/// comes from. When auto-generated, this ID will be a triangle index into the
/// original mesh. All neighboring coplanar triangles from that input mesh
/// will refer to a single triangle of that group as the faceID. When
/// supplying faceIDs, ensure that triangles with the same ID are in fact
/// coplanar and have consistent properties (within some tolerance) or the
/// output will be surprising.
std::vector<uint32_t> faceID;
/// Optional: The X-Y-Z-W weighted tangent vectors for smooth Refine(). If
/// non-empty, must be exactly four times as long as Mesh.triVerts. Indexed
/// as 4 * (3 * tri + i) + j, i < 3, j < 4, representing the tangent value
/// Mesh.triVerts[tri][i] along the CCW edge. If empty, mesh is faceted.
std::vector<float> halfedgeTangent;
MeshGL() = default;
MeshGL(const Mesh& mesh) {
numProp = 3;
vertProperties.resize(numProp * mesh.vertPos.size());
for (int i = 0; i < mesh.vertPos.size(); ++i) {
for (int j : {0, 1, 2}) vertProperties[3 * i + j] = mesh.vertPos[i][j];
}
triVerts.resize(3 * mesh.triVerts.size());
for (int i = 0; i < mesh.triVerts.size(); ++i) {
for (int j : {0, 1, 2}) triVerts[3 * i + j] = mesh.triVerts[i][j];
}
halfedgeTangent.resize(4 * mesh.halfedgeTangent.size());
for (int i = 0; i < mesh.halfedgeTangent.size(); ++i) {
for (int j : {0, 1, 2, 3})
halfedgeTangent[4 * i + j] = mesh.halfedgeTangent[i][j];
}
}
/// The absolute precision of the vertex positions, based on accrued rounding
/// errors. When creating a Manifold, the precision used will be the maximum
/// of this and a baseline precision from the size of the bounding box. Any
/// edge shorter than precision may be collapsed.
float precision = 0;
};
/**
@@ -244,16 +172,6 @@ struct Properties {
float surfaceArea, volume;
};
/**
* Discrete curvature of a manifold calculated at every vertex. See
* Manifold.GetCurvature() for details.
*/
struct Curvature {
float maxMeanCurvature, minMeanCurvature;
float maxGaussianCurvature, minGaussianCurvature;
std::vector<float> vertMeanCurvature, vertGaussianCurvature;
};
struct Box {
glm::vec3 min = glm::vec3(std::numeric_limits<float>::infinity());
glm::vec3 max = glm::vec3(-std::numeric_limits<float>::infinity());
@@ -580,6 +498,21 @@ void Dump(const std::vector<T>& vec) {
}
std::cout << std::endl;
}
template <typename T>
void Diff(const std::vector<T>& a, const std::vector<T>& b) {
std::cout << "Diff = " << std::endl;
if (a.size() != b.size()) {
std::cout << "a and b must have the same length, aborting Diff"
<< std::endl;
return;
}
for (int i = 0; i < a.size(); ++i) {
if (a[i] != b[i])
std::cout << i << ": " << a[i] << ", " << b[i] << std::endl;
}
std::cout << std::endl;
}
/** @} */
#endif
} // namespace manifold

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