Compare commits

...
164 Commits
Author SHA1 Message Date
Emmett Lalish 5e7a576cea v2.5.1 (#847) 2024-06-14 13:48:05 -07:00
Emmett Lalish 80a5f2e0c5 fixed property smoothing (#845) 2024-06-14 11:41:26 -07:00
Emmett Lalish 67c4647464 Several smoothing fixes (#844)
* fixed several bugs

* fixed average rotation
2024-06-13 15:26:57 -07:00
Emmett Lalish edd149e753 fixed missing default params in JS bindings (#843) 2024-06-13 09:48:06 -07:00
Emmett Lalish b5c175162a fixed folded normals smoothing (#842) 2024-06-13 09:32:11 -07:00
Clifford Yapp 1df22bee48 Clear compiler warning from BSD. (#841)
* Clear compiler warning from BSD.

* Fix formatting per Github CI
2024-06-13 07:53:23 -07:00
Emmett Lalish d5c0418309 removed animation from roundedFrame (#839) 2024-06-12 14:28:35 -07:00
Emmett Lalish 7a5d5421d4 Fix PyPI again (#838)
* test all OSes

* try older windows

* updated release docs
2024-06-12 13:44:16 -07:00
Emmett Lalish 69437096d0 update cibuildwheels (#837) 2024-06-12 10:33:29 -07:00
Emmett Lalish 3931b3296d v2.5 (#836)
update to v2.5.0
2024-06-12 09:22:41 -07:00
Emmett Lalish 62d0f25ce0 Flair normals (#835)
* added comments

* flaired normals working

* updated tests
2024-06-11 11:38:49 -07:00
Emmett Lalish 1bd16f4c02 added convex polygon triangulation fast path (#829)
* added convex polygon triangulation fast path

* fixed transform_reduce

* fix sliver convexity test
2024-06-10 14:01:35 -07:00
Emmett Lalish 3afe846570 Fixed backward zebra polygon (#834)
fixed two more bugs
2024-06-08 17:31:33 -07:00
Emmett Lalish 12b9494fdd Fix the zebra polygon (#832)
* add Zebra polygon stress test

* fixed Zebra polygon - 4 bugs

* fix MSVC build

* try MSVC fix
2024-06-08 13:00:26 -07:00
Emmett Lalish 0577aabe7f Optimize triangulator with collider (#830)
* implemented - weird linker error

* fix linker

* fixed empty collider

* improved perf and cleanup

* add comments

* more docs
2024-06-05 10:28:59 -07:00
Emmett Lalish bdae9dd11a Improve smooth (#828)
* add max curvature tests

* back to radial basis functions

* fix longWay slerp

* cusps eliminated

* cleanup
2024-06-04 09:04:47 -07:00
Chris Mayo 5f4059ab1f Create a release file containing submodules (#825) 2024-05-31 13:30:51 -07:00
Emmett Lalish 9cb3969f70 Clean up test files (#826)
* match names to files

* GLB naming

* tighten matter tests

* partial revert

* stop skipping my job

* stop cancelling my jobs

* revert CI changes
2024-05-30 16:18:00 -07:00
Hiroshi OgawaandEmmett Lalish 4fb3520287 refactor(wasm/examples): support export models without web worker (#822)
* refactor(wasm/examples): support `evaluateCADToModel` without web worker

* test: copy test

* chore: update vitest

* chore: revert some

* chore: revert more

* refactor: simplify worker-wrapper.ts

* chore: revert format

* test: fix cleanup

* refactor: single module

* chore: format

* chore: revert .vscode/settings.json

* chore: format

* test: tweak

* chore: format

* chore: rename

* refactor: minor

* chore: remove wrong lock file

* added resetToCircularDefaults

* cleanup

* added headers

* removed vitest/web-worker

---------

Co-authored-by: Emmett Lalish <elalish@google.com>
2024-05-28 13:50:25 -07:00
Marius KintelandEmmett Lalish 376d317f14 Add polygon test showing bad tessellation result + fix (#824)
* Add polygon test showing bad tessellation result

* fixed ear cost

---------

Co-authored-by: Emmett Lalish <elalish@google.com>
2024-05-28 11:21:14 -07:00
Emmett Lalish bf18c9f759 Improve smoothing of flat faces (#821)
* Improve SmoothByNormals

* improving SmoothByNormals

* parallelize

* simplify SmoothByNormals

* added DistributeTangents

* fixed NaN bug

* use normals for sharp smoothing

* remove LinearizeTangents

* distrubute tangents for SmoothOut

* Revert "remove LinearizeTangents"

This reverts commit 7276609dc245c26b9a720b5bda1f018d64639954.

* fixed SmoothOut and added tests

* expanded test

* update wasm test
2024-05-28 11:06:27 -07:00
Emmett Lalish cdd1475fe1 added project test (#818)
* added failing test

* update test
2024-05-28 08:46:58 -07:00
Chris Mayo 55a0221f9b CI: use GCC 14 in Linux build (#816)
* CI: use GCC 14 in Linux build

Ubuntu 24.04 packages GCC 13. GitHub add GCC 14 to their runner.

* Ignore lcov mismatch errors

Processing ./test/CMakeFiles/manifold_test.dir/smooth_test.cpp.gcno
geninfo: ERROR: mismatched end line for _ZN23Smooth_Tetrahedron_Test8TestBodyEv at /home/runner/work/manifold/manifold/test/smooth_test.cpp:26: 26 -> 39
	(use "geninfo --ignore-errors mismatch ..." to bypass this error)
2024-05-27 19:02:47 -07:00
pca006132 d78b1e2042 update clipper2 (#817)
* update clipper2

* test

* test2

* test3
2024-05-25 00:41:31 +08:00
Chris Mayo 562ff02232 Update glm to 1.0.1 (#819)
* Update glm to 1.0.1

* Define GLM_ENABLE_EXPERIMENTAL for glm/gtx/quaternion.hpp

With GLM 1:

In file included from manifold/manifold/src/manifold/src/smoothing.cpp:15:
manifold/manifold/build/_deps/glm-src/glm/gtx/quaternion.hpp:24:3: error: "GLM: GLM_GTX_quaternion is an experimental extension
  and may change in the future. Use #define GLM_ENABLE_EXPERIMENTAL before including it, if you really want to use it."
2024-05-23 11:56:14 -07:00
Peter Harman 8e2b1dd485 Add manifold_meshgl_merge to c bindings (#820)
Add manifold_meshgl_merge to call MeshGL::Merge() on a duplicate mesh via the c bindings
2024-05-24 00:18:47 +08:00
Ian McInerney 5ef09daf45 Set import library for Clipper2 target when using system version (#815)
On Windows, the import library is also needed to link against the
Clipper2 DLL, and that must be specified using a different target
property.
2024-05-21 09:42:28 -07:00
Albert Siddhartha Slawinski 6d932a2c7f Fix Manifold.prototype.{split,splitByPlane,rotate} JavaScript bindings functions (#811)
* Fix the JS function to use the correct WASM function names

* Add a simple test

* Update tests and examples based on PR review
2024-05-15 10:14:43 -07:00
Daso Jung fd81cd6172 Split manifold tests (#800)
* Split Boolean test

* Fix name

* formatting

* move cubes test

* formatting

* format

* move tests

* update complex_boolean

* Split manifold test

* Update CMakeLists.txt

* fix typo

* move mingap to properties

* fix conflict

* Update CMakeLists.txt

* organize tests

* fix format

* Update properties.cpp
2024-05-13 09:37:50 -07:00
Emmett Lalish 6b4a4ed841 Update examples from JS -> TS (#807)
* put types back

* updated MV example to TS

* fixed TS errors

* added docs

* switch make-manifold to TS

* fix TS errors

* added docs

* update MV

* fix TS build

* forgot declaration
2024-05-10 16:43:57 -07:00
Emmett Lalish 887f631b8c Make runIndex easier to use (#806)
* handled unsorted groups

* forgot types
2024-05-10 12:52:19 -07:00
Emmett Lalish 443863d09e Runs example (#805)
* switched three example to TS

* use merge

* cleanup

* material groups

* added groups and ids

* working

* added docs
2024-05-09 22:46:16 -07:00
Peter Harman 5301542254 C bindings: pass context to set_properties and warp operation callbacks (#795)
* Fix for #786 by setting numProps in GatherFaces method. Added test for Decompose with properties, and ExpectProperties function in test_main in order to check properties in a mesh match those set by SetProperties.

* Correct formatting of ExpectProperties

* Correct more formatting

* Add x_context variants of set_properties and warp functions in cbindings, along with tests

* Revert "Correct more formatting"

This reverts commit 6e409cf0a9e2c000bddd70ba41667d3209410d4a.

* Revert "Correct formatting of ExpectProperties"

This reverts commit 4d5ee88d9c52e380c6722f347bcd032a8a0d2492.

* Revert "Fix for #786 by setting numProps in GatherFaces method. Added test for Decompose with properties, and ExpectProperties function in test_main in order to check properties in a mesh match those set by SetProperties."

This reverts commit 03848fac198851ad3617de35348513ebfdf00791.

* Remove _context variants of functions and make context argument the default for all callbacks. Add bounds checks to warp test and volume and surface area checks to level set test

* Correct formatting

* Use M_PI not M_PIf as Windows/Mac don't have it

* Use glm not C math

* Mac build sensitive to glm::pow<float,float> vs glm::pow<float>

* Use std::pow for pow
2024-05-03 09:26:56 -07:00
Emmett Lalish a122bde778 Quaternion smoothing (#801)
* revert smoothness change

* updated to quaternions - not right yet

* fixed triangles, mostly

* fixed numerical stability problem

* fixed more nans

* fixed several bugs

* refactor bezier2D

* improved circularity

* added test

* updated tests

* fixed mirror test

* updated manual smooth model

* updated JS test

* cleanup
2024-04-30 21:41:32 -07:00
Peter Harman c9810fd517 Ensure properties maintained through Decompose operation (#794)
* Fix for #786 by setting numProps in GatherFaces method. Added test for Decompose with properties, and ExpectProperties function in test_main in order to check properties in a mesh match those set by SetProperties.

* Correct formatting of ExpectProperties

* Correct more formatting

* Removed ExpectProperties and instead use existing test methods

* Call RelatedGL for each Decomposed Manifold
2024-04-26 08:09:02 -07:00
Emmett Lalish 4c57a56355 split subdivision out of smoothing (#797) 2024-04-25 22:25:08 -07:00
Emmett Lalish cdcbd329e6 Smoothing fixes (#796)
* fixed nan normals

* re-identify flat faces after refine

* fix color interpolation

* fix build
2024-04-25 16:35:35 -07:00
Emmett Lalish c738f1eea1 Smooth quads (#793)
* switched to ivec4, tests still pass

* added partition quad support

* added quad support for property interp

* forming quad tangents

* convex barycentric

* fix interpolation

* added linear quad interp

* added quad smoothing

* fixed vec out of range

* all tests pass

* fixed quad tangents

* tightened quad test

* cleanup

* avoid dome duplicate work

* added docs

* update test
2024-04-25 08:23:48 -07:00
Daso Jung a8b8bf71b1 Split boolean tests (#792)
* Split Boolean test

* Fix name

* formatting

* move cubes test

* formatting

* format

* move tests

* update complex_boolean
2024-04-25 08:10:12 -07:00
Emmett LalishandJohn Collins c60df1b704 Use forVert (#789)
* updated a bunch of do loops to ForVert

* more cleanup

* Hull failure case

* test cleanup

* test helper cleanup

* fixed edge_op

---------

Co-authored-by: John Collins <jmicahc@gmail.com>
2024-04-16 15:23:29 -07:00
Emmett Lalish 8d67ba3a3a Revert "Use ForVert" (#788)
Revert "Use ForVert (#778)"

This reverts commit 32eefe9157.
2024-04-12 21:31:08 -07:00
Marcell Leleszi 3258152e2c Add MinGap function (#765)
* Add tests for intersection

* Add POC MinGap implementation and tests

* Add tests where closest point falls on the triangle's face or on one of the edges

* Create TriangleDistance declaration

* Delete intersect tests

* Add stub MinGap definition

* Add triangle distance impl

* Add comments

* Fix tests

* Fix build

* Use collider

* Collider exploration

* Collider impl, cleanup

* Use par transform

* Revert settings.json, add comments

* Add comments

* Move tri_dist to utils

* Delete unnecessary include

* Create lambda for bounding box expansion

* Move duplicate logic to lambda

* Cleanup

* Use floats instead of doubles, use glm::clamp instead of own implementation

* User ternaries

* Remove DistanceTriangleTriangleSquared out params

* Move MinGap impl to Impl

* Use std::array instead of C-style

* Modify MinGap test cases for clarity, add overlapping tri_dist test

* Add c, wasm, python bindings

* Fix formatting

* Clean up comments

* Test whether faceboxes need to be sorted

* Add min_gap example to all_apis.py

* Fix formatting

* Clean up python binding

* Fix edge to edge test case

* Test BVH culling

* Remove sorting of other manifold's boxes

* Use collider_ member, remove sorting

* Move tests to manifold_test.cpp

* Remove whitespace

* Rename some tests for clarity

* Remove redundant test, modify test for edge to edge test for clarity

* Fix comments

* Parallelize narrow phase

* Reduce sphere res in test case to not cause out of memory error

* Add complex test case

* Remove unnecessary cast

* Refactor functor is lambda
2024-04-02 09:32:06 -07:00
pca006132 78729599a5 fix glm install (#782)
* fix glm install

* try again?

* proper C binding install

* fix glm install
2024-03-30 13:27:09 +08:00
Emmett Lalish 32eefe9157 Use ForVert (#778)
* updated a bunch of do loops to ForVert

* more cleanup
2024-03-26 13:15:26 -07:00
Emmett Lalish 3e05284250 Smooth By Normals (#776)
* SmoothByNormals compiles

* added ForVert

* tests pass

* added bindings

* stupid formatting...

* fix typo

* small fix

* aligned weights
2024-03-26 09:30:13 -07:00
Emmett Lalish c9ec16969d CalculateNormals and Smooth (#771)
* started implementation

* update to Vec

* refactor

* snap normals to faces

* added flat face sharpening

* added Smooth member

* Smooth working

* refactor SetNormals

* calculateNormals runs

* CalculateNormals works

* update test

* add bindings

* fix python bindings

* fix format

* Smooth -> SmoothOut

* a few fixes

* weird format bug

* addressing feedback

* update docs
2024-03-19 10:29:02 -07:00
John Collins 9b1d255931 Java bindings pointer in README (#769) 2024-03-11 12:48:19 -07:00
Emmett Lalish 22d94d5688 v2.4.5 (#762)
remove skip condition
2024-03-04 19:58:15 -08:00
Emmett Lalish e073141f14 v2.4.4 (#761) 2024-03-04 17:57:15 -08:00
Emmett Lalish f54e535c6a v2.4.3 (#760)
fix artifacts
2024-03-04 17:11:17 -08:00
Emmett Lalish c6801c82ea v2.4.2 (#759) 2024-03-04 15:04:57 -08:00
Emmett Lalish a67aa13133 v2.4.1 (#758)
update actions
2024-03-04 14:28:52 -08:00
Emmett Lalish d43709715d v2.4 (#757)
* v2.4.0

* fix cmake version
2024-03-04 13:24:41 -08:00
Emmett Lalish 2ddbd0a1a0 add docs about vertex properties, etc. (#755)
add docs
2024-03-02 17:32:07 -08:00
Emmett Lalish dc4afa950b Refine To Length (#741)
* sketched out implementation

* started PartitionQuad

* added edge directions

* compiles

* first test passing

* minor progress

* simplify

* refactor

* fixed interior verts

* getting manifold results

* improved triangulation

* finished algorithm

* fixed fans

* a few fixes

* fixed 3-2-2

* handle reversed triangles

* use Vec

* fixed another bug

* refine working

* replaced old subdivide

* fix allocation error

* fixed sphere

* fix cache

* fixed undefined behavior

* fix rounding error

* updated test

* cleanup

* add lock

* added properties support

* tightened test

* fixed edge colors

* fixed corners

* only duplicate props when necessary

* fix boolean props

* fixed manifoldness error

* fixed some narrow triangles

* fix compile

* added bindings

* fix formatting

* addressing feedback

* revert edge divisions change
2024-03-02 16:15:44 -08:00
pca006132 c03a5ac3ad prefer find_package for tbb (#747) 2024-02-19 13:03:37 +08:00
pca006132 cae5a01af1 README: change API documentation URL (#746) 2024-02-18 20:06:26 +08:00
pca006132 d59b9b93bd Fix sparseindices (#742)
* accept size_t for Vec

* Prepare a test case for "SparseIndices too large" core dump

* fix python docstring deps

* fix compilation errors

* limit sdf hashtable size

* another fix to hashtable

* log2(0) is undefined

* fix formatting

* Revert "fix compilation errors"

This reverts commit adfb86c18fb8a3f44f138f8b66d583b3c1c81193.

* fix size_t stuff

* remove processOverlap setting in test

* fix hashtable

* vertex number limit

* address comments
2024-02-16 08:45:43 -08:00
pca006132 f3c5ada9f0 test windows dll build (#743)
* test windows dll build

* export all symbols on windows

* skip manifoldc_test on emscripten

* try fix windows build

* fix

* fix2

* fix manifoldc on emscripten

* fix

* fix manifoldc

* fixes

* fix windows ci

* fix python flags

* document windows shenanigans

* I hate windows

* tell users we are downloading thrust from source
2024-02-15 15:28:41 -08:00
Kyle Finn 4395ab622c [python] add MeshGL::Merge() to python API (#739)
* [python] add MeshGL::Merge() to python API

* [python] mesh.merge in all_apis.py
2024-02-13 09:08:50 -08:00
Emmett Lalish 994956d7c3 switch share to hash (#736) 2024-02-08 17:19:47 -08:00
Emmett Lalish 79f735c59a update manifold, fix startup bug (#735) 2024-02-08 15:31:34 -08:00
Emmett Lalish dd67e992bd Editor buttons (#733)
* added undo, redo, format

* share button works

* cleanup

* reset scroll

* addressing feedback
2024-02-06 08:21:57 -08:00
briansturgill 5bdb8bc233 Added test for hull problem fixed in #730. (#732) 2024-02-05 10:47:22 -08:00
briansturgill c9ae400647 Fix to 2D HullImpl problem. (#730)
* Fixed 2D Hull problem

* Ran document formatting.

* Using public.h CCW, refactored as requested.

* Decided that precision value should 0.
2024-02-05 08:13:35 -08:00
Chris Mayo 9c1517e54e Install headers into manifold subdirectory (#729) 2024-02-05 08:12:57 -08:00
pca006132 3b8282e1d5 fix cube performance (#717)
* fix cube performance

* fix test
2024-01-29 13:46:55 +08:00
pca006132 1635864cbc optimize triangulator for msvc (#722)
optimize for msvc
2024-01-29 12:31:35 +08:00
Chris Mayo 65b2ef8198 Fix manifold3d module being built again in the install phase (#725)
With add_custom_target() "the target has no output file and is always
considered out of date".
2024-01-29 01:54:48 +08:00
Chris Mayo a9055a8abf python/gen_docs.py tidy-up (#724)
* python/gen_docs: Use raw strings in method_key()

Resolve Python 3.12 invalid escape sequence warnings.

* python/gen_docs: Remove unused f-string and md5 import
2024-01-29 01:53:38 +08:00
Chris Mayo 0b7ee87489 GLM 1.0 compatibility (#721)
Using glm/gtx/compatibility.hpp without GLM_ENABLE_EXPERIMENTAL has
become an error:

error: #error "GLM: GLM_GTX_compatibility is an experimental extension
               and may change in the future. Use #define GLM_ENABLE_EXPERIMENTAL
	       before including it, if you really want to use it."
2024-01-28 08:04:12 +08:00
pca006132 9690305b8d optimize triangulator performance (#718) 2024-01-27 18:30:33 +08:00
pca006132 aa88959899 Update manifoldDeps.cmake (#716) 2024-01-23 15:29:51 +13:00
Emmett LalishandClifford Yapp dbb0ca76b4 Fixed float overflow (#714)
* Set up test for subtraction of a very large cuboid

Test case for https://github.com/elalish/manifold/issues/712 - these glb
inputs were generated from manifolds that succeeded in producing a
Boolean output, but (on one platform but not another) the Status() call
on the output boolean manifold crashed.

(A test for calling Status() on the output captured on the crashing
platform (in case there was something specifically bad about that
particular output on that platform) was tried, but the result was simply
a successful Status() call even with that specific input - hence, that
additional test is not included in this commit.)

* fixed 0 * inf

* switch to ASSERT

* tighten test

---------

Co-authored-by: Clifford Yapp <238416+starseeker@users.noreply.github.com>
2024-01-20 15:16:50 +13:00
Chris Mayo 9018b2ac45 Install Python module into site-packages (#704)
Python_SITEARCH is not writeable by build_nix (tbb) job.
2024-01-17 09:22:27 +13:00
briansturgill f407e43e5b Added BatchBoolean to Python API, both CS & M. (#707) 2024-01-16 15:13:33 +08:00
pca006132 d7cbcb8c2e fix sdist submodule (#709) 2024-01-16 15:13:12 +08:00
pca006132 e27c9c100f cmake updates (#696)
* cmake packaging

* compatibility

* update

* update

* tbb

* fix rpath stuff

* update tbb

* fix nix

* fix bugs

* fix emscripten

* PIC

* CI stuff

* only publish if on master

* assimp dependency for meshIO

* add cmake consumer test

* remove THRUST_HOST_SYSTEM flag

* fix CI permission issue

* cmake minimum required

* install without admin privilege on mac

* fix install directory

* update RELEASE_CHECKLIST.md
2024-01-14 23:31:28 +13:00
briansturgill 8ad5f8e427 Extrude and Resolve should be static on Manifold. (#692)
* Extrude and Resolve should be static on Manifold.

* Fixed unit test for extrude/revolve issue.

* More test issues fixed.

* Clean up formatting.

* Placed extrude and revolve on both CrossSection and Manifold.

* Formatting.
2024-01-14 23:06:11 +13:00
briansturgill afc64f248a CrossSection::Compose was accidently left out of Python API. (#700) 2024-01-14 05:41:36 +08:00
pca006132 c0fb3766e4 update nanobind to v1.8.0 (#699) 2024-01-14 02:52:51 +08:00
Emmett Lalish 887a5021af bump package versions to 2.3.1 (#689)
bump version
2024-01-08 23:26:16 +13:00
Emmett Lalish e83b9bd158 v2.3.1 (#688)
* add missing pytest

* fix npm publish
2024-01-08 22:28:44 +13:00
Emmett Lalish 2d0c705839 v2.3 (#685)
bumped version
2024-01-08 21:06:58 +13:00
Emmett Lalish b36ec2ce0e fix model-viewer example (#686)
updated glTF extension name in GLBs
2024-01-07 22:59:19 +13:00
Emmett Lalish b8d143dd72 fix default partial revolve segments (#684)
* fixed revolve

* correct default segments
2024-01-07 21:27:02 +13:00
Emmett Lalish be022a58e4 fixed revolve (#682) 2024-01-06 23:30:03 +13:00
Emmett Lalish 30e0af942a remove simplify from warp (#677)
remove simplify
2023-12-29 10:24:07 +13:00
Kyle Finn 8c77a6aa05 [python] fix warp callback (#674)
* [python] fix warp callback

I recently broke this making it a no-op due to
the vec reference becoming a by-value copy in python
callback. This fix provides a simple adapter like
we had before allowing python to return modified
values from the callback.

* update torus_knot.py demo to expose both warp modes

* add torus_knot.py to pytest

* use pytest in github CI

* add pytest to flake.nix
2023-12-28 11:54:54 +13:00
Kyle Finn 54fa822408 Python auto doc (#665)
* python autogen docstrings

* use function signature hashes in docstring key

* remove sys.path debug code

* docstrings use param names

* autogen docstrings during cmake

* document gen_docs.py

* more readme, remap mesh_gl references

* readme about verify python docstrings, autogen depend on sources
2023-12-27 09:04:01 +13:00
Kyle Finn 7d564cfbc1 Python more numpy (#656)
* [python] numpy updates and cleanup

Also add CrossSection::Bounds()

* cleanup - const and reference args in py bind

* fix unnamed args

* all_apis.py more coverage

* fixup - more param renaming, docstring fixes

* fixup - run black

* fixup - move const to left side

* fixup extrude.py get_volume

* fix python examples for api update
2023-12-19 08:34:40 +08:00
Kyle Finn bc0bded294 add WarpBatch - array process warp with single callback (#651)
Add WarpBatch - array process warp with single callback
2023-12-17 22:15:28 +13:00
pca006132 d52decc4f1 example url (#643)
* example url

* update

* format

* address comments

* fix formatting
2023-12-16 18:25:45 +08:00
pca006132 8060376e12 add formatting script (#660)
* update formatting

* update formatter
2023-12-16 18:15:53 +08:00
Kyle Finn eba866d266 [python] manifold3d.triangulate and CrossSection.to_polygons_numpy (#650) 2023-12-14 21:51:48 +13:00
Emmett Lalish cc4966f062 added Colab Python example (#648)
added colab example
2023-12-14 07:33:50 +13:00
Emmett Lalish 3216a24be8 added Slice() (#644)
* added slice

* added test, addressed feedback

* added JS bindings

* remove code oddity

* added python bindings

* added C bindings
2023-12-13 13:48:50 +13:00
Kyle Finn 5e566c8c36 [python] fix JoinType.Round and simplify(epsilon) (#647) 2023-12-12 23:04:01 +13:00
Kyle Finn 80edfee978 [python] add revolveDegrees to revolve() API (#646) 2023-12-12 23:02:19 +13:00
pca006132 0e0d1c4f69 change extrude behavior (#642) 2023-12-12 13:17:07 +08:00
Emmett Lalish e15f4cf375 Added Project() (#638)
* added Project

* updated Clipper2 to 1.3.0

* fix simplify to remove more cruft

* update docs
2023-12-10 19:33:27 +08:00
pca006132 c9322ca63a Misc (#640)
* update tracy

* fix warnings

* remove dead code and warnings

* add comment
2023-12-09 15:40:34 +08:00
Emmett Lalish 45826d6c47 Added morph animation to ManifoldCAD.org (#637)
* added morph animation

* update test
2023-12-06 22:45:25 +13:00
Emmett Lalish dd781cdbdf Fix precision (#635)
* fixed empty collider

* disable test

* added svd

* use glm::mat3

* cleanup

* fix casing

* fixed typo
2023-12-06 10:28:08 +13:00
Emmett Lalish 638badaf9b added fuzzer cases (#633)
* added fuzzer cases

* not infinite looping anymore

* fixed loop check

* fixed bound calc
2023-12-04 15:14:33 +13:00
Emmett Lalish 0149e482bc EXT_manifold -> EXT_mesh_manifold (#630) 2023-11-27 16:18:05 -08:00
pca006132 f683e8c9c0 added triangulator fuzzer (#620)
* added triangulator fuzzer

* added easy to use minimizer script

* update readme

* added rounding
2023-11-25 21:51:07 +08:00
Chun Kit LAM 1f8e8037f1 real fix for 3MF export (#627)
* real fix

* formatting

* updated jscadui/3mf-export
2023-11-23 02:28:11 +08:00
Chun Kit LAM 10a15c1dd4 add thumbnail file to 3mf output (#626)
add _rel to 3mf output
2023-11-22 08:14:37 +08:00
Emmett Lalish 6e1629ddce updated tet puzzle example (#625)
* updated tet puzzle example

* updated description
2023-11-19 23:01:33 -08:00
Emmett Lalish e273f6b09d Add animation to manifoldCAD (#624)
* added global config

* translation animation works

* single times accessor

* animations working

* animation controls working

* add icons
2023-11-19 10:49:29 -08:00
Emmett Lalish 67c468211d maybe fix release actions (#623) 2023-11-18 21:50:02 -08:00
Emmett Lalish 01c38e42ca V2.2.2 (#622)
* bumped version

* added npm publishing automation
2023-11-17 10:20:16 -08:00
Chun Kit LAM 1991232eb7 fix performance regression for CreateProperties (#621)
* fix performance regression for CreateProperties

* added SparseIndices size check

* faster CreateProperties

* fix

* rollback and fix

* applied suggestion
2023-11-17 07:58:33 -08:00
Chun Kit LAM 4551288f95 Use black formatter for python format checking (#618)
* added black formatter to ci

* format gyroid_module.py
2023-11-16 20:44:07 +08:00
Emmett Lalishandpca006132 2e29784449 Update python bindings (#615)
* updated python bindings

* added scallop example

* fix python binding and example

* apply black formatting

---------

Co-authored-by: pca006132 <john.lck40@gmail.com>
2023-11-15 22:51:51 -08:00
Johnathon Selstad 2a2cb6f88e [FEAT] Add LevelSet Function to Python Bindings (#616)
* Commit LevelSet Python Binding

* Address PR Comments

* Standardize Indentation
2023-11-15 22:24:45 -08:00
Rob McDonald 1a91d8db2a RFC to make SimplifyTopology() steps optional (#603)
Add option to skip cleaning up triangles in SimplifyTopology()
2023-11-14 15:23:51 -08:00
Emmett Lalish 82b98d9353 Improve gltf-io and manifold-gltf (#612)
* fixed build copy for npm

* update deps

* fixed glTF validator errors

* fixed bug and Vite

* clamped color

* converted manifold-gltf to TS

* converted gltf-io to TS

* fixed TS build

* avoid usage warnings

* update MV

* clean up types

* update gltf-io API

* fixed bugs

* fixed disposal

* fixed example

* a few fixes and cleanup
2023-11-14 15:23:25 -08:00
Emmett Lalish 7273c9bf0f Fix odd number halfedges (#610)
* fixed odd number of halfedges

* both tests fixed

* reenable tests

* wrong isfinite
2023-11-12 16:09:42 -08:00
4ff3f7cf68 Apparent infinite loop case for boolean operation (#608)
* Apparent infinite loop case for boolean operation

This comes from the FAA Generic Twin example model included with BRL-CAD
- the glb files are written out from the input data structures that
triggered the infinite loop behavior.

* Fix formatting

* disabled GenericTwinBooleanTest7863

---------

Co-authored-by: pca006132 <john.lck40@gmail.com>
Co-authored-by: Emmett Lalish <elalish@gmail.com>
2023-11-12 15:23:37 -08:00
Clifford Yappandpca006132 fafe4792da Prepare a test input with failing boolean (#609)
* Prepare a test input with failing boolean

Encountered this case when testing Manifold with the BRL-CAD example
file - specifically, the r.glass8 region.  C++ exception "Vec out of
range" is thrown.  Both inputs are the outputs of previous Manifold
boolean evaluations.

* disabled Havocglass8Bool test

---------

Co-authored-by: pca006132 <john.lck40@gmail.com>
2023-11-12 14:30:26 -08:00
Emmett Lalish b439603eb9 Fix glTF validator errors (#596)
* fixed build copy for npm

* update deps

* fixed glTF validator errors

* fixed bug and Vite
2023-11-07 16:10:52 -08:00
Chun Kit LAM b6eea01021 fix cmake build error with thrust installed (#595) 2023-11-07 22:49:57 +08:00
Johnathon Selstad b9f7228064 [FIX] Packaged Split() output to Tuple (#593)
* Fix the Split Function

* Fix Formatting

* Also update split_by_plane

* Add Cube Splitting Example
2023-11-07 12:18:27 +08:00
Chun Kit LAM 0ef5f70434 fix wasm cmake problems (#591)
* fix cmake problems

* update emsdk version
2023-11-07 09:38:06 +08:00
Chun Kit LAM bb6acf1f6e V2.2.1 (#589)
* added notes about twistDegree and nDivisions

* update version

* removed perf comparison figure

it is pretty outdated considering we don't have the GPU backend anymore
and performance is different now.
2023-11-02 22:00:27 -07:00
Emmett Lalish 5930b5c09d Fix NaN halfedgeTangents (#588)
fixed CreateTangents
2023-11-02 12:35:44 -07:00
Emmett Lalish a842de97c6 Fix properties (#587)
* fix testing

* added test

* add comments

* tests pass

* cleanup
2023-11-02 12:07:58 -07:00
Chun Kit LAM 2942229b49 push to pypi (#583)
* push to pypi

* update action name
2023-10-31 11:13:02 +08:00
Chun Kit LAM 0856539bfc fix ubuntu build (#582) 2023-10-30 17:56:09 +08:00
Emmett Lalish 621835683f Earclip (#564)
* earclip compiles

* some tests passing

* 46 tests passing

* refactor

* cleanup

* new cost

* cleanup

* fixed TouchingHole

* more fixes

* only 5 tests failing

* fixed short sides

* fixed Sponge2

* better cost

* improved keyholing

* Moved Rect to public

* added top/bottom check

* fixed bBox

* Polygon tests pass

* fixed Sponge3a

* another fix, another test

* keyhole refactor

* Added ClipDegenerates

* Refactor FindStarts

* fixed bugs

* fixed Sponge4

* refactor clipped

* another bug, another test

* another bug, another test

* Sponge4 passing

* all tests passing

* removed old triangulator

* fix isfinite

* debug checks

* remove 2-manifold check

* added comments

* fixed missing point bug

* another test, another fix

* two tests added and fixed

* fix build

* fixed CoplanarProp

* fixed seg fault

* reenable 2-manifold check

* refactor for polygon area

* factored out loop

* tests passing

* another test, another fix

* another test, another fix

* fixed dedupe segfault

* ignore duplicated verts

* re-enabled turn180

* re-enabled duplicate tests

* another test, another fix

* refactor keyholing

* more precise area computation

* disabled a test

* added test

* another test, another test

* fixed bridge bound

* fewer short edges

* replaced intersection sorting with InsideEdge

* another test, another fix

* allow a few degenerates in Sponge4
2023-10-25 08:16:03 -07:00
Chun Kit LAM bec8164120 fix osx 10.13 error (#577)
* fix osx 10.13 error

* errors regarding std::optional

* format
2023-10-15 09:12:51 -07:00
Sebastien Loriot 062399c1c7 Fix perf test (#575)
do translate the sphere
2023-10-11 02:08:46 +08:00
Chun Kit LAM d51ee709c2 optimized connected component computation (#569)
* use union find for graph connected components

* update readme

* remove graphlite dep
2023-10-05 23:42:57 +08:00
Chun Kit LAM 206871ac11 dependency update (#559)
* dependency update

* revert BUILD_SHARED_LIBS changes

* test

* .

* don't try weird stuff with windows

* update

* update CI
2023-09-15 00:42:12 +08:00
Johnathon Selstad 2055b9c412 [FEAT] Add Status() to the Python Bindings (#558)
* Add Status() to the Python Bindings

This will help consumers of the Python bindings diagnose why their input meshes are not translating into manifolds properly...

* Make clang-format happier

* Use its suggestions verbatim
2023-09-12 08:24:30 -07:00
Chun Kit LAM 5e103931f1 added deterministic option (#550)
* added deterministic option

* fix formatting

* address some comments

* use kahan summation

* revert DoesOverlap changes

* remove old code

* formatting
2023-09-05 14:12:34 +08:00
Chun Kit LAM 77051ead35 misc (#548)
* use stl unique

* use stl sort
2023-09-02 15:03:22 +08:00
Eric Vin 83b78f8356 Python Bindings: Change tri_verts type from float to int (#546)
Change tri_verts type from float to int
2023-08-31 01:03:10 +08:00
Chun Kit LAM 6c6193c25b fix ndarray creation (#543) (#544) 2023-08-31 00:09:07 +08:00
Chun Kit LAM e79d343bec fix openscad build (#539)
* fix openscad build

* update clipper2

* conditional compile with pstl

* try NOMINMAX for MSVC

* detect win32

* use struct instead of lambda

* fix non-tbb build

* try to add mxe to CI

* disable pybind for mxe
2023-08-29 03:05:22 +08:00
Chun Kit LAM 1d6a89ac87 fix undefined behavior for SparseIndices (#540) 2023-08-28 13:47:21 +08:00
Chun Kit LAM 8651bf8bd5 nanobind (#537)
* python stuff

* do not choose musllinux_1_2

* skip musl build

musllinux_1_1 is too old

* try musllinux_1_2

* pep-0632

* fix

* update

* fix CI

* skip emulated tests on mac

* use build frontend

* use scikit-build-core

* re-enable action

* minor changes

speed up compilation a bit

* use nanobind

* don't use SABIModule for older python versions

* fix build error

* fix find and replace error

* Revert "re-enable action"

This reverts commit b3ede2a707743def16ded29bfb8e2b9a1c37f596.
2023-08-28 13:41:34 +08:00
Chun Kit LAM 1514c15184 fix decimator (#534) 2023-08-24 13:00:09 +08:00
Chun Kit LAM cc83e1b7aa misc (#533)
* use better policies

* removed redundant copy

* added condensed_matter test

* disable normal check for CondensedMatter16

* disable overlap checking as well
2023-08-24 02:09:51 +08:00
pca006132 111350f1ef fix memory leak for vec (#532)
* fix memory leak for vec

* removing reminiscence of CUDA

* formatting
2023-08-16 23:28:26 +08:00
pca006132 b4f583fcae refactor (#531)
* added bound check for vec_dh

* fix cmakefile

* refactored VecDH

* renamed VecDH into Vec

* better packaging

* formatting

* include <algorithm>

* fix windows

* documentation and copyright fixes

* use shorter names and implicit conversions

* formatting

* minimum circular segments = 3
2023-08-16 12:08:10 +08:00
Emmett Lalish 8011fb6072 Triangulator: shift on merge (#528)
* allow shift on merge

* fixed more tests with proper uncertainty

* fixed another test
2023-08-13 22:00:15 -07:00
pca006132 9290d420b0 performance tuning and disabling deprecated backends (#525)
* use single vector SparseIndices

* faster binary search

* tracy

* disable cuda and omp backends

* formatting

* find data races

* remove some cuda stuff

* rename indices to vertices

* fix

* better SparseIndices API

* minor fixes

* parallel boolean

* optimize csg_tree

* relax Samples.GyroidModule

* small fixes

* vec_dh refactor

* fix compiler errors

* disable some gcc warnings

* only apply new warning flags for GCC
2023-08-12 14:22:28 +08:00
Emmett Lalish fca66380f5 3mf node export (#517)
* added 3mf hierarchy support, not tested yet

* 3mf nodes work with updated exporter

* updated 3mfexport version
2023-08-07 22:35:50 -07:00
pca006132 8497e44915 parallel some more (#522)
* parallel some more

* use std::mutex

* fix compilation error

* check if max concurrency > 1

* use task_group

* disable face reordering

* preserve face order

* comments

* fix cuda build

* fix meshid not found situation

* use explicit stack and scratch buffer

this avoids potential stack overflow and reduces allocation calls

* faster collider

* please cuda

* missing commit

* remove collider optimization

* dedup face_op.cpp

* dedup boolean_result.cpp

* remove ambiguous comment

* include array
2023-08-08 01:26:09 +08:00
pca006132 a7eac92087 third_party dependencies (#523)
* use system gtest if possible

* optional tracy support

* fix meshid not found situation

* try fix CI

* fix ubuntu build

* disable windows tbb build first

* update tbb config

* format

* fix cmake BUILD_SHARED_LIBS

we should make the library type implicit, so users can decide whether or
not to build shared libraries.

* revert public.h changes

* remove version requirement for gtest

* update tracy instructions

no need to use sudo,
and we need some debug flags for good stack capture

* fix js build
2023-08-08 01:12:59 +08:00
pca006132 15399474fa more triangulator check (#516)
* add minimize_testcase helper

* added testcases from #502

* minor cleanup

* Update polygon.cpp

* Update face_op.cpp
2023-08-06 14:57:07 +08:00
pca006132 2d513446f7 fixed for_each_host and for_each_n_host (#519) 2023-08-04 16:50:38 +08:00
Emmett Lalish abff7e8ed3 triangulator fix (#509)
* added tests

* revert param changes

* fixed Precision3 and Hole

* revert test change
2023-07-30 23:42:30 -07:00
Emmett Lalish 7c454dc7a4 tighten CCW (#508) 2023-07-29 22:37:52 -07:00
Emmett Lalishandpca006132 ffdd97d4d3 adding python set_properties (#504)
* adding test of properties

* python set_properties

* fixed sponge example

---------

Co-authored-by: pca006132 <john.lck40@gmail.com>
2023-07-29 22:10:28 -07:00
Emmett Lalishandpca006132 7facd0ecc0 Make save dropdown menu (#505)
* fix include

* made new dropdown

* mobile ui

* cleanup

---------

Co-authored-by: pca006132 <john.lck40@gmail.com>
2023-07-28 12:23:57 -07:00
pca006132 59760b3369 added basic 3mf export (#500)
* added basic 3mf export

* updated worker test

* fixes

* formatting

* fix
2023-07-27 08:34:51 -07:00
Stéphane GINIER 5f9b484b6c Preventing crash with self intersection (#501)
* Prevent infinite recursion

* Add test for self intersection
2023-07-26 21:43:55 -07:00
pca006132 18edefc6c4 some more optimizations (#503)
* simple speedup from stl algorithms

* fix build

* std::list allocator optimization

* added documentation

* fix weird formatting issues

* try fixing MacOS build

it seems that Apple Clang 14 does not support pstl

* fix again

* fix macos build

* fix apple

* clean up using macro

* fix apple?

* small fix...

* fix apple (again...)
2023-07-25 02:33:15 +08:00
Emmett Lalishandpca006132 93e06a2de8 Update python mesh (#492)
* updated python mesh

* updated python meshgl

* clean up array related stuff

* fix hsplit

* fix according to comments

* make to_mesh accept MeshGL

* fixes

---------

Co-authored-by: pca006132 <john.lck40@gmail.com>
2023-07-24 12:08:00 +08:00
Geoff deRosenroll 047159e2c4 Add hull bindings (c, wasm/js, and python) (#497)
* Add C bindings to hull methods

* Add hull python bindings

* WIP adding JS bindings to hull methods

* Add flexible static hull for manifold

* Add hull_points to manifold class

* Remove compose binding

* Cleanup translate/rotate signature overloads
2023-07-20 09:25:27 -07:00
184 changed files with 86693 additions and 14773 deletions
+59
View File
@@ -0,0 +1,59 @@
name: build_wheels
on:
release:
types: [published]
workflow_dispatch:
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, windows-2019, macos-latest]
steps:
- run: |
git config --global submodule.fetchJobs 8
git config --global core.longpaths true
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build wheels
uses: pypa/cibuildwheel@v2.19
- uses: actions/upload-artifact@v4
with:
name: python-${{ matrix.os }}
path: ./wheelhouse/*.whl
make_sdist:
name: Make SDist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build SDist
run: pipx run build --sdist
- uses: actions/upload-artifact@v4
with:
name: python-sdist
path: dist/*.tar.gz
pypi-publish:
name: Upload release to PyPI
needs: [build_wheels, make_sdist]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/manifold3d
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/download-artifact@v4
with:
pattern: python-*
merge-multiple: true
path: dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+7 -4
View File
@@ -11,11 +11,14 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: DoozyX/clang-format-lint-action@v0.14
- uses: actions/checkout@v4
- uses: DoozyX/clang-format-lint-action@v0.17
with:
source: '.'
exclude: '*/third_party'
extensions: 'h,cpp,js,ts,html'
clangFormatVersion: 12
clangFormatVersion: 11
- uses: psf/black@stable
with:
options: "--check --verbose"
src: "./bindings/python/examples"
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Doxygen Action
uses: mattnotmitt/doxygen-action@v1
@@ -22,7 +22,7 @@ jobs:
mv ./docs ./public
- name: Download built examples
uses: dawidd6/action-download-artifact@v2
uses: dawidd6/action-download-artifact@v3
with:
workflow: manifold.yml
workflow_conclusion: completed
+110 -76
View File
@@ -15,84 +15,90 @@ jobs:
timeout-minutes: 45
strategy:
matrix:
cuda_support: [ON, OFF]
parallel_backend: [NONE, OMP, TBB]
runs-on: ubuntu-22.04
parallel_backend: [NONE, TBB]
gcc: [13, 14]
runs-on: ubuntu-24.04
env:
CC: gcc-${{ matrix.gcc }}
CXX: g++-${{ matrix.gcc }}
if: github.event.pull_request.draft == false
container:
image: docker://nvidia/cuda:12.0.1-devel-ubuntu22.04
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
apt-get -y update
DEBIAN_FRONTEND=noninteractive apt install -y libomp-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip lcov
pip install trimesh
- uses: actions/checkout@v3
sudo apt-get update
sudo apt-get install libglm-dev libgtest-dev libassimp-dev git libtbb-dev libthrust-dev pkg-config libpython3-dev lcov
python -m pip install -U trimesh pytest
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v1.12
- uses: jwlawson/actions-setup-cmake@v2
- name: Build ${{matrix.backend}}
if: matrix.parallel_backend != 'NONE' || matrix.cuda_support != 'OFF'
if: matrix.parallel_backend != 'NONE'
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} .. && make
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
# note that the test for CUDA backend does not really test CUDA, as we
# don't have CUDA GPU on GitHub Action
if: matrix.parallel_backend != 'NONE' || matrix.cuda_support != 'OFF'
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
- name: Test ${{matrix.parallel_backend}}
if: matrix.parallel_backend != 'NONE'
run: |
export PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python
cd build/test
./manifold_test
cd ../../
python3 bindings/python/examples/run_all.py
python3 bindings/python/examples/run_all.py -e
python3 -m pytest
- name: Coverage Report
# only do code coverage for default sequential backend, it seems that TBB
# backend will cause failure
# perhaps issue related to invalid memory access?
if: matrix.parallel_backend == 'NONE' && matrix.cuda_support == 'OFF'
if: matrix.parallel_backend == 'NONE'
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} -DCODE_COVERAGE=ON .. && make
lcov --capture --initial --directory . --output-file ./code_coverage_init.info
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DCODE_COVERAGE=ON .. && make
lcov --capture --gcov-tool gcov-${{ matrix.gcc }} --ignore-errors mismatch --initial --directory . --output-file ./code_coverage_init.info
cd test
./manifold_test
cd ../
lcov --capture --directory . --output-file ./code_coverage_test.info
lcov --capture --gcov-tool gcov-${{ matrix.gcc }} --ignore-errors mismatch --directory . --output-file ./code_coverage_test.info
lcov --add-tracefile ./code_coverage_init.info --add-tracefile ./code_coverage_test.info --output-file ./code_coverage_total.info
lcov --remove ./code_coverage_total.info '/usr/*' '*/third_party/*' '*/test/*' '*/extras/*' '*/bindings/*' --output-file ./code_coverage.info
- uses: codecov/codecov-action@v2
if: matrix.parallel_backend == 'NONE' && matrix.cuda_support == 'OFF'
cd ../
- uses: codecov/codecov-action@v4
if: matrix.parallel_backend == 'NONE'
with:
files: build/code_coverage.info
fail_ci_if_error: false
name: ${{matrix.parallel_backend}}-${{matrix.cuda_support}}
name: ${{matrix.parallel_backend}}
- name: test cmake consumer
run: |
cd build
sudo cmake --install .
cd ..
./test-cmake.sh
build_cbind:
timeout-minutes: 30
runs-on: ubuntu-22.04
if: github.event.pull_request.draft == false
container:
image: docker://nvidia/cuda:12.0.1-devel-ubuntu22.04
steps:
- name: Install dependencies
run: |
apt-get -y update
DEBIAN_FRONTEND=noninteractive apt install -y libomp-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip
- uses: actions/checkout@v3
sudo apt-get -y update
DEBIAN_FRONTEND=noninteractive sudo apt install -y libgtest-dev libglm-dev libassimp-dev git libtbb-dev pkg-config libpython3-dev python3 python3-distutils python3-pip
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v1.12
- name: Build C bindings with OMP and CUDA
- uses: jwlawson/actions-setup-cmake@v2
- name: Build C bindings with TBB
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=OMP -DMANIFOLD_USE_CUDA=ON .. && make
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
# note that the test for CUDA backend does not really test CUDA, as we
# don't have CUDA GPU on GitHub Action
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_CBIND=ON -DMANIFOLD_PAR=TBB .. && make
- name: Test ${{matrix.parallel_backend}}
run: |
cd build/test
./manifold_test --gtest_filter=CBIND.*
@@ -105,8 +111,8 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs
- uses: actions/checkout@v3
DEBIAN_FRONTEND=noninteractive sudo apt install -y nodejs libglm-dev
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup WASM
@@ -114,9 +120,9 @@ jobs:
# setup emscripten
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
- uses: jwlawson/actions-setup-cmake@v1.12
./emsdk install 3.1.45
./emsdk activate 3.1.45
- uses: jwlawson/actions-setup-cmake@v2
- name: Build WASM
run: |
source ./emsdk/emsdk_env.sh
@@ -133,56 +139,69 @@ jobs:
npm ci
npm run build
npm test
cp ../manifold.* ./dist/
- name: Upload WASM files
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wasm
path: bindings/wasm/examples/dist/
retention-days: 90
overwrite: true
build_windows:
timeout-minutes: 30
strategy:
matrix:
parallel_backend: [NONE]
cuda_support: [OFF]
parallel_backend: [NONE, TBB]
max-parallel: 1
runs-on: windows-2019
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v1.12
- uses: jwlawson/actions-setup-cmake@v2
- uses: ilammy/msvc-dev-cmd@v1
- name: Install CUDA
if: matrix.backend == 'CUDA'
env:
cuda: "11.3.0"
visual_studio: "Visual Studio 16 2019"
shell: powershell
run: |
Invoke-RestMethod https://raw.githubusercontent.com/ptheywood/cuda-cmake-github-actions/21811d232fc775fa8d44dddf7f8c5ce2cf8d2b81/scripts/actions/install_cuda_windows.ps1 -OutFile install_cuda_windows.ps1
.\install_cuda_windows.ps1
- name: nvcc check
if: matrix.backend == 'CUDA'
shell: powershell
run: |
nvcc -V
ls $env:CUDA_PATH
ls $env:CUDA_PATH\bin
ls $env:CUDA_PATH\include
- name: Build ${{matrix.backend}}
shell: powershell
run: |
cmake . -DCMAKE_BUILD_TYPE=Release -B build -DMANIFOLD_DEBUG=ON -DPYBIND11_FINDPYTHON=OFF -DMANIFOLD_PAR=${{matrix.parallel_backend}} -DMANIFOLD_USE_CUDA=${{matrix.cuda_support}} -A x64
cmake . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64 -B build
cd build
cmake --build . --target ALL_BUILD --config Release
- name: Test ${{matrix.parallel_backend}} with CUDA ${{matrix.cuda_support}}
- name: Test ${{matrix.parallel_backend}}
shell: bash
run: |
cd build/bin/Release
./manifold_test.exe
cd ../../
- name: test cmake consumer
run: |
cd build
cmake --install .
cd ..
./test-cmake.sh
build_mxe:
timeout-minutes: 30
strategy:
matrix:
parallel_backend: [NONE, TBB]
max-parallel: 1
runs-on: ubuntu-latest
container: openscad/mxe-x86_64-gui:latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_PYBIND=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
- name: Test
run: |
cd build/test
./Release/manifold_test.exe
./manifold_test
build_mac:
timeout-minutes: 30
@@ -194,38 +213,53 @@ jobs:
steps:
- name: Install common dependencies
run: |
brew install pkg-config assimp
pip install trimesh
- name: Install OpenMP
if: matrix.parallel_backend == 'OMP'
run: brew install libomp
brew install pkg-config googletest assimp
pip install trimesh pytest
- name: Install TBB
if: matrix.parallel_backend == 'TBB'
run: brew install tbb
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: jwlawson/actions-setup-cmake@v1.12
- uses: jwlawson/actions-setup-cmake@v2
- name: Build
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DMANIFOLD_DEBUG=ON -DMANIFOLD_PYBIND=ON -DMANIFOLD_EXPORT=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} .. && make
- name: Test
run: |
cd build/test
./manifold_test
cd ../../
- name: test cmake consumer
run: |
cd build
sudo cmake --install .
cd ..
./test-cmake.sh
build_nix:
timeout-minutes: 30
strategy:
matrix:
variant: [none, omp, tbb, none-cuda, omp-cuda, tbb-cuda, js]
variant: [none, tbb, js]
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: cachix/install-nix-action@v15
- uses: cachix/install-nix-action@v22
- run: nix build -L '.?submodules=1#manifold-${{matrix.variant}}'
build_nix_python:
timeout-minutes: 30
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: cachix/install-nix-action@v22
- run: nix build -L '.?submodules=1#manifold3d'
+34
View File
@@ -0,0 +1,34 @@
name: publish_npm
on:
release:
types: [published]
workflow_dispatch:
jobs:
publish_npm:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Download built examples
uses: dawidd6/action-download-artifact@v3
with:
workflow: manifold.yml
workflow_conclusion: completed
name: wasm
path: ./bindings/wasm/
- name: Publish to npm
run: |
cd ./bindings/wasm/
npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
+31
View File
@@ -0,0 +1,31 @@
name: Release File
on:
release:
types: [published]
workflow_dispatch:
jobs:
upload_archive:
name: Upload archive
runs-on: ubuntu-latest
steps:
- run: |
ref=${{ github.ref_name }}
echo "release_name=manifold-${ref#v}" >> $GITHUB_ENV
- uses: actions/checkout@v4
with:
path: ${{ env.release_name }}
submodules: recursive
- name: Build archive
run: >
tar --exclude=".git*" -cz ${{ env.release_name }} -f ${{ env.release_name }}.tar.gz
- name: Log checksum
run: >
sha256sum ${{ env.release_name }}.tar.gz
- name: Add file to release
run: |
cd ${{ env.release_name }} || exit 1
gh release upload ${{ github.ref_name }} ../${{ env.release_name }}.tar.gz
env:
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
+3 -16
View File
@@ -1,19 +1,6 @@
[submodule "third_party/thrust"]
path = src/third_party/thrust
url = https://github.com/NVIDIA/thrust
[submodule "third_party/glm"]
path = src/third_party/glm
url = https://github.com/g-truc/glm
[submodule "third_party/google_test"]
path = test/third_party/google_test
url = https://github.com/google/googletest.git
[submodule "third_party/pybind11"]
path = bindings/python/third_party/pybind11
url = https://github.com/pybind/pybind11
branch = stable
[submodule "src/third_party/clipper2"]
path = src/third_party/clipper2
url = https://github.com/AngusJohnson/clipper2
[submodule "src/third_party/quickhull"]
path = src/third_party/quickhull
url = https://github.com/akuukka/quickhull
[submodule "bindings/python/third_party/nanobind"]
path = bindings/python/third_party/nanobind
url = https://github.com/wjakob/nanobind
+4 -2
View File
@@ -7,11 +7,12 @@
{
"name": "manifold test",
"type": "cppdbg",
"MIMode": "lldb",
"request": "launch",
"program": "${workspaceFolder}/build/test/manifold_test",
"args": [
"--gtest_break_on_failure",
"--gtest_filter=Manifold.GetMesh"
"--gtest_filter=Smooth.RefineQuads"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build/test",
@@ -20,7 +21,8 @@
"name": "MALLOC_CHECK_",
"value": "2"
}
]
],
"externalConsole": false
}
]
}
+13 -3
View File
@@ -108,17 +108,22 @@
"latch": "cpp",
"semaphore": "cpp",
"span": "cpp",
"__sso_allocator": "cpp"
"__sso_allocator": "cpp",
"source_location": "cpp",
"format": "cpp",
"numbers": "cpp",
"ranges": "cpp",
"stop_token": "cpp"
},
"C_Cpp.clang_format_fallbackStyle": "google",
"editor.formatOnSave": true,
"cmake.configureArgs": [
"-DBUILD_SHARED_LIBS=ON",
"-DMANIFOLD_CBIND=ON",
"-DMANIFOLD_PYBIND=ON",
"-DMANIFOLD_EXPORT=ON",
"-DMANIFOLD_DEBUG=ON",
"-DMANIFOLD_USE_CUDA=OFF",
"-DMANIFOLD_PAR=NONE",
"-DMANIFOLD_PAR=TBB",
"-DCODE_COVERAGE=OFF",
"-DCMAKE_CXX_FLAGS=''" //'-fsanitize=address,undefined'"
],
@@ -140,4 +145,9 @@
"Verts"
],
"javascript.format.semicolons": "insert",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none",
"clang-format.executable": "clang-format",
}
+99 -40
View File
@@ -16,68 +16,101 @@ cmake_minimum_required(VERSION 3.18)
project(manifold LANGUAGES CXX)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(MANIFOLD_PAR "NONE" CACHE STRING "Parallel backend, either \"TBB\" or \"NONE\"")
set(MANIFOLD_FLAGS "" CACHE STRING "Manifold compiler flags")
option(MANIFOLD_EXPORT "Build mesh export (via assimp) utility library" OFF)
option(MANIFOLD_TEST "Enable testing suite" ON)
# fuzztest is a rather large dependency
option(MANIFOLD_FUZZ "Enable fuzzing tests" OFF)
option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF)
option(MANIFOLD_USE_CUDA "Enable GPU support via CUDA" OFF)
option(MANIFOLD_PYBIND "Build python bindings" ON)
option(MANIFOLD_CBIND "Build C (FFI) bindings" OFF)
set(MANIFOLD_PAR "NONE" CACHE STRING "Parallel backend, either \"TBB\" or \"OpenMP\" or \"NONE\"")
set(MANIFOLD_FLAGS -O3)
option(MANIFOLD_PYBIND "Build python bindings" OFF)
option(MANIFOLD_CBIND "Build C (FFI) bindings" ON)
option(MANIFOLD_JSBIND "Build js binding" ${EMSCRIPTEN})
option(BUILD_SHARED_LIBS "Build shared library" ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
if(MANIFOLD_FUZZ)
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(FATAL_ERROR "fuzztest only supports clang" )
endif()
# we should enable debug checks
set(MANIFOLD_DEBUG ON)
# enable fuzztest fuzzing mode
set(FUZZTEST_FUZZING_MODE ON)
# address sanitizer required
set(CMAKE_CXX_FLAGS "$CMAKE_CXX_FLAGS -fsanitize=address")
endif()
option(TRACY_ENABLE "Use tracy profiling" OFF)
option(TRACY_MEMORY_USAGE "Track memory allocation with tracy (expensive)" OFF)
option(BUILD_TEST_CGAL "Build CGAL performance comparisons" OFF)
if(TRACY_ENABLE)
option(CMAKE_BUILD_TYPE "Build type" RelWithDebInfo)
if(TRACY_MEMORY_USAGE)
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} -DTRACY_MEMORY_USAGE)
endif()
if(NOT MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer")
endif()
else()
option(CMAKE_BUILD_TYPE "Build type" Release)
endif()
if(EMSCRIPTEN)
message("Building for Emscripten")
set(MANIFOLD_FLAGS -fexceptions -D_LIBCUDACXX_HAS_THREAD_API_EXTERNAL -D_LIBCUDACXX_HAS_THREAD_API_CUDA)
set(CMAKE_EXE_LINKER_FLAGS ${CMAKE_EXE_LINKER_FLAGS} -sALLOW_MEMORY_GROWTH=1)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -sALLOW_MEMORY_GROWTH=1 -fexceptions -sDISABLE_EXCEPTION_CATCHING=0")
set(MANIFOLD_PYBIND OFF)
set(BUILD_SHARED_LIBS OFF)
endif()
option(PYBIND11_FINDPYTHON "Enable PyBind to perform FindPython for you" ON)
include(${CMAKE_CURRENT_SOURCE_DIR}/manifoldDeps.cmake)
option(BUILD_TEST_CGAL "Build CGAL performance comparisons" OFF)
option(BUILD_SHARED_LIBS "Build dynamic/shared libraries" OFF)
set(PYBIND11_DIR ${PROJECT_SOURCE_DIR}/bindings/python/third_party/pybind11)
set(THRUST_INC_DIR ${PROJECT_SOURCE_DIR}/src/third_party/thrust)
if(BUILD_SHARED_LIBS OR MANIFOLD_CBIND)
# Allow shared libraries to be relocatable (add Place Independent Code flag).
# Also include when statically linking C bindings to avoid issues with bundling
# artefacts in host languages using them for FFI.
add_compile_options(-fPIC)
if(MANIFOLD_PYBIND)
if(Python_VERSION VERSION_LESS 3.12)
find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
else()
find_package(Python COMPONENTS Interpreter Development.SABIModule REQUIRED)
endif()
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
if(MANIFOLD_USE_CUDA)
enable_language(CUDA)
find_package(CUDA REQUIRED)
# we cannot set THRUST_INC_DIR when building with CUDA, otherwise the
# compiler will not use the system CUDA headers which causes incompatibility
# clear THRUST_INC_DIR, we use the one from nvcc
set(THRUST_INC_DIR "")
set(MANIFOLD_NVCC_RELEASE_FLAGS -O3 -lineinfo)
set(MANIFOLD_NVCC_DEBUG_FLAGS -G)
set(MANIFOLD_NVCC_FLAGS -Xcudafe --diag_suppress=esa_on_defaulted_function_ignored --extended-lambda --expt-relaxed-constexpr
"$<$<CONFIG:RELEASE>:${MANIFOLD_NVCC_RELEASE_FLAGS}>"
"$<$<CONFIG:DEBUG>:${MANIFOLD_NVCC_DEBUG_FLAGS}>")
if(CMAKE_EXPORT_COMPILE_COMMANDS AND NOT EMSCRIPTEN)
# for nixos
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES
${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
endif()
if(NOT MSVC)
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
add_compile_options(
"$<$<COMPILE_LANGUAGE:CXX>:${WARNING_FLAGS}>"
"$<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=${WARNING_FLAGS}>")
if (MSVC)
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} /DNOMINMAX /bigobj)
else()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused -Wno-array-bounds
-Wno-stringop-overflow -Wno-alloc-size-larger-than)
else()
set(WARNING_FLAGS -Werror -Wall -Wno-sign-compare -Wno-unused)
endif()
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${WARNING_FLAGS})
endif()
if(CODE_COVERAGE AND NOT MSVC)
set(COVERAGE_FLAGS -coverage -fno-inline-small-functions -fkeep-inline-functions -fkeep-static-functions)
add_compile_options(
"$<$<COMPILE_LANGUAGE:CXX>:${COVERAGE_FLAGS}>"
"$<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=-coverage>")
set(MANIFOLD_FLAGS ${MANIFOLD_FLAGS} ${COVERAGE_FLAGS})
add_link_options("-coverage")
endif()
# installation rpath
set(CMAKE_SKIP_BUILD_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES "${CMAKE_INSTALL_PREFIX}/lib" isSystemDir)
if("${isSystemDir}" STREQUAL "-1")
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
endif("${isSystemDir}" STREQUAL "-1")
add_subdirectory(src)
add_subdirectory(bindings)
@@ -90,3 +123,29 @@ if(MANIFOLD_TEST)
add_subdirectory(test)
add_subdirectory(extras)
endif()
# installation related
set(MANIFOLD_VERSION_MAJOR 2)
set(MANIFOLD_VERSION_MINOR 5)
set(MANIFOLD_VERSION_PATCH 1)
set(MANIFOLD_VERSION "${MANIFOLD_VERSION_MAJOR}.${MANIFOLD_VERSION_MINOR}.${MANIFOLD_VERSION_PATCH}")
set_property(TARGET manifold PROPERTY VERSION "${MANIFOLD_VERSION}")
set_property(TARGET manifold PROPERTY SOVERSION 2)
if(NOT EMSCRIPTEN)
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
${CMAKE_BINARY_DIR}/cmake/manifoldConfigVersion.cmake
VERSION ${MANIFOLD_VERSION}
COMPATIBILITY SameMajorVersion
)
install(EXPORT manifoldTargets DESTINATION ${CMAKE_INSTALL_DATADIR}/manifold)
configure_file(manifoldConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/manifoldConfig.cmake @ONLY)
install(FILES
${CMAKE_CURRENT_SOURCE_DIR}/manifoldDeps.cmake
${CMAKE_CURRENT_BINARY_DIR}/cmake/manifoldConfigVersion.cmake
${CMAKE_CURRENT_BINARY_DIR}/manifoldConfig.cmake
DESTINATION ${CMAKE_INSTALL_DATADIR}/manifold
)
endif()
+88 -25
View File
@@ -1,41 +1,46 @@
[![codecov](https://codecov.io/github/elalish/manifold/branch/master/graph/badge.svg?token=IIA8G5HVS7)](https://codecov.io/github/elalish/manifold)
[![PyPI version](https://badge.fury.io/py/manifold3d.svg)](https://badge.fury.io/py/manifold3d)
[![npm version](https://badge.fury.io/js/manifold-3d.svg)](https://badge.fury.io/js/manifold-3d)
[![twitter](https://img.shields.io/twitter/follow/manifoldcad?style=social&logo=twitter)](https://twitter.com/intent/follow?screen_name=manifoldcad)
## Users
[OpenSCAD](https://openscad.org/), [IFCjs](https://ifcjs.github.io/info/), [Grid.Space](https://grid.space/), and [OCADml](https://github.com/OCADml/OManifold) have all integrated our Manifold geometry kernel! Why? Because its reliability is guaranteed and it's 1,000 times faster than other libraries. See our [usage](https://github.com/elalish/manifold/discussions/340) and [performance](https://github.com/elalish/manifold/discussions/383) discussions for all the latest and to add your own projects & analyses.
For example, here is a log-log plot of Manifold's performance vs. earlier OpenSCAD geometry backends:
## Manifold Frontend Sandboxes
<img src="https://elalish.github.io/manifold/samples/models/perfSpheres.png" width="350px"/>
### [ManifoldCAD.org](https://manifoldcad.org)
## [ManifoldCAD.org](https://manifoldcad.org)
If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid modelling web app where you script in JS/TS. This uses our npm package, [manifold-3d](https://www.npmjs.com/package/manifold-3d), built via WASM. It's not quite as fast as our raw C++, but it's hard to beat for interoperability.
If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid modelling web app. Our WASM is not GPU-accelerated, but it's still quite fast and a good way to test out our Manifold library.
*Note for Firefox users: If you find the editor is stuck on **Loading...**, setting
`dom.workers.modules.enabled: true` in your `about:config`, as mentioned in
[issue#328](https://github.com/elalish/manifold/issues/328#issuecomment-1473847102)
may solve the problem.*
### [Python Colab Example](https://colab.research.google.com/drive/1VxrFYHPSHZgUbl9TeWzCeovlpXrPQ5J5?usp=sharing)
If you prefer Python to JS/TS, make your own copy of the example notebook above. It demonstrates interop between our [`manifold3d`](https://pypi.org/project/manifold3d/) PyPI library and the popular [`trimesh`](https://pypi.org/project/trimesh/) library, including showing the interactive model right in the notebook and saving 3D model output.
![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)
[**API Documentation**](https://elalish.github.io/manifold/docs/html/topics.html) | [**Algorithm Documentation**](https://github.com/elalish/manifold/wiki/Manifold-Library) | [**Blog Posts**](https://elalish.blogspot.com/search/label/Manifold) | [**Web Examples**](https://elalish.github.io/manifold/model-viewer.html)
[Manifold](https://github.com/elalish/manifold) is a geometry library dedicated to creating and operating on manifold triangle meshes. A [manifold mesh](https://github.com/elalish/manifold/wiki/Manifold-Library#manifoldness) is a mesh that represents a solid object, and so is very important in manufacturing, CAD, structural analysis, etc. Further information can be found on the [wiki](https://github.com/elalish/manifold/wiki/Manifold-Library).
This is a modern C++ library that Github's CI verifies builds and runs on a variety of platforms. Additionally, we build bindings for JavaScript ([manifold-3d](https://www.npmjs.com/package/manifold-3d) on npm), Python, and C to make this library more portable and easy to use.
This is a modern C++ library that Github's CI verifies builds and runs on a variety of platforms. Additionally, we build bindings for JavaScript ([manifold-3d](https://www.npmjs.com/package/manifold-3d) on npm), Python ([manifold3d](https://pypi.org/project/manifold3d/)), and C to make this library more portable and easy to use.
We have four core dependencies, making use of submodules to ensure compatibility:
- `graphlite`: connected components algorithm
- `Clipper2`: provides our 2D subsystem
- `GLM`: a compact vector library
- `Thrust`: Nvidia's parallel algorithms library (basically a superset of C++17 std::parallel_algorithms)
System Dependencies (note that we will automatically download the dependency if there is no such package on the system):
- [`GLM`](https://github.com/g-truc/glm/): A compact header-only vector library.
- [`Thrust`](https://github.com/NVIDIA/thrust): NVIDIA's parallel algorithms library (basically a superset of C++17 std::parallel_algorithms)
- [`tbb`](https://github.com/oneapi-src/oneTBB/): Intel's thread building blocks library. (only when `MANIFOLD_PAR=TBB` is enabled)
- [`gtest`](https://github.com/google/googletest/): Google test library (only when test is enabled, i.e. `MANIFOLD_TEST=ON`)
Other dependencies:
- [`Clipper2`](https://github.com/AngusJohnson/Clipper2): provides our 2D subsystem
- [`quickhull`](https://github.com/akuukka/quickhull): 3D convex hull algorithm.
## What's here
@@ -43,7 +48,9 @@ This library is fast with guaranteed manifold output. As such you need manifold
The most significant contribution here is a guaranteed-manifold [mesh Boolean](https://github.com/elalish/manifold/wiki/Manifold-Library#mesh-boolean) algorithm, which I believe is the first of its kind. If you know of another, please open a discussion - a mesh Boolean algorithm robust to edge cases has been an open problem for many years. Likewise, if the Boolean here ever fails you, please submit an issue! This Boolean forms the basis of a CAD kernel, as it allows simple shapes to be combined into more complex ones.
To aid in speed, this library makes extensive use of parallelization, generally through Nvidia's Thrust library. You can switch between the CUDA, OMP, TBB, and serial C++ backends by setting a CMake flag. Not everything is so parallelizable, for instance a [polygon triangulation](https://github.com/elalish/manifold/wiki/Manifold-Library#polygon-triangulation) algorithm is included which is serial. Even if compiled for CUDA, the code will still run without a GPU, falling back to the serial version of the algorithms. The WASM build is serial-only for now, but still fast.
To aid in speed, this library makes extensive use of parallelization, generally through Nvidia's Thrust library. You can switch between the TBB, and serial C++ backends by setting a CMake flag. Not everything is so parallelizable, for instance a [polygon triangulation](https://github.com/elalish/manifold/wiki/Manifold-Library#polygon-triangulation) algorithm is included which is serial. Even if compiled with parallel backend, the code will still fall back to the serial version of the algorithms if the problem size is small. The WASM build is serial-only for now, but still fast.
> Note: OMP and CUDA backends are now removed
Look in the [samples](https://github.com/elalish/manifold/tree/master/samples) directory for examples of how to use this library to make interesting 3D models. You may notice that some of these examples bare a certain resemblance to my OpenSCAD designs on [Thingiverse](https://www.thingiverse.com/emmett), which is no accident. Much as I love OpenSCAD, my library is dramatically faster and the code is more flexible.
@@ -61,16 +68,30 @@ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON .. && make
test/manifold_test
```
CMake flags (usage e.g. `-DMANIFOLD_USE_CUDA=ON`):
- `MANIFOLD_USE_CUDA=[<OFF>, ON]`: Provides Nvidia GPU parallelization, requires CUDA Toolkit and its nvcc compiler.
- `MANIFOLD_PAR=[<NONE>, OMP, TBB]`: Provides multi-thread parallelization, requires `libomp-dev` or `libtbb-dev`.
CMake flags (usage e.g. `-DMANIFOLD_DEBUG=ON`):
- `MANIFOLD_JSBIND=[OFF, <ON>]`: Build js binding when using emscripten.
- `MANIFOLD_CBIND=[<OFF>, ON]`: Build C FFI binding.
- `MANIFOLD_PYBIND=[OFF, <ON>]`: Build python binding.
- `MANIFOLD_PAR=[<NONE>, TBB]`: Provides multi-thread parallelization, requires `libtbb-dev` if `TBB` backend is selected.
- `MANIFOLD_EXPORT=[<OFF>, ON]`: Enables GLB export of 3D models from the tests, requires `libassimp-dev`.
- `MANIFOLD_DEBUG=[<OFF>, ON]`: Enables internal assertions and exceptions.
- `MANIFOLD_TEST=[OFF, <ON>]`: Build unittests.
- `TRACY_ENABLE=[<OFF>, ON]`: Enable integration with tracy profiler.
See profiling section below.
- `BUILD_TEST_CGAL=[<OFF>, ON]`: Builds a CGAL-based performance [comparison](https://github.com/elalish/manifold/tree/master/extras), requires `libcgal-dev`.
Offline building:
- `FETCHCONTENT_SOURCE_DIR_GLM`: path to glm source.
- `FETCHCONTENT_SOURCE_DIR_GOOGLETEST`: path to googletest source.
- `FETCHCONTENT_SOURCE_DIR_THRUST`: path to NVIDIA thrust source.
The build instructions used by our CI are in [manifold.yml](https://github.com/elalish/manifold/blob/master/.github/workflows/manifold.yml), which is a good source to check if something goes wrong and for instructions specific to other platforms, like Windows.
### WASM
> Note that we have only tested emscripten version 3.1.45. It is known that
3.1.48 has some issues compiling manifold.
To build the JS WASM library, first install NodeJS and set up emscripten:
(on Mac):
@@ -99,12 +120,12 @@ node test/manifold_test.js
### Python
The CMake script will build the python binding `manifold3d` automatically. To
use the extension, please add `$BUILD_DIR/tools` to your `PYTHONPATH`, where
use the extension, please add `$BUILD_DIR/bindings/python` to your `PYTHONPATH`, where
`$BUILD_DIR` is the build directory for CMake. Examples using the python binding
can be found in `bindings/python/examples`. To see exported samples, run:
```
sudo apt install pkg-config libpython3-dev python3 python3-distutils python3-pip
pip install trimesh
pip install trimesh pytest
python3 run_all.py -e
```
@@ -118,10 +139,52 @@ python binding documentation:
For more detailed documentation, please refer to the C++ API.
### Java / Clojure
Unofficial java bindings are currently maintained in [a fork](https://github.com/SovereignShop/manifold).
There is also a Clojure [library](https://github.com/SovereignShop/clj-manifold3d).
### Windows Shenanigans
Windows users should build with `-DBUILD_SHARED_LIBS=OFF`, as enabling shared
libraries in general makes things very complicated.
The DLL file for manifoldc (C FFI bindings) when built with msvc is in `${CMAKE_BINARY_DIR}/bin/${BUILD_TYPE}/manifoldc.dll`.
For example, for the following command, the path relative to the project root directory is `build/bin/Release/manifoldc.dll`.
```sh
cmake . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DMANIFOLD_DEBUG=ON -DMANIFOLD_PAR=${{matrix.parallel_backend}} -A x64 -B build
```
## Contributing
Contributions are welcome! A lower barrier contribution is to simply make a PR that adds a test, especially if it repros an issue you've found. Simply name it prepended with DISABLED_, so that it passes the CI. That will be a very strong signal to me to fix your issue. However, if you know how to fix it yourself, then including the fix in your PR would be much appreciated!
### Formatting
There is a formatting script `format.sh` that automatically formats everything.
It requires clang-format 11 and black formatter for python.
If you have clang-format installed but without clang-11, you can specify the
clang-format executable by setting the `CLANG_FORMAT` environment variable.
### Profiling
There is now basic support for the [Tracy profiler](https://github.com/wolfpld/tracy) for our tests.
To enable tracing, compile with `-DTRACY_ENABLE=on` cmake option, and run the test with Tracy server running.
To enable memory profiling in addition to tracing, compile with `-DTRACY_MEMORY_USAGE=ON` in addition to `-DTRACY_ENABLE=ON`.
### Fuzzing Support
We use https://github.com/google/fuzztest for fuzzing the triangulator.
To enable fuzzing, make sure that you are using clang compiler (`-DCMAKE_CXX_COMPILER=clang -DCMAKE_C_COMPILER=clang`), running Linux, and enable fuzzing support by setting `-DMANIFOLD_FUZZ=ON`.
To run the fuzzer and minimize testcase, do
```
../minimizer.sh ./test/polygon_fuzz --fuzz=PolygonFuzz.TriangulationNoCrash
```
## About the author
This library was started by [Emmett Lalish](https://elalish.blogspot.com/). I am currently a Google employee and this is my 20% project, not an official Google project. At my day job I'm the maintainer of [\<model-viewer\>](https://modelviewer.dev/). I was the first employee at a 3D video startup, [Omnivor](https://www.omnivor.io/), and before that I worked on 3D printing at Microsoft, including [3D Builder](https://www.microsoft.com/en-us/p/3d-builder/9wzdncrfj3t6?activetab=pivot%3Aoverviewtab). Originally an aerospace engineer, I started at a small DARPA contractor doing seedling projects, one of which became [Sea Hunter](https://en.wikipedia.org/wiki/Sea_Hunter). I earned my doctorate from the University of Washington in control theory and published some [papers](https://www.researchgate.net/scientific-contributions/75011026_Emmett_Lalish).
+14
View File
@@ -0,0 +1,14 @@
1. Go through Github issues to verify bugs have been fixed and closed.
1. Verify [ManifoldCAD.org](https://manifoldcad.org) - check a few examples, run them, download a GLB and a 3MF.
1. Verify our three.js [example](https://manifoldcad.org/three) is functional.
1. Verify our model-viewer [example](https://manifoldcad.org/model-viewer) is functional - select Union and Intersection.
1. Verify [make-manifold](https://manifoldcad.org/make-manifold) is functional. Try dropping [DragonAttenuation.glb](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/DragonAttenuation/glTF-Binary/DragonAttenuation.glb) in and verify you can select "View Manifold GLB" and that the dragon is still present while the backdrop is removed. Download the GLB.
1. Make a new branch called the version, e.g. v2.3.0.
1. Use VSCode to search and replace the old version with the new - so far in test-cmake.sh, flake.nix, pyproject.toml, and package.json.
1. Also update CMakeLists.txt version by searching for "set(MANIFOLD_VERSION_".
1. Commit, push, open a PR, verify tests pass, merge.
1. On Github, draft a new release, make a new tag with the version number, add release notes, and publish.
1. Check the Actions and verify that both PyPI and npm publishing actions ran successfully.
1. Verify the npm [package](https://www.npmjs.com/package/manifold-3d?activeTab=code) looks good - unpacked size should be close to 1MB.
1. Verify PyPI [package](https://pypi.org/project/manifold3d/#files) looks good - a bunch of built distributions ranging from ~600kB to ~1.1MB.
1. If there's a problem with release deployment, the release workflows can be triggered separately, manually for any branch, under the Actions tab.
+1 -1
View File
@@ -20,6 +20,6 @@ if(NOT EMSCRIPTEN AND MANIFOLD_PYBIND)
add_subdirectory(python)
endif()
if(EMSCRIPTEN)
if(EMSCRIPTEN AND MANIFOLD_JSBIND)
add_subdirectory(wasm)
endif()
+18 -9
View File
@@ -1,22 +1,31 @@
project(manifoldc)
add_library(${PROJECT_NAME} manifoldc.cpp conv.cpp)
# this is a shared library for FFI bindings
add_library(${PROJECT_NAME} SHARED manifoldc.cpp conv.cpp)
if(MANIFOLD_USE_CUDA)
set_source_files_properties(manifoldc.cpp conv.cpp PROPERTIES LANGUAGE CUDA)
set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES 61)
if(MSVC)
set_target_properties(${PROJECT_NAME} PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/bin )
set_target_properties(${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/bin )
endif()
if(MANIFOLD_EXPORT)
target_link_libraries(${PROJECT_NAME} PRIVATE meshIO)
target_compile_options(${PROJECT_NAME} PUBLIC -DMANIFOLD_EXPORT)
target_link_libraries(${PROJECT_NAME} PRIVATE meshIO)
target_compile_options(${PROJECT_NAME} PUBLIC -DMANIFOLD_EXPORT)
endif()
target_link_libraries(
${PROJECT_NAME}
PRIVATE manifold sdf graphlite cross_section
${PROJECT_NAME}
PRIVATE manifold sdf cross_section
)
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_include_directories(${PROJECT_NAME} PUBLIC
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
install(FILES include/manifoldc.h include/types.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
+16 -3
View File
@@ -1,7 +1,20 @@
#include <conv.h>
#include <manifold.h>
#include <public.h>
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "conv.h"
#include "manifold.h"
#include "public.h"
#include "types.h"
ManifoldBox *manifold_box(void *mem, float x1, float y1, float z1, float x2,
+17 -4
View File
@@ -1,12 +1,25 @@
#include <conv.h>
#include <manifold.h>
#include <sdf.h>
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "conv.h"
#include <vector>
#include "cross_section.h"
#include "include/types.h"
#include "manifold.h"
#include "public.h"
#include "sdf.h"
#include "types.h"
ManifoldManifold *to_c(manifold::Manifold *m) {
+47 -6
View File
@@ -1,10 +1,23 @@
#include <conv.h>
#include <manifold.h>
#include <public.h>
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <vector>
#include "conv.h"
#include "cross_section.h"
#include "manifold.h"
#include "public.h"
#include "types.h"
ManifoldCrossSection *manifold_cross_section_empty(void *mem) {
@@ -106,6 +119,30 @@ ManifoldCrossSection *manifold_cross_section_intersection(
return to_c(new (mem) CrossSection(cs));
}
ManifoldCrossSection *manifold_cross_section_hull(void *mem,
ManifoldCrossSection *cs) {
auto hulled = from_c(cs)->Hull();
return to_c(new (mem) CrossSection(hulled));
}
ManifoldCrossSection *manifold_cross_section_batch_hull(
void *mem, ManifoldCrossSectionVec *css) {
auto hulled = CrossSection::Hull(*from_c(css));
return to_c(new (mem) CrossSection(hulled));
}
ManifoldCrossSection *manifold_cross_section_hull_simple_polygon(
void *mem, ManifoldSimplePolygon *ps) {
auto hulled = CrossSection::Hull(*from_c(ps));
return to_c(new (mem) CrossSection(hulled));
}
ManifoldCrossSection *manifold_cross_section_hull_polygons(
void *mem, ManifoldPolygons *ps) {
auto hulled = CrossSection::Hull(*from_c(ps));
return to_c(new (mem) CrossSection(hulled));
}
ManifoldCrossSection *manifold_cross_section_translate(void *mem,
ManifoldCrossSection *cs,
float x, float y) {
@@ -145,9 +182,13 @@ ManifoldCrossSection *manifold_cross_section_transform(void *mem,
}
ManifoldCrossSection *manifold_cross_section_warp(
void *mem, ManifoldCrossSection *cs, ManifoldVec2 (*fun)(float, float)) {
std::function<void(glm::vec2 & v)> warp = [fun](glm::vec2 &v) {
v = from_c(fun(v.x, v.y));
void *mem, ManifoldCrossSection *cs,
ManifoldVec2 (*fun)(float, float, void *), void *ctx) {
// Bind function with context argument to one without
using namespace std::placeholders;
std::function<ManifoldVec2(float, float)> f2 = std::bind(fun, _1, _2, ctx);
std::function<void(glm::vec2 & v)> warp = [f2](glm::vec2 &v) {
v = from_c(f2(v.x, v.y));
};
auto warped = from_c(cs)->Warp(warp);
return to_c(new (mem) CrossSection(warped));
+20 -5
View File
@@ -1,13 +1,28 @@
#pragma once
#include <manifold.h>
#include <public.h>
#include <sdf.h>
#include <types.h>
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cstring>
#include <vector>
#include "cross_section.h"
#include "manifold.h"
#include "public.h"
#include "sdf.h"
#include "types.h"
using namespace manifold;
using ManifoldVec = std::vector<Manifold>;
using CrossSectionVec = std::vector<CrossSection>;
+76 -18
View File
@@ -1,6 +1,21 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stddef.h>
#include <stdint.h>
#include <types.h>
#include "types.h"
#ifdef __cplusplus
#include <cstdint>
@@ -8,6 +23,7 @@ extern "C" {
#endif
// Polygons
ManifoldSimplePolygon *manifold_simple_polygon(void *mem, ManifoldVec2 *ps,
size_t length);
ManifoldPolygons *manifold_polygons(void *mem, ManifoldSimplePolygon **ps,
@@ -24,6 +40,7 @@ ManifoldVec2 manifold_polygons_get_point(ManifoldPolygons *ps, int simple_idx,
int pt_idx);
// Mesh Construction
ManifoldMeshGL *manifold_meshgl(void *mem, float *vert_props, size_t n_verts,
size_t n_props, uint32_t *tri_verts,
size_t n_tris);
@@ -34,6 +51,7 @@ ManifoldMeshGL *manifold_meshgl_w_tangents(void *mem, float *vert_props,
float *halfedge_tangent);
ManifoldMeshGL *manifold_get_meshgl(void *mem, ManifoldManifold *m);
ManifoldMeshGL *manifold_meshgl_copy(void *mem, ManifoldMeshGL *m);
ManifoldMeshGL *manifold_meshgl_merge(void *mem, ManifoldMeshGL *m);
// SDF
// By default, the execution policy (sequential or parallel) of
@@ -42,20 +60,11 @@ ManifoldMeshGL *manifold_meshgl_copy(void *mem, ManifoldMeshGL *m);
// using these bindings from a language that has a runtime lock preventing the
// parallel execution of closures, then you should use manifold_level_set_seq to
// force sequential execution.
ManifoldMeshGL *manifold_level_set(void *mem, float (*sdf)(float, float, float),
ManifoldMeshGL *manifold_level_set(void *mem,
float (*sdf)(float, float, float, void *),
ManifoldBox *bounds, float edge_length,
float level);
ManifoldMeshGL *manifold_level_set_seq(void *mem,
float (*sdf)(float, float, float),
ManifoldBox *bounds, float edge_length,
float level);
// The _context variants of manifold_level_set allow a pointer to be passed
// back to each invocation of the sdf function pointer, for languages that
// need additional data.
ManifoldMeshGL *manifold_level_set_context(
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
float edge_length, float level, void *ctx);
ManifoldMeshGL *manifold_level_set_seq_context(
float level, void *ctx);
ManifoldMeshGL *manifold_level_set_seq(
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
float edge_length, float level, void *ctx);
@@ -94,7 +103,20 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
float normal_x, float normal_y,
float normal_z, float offset);
// 3D to 2D
ManifoldCrossSection *manifold_slice(void *mem, ManifoldManifold *m,
float height);
ManifoldCrossSection *manifold_project(void *mem, ManifoldManifold *m);
// Convex Hulls
ManifoldManifold *manifold_hull(void *mem, ManifoldManifold *m);
ManifoldManifold *manifold_batch_hull(void *mem, ManifoldManifoldVec *ms);
ManifoldManifold *manifold_hull_pts(void *mem, ManifoldVec3 *ps, size_t length);
// Manifold Transformations
ManifoldManifold *manifold_translate(void *mem, ManifoldManifold *m, float x,
float y, float z);
ManifoldManifold *manifold_rotate(void *mem, ManifoldManifold *m, float x,
@@ -108,10 +130,19 @@ ManifoldManifold *manifold_transform(void *mem, ManifoldManifold *m, float x1,
ManifoldManifold *manifold_mirror(void *mem, ManifoldManifold *m, float nx,
float ny, float nz);
ManifoldManifold *manifold_warp(void *mem, ManifoldManifold *m,
ManifoldVec3 (*fun)(float, float, float));
ManifoldVec3 (*fun)(float, float, float,
void *),
void *ctx);
ManifoldManifold *manifold_smooth_by_normals(void *mem, ManifoldManifold *m,
int normalIdx);
ManifoldManifold *manifold_smooth_out(void *mem, ManifoldManifold *m,
float minSharpAngle, float minSmoothness);
ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine);
ManifoldManifold *manifold_refine_to_length(void *mem, ManifoldManifold *m,
float length);
// Manifold Shapes / Constructors
ManifoldManifold *manifold_empty(void *mem);
ManifoldManifold *manifold_copy(void *mem, ManifoldManifold *m);
ManifoldManifold *manifold_tetrahedron(void *mem);
@@ -137,6 +168,7 @@ ManifoldManifoldVec *manifold_decompose(void *mem, ManifoldManifold *m);
ManifoldManifold *manifold_as_original(void *mem, ManifoldManifold *m);
// Manifold Info
int manifold_is_empty(ManifoldManifold *m);
ManifoldError manifold_status(ManifoldManifold *m);
int manifold_num_vert(ManifoldManifold *m);
@@ -151,9 +183,16 @@ int manifold_original_id(ManifoldManifold *m);
uint32_t manifold_reserve_ids(uint32_t n);
ManifoldManifold *manifold_set_properties(
void *mem, ManifoldManifold *m, int num_prop,
void (*fun)(float *new_prop, ManifoldVec3 position, const float *old_prop));
void (*fun)(float *new_prop, ManifoldVec3 position, const float *old_prop,
void *ctx),
void *ctx);
ManifoldManifold *manifold_calculate_curvature(void *mem, ManifoldManifold *m,
int gaussian_idx, int mean_idx);
float manifold_min_gap(ManifoldManifold *m, ManifoldManifold *other,
float searchLength);
ManifoldManifold *manifold_calculate_normals(void *mem, ManifoldManifold *m,
int normal_idx,
int min_sharp_angle);
// CrossSection Shapes/Constructors
@@ -204,6 +243,17 @@ ManifoldCrossSection *manifold_cross_section_difference(
ManifoldCrossSection *manifold_cross_section_intersection(
void *mem, ManifoldCrossSection *a, ManifoldCrossSection *b);
// CrossSection Convex Hulls
ManifoldCrossSection *manifold_cross_section_hull(void *mem,
ManifoldCrossSection *cs);
ManifoldCrossSection *manifold_cross_section_batch_hull(
void *mem, ManifoldCrossSectionVec *css);
ManifoldCrossSection *manifold_cross_section_hull_simple_polygon(
void *mem, ManifoldSimplePolygon *ps);
ManifoldCrossSection *manifold_cross_section_hull_polygons(
void *mem, ManifoldPolygons *ps);
// CrossSection Transformation
ManifoldCrossSection *manifold_cross_section_translate(void *mem,
@@ -225,6 +275,9 @@ ManifoldCrossSection *manifold_cross_section_transform(void *mem,
float x3, float y3);
ManifoldCrossSection *manifold_cross_section_warp(
void *mem, ManifoldCrossSection *cs, ManifoldVec2 (*fun)(float, float));
ManifoldCrossSection *manifold_cross_section_warp_context(
void *mem, ManifoldCrossSection *cs,
ManifoldVec2 (*fun)(float, float, void *), void *ctx);
ManifoldCrossSection *manifold_cross_section_simplify(void *mem,
ManifoldCrossSection *cs,
double epsilon);
@@ -264,10 +317,9 @@ ManifoldRect *manifold_rect_mul(void *mem, ManifoldRect *r, float x, float y);
int manifold_rect_does_overlap_rect(ManifoldRect *a, ManifoldRect *r);
int manifold_rect_is_empty(ManifoldRect *r);
int manifold_rect_is_finite(ManifoldRect *r);
ManifoldCrossSection *manifold_rect_as_cross_section(void *mem,
ManifoldRect *r);
// Bounding Box
ManifoldBox *manifold_box(void *mem, float x1, float y1, float z1, float x2,
float y2, float z2);
ManifoldVec3 manifold_box_min(ManifoldBox *b);
@@ -292,11 +344,13 @@ int manifold_box_does_overlap_box(ManifoldBox *a, ManifoldBox *b);
int manifold_box_is_finite(ManifoldBox *b);
// Static Quality Globals
void manifold_set_min_circular_angle(float degrees);
void manifold_set_min_circular_edge_length(float length);
void manifold_set_circular_segments(int number);
// Manifold Mesh Extraction
int manifold_meshgl_num_prop(ManifoldMeshGL *m);
int manifold_meshgl_num_vert(ManifoldMeshGL *m);
int manifold_meshgl_num_tri(ManifoldMeshGL *m);
@@ -319,6 +373,7 @@ uint32_t *manifold_meshgl_face_id(void *mem, ManifoldMeshGL *m);
float *manifold_meshgl_halfedge_tangent(void *mem, ManifoldMeshGL *m);
// memory size
size_t manifold_manifold_size();
size_t manifold_manifold_vec_size();
size_t manifold_cross_section_size();
@@ -332,6 +387,7 @@ size_t manifold_rect_size();
size_t manifold_curvature_size();
// destruction
void manifold_destruct_manifold(ManifoldManifold *m);
void manifold_destruct_manifold_vec(ManifoldManifoldVec *ms);
void manifold_destruct_cross_section(ManifoldCrossSection *m);
@@ -343,6 +399,7 @@ void manifold_destruct_box(ManifoldBox *b);
void manifold_destruct_rect(ManifoldRect *b);
// pointer free + destruction
void manifold_delete_manifold(ManifoldManifold *m);
void manifold_delete_manifold_vec(ManifoldManifoldVec *ms);
void manifold_delete_cross_section(ManifoldCrossSection *cs);
@@ -354,6 +411,7 @@ void manifold_delete_box(ManifoldBox *b);
void manifold_delete_rect(ManifoldRect *b);
// MeshIO / Export
#ifdef MANIFOLD_EXPORT
ManifoldMaterial *manifold_material(void *mem);
void manifold_material_set_roughness(ManifoldMaterial *mat, float roughness);
+14
View File
@@ -1,3 +1,17 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <stddef.h>
+113 -42
View File
@@ -1,3 +1,17 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <conv.h>
#include <cross_section.h>
#include <manifold.h>
@@ -21,21 +35,10 @@
using namespace manifold;
namespace {
ManifoldMeshGL *level_set(void *mem, float (*sdf)(float, float, float),
ManifoldMeshGL *level_set(void *mem,
float (*sdf_context)(float, float, float, void *),
ManifoldBox *bounds, float edge_length, float level,
bool seq) {
// typing with std::function rather than auto compiles when CUDA is on,
// passing it into GPU (and crashing) is avoided dynamically in `sdf.h`
std::function<float(glm::vec3)> fun = [sdf](glm::vec3 v) {
return (sdf(v.x, v.y, v.z));
};
auto 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));
}
ManifoldMeshGL *level_set_context(
void *mem, float (*sdf_context)(float, float, float, void *),
ManifoldBox *bounds, float edge_length, float level, bool seq, void *ctx) {
bool seq, void *ctx) {
// Bind function with context argument to one without
using namespace std::placeholders;
std::function<float(float, float, float)> sdf =
@@ -43,8 +46,7 @@ ManifoldMeshGL *level_set_context(
std::function<float(glm::vec3)> fun = [sdf](glm::vec3 v) {
return (sdf(v.x, v.y, v.z));
};
auto pol = seq ? std::make_optional(ExecutionPolicy::Seq) : std::nullopt;
auto mesh = LevelSet(fun, *from_c(bounds), edge_length, level, pol);
auto mesh = LevelSet(fun, *from_c(bounds), edge_length, level, !seq);
return to_c(new (mem) MeshGL(mesh));
}
} // namespace
@@ -190,6 +192,37 @@ ManifoldManifold *manifold_trim_by_plane(void *mem, ManifoldManifold *m,
return to_c(new (mem) Manifold(trimmed));
}
ManifoldCrossSection *manifold_slice(void *mem, ManifoldManifold *m,
float height) {
auto poly = from_c(m)->Slice(height);
return to_c(new (mem) CrossSection(poly));
}
ManifoldCrossSection *manifold_project(void *mem, ManifoldManifold *m) {
auto poly = from_c(m)->Project();
return to_c(new (mem) CrossSection(poly));
}
ManifoldManifold *manifold_hull(void *mem, ManifoldManifold *m) {
auto hulled = from_c(m)->Hull();
return to_c(new (mem) Manifold(hulled));
}
ManifoldManifold *manifold_batch_hull(void *mem, ManifoldManifoldVec *ms) {
auto hulled = Manifold::Hull(*from_c(ms));
return to_c(new (mem) Manifold(hulled));
}
ManifoldManifold *manifold_hull_pts(void *mem, ManifoldVec3 *ps,
size_t length) {
std::vector<glm::vec3> vec(length);
for (int i = 0; i < length; ++i) {
vec[i] = {ps[i].x, ps[i].y, ps[i].z};
}
auto hulled = Manifold::Hull(vec);
return to_c(new (mem) Manifold(hulled));
}
ManifoldManifold *manifold_translate(void *mem, ManifoldManifold *m, float x,
float y, float z) {
auto v = glm::vec3(x, y, z);
@@ -226,37 +259,44 @@ ManifoldManifold *manifold_mirror(void *mem, ManifoldManifold *m, float nx,
}
ManifoldManifold *manifold_warp(void *mem, ManifoldManifold *m,
ManifoldVec3 (*fun)(float, float, float)) {
std::function<void(glm::vec3 & v)> warp = [fun](glm::vec3 &v) {
v = from_c(fun(v.x, v.y, v.z));
ManifoldVec3 (*fun)(float, float, float,
void *),
void *ctx) {
// Bind function with context argument to one without
using namespace std::placeholders;
std::function<ManifoldVec3(float, float, float)> f3 =
std::bind(fun, _1, _2, _3, ctx);
std::function<void(glm::vec3 & v)> warp = [f3](glm::vec3 &v) {
v = from_c(f3(v.x, v.y, v.z));
};
auto warped = from_c(m)->Warp(warp);
return to_c(new (mem) Manifold(warped));
}
ManifoldMeshGL *manifold_level_set(void *mem, float (*sdf)(float, float, float),
ManifoldMeshGL *manifold_level_set(void *mem,
float (*sdf)(float, float, float, void *),
ManifoldBox *bounds, float edge_length,
float level) {
return level_set(mem, sdf, bounds, edge_length, level, false);
float level, void *ctx) {
return level_set(mem, sdf, bounds, edge_length, level, false, ctx);
}
ManifoldMeshGL *manifold_level_set_seq(void *mem,
float (*sdf)(float, float, float),
ManifoldBox *bounds, float edge_length,
float level) {
return level_set(mem, sdf, bounds, edge_length, level, true);
}
ManifoldMeshGL *manifold_level_set_context(
ManifoldMeshGL *manifold_level_set_seq(
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
float edge_length, float level, void *ctx) {
return level_set_context(mem, sdf, bounds, edge_length, level, false, ctx);
return level_set(mem, sdf, bounds, edge_length, level, true, ctx);
}
ManifoldMeshGL *manifold_level_set_seq_context(
void *mem, float (*sdf)(float, float, float, void *), ManifoldBox *bounds,
float edge_length, float level, void *ctx) {
return level_set_context(mem, sdf, bounds, edge_length, level, true, ctx);
ManifoldManifold *manifold_smooth_by_normals(void *mem, ManifoldManifold *m,
int normalIdx) {
auto smoothed = from_c(m)->SmoothByNormals(normalIdx);
return to_c(new (mem) Manifold(smoothed));
}
ManifoldManifold *manifold_smooth_out(void *mem, ManifoldManifold *m,
float minSharpAngle,
float minSmoothness) {
auto smoothed = from_c(m)->SmoothOut(minSharpAngle, minSmoothness);
return to_c(new (mem) Manifold(smoothed));
}
ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine) {
@@ -264,6 +304,12 @@ ManifoldManifold *manifold_refine(void *mem, ManifoldManifold *m, int refine) {
return to_c(new (mem) Manifold(refined));
}
ManifoldManifold *manifold_refine_to_length(void *mem, ManifoldManifold *m,
float length) {
auto refined = from_c(m)->RefineToLength(length);
return to_c(new (mem) Manifold(refined));
}
ManifoldManifold *manifold_empty(void *mem) {
return to_c(new (mem) Manifold());
}
@@ -371,6 +417,15 @@ ManifoldMeshGL *manifold_meshgl_copy(void *mem, ManifoldMeshGL *m) {
return to_c(new (mem) MeshGL(*from_c(m)));
}
ManifoldMeshGL *manifold_meshgl_merge(void *mem, ManifoldMeshGL *m) {
auto duplicate = new (mem) MeshGL(*from_c(m));
if (duplicate->Merge()) {
return to_c(duplicate);
}
delete duplicate;
return m;
}
int manifold_meshgl_num_prop(ManifoldMeshGL *m) { return from_c(m)->numProp; }
int manifold_meshgl_num_vert(ManifoldMeshGL *m) { return from_c(m)->NumVert(); }
int manifold_meshgl_num_tri(ManifoldMeshGL *m) { return from_c(m)->NumTri(); }
@@ -477,14 +532,18 @@ float manifold_precision(ManifoldManifold *m) { return from_c(m)->Precision(); }
uint32_t manifold_reserve_ids(uint32_t n) { return Manifold::ReserveIDs(n); }
ManifoldManifold *manifold_set_properties(void *mem, ManifoldManifold *m,
int num_prop,
void (*fun)(float *new_prop,
ManifoldVec3 position,
const float *old_prop)) {
ManifoldManifold *manifold_set_properties(
void *mem, ManifoldManifold *m, int num_prop,
void (*fun)(float *new_prop, ManifoldVec3 position, const float *old_prop,
void *ctx),
void *ctx) {
// Bind function with context argument to one without
using namespace std::placeholders;
std::function<void(float *, ManifoldVec3, const float *)> f3 =
std::bind(fun, _1, _2, _3, ctx);
std::function<void(float *, glm::vec3, const float *)> f =
[fun](float *new_prop, glm::vec3 v, const float *old_prop) {
fun(new_prop, to_c(v), old_prop);
[f3](float *new_prop, glm::vec3 v, const float *old_prop) {
return (f3(new_prop, to_c(v), old_prop));
};
auto man = from_c(m)->SetProperties(num_prop, f);
return to_c(new (mem) Manifold(man));
@@ -496,6 +555,18 @@ ManifoldManifold *manifold_calculate_curvature(void *mem, ManifoldManifold *m,
return to_c(new (mem) Manifold(man));
}
float manifold_min_gap(ManifoldManifold *m, ManifoldManifold *other,
float searchLength) {
return from_c(m)->MinGap(*from_c(other), searchLength);
}
ManifoldManifold *manifold_calculate_normals(void *mem, ManifoldManifold *m,
int normal_idx,
int min_sharp_angle) {
auto man = from_c(m)->CalculateNormals(normal_idx, min_sharp_angle);
return to_c(new (mem) Manifold(man));
}
// Static Quality Globals
void manifold_set_min_circular_angle(float degrees) {
+16 -3
View File
@@ -1,8 +1,21 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "meshIO.h"
#include <conv.h>
#include "include/types.h"
#include "conv.h"
#include "types.h"
// C <-> C++ conversions
+16 -9
View File
@@ -1,8 +1,21 @@
#include <conv.h>
#include <manifold.h>
#include <public.h>
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "conv.h"
#include "cross_section.h"
#include "manifold.h"
#include "public.h"
#include "types.h"
ManifoldRect *manifold_rect(void *mem, float x1, float y1, float x2, float y2) {
@@ -85,9 +98,3 @@ int manifold_rect_does_overlap_rect(ManifoldRect *a, ManifoldRect *r) {
int manifold_rect_is_empty(ManifoldRect *r) { return from_c(r)->IsEmpty(); }
int manifold_rect_is_finite(ManifoldRect *r) { return from_c(r)->IsFinite(); }
ManifoldCrossSection *manifold_rect_as_cross_section(void *mem,
ManifoldRect *r) {
auto cs = from_c(r)->AsCrossSection();
return to_c(new (mem) CrossSection(cs));
}
+39 -5
View File
@@ -15,10 +15,44 @@
project(python)
add_subdirectory(third_party)
pybind11_add_module(manifold3d manifold3d.cpp)
target_link_libraries(manifold3d PRIVATE manifold)
target_compile_options(manifold3d PRIVATE ${MANIFOLD_FLAGS})
nanobind_add_module(
manifold3d
NB_STATIC STABLE_ABI LTO
autogen_docstrings.inl
manifold3d.cpp)
target_link_libraries(manifold3d PRIVATE manifold sdf polygon)
target_compile_options(manifold3d PRIVATE ${MANIFOLD_FLAGS} -DMODULE_NAME=manifold3d)
target_compile_features(manifold3d PUBLIC cxx_std_17)
target_include_directories(manifold3d
PRIVATE ${PYBIND11_DIR}/include
set_target_properties(manifold3d PROPERTIES OUTPUT_NAME "manifold3d")
message(Python_EXECUTABLE = ${Python_EXECUTABLE})
# ideally we should generate a dependency file from python...
set(DOCSTRING_DEPS
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/manifold.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/constructors.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/manifold/src/sort.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/cross_section/src/cross_section.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/polygon/src/polygon.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/utilities/include/public.h
)
add_custom_command(
OUTPUT autogen_docstrings.inl
DEPENDS ${DOCSTRING_DEPS}
COMMAND ${Python_EXECUTABLE} ARGS ${CMAKE_CURRENT_SOURCE_DIR}/gen_docs.py
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
target_include_directories(manifold3d PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
if(SKBUILD)
install(
TARGETS manifold3d
LIBRARY DESTINATION ${SKBUILD_PLATLIB_DIR}
COMPONENT bindings
)
else()
install(
TARGETS manifold3d
LIBRARY DESTINATION ${Python_SITEARCH}
COMPONENT bindings
)
endif()
+40
View File
@@ -0,0 +1,40 @@
# Python Bindings
## Autogenerated Doc-Strings
Doc-strings for the python API wrapper are generated by gen_docs.py
The script is run automatically during the build to keep python
doc strings fully up-to-date with c++ sources.
It scrapes documentation comments and c++ function signatures
from the manifold c++ sources, in order to generate a c++ header file
exposing the comments as string variables named by function names.
This allows python bindings to re-use the c++ comments directly.
Some snake-casing of params is applied for python use case.
---
When modifying the Manifold C++ sources, you may need to update
gen_docs.py. For example, top-level free functions are white-listed,
so if you add a new one, you will need to add it in gen_docs.py.
Similarly, the list of source files to parse is also white listed,
so if you define functions in new files that need python wrappers,
you will also need to up gen_docs.py.
To verify that python docs are correct after changes, you can
run the following commends from the manifold repo root:
```
pip install .
python -c 'import manifold3d; help(manifold3d)'
```
Alternateively you could generate stubs with roughly the same info
```
pip install nanobind-stubgen
pip install .
nanobind-stubgen manifold3d
```
It will emit some warnings and write a file `manifold3d.pyi`
which will show all the function signatures and docstrings.
+128
View File
@@ -0,0 +1,128 @@
from manifold3d import *
import numpy as np
def all_root_level():
set_min_circular_angle(10)
set_min_circular_edge_length(1)
set_circular_segments(22)
n = get_circular_segments(1)
assert n == 22
poly = [[0, 0], [1, 0], [1, 1]]
tris = triangulate([poly])
tris = triangulate([np.array(poly)])
def all_cross_section():
poly = [[0, 0], [1, 0], [1, 1]]
c = CrossSection([np.array(poly)])
c = CrossSection([poly])
c = CrossSection() + c
a = c.area()
c = CrossSection.batch_boolean(
[
CrossSection.circle(1),
CrossSection.square((3, 3)),
CrossSection.circle(3).translate((1, 1)),
],
OpType.Add,
)
c = CrossSection.batch_hull([c, c.translate((1, 0))])
b = c.bounds()
c = CrossSection.circle(1)
c = CrossSection.compose([c, c.translate((1, 0))])
cs = c.decompose()
m = c.extrude(1)
c = c.hull()
c = CrossSection.hull_points(poly)
c = CrossSection.hull_points(np.array(poly))
e = c.is_empty()
c = c.mirror((0, 1))
n = c.num_contour()
n = c.num_vert()
c = c.offset(1, JoinType.Round)
m = c.revolve()
c = c.rotate(90)
c = c.scale((2, 2))
c = c.simplify()
c = CrossSection.square((1, 1))
p = c.to_polygons()
c = c.transform([[1, 0, 0], [0, 1, 0]])
c = c.translate((1, 1))
c = c.warp(lambda p: (p[0] + 1, p[1] / 2))
c = c.warp_batch(lambda ps: ps * [1, 0.5] + [1, 0])
def all_manifold():
mesh = Manifold.sphere(1).to_mesh()
m = Manifold(mesh)
m = Manifold() + m
m = m.as_original()
m = Manifold.batch_boolean(
[
Manifold.cylinder(4, 1),
Manifold.cube((3, 2, 1)),
Manifold.cylinder(5, 3).translate((1, 1, 1)),
],
OpType.Add,
)
m = Manifold.batch_hull([m, m.translate((0, 0, 1))])
b = m.bounding_box()
m = m.calculate_curvature(4, 5)
m = m.calculate_normals(0)
m = m.smooth_by_normals(0)
m = Manifold.compose([m, m.translate((5, 0, 0))])
m = Manifold.cube((1, 1, 1))
m = Manifold.cylinder(1, 1)
ms = m.decompose()
m = Manifold.extrude(CrossSection.circle(1), 1)
m = Manifold.revolve(CrossSection.circle(1))
g = m.genus()
a = m.surface_area()
v = m.volume()
m = m.hull()
m = m.hull_points(mesh.vert_properties)
e = m.is_empty()
m = m.mirror((0, 0, 1))
n = m.num_edge()
n = m.num_prop()
n = m.num_prop_vert()
n = m.num_tri()
n = m.num_vert()
i = m.original_id()
p = m.precision()
c = m.project()
m = m.refine(2)
m = m.refine_to_length(0.1)
m = m.smooth_out()
i = Manifold.reserve_ids(1)
m = m.scale((1, 2, 3))
m = m.set_properties(3, lambda pos, prop: pos)
c = m.slice(0.5)
m = Manifold.smooth(mesh, [0], [0.5])
m = Manifold.sphere(1)
m, n = m.split(m.translate((1, 0, 0)))
m, n = m.split_by_plane((0, 0, 1), 0)
e = m.status()
m = Manifold.tetrahedron()
mesh = m.to_mesh()
ok = mesh.merge()
m = m.transform([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]])
m = m.translate((0, 0, 0))
m = m.trim_by_plane((0, 0, 1), 0)
m = m.warp(lambda p: (p[0] + 1, p[1] / 2, p[2] * 2))
m = m.warp_batch(lambda ps: ps * [1, 0.5, 2] + [1, 0, 0])
m = Manifold.cube()
m2 = Manifold.cube().translate([2, 0, 0])
d = m.min_gap(m2, 2)
def run():
all_root_level()
all_cross_section()
all_manifold()
return Manifold()
if __name__ == "__main__":
run()
+52 -30
View File
@@ -20,60 +20,82 @@ from manifold3d import Manifold
INCHES = 25.4
brick_depth = (3 + 5/8) * INCHES
brick_height = (2 + 1/4) * INCHES
brick_length = (7 + 5/8) * INCHES
brick_depth = (3 + 5 / 8) * INCHES
brick_height = (2 + 1 / 4) * INCHES
brick_length = (7 + 5 / 8) * INCHES
mortar_gap = (3/8) * INCHES
mortar_gap = (3 / 8) * INCHES
def brick():
return Manifold.cube(brick_length, brick_depth, brick_height)
return Manifold.cube([brick_length, brick_depth, brick_height])
def halfbrick():
return Manifold.cube((brick_length - mortar_gap)/2, brick_depth, brick_height)
return Manifold.cube([(brick_length - mortar_gap) / 2, brick_depth, brick_height])
def row(length):
bricks = [brick().translate((brick_length + mortar_gap)*x, 0, 0)
for x in range(length)]
return Manifold(bricks)
bricks = [
brick().translate([(brick_length + mortar_gap) * x, 0, 0])
for x in range(length)
]
return sum(bricks, Manifold())
def wall(length, height, alternate=0):
bricks = [row(length).translate(
((z+alternate) % 2)*(brick_depth+mortar_gap), 0,
(brick_height+mortar_gap)*z) for z in range(height)]
return Manifold(bricks)
bricks = [
row(length).translate(
[
((z + alternate) % 2) * (brick_depth + mortar_gap),
0,
(brick_height + mortar_gap) * z,
]
)
for z in range(height)
]
return sum(bricks, Manifold())
def walls(length, width, height):
return Manifold([
wall(length, height),
wall(width, height, 1).rotate(0, 0, 90).translate(brick_depth, 0, 0),
wall(length, height, 1).translate(
0, (width)*(brick_length+mortar_gap), 0),
wall(width, height).rotate(0, 0, 90)
.translate((length+0.5)*(brick_length+mortar_gap) - mortar_gap, 0, 0),
])
return sum(
[
wall(length, height),
wall(width, height, 1).rotate([0, 0, 90]).translate([brick_depth, 0, 0]),
wall(length, height, 1).translate(
[0, (width) * (brick_length + mortar_gap), 0]
),
wall(width, height)
.rotate([0, 0, 90])
.translate(
[(length + 0.5) * (brick_length + mortar_gap) - mortar_gap, 0, 0]
),
],
Manifold(),
)
def floor(length, width):
results = [walls(length, width, 1)]
if length > 1 and width > 1:
results.append(floor(length-1, width-1).translate(brick_depth +
mortar_gap, brick_depth+mortar_gap, 0))
results.append(
floor(length - 1, width - 1).translate(
[brick_depth + mortar_gap, brick_depth + mortar_gap, 0]
)
)
if length == 1 and width > 1:
results.append(row(width-1).rotate(0, 0, 90))
results.append(row(width - 1).rotate(0, 0, 90))
if width == 1 and length > 1:
results.append(row(length-1).translate(2*(brick_depth +
mortar_gap), brick_depth+mortar_gap, 0))
results.append(halfbrick().translate(
brick_depth+mortar_gap, brick_depth+mortar_gap, 0))
return Manifold(results)
results.append(
row(length - 1).translate(
[2 * (brick_depth + mortar_gap), brick_depth + mortar_gap, 0]
)
)
results.append(
halfbrick().translate([brick_depth + mortar_gap, brick_depth + mortar_gap, 0])
)
return sum(results, Manifold())
def run(width=10, length=10, height=10):
return walls(length, width, height) + floor(length, width)
+10 -7
View File
@@ -13,18 +13,21 @@
See the License for the specific language governing permissions and
limitations under the License.
"""
from manifold3d import Manifold
from functools import reduce
# https://gist.github.com/ochafik/2db96400e3c1f73558fcede990b8a355#file-cube-with-half-spheres-dents-scad
def run(n=5, overlap=True):
a = Manifold.cube(n, n, 0.5).translate(-0.5, -0.5, -0.5)
a = Manifold.cube([n, n, 0.5]).translate([-0.5, -0.5, -0.5])
spheres = [Manifold.sphere(0.45 if overlap else 0.55, 50).translate(i, j, 0)
for i in range(n) for j in range(n)]
spheres = reduce(lambda a, b: a + b, spheres)
return a - spheres
spheres = [
Manifold.sphere(0.45 if overlap else 0.55, 50).translate([i, j, 0])
for i in range(n)
for j in range(n)
]
# spheres = reduce(lambda a, b: a + b, spheres)
return a - sum(spheres, Manifold())
+19 -11
View File
@@ -1,4 +1,4 @@
from manifold3d import CrossSection, FillRule
from manifold3d import CrossSection, Manifold
def run():
@@ -10,29 +10,37 @@ def run():
cross_section = CrossSection(polygons_points)
polygons = cross_section.to_polygons()
polygon = polygons[0]
if set(polygon) != set(polygon_points):
raise Exception(f"{polygon=} differs from {polygon_points=}")
if set([tuple(p) for p in polygon]) != set(polygon_points):
raise Exception(
f"polygon={polygon} differs from polygon_points={polygon_points}"
)
# extrude a polygon to create a manifold
extruded_polygon = cross_section.extrude(10.0)
extruded_polygon = Manifold.extrude(cross_section, 10.0)
eps = 0.001
observed_volume = extruded_polygon.get_volume()
observed_volume = extruded_polygon.volume()
expected_volume = 10.0
if abs(observed_volume - expected_volume) > eps:
raise Exception(f"{observed_volume=} differs from {expected_volume=}")
observed_surface_area = extruded_polygon.get_surface_area()
raise Exception(
f"observed_volume={observed_volume} differs from expected_volume={expected_volume}"
)
observed_surface_area = extruded_polygon.surface_area()
expected_surface_area = 42.0
if abs(observed_surface_area - expected_surface_area) > eps:
raise Exception(f"{observed_surface_area=} differs from {expected_surface_area=}")
raise Exception(
f"observed_surface_area={observed_surface_area} differs from expected_surface_area={expected_surface_area}"
)
# get bounding box from manifold
observed_bbox = extruded_polygon.bounding_box
observed_bbox = extruded_polygon.bounding_box()
expected_bbox = (0.0, 0.0, 0.0, 1.0, 1.0, 10.0)
if observed_bbox != expected_bbox:
raise Exception(f"{observed_bbox=} differs from {expected_bbox=}")
raise Exception(
f"observed_bbox={observed_bbox} differs from expected_bbox={expected_bbox}"
)
return extruded_polygon
if __name__ == "__main__":
run()
run()
+59
View File
@@ -0,0 +1,59 @@
"""
Copyright 2023 The Manifold Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import math
import numpy as np
from manifold3d import Mesh, Manifold
def gyroid(x, y, z):
xi = x - math.pi / 4.0
yi = y - math.pi / 4.0
zi = z - math.pi / 4.0
return (
math.cos(xi) * math.sin(yi)
+ math.cos(yi) * math.sin(zi)
+ math.cos(zi) * math.sin(xi)
)
def gyroid_levelset(level, period, size, n):
return Manifold(
Mesh.level_set(
gyroid,
[-period, -period, -period, period, period, period],
period / n,
level,
)
).scale([size / period] * 3)
def rhombic_dodecahedron(size):
box = Manifold.cube(size * math.sqrt(2.0) * np.array([1, 1, 2]), True)
result = box.rotate([90, 45, 0]) ^ box.rotate([90, 45, 90])
return result ^ box.rotate([0, 0, 45])
def gyroid_module(size=20, n=15):
period = math.pi * 2.0
result = (
gyroid_levelset(-0.4, period, size, n) ^ rhombic_dodecahedron(size)
) - gyroid_levelset(0.4, period, size, n)
return result.rotate([-45, 0, 90]).translate([0, 0, size / math.sqrt(2.0)])
def run(size=20, n=15):
return gyroid_module(size, n)
File diff suppressed because it is too large Load Diff
+14 -7
View File
@@ -18,15 +18,15 @@ import pathlib
import sys
import importlib
import trimesh
import numpy as np
from time import time
if __name__ == "__main__":
current_file = pathlib.Path(__file__)
current_dir = current_file.parent
files = [f.parts[-1][:-3]
for f in current_dir.glob('*.py') if f != current_file]
files = [f.parts[-1][:-3] for f in current_dir.glob("*.py") if f != current_file]
export_models = len(sys.argv) == 2 and sys.argv[-1] == '-e'
export_models = len(sys.argv) == 2 and sys.argv[-1] == "-e"
for f in files:
module = importlib.import_module(f)
@@ -34,9 +34,16 @@ if __name__ == "__main__":
model = module.run()
mesh = model.to_mesh()
if export_models:
if mesh.vert_properties.shape[1] > 3:
vertices = mesh.vert_properties[:, :3]
colors = (mesh.vert_properties[:, 3:] * 255).astype(np.uint8)
else:
vertices = mesh.vert_properties
colors = None
meshOut = trimesh.Trimesh(
vertices=mesh.vert_pos, faces=mesh.tri_verts)
trimesh.exchange.export.export_mesh(meshOut, f'{f}.glb', 'glb')
print(f'Exported model to {f}.glb')
vertices=vertices, faces=mesh.tri_verts, vertex_colors=colors
)
trimesh.exchange.export.export_mesh(meshOut, f"{f}.glb", "glb")
print(f"Exported model to {f}.glb")
t1 = time()
print(f'Took {(t1-t0)*1000:.1f}ms for {f}')
print(f"Took {(t1-t0)*1000:.1f}ms for {f}")
+81
View File
@@ -0,0 +1,81 @@
"""
Copyright 2023 The Manifold Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from manifold3d import Manifold, Mesh
import numpy as np
def run():
# A smoothed manifold demonstrating selective edge sharpening with
# smooth() and refine(), see more details at:
# https://elalish.blogspot.com/2022/03/smoothing-triangle-meshes.html
height = 10
radius = 30
offset = 20
wiggles = 12
sharpness = 0.8
n = 50
triangles = []
positions = [[-offset, 0, height], [-offset, 0, -height]]
sharpenedEdges = []
delta = np.pi / wiggles
for i in range(2 * wiggles):
theta = (i - wiggles) * delta
amp = 0.5 * height * max(np.cos(0.8 * theta), 0)
positions.append(
[
radius * np.cos(theta),
radius * np.sin(theta),
amp * (1 if i % 2 == 0 else -1),
]
)
j = i + 1
if j == 2 * wiggles:
j = 0
smoothness = 1 - sharpness * np.cos((theta + delta / 2) / 2)
halfedge = 3 * len(triangles) + 1
sharpenedEdges.append((halfedge, smoothness))
triangles.append([0, 2 + i, 2 + j])
halfedge = 3 * len(triangles) + 1
sharpenedEdges.append((halfedge, smoothness))
triangles.append([1, 2 + j, 2 + i])
scallop = Mesh(
tri_verts=np.array(triangles, np.int32),
vert_properties=np.array(positions, np.float32),
)
def colorCurvature(_pos, oldProp):
a = max(0, min(1, oldProp[0] / 3 + 0.5))
b = a * a * (3 - 2 * a)
red = [1, 0, 0]
blue = [0, 0, 1]
return [(1 - b) * blue[i] + b * red[i] for i in range(3)]
edges, smoothing = zip(*sharpenedEdges)
return (
Manifold.smooth(scallop, edges, smoothing)
.refine(n)
.calculate_curvature(-1, 0)
.set_properties(3, colorCurvature)
)
+23
View File
@@ -0,0 +1,23 @@
"""
Copyright 2022 The Manifold Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from manifold3d import Manifold
def run():
a = Manifold.cube([1.0, 1.0, 1.0])
b = Manifold.cube([1.0, 1.0, 1.0]).rotate([45.0, 45.0, 45.0])
return a.split(b)[0]
+43
View File
@@ -0,0 +1,43 @@
from manifold3d import Manifold
import numpy as np
def fractal(holes, hole, w, position, depth, maxDepth):
w /= 3
holes.append(hole.scale([w, w, 1.0]).translate([position[0], position[1], 0.0]))
if depth == maxDepth:
return
offsets = np.array(
[
[-w, -w],
[-w, 0.0],
[-w, w],
[0.0, w],
[w, w],
[w, 0.0],
[w, -w],
[0.0, -w],
]
)
for offset in offsets:
fractal(holes, hole, w, position + offset, depth + 1, maxDepth)
def posColors(pos, _):
return [-p + 0.5 for p in pos] + [1.0]
def run(n=1):
result = Manifold.cube([1, 1, 1], True)
holes = []
fractal(holes, result, 1.0, np.array([0.0, 0.0]), 1, n)
hole = Manifold.compose(holes)
result -= hole
result -= hole.rotate([90, 0, 0])
result -= hole.rotate([0, 90, 0])
return (
result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale([100] * 3)
)
@@ -0,0 +1,98 @@
from manifold3d import *
import numpy as np
import pytest
# Creates a classic torus knot, defined as a string wrapping periodically
# around the surface of an imaginary donut. If p and q have a common
# factor then you will get multiple separate, interwoven knots. This is
# an example of using the warp() method, thus avoiding any direct
# handling of triangles.
def run(warp_single=False):
# The number of times the thread passes through the donut hole.
p = 1
# The number of times the thread circles the donut.
q = 3
# Radius of the interior of the imaginary donut.
majorRadius = 25
# Radius of the small cross-section of the imaginary donut.
minorRadius = 10
# Radius of the small cross-section of the actual object.
threadRadius = 3.75
# Number of linear segments making up the threadRadius circle. Default is
# getCircularSegments(threadRadius).
circularSegments = -1
# Number of segments along the length of the knot. Default makes roughly
# square facets.
linearSegments = -1
# These default values recreate Matlab Knot by Emmett Lalish:
# https://www.thingiverse.com/thing:7080
kLoops = np.gcd(p, q)
pk = p / kLoops
qk = q / kLoops
n = (
circularSegments
if circularSegments > 2
else get_circular_segments(threadRadius)
)
m = linearSegments if linearSegments > 2 else n * qk * majorRadius / threadRadius
offset = 2
circle = CrossSection.circle(1, n).translate([offset, 0])
def ax_rotate(x, theta):
a, b = (x + 1) % 3, (x + 2) % 3
s, c = np.sin(theta), np.cos(theta)
m = np.zeros((len(theta), 4, 4), dtype=np.float32)
m[:, a, a], m[:, a, b] = c, s
m[:, b, a], m[:, b, b] = -s, c
m[:, x, x], m[:, 3, 3] = 1, 1
return m
def func(pts):
npts = pts.shape[0]
x, y, z = pts[:, 0], pts[:, 1], pts[:, 2]
psi = qk * np.arctan2(x, y)
theta = psi * pk / qk
x1 = np.sqrt(x * x + y * y)
phi = np.arctan2(x1 - offset, z)
v = np.zeros((npts, 4), dtype=np.float32)
v[:, 0] = threadRadius * np.cos(phi)
v[:, 2] = threadRadius * np.sin(phi)
v[:, 3] = 1
r = majorRadius + minorRadius * np.cos(theta)
m1 = ax_rotate(0, -np.arctan2(pk * minorRadius, qk * r))
m1[:, 3, 0] = minorRadius
m2 = ax_rotate(1, theta)
m2[:, 3, 0] = majorRadius
m3 = ax_rotate(2, psi)
v = v[:, None, :] @ m1 @ m2 @ m3
pts[:] = v[:, 0, :3]
def func_single(v):
pts = np.array([v])
func(pts)
return pts[0]
if warp_single:
return Manifold.revolve(circle, int(m)).warp(func_single)
else:
return Manifold.revolve(circle, int(m)).warp_batch(func)
@pytest.mark.parametrize("warp_single", [True, False])
def test_warp(warp_single):
m = run(warp_single=warp_single)
assert m.volume() == pytest.approx(20785.76)
assert m.surface_area() == pytest.approx(11176.8)
assert m.genus() == 1
if __name__ == "__main__":
run()
+2 -1
View File
@@ -1,7 +1,8 @@
from manifold3d import Manifold
def run():
# for some reason this causes collider error
obj = Manifold.cube()
obj += Manifold([Manifold.cube()]).rotate(0, 0, 45+180)
obj += Manifold.cube().rotate([0, 0, 45 + 180])
return obj
+109
View File
@@ -0,0 +1,109 @@
from os.path import dirname
import re
base = dirname(dirname(dirname(__file__)))
def snake_case(name):
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
def python_param_modifier(comment):
# p = f":{snake_case(m[0][1:])}:"
comment = re.sub(r"@(param \w+)", lambda m: f':{snake_case(m[1])}:', comment)
# python API renames `MeshGL` to `Mesh`
comment = re.sub('mesh_gl', 'mesh', comment)
comment = re.sub('MeshGL', 'Mesh', comment)
return comment
def method_key(name):
name = re.sub(r"\+", "_plus", name)
name = re.sub(r"\-", "_minus", name)
name = re.sub(r"\^", "_xor", name)
name = re.sub(r"\=", "_eq", name)
name = re.sub(r"\:", "_", name)
name = re.sub(r"\~", "destroy_", name)
return name
parens_re = re.compile(r"[^(]+\(([^(]*(\(.*\))*[^(]*\))", flags=re.DOTALL)
args_re = re.compile(
r"^[^,^\(^\)]*(\(.*\))*[^,^\(^\)]*[\s\&\*]([0-9\w]+)\s*[,\)]", flags=re.DOTALL
)
def parse_args(s):
par = parens_re.match(s)
if not par:
return None
out = []
arg_str = par[1]
while m := re.search(args_re, arg_str):
out += [snake_case(m[2])]
arg_str = arg_str[m.span()[1] :]
return out
def collect(fname, matcher, param_modifier=python_param_modifier):
comment = ""
with open(fname) as f:
for line in f:
line = line.lstrip()
if line.startswith("/**"):
comment = ""
elif line.startswith("*/"):
pass
elif line.startswith("*") and comment is not None:
comment += line[1:].lstrip()
elif comment and (m := matcher(line)):
while (args := parse_args(line)) is None:
line += next(f)
if len(line) > 500:
break
method = method_key(snake_case(m[1]))
# comment = re.sub(param_re, param_modifier, comment)
comment = param_modifier(comment)
method = "__".join([method, *args])
assert method not in comments
comments[method] = comment
comment = ""
comments = {}
method_re = re.compile(r"(\w+::[\w\-\+\^\=\:]+)\(")
function_re = re.compile(r"([\w\-\+\^\=\:]+)\(")
# we don't handle inline functions in classes properly
# so instead just white-list functions we want
def select_functions(s):
m = function_re.search(s)
if m and "Triangulate" in m[0]:
return m
if m and "Circular" in m[0]:
return m
return None
collect(f"{base}/src/manifold/src/manifold.cpp", lambda s: method_re.search(s))
collect(f"{base}/src/manifold/src/constructors.cpp", lambda s: method_re.search(s))
collect(f"{base}/src/manifold/src/sort.cpp", lambda s: method_re.search(s))
collect(
f"{base}/src/cross_section/src/cross_section.cpp", lambda s: method_re.search(s)
)
collect(f"{base}/src/polygon/src/polygon.cpp", select_functions)
collect(f"{base}/src/utilities/include/public.h", select_functions)
comments = dict(sorted(comments.items()))
gen_h = "autogen_docstrings.inl"
with open(gen_h, "w") as f:
f.write("#pragma once\n\n")
f.write("// --- AUTO GENERATED ---\n")
f.write("// gen_docs.py is run by cmake build\n\n")
f.write("namespace manifold_docstrings {\n")
for key, doc in comments.items():
f.write(f'const char* {key} = R"___({doc.strip()})___";\n')
f.write("} // namespace manifold_docs")
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
add_subdirectory(pybind11)
add_subdirectory(nanobind)
+2
View File
@@ -0,0 +1,2 @@
manifold.js
manifold.wasm
+9 -1
View File
@@ -32,13 +32,21 @@ add_custom_target(js_deps ALL
DEPENDS manifoldjs
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts)
# copy WASM build back here for publishing to npm
add_custom_command(
TARGET js_deps POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_BINARY_DIR}/manifold.*
${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts
${CMAKE_CURRENT_SOURCE_DIR})
# copy WASM build and TS declarations for Vite to package into ManifoldCAD.org
add_custom_command(
TARGET js_deps POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/manifold*
${CMAKE_CURRENT_SOURCE_DIR}/examples/built/)
# copy TS declarations to public so they can be accessed by our editor
add_custom_command(
TARGET js_deps POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
+1 -1
View File
@@ -24,7 +24,7 @@ Since Manifold is a WASM module, it does not automatically garbage-collect like
## Examples
Please see our usage [examples](https://github.com/elalish/manifold/tree/master/bindings/wasm/examples) to see how to interface this library with `three.js`, `<model-viewer>`, and `glTF`. Of particular note are the included libraries for lossless roundtrip of manifold meshes through glTF files, via a new extension: [EXT_manifold](https://github.com/KhronosGroup/glTF/pull/2286).
Please see our usage [examples](https://github.com/elalish/manifold/tree/master/bindings/wasm/examples) to see how to interface this library with `three.js`, `<model-viewer>`, and `glTF`. Of particular note are the included libraries for lossless roundtrip of manifold meshes through glTF files, via a new extension: [EXT_mesh_manifold](https://github.com/KhronosGroup/glTF/pull/2286).
## About the author
+20 -3
View File
@@ -14,14 +14,14 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <manifold.h>
#include <polygon.h>
#include <sdf.h>
#include <vector>
#include "cross_section.h"
#include "helpers.cpp"
#include "manifold.h"
#include "polygon.h"
#include "sdf.h"
using namespace emscripten;
using namespace manifold;
@@ -125,6 +125,10 @@ EMSCRIPTEN_BINDINGS(whatever) {
function("_crossSectionUnionN", &cross_js::UnionN);
function("_crossSectionDifferenceN", &cross_js::DifferenceN);
function("_crossSectionIntersectionN", &cross_js::IntersectionN);
function("_crossSectionCollectVertices", &cross_js::CollectVertices);
function("_crossSectionHullPoints",
select_overload<CrossSection(std::vector<glm::vec2>)>(
&CrossSection::Hull));
class_<Manifold>("Manifold")
.constructor(&man_js::FromMeshJS)
@@ -134,8 +138,14 @@ EMSCRIPTEN_BINDINGS(whatever) {
.function("_Split", &man_js::Split)
.function("_SplitByPlane", &man_js::SplitByPlane)
.function("_TrimByPlane", &Manifold::TrimByPlane)
.function("_Slice", &Manifold::Slice)
.function("project", &Manifold::Project)
.function("hull", select_overload<Manifold() const>(&Manifold::Hull))
.function("_GetMeshJS", &js::GetMeshJS)
.function("refine", &Manifold::Refine)
.function("refineToLength", &Manifold::RefineToLength)
.function("smoothByNormals", &Manifold::SmoothByNormals)
.function("_SmoothOut", &Manifold::SmoothOut)
.function("_Warp", &man_js::Warp)
.function("_SetProperties", &man_js::SetProperties)
.function("transform", &man_js::Transform)
@@ -156,7 +166,9 @@ EMSCRIPTEN_BINDINGS(whatever) {
.function("precision", &Manifold::Precision)
.function("genus", &Manifold::Genus)
.function("getProperties", &Manifold::GetProperties)
.function("minGap", &Manifold::MinGap)
.function("calculateCurvature", &Manifold::CalculateCurvature)
.function("_CalculateNormals", &Manifold::CalculateNormals)
.function("originalID", &Manifold::OriginalID)
.function("asOriginal", &Manifold::AsOriginal);
@@ -176,10 +188,15 @@ EMSCRIPTEN_BINDINGS(whatever) {
function("_manifoldUnionN", &man_js::UnionN);
function("_manifoldDifferenceN", &man_js::DifferenceN);
function("_manifoldIntersectionN", &man_js::IntersectionN);
function("_manifoldCollectVertices", &man_js::CollectVertices);
function("_manifoldHullPoints",
select_overload<Manifold(const std::vector<glm::vec3>&)>(
&Manifold::Hull));
// Quality Globals
function("setMinCircularAngle", &Quality::SetMinCircularAngle);
function("setMinCircularEdgeLength", &Quality::SetMinCircularEdgeLength);
function("setCircularSegments", &Quality::SetCircularSegments);
function("getCircularSegments", &Quality::GetCircularSegments);
function("resetToCircularDefaults", &Quality::ResetToDefaults);
}
+79 -4
View File
@@ -196,6 +196,11 @@ Module.setup = function() {
// Manifold methods
Module.Manifold.prototype.smoothOut = function(
minSharpAngle = 60, minSmoothness = 0) {
return this._SmoothOut(minSharpAngle, minSmoothness);
};
Module.Manifold.prototype.warp = function(func) {
const wasmFuncPtr = addFunction(function(vec3Ptr) {
const x = getValue(vec3Ptr, 'float');
@@ -217,6 +222,11 @@ Module.setup = function() {
return out;
};
Module.Manifold.prototype.calculateNormals = function(
normalIdx, minSharpAngle = 60) {
return this._CalculateNormals(normalIdx, minSharpAngle);
};
Module.Manifold.prototype.setProperties = function(numProp, func) {
const oldNumProp = this.numProp();
const wasmFuncPtr = addFunction(function(newPtr, vec3Ptr, oldPtr) {
@@ -248,8 +258,12 @@ Module.setup = function() {
return this._Translate(vararg2vec3(vec));
};
Module.Manifold.prototype.rotate = function(vec) {
return this._Rotate(...vec);
Module.Manifold.prototype.rotate = function(xOrVec, y, z) {
if (Array.isArray(xOrVec)) {
return this._Rotate(...xOrVec);
} else {
return this._Rotate(xOrVec, y || 0, z || 0);
}
};
Module.Manifold.prototype.scale = function(vec) {
@@ -268,15 +282,19 @@ Module.setup = function() {
return this._TrimByPlane(vararg2vec3([normal]), offset);
};
Module.Manifold.prototype.slice = function(height = 0.) {
return this._Slice(height);
};
Module.Manifold.prototype.split = function(manifold) {
const vec = this._split(manifold);
const vec = this._Split(manifold);
const result = fromVec(vec);
vec.delete();
return result;
};
Module.Manifold.prototype.splitByPlane = function(normal, offset = 0.) {
const vec = this._splitByPlane(vararg2vec3([normal]), offset);
const vec = this._SplitByPlane(vararg2vec3([normal]), offset);
const result = fromVec(vec);
vec.delete();
return result;
@@ -472,6 +490,36 @@ Module.setup = function() {
Module.CrossSection.difference = crossSectionBatchbool('DifferenceN');
Module.CrossSection.intersection = crossSectionBatchbool('IntersectionN');
function pushVec2(vec, ps) {
toVec(vec, ps, p => {
if (p instanceof Array) return {x: p[0], y: p[1]};
return p;
})
}
Module.CrossSection.hull = function(...args) {
if (args.length == 1) args = args[0];
let pts = new Module.Vector_vec2();
for (const cs of args) {
if (cs instanceof CrossSectionCtor) {
Module._crossSectionCollectVertices(pts, cs);
} else if (
cs instanceof Array && cs.length == 2 && typeof cs[0] == 'number') {
pts.push_back({x: cs[0], y: cs[1]});
} else if (cs.x) {
pts.push_back(cs);
} else {
const wrap =
((cs[0].length == 2 && typeof cs[0][0] == 'number') || cs[0].x);
const polys = wrap ? [cs] : cs;
for (const poly of polys) pushVec2(pts, poly);
}
}
const result = Module._crossSectionHullPoints(pts);
pts.delete();
return result;
};
Module.CrossSection.prototype = Object.create(CrossSectionCtor.prototype);
// Because the constructor and prototype are being replaced, instanceof will
@@ -596,6 +644,33 @@ Module.setup = function() {
return out;
};
function pushVec3(vec, ps) {
toVec(vec, ps, p => {
if (p instanceof Array) return {x: p[0], y: p[1], z: p[2]};
return p;
})
}
Module.Manifold.hull = function(...args) {
if (args.length == 1) args = args[0];
let pts = new Module.Vector_vec3();
for (const m of args) {
if (m instanceof ManifoldCtor) {
Module._manifoldCollectVertices(pts, m);
} else if (
m instanceof Array && m.length == 3 && typeof m[0] == 'number') {
pts.push_back({x: m[0], y: m[1], z: m[2]});
} else if (m.x) {
pts.push_back(m);
} else {
pushVec3(pts, m);
}
}
const result = Module._manifoldHullPoints(pts);
pts.delete();
return result;
};
Module.Manifold.prototype = Object.create(ManifoldCtor.prototype);
// Because the constructor and prototype are being replaced, instanceof will
+1
View File
@@ -0,0 +1 @@
declare module '@jscadui/3mf-export';
+142 -11
View File
@@ -65,7 +65,7 @@ button:hover {
filter: brightness(0.9);
}
input {
.name {
border: none;
height: 36px;
background-color: transparent;
@@ -77,12 +77,12 @@ input {
-webkit-font-smoothing: antialiased;
}
input:focus {
.name:focus {
outline: none;
}
p {
margin: 1em;
margin: 18px;
text-align: left;
}
@@ -130,18 +130,14 @@ model-viewer {
.header .margin {
font-size: 16px;
margin: 1em;
}
#file {
width: 250px;
margin: 18px 8px;
}
.dropdown {
display: none;
position: absolute;
z-index: 1;
width: 250px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
}
@@ -150,6 +146,10 @@ model-viewer {
display: block;
}
.hide {
display: none;
}
.uparrow {
display: inline-block;
position: relative;
@@ -168,17 +168,57 @@ model-viewer {
position: relative;
}
.top-edit {
display: inline-block;
vertical-align: middle;
height: 36px;
top: 18px;
border: none;
border-radius: 18px;
}
.top-icon {
display: inline-block;
vertical-align: middle;
width: 36px;
background-size: 24px;
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
}
.squish {
margin-right: -12px;
}
.icon {
display: inline-block;
background-color: transparent;
position: absolute;
width: 36px;
top: 0px;
right: 0px;
background-size: 20px;
background-repeat: no-repeat;
background-position: center;
}
.undo {
background-image: url(/icons/undo.png);
}
.redo {
background-image: url(/icons/redo.png);
}
.star {
background-image: url(/icons/star.png);
}
.share {
background-image: url(/icons/share.png);
}
.cancel {
background-image: url(/icons/close.png);
background-size: 20px;
@@ -193,13 +233,24 @@ model-viewer {
.trash {
background-image: url(/icons/trash.png);
right: 0px;
}
.play {
background-image: url(/icons/play.png);
}
.pause {
background-image: url(/icons/pause.png);
}
.icon:hover {
background-color: rgba(0, 0, 0, 0.2);
}
.top-icon:hover {
background-color: rgba(0, 0, 0, 0.2);
}
.blue {
background-color: rgb(148, 221, 255);
}
@@ -212,7 +263,61 @@ model-viewer {
background-color: rgb(255, 177, 177);
}
@media only screen and (max-width: 600px) {
#file-margin {
position: relative;
width: 250px;
}
#save {
width: 125px;
}
#animation {
display: none;
align-items: center;
position: absolute;
width: 100%;
bottom: 0px;
margin: 2px;
}
#play {
position: relative;
background-color: #ccc;
margin: 8px;
}
.slider {
position: relative;
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 10px;
border-radius: 5px;
margin-right: 16px;
background: #ffffff55;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
border-radius: 50%;
background: #ccc;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 25px;
height: 25px;
border-radius: 50%;
background: #ccc;
cursor: pointer;
}
@media only screen and (max-width: 820px) {
.container {
flex-direction: column;
width: 100%;
@@ -220,5 +325,31 @@ model-viewer {
.col {
width: 100%;
height: 50%;
}
#file-margin {
width: calc(100% - 32px);
}
p {
margin: .2em 1em;
text-align: center;
}
.header .margin {
margin: 0.2em 16px;
}
#button-container {
flex-direction: row;
margin-left: 8px;
margin-right: 8px;
width: calc(100% - 16px);
}
#button-container .margin {
width: calc(50% - 16px);
margin: 8px;
}
}
+161 -39
View File
@@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import ManifoldWorker from './worker?worker';
import ManifoldWorker from './worker-wrapper?worker';
const CODE_START = '<code>';
// Loaded globally by examples.js
const exampleFunctions = self.examples.functionBodies;
@@ -24,22 +25,55 @@ if (navigator.serviceWorker) {
let editor = undefined;
// Edit UI ------------------------------------------------------------
const undoButton = document.querySelector('#undo');
const redoButton = document.querySelector('#redo');
const formatButton = document.querySelector('#format');
const shareButton = document.querySelector('#share');
undoButton.onclick = () => editor.trigger('ignored', 'undo');
redoButton.onclick = () => editor.trigger('ignored', 'redo');
formatButton.onclick = () =>
editor.trigger('ignored', 'editor.action.formatDocument');
shareButton.onclick = () => {
const url = new URL(window.location.toString());
url.hash =
'#' +
encodeURIComponent(
currentFileElement.textContent + CODE_START + editor.getValue());
navigator.clipboard.writeText(url.toString());
console.log('Shareable link copied to clipboard!');
console.log('Consider shortening this URL using tinyURL.com');
};
// File UI ------------------------------------------------------------
const fileButton = document.querySelector('#file');
const currentElement = document.querySelector('#current');
const arrow = document.querySelector('.uparrow');
const dropdown = document.querySelector('.dropdown');
const currentFileElement = document.querySelector('#current');
const fileArrow = document.querySelector('#file .uparrow');
const fileDropdown = document.querySelector('#fileDropdown');
const saveContainer = document.querySelector('#save');
const saveDropdown = document.querySelector('#saveDropdown');
const saveArrow = document.querySelector('#save .uparrow');
const hideDropdown = function() {
dropdown.classList.remove('show');
arrow.classList.remove('down');
fileDropdown.classList.remove('show');
saveDropdown.classList.remove('show');
fileArrow.classList.remove('down');
saveArrow.classList.remove('down');
};
const toggleDropdown = function(event) {
const toggleFileDropdown = function(event) {
event.stopPropagation();
dropdown.classList.toggle('show');
arrow.classList.toggle('down');
fileDropdown.classList.toggle('show');
fileArrow.classList.toggle('down');
};
fileButton.onclick = toggleDropdown;
const toggleSaveDropdown = function(event) {
event.stopPropagation();
saveDropdown.classList.toggle('show');
saveArrow.classList.toggle('down');
};
fileButton.onclick = toggleFileDropdown;
saveArrow.parentElement.onclick = toggleSaveDropdown;
document.body.onclick = hideDropdown;
const prefix = 'ManifoldCAD';
@@ -62,7 +96,7 @@ function nthKey(n) {
function saveCurrent() {
if (editor) {
const currentName = currentElement.textContent;
const currentName = currentFileElement.textContent;
if (!exampleFunctions.get(currentName)) {
setScript(currentName, editor.getValue());
}
@@ -77,11 +111,12 @@ let isExample = true;
function switchTo(scriptName) {
if (editor) {
switching = true;
currentElement.textContent = scriptName;
currentFileElement.textContent = scriptName;
setScript('currentName', scriptName);
const code =
exampleFunctions.get(scriptName) ?? getScript(scriptName) ?? '';
isExample = exampleFunctions.get(scriptName) != null;
const code = isExample ? exampleFunctions.get(scriptName).substring(1) :
getScript(scriptName) ?? '';
window.location.hash = '#' + scriptName;
editor.setValue(code);
}
}
@@ -135,6 +170,7 @@ function addEdit(button) {
const code = getScript(oldName);
const form = document.createElement('form');
const inputElement = document.createElement('input');
inputElement.classList.add('name');
inputElement.value = oldName;
label.textContent = '';
button.appendChild(form);
@@ -148,8 +184,8 @@ function addEdit(button) {
if (!input) return;
const newName = uniqueName(input);
label.textContent = newName;
if (currentElement.textContent == oldName) {
currentElement.textContent = newName;
if (currentFileElement.textContent == oldName) {
currentFileElement.textContent = newName;
}
removeScript(oldName);
setScript(newName, code);
@@ -182,7 +218,7 @@ function addEdit(button) {
}, {once: true});
} else if (performance.now() - lastClick > 500) {
removeScript(label.textContent);
if (currentElement.textContent == label.textContent) {
if (currentFileElement.textContent == label.textContent) {
switchTo('Intro');
}
const container = button.parentElement;
@@ -192,16 +228,16 @@ function addEdit(button) {
}
const newButton = document.querySelector('#new');
function newItem(code) {
const name = uniqueName('New Script');
function newItem(code, scriptName = undefined) {
const name = uniqueName(scriptName ?? 'New Script');
setScript(name, code);
const nextButton = createDropdownItem(name);
newButton.insertAdjacentElement('afterend', nextButton.parentElement);
addEdit(nextButton);
nextButton.click();
return {button: nextButton, name};
};
newButton.onclick = function() {
newItem('');
newItem('').button.click();
};
const runButton = document.querySelector('#compile');
@@ -214,7 +250,7 @@ function initializeRun() {
if (autoExecute) {
runButton.click();
} else {
poster.textContent = 'Auto-run disabled due to prior failure';
poster.textContent = 'Auto-run disabled';
}
}
@@ -240,6 +276,7 @@ declare interface ManifoldToplevel {
setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength;
setCircularSegments: typeof T.setCircularSegments;
getCircularSegments: typeof T.getCircularSegments;
resetToCircularDefaults: typeof T.resetToCircularDefaults;
setup: () => void;
}
declare const module: ManifoldToplevel;
@@ -269,10 +306,10 @@ require(['vs/editor/editor.main'], async function() {
for (const [name] of exampleFunctions) {
const button = createDropdownItem(name);
dropdown.appendChild(button.parentElement);
fileDropdown.appendChild(button.parentElement);
}
let currentName = currentElement.textContent;
let currentName = currentFileElement.textContent;
for (let i = 0; i < window.localStorage.length; i++) {
const key = nthKey(i);
@@ -287,7 +324,24 @@ require(['vs/editor/editor.main'], async function() {
addEdit(button);
}
}
switchTo(currentName);
if (window.location.hash.length > 0) {
const fragment = decodeURIComponent(window.location.hash.substring(1));
const codeIdx = fragment.indexOf(CODE_START);
if (codeIdx != -1) {
autoExecute = true;
const name = fragment.substring(0, codeIdx);
switchTo(
newItem(fragment.substring(codeIdx + CODE_START.length), name).name);
} else {
if (fragment != currentName) {
autoExecute = true;
}
switchTo(fragment);
}
} else {
switchTo(currentName);
}
if (manifoldInitialized) {
initializeRun();
@@ -297,11 +351,12 @@ require(['vs/editor/editor.main'], async function() {
runButton.disabled = false;
if (switching) {
switching = false;
editor.setScrollTop(0);
return;
}
if (isExample) {
const cursor = editor.getPosition();
newItem(editor.getValue());
newItem(editor.getValue()).button.click();
editor.setPosition(cursor);
}
});
@@ -311,6 +366,51 @@ require(['vs/editor/editor.main'], async function() {
};
});
// Animation ------------------------------------------------------------
const mv = document.querySelector('model-viewer');
const animationContainer = document.querySelector('#animation');
const playButton = document.querySelector('#play');
const scrubber = document.querySelector('#scrubber');
let paused = false;
mv.addEventListener('load', () => {
const hasAnimation = mv.availableAnimations.length > 0;
animationContainer.style.display = hasAnimation ? 'flex' : 'none';
if (hasAnimation) {
play();
}
});
function play() {
mv.play();
playButton.classList.remove('play');
playButton.classList.add('pause');
paused = false;
scrubber.classList.add('hide');
}
function pause() {
mv.pause();
playButton.classList.remove('pause');
playButton.classList.add('play');
paused = true;
scrubber.max = mv.duration;
scrubber.value = mv.currentTime;
scrubber.classList.remove('hide');
}
playButton.onclick = function() {
if (paused) {
play();
} else {
pause();
}
};
scrubber.oninput = function() {
mv.currentTime = scrubber.value;
};
// Execution ------------------------------------------------------------
const consoleElement = document.querySelector('#console');
const oldLog = console.log;
@@ -346,8 +446,10 @@ function finishRun() {
`Took ${(Math.round((t1 - t0) / 10) / 100).toLocaleString()} seconds`);
}
const mv = document.querySelector('model-viewer');
let objectURL = null;
const output = {
glbURL: null,
threeMFURL: null
};
let manifoldWorker = null;
function createWorker() {
@@ -370,10 +472,16 @@ function createWorker() {
finishRun();
runButton.disabled = true;
URL.revokeObjectURL(objectURL);
objectURL = e.data.objectURL;
mv.src = objectURL;
if (objectURL == null) {
if (output.threeMFURL != null) {
URL.revokeObjectURL(output.threeMFURL);
output.threeMFURL = null;
}
URL.revokeObjectURL(output.glbURL);
output.glbURL = e.data.glbURL;
output.threeMFURL = e.data.threeMFURL;
threemfButton.disabled = output.threeMFURL == null;
mv.src = output.glbURL;
if (output.glbURL == null) {
mv.showPoster();
poster.textContent = 'Error';
createWorker();
@@ -411,10 +519,24 @@ runButton.onclick = function() {
}
};
const downloadButton = document.querySelector('#download');
downloadButton.onclick = function() {
const link = document.createElement('a');
link.download = 'manifold.glb';
link.href = objectURL;
link.click();
};
function clickSave(saveButton, filename, outputName) {
const container = saveButton.parentElement;
return () => {
const oldSave = saveContainer.firstElementChild;
if (oldSave !== container) {
saveDropdown.insertBefore(oldSave, saveDropdown.firstElementChild);
saveContainer.insertBefore(container, saveDropdown);
container.appendChild(saveArrow.parentElement);
}
const link = document.createElement('a');
link.download = filename;
link.href = output[outputName];
link.click();
};
}
const glbButton = document.querySelector('#glb');
glbButton.onclick = clickSave(glbButton, 'manifold.glb', 'glbURL');
const threemfButton = document.querySelector('#threemf');
threemfButton.onclick = clickSave(threemfButton, 'manifold.3mf', 'threeMFURL');
-285
View File
@@ -1,285 +0,0 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Accessor} from '@gltf-transform/core';
import {EXTManifold} from './manifold-gltf.js';
export const attributeDefs = {
'POSITION': {type: Accessor.Type.VEC3, components: 3},
'NORMAL': {type: Accessor.Type.VEC3, components: 3},
'TANGENT': {type: Accessor.Type.VEC4, components: 4},
'TEXCOORD_0': {type: Accessor.Type.VEC2, components: 2},
'TEXCOORD_1': {type: Accessor.Type.VEC2, components: 2},
'COLOR_0': {type: Accessor.Type.VEC3, components: 3},
'JOINTS_0': {type: Accessor.Type.VEC4, components: 4},
'WEIGHTS_0': {type: Accessor.Type.VEC4, components: 4},
'SKIP_1': {type: null, components: 1},
'SKIP_2': {type: null, components: 2},
'SKIP_3': {type: null, components: 3},
'SKIP_4': {type: null, components: 4},
};
export function setupIO(io) {
return io.registerExtensions([EXTManifold]);
}
function readPrimitive(primitive, numProp, attributes) {
const position = primitive.getAttribute('POSITION');
const numVert = position.getCount();
const vertProperties = [];
let offset = 0;
for (const attribute of attributes) {
const size = attributeDefs[attribute].components;
if (attribute === 'SKIP') {
offset += size;
continue;
}
const accessor = primitive.getAttribute(attribute);
if (accessor) {
const array = accessor.getArray();
for (let i = 0; i < numVert; ++i) {
for (let j = 0; j < size; ++j) {
vertProperties[numProp * i + offset + j] = array[i * size + j];
}
}
} else {
for (let i = 0; i < numVert; ++i) {
for (let j = 0; j < size; ++j) {
vertProperties[numProp * i + offset + j] = 0;
}
}
}
offset += size;
}
return vertProperties;
}
export function readMesh(mesh, attributes, materials) {
const primitives = mesh.listPrimitives();
if (primitives.length === 0) {
return {};
}
if (attributes.length === 0) {
const attributeSet = new Set();
for (const primitive of primitives) {
const semantics = primitive.listSemantics();
for (const semantic of semantics) {
attributeSet.add(semantic);
}
}
for (const semantic in attributeDefs) {
if (attributeSet.has(semantic)) {
attributes.push(semantic);
attributeSet.delete(semantic);
}
}
for (const semantic of attributeSet.keys()) {
attributes.push(semantic);
}
}
if (attributes.length < 1 || attributes[0] !== 'POSITION')
throw new Error('First attribute must be "POSITION".');
const numProp = attributes.map((def) => attributeDefs[def].components)
.reduce((a, b) => a + b);
const manifoldPrimitive = mesh.getExtension('EXT_manifold');
let vertPropArray = [];
let triVertArray = [];
const runIndexArray = [0];
const mergeFromVert = [];
const mergeToVert = [];
if (manifoldPrimitive != null) {
vertPropArray = readPrimitive(primitives[0], numProp, attributes);
for (const primitive of primitives) {
triVertArray = [...triVertArray, ...primitive.getIndices().getArray()];
runIndexArray.push(triVertArray.length);
materials.push(primitive.getMaterial());
}
const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? [];
const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? [];
const vert2merge = new Map();
for (const [i, idx] of mergeTriVert.entries()) {
vert2merge.set(triVertArray[idx], mergeTo[i]);
}
for (const [from, to] of vert2merge.entries()) {
mergeFromVert.push(from);
mergeToVert.push(to);
}
} else {
for (const primitive of primitives) {
const numVert = vertPropArray.length / numProp;
vertPropArray =
[...vertPropArray, ...readPrimitive(primitive, numProp, attributes)];
triVertArray = [
...triVertArray,
...primitive.getIndices().getArray().map((i) => i + numVert)
];
runIndexArray.push(triVertArray.length);
materials.push(primitive.getMaterial());
}
}
const vertProperties = new Float32Array(vertPropArray);
const triVerts = new Uint32Array(triVertArray);
const runIndex = new Uint32Array(runIndexArray);
return {
numProp,
triVerts,
vertProperties,
runIndex,
mergeFromVert,
mergeToVert
};
}
export function writeMesh(doc, manifoldMesh, attributes, materials) {
if (doc.getRoot().listBuffers().length === 0) {
doc.createBuffer();
}
const buffer = doc.getRoot().listBuffers()[0];
const manifoldExtension = doc.createExtension(EXTManifold);
const attributeUnion = [];
for (const matAttributes of attributes) {
matAttributes.forEach((attribute, i) => {
if (i >= attributeUnion.length) {
attributeUnion.push(attribute);
} else {
const size = attributeDefs[attribute].components;
const unionSize = attributeDefs[attributeUnion[i]].components;
if (size != unionSize) {
throw new Error(
'Attribute sizes do not correspond: ', attribute, ' and ',
attributeUnion[i]);
}
if (attributeDefs[attributeUnion[i]].type == null) {
attributeUnion[i] = attribute;
}
}
});
}
if (attributeUnion.length < 1 || attributeUnion[0] !== 'POSITION')
throw new Error('First attribute must be "POSITION".');
const mesh = doc.createMesh();
const numPrimitive = manifoldMesh.runIndex.length - 1;
for (let run = 0; run < numPrimitive; ++run) {
const indices = doc.createAccessor('indices')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array());
const primitive = doc.createPrimitive().setIndices(indices);
const material = materials[run];
if (material) {
primitive.setMaterial(material);
}
mesh.addPrimitive(primitive);
}
const numVert = manifoldMesh.numVert;
const numProp = manifoldMesh.numProp;
let offset = 0;
attributeUnion.forEach((attribute, aIdx) => {
if (attribute === 'SKIP') {
++offset;
return;
}
const def = attributeDefs[attribute];
if (def == null)
throw new Error(attribute + ' is not a recognized attribute.');
const n = def.components;
if (offset + n > numProp) throw new Error('Too many attribute channels.');
const array = new Float32Array(n * numVert);
for (let v = 0; v < numVert; ++v) {
for (let i = 0; i < n; ++i) {
array[n * v + i] =
manifoldMesh.vertProperties[numProp * v + offset + i];
}
}
const accessor =
doc.createAccessor().setBuffer(buffer).setType(def.type).setArray(
array);
mesh.listPrimitives().forEach((primitive, pIdx) => {
if (attributes[pIdx].length > aIdx &&
attributeDefs[attributes[pIdx][aIdx]].type != null) {
primitive.setAttribute(attribute, accessor);
}
});
offset += n;
});
const manifoldPrimitive = manifoldExtension.createManifoldPrimitive();
mesh.setExtension('EXT_manifold', manifoldPrimitive);
const indices = doc.createAccessor('indices')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(manifoldMesh.triVerts);
manifoldPrimitive.setIndices(indices);
manifoldPrimitive.setRunIndex(manifoldMesh.runIndex);
const vert2merge = [...Array(manifoldMesh.numVert).keys()];
for (const [i, from] of manifoldMesh.mergeFromVert.entries()) {
vert2merge[from] = manifoldMesh.mergeToVert[i];
}
const ind = [];
const val = [];
for (const [i, vert] of manifoldMesh.triVerts.entries()) {
const newVert = vert2merge[vert];
if (vert !== newVert) {
ind.push(i);
val.push(newVert);
}
}
const indicesAccessor = doc.createAccessor()
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(ind));
const valuesAccessor = doc.createAccessor()
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(val));
manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor);
return mesh;
}
export function disposeMesh(mesh) {
if (!mesh) return;
const primitives = mesh.listPrimitives();
for (const primitive of primitives) {
primitive.getIndices()?.dispose();
for (const accessor of primitive.listAttributes()) {
accessor.dispose();
}
}
mesh.dispose();
}
export async function loadTexture(texture, uri) {
const response = await fetch(uri);
const blob = await response.blob();
texture.setMimeType(blob.type);
texture.setImage(new Uint8Array(await blob.arrayBuffer()));
}
+424
View File
@@ -0,0 +1,424 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Accessor, Document, Material, Mesh, Primitive, Texture, WebIO} from '@gltf-transform/core';
import {EXTManifold, ManifoldPrimitive} from './manifold-gltf';
import {Mesh as ManifoldMesh, MeshOptions} from './public/manifold-encapsulated-types';
export const attributeDefs = {
'POSITION': {type: Accessor.Type.VEC3, components: 3},
'NORMAL': {type: Accessor.Type.VEC3, components: 3},
'TANGENT': {type: Accessor.Type.VEC4, components: 4},
'TEXCOORD_0': {type: Accessor.Type.VEC2, components: 2},
'TEXCOORD_1': {type: Accessor.Type.VEC2, components: 2},
'COLOR_0': {type: Accessor.Type.VEC3, components: 3},
'JOINTS_0': {type: Accessor.Type.VEC4, components: 4},
'WEIGHTS_0': {type: Accessor.Type.VEC4, components: 4},
'SKIP_1': {type: null, components: 1},
'SKIP_2': {type: null, components: 2},
'SKIP_3': {type: null, components: 3},
'SKIP_4': {type: null, components: 4},
};
export type Attribute = keyof(typeof attributeDefs);
export interface Properties {
material: Material;
attributes: Attribute[];
}
/**
* Call this first to register the manifold extension so that readMesh and
* writeMesh will work.
*/
export function setupIO(io: WebIO) {
return io.registerExtensions([EXTManifold]);
}
/**
* Read an input mesh into Manifold-compatible data structures, whether it
* contains the EXT_mesh_manifold extension or not.
*
* @param mesh The Mesh to read.
* @param attributes An array of attributes representing the order of desired
* properties returned in the vertProperties array of the output mesh. If
* omitted, this will be populated with the union of all attributes defined
* in the primitives of the input mesh. If present, the first entry must be
* 'POSITION', and any attributes in the primitives that are not included in
* this list will be ignored, while those in the list but not defined in a
* primitive will be populated with zeros.
* @returns The returned mesh is suitable for initializing a Manifold or Mesh of
* the Manifold library if desired. See Manifold documentation if you prefer
* to use these GL arrays in a different library. The runProperties array
* gives the Material and attributes list associated with each triangle run,
* which in turn corresponds to a primitive of the input mesh. These
* attributes are the intersection of the attributes present on the
* primitive and those requested in the attributes input.
*/
export function readMesh(mesh: Mesh, attributes: Attribute[] = []):
{mesh: MeshOptions, runProperties: Properties[]}|null {
const primitives = mesh.listPrimitives();
if (primitives.length === 0) {
return null;
}
if (attributes.length === 0) {
const attributeSet = new Set<Attribute>();
for (const primitive of primitives) {
const semantics = primitive.listSemantics() as Attribute[];
for (const semantic of semantics) {
attributeSet.add(semantic);
}
}
let semantic: Attribute;
for (semantic in attributeDefs) {
if (attributeSet.has(semantic)) {
attributes.push(semantic);
attributeSet.delete(semantic);
}
}
for (const semantic of attributeSet.keys()) {
attributes.push(semantic);
}
}
if (attributes.length < 1 || attributes[0] !== 'POSITION')
throw new Error('First attribute must be "POSITION".');
let numProp = 0;
const attributeOffsets = attributes.map((numProp = 0, def => {
const last = numProp;
numProp += attributeDefs[def].components;
return last;
}));
const manifoldPrimitive =
mesh.getExtension('EXT_mesh_manifold') as ManifoldPrimitive;
let vertPropArray = Array<number>();
let triVertArray = Array<number>();
const runIndexArray = [0];
const mergeFromVertArray = Array<number>();
const mergeToVertArray = Array<number>();
const runProperties = Array<Properties>();
if (manifoldPrimitive != null) {
const numVert = primitives[0].getAttribute('POSITION')!.getCount();
const foundAttribute = attributes.map((a) => attributeDefs[a].type == null);
vertPropArray = new Array<number>(numProp * numVert);
for (const primitive of primitives) {
const indices = primitive.getIndices();
if (!indices) {
console.log('Skipping non-indexed primitive ', primitive.getName());
continue;
}
const attributesIn = primitive.listSemantics() as Attribute[];
attributes.forEach((attributeOut, idx) => {
if (foundAttribute[idx]) {
return;
}
for (const attributeIn of attributesIn) {
if (attributeIn === attributeOut) {
foundAttribute[idx] = true;
const accessor = primitive.getAttribute(attributeIn)!;
writeProperties(
vertPropArray, accessor, numProp, attributeOffsets[idx]);
}
}
});
triVertArray = [...triVertArray, ...indices.getArray()!];
runIndexArray.push(triVertArray.length);
runProperties.push({
material: primitive.getMaterial()!,
attributes: attributesIn.filter(b => attributes.some(a => a == b))
});
}
const mergeTriVert = manifoldPrimitive.getMergeIndices()?.getArray() ?? [];
const mergeTo = manifoldPrimitive.getMergeValues()?.getArray() ?? [];
const vert2merge = new Map<number, number>();
for (const [i, idx] of mergeTriVert.entries()) {
vert2merge.set(triVertArray[idx], mergeTo[i]);
}
for (const [from, to] of vert2merge.entries()) {
mergeFromVertArray.push(from);
mergeToVertArray.push(to);
}
} else {
for (const primitive of primitives) {
const indices = primitive.getIndices();
if (!indices) {
console.log('Skipping non-indexed primitive ', primitive.getName());
continue;
}
const numVert = vertPropArray.length / numProp;
vertPropArray =
[...vertPropArray, ...readPrimitive(primitive, numProp, attributes)];
triVertArray =
[...triVertArray, ...indices.getArray()!.map((i) => i + numVert)];
runIndexArray.push(triVertArray.length);
const attributesIn = primitive.listSemantics() as Attribute[];
runProperties.push({
material: primitive.getMaterial()!,
attributes: attributesIn.filter(b => attributes.some(a => a == b))
});
}
}
const vertProperties = new Float32Array(vertPropArray);
const triVerts = new Uint32Array(triVertArray);
const runIndex = new Uint32Array(runIndexArray);
const mergeFromVert = new Uint32Array(mergeFromVertArray);
const mergeToVert = new Uint32Array(mergeToVertArray);
const meshOut =
{numProp, triVerts, vertProperties, runIndex, mergeFromVert, mergeToVert};
return {mesh: meshOut, runProperties};
}
/**
* Write a Manifold Mesh into a glTF Mesh object, using the EXT_mesh_manifold
* extension to allow for lossless roundtrip of the manifold mesh through the
* glTF file.
*
* @param doc The glTF Document to which this Mesh will be added.
* @param manifoldMesh The Manifold Mesh to convert to glTF.
* @param id2properties A map from originalID to Properties that include the
* glTF Material and the set of attributes to output. All triangle runs with
* the same originalID will be combined into a single output primitive. Any
* originalIDs not found in the map will have the glTF default material and
* no attributes beyond 'POSITION'. Each attributes array must correspond to
* the manifoldMesh vertProperties, thus the first attribute must always be
* 'POSITION'. Any properties that should not be output for a given
* primitive must use the 'SKIP_*' attributes.
* @returns The glTF Mesh to add to the Document.
*/
export function writeMesh(
doc: Document, manifoldMesh: ManifoldMesh,
id2properties: Map<number, Properties>): Mesh {
if (doc.getRoot().listBuffers().length === 0) {
doc.createBuffer();
}
const buffer = doc.getRoot().listBuffers()[0];
const manifoldExtension = doc.createExtension(EXTManifold);
const mesh = doc.createMesh();
const runIndex = Array<number>();
const attributeUnion = Array<Attribute>();
const primitive2attributes = new Map<Primitive, Attribute[]>();
const numRun = manifoldMesh.runIndex.length - 1;
let lastID = -1;
for (let run = 0; run < numRun; ++run) {
const id = manifoldMesh.runOriginalID[run];
if (id == lastID) {
continue;
}
lastID = id;
runIndex.push(manifoldMesh.runIndex[run]);
const indices = doc.createAccessor('primitive indices of ID ' + id)
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(1));
const primitive = doc.createPrimitive().setIndices(indices);
const properties = id2properties.get(id);
if (properties) {
const {material, attributes} = properties;
if (attributes.length < 1 || attributes[0] !== 'POSITION')
throw new Error('First attribute must be "POSITION".');
primitive.setMaterial(material);
primitive2attributes.set(primitive, attributes);
properties.attributes.forEach((attribute, i) => {
if (i >= attributeUnion.length) {
attributeUnion.push(attribute);
} else {
const size = attributeDefs[attribute].components;
const unionSize = attributeDefs[attributeUnion[i]].components;
if (size != unionSize) {
throw new Error(
'Attribute sizes do not correspond: ' + attribute + ' and ' +
attributeUnion[i]);
}
if (attributeDefs[attributeUnion[i]].type == null) {
attributeUnion[i] = attribute;
}
}
});
} else {
primitive2attributes.set(primitive, ['POSITION']);
}
mesh.addPrimitive(primitive);
}
runIndex.push(manifoldMesh.runIndex[numRun]);
const numVert = manifoldMesh.numVert;
const numProp = manifoldMesh.numProp;
let offset = 0;
attributeUnion.forEach((attribute, aIdx) => {
const def = attributeDefs[attribute];
if (def == null)
throw new Error(attribute + ' is not a recognized attribute.');
if (def.type == null) {
++offset;
return;
}
const n = def.components;
if (offset + n > numProp) throw new Error('Too many attribute channels.');
const array = new Float32Array(n * numVert);
for (let v = 0; v < numVert; ++v) {
for (let i = 0; i < n; ++i) {
let x = manifoldMesh.vertProperties[numProp * v + offset + i];
if (attribute == 'COLOR_0') {
x = Math.max(0, Math.min(1, x));
}
array[n * v + i] = x;
}
}
const accessor = doc.createAccessor(attribute)
.setBuffer(buffer)
.setType(def.type)
.setArray(array);
for (const primitive of mesh.listPrimitives()) {
const attributes = primitive2attributes.get(primitive)!;
if (attributes.length > aIdx &&
attributeDefs[attributes[aIdx]].type != null) {
primitive.setAttribute(attribute, accessor);
}
}
offset += n;
});
const manifoldPrimitive = manifoldExtension.createManifoldPrimitive();
mesh.setExtension('EXT_mesh_manifold', manifoldPrimitive);
const indices = doc.createAccessor('manifold indices')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(manifoldMesh.triVerts);
manifoldPrimitive.setIndices(indices);
manifoldPrimitive.setRunIndex(runIndex);
const vert2merge = [...Array(manifoldMesh.numVert).keys()];
const ind = Array<number>();
const val = Array<number>();
if (manifoldMesh.mergeFromVert && manifoldMesh.mergeToVert) {
for (const [i, from] of manifoldMesh.mergeFromVert.entries()) {
vert2merge[from] = manifoldMesh.mergeToVert[i];
}
for (const [i, vert] of manifoldMesh.triVerts.entries()) {
const newVert = vert2merge[vert];
if (vert !== newVert) {
ind.push(i);
val.push(newVert);
}
}
}
if (ind.length > 0) {
const indicesAccessor = doc.createAccessor('merge from')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(ind));
const valuesAccessor = doc.createAccessor('merge to')
.setBuffer(buffer)
.setType(Accessor.Type.SCALAR)
.setArray(new Uint32Array(val));
manifoldPrimitive.setMerge(indicesAccessor, valuesAccessor);
}
return mesh;
}
/**
* Helper function to dispose of a Mesh, useful when replacing an existing Mesh
* with one from writeMesh.
*/
export function disposeMesh(mesh: Mesh) {
if (!mesh) return;
const primitives = mesh.listPrimitives();
for (const primitive of primitives) {
primitive.getIndices()?.dispose();
for (const accessor of primitive.listAttributes()) {
accessor.dispose();
}
}
const manifoldPrimitive =
mesh.getExtension('EXT_mesh_manifold') as ManifoldPrimitive;
if (manifoldPrimitive) {
manifoldPrimitive.getIndices()?.dispose();
manifoldPrimitive.getMergeIndices()?.dispose();
manifoldPrimitive.getMergeValues()?.dispose();
}
mesh.dispose();
}
/**
* Helper function to download an image and apply it to the given texture.
*
* @param texture The texture to update
* @param uri The location of the image to download
*/
export async function loadTexture(texture: Texture, uri: string) {
const response = await fetch(uri);
const blob = await response.blob();
texture.setMimeType(blob.type);
texture.setImage(new Uint8Array(await blob.arrayBuffer()));
}
function writeProperties(
vertProperties: number[], accessor: Accessor, numProp: number,
offset: number) {
const array = accessor.getArray()!;
const size = accessor.getElementSize();
const numVert = accessor.getCount();
for (let i = 0; i < numVert; ++i) {
for (let j = 0; j < size; ++j) {
vertProperties[numProp * i + offset + j] = array[i * size + j];
}
}
}
function readPrimitive(
primitive: Primitive, numProp: number, attributes: Attribute[]) {
const vertProperties: number[] = [];
let offset = 0;
for (const attribute of attributes) {
const size = attributeDefs[attribute].components;
if (attributeDefs[attribute].type == null) {
offset += size;
continue;
}
const accessor = primitive.getAttribute(attribute);
if (accessor) {
writeProperties(vertProperties, accessor, numProp, offset);
}
offset += size;
}
return vertProperties;
}
+31 -8
View File
@@ -39,25 +39,48 @@
<body>
<div class="page">
<div class="container header" style="background-color: #A288B3;">
<p style="flex: 2;"><a target="_blank" href="https://github.com/elalish/manifold">Manifold</a>CAD </p>
<div class="margin" style="position: relative;">
<button type="button" id="file" class="blue"><b class="uparrow"></b>&nbsp;&nbsp;<span
<p style="flex: 2;"><a target="_blank" href="https://github.com/elalish/manifold">Manifold</a>CAD
&nbsp;&nbsp;<span class="top-edit blue">
<button type="button" id="undo" class="top-icon undo squish"></button>
<button type="button" id="redo" class="top-icon redo squish"></button>
<button type="button" id="format" class="top-icon star squish"></button>
<button type="button" id="share" class="top-icon share"></button>
</span>
</p>
<div class="margin" id="file-margin">
<button type="button" id="file" class="blue item"><b class="uparrow"></b>&nbsp;&nbsp;<span
id="current">Intro</span></button>
<div class="dropdown">
<div class="dropdown" id="fileDropdown">
<button type="button" class="green item" id="new">New</button>
</div>
</div>
<button type="button" id="compile" class="margin green" disabled><span>Run</span></button>
<button type="button" id="download" class="blue margin">Save GLB</button>
<div class="container" id="button-container">
<button type="button" id="compile" class="margin green" disabled><span>Run</span></button>
<div id="save" class="margin" style="position: relative; ">
<div class="item">
<button type="button" id="glb" class="blue item">Save GLB</button>
<button type="button" class="icon"><b class="uparrow" style="right: 2px;"></b></button>
</div>
<div class="dropdown" id="saveDropdown">
<div class="item">
<button type="button" id="threemf" class="blue item">Save 3MF</button>
</div>
</div>
</div>
</div>
</div>
<div class="container" style="flex: 2;">
<div id="editor" class="col"></div>
<div class="col container" style="flex-direction: column;">
<model-viewer camera-controls min-camera-orbit="auto 0deg auto" max-camera-orbit="auto 180deg auto"
shadow-intensity="1" interaction-prompt="none" alt="Editor 3D output">
shadow-intensity="1" tone-mapping="neutral" interaction-prompt="none" alt="Editor 3D output">
<div class="center" slot="poster">
<p id="poster" style="text-align: center; line-height: 36px;"> Loading... </p>
</div>
<div id="animation">
<button type="button" id="play" class="icon pause"></button>
<input type="range" min="0" max="1" step="0.01" value="0" class="slider" id="scrubber">
</div>
</model-viewer>
<div id="console"></div>
</div>
@@ -72,6 +95,6 @@
integrity="sha512-iQEIc0rsSDujsfjtD+lfyJ1W23Bh/lbgriubKDAym6VlEIDRj9rrbSIyJRyshOrl8s0yRcQ0+gyrZfSLyjJGWQ=="
crossorigin="anonymous" referrerpolicy="no-referrer">
<script type="module" src="editor.js"></script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.1.1/model-viewer.min.js"></script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
</html>
+4 -128
View File
@@ -46,7 +46,7 @@
<body>
<p>Load a glTF/GLB model and our <a href="https://github.com/elalish/manifold">Manifold</a> library will attempt to
merge it into a set of manifold objects. If the model is water-tight, you can download the new GLB which will have
our <a href="https://github.com/KhronosGroup/glTF/pull/2286">EXT_manifold</a> extension, thus preserving the
our <a href="https://github.com/KhronosGroup/glTF/pull/2286">EXT_mesh_manifold</a> extension, thus preserving the
manifold data without losing any mesh properties.</p>
<p> If the View Manifold GLB checkbox is enabled, then some meshes in the model have open edges and don't represent a
solid object. Check this box to see only the manifold parts, which will also enable them to be downloaded. If the
@@ -55,135 +55,11 @@
<button id="download" disabled>Download manifold GLB</button>
<input type="checkbox" id="viewFinal" disabled>
<label for="viewFinal">View Manifold GLB</label>
<model-viewer camera-controls shadow-intensity="1" alt="Loaded GLB model">
<model-viewer camera-controls shadow-intensity="1" tone-mapping="neutral" alt="Loaded GLB model">
<span id="poster" slot="poster">Drop a GLB here</span>
</model-viewer>
</body>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/2.0.1/model-viewer.min.js"></script>
<script type="module">
import { WebIO } from '@gltf-transform/core';
import { prune } from '@gltf-transform/functions';
import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions';
import { SimpleDropzone } from 'simple-dropzone';
import { readMesh, writeMesh, setupIO } from './gltf-io.js';
import Module from './built/manifold.js';
const io = setupIO(new WebIO());
io.registerExtensions(KHRONOS_EXTENSIONS);
const wasm = await Module();
wasm.setup();
const { Manifold, Mesh } = wasm;
const mv = document.querySelector('model-viewer');
const inputEl = document.querySelector('#input');
const downloadButton = document.querySelector('#download');
const checkbox = document.querySelector('#viewFinal');
const dropCtrl = new SimpleDropzone(mv, inputEl);
let inputGLBurl = null;
let outputGLBurl = null;
let allManifold = true;
let anyManifold = false;
dropCtrl.on('drop', async ({ files }) => {
for (const [path, file] of files) {
const filename = file.name.toLowerCase();
if (filename.match(/\.(gltf|glb)$/)) {
URL.revokeObjectURL(inputGLBurl);
inputGLBurl = URL.createObjectURL(file);
await writeGLB(await readGLB(inputGLBurl));
updateUI();
break;
}
}
});
function updateUI() {
if (allManifold) {
checkbox.checked = true;
checkbox.disabled = true;
} else if (anyManifold) {
checkbox.checked = false;
checkbox.disabled = false;
} else {
checkbox.checked = false;
checkbox.disabled = true;
}
onClick();
}
checkbox.onclick = onClick;
function onClick() {
mv.src = checkbox.checked ? outputGLBurl : inputGLBurl;
downloadButton.disabled = !checkbox.checked;
};
downloadButton.onclick = function () {
const link = document.createElement('a');
link.download = 'manifold.glb';
link.href = outputGLBurl;
link.click();
};
async function writeGLB(doc) {
URL.revokeObjectURL(outputGLBurl);
if (!anyManifold) {
return;
}
const glb = await io.writeBinary(doc);
const blob = new Blob([glb], { type: 'application/octet-stream' });
outputGLBurl = URL.createObjectURL(blob);
}
async function readGLB(url) {
allManifold = false;
anyManifold = false;
updateUI();
allManifold = true;
const docIn = await io.read(url);
const nodes = docIn.getRoot().listNodes();
for (const node of nodes) {
const attributes = [];
const mesh = node.getMesh();
if (!mesh) {
continue;
}
const materials = [];
const tmpMesh = readMesh(mesh, attributes, materials);
tmpMesh.runOriginalID = [];
const firstID = Manifold.reserveIDs(materials.length);
for (let i = 0; i < materials.length; ++i) {
tmpMesh.runOriginalID.push(firstID + i);
}
const manifoldMesh = new Mesh(tmpMesh);
mesh.dispose();
const matAttributes = [];
materials.forEach(() => matAttributes.push(attributes));
manifoldMesh.merge();
try {
// Test manifoldness - will throw if not.
const manifold = Manifold(manifoldMesh);
node.setMesh(writeMesh(docIn, manifold.getMesh(), matAttributes, materials));
manifold.delete();
anyManifold = true;
} catch (e) {
console.log(mesh.getName(), e);
allManifold = false;
}
}
await docIn.transform(prune());
return docIn;
}
</script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
<script type="module" src="make-manifold.ts"></script>
</html>
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2024 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Document, WebIO} from '@gltf-transform/core';
import {KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
import {prune} from '@gltf-transform/functions';
import {SimpleDropzone} from 'simple-dropzone';
import Module from './built/manifold.js';
import {disposeMesh, Properties, readMesh, setupIO, writeMesh} from './gltf-io';
// Set up gltf-transform
const io = setupIO(new WebIO());
io.registerExtensions(KHRONOS_EXTENSIONS);
// Set up Manifold WASM library
const wasm = await Module();
wasm.setup();
const {Manifold, Mesh} = wasm;
// UX elements
const mv = document.querySelector('model-viewer');
const inputEl = document.querySelector('#input') as HTMLInputElement;
const downloadButton = document.querySelector('#download') as HTMLButtonElement;
const checkbox = document.querySelector('#viewFinal') as HTMLInputElement;
const dropCtrl = new SimpleDropzone(mv, inputEl) as any;
// glTF objects in memory
let inputGLBurl = '';
let outputGLBurl = '';
// Status of manifoldness of all meshes
let allManifold = true;
let anyManifold = false;
// The processing is run when a glTF is drag-and-dropped onto this element.
dropCtrl.on('drop', async ({files}: {files: Map<string, File>}) => {
for (const [_path, file] of files) {
const filename = file.name.toLowerCase();
if (filename.match(/\.(gltf|glb)$/)) {
URL.revokeObjectURL(inputGLBurl);
inputGLBurl = URL.createObjectURL(file);
await writeGLB(await readGLB(inputGLBurl));
updateUI();
break;
}
}
});
// UI functions
function updateUI() {
if (allManifold) {
checkbox.checked = true;
checkbox.disabled = true;
} else if (anyManifold) {
checkbox.checked = false;
checkbox.disabled = false;
} else {
checkbox.checked = false;
checkbox.disabled = true;
}
onClick();
}
checkbox.onclick = onClick;
function onClick() {
(mv as any).src = checkbox.checked ? outputGLBurl : inputGLBurl;
downloadButton.disabled = !checkbox.checked;
};
downloadButton.onclick = () => {
const link = document.createElement('a');
link.download = 'manifold.glb';
link.href = outputGLBurl;
link.click();
};
// Write output glTF using gltf-transform, which contains only the meshes that
// are manifold, and using the EXT_mesh_manifold extension.
async function writeGLB(doc: Document): Promise<void> {
URL.revokeObjectURL(outputGLBurl);
if (!anyManifold) {
return;
}
const glb = await io.writeBinary(doc);
const blob = new Blob([glb], {type: 'application/octet-stream'});
outputGLBurl = URL.createObjectURL(blob);
}
// Read the glTF ObjectURL and return a gltf-transform document with all the
// non-manifold meshes stripped out.
async function readGLB(url: string): Promise<Document> {
allManifold = false;
anyManifold = false;
updateUI();
allManifold = true;
const docIn = await io.read(url);
const nodes = docIn.getRoot().listNodes();
for (const node of nodes) {
const mesh = node.getMesh();
if (!mesh) continue;
const tmp = readMesh(mesh);
if (!tmp) continue;
const id2properties = new Map<number, Properties>();
const numID = tmp.runProperties.length;
const firstID = Manifold.reserveIDs(numID);
tmp.mesh.runOriginalID = new Uint32Array(numID);
for (let i = 0; i < numID; ++i) {
tmp.mesh.runOriginalID[i] = firstID + i;
id2properties.set(firstID + i, tmp.runProperties[i]);
}
const manifoldMesh = new Mesh(tmp.mesh);
disposeMesh(mesh);
// Make the mesh manifold if it's close.
manifoldMesh.merge();
try {
// Test manifoldness - will throw if not.
const manifold = new Manifold(manifoldMesh);
// Replace the mesh with a manifold version
node.setMesh(writeMesh(docIn, manifold.getMesh(), id2properties));
manifold.delete();
anyManifold = true;
} catch (e) {
console.log(mesh.getName(), e);
allManifold = false;
}
}
// Prune the leftovers after non-manifold mesh removal.
await docIn.transform(prune());
return docIn;
}
@@ -12,15 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Extension, ExtensionProperty, PropertyType} from '@gltf-transform/core';
import {Accessor, Extension, ExtensionProperty, GLTF, IProperty, PropertyType, ReaderContext, WriterContext} from '@gltf-transform/core';
const NAME = 'EXT_manifold';
const NAME = 'EXT_mesh_manifold';
const MERGE = 'MERGE';
export class ManifoldPrimitive extends ExtensionProperty {
interface IManifoldPrimitive extends IProperty {
mergeIndices: Accessor;
mergeValues: Accessor;
indices: Accessor;
runIndex: number[]|Uint32Array;
}
interface ManifoldDef {
manifoldPrimitive: GLTF.IMeshPrimitive;
mergeIndices: number;
mergeValues: number;
}
export class ManifoldPrimitive extends ExtensionProperty<IManifoldPrimitive> {
static EXTENSION_NAME = NAME;
public declare extensionName: typeof NAME;
public declare propertyType: 'ManifoldPrimitive';
public declare parentTypes: [PropertyType.MESH];
init() {
this.EXTENSION_NAME = NAME;
ManifoldPrimitive.EXTENSION_NAME = NAME;
this.propertyType = 'ManifoldPrimitive';
this.parentTypes = [PropertyType.MESH];
}
@@ -39,7 +56,7 @@ export class ManifoldPrimitive extends ExtensionProperty {
return this.getRef('mergeValues');
}
setMerge(indicesAccessor, valuesAccessor) {
setMerge(indicesAccessor: Accessor, valuesAccessor: Accessor) {
if (indicesAccessor.getCount() !== valuesAccessor.getCount())
throw new Error('merge vectors must be the same length.');
this.setRef('mergeIndices', indicesAccessor);
@@ -50,28 +67,29 @@ export class ManifoldPrimitive extends ExtensionProperty {
return this.get('runIndex');
}
setRunIndex(runIndex) {
setRunIndex(runIndex: number[]|Uint32Array) {
return this.set('runIndex', runIndex);
}
setIndices(indices) {
setIndices(indices: Accessor) {
return this.setRef('indices', indices);
}
getIndices() {
return this.getRef('indices');
return this.getRef('indices')!;
}
}
export class EXTManifold extends Extension {
extensionName = NAME;
prewriteTypes = [PropertyType.ACCESSOR];
static EXTENSION_NAME = NAME;
createManifoldPrimitive() {
return new ManifoldPrimitive(this.document.getGraph());
}
read(context) {
read(context: ReaderContext) {
const {json} = context.jsonDoc;
const meshDefs = json.meshes || [];
@@ -82,20 +100,27 @@ export class EXTManifold extends Extension {
const manifoldPrimitive = this.createManifoldPrimitive();
mesh.setExtension(NAME, manifoldPrimitive);
const manifoldDef = meshDef.extensions[NAME];
const manifoldDef = meshDef.extensions[NAME] as ManifoldDef;
if (manifoldDef.manifoldPrimitive) {
let count = 0;
const runIndex = [];
const runIndex = Array<number>();
runIndex.push(count);
for (const primitive of mesh.listPrimitives()) {
count += primitive.getIndices().getCount();
const indices = primitive.getIndices();
if (!indices) {
console.log('Skipping non-indexed primitive ', primitive.getName());
continue;
}
count += indices.getCount();
runIndex.push(count);
}
manifoldPrimitive.setRunIndex(runIndex);
manifoldPrimitive.setIndices(
context.accessors[manifoldDef.manifoldPrimitive.indices!]);
}
if (manifoldDef.mergeIndices && manifoldDef.mergeValues) {
if (manifoldDef.mergeIndices != null && manifoldDef.mergeValues != null) {
manifoldPrimitive.setMerge(
context.accessors[manifoldDef.mergeIndices],
context.accessors[manifoldDef.mergeValues]);
@@ -105,15 +130,34 @@ export class EXTManifold extends Extension {
return this;
}
write(context) {
prewrite(context: WriterContext) {
this.document.getRoot().listMeshes().forEach((mesh) => {
const manifoldPrimitive = mesh.getExtension(NAME) as ManifoldPrimitive;
if (!manifoldPrimitive) return;
const indices = manifoldPrimitive.getIndices();
context.addAccessorToUsageGroup(
indices, WriterContext.BufferViewUsage.ELEMENT_ARRAY_BUFFER);
const mergeFrom = manifoldPrimitive.getMergeIndices();
const mergeTo = manifoldPrimitive.getMergeValues();
if (!mergeFrom || !mergeTo) return;
context.addAccessorToUsageGroup(mergeFrom, MERGE);
context.addAccessorToUsageGroup(mergeTo, MERGE);
});
return this;
}
write(context: WriterContext) {
const {json} = context.jsonDoc;
this.document.getRoot().listMeshes().forEach((mesh) => {
const manifoldPrimitive = mesh.getExtension(NAME);
const manifoldPrimitive = mesh.getExtension(NAME) as ManifoldPrimitive;
if (!manifoldPrimitive) return;
const meshIndex = context.meshIndexMap.get(mesh);
const meshDef = json.meshes[meshIndex];
const meshIndex = context.meshIndexMap.get(mesh)!;
const meshDef = json.meshes![meshIndex];
const runIndex = manifoldPrimitive.getRunIndex();
const numPrimitive = runIndex.length - 1;
@@ -124,20 +168,20 @@ export class EXTManifold extends Extension {
}
const mergeIndicesIndex =
context.accessorIndexMap.get(manifoldPrimitive.getMergeIndices());
context.accessorIndexMap.get(manifoldPrimitive.getMergeIndices()!)!;
const mergeValuesIndex =
context.accessorIndexMap.get(manifoldPrimitive.getMergeValues());
const mergeIndices = json.accessors[mergeIndicesIndex];
const mergeValues = json.accessors[mergeValuesIndex];
context.accessorIndexMap.get(manifoldPrimitive.getMergeValues()!)!;
const mergeIndices = json.accessors![mergeIndicesIndex];
const mergeValues = json.accessors![mergeValuesIndex];
const existingPrimitive = meshDef.primitives[0];
const primitive = {
indices: context.accessorIndexMap.get(manifoldPrimitive.getIndices()),
indices: context.accessorIndexMap.get(manifoldPrimitive.getIndices())!,
mode: existingPrimitive.mode,
attributes: {'POSITION': existingPrimitive.attributes['POSITION']}
};
const indices = json.accessors[primitive.indices];
const indices = json.accessors![primitive.indices];
if (!indices) {
return;
}
@@ -146,21 +190,21 @@ export class EXTManifold extends Extension {
indices.sparse = {
count: mergeIndices.count,
indices: {
bufferView: mergeIndices.bufferView,
bufferView: mergeIndices.bufferView!,
byteOffset: mergeIndices.byteOffset,
componentType: mergeIndices.componentType
},
values: {
bufferView: mergeValues.bufferView,
bufferView: mergeValues.bufferView!,
byteOffset: mergeValues.byteOffset,
}
};
}
for (let i = 0; i < numPrimitive; ++i) {
const accessor = json.accessors[meshDef.primitives[i].indices];
const accessor = json.accessors![meshDef.primitives[i].indices!];
accessor.bufferView = indices.bufferView;
accessor.byteOffset = indices.byteOffset + 4 * runIndex[i];
accessor.byteOffset = indices.byteOffset! + 4 * runIndex[i];
accessor.count = runIndex[i + 1] - runIndex[i];
}
@@ -0,0 +1,124 @@
// Copyright 2024 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Document, WebIO} from '@gltf-transform/core';
import {clearNodeTransform, flatten, prune} from '@gltf-transform/functions';
import Module, {Manifold, Mesh} from './built/manifold';
import {disposeMesh, Properties, readMesh, setupIO, writeMesh} from './gltf-io';
// Set up gltf-transform
const io = setupIO(new WebIO());
const doc = new Document();
// Set up Manifold WASM library
const wasm = await Module();
wasm.setup();
const {Manifold, Mesh} = wasm;
// Map of OriginalID to glTF material and attributes
const id2properties = new Map<number, Properties>();
// Wrapper for gltf-io readMesh() that processes a whole glTF and stores the
// properties in the above map. This function is simplified and intended only
// for reading single-object glTFs, as it simply unions any extra meshes
// together rather than returning a scene hierarchy.
async function readGLB(url: string) {
const manifolds = Array<Manifold>();
const docIn = await io.read(url);
await docIn.transform(flatten());
const nodes = docIn.getRoot().listNodes();
const ids = Array<number>();
for (const node of nodes) {
clearNodeTransform(node);
const gltfMesh = node.getMesh();
if (gltfMesh == null) continue;
const tmp = readMesh(gltfMesh);
if (tmp == null) continue;
const numID = tmp.runProperties.length;
const firstID = Manifold.reserveIDs(numID);
tmp.mesh.runOriginalID = new Uint32Array(numID);
for (let i = 0; i < numID; ++i) {
tmp.mesh.runOriginalID[i] = firstID + i;
ids.push(firstID + i);
id2properties.set(firstID + i, tmp.runProperties[i]);
}
manifolds.push(new Manifold(new Mesh(tmp.mesh)));
}
// pull in materials, TODO: replace with transfer() when available
const startIdx = doc.getRoot().listMaterials().length;
doc.merge(docIn);
doc.getRoot().listScenes().forEach((s) => s.dispose());
doc.getRoot().listBuffers().forEach((s) => s.dispose());
doc.getRoot().listAccessors().forEach((s) => s.dispose());
for (const [i, id] of ids.entries()) {
const material = doc.getRoot().listMaterials()[startIdx + i];
id2properties.get(id)!.material = material;
}
return Manifold.union(manifolds);
}
// Read static input glTFs
const space = await readGLB('/models/space.glb');
const moon = await readGLB('/models/moon.glb');
const node = doc.createNode();
doc.createScene().addChild(node);
// Set up UI for operations
type BooleanOp = 'union'|'difference'|'intersection';
function csg(operation: BooleanOp) {
push2MV(Manifold[operation](space, moon));
}
csg('difference');
const selectElement = document.querySelector('select')!;
selectElement.onchange = function() {
csg(selectElement.value as BooleanOp);
};
// The resulting glTF
let objectURL = '';
// Set up download UI
const downloadButton = document.querySelector('#download') as HTMLButtonElement;
downloadButton.onclick = function() {
const link = document.createElement('a');
link.download = 'manifold.glb';
link.href = objectURL;
link.click();
};
// <model-viewer> element for rendering resulting glTF.
const mv = document.querySelector('model-viewer');
// Use gltf-io and gltf-transform to convert the resulting Manifold to a glTF
// and display it with <model-viewer>.
async function push2MV(manifold: Manifold) {
disposeMesh(node.getMesh()!);
const mesh = writeMesh(doc, manifold.getMesh(), id2properties);
node.setMesh(mesh);
await doc.transform(prune());
const glb = await io.writeBinary(doc);
const blob = new Blob([glb], {type: 'application/octet-stream'});
URL.revokeObjectURL(objectURL);
objectURL = URL.createObjectURL(blob);
(mv as any).src = objectURL;
}
+12 -105
View File
@@ -35,117 +35,24 @@
</head>
<body>
<p>This example demonstrates feeding Manifold's output into <a
href="https://modelviewer.dev/">&lt;model-viewer&gt;</a> via <a
href="https://gltf-transform.donmccurdy.com/index.html">gltf-transform</a>. It also shows how to pass mesh
properties like UV coordinates through Manifold and how to re-associate textures after operations. A more basic
three.js example is <a href="three.html">here</a>. </p>
<p>This example demonstrates reading and writing glTF models using <a
href="https://gltf-transform.donmccurdy.com/index.html">gltf-transform</a> and using this to feed Manifold's
output into <a href="https://modelviewer.dev/">&lt;model-viewer&gt;</a>. It also shows how to pass mesh properties
like UV coordinates through Manifold and how to re-associate textures and materials after operations, made simple
using our `gltf-io.ts` library. The resulting glTF includes the EXT_mesh_manifold extension, using our
`manifold-gltf.ts` extension to gltf-transform, which ensures lossless data transfer.</p>
<p>Please open dev tools to inspect our source code - everything is source mapped back to our TS files and commented
thoroughly. A more basic three.js example is <a href="three.html">here</a>. </p>
<select>
<option value="difference" selected>Difference</option>
<option value="intersection">Intersection</option>
<option value="union">Union</option>
</select>
<button id="download">Download manifold GLB</button>
<model-viewer camera-controls shadow-intensity="1" alt="Output of mesh Boolean operation"></model-viewer>
<model-viewer camera-controls shadow-intensity="1" tone-mapping="neutral"
alt="Output of mesh Boolean operation"></model-viewer>
</body>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/2.0.1/model-viewer.min.js"></script>
<script type="module">
import { Document, WebIO } from '@gltf-transform/core';
import { clearNodeTransform, flatten, prune } from '@gltf-transform/functions';
import { loadTexture, readMesh, writeMesh, disposeMesh, setupIO } from './gltf-io.js';
import Module from './built/manifold.js';
const io = setupIO(new WebIO());
const doc = new Document();
const wasm = await Module();
wasm.setup();
const { Manifold, Mesh } = wasm;
const attributes = ['POSITION', 'TEXCOORD_0'];
const materialMap = new Map();
async function readGLB(url, attributes, materialMap) {
const manifolds = [];
const docIn = await io.read(url);
await docIn.transform(flatten());
const nodes = docIn.getRoot().listNodes();
const ids = [];
for (const node of nodes) {
clearNodeTransform(node);
const materials = [];
const manifoldMesh = readMesh(node.getMesh(), attributes, materials);
manifoldMesh.runOriginalID = [];
const firstID = Manifold.reserveIDs(materials.length);
for (let i = 0; i < materials.length; ++i) {
manifoldMesh.runOriginalID.push(firstID + i);
ids.push(firstID + i);
}
manifolds.push(Manifold(new Mesh(manifoldMesh)));
}
// pull in materials, TODO: replace with transfer() when available
const startIdx = doc.getRoot().listMaterials().length;
doc.merge(docIn);
doc.getRoot().listScenes().forEach((s) => s.dispose());
doc.getRoot().listBuffers().forEach((s) => s.dispose());
for (const [i, id] of ids.entries()) {
const material = doc.getRoot().listMaterials()[startIdx + i];
materialMap.set(id, material);
}
return Manifold.union(manifolds);
}
const space = await readGLB('/models/space.glb', attributes, materialMap);
const moon = await readGLB('/models/moon.glb', attributes, materialMap);
const node = doc.createNode();
doc.createScene().addChild(node);
function csg(operation) {
push2MV(Manifold[operation](space, moon));
};
document.querySelector('select').onchange = function (event) {
csg(event.target.value);
};
csg('difference');
const mv = document.querySelector('model-viewer');
let objectURL = null;
const downloadButton = document.querySelector('#download');
downloadButton.onclick = function () {
const link = document.createElement('a');
link.download = 'manifold.glb';
link.href = objectURL;
link.click();
};
async function push2MV(manifold) {
// From Z-up to Y-up (glTF)
const manifoldMesh = manifold.rotate([-90, 0, 0]).getMesh();
disposeMesh(node.getMesh());
const materials = [];
const matAttributes = [];
for (const id of manifoldMesh.runOriginalID) {
materials.push(materialMap.get(id));
matAttributes.push(attributes);
}
const mesh = writeMesh(doc, manifoldMesh, matAttributes, materials);
node.setMesh(mesh);
await doc.transform(prune());
const glb = await io.writeBinary(doc);
const blob = new Blob([glb], { type: 'application/octet-stream' });
URL.revokeObjectURL(objectURL);
objectURL = URL.createObjectURL(blob);
mv.src = objectURL;
}
</script>
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
<script type="module" src="model-viewer-script.ts"></script>
</html>
+2284 -1018
View File
File diff suppressed because it is too large Load Diff
+14 -12
View File
@@ -8,23 +8,25 @@
"build": "tsc --noEmit && vite build",
"build:watch": "vite build --watch",
"preview": "vite preview",
"test": "vitest",
"test": "vitest run",
"test:ui": "vitest --ui",
"test:run": "vitest run"
"test:dev": "vitest"
},
"dependencies": {
"@gltf-transform/core": "^3.2.1",
"@gltf-transform/extensions": "^3.2.1",
"@gltf-transform/functions": "^3.2.1",
"@gltf-transform/core": "^3.8.0",
"@gltf-transform/extensions": "^3.8.0",
"@gltf-transform/functions": "^3.8.0",
"@jscadui/3mf-export": "^0.5.0",
"@types/three": "^0.164.0",
"fflate": "^0.8.0",
"gl-matrix": "^3.4.3",
"simple-dropzone": "0.8.3",
"three": "0.151.2"
"three": "0.164.1"
},
"devDependencies": {
"@vitest/ui": "^0.31.1",
"@vitest/web-worker": "^0.31.1",
"typescript": "5.0.4",
"vite": "^4.3.2",
"vitest": "^0.31.1"
"@vitest/ui": "^1.6.0",
"typescript": "5.2.2",
"vite": "^4.5.0",
"vitest": "^1.6.0"
}
}
}
+42 -6
View File
@@ -25,8 +25,11 @@ declare class GLTFNode {
clone(parent?: GLTFNode): GLTFNode;
}
type Attribute = 'POSITION'|'NORMAL'|'TANGENT'|'TEXCOORD_0'|'TEXCOORD_1'|
'COLOR_0'|'JOINTS_0'|'WEIGHTS_0'|'SKIP_1'|'SKIP_2'|'SKIP_3'|'SKIP_4';
declare class GLTFMaterial {
attributes?: string[];
attributes?: Attribute[];
roughness?: number;
metallic?: number;
baseColorFactor?: [number, number, number];
@@ -35,15 +38,48 @@ declare class GLTFMaterial {
name?: string;
}
declare const globalDefaults: {
roughness: number,
metallic: number,
baseColorFactor: [number, number, number],
alpha: number,
unlit: boolean,
animationLength: number,
animationMode: 'loop'|'ping-pong';
}
/**
* Set material properties on the input manifold. They will be carried along
* through operations.
* Returns a shallow copy of the input manifold with the given material
* properties applied. They will be carried along through operations.
*
* @param manifold The object to add properties to - returned for chaining.
* @param manifold The input object.
* @param material A set of material properties to apply to this manifold.
*/
declare function setMaterial(
manifold: Manifold, material: GLTFMaterial): Manifold;
declare function setMaterial(manifold: Manifold, material: GLTFMaterial):
Manifold;
/**
* Apply a morphing animation to the input manifold. Specify the start
* function which will be applied to the vertex positions of the first frame and
* linearly interpolated across the length of the overall animation. This
* animation will only be shown if this manifold is used directly on a GLTFNode.
*
* @param manifold The object to add morphing animation to.
* @param func A warping function to apply to the first animation frame.
*/
declare function setMorphStart(
manifold: Manifold, func: (v: Vec3) => void): void;
/**
* Apply a morphing animation to the input manifold. Specify the end
* function which will be applied to the vertex positions of the last frame and
* linearly interpolated across the length of the overall animation. This
* animation will only be shown if this manifold is used directly on a GLTFNode.
*
* @param manifold The object to add morphing animation to.
* @param func A warping function to apply to the last animation frame.
*/
declare function setMorphEnd(manifold: Manifold, func: (v: Vec3) => void): void;
/**
* Wrap any shape object with this method to display it and any copies in
+134 -91
View File
@@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/** @type {import("../../manifold-encapsulated-types")["Manifold"]} */
const Manifold = globalThis.Manifold;
export const examples = {
functions: {
Intro: function() {
@@ -50,9 +53,9 @@ export const examples = {
TetrahedronPuzzle: function() {
// A tetrahedron cut into two identical halves that can screw together as
// a puzzle. This only outputs one of the halves. This demonstrates how
// redundant points along a polygon can be used to make twisted extrusions
// smoother. Based on the screw puzzle by George Hart:
// a puzzle. This demonstrates how redundant points along a polygon can be
// used to make twisted extrusions smoother. It also showcases animated
// assemblies. Based on the screw puzzle by George Hart:
// https://www.thingiverse.com/thing:186372
const edgeLength = 50; // Length of each edge of the overall tetrahedron.
@@ -61,19 +64,21 @@ export const examples = {
const scale = edgeLength / (2 * Math.sqrt(2));
const tet = Manifold.tetrahedron().scale(scale);
const tet = Manifold.tetrahedron().intersect(
Manifold.tetrahedron().rotate([0, 0, 90]).scale(2.5));
const box = [];
box.push([1, -1], [1, 1]);
box.push([2, -2], [2, 2]);
for (let i = 0; i <= nDivisions; ++i) {
box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]);
box.push([gap / (2 * scale), 2 - i * 4 / nDivisions]);
}
const cyan = [0, 1, 1];
const magenta = [1, 0, 1];
const fade = (color, pos) => {
for (let i = 0; i < 3; ++i) {
color[i] = cyan[i] * pos[2] + magenta[i] * (1 - pos[2]);
const x = pos[2] / 2;
color[i] = cyan[i] * x + magenta[i] * (1 - x);
}
};
@@ -82,25 +87,43 @@ export const examples = {
// channels as colors, and sets the factor to white, since our default is
// yellow.
const screw = setMaterial(
Manifold.extrude(box, 1, nDivisions, 270).setProperties(3, fade),
Manifold.extrude(box, 2, nDivisions, 270).setProperties(3, fade),
{baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']});
const result = tet.intersect(
screw.rotate([0, 0, -45]).translate([0, 0, -0.5]).scale(2 * scale));
const result =
tet.intersect(screw.rotate([0, 0, -45]).translate([0, 0, -1]))
.scale(scale);
// Assigned materials are only applied to a GLTFNode. Note that material
// definitions cascade, applying recursively to all child surfaces, but
// overridden by any materials defined lower down. Our default material:
// {
// roughness = 0.2,
// metallic = 1,
// baseColorFactor = [1, 1, 0],
// alpha = 1,
// unlit = false,
// name = ''
// }
const node = new GLTFNode();
node.manifold = result;
// overridden by any materials defined lower down. The default material
// properties, as well as animation parameters can be set via
// globalDefaults.
const layFlat = new GLTFNode();
layFlat.rotation =
[45, -Math.atan(1 / Math.sqrt(2)) * 180 / Math.PI, 120];
layFlat.translation = [0, 0, scale * Math.sqrt(3) / 3];
const fixed = new GLTFNode(layFlat);
fixed.manifold = result;
fixed.rotation = [0, 0, 180];
// For 3MF export, only top-level objects are independently arrangeable.
const layFlat2 = layFlat.clone();
const moving = new GLTFNode(layFlat2);
moving.manifold = result;
// Use functions to create animation, which runs from t=0 to t=1.
moving.translation = (t) => {
const a = 1 - t;
const x = a > 0.5 ? scale * 2 * (0.5 - a) : 0;
return [x, x, -2 * x + scale * 4 * (0.5 - Math.abs(0.5 - a))];
};
moving.rotation = (t) => [0, 0, 270 * 2 * (1 - t)];
globalDefaults.animationLength = 10; // seconds
globalDefaults.animationMode = 'ping-pong';
return result;
},
@@ -108,7 +131,7 @@ export const examples = {
// Demonstrates how at 90-degree intersections, the sphere and cylinder
// facets match up perfectly, for any choice of global resolution
// parameters.
const {sphere, cylinder, union} = Manifold;
const {sphere, cylinder, union, cube} = Manifold;
function roundedFrame(edgeLength, radius, circularSegments = 0) {
const edge = cylinder(edgeLength, radius, -1, circularSegments);
@@ -132,6 +155,17 @@ export const examples = {
setMinCircularAngle(3);
setMinCircularEdgeLength(0.5);
const result = roundedFrame(100, 10);
// Demonstrate how you can use the .split method to perform
// a subtraction and an intersection at once
const [inside, outside] = result.split(cube(100, true));
const outsideNode = new GLTFNode();
outsideNode.manifold = outside;
const insideNode = new GLTFNode();
insideNode.manifold = inside;
insideNode.material = {baseColorFactor: [0, 1, 1]};
return result;
},
@@ -139,6 +173,7 @@ export const examples = {
// Smooth, complex manifolds can be created using the warp() function.
// This example recreates the Exploitable Heart by Emmett Lalish:
// https://www.thingiverse.com/thing:6190
// It also demonstrates the use of setMorph to animate a warping function.
const func = (v) => {
const x2 = v[0] * v[0];
@@ -174,9 +209,20 @@ export const examples = {
};
const ball = Manifold.sphere(1, 200);
const heart = ball.warp(func);
const box = heart.boundingBox();
const result = heart.scale(100 / (box.max[0] - box.min[0]));
const scale = 100 / (box.max[0] - box.min[0]);
setMorphStart(ball, func);
const node = new GLTFNode();
node.manifold = ball;
node.scale = [scale, scale, scale];
globalDefaults.animationLength = 5; // seconds
globalDefaults.animationMode = 'ping-pong';
const result = heart.scale(scale);
return result;
},
@@ -253,71 +299,68 @@ export const examples = {
// an example of using the warp() method, thus avoiding any direct
// handling of triangles.
// @param p The number of times the thread passes through the donut hole.
// @param q The number of times the thread circles the donut.
// @param majorRadius Radius of the interior of the imaginary donut.
// @param minorRadius Radius of the small cross-section of the imaginary
// donut.
// @param threadRadius Radius of the small cross-section of the actual
// object.
// @param circularSegments Number of linear segments making up the
// threadRadius circle. Default is getCircularSegments(threadRadius).
// @param linearSegments Number of segments along the length of the knot.
// Default makes roughly square facets.
// The number of times the thread passes through the donut hole.
const p = 1;
// The number of times the thread circles the donut.
const q = 3;
// Radius of the interior of the imaginary donut.
const majorRadius = 25;
// Radius of the small cross-section of the imaginary donut.
const minorRadius = 10;
// Radius of the small cross-section of the actual object.
const threadRadius = 3.75;
// Number of linear segments making up the threadRadius circle. Default is
// getCircularSegments(threadRadius).
const circularSegments = -1;
// Number of segments along the length of the knot. Default makes roughly
// square facets.
const linearSegments = -1;
function torusKnot(
p, q, majorRadius, minorRadius, threadRadius, circularSegments = 0,
linearSegments = 0) {
const {vec3} = glMatrix;
function gcd(a, b) {
return b == 0 ? a : gcd(b, a % b);
}
const kLoops = gcd(p, q);
p /= kLoops;
q /= kLoops;
const n = circularSegments > 2 ? circularSegments :
getCircularSegments(threadRadius);
const m = linearSegments > 2 ? linearSegments :
n * q * majorRadius / threadRadius;
const offset = 2
const circle = CrossSection.circle(1, n).translate([offset, 0]);
const func = (v) => {
const psi = q * Math.atan2(v[0], v[1]);
const theta = psi * p / q;
const x1 = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
const phi = Math.atan2(x1 - offset, v[2]);
vec3.set(
v, threadRadius * Math.cos(phi), 0, threadRadius * Math.sin(phi));
const center = vec3.fromValues(0, 0, 0);
const r = majorRadius + minorRadius * Math.cos(theta);
vec3.rotateX(v, v, center, -Math.atan2(p * minorRadius, q * r));
v[0] += minorRadius;
vec3.rotateY(v, v, center, theta);
v[0] += majorRadius;
vec3.rotateZ(v, v, center, psi);
};
let knot = Manifold.revolve(circle, m).warp(func);
if (kLoops > 1) {
const knots = [];
for (let k = 0; k < kLoops; ++k) {
knots.push(knot.rotate([0, 0, 360 * (k / kLoops) * (q / p)]));
}
knot = Manifold.compose(knots);
}
return knot;
}
// This recreates Matlab Knot by Emmett Lalish:
// These default values recreate Matlab Knot by Emmett Lalish:
// https://www.thingiverse.com/thing:7080
const result = torusKnot(1, 3, 25, 10, 3.75);
const {vec3} = glMatrix;
function gcd(a, b) {
return b == 0 ? a : gcd(b, a % b);
}
const kLoops = gcd(p, q);
const pk = p / kLoops;
const qk = q / kLoops;
const n = circularSegments > 2 ? circularSegments :
getCircularSegments(threadRadius);
const m = linearSegments > 2 ? linearSegments :
n * qk * majorRadius / threadRadius;
const offset = 2
const circle = CrossSection.circle(1, n).translate([offset, 0]);
const func = (v) => {
const psi = qk * Math.atan2(v[0], v[1]);
const theta = psi * pk / qk;
const x1 = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
const phi = Math.atan2(x1 - offset, v[2]);
vec3.set(
v, threadRadius * Math.cos(phi), 0, threadRadius * Math.sin(phi));
const center = vec3.fromValues(0, 0, 0);
const r = majorRadius + minorRadius * Math.cos(theta);
vec3.rotateX(v, v, center, -Math.atan2(pk * minorRadius, qk * r));
v[0] += minorRadius;
vec3.rotateY(v, v, center, theta);
v[0] += majorRadius;
vec3.rotateZ(v, v, center, psi);
};
const result = Manifold.revolve(circle, m).warp(func);
for (let k = 0; k < kLoops; ++k) {
const node = new GLTFNode();
node.manifold = result;
node.rotation = [0, 0, 360 * (k / kLoops) * (qk / pk)];
node.translation = [0, 0, result.boundingBox().min[2]];
}
return result;
},
@@ -350,12 +393,12 @@ export const examples = {
const hole = Manifold.compose(holes);
result = Manifold.difference(
result,
hole,
hole.rotate([90, 0, 0]),
hole.rotate([0, 90, 0]),
);
result = Manifold.difference([
result,
hole,
hole.rotate([90, 0, 0]),
hole.rotate([0, 90, 0]),
]);
return result;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
declare module 'simple-dropzone';
+11 -73
View File
@@ -35,83 +35,21 @@
</head>
<body>
<p>This demonstrates the minimum code to connect Manifold to a three.js rendering. A slightly more complex example
involving GLTF export and &lt;model-viewer&gt; can be found <a href="model-viewer.html">here</a>. </p>
<p>This example demonstrates interop between Manifold and Three.js with minimal code - please open dev tools to
inspect the source. Here we generate two multi-material Three.js meshes and convert them to Manifolds while building
a mapping from material to Manifold ID. Then Boolean operations are performed on the Manifolds and the result is
converted back to a Three.js mesh using the material mapping. The input cube has half red faces and half
normal-shaded, while the icosahedron has half blue faces and half normal-shaded. The resulting mesh has three
materials, since one (normal-shaded) was common between the input models.</p>
<p>A slightly more complex example involving our library for GLTF import/export and &lt;model-viewer&gt; can be found
<a href="model-viewer.html">here</a>. </p>
<select>
<option value="difference" selected>Difference</option>
<option value="union" selected>Union</option>
<option value="difference">Difference</option>
<option value="intersection">Intersection</option>
<option value="union">Union</option>
</select><br />
<canvas id="output"></canvas>
</body>
<script type="module">
import * as THREE from 'three';
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import Module from './built/manifold.js';
const wasm = await Module();
wasm.setup();
const { Manifold, Mesh } = wasm;
// we have manifold module, let's do some three.js
const camera = new THREE.PerspectiveCamera(30, 1, 0.01, 10);
camera.position.z = 1;
const scene = new THREE.Scene();
const mesh = new THREE.Mesh(undefined, new THREE.MeshNormalMaterial({
flatShading: true
}));
scene.add(mesh);
const icosahedron = simplify(new THREE.IcosahedronGeometry(0.16));
const manifold_1 = Manifold.cube([0.2, 0.2, 0.2], true);
const manifold_2 = new Manifold(geometry2mesh(icosahedron));
const csg = function (operation) {
mesh.geometry?.dispose();
mesh.geometry = mesh2geometry(Manifold[operation](manifold_1, manifold_2).getMesh());
};
document.querySelector('select').onchange = function (event) {
csg(event.target.value);
};
csg('difference');
const output = document.querySelector("#output");
const renderer = new THREE.WebGLRenderer({ canvas: output, antialias: true });
const dim = output.getBoundingClientRect();
renderer.setSize(dim.width, dim.height);
renderer.setAnimationLoop(function (time) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render(scene, camera);
});
// functions to convert between three.js and wasm
function geometry2mesh(geometry) {
const vertProperties = geometry.attributes.position.array;
const triVerts = geometry.index.array;
return new Mesh({ numProp: 3, vertProperties, triVerts });
}
function mesh2geometry(mesh) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(mesh.vertProperties, 3));
geometry.setIndex(new THREE.BufferAttribute(mesh.triVerts, 1));
return geometry;
}
// most of three.js geometries aren't manifolds, so...
function simplify(geometry) {
delete geometry.attributes.normal;
delete geometry.attributes.uv;
const simplified = mergeVertices(geometry);
simplified.computeVertexNormals();
return simplified;
}
</script>
<script type="module" src="three.ts"></script>
</html>
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2024 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {BoxGeometry, BufferAttribute, BufferGeometry, IcosahedronGeometry, Mesh as ThreeMesh, MeshLambertMaterial, MeshNormalMaterial, PerspectiveCamera, PointLight, Scene, WebGLRenderer} from 'three';
import Module, {Mesh} from './built/manifold';
// Load Manifold WASM library
const wasm = await Module();
wasm.setup();
const {Manifold, Mesh} = wasm;
// Define our set of materials
const materials = [
new MeshNormalMaterial({flatShading: true}),
new MeshLambertMaterial({color: 'red', flatShading: true}),
new MeshLambertMaterial({color: 'blue', flatShading: true})
];
const result = new ThreeMesh(undefined, materials);
// Set up Manifold IDs corresponding to materials
const firstID = Manifold.reserveIDs(materials.length);
// ids vector is parallel to materials vector - same indexing
const ids = [...Array<number>(materials.length)].map((_, idx) => firstID + idx);
// Build a mapping to get back from ID to material index
const id2matIndex = new Map();
ids.forEach((id, idx) => id2matIndex.set(id, idx));
// Set up Three.js scene
const scene = new Scene();
const camera = new PerspectiveCamera(30, 1, 0.01, 10);
camera.position.z = 1;
camera.add(new PointLight(0xffffff, 1));
scene.add(camera);
scene.add(result);
// Set up Three.js renderer
const output = document.querySelector('#output')!;
const renderer = new WebGLRenderer({canvas: output, antialias: true});
const dim = output.getBoundingClientRect();
renderer.setSize(dim.width, dim.height);
renderer.setAnimationLoop(function(time: number) {
result.rotation.x = time / 2000;
result.rotation.y = time / 1000;
renderer.render(scene, camera);
});
// Create input meshes in Three.js
const cube = new BoxGeometry(0.2, 0.2, 0.2);
cube.clearGroups();
cube.addGroup(0, 18, 0); // First 6 faces colored by normal
cube.addGroup(18, Infinity, 1); // Rest of faces are red
const icosahedron = new IcosahedronGeometry(0.16);
icosahedron.clearGroups();
icosahedron.addGroup(30, Infinity, 2); // Last faces are blue
icosahedron.addGroup(0, 30, 0); // First 10 faces colored by normal
// The above groups are in reversed order to demonstrate the need for sorting.
// Convert Three.js input meshes to Manifolds
const manifoldCube = new Manifold(geometry2mesh(cube));
const manifoldIcosahedron = new Manifold(geometry2mesh(icosahedron));
// Set up UI for operations
type BooleanOp = 'union'|'difference'|'intersection';
function csg(operation: BooleanOp) {
result.geometry?.dispose();
result.geometry = mesh2geometry(
Manifold[operation](manifoldCube, manifoldIcosahedron).getMesh());
}
csg('union');
const selectElement = document.querySelector('select')!;
selectElement.onchange = function() {
csg(selectElement.value as BooleanOp);
};
// Convert Three.js BufferGeometry to Manifold Mesh
function geometry2mesh(geometry: BufferGeometry) {
// Only using position in this sample for simplicity. Can interleave any other
// desired attributes here such as UV, normal, etc.
const vertProperties = geometry.attributes.position.array as Float32Array;
// Manifold only uses indexed geometry, so generate an index if necessary.
const triVerts = geometry.index != null ?
geometry.index.array as Uint32Array :
new Uint32Array(vertProperties.length / 3).map((_, idx) => idx);
// Create a triangle run for each group (material) - akin to a draw call.
const starts = [...Array(geometry.groups.length)].map(
(_, idx) => geometry.groups[idx].start);
// Map the materials to ID.
const originalIDs = [...Array(geometry.groups.length)].map(
(_, idx) => ids[geometry.groups[idx].materialIndex!]);
// List the runs in sequence.
const indices = Array.from(starts.keys())
indices.sort((a, b) => starts[a] - starts[b])
const runIndex = new Uint32Array(indices.map(i => starts[i]));
const runOriginalID = new Uint32Array(indices.map(i => originalIDs[i]));
// Create the MeshGL for I/O with Manifold library.
const mesh =
new Mesh({numProp: 3, vertProperties, triVerts, runIndex, runOriginalID});
// Automatically merge vertices with nearly identical positions to create a
// Manifold. This only fills in the mergeFromVert and mergeToVert vectors -
// these are automatically filled in for any mesh returned by Manifold. These
// are necessary because GL drivers require duplicate verts when any
// properties change, e.g. a UV boundary or sharp corner.
mesh.merge();
return mesh;
}
// Convert Manifold Mesh to Three.js BufferGeometry
function mesh2geometry(mesh: Mesh) {
const geometry = new BufferGeometry();
// Assign buffers
geometry.setAttribute(
'position', new BufferAttribute(mesh.vertProperties, 3));
geometry.setIndex(new BufferAttribute(mesh.triVerts, 1));
// Create a group (material) for each ID. Note that there may be multiple
// triangle runs returned with the same ID, though these will always be
// sequential since they are sorted by ID. In this example there are two runs
// for the MeshNormalMaterial, one corresponding to each input mesh that had
// this ID. This allows runTransform to return the total transformation matrix
// applied to each triangle run from its input mesh - even after many
// consecutive operations.
let id = mesh.runOriginalID[0];
let start = mesh.runIndex[0];
for (let run = 0; run < mesh.numRun; ++run) {
const nextID = mesh.runOriginalID[run + 1];
if (nextID !== id) {
const end = mesh.runIndex[run + 1];
geometry.addGroup(start, end - start, id2matIndex.get(id));
id = nextID;
start = end;
}
}
return geometry;
}
+1 -1
View File
@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "node",
"moduleResolution": "Bundler",
"lib": [
"es2022",
"dom"
+1
View File
@@ -9,6 +9,7 @@ export default defineConfig({
},
build: {
target: 'esnext',
sourcemap: true,
rollupOptions: {
input: {
manifoldCAD: resolve(__dirname, 'index.html'),
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2024 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {cleanup, evaluateCADToModel} from './worker';
// Setup complete
self.postMessage(null);
if (self.console) {
const oldLog = self.console.log;
self.console.log = function(...args) {
let message = '';
for (const arg of args) {
if (arg == null) {
message += 'undefined';
} else if (typeof arg == 'object') {
message += JSON.stringify(arg, null, 4);
} else {
message += arg.toString();
}
}
self.postMessage({log: message});
oldLog(...args);
};
}
self.onmessage = async (e) => {
try {
const result = await evaluateCADToModel(e.data);
self.postMessage(result);
} catch (error: any) {
console.log(error.toString());
self.postMessage({objectURL: null});
} finally {
cleanup();
}
};
@@ -12,79 +12,44 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import '@vitest/web-worker';
import {WebIO} from '@gltf-transform/core';
import {expect, suite, test} from 'vitest';
import assert from 'node:assert';
import {afterEach, expect, suite, test} from 'vitest';
import Module from './built/manifold.js';
import {readMesh, setupIO} from './gltf-io.js';
import {readMesh, setupIO} from './gltf-io';
// @ts-ignore
import {examples} from './public/examples.js';
import ManifoldWorker from './worker?worker';
import {Mesh} from './public/manifold';
import {cleanup, evaluateCADToModel, module} from './worker';
const io = setupIO(new WebIO());
const wasm = await Module();
wasm.setup();
function initialized(worker) {
return new Promise((resolve) => {
worker.onmessage = function(e) {
if (e.data == null) {
resolve();
} else {
reject();
}
};
});
async function runExample(name: string) {
const code = examples.functionBodies.get(name);
const result = await evaluateCADToModel(code);
cleanup();
assert.ok(result?.glbURL);
const docIn = await io.read(result.glbURL);
URL.revokeObjectURL(result.glbURL);
const nodes = docIn.getRoot().listNodes();
for (const node of nodes) {
const docMesh = node.getMesh();
if (!docMesh) {
continue;
}
const {mesh} = readMesh(docMesh)!;
const manifold = new module.Manifold(mesh as Mesh);
const prop = manifold.getProperties();
const genus = manifold.genus();
manifold.delete();
return {...prop, genus};
}
assert.ok(false);
}
let 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));
});
}
// allow vitest to report progress after each test
// before going into heavy computation which blocks main thread
afterEach(async () => {await new Promise(resolve => setTimeout(resolve, 500))})
suite('Examples', () => {
test('Intro', async () => {
@@ -97,29 +62,29 @@ suite('Examples', () => {
test('Tetrahedron Puzzle', async () => {
const result = await runExample('Tetrahedron Puzzle');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(7297, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(3303, 1, 'Surface Area');
expect(result.volume).to.be.closeTo(7240, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(3235, 1, 'Surface Area');
});
test('Rounded Frame', async () => {
const result = await runExample('Rounded Frame');
expect(result.genus).to.equal(5, 'Genus');
expect(result.volume).to.be.closeTo(353706, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(68454, 1, 'Surface Area');
expect(result.volume).to.be.closeTo(270807, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(74599, 1, 'Surface Area');
});
test('Heart', async () => {
const result = await runExample('Heart');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(282743, 10, 'Volume');
expect(result.surfaceArea).to.be.closeTo(22187, 1, 'Surface Area');
expect(result.volume).to.be.closeTo(3.342, 0.001, 'Volume');
expect(result.surfaceArea).to.be.closeTo(11.51, 0.01, 'Surface Area');
});
test('Scallop', async () => {
const result = await runExample('Scallop');
expect(result.genus).to.equal(0, 'Genus');
expect(result.volume).to.be.closeTo(41284, 1, 'Volume');
expect(result.surfaceArea).to.be.closeTo(7810, 1, 'Surface Area');
expect(result.volume).to.be.closeTo(39900, 100, 'Volume');
expect(result.surfaceArea).to.be.closeTo(7930, 10, 'Surface Area');
});
test('Torus Knot', async () => {
+457 -133
View File
@@ -12,25 +12,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Document, Material, Node, WebIO} from '@gltf-transform/core';
import {Accessor, Animation, AnimationSampler, Document, mat4, Material, Mesh as GLTFMesh, Node, WebIO} from '@gltf-transform/core';
import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
import {fileForContentTypes, FileForRelThumbnail, to3dmodel} from '@jscadui/3mf-export';
import {strToU8, Zippable, zipSync} from 'fflate'
import * as glMatrix from 'gl-matrix';
import Module from './built/manifold';
//@ts-ignore
import {setupIO, writeMesh} from './gltf-io';
import type {GLTFMaterial, Quat} from './public/editor';
import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './public/manifold';
import Module, {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './built/manifold';
import {Properties, setupIO, writeMesh} from './gltf-io';
import {GLTFMaterial, Quat} from './public/editor';
interface GlobalDefaults {
roughness: number;
metallic: number;
baseColorFactor: [number, number, number];
alpha: number;
unlit: boolean;
animationLength: number;
animationMode: 'loop'|'ping-pong';
}
interface WorkerStatic extends ManifoldToplevel {
GLTFNode: typeof GLTFNode;
show(manifold: Manifold): Manifold;
only(manifold: Manifold): Manifold;
setMaterial(manifold: Manifold, material: GLTFMaterial): void;
setMaterial(manifold: Manifold, material: GLTFMaterial): Manifold;
setMorphStart(manifold: Manifold, func: (v: Vec3) => void): void;
setMorphEnd(manifold: Manifold, func: (v: Vec3) => void): void;
cleanup(): void;
}
const module = await Module() as unknown as WorkerStatic;
export const module = await Module() as WorkerStatic;
module.setup();
// Faster on modern browsers than Float32Array
@@ -42,29 +54,51 @@ io.registerExtensions(KHRONOS_EXTENSIONS);
// manifold static methods (that return a new manifold)
const manifoldStaticFunctions = [
'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'compose',
'union', 'difference', 'intersection', 'levelSet', 'smooth', 'ofMesh'
'union', 'difference', 'intersection', 'levelSet', 'smooth', 'ofMesh', 'hull'
];
// manifold member functions (that return a new manifold)
const manifoldMemberFunctions = [
'add', 'subtract', 'intersect', 'decompose', 'warp', 'transform', 'translate',
'rotate', 'scale', 'mirror', 'refine', 'setProperties', 'asOriginal',
'trimByPlane', 'split', 'splitByPlane'
'add',
'subtract',
'intersect',
'decompose',
'warp',
'transform',
'translate',
'rotate',
'scale',
'mirror',
'calculateCurvature',
'calculateNormals',
'smoothByNormals',
'smoothOut',
'refine',
'refineToLength',
'setProperties',
'asOriginal',
'trimByPlane',
'split',
'splitByPlane',
'slice',
'project',
'hull'
];
// CrossSection static methods (that return a new cross-section)
const crossSectionStaticFunctions = [
'square', 'circle', 'union', 'difference', 'intersection', 'compose',
'ofPolygons'
'ofPolygons', 'hull'
];
// CrossSection member functions (that return a new cross-section)
const crossSectionMemberFunctions = [
'add', 'subtract', 'intersect', 'rectClip', 'decompose', 'transform',
'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset'
'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset', 'hull'
];
// top level functions that construct a new manifold/mesh
const toplevelConstructors = ['show', 'only', 'setMaterial'];
const toplevel = [
'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments',
'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection'
'getCircularSegments', 'resetToCircularDefaults', 'Mesh', 'GLTFNode',
'Manifold', 'CrossSection', 'setMorphStart', 'setMorphEnd'
];
const exposedFunctions = toplevelConstructors.concat(toplevel);
@@ -81,15 +115,13 @@ function addMembers(
const cls = module[className];
const obj = areStatic ? cls : cls.prototype;
for (const name of methodNames) {
if (name != 'cylinder') {
const originalFn = obj[name];
obj[name] = function(...args: any) {
//@ts-ignore
const result = originalFn(...args);
memoryRegistry.push(result);
return result;
};
}
const originalFn = obj[name];
obj[name] = function(...args: any) {
//@ts-ignore
const result = originalFn(...args);
memoryRegistry.push(result);
return result;
};
}
}
@@ -109,22 +141,23 @@ for (const name of toplevelConstructors) {
};
}
module.cleanup = function() {
for (const obj of memoryRegistry) {
// decompose result is an array of manifolds
if (obj instanceof Array)
for (const elem of obj) elem.delete();
else
obj.delete();
}
memoryRegistry.length = 0;
};
// Debug setup to show source meshes
let ghost = false;
const shown = new Map<number, Mesh>();
const singles = new Map<number, Mesh>();
const FPS = 30;
const GLOBAL_DEFAULTS = {
roughness: 0.2,
metallic: 1,
baseColorFactor: [1, 1, 0] as [number, number, number],
alpha: 1,
unlit: false,
animationLength: 1,
animationMode: 'loop'
};
const SHOW = {
baseColorFactor: [1, 0, 0],
alpha: 0.25,
@@ -142,22 +175,85 @@ const GHOST = {
const nodes = new Array<GLTFNode>();
const id2material = new Map<number, GLTFMaterial>();
const materialCache = new Map<GLTFMaterial, Material>();
const object2globalID = new Map<GLTFNode|Manifold, number>();
const manifold2morph = new Map<Manifold, Morph>();
// lib3mf doesn't like objectid=0
let nextGlobalID = 1;
let animation: Animation;
let timesAccessor: Accessor;
let weightsAccessor: Accessor;
let weightsSampler: AnimationSampler;
let hasAnimation: boolean;
export function cleanup() {
for (const obj of memoryRegistry) {
// decompose result is an array of manifolds
if (obj instanceof Array)
for (const elem of obj) elem.delete();
else
obj.delete();
}
memoryRegistry.length = 0;
function cleanup() {
ghost = false;
shown.clear();
singles.clear();
nodes.length = 0;
id2material.clear();
materialCache.clear();
object2globalID.clear();
manifold2morph.clear();
nextGlobalID = 1;
}
interface Morph {
start?: (v: Vec3) => void;
end?: (v: Vec3) => void;
}
interface Mesh3MF {
id: string;
vertices: Float32Array;
indices: Uint32Array;
name?: string;
}
interface Child3MF {
objectID: string;
transform?: mat4;
}
interface Component3MF {
id: string;
children: Array<Child3MF>;
name?: string;
}
interface Header {
unit?: 'micron'|'millimeter'|'centimeter'|'inch'|'foot'|'meter';
title?: string;
author?: string;
description?: string;
application?: string;
creationDate?: string;
license?: string;
modificationDate?: string;
}
interface To3MF {
meshes: Array<Mesh3MF>;
components: Array<Component3MF>;
items: Array<Child3MF>;
precision: number;
header: Header;
}
class GLTFNode {
private _parent?: GLTFNode;
manifold?: Manifold;
translation?: Vec3;
rotation?: Vec3;
scale?: Vec3;
translation?: Vec3|((t: number) => Vec3);
rotation?: Vec3|((t: number) => Vec3);
scale?: Vec3|((t: number) => Vec3);
material?: GLTFMaterial;
name?: string;
@@ -178,12 +274,32 @@ class GLTFNode {
module.GLTFNode = GLTFNode;
const globalDefaults = {...GLOBAL_DEFAULTS};
module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => {
const out = manifold.asOriginal();
id2material.set(out.originalID(), material);
return out;
};
module.setMorphStart = (manifold: Manifold, func: (v: Vec3) => void): void => {
const morph = manifold2morph.get(manifold);
if (morph != null) {
morph.start = func;
} else {
manifold2morph.set(manifold, {start: func});
}
};
module.setMorphEnd = (manifold: Manifold, func: (v: Vec3) => void): void => {
const morph = manifold2morph.get(manifold);
if (morph != null) {
morph.end = func;
} else {
manifold2morph.set(manifold, {end: func});
}
};
function debug(manifold: Manifold, map: Map<number, Mesh>) {
let result = manifold.asOriginal();
map.set(result.originalID(), result.getMesh());
@@ -199,69 +315,153 @@ module.only = (manifold) => {
return debug(manifold, singles);
};
// Setup complete
self.postMessage(null);
if (self.console) {
const oldLog = self.console.log;
self.console.log = function(...args) {
let message = '';
for (const arg of args) {
if (arg == null) {
message += 'undefined';
} else if (typeof arg == 'object') {
message += JSON.stringify(arg, null, 4);
} else {
message += arg.toString();
}
}
self.postMessage({log: message});
oldLog(...args);
};
}
// Swallow informational logs in testing framework
function log(...args: any[]) {
if (self.console) {
if (typeof self !== 'undefined' && self.console) {
self.console.log(...args);
}
}
self.onmessage = async (e) => {
const content = e.data +
'\nreturn 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 euler2quat(rotation: Vec3): Quat {
const {quat} = glMatrix;
const deg2rad = Math.PI / 180;
const q = quat.create() as Quat;
quat.rotateZ(q, q, deg2rad * rotation[2]);
quat.rotateY(q, q, deg2rad * rotation[1]);
quat.rotateX(q, q, deg2rad * rotation[0]);
return q;
}
function createGLTFnode(doc: Document, node: GLTFNode) {
function addMotion(
doc: Document, type: 'translation'|'rotation'|'scale', node: GLTFNode,
out: Node): Vec3|null {
const motion = node[type];
if (motion == null) {
return null;
}
if (typeof motion !== 'function') {
return motion;
}
const nFrames = timesAccessor.getCount();
const nEl = type == 'rotation' ? 4 : 3;
const frames = new Float32Array(nEl * nFrames);
for (let i = 0; i < nFrames; ++i) {
const x = i / (nFrames - 1);
const m = motion(
globalDefaults.animationMode !== 'ping-pong' ?
x :
(1 - Math.cos(x * 2 * Math.PI)) / 2);
frames.set(nEl === 4 ? euler2quat(m) : m, nEl * i);
}
const framesAccessor =
doc.createAccessor(node.name + ' ' + type + ' frames')
.setBuffer(doc.getRoot().listBuffers()[0])
.setArray(frames)
.setType(nEl === 4 ? Accessor.Type.VEC4 : Accessor.Type.VEC3);
const sampler = doc.createAnimationSampler()
.setInput(timesAccessor)
.setOutput(framesAccessor)
.setInterpolation('LINEAR');
const channel = doc.createAnimationChannel()
.setTargetPath(type)
.setTargetNode(out)
.setSampler(sampler);
animation.addSampler(sampler);
animation.addChannel(channel);
hasAnimation = true;
return motion(0);
}
function setMorph(doc: Document, node: Node, manifold: Manifold) {
if (manifold2morph.has(manifold)) {
const channel = doc.createAnimationChannel()
.setTargetPath('weights')
.setTargetNode(node)
.setSampler(weightsSampler);
animation.addChannel(channel);
hasAnimation = true;
}
}
function morphStart(manifoldMesh: Mesh, morph?: Morph): number[] {
const inputPositions: number[] = [];
if (morph == null) {
return inputPositions;
}
for (let i = 0; i < manifoldMesh.numVert; ++i) {
for (let j = 0; j < 3; ++j)
inputPositions[i * 3 + j] =
manifoldMesh.vertProperties[i * manifoldMesh.numProp + j];
}
if (morph.start) {
for (let i = 0; i < manifoldMesh.numVert; ++i) {
const vertProp = manifoldMesh.vertProperties;
const offset = i * manifoldMesh.numProp;
const pos = inputPositions.slice(offset, offset + 3) as Vec3;
morph.start(pos);
for (let j = 0; j < 3; ++j) vertProp[offset + j] = pos[j];
}
}
return inputPositions;
}
function morphEnd(
doc: Document, manifoldMesh: Mesh, mesh: GLTFMesh, inputPositions: number[],
morph?: Morph) {
if (morph == null) {
return;
}
mesh.setWeights([0]);
mesh.listPrimitives().forEach((primitive, i) => {
if (morph.end) {
for (let i = 0; i < manifoldMesh.numVert; ++i) {
const pos = inputPositions.slice(3 * i, 3 * (i + 1)) as Vec3;
morph.end(pos);
inputPositions.splice(3 * i, 3, ...pos);
}
}
const startPosition = primitive.getAttribute('POSITION')!.getArray()!;
const array = new Float32Array(startPosition.length);
const offset = manifoldMesh.runIndex[i];
for (let j = 0; j < array.length; ++j) {
array[j] = inputPositions[offset + j] - startPosition[j];
}
const morphAccessor = doc.createAccessor(mesh.getName() + ' morph target')
.setBuffer(doc.getRoot().listBuffers()[0])
.setArray(array)
.setType(Accessor.Type.VEC3);
const morphTarget =
doc.createPrimitiveTarget().setAttribute('POSITION', morphAccessor);
primitive.addTarget(morphTarget);
});
}
function createGLTFnode(doc: Document, node: GLTFNode): Node {
const out = doc.createNode(node.name);
if (node.translation) {
out.setTranslation(node.translation);
const pos = addMotion(doc, 'translation', node, out);
if (pos != null) {
out.setTranslation(pos);
}
if (node.rotation) {
const {quat} = glMatrix;
const deg2rad = Math.PI / 180;
const q = quat.create() as Quat;
quat.rotateX(q, q, deg2rad * node.rotation[0]);
quat.rotateY(q, q, deg2rad * node.rotation[1]);
quat.rotateZ(q, q, deg2rad * node.rotation[2]);
out.setRotation(q);
const rot = addMotion(doc, 'rotation', node, out);
if (rot != null) {
out.setRotation(euler2quat(rot));
}
if (node.scale) {
out.setScale(node.scale);
const scale = addMotion(doc, 'scale', node, out);
if (scale != null) {
out.setScale(scale);
}
return out;
}
@@ -275,15 +475,13 @@ function getBackupMaterial(node?: GLTFNode): GLTFMaterial {
return node.material;
}
function makeDefaultedMaterial(doc: Document, {
roughness = 0.2,
metallic = 1,
baseColorFactor = [1, 1, 0],
alpha = 1,
unlit = false,
name = ''
}: GLTFMaterial = {}): Material {
const material = doc.createMaterial(name);
function makeDefaultedMaterial(
doc: Document, matIn: GLTFMaterial = {}): Material {
const defaults = {...globalDefaults};
Object.assign(defaults, matIn);
const {roughness, metallic, baseColorFactor, alpha, unlit} = defaults;
const material = doc.createMaterial(matIn.name ?? '');
if (unlit) {
const unlit = doc.createExtension(KHRMaterialsUnlit).createUnlit();
@@ -307,7 +505,7 @@ function getCachedMaterial(doc: Document, matDef: GLTFMaterial): Material {
}
function addMesh(
doc: Document, node: Node, manifold: Manifold,
doc: Document, to3mf: To3MF, node: Node, manifold: Manifold,
backupMaterial: GLTFMaterial = {}) {
const numTri = manifold.numTri();
if (numTri == 0) {
@@ -330,36 +528,51 @@ function addMesh(
// From Z-up to Y-up (glTF)
const manifoldMesh = manifold.getMesh();
const materials = new Array<GLTFMaterial>();
const attributes = new Array<Array<string>>();
const id2properties = new Map<number, Properties>();
for (const id of manifoldMesh.runOriginalID!) {
const material = id2material.get(id) || backupMaterial;
materials.push(material);
attributes.push(['POSITION', ...material.attributes ?? []]);
id2properties.set(id, {
material: getCachedMaterial(doc, ghost ? GHOST : material),
attributes: ['POSITION', ...material.attributes ?? []]
});
}
const gltfMaterials = materials.map((matDef) => {
return getCachedMaterial(doc, ghost ? GHOST : matDef);
});
const morph = manifold2morph.get(manifold);
const inputPositions = morphStart(manifoldMesh, morph);
node.setMesh(writeMesh(doc, manifoldMesh, attributes, gltfMaterials));
const mesh = writeMesh(doc, manifoldMesh, id2properties);
mesh.setName(node.getName());
node.setMesh(mesh);
morphEnd(doc, manifoldMesh, mesh, inputPositions, morph);
const vertices = manifoldMesh.numProp === 3 ?
manifoldMesh.vertProperties :
new Float32Array(manifoldMesh.numVert * 3);
if (manifoldMesh.numProp > 3) {
for (let i = 0; i < manifoldMesh.numVert; ++i) {
for (let j = 0; j < 3; ++j)
vertices[i * 3 + j] =
manifoldMesh.vertProperties[i * manifoldMesh.numProp + j];
}
}
object2globalID.set(manifold, nextGlobalID);
to3mf.meshes.push(
{vertices, indices: manifoldMesh.triVerts, id: `${nextGlobalID++}`});
for (const [run, id] of manifoldMesh.runOriginalID!.entries()) {
let inMesh = shown.get(id);
let single = false;
if (inMesh == null) {
single = true;
inMesh = singles.get(id);
}
const show = shown.has(id);
const inMesh = show ? shown.get(id) : singles.get(id);
if (inMesh == null) {
continue;
}
const mat = single ? materials[run] : SHOW;
const debugNode =
doc.createNode('debug')
.setMesh(writeMesh(
doc, inMesh, attributes, [getCachedMaterial(doc, mat)]))
.setMatrix(manifoldMesh.transform(run));
id2properties.get(id)!.material = getCachedMaterial(
doc, show ? SHOW : (id2material.get(id) || backupMaterial));
const debugNode = doc.createNode('debug')
.setMesh(writeMesh(doc, inMesh, id2properties))
.setMatrix(manifoldMesh.transform(run));
node.addChild(debugNode);
}
}
@@ -388,17 +601,19 @@ function cloneNodeNewMaterial(
}
function createNodeFromCache(
doc: Document, nodeDef: GLTFNode,
doc: Document, to3MF: To3MF, nodeDef: GLTFNode,
manifold2node: Map<Manifold, Map<GLTFMaterial, Node>>): Node {
const node = createGLTFnode(doc, nodeDef);
if (nodeDef.manifold != null) {
const {manifold} = nodeDef;
if (manifold != null) {
setMorph(doc, node, manifold);
const backupMaterial = getBackupMaterial(nodeDef);
const cachedNodes = manifold2node.get(nodeDef.manifold);
const cachedNodes = manifold2node.get(manifold);
if (cachedNodes == null) {
addMesh(doc, node, nodeDef.manifold, backupMaterial);
addMesh(doc, to3MF, node, manifold, backupMaterial);
const cache = new Map<GLTFMaterial, Node>();
cache.set(backupMaterial, node);
manifold2node.set(nodeDef.manifold, cache);
manifold2node.set(manifold, cache);
} else {
const cachedNode = cachedNodes.get(backupMaterial);
if (cachedNode == null) {
@@ -412,10 +627,21 @@ function createNodeFromCache(
}
}
}
object2globalID.set(nodeDef, nextGlobalID);
to3MF.components.push({
id: `${nextGlobalID++}`,
name: nodeDef.name,
children:
manifold == null ? [] : [{objectID: `${object2globalID.get(manifold)}`}]
});
return node;
}
async function exportGLB(manifold?: Manifold) {
async function exportModels(defaults: GlobalDefaults, manifold?: Manifold) {
Object.assign(globalDefaults, GLOBAL_DEFAULTS);
Object.assign(globalDefaults, defaults);
const doc = new Document();
const halfRoot2 = Math.sqrt(2) / 2;
const mm2m = 1 / 1000;
@@ -424,13 +650,53 @@ async function exportGLB(manifold?: Manifold) {
.setScale([mm2m, mm2m, mm2m]);
doc.createScene().addChild(wrapper);
animation = doc.createAnimation('');
hasAnimation = false;
const nFrames = Math.round(globalDefaults.animationLength * FPS) + 1;
const times = new Float32Array(nFrames);
const weights = new Float32Array(nFrames);
for (let i = 0; i < nFrames; ++i) {
const x = i / (nFrames - 1);
times[i] = x * globalDefaults.animationLength;
weights[i] = globalDefaults.animationMode !== 'ping-pong' ?
x :
(1 - Math.cos(x * 2 * Math.PI)) / 2;
}
timesAccessor = doc.createAccessor('animation times')
.setBuffer(doc.createBuffer())
.setArray(times)
.setType(Accessor.Type.SCALAR);
weightsAccessor = doc.createAccessor('animation weights')
.setBuffer(doc.getRoot().listBuffers()[0])
.setArray(weights)
.setType(Accessor.Type.SCALAR);
weightsSampler = doc.createAnimationSampler()
.setInput(timesAccessor)
.setOutput(weightsAccessor)
.setInterpolation('LINEAR');
animation.addSampler(weightsSampler);
const to3mf = {
meshes: [],
components: [],
items: [],
precision: 7,
header: {
unit: 'millimeter',
title: 'ManifoldCAD.org model',
description: 'ManifoldCAD.org model',
application: 'ManifoldCAD.org',
}
} as To3MF;
if (nodes.length > 0) {
const node2gltf = new Map<GLTFNode, Node>();
const manifold2node = new Map<Manifold, Map<GLTFMaterial, Node>>();
let leafNodes = 0;
for (const nodeDef of nodes) {
node2gltf.set(nodeDef, createNodeFromCache(doc, nodeDef, manifold2node));
node2gltf.set(
nodeDef, createNodeFromCache(doc, to3mf, nodeDef, manifold2node));
if (nodeDef.manifold) {
++leafNodes;
}
@@ -438,10 +704,19 @@ async function exportGLB(manifold?: Manifold) {
for (const nodeDef of nodes) {
const gltfNode = node2gltf.get(nodeDef)!;
if (nodeDef.parent == null) {
const child = {
objectID: `${object2globalID.get(nodeDef)}`,
transform: gltfNode.getMatrix()
};
const {parent} = nodeDef;
if (parent == null) {
wrapper.addChild(gltfNode);
to3mf.items.push(child);
} else {
node2gltf.get(nodeDef.parent)!.addChild(gltfNode);
node2gltf.get(parent)!.addChild(gltfNode);
const parent3mf = to3mf.components.find(
(comp) => comp.id == `${object2globalID.get(parent)}`)!;
parent3mf.children.push(child);
}
}
@@ -453,12 +728,61 @@ async function exportGLB(manifold?: Manifold) {
return;
}
const node = doc.createNode();
addMesh(doc, node, manifold);
addMesh(doc, to3mf, node, manifold);
wrapper.addChild(node);
to3mf.items.push({objectID: `${object2globalID.get(manifold)}`});
}
if (!hasAnimation) {
timesAccessor.dispose();
weightsAccessor.dispose();
weightsSampler.dispose();
animation.dispose();
}
const glb = await io.writeBinary(doc);
const blobGLB = new Blob([glb], {type: 'application/octet-stream'});
const blob = new Blob([glb], {type: 'application/octet-stream'});
self.postMessage({objectURL: URL.createObjectURL(blob)});
const fileForRelThumbnail = new FileForRelThumbnail();
fileForRelThumbnail.add3dModel('3D/3dmodel.model')
const model = to3dmodel(to3mf as any);
const files: Zippable = {};
files['3D/3dmodel.model'] = strToU8(model);
files[fileForContentTypes.name] = strToU8(fileForContentTypes.content);
files[fileForRelThumbnail.name] = strToU8(fileForRelThumbnail.content);
const zipFile = zipSync(files);
const blob3MF = new Blob(
[zipFile],
{type: 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml'});
return ({
glbURL: URL.createObjectURL(blobGLB),
threeMFURL: URL.createObjectURL(blob3MF)
});
}
function evaluateCADToManifold(code: string) {
const globalDefaults = {} as GlobalDefaults;
const context = {
globalDefaults,
exportModels,
glMatrix,
module,
...Object.fromEntries(
exposedFunctions.map((name) => [name, (module as any)[name]]),
),
};
const evalFn = new Function(
...Object.keys(context),
'resetToCircularDefaults();\n' + code +
'\n return typeof result === "undefined" ? undefined : result;',
);
const manifold = evalFn(...Object.values(context));
return {globalDefaults, manifold};
}
export async function evaluateCADToModel(code: string) {
const {globalDefaults, manifold} = evaluateCADToManifold(code);
return await exportModels(globalDefaults, manifold);
}
+13 -3
View File
@@ -1,12 +1,12 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <manifold.h>
#include <polygon.h>
#include <sdf.h>
#include <vector>
#include "cross_section.h"
#include "manifold.h"
#include "polygon.h"
#include "sdf.h"
using namespace emscripten;
using namespace manifold;
@@ -159,6 +159,11 @@ CrossSection Offset(CrossSection& cross_section, double delta, int join_type,
: CrossSection::JoinType::Miter;
return cross_section.Offset(delta, jt, miter_limit, arc_tolerance);
}
void CollectVertices(std::vector<glm::vec2>& verts, const CrossSection& cs) {
auto polys = cs.ToPolygons();
for (auto poly : polys) verts.insert(verts.end(), poly.begin(), poly.end());
}
} // namespace cross_js
namespace man_js {
@@ -219,4 +224,9 @@ std::vector<Manifold> SplitByPlane(Manifold& m, glm::vec3 normal,
auto [a, b] = m.SplitByPlane(normal, originOffset);
return {a, b};
}
void CollectVertices(std::vector<glm::vec3>& verts, const Manifold& manifold) {
Mesh mesh = manifold.GetMesh();
verts.insert(verts.end(), mesh.vertPos.begin(), mesh.vertPos.end());
}
} // namespace man_js
+129 -19
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {Box, FillRule, JoinType, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types';
import {Box, FillRule, JoinType, Mat3, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types';
/**
* Triangulates a set of /epsilon-valid polygons.
@@ -38,6 +38,7 @@ export function setMinCircularAngle(angle: number): void;
export function setMinCircularEdgeLength(length: number): void;
export function setCircularSegments(segments: number): void;
export function getCircularSegments(radius: number): number;
export function resetToCircularDefaults(): void;
///@}
export class CrossSection {
@@ -111,6 +112,7 @@ export class CrossSection {
* @param v The vector to add to every vertex.
*/
translate(v: Vec2): CrossSection;
translate(x: number, y?: number): CrossSection;
/**
* Applies a (Z-axis) rotation to the CrossSection, in degrees. This operation
@@ -173,11 +175,6 @@ export class CrossSection {
delta: number, joinType?: JoinType, miterLimit?: number,
circularSegments?: number): CrossSection;
/**
* Compute the convex hull of the contours in this CrossSection.
*/
hull(): CrossSection;
/**
* Remove vertices from the contours in this CrossSection that are less than
* the specified distance epsilon from an imaginary line that passes through
@@ -246,6 +243,18 @@ export class CrossSection {
*/
static intersection(polygons: (CrossSection|Polygons)[]): CrossSection;
// Convex Hulls
/**
* Compute the convex hull of the contours in this CrossSection.
*/
hull(): CrossSection;
/**
* Compute the convex hull of all points in a list of polygons/cross-sections.
*/
static hull(polygons: (CrossSection|Polygons)[]): CrossSection;
// Topological Operations
/**
@@ -507,6 +516,7 @@ export class Manifold {
* @param v The vector to add to every vertex.
*/
translate(v: Vec3): Manifold;
translate(x: number, y?: number, z?: number): Manifold;
/**
* Applies an Euler angle rotation to the manifold, first about the X axis,
@@ -519,6 +529,7 @@ export class Manifold {
* @param v [X, Y, Z] rotation in degrees.
*/
rotate(v: Vec3): Manifold;
rotate(x: number, y?: number, z?: number): Manifold;
/**
* Scale this Manifold in space. This operation can be chained. Transforms are
@@ -549,6 +560,37 @@ export class Manifold {
*/
warp(warpFunc: (vert: Vec3) => void): Manifold;
/**
* Smooths out the Manifold by filling in the halfedgeTangent vectors. The
* geometry will remain unchanged until Refine or RefineToLength is called to
* interpolate the surface. This version uses the supplied vertex normal
* properties to define the tangent vectors.
*
* @param normalIdx The first property channel of the normals. NumProp must be
* at least normalIdx + 3. Any vertex where multiple normals exist and don't
* agree will result in a sharp edge.
*/
smoothByNormals(normalIdx: number): Manifold;
/**
* Smooths out the Manifold by filling in the halfedgeTangent vectors. The
* geometry will remain unchanged until Refine or RefineToLength is called to
* interpolate the surface. This version uses the geometry of the triangles
* and pseudo-normals to define the tangent vectors.
*
* @param minSharpAngle degrees, default 60. Any edges with angles greater
* than this value will remain sharp. The rest will be smoothed to G1
* continuity, with the caveat that flat faces of three or more triangles will
* always remain flat. With a value of zero, the model is faceted, but in this
* case there is no point in smoothing.
*
* @param minSmoothness range: 0 - 1, default 0. The smoothness applied to
* sharp angles. The default gives a hard edge, while values > 0 will give a
* small fillet on these sharp edges. A value of 1 is equivalent to a
* minSharpAngle of 180 - all edges will be smooth.
*/
smoothOut(minSharpAngle?: number, minSmoothness?: number): Manifold;
/**
* Increase the density of the mesh by splitting every edge into n pieces. For
* instance, with n = 2, each triangle will be split into 4 triangles. These
@@ -561,6 +603,18 @@ export class Manifold {
*/
refine(n: number): Manifold;
/**
* Increase the density of the mesh by splitting each edge into pieces of
* roughly the input length. Interior verts are added to keep the rest of the
* triangulation edges also of roughly the same length. If halfedgeTangents
* are present (e.g. from the Smooth() constructor), the new vertices will be
* moved to the interpolated surface according to their barycentric
* coordinates.
*
* @param length The length that edges will be broken down to.
*/
refineToLength(length: number): Manifold;
/**
* Create a new copy of this manifold with updated vertex properties by
* supplying a function that takes the existing position and properties as
@@ -596,6 +650,24 @@ export class Manifold {
*/
calculateCurvature(gaussianIdx: number, meanIdx: number): Manifold;
/**
* Fills in vertex properties for normal vectors, calculated from the mesh
* geometry. Flat faces composed of three or more triangles will remain flat.
*
* @param normalIdx The property channel in which to store the X
* values of the normals. The X, Y, and Z channels will be sequential. The
* property set will be automatically expanded to include up through normalIdx
* + 2.
*
* @param minSharpAngle Any edges with angles greater than this value will
* remain sharp, getting different normal vector properties on each side of
* the edge. By default, no edges are sharp and all normals are shared. With a
* value of zero, the model is faceted and all normals match their triangle
* normals, but in this case it would be better not to calculate normals at
* all.
*/
calculateNormals(normalIdx: number, minSharpAngle: number): Manifold;
// Boolean Operations
/**
@@ -675,6 +747,35 @@ export class Manifold {
*/
trimByPlane(normal: Vec3, originOffset: number): Manifold;
/**
* Returns the cross section of this object parallel to the X-Y plane at the
* specified height. Using a height equal to the bottom
* of the bounding box will return the bottom faces, while using a height
* equal to the top of the bounding box will return empty.
*
* @param height Z-level of slice.
*/
slice(height: number): CrossSection;
/**
* Returns a cross section representing the projected outline of this object
* onto the X-Y plane.
*/
project(): CrossSection;
// Convex Hulls
/**
* Compute the convex hull of all points in this Manifold.
*/
hull(): Manifold;
/**
* Compute the convex hull of all points contained within a set of Manifolds
* and point vectors.
*/
static hull(points: (Manifold|Vec3)[]): Manifold;
// Topological Operations
/**
@@ -758,6 +859,13 @@ export class Manifold {
*/
getProperties(): Properties;
/*
* Returns the minimum gap between two manifolds. Returns a float between
* 0 and searchLength.
*/
minGap(other: Manifold, searchLength: number): number;
// Export
/**
@@ -815,19 +923,7 @@ export class Manifold {
delete(): void;
}
export class Mesh {
constructor(options: {
numProp: number,
vertProperties: Float32Array,
triVerts: Uint32Array,
mergeFromVert?: Uint32Array,
mergeToVert?: Uint32Array,
runIndex?: Uint32Array,
runOriginalID?: Uint32Array,
runTransform?: Float32Array,
faceID?: Uint32Array,
halfedgeTangent?: Float32Array
});
export interface MeshOptions {
numProp: number;
vertProperties: Float32Array;
triVerts: Uint32Array;
@@ -838,6 +934,20 @@ export class Mesh {
runTransform?: Float32Array;
faceID?: Uint32Array;
halfedgeTangent?: Float32Array;
}
export class Mesh {
constructor(options: MeshOptions);
numProp: number;
vertProperties: Float32Array;
triVerts: Uint32Array;
mergeFromVert: Uint32Array;
mergeToVert: Uint32Array;
runIndex: Uint32Array;
runOriginalID: Uint32Array;
runTransform: Float32Array;
faceID: Uint32Array;
halfedgeTangent: Float32Array;
get numTri(): number;
get numVert(): number;
get numRun(): number;
+12
View File
@@ -22,6 +22,18 @@ export interface SealedFloat32Array<N extends number> extends Float32Array {
export type Vec2 = [number, number];
export type Vec3 = [number, number, number];
// 3x3 matrix stored in column-major order
export type Mat3 = [
number,
number,
number,
number,
number,
number,
number,
number,
number,
];
// 4x4 matrix stored in column-major order
export type Mat4 = [
number,
+1
View File
@@ -28,6 +28,7 @@ export interface ManifoldToplevel {
setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength;
setCircularSegments: typeof T.setCircularSegments;
getCircularSegments: typeof T.getCircularSegments;
resetToCircularDefaults: typeof T.resetToCircularDefaults;
setup: () => void;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "manifold-3d",
"version": "2.1.0",
"version": "2.5.1",
"description": "Geometry library for topological robustness",
"main": "manifold.js",
"files": [
+9 -2
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_DEBUG)
add_executable(minimizeTestcase minimize_testcase.cpp)
target_link_libraries(minimizeTestcase polygon)
target_compile_options(minimizeTestcase PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(minimizeTestcase PUBLIC cxx_std_17)
endif()
if(MANIFOLD_EXPORT)
add_executable(convertFile convert_file.cpp)
target_link_libraries(convertFile manifold meshIO)
@@ -34,9 +41,9 @@ endif()
if(BUILD_TEST_CGAL)
add_executable(perfTestCGAL perf_test_cgal.cpp)
find_package(CGAL REQUIRED COMPONENTS Core)
find_package(Boost REQUIRED COMPONENTS thread)
target_compile_definitions(perfTestCGAL PRIVATE CGAL_USE_GMPXX)
# target_compile_definitions(perfTestCGAL PRIVATE CGAL_DEBUG)
target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core)
target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core Boost::thread)
target_compile_options(perfTestCGAL PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(perfTestCGAL PUBLIC cxx_std_17)
endif()
+391
View File
@@ -0,0 +1,391 @@
#include <algorithm>
#include <fstream>
#include <string>
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
#include <execution>
#endif
#include "polygon.h"
#include "utils.h"
using namespace manifold;
inline float cross(glm::vec2 p, glm::vec2 q) { return p.x * q.y - p.y * q.x; }
// return true if p intersects with q
// note that we don't care about collinear, intersection in the ends etc.
// https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
inline bool intersect(glm::vec2 p0, glm::vec2 p1, glm::vec2 q0, glm::vec2 q1,
float precision) {
glm::vec2 r = p1 - p0;
glm::vec2 s = q1 - q0;
// allow some error in the intersection point
float epsilon_r = 2.0 * precision / glm::length(r);
float epsilon_s = 2.0 * precision / glm::length(s);
float rxs = cross(r, s);
// in case they are nearly collinear, ignore them...
// this is to avoid treating degenerate triangles as intersecting
//
// note that this does not correspond to a fixed angle,
// but seems to work well enough
if (rxs < kTolerance && rxs > -kTolerance) {
return false;
}
float u = cross(q0 - p0, r) / rxs;
float t = cross(q0 - p0, s) / rxs;
// we only care about intersection in the middle of both lines, excluding
// their ends
if (epsilon_r <= u && u <= (1 - epsilon_r) && //
epsilon_s <= t && t <= (1 - epsilon_s)) {
// there can be cases which r and s are close to collinear,
// so even though the lines are not intersecting,
// perturbation along r/s is not enough.
//
// in that case, apply perturbation perpendicular to r
glm::vec2 r_orth = glm::normalize(glm::vec2(-r.y, r.x)) * precision;
float u1 = cross(q0 - p0 + r_orth, r) / rxs;
float u2 = cross(q0 - p0 - r_orth, r) / rxs;
float t1 = cross(q0 - p0 + r_orth, s) / rxs;
float t2 = cross(q0 - p0 - r_orth, s) / rxs;
return 0 <= u1 && u1 <= 1 && //
0 <= t1 && t1 <= 1 && //
0 <= u2 && u2 <= 1 && //
0 <= t2 && t2 <= 1;
return true;
} else {
return false;
}
}
// check if removing point j in polygon i will introduce self-intersection
// this checks if the new edge j-1 <-> j+1 intersects with any other edges,
// assuming the original polygon does not contain self-intersection
bool safeToRemove(const Polygons &polys, int i, int j, float precision) {
if (polys[i].size() == 3) return false;
glm::vec2 prev = polys[i][j == 0 ? polys[i].size() - 1 : (j - 1)];
glm::vec2 next = polys[i][j == (polys[i].size() - 1) ? 0 : (j + 1)];
for (int k = 0; k < polys.size(); k++) {
auto ll = [&](size_t l) {
return l == (polys[k].size() - 1) ? 0 : (l + 1);
};
const glm::vec2 *polysk = polys[k].data();
if (!std::all_of(
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
std::execution::par,
#endif
countAt((size_t)0), countAt(polys[k].size()), [=](size_t l) {
if (i == k && (l == j || ll(l) == j)) return true;
return !intersect(prev, next, polysk[l], polysk[ll(l)],
precision);
}))
return false;
}
return true;
}
std::pair<int, int> findIndex(const Polygons &polys, int i) {
int outer = 0;
while (i >= polys[outer].size()) i -= polys[outer++].size();
return std::make_pair(outer, i);
}
void Dump(const Polygons &polys) {
for (const SimplePolygon &poly : polys) {
std::cout << "polys.push_back({" << std::setprecision(9) << std::endl;
for (const glm::vec2 v : poly) {
std::cout << " {" << v.x << ", " << v.y << "}, //" << std::endl;
}
std::cout << "});" << std::endl;
}
for (const SimplePolygon &poly : polys) {
std::cout << "show(array([" << std::endl;
for (const glm::vec2 v : poly) {
std::cout << " [" << v.x << ", " << v.y << "]," << std::endl;
}
std::cout << "]))" << std::endl;
}
}
void DumpTriangulation(const Polygons &polys, float precision) {
ExecutionParams oldParams = PolygonParams();
manifold::PolygonParams().processOverlaps = true;
std::vector<glm::ivec3> result = Triangulate(polys);
PolygonParams() = oldParams;
for (const glm::ivec3 &tri : result) {
std::pair<int, int> xid = findIndex(polys, tri.x);
std::pair<int, int> yid = findIndex(polys, tri.y);
std::pair<int, int> zid = findIndex(polys, tri.z);
glm::vec2 x = polys[xid.first][xid.second];
glm::vec2 y = polys[yid.first][yid.second];
glm::vec2 z = polys[zid.first][zid.second];
printf("show(array([[%.7f, %.7f], [%.7f, %.7f], [%.7f, %.7f]]))\n", x.x,
x.y, y.x, y.y, z.x, z.y);
if (CCW(x, y, z, precision) == -1) {
printf("^ not CCW\n");
}
}
}
// x direction ray
// idk which direction, but it is a fixed direction...
// https://stackoverflow.com/questions/11716268/point-in-polygon-algorithm
bool rayHit(glm::vec2 point, glm::vec2 q0, glm::vec2 q1) {
return ((q0.y >= point.y) != (q1.y >= point.y)) &&
(point.x <= (q1.x - q0.x) * (point.y - q0.y) / (q1.y - q0.y) + q0.x);
}
// get the list of polygons inside polygon i
// this enumerates over the first point in all other polygons,
// and check if they are inside the current polygon
// this algorithm assumes that polygons are non-intersecting
std::vector<int> getChildren(const Polygons &polys, int i) {
std::vector<int> results;
for (int j = 0; j < polys.size(); j++) {
if (i == j) continue;
glm::vec2 point = polys[j][0];
auto k1 = [&](size_t k) {
return k == (polys[i].size() - 1) ? 0 : (k + 1);
};
int count = std::count_if(
#if MANIFOLD_PAR == 'T' && __has_include(<pstl/glue_execution_defs.h>)
std::execution::par,
#endif
countAt((size_t)0), countAt(polys[i].size()),
[&](size_t k) { return rayHit(point, polys[i][k], polys[i][k1(k)]); });
if (count % 2) results.push_back(j);
}
return results;
}
// assuming polys is valid
// we try to remove vertices from polys such that
// 1. the updated polys is still valid (no overlapping edges, correct winding
// direction)
// 2. same error in triangulation (either geometryErr or overlapping triangles)
void simplify(Polygons &polys, float precision = -1) {
bool removedSomething = true;
std::string msg;
while (removedSomething) {
int points = 0;
for (const SimplePolygon &poly : polys) points += poly.size();
std::cout << "trying to simplify " << points << " points" << std::endl;
removedSomething = false;
// try to remove simple polygons
for (int i = 0; i < polys.size(); i++) {
std::vector<int> children = getChildren(polys, i);
// if there are children, we can't remove it or we will mess up with
// winding direction...
if (!children.empty()) continue;
SimplePolygon poly = std::move(polys[i]);
polys.erase(polys.begin() + i);
try {
std::vector<glm::ivec3> result = Triangulate(polys, precision);
polys.insert(polys.begin() + i, std::move(poly));
} catch (geometryErr &e) {
if (msg.size() > 0 && msg.compare(e.what()) != 0) {
polys.insert(polys.begin() + i, std::move(poly));
} else {
removedSomething = true;
msg = e.what();
}
} catch (topologyErr &e) {
polys.insert(polys.begin() + i, std::move(poly));
}
}
for (int i = 0; i < polys.size(); i++) {
std::vector<int> children = getChildren(polys, i);
for (int j = 0; j < polys[i].size(); j++) {
// removed vertex cannot change inclusion relation
// we just check if the vertex
// x: intersects with j0, j (original edge 1)
// y: intersects with j, j1 (original edge 2)
// z: intersects with j0, j1 (new edge)
// if (x ^ y ^ z) is true, it means that the count % 2 is changed,
// and we changed inclusion relation, so vertex j cannot be removed
int j0 = j == 0 ? polys[i].size() - 1 : j - 1;
int j1 = j == polys[i].size() - 1 ? 0 : j + 1;
if (std::any_of(children.begin(), children.end(), [&](int k) {
return rayHit(polys[k][0], polys[i][j0], polys[i][j]) ^
rayHit(polys[k][0], polys[i][j], polys[i][j1]) ^
rayHit(polys[k][0], polys[i][j0], polys[i][j1]);
}))
continue;
// removed vertex cannot introduce intersection
if (!safeToRemove(polys, i, j, precision)) continue;
glm::vec2 removed = polys[i][j];
polys[i].erase(polys[i].begin() + j);
try {
std::vector<glm::ivec3> result = Triangulate(polys, precision);
polys[i].insert(polys[i].begin() + j, removed);
} catch (geometryErr &e) {
if (msg.size() > 0 && msg.compare(e.what()) != 0) {
polys[i].insert(polys[i].begin() + j, removed);
} else {
removedSomething = true;
msg = e.what();
}
} catch (topologyErr &e) {
polys[i].insert(polys[i].begin() + j, removed);
}
}
}
}
std::cout << "Error message for triangulation:" << std::endl
<< msg.c_str() << std::endl;
}
struct Edge {
glm::vec2 west; // -x side vertex
glm::vec2 east; // +x side vertex
size_t i, j; // indices of origin vertex
bool operator<(const Edge &other) const {
return west.x < other.west.x ||
(west.x == other.west.x && east.x < other.east.x);
}
Edge(const Polygons &polys, size_t i, size_t j) : i(i), j(j) {
size_t j1 = j == polys[i].size() - 1 ? 0 : j + 1;
east = polys[i][j];
west = polys[i][j1];
if (west.x > east.x) std::swap(east, west);
}
};
int isValid(const Polygons &polys, float precision = -1) {
size_t numEdge = 0;
for (const SimplePolygon &poly : polys) numEdge += poly.size();
std::vector<Edge> edges;
edges.reserve(numEdge);
for (size_t i = 0; i < polys.size(); i++)
for (size_t j = 0; j < polys[i].size(); j++)
edges.push_back(Edge(polys, i, j));
std::sort(edges.begin(), edges.end());
// check intersection
std::mutex mutex;
std::vector<std::pair<Edge, Edge>> overlappingPairs;
std::for_each( //
countAt((size_t)0), countAt(edges.size()), [&](size_t i) {
// check all subsequent edges e' until e'.west.x > e.east.x
// this ensures we checked all edges e' where
// e.west.x < e'.west.x < e.east.x
Edge e = edges[i];
for (size_t j = i + 1; j < edges.size() && edges[j].west.x < e.east.x;
j++) {
if (intersect(e.west, e.east, edges[j].west, edges[j].east,
precision)) {
mutex.lock();
overlappingPairs.push_back(std::make_pair(e, edges[j]));
mutex.unlock();
}
}
});
if (!overlappingPairs.empty()) {
std::cout << "found " << overlappingPairs.size() << " self-intersection"
<< std::endl;
for (const std::pair<Edge, Edge> &pairs : overlappingPairs) {
std::cout << pairs.first.i << " " << pairs.first.j << "<->" //
<< pairs.second.i << " " << pairs.second.j << std::endl;
printf("array([[%.7f, %.7f], [%.7f, %.7f]])\n", pairs.first.west.x,
pairs.first.west.y, pairs.first.east.x, pairs.first.east.y);
printf("array([[%.7f, %.7f], [%.7f, %.7f]])\n", pairs.second.west.x,
pairs.second.west.y, pairs.second.east.x, pairs.second.east.y);
}
} else {
std::cout << "No self-intersection" << std::endl;
}
return 0;
}
int main(int argc, char **argv) {
PolygonParams().intermediateChecks = true;
PolygonParams().processOverlaps = false;
PolygonParams().suppressErrors = true;
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <input file>" << std::endl;
return 0;
}
std::ifstream fin(argv[1]);
std::string line;
float precision = -1;
Polygons polys;
SimplePolygon poly;
// search for precision first
while (std::getline(fin, line)) {
int index = line.find("Precision = ");
if (index != std::string::npos) {
std::istringstream iss(line.substr(index + 12));
if (!(iss >> precision)) {
std::cerr << "Error parsing precision" << std::endl;
return 1;
}
break;
}
}
std::cout << "Precision = " << precision << std::endl;
// fill polygons
while (std::getline(fin, line)) {
if (line.length() > 5 && line.substr(0, 5).compare(" {") == 0) {
std::istringstream iss(line.substr(5));
float x, y;
if (!(iss >> x)) {
std::cerr << "Error parsing coordinate:" << std::endl;
std::cerr << "Line: " << line << std::endl;
return 1;
}
iss.ignore(2);
if (!(iss >> y)) {
std::cerr << "Error parsing coordinate:" << std::endl;
std::cerr << "Line: " << line << std::endl;
return 1;
}
poly.push_back(glm::vec2(x, y));
} else if (line.length() >= 3 && line.substr(0, 3).compare("});") == 0) {
if (poly.size() < 3) {
std::cerr << "Error: empty/invalid polygon" << std::endl;
return 1;
}
polys.push_back(std::move(poly));
poly.clear();
} else if (line.length() >= 5 && line.substr(0, 5).compare("array") == 0) {
break;
}
}
if (polys.size() == 0) {
std::cerr << "Error: empty polygon" << std::endl;
return 1;
}
if (precision == -1) {
float bound = 0;
for (const SimplePolygon &poly : polys) {
for (const glm::vec2 &pt : poly) {
bound = glm::max(bound, glm::abs(pt.x));
bound = glm::max(bound, glm::abs(pt.y));
}
}
precision = bound * kTolerance;
}
std::cout << "------------" << std::endl;
isValid(polys, precision);
simplify(polys, precision);
std::cout << "------------" << std::endl;
std::cout << "Final polygon:" << std::endl;
Dump(polys);
std::cout << "------------" << std::endl;
std::cout << "Erroneous triangulation:" << std::endl;
DumpTriangulation(polys, precision);
return 0;
}
+2 -3
View File
@@ -57,10 +57,9 @@ void manifoldToCGALSurfaceMesh(Manifold &manifold, TriangleMesh &cgalMesh) {
int main(int argc, char **argv) {
for (int i = 0; i < 8; ++i) {
Manifold sphere = Manifold::Sphere(1, (8 << i) * 4);
Manifold sphere2 = sphere;
sphere2.Translate(glm::vec3(0.5));
Manifold sphere2 = sphere.Translate(glm::vec3(0.5));
TriangleMesh cgalSphere, cgalSphere2, cgalOut;
TriangleMesh cgalSphere, cgalSphere2;
manifoldToCGALSurfaceMesh(sphere, cgalSphere);
manifoldToCGALSurfaceMesh(sphere2, cgalSphere2);
Generated
+80 -8
View File
@@ -1,12 +1,31 @@
{
"nodes": {
"flake-utils": {
"clipper2-src": {
"flake": false,
"locked": {
"lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
"lastModified": 1715688111,
"narHash": "sha256-iy2QV8IVEEIPijWIpzIQtqIQeTU4bfiH41pC0X6m72U=",
"owner": "AngusJohnson",
"repo": "Clipper2",
"rev": "ff378668baae3570e9d8070aa9eb339bdd5a6aba",
"type": "github"
},
"original": {
"owner": "AngusJohnson",
"repo": "Clipper2",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -15,13 +34,30 @@
"type": "github"
}
},
"gtest-src": {
"flake": false,
"locked": {
"lastModified": 1690989893,
"narHash": "sha256-t0RchAHTJbuI5YW4uyBPykTvcjy90JW9AOPNjIhwh6U=",
"owner": "google",
"repo": "googletest",
"rev": "f8d7d77c06936315286eb55f8de22cd23c188571",
"type": "github"
},
"original": {
"owner": "google",
"ref": "v1.14.0",
"repo": "googletest",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1689008574,
"narHash": "sha256-VFMgyHDiqsGDkRg73alv6OdHJAqhybryWHv77bSCGIw=",
"lastModified": 1704722960,
"narHash": "sha256-mKGJ3sPsT6//s+Knglai5YflJUF2DGj7Ai6Ynopz0kI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4a729ce4b1fe5ec4fffc71c67c96aa5184ebb462",
"rev": "317484b1ead87b9c1b8ac5261a8d2dd748a0492d",
"type": "github"
},
"original": {
@@ -32,8 +68,44 @@
},
"root": {
"inputs": {
"clipper2-src": "clipper2-src",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"gtest-src": "gtest-src",
"nixpkgs": "nixpkgs",
"thrust-src": "thrust-src"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"thrust-src": {
"flake": false,
"locked": {
"lastModified": 1696873248,
"narHash": "sha256-mUhMXGPbO2t83EvI8YNDRLngvLiwfzs4EgRrcxKfhHs=",
"ref": "refs/heads/main",
"rev": "756c5afc0750f1413da05bd2b6505180e84c53d4",
"revCount": 4758,
"submodules": true,
"type": "git",
"url": "https://github.com/NVIDIA/thrust.git"
},
"original": {
"submodules": true,
"type": "git",
"url": "https://github.com/NVIDIA/thrust.git"
}
}
},
+91 -56
View File
@@ -1,8 +1,19 @@
{
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
outputs = { self, nixpkgs, flake-utils }:
inputs.gtest-src = {
url = "github:google/googletest/v1.14.0";
flake = false;
};
inputs.thrust-src = {
url = "git+https://github.com/NVIDIA/thrust.git?submodules=1";
flake = false;
};
inputs.clipper2-src = {
url = "github:AngusJohnson/Clipper2";
flake = false;
};
outputs = { self, nixpkgs, flake-utils, gtest-src, thrust-src, clipper2-src }:
flake-utils.lib.eachDefaultSystem
(system:
let
@@ -10,97 +21,87 @@
inherit system;
config.allowUnfree = true;
};
clipper2 = pkgs.clipper2.overrideAttrs (_: _: {
version = "14052024";
src = clipper2-src;
});
manifold =
{ parallel-backend ? "none"
, cuda-support ? false
, doCheck ? true
, build-tools ? [ ]
, ...
}: pkgs.stdenv.mkDerivation {
inherit doCheck;
pname =
if cuda-support then
"manifold-${parallel-backend}-cuda"
else
"manifold-${parallel-backend}";
version = "beta";
pname = "manifold-${parallel-backend}";
version = "2.5.1";
src = self;
nativeBuildInputs = (with pkgs; [ cmake (python39.withPackages (ps: with ps; [ trimesh ])) ]) ++ build-tools ++
(if cuda-support then with pkgs.cudaPackages; [ cuda_nvcc cuda_cudart cuda_cccl pkgs.addOpenGLRunpath ] else [ ]);
nativeBuildInputs = (with pkgs; [
cmake
ninja
(python3.withPackages
(ps: with ps; [ trimesh pytest ]))
gtest
pkg-config
]) ++ build-tools;
buildInputs = with pkgs; [
glm
clipper2
assimp
];
cmakeFlags = [
"-DMANIFOLD_PYBIND=ON"
"-DMANIFOLD_CBIND=ON"
"-DBUILD_SHARED_LIBS=ON"
"-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src}"
"-DMANIFOLD_PAR=${pkgs.lib.strings.toUpper parallel-backend}"
"-DMANIFOLD_USE_CUDA=${if cuda-support then "ON" else "OFF"}"
];
prePatch = ''
substituteInPlace bindings/python/CMakeLists.txt \
--replace 'DESTINATION ''${Python_SITEARCH}' 'DESTINATION "${placeholder "out"}/${pkgs.python3.sitePackages}"'
'';
checkPhase = ''
cd test
./manifold_test
cd ../../
PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python python3 bindings/python/examples/run_all.py
PYTHONPATH=$PYTHONPATH:$(pwd)/build/bindings/python python3 -m pytest
cd build
'';
installPhase = ''
mkdir -p $out
cp src/manifold/libmanifold.a $out/
cp extras/perfTest $out
cp bindings/python/manifold3d* $out
'';
};
parallelBackends = [
{ parallel-backend = "none"; }
{
parallel-backend = "omp";
build-tools = [ pkgs.llvmPackages_13.openmp ];
}
{
parallel-backend = "tbb";
build-tools = with pkgs; [ tbb pkg-config ];
build-tools = with pkgs; [ tbb_2021_8 pkg-config ];
}
];
buildMatrix = with pkgs; with lib; lists.flatten (map
(env: map
(x: x // env)
parallelBackends) [
{ cuda-support = false; }
{
cuda-support = true;
}
]);
devShell = { additional ? [ ] }: pkgs.mkShell {
buildInputs = with pkgs; [
cmake
llvmPackages_13.openmp
clang-tools
clang_13
emscripten
tbb
lcov
] ++ additional;
};
in
{
packages = (builtins.listToAttrs
(map
(x: {
name = "manifold-" + x.parallel-backend + (if
x.cuda-support then "-cuda" else "");
name = "manifold-" + x.parallel-backend;
value = manifold x;
})
buildMatrix)) // {
parallelBackends)) // {
manifold-js = pkgs.buildEmscriptenPackage {
name = "manifold-js";
version = "beta";
version = "2.5.1";
src = self;
nativeBuildInputs = (with pkgs; [ cmake python39 ]);
buildInputs = [ pkgs.nodejs ];
configurePhase = ''
cp -r ${clipper2-src} clipper2
chmod -R +w clipper2
mkdir -p .emscriptencache
export EM_CACHE=$(pwd)/.emscriptencache
mkdir build
cd build
mkdir cache
export EM_CACHE=$(pwd)/cache
emcmake cmake -DCMAKE_BUILD_TYPE=Release ..
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
-DFETCHCONTENT_SOURCE_DIR_GLM=${pkgs.glm.src} \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=${gtest-src} \
-DFETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src} \
-DFETCHCONTENT_SOURCE_DIR_CLIPPER2=../clipper2 ..
'';
buildPhase = ''
emmake make -j''${NIX_BUILD_CORES}
@@ -116,13 +117,47 @@
cp {extras,wasm}/*.wasm $out/
'';
};
# but how should we make it work with other python versions?
manifold3d = with pkgs.python3Packages; buildPythonPackage {
pname = "manifold3d";
version = "2.5.1";
src = self;
propagatedBuildInputs = [
numpy
];
buildInputs = with pkgs; [
tbb_2021_8
glm
clipper2
];
nativeBuildInputs = with pkgs; [
cmake
ninja
setuptools
scikit-build-core
pyproject-metadata
pathspec
pkg-config
];
SKBUILD_CMAKE_DEFINE = "FETCHCONTENT_SOURCE_DIR_THRUST=${thrust-src}";
checkInputs = [
trimesh
pytest
];
format = "pyproject";
dontUseCmakeConfigure = true;
doCheck = true;
checkPhase = ''
python3 bindings/python/examples/run_all.py
python3 -m pytest
'';
};
};
devShell = devShell { };
devShells.cuda = devShell {
additional = with pkgs.cudaPackages; [
cuda_nvcc
cuda_cudart
cuda_cccl
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
cmake
tbb_2021_8
gtest
];
};
}
Executable
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
shopt -s extglob
if [ -z "$CLANG_FORMAT" ]; then
CLANG_FORMAT=$(dirname $(which clang-11))/clang-format
fi
$CLANG_FORMAT -i extras/*.cpp
$CLANG_FORMAT -i meshIO/**/*.{h,cpp}
$CLANG_FORMAT -i samples/**/*.{h,cpp}
$CLANG_FORMAT -i test/*.{h,cpp}
$CLANG_FORMAT -i bindings/*/*.cpp
$CLANG_FORMAT -i bindings/c/include/*.h
$CLANG_FORMAT -i bindings/wasm/**/*.{js,ts,html}
$CLANG_FORMAT -i src/!(third_party)/*/*.{h,cpp}
black bindings/python/examples/*.py
+4
View File
@@ -0,0 +1,4 @@
set(MANIFOLD_PAR "@MANIFOLD_PAR@")
set(MANIFOLD_EXPORT "@MANIFOLD_EXPORT@")
include("${CMAKE_CURRENT_LIST_DIR}/manifoldDeps.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/manifoldTargets.cmake")
+79
View File
@@ -0,0 +1,79 @@
include(FetchContent)
include(GNUInstallDirs)
find_package(PkgConfig QUIET)
find_package(Clipper2)
if(MANIFOLD_PAR STREQUAL "TBB")
find_package(TBB QUIET)
endif()
if (PKG_CONFIG_FOUND)
if (NOT Clipper2_FOUND)
pkg_check_modules(Clipper2 Clipper2)
endif()
if(MANIFOLD_PAR STREQUAL "TBB" AND NOT TBB_FOUND)
pkg_check_modules(TBB tbb)
endif()
endif()
if(Clipper2_FOUND)
add_library(Clipper2 SHARED IMPORTED)
set_property(TARGET Clipper2 PROPERTY
IMPORTED_LOCATION ${Clipper2_LINK_LIBRARIES})
if(WIN32)
set_property(TARGET Clipper2 PROPERTY
IMPORTED_IMPLIB ${Clipper2_LINK_LIBRARIES})
endif()
target_include_directories(Clipper2 INTERFACE ${Clipper2_INCLUDE_DIRS})
else()
message(STATUS "clipper2 not found, downloading from source")
set(CLIPPER2_UTILS OFF)
set(CLIPPER2_EXAMPLES OFF)
set(CLIPPER2_TESTS OFF)
set(CLIPPER2_USINGZ "OFF" CACHE STRING "Preempt cache default of USINGZ (we only use 2d)")
FetchContent_Declare(Clipper2
GIT_REPOSITORY https://github.com/AngusJohnson/Clipper2.git
GIT_TAG ff378668baae3570e9d8070aa9eb339bdd5a6aba
GIT_PROGRESS TRUE
SOURCE_SUBDIR CPP
)
FetchContent_MakeAvailable(Clipper2)
if(NOT EMSCRIPTEN)
install(TARGETS Clipper2)
endif()
endif()
find_package(glm QUIET)
if(NOT glm_FOUND)
message(STATUS "glm not found, downloading from source")
set(GLM_BUILD_INSTALL "ON" CACHE STRING "")
FetchContent_Declare(glm
GIT_REPOSITORY https://github.com/g-truc/glm.git
GIT_TAG 1.0.1
GIT_PROGRESS TRUE
)
FetchContent_MakeAvailable(glm)
if(NOT EMSCRIPTEN)
install(TARGETS glm)
endif()
endif()
if(MANIFOLD_PAR STREQUAL "TBB" AND NOT TBB_FOUND)
message(STATUS "tbb not found, downloading from source")
include(FetchContent)
set(TBB_TEST OFF CACHE INTERNAL "" FORCE)
set(TBB_STRICT OFF CACHE INTERNAL "" FORCE)
FetchContent_Declare(TBB
GIT_REPOSITORY https://github.com/oneapi-src/oneTBB.git
GIT_TAG v2021.11.0
GIT_PROGRESS TRUE
)
FetchContent_MakeAvailable(TBB)
set_property(DIRECTORY ${tbb_SOURCE_DIR} PROPERTY EXCLUDE_FROM_ALL YES)
# note: we do want to install tbb to the user machine when built from
# source
if(NOT EMSCRIPTEN)
install(TARGETS tbb)
endif()
endif()
if(MANIFOLD_EXPORT)
find_package(assimp REQUIRED)
endif()
+6 -5
View File
@@ -14,13 +14,11 @@
project(meshIO)
find_package(assimp REQUIRED)
add_library(${PROJECT_NAME} src/meshIO.cpp)
target_include_directories(${PROJECT_NAME}
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
target_include_directories(${PROJECT_NAME} PUBLIC
$<INSTALL_INTERFACE:include/${CMAKE_PROJECT_NAME}>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
target_link_libraries(${PROJECT_NAME}
PUBLIC manifold
@@ -29,3 +27,6 @@ target_link_libraries(${PROJECT_NAME}
target_compile_options(${PROJECT_NAME} PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
install(TARGETS ${PROJECT_NAME} EXPORT manifoldTargets)
install(FILES include/meshIO.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
+1
View File
@@ -234,6 +234,7 @@ void ExportMesh(const std::string& filename, const MeshGL& mesh,
? 1
: mesh.vertProperties[i * mesh.numProp +
options.mat.colorChannels[j]];
c = glm::saturate(c);
mesh_out->mColors[0][i] = aiColor4D(c.r, c.g, c.b, c.a);
}
}
Executable
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Modified from https://github.com/google/fuzztest/blob/main/tools/minimizer.sh
# If you want to use a specific reproducer, pass it in FUZZTEST_MINIMIZE_REPRODUCER
# environment variable.
# Otherwise, just run it as
# $ ./minimizer.sh ./build/test/polygon_fuzz --fuzz=PolygonFuzz.TriangulationNoCrash
# If you want to kill it, try SIGKILL, SIGTERM is not very useful
# If you want to end the loop early, rename $(pwd)/reproducers into something
# else...
if [ -z "${FUZZTEST_MINIMIZE_REPRODUCER}"]; then
mkdir -p $(pwd)/reproducers
FUZZTEST_REPRODUCERS_OUT_DIR=$(pwd)/reproducers "$@"
wildcard="$(pwd)/reproducers/*"
reproducers=($wildcard)
FUZZTEST_MINIMIZE_REPRODUCER="${reproducers[0]}"
fi
readonly ORIGINAL_REPRODUCER="${FUZZTEST_MINIMIZE_REPRODUCER}"
for i in {0001..9999}; do
echo
echo "╔════════════════════════════════════════════════╗"
echo "║ Minimization round: ${i} ║"
echo "╚════════════════════════════════════════════════╝"
echo "Note that to terminate early, simply move $ORIGINAL_REPRODUCER to somewhere else..."
echo
if [ ! -f $ORIGINAL_REPRODUCER ]; then
echo "Terminated by the user"
break
fi
TEMP_DIR=$(mktemp -d)
FUZZTEST_REPRODUCERS_OUT_DIR="${TEMP_DIR}" \
FUZZTEST_MINIMIZE_REPRODUCER="${FUZZTEST_MINIMIZE_REPRODUCER}" \
"$@"
if [ $? -eq 130 ]; then
echo
echo "╔═══════════════════════════════════════════════╗"
echo "║ Minimization terminated. ║"
echo "╚═══════════════════════════════════════════════╝"
echo
echo "Find the smallest reproducer at:"
echo
echo "${FUZZTEST_MINIMIZE_REPRODUCER}"
rm -rf "${TEMP_DIR}"
break
fi
SMALLER_REPRODUCER=$(find "${TEMP_DIR}" -type f)
NEW_NAME="${ORIGINAL_REPRODUCER}-min-${i}"
mv "${SMALLER_REPRODUCER}" "${NEW_NAME}"
FUZZTEST_MINIMIZE_REPRODUCER="${NEW_NAME}"
rm -rf "${TEMP_DIR}"
done
+72
View File
@@ -0,0 +1,72 @@
[project]
name = "manifold3d"
version = "2.5.1"
authors = [
{ name="Emmett Lalish", email="elalish@gmail.com" },
]
description = "Library for geometric robustness"
readme = "README.md"
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: C++",
"Topic :: Multimedia :: Graphics :: 3D Modeling",
]
requires-python = ">=3.8"
dependencies = [
"numpy; python_version<'3.12'",
"numpy>=1.26.0b1; python_version>='3.12'"
]
[project.urls]
"Homepage" = "https://github.com/elalish/manifold"
"Bug Tracker" = "https://github.com/elalish/manifold/issues"
[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"
[tool.scikit-build]
cmake.minimum-version = "3.18"
sdist.exclude = [
".github",
"bindings/c",
"bindings/wasm",
"docs",
"extras",
"meshIO",
"samples",
"test",
"oneTBB", # we may have this when we build with cibuildwheel
"bindings/python/third_party/nanobind/tests/",
"bindings/python/third_party/nanobind/docs/",
]
wheel.packages = ["manifold3d"]
cmake.args = ["-DMANIFOLD_PYBIND=ON", "-DMANIFOLD_PAR=TBB", "-DMANIFOLD_TEST=OFF", "-DBUILD_SHARED_LIBS=OFF"]
install.components = ["bindings"]
[tool.cibuildwheel]
build-frontend = "build"
test-requires = ["trimesh", "pytest"]
test-command = "python {project}/bindings/python/examples/run_all.py"
# Setuptools bug causes collision between pypy and cpython artifacts
manylinux-x86_64-image = "manylinux_2_28"
musllinux-x86_64-image = "musllinux_1_2"
skip = ["*-win32", "*_i686", "pp*", "*-musllinux*"]
# only clone TBB once
before-all = "git clone --depth 1 --branch v2021.10.0 https://github.com/oneapi-src/oneTBB.git"
[tool.cibuildwheel.config-settings]
"cmake.define.FETCHCONTENT_SOURCE_DIR_TBB" = "./oneTBB"
"cmake.define.FETCHCONTENT_UPDATES_DISCONNECTED" = "ON"
[tool.cibuildwheel.macos]
archs = ["x86_64", "arm64"]
environment = "MACOSX_DEPLOYMENT_TARGET=10.14"
test-skip = "*-macosx_arm64 *-macosx_universal2:arm64"
[tool.cibuildwheel.windows]
before-build = "pip install delvewheel"
repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}"
+3
View File
@@ -0,0 +1,3 @@
[pytest]
testpaths =
bindings/python/examples
+4 -24
View File
@@ -14,37 +14,17 @@
project(samples)
add_library(samples src/bracelet.cpp src/knot.cpp src/menger_sponge.cpp src/rounded_frame.cpp src/scallop.cpp src/tet_puzzle.cpp)
add_library(samples src/bracelet.cpp src/knot.cpp src/menger_sponge.cpp
src/rounded_frame.cpp src/scallop.cpp src/tet_puzzle.cpp
src/gyroid_module.cpp src/condensed_matter.cpp)
target_include_directories(samples
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(samples
PUBLIC manifold
PUBLIC manifold sdf
)
target_compile_options(samples PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(samples PUBLIC cxx_std_17)
project(samplesGPU)
add_library(samplesGPU src/gyroid_module.cpp)
if(MANIFOLD_USE_CUDA)
set_source_files_properties(src/gyroid_module.cpp PROPERTIES LANGUAGE CUDA)
set_property(TARGET samplesGPU PROPERTY CUDA_ARCHITECTURES 61)
endif()
target_include_directories(samplesGPU
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(samplesGPU
PUBLIC manifold sdf
)
target_compile_options(samplesGPU PRIVATE ${MANIFOLD_FLAGS})
target_compile_features(samplesGPU
PUBLIC cxx_std_17
)
+2
View File
@@ -51,5 +51,7 @@ Manifold TetPuzzle(float edgeLength, float gap, int nDivisions);
Manifold Scallop();
Manifold GyroidModule(float size = 20, int n = 20);
Manifold CondensedMatter(int fn = 16);
/** @} */ // end of Samples
} // namespace manifold
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

+149
View File
@@ -0,0 +1,149 @@
// Copyright 2023 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// From
// https://gist.github.com/ochafik/70a6b15e982b7ccd5a79ff9afd99dbcf#file-condensed-matter-scad
#include "samples.h"
namespace {
using namespace manifold;
using namespace glm;
constexpr float AtomicRadiusN2 = 0.65;
constexpr float BondPairN2 = 1.197;
constexpr float AtomicRadiusSi = 1.1;
constexpr float LatticeCellSizeSi = 5.4309;
constexpr float fccOffset = 0.25;
constexpr float AtomicRadiusC = 0.7;
constexpr float LatticeCellSizeC = 3.65;
constexpr float cellLenA = 2.464;
constexpr float cellLenB = cellLenA;
constexpr float cellLenC = 6.711;
constexpr float cellAngleA = 90;
constexpr float cellAngleB = cellAngleA;
constexpr float cellAngleC = 120;
constexpr float LayerSeperationC = 3.364;
Manifold bond(int fn, vec3 p1 = {0, 0, 0}, vec3 p2 = {1, 1, 1}, float ar1 = 1.0,
float ar2 = 2.0) {
float cyR = std::min(ar1, ar2) / 5.0;
float dist = length(p1 - p2);
vec3 cyC = (p1 + p2) / 2.0f;
float beta = degrees(acos((p1.z - p2.z) / dist));
float gamma = degrees(atan2(p1.y - p2.y, p1.x - p2.x));
vec3 rot = {0.0, beta, gamma};
return Manifold::Cylinder(dist, cyR, -1, fn, true)
.Rotate(rot.x, rot.y, rot.z)
.Translate(cyC);
}
Manifold bondPair(int fn, float d = 0.0, float ar = 1.0) {
float axD = pow(d, 1.0 / 3.0);
vec3 p1 = {+axD, -axD, -axD};
vec3 p2 = {-axD, +axD, +axD};
Manifold sphere = Manifold::Sphere(ar, fn);
return sphere.Translate(p1) + sphere.Translate(p2) + bond(fn, p1, p2, ar, ar);
}
Manifold hexagonalClosePacked(int fn, vec3 dst = {1.0, 1.0, 1.0},
float ar = 1.0) {
std::vector<Manifold> parts;
vec3 p1 = {0, 0, 0};
parts.push_back(Manifold::Sphere(ar, fn));
float baseAg = 30;
vec3 ag = {baseAg, baseAg + 120, baseAg + 240};
vec3 points[] = {{cosd(ag.x) * dst.x, sind(ag.x) * dst.x, 0},
{cosd(ag.y) * dst.y, sind(ag.y) * dst.y, 0},
{cosd(ag.z) * dst.z, sind(ag.z) * dst.z, 0}};
for (vec3 p2 : points) {
parts.push_back(Manifold::Sphere(ar, fn).Translate(p2));
parts.push_back(bond(fn, p1, p2, ar, ar));
}
return Manifold::BatchBoolean(parts, OpType::Add);
}
Manifold fccDiamond(int fn, float ar = 1.0, float unitCell = 2.0,
float fccOffset = 0.25) {
std::vector<Manifold> parts;
float huc = unitCell / 2.0;
float od = fccOffset * unitCell;
vec3 interstitial[] = {
{+od, +od, +od}, {+od, -od, -od}, {-od, +od, -od}, {-od, -od, +od}};
vec3 corners[] = {{+huc, +huc, +huc},
{+huc, -huc, -huc},
{-huc, +huc, -huc},
{-huc, -huc, +huc}};
vec3 fcc[] = {{+huc, 0, 0}, {-huc, 0, 0}, {0, +huc, 0},
{0, -huc, 0}, {0, 0, +huc}, {0, 0, -huc}};
for (auto p : corners) parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
for (auto p : fcc) parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
for (auto p : interstitial)
parts.push_back(Manifold::Sphere(ar, fn).Translate(p));
vec3 bonds[][2] = {{interstitial[0], corners[0]}, {interstitial[0], fcc[0]},
{interstitial[0], fcc[2]}, {interstitial[0], fcc[4]},
{interstitial[1], corners[1]}, {interstitial[1], fcc[0]},
{interstitial[1], fcc[3]}, {interstitial[1], fcc[5]},
{interstitial[2], corners[2]}, {interstitial[2], fcc[1]},
{interstitial[2], fcc[2]}, {interstitial[2], fcc[5]},
{interstitial[3], corners[3]}, {interstitial[3], fcc[1]},
{interstitial[3], fcc[3]}, {interstitial[3], fcc[4]}};
for (auto b : bonds) parts.push_back(bond(fn, b[0], b[1], ar, ar));
return Manifold::BatchBoolean(parts, OpType::Add);
}
Manifold SiCell(int fn, float x = 1.0, float y = 1.0, float z = 1.0) {
return fccDiamond(fn, AtomicRadiusSi, LatticeCellSizeSi, fccOffset)
.Translate({LatticeCellSizeSi * x, LatticeCellSizeSi * y,
LatticeCellSizeSi * z});
}
Manifold SiN2Cell(int fn, float x = 1.0, float y = 1.0, float z = 1.0) {
float n2Offset = LatticeCellSizeSi / 8;
return bondPair(fn, BondPairN2, AtomicRadiusN2)
.Translate({LatticeCellSizeSi * x - n2Offset,
LatticeCellSizeSi * y + n2Offset,
LatticeCellSizeSi * z + n2Offset}) +
SiCell(fn, x, y, z);
}
Manifold GraphiteCell(int fn, vec3 xyz = {1.0, 1.0, 1.0}) {
vec3 loc = {(cellLenA * xyz.x * cosd(30) * 2),
((cellLenB * sind(30)) + cellLenC) * xyz.y, xyz.z};
return hexagonalClosePacked(fn, {cellLenA, cellLenB, cellLenC}, AtomicRadiusC)
.Translate(loc);
}
} // namespace
namespace manifold {
Manifold CondensedMatter(int fn) {
std::vector<Manifold> parts;
float siOffset = 3.0 * LatticeCellSizeSi / 8.0;
for (int x = -3; x <= 3; x++)
for (int y = -1; y <= 2; y++)
parts.push_back(
GraphiteCell(fn, {x + (y % 2 == 0 ? 0.0 : 0.5), y,
LayerSeperationC * 0.5 + LatticeCellSizeSi * 1.5})
.Translate({0, -siOffset, 0})
.Rotate(0, 0, 45));
float xyPlane[] = {-2, -1, 0, +1, +2};
for (float x : xyPlane)
for (float y : xyPlane) parts.push_back(SiN2Cell(fn, x, y, 1));
return Manifold::BatchBoolean(parts, OpType::Add);
}
} // namespace manifold

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