diff --git a/README.md b/README.md index 8f2392473..d489dc5e8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Users -[OpenSCAD](https://openscad.org/), [IFCjs](https://ifcjs.github.io/info/), [Grid.Space](https://grid.space/), and [OCADml](https://github.com/OCADml/OManifold) have all integrated our Manifold geometry kernel! Why? Because its reliability is guaranteed and it's 1,000 times faster than other libraries. See our [usage](https://github.com/elalish/manifold/discussions/340) and [performance](https://github.com/elalish/manifold/discussions/383) discussions for all the latest and to add your own projects & analyses. +[OpenSCAD](https://openscad.org/), [IFCjs](https://ifcjs.github.io/info/), [Grid.Space](https://grid.space/), and [OCADml](https://github.com/OCADml/OManifold) have all integrated our Manifold geometry kernel! Why? Because its reliability is guaranteed and it's 1,000 times faster than other libraries. See our [usage](https://github.com/elalish/manifold/discussions/340) and [performance](https://github.com/elalish/manifold/discussions/383) discussions for all the latest and to add your own projects & analyses. For example, here is a log-log plot of Manifold's performance vs. earlier OpenSCAD geometry backends: @@ -15,15 +15,23 @@ If you like OpenSCAD / JSCAD, you might also like ManifoldCAD - our own solid mo ![A metallic Menger sponge](https://elalish.github.io/manifold/samples/models/mengerSponge3.webp "A metallic Menger sponge") +### Note for Firefox users + +If you find the editor is stuck on **Loading...**, setting +`dom.workers.modules.enabled: true` in your `about:config`, as mentioned in the +discussion of the +[issue#328](https://github.com/elalish/manifold/issues/328#issuecomment-1473847102) +of this repository may solve the problem. + # Manifold [**API Documentation**](https://elalish.github.io/manifold/docs/html/modules.html) | [**Algorithm Documentation**](https://github.com/elalish/manifold/wiki/Manifold-Library) | [**Blog Posts**](https://elalish.blogspot.com/search/label/Manifold) | [**Web Examples**](https://elalish.github.io/manifold/model-viewer.html) [Manifold](https://github.com/elalish/manifold) is a geometry library dedicated to creating and operating on manifold triangle meshes. A [manifold mesh](https://github.com/elalish/manifold/wiki/Manifold-Library#manifoldness) is a mesh that represents a solid object, and so is very important in manufacturing, CAD, structural analysis, etc. Further information can be found on the [wiki](https://github.com/elalish/manifold/wiki/Manifold-Library). -This is a modern C++ library that Github's CI verifies builds and runs on a variety of platforms. Additionally, we build bindings for JavaScript ([manifold-3d](https://www.npmjs.com/package/manifold-3d) on npm), Python, and C to make this library more portable and easy to use. +This is a modern C++ library that Github's CI verifies builds and runs on a variety of platforms. Additionally, we build bindings for JavaScript ([manifold-3d](https://www.npmjs.com/package/manifold-3d) on npm), Python, and C to make this library more portable and easy to use. -We have four core dependencies, making use of submodules to ensure compatibility: +We have four core dependencies, making use of submodules to ensure compatibility: - `graphlite`: connected components algorithm - `Clipper2`: provides our 2D subsystem - `GLM`: a compact vector library @@ -31,7 +39,7 @@ We have four core dependencies, making use of submodules to ensure compatibility ## What's here -This library is fast with guaranteed manifold output. As such you need manifold meshes as input, which this library can create using constructors inspired by the OpenSCAD API, as well as more advanced features like smoothing and signed-distance function (SDF) level sets. You can also pass in your own mesh data, but you'll get an error status if the imported mesh isn't manifold. Various automated repair tools exist online for fixing non manifold models, usually for 3D printing. +This library is fast with guaranteed manifold output. As such you need manifold meshes as input, which this library can create using constructors inspired by the OpenSCAD API, as well as more advanced features like smoothing and signed-distance function (SDF) level sets. You can also pass in your own mesh data, but you'll get an error status if the imported mesh isn't manifold. Various automated repair tools exist online for fixing non manifold models, usually for 3D printing. The most significant contribution here is a guaranteed-manifold [mesh Boolean](https://github.com/elalish/manifold/wiki/Manifold-Library#mesh-boolean) algorithm, which I believe is the first of its kind. If you know of another, please open a discussion - a mesh Boolean algorithm robust to edge cases has been an open problem for many years. Likewise, if the Boolean here ever fails you, please submit an issue! This Boolean forms the basis of a CAD kernel, as it allows simple shapes to be combined into more complex ones. @@ -41,7 +49,7 @@ Look in the [samples](https://github.com/elalish/manifold/tree/master/samples) d ## Building -Only CMake, a C++ compiler, and Python are required to be installed and set up to build this library (it has been tested with GCC, LLVM, MSVC). However, a variety of optional dependencies can bring in more functionality, see below. +Only CMake, a C++ compiler, and Python are required to be installed and set up to build this library (it has been tested with GCC, LLVM, MSVC). However, a variety of optional dependencies can bring in more functionality, see below. Build and test (Ubuntu or similar): ``` diff --git a/bindings/wasm/CMakeLists.txt b/bindings/wasm/CMakeLists.txt index 655394b13..876428b54 100644 --- a/bindings/wasm/CMakeLists.txt +++ b/bindings/wasm/CMakeLists.txt @@ -16,9 +16,7 @@ project(wasm) add_executable(manifoldjs bindings.cpp) -# make sure that we recompile the wasm when bindings.js is being modified -set_source_files_properties(bindings.cpp OBJECT_DEPENDS - ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js) +set_source_files_properties(bindings.cpp PROPERTIES OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js) target_link_libraries(manifoldjs manifold sdf cross_section polygon) target_compile_options(manifoldjs PRIVATE ${MANIFOLD_FLAGS}) target_link_options(manifoldjs PUBLIC --pre-js ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js --bind -sALLOW_TABLE_GROWTH=1 @@ -29,15 +27,20 @@ set_target_properties(manifoldjs PROPERTIES OUTPUT_NAME "manifold") file(MAKE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/examples/built) +# ensure that interface files are copied over when modified +add_custom_target(js_deps ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/manifold.* + ${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts) + add_custom_command( - TARGET manifoldjs POST_BUILD + TARGET js_deps POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_BINARY_DIR}/manifold.* - ${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts - ${CMAKE_CURRENT_SOURCE_DIR}/examples/built/) + ${CMAKE_CURRENT_BINARY_DIR}/manifold.* + ${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts + ${CMAKE_CURRENT_SOURCE_DIR}/examples/built/) add_custom_command( - TARGET manifoldjs POST_BUILD + TARGET js_deps POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts - ${CMAKE_CURRENT_SOURCE_DIR}/examples/public/) + ${CMAKE_CURRENT_SOURCE_DIR}/manifold*.d.ts + ${CMAKE_CURRENT_SOURCE_DIR}/examples/public/) diff --git a/bindings/wasm/bindings.cpp b/bindings/wasm/bindings.cpp index 06434a196..7113af32b 100644 --- a/bindings/wasm/bindings.cpp +++ b/bindings/wasm/bindings.cpp @@ -14,178 +14,17 @@ #include #include - -#include - -using namespace emscripten; - #include #include #include -using namespace manifold; - -Manifold Union(const Manifold& a, const Manifold& b) { return a + b; } - -Manifold Difference(const Manifold& a, const Manifold& b) { return a - b; } - -Manifold Intersection(const Manifold& a, const Manifold& b) { return a ^ b; } - -Manifold UnionN(const std::vector& manifolds) { - return Manifold::BatchBoolean(manifolds, OpType::Add); -} - -Manifold DifferenceN(const std::vector& manifolds) { - return Manifold::BatchBoolean(manifolds, OpType::Subtract); -} - -Manifold IntersectionN(const std::vector& manifolds) { - return Manifold::BatchBoolean(manifolds, OpType::Intersect); -} - -std::vector ToPolygon( - std::vector>& polygons) { - std::vector simplePolygons(polygons.size()); - for (int i = 0; i < polygons.size(); i++) { - std::vector vertices(polygons[i].size()); - for (int j = 0; j < polygons[i].size(); j++) { - vertices[j] = polygons[i][j]; - } - simplePolygons[i] = {vertices}; - } - return simplePolygons; -} - -val MeshGL2JS(const MeshGL& mesh) { - val meshJS = val::object(); - - meshJS.set("numProp", mesh.numProp); - meshJS.set("triVerts", - val(typed_memory_view(mesh.triVerts.size(), mesh.triVerts.data())) - .call("slice")); - meshJS.set("vertProperties", - val(typed_memory_view(mesh.vertProperties.size(), - mesh.vertProperties.data())) - .call("slice")); - meshJS.set("mergeFromVert", val(typed_memory_view(mesh.mergeFromVert.size(), - mesh.mergeFromVert.data())) - .call("slice")); - meshJS.set("mergeToVert", val(typed_memory_view(mesh.mergeToVert.size(), - mesh.mergeToVert.data())) - .call("slice")); - meshJS.set("runIndex", - val(typed_memory_view(mesh.runIndex.size(), mesh.runIndex.data())) - .call("slice")); - meshJS.set("runOriginalID", val(typed_memory_view(mesh.runOriginalID.size(), - mesh.runOriginalID.data())) - .call("slice")); - meshJS.set("faceID", - val(typed_memory_view(mesh.faceID.size(), mesh.faceID.data())) - .call("slice")); - meshJS.set("halfedgeTangent", - val(typed_memory_view(mesh.halfedgeTangent.size(), - mesh.halfedgeTangent.data())) - .call("slice")); - meshJS.set("runTransform", val(typed_memory_view(mesh.runTransform.size(), - mesh.runTransform.data())) - .call("slice")); - - return meshJS; -} - -MeshGL MeshJS2GL(const val& mesh) { - MeshGL out; - out.numProp = mesh["numProp"].as(); - out.triVerts = convertJSArrayToNumberVector(mesh["triVerts"]); - out.vertProperties = - convertJSArrayToNumberVector(mesh["vertProperties"]); - if (mesh["mergeFromVert"] != val::undefined()) { - out.mergeFromVert = - convertJSArrayToNumberVector(mesh["mergeFromVert"]); - } - if (mesh["mergeToVert"] != val::undefined()) { - out.mergeToVert = - convertJSArrayToNumberVector(mesh["mergeToVert"]); - } - if (mesh["runIndex"] != val::undefined()) { - out.runIndex = convertJSArrayToNumberVector(mesh["runIndex"]); - } - if (mesh["runOriginalID"] != val::undefined()) { - out.runOriginalID = - convertJSArrayToNumberVector(mesh["runOriginalID"]); - } - if (mesh["faceID"] != val::undefined()) { - out.faceID = convertJSArrayToNumberVector(mesh["faceID"]); - } - if (mesh["halfedgeTangent"] != val::undefined()) { - out.halfedgeTangent = - convertJSArrayToNumberVector(mesh["halfedgeTangent"]); - } - if (mesh["runTransform"] != val::undefined()) { - out.runTransform = - convertJSArrayToNumberVector(mesh["runTransform"]); - } - return out; -} - -val GetMeshJS(const Manifold& manifold, const glm::ivec3& normalIdx) { - MeshGL mesh = manifold.GetMeshGL(normalIdx); - return MeshGL2JS(mesh); -} - -val Merge(const val& mesh) { - val out = val::object(); - MeshGL meshGL = MeshJS2GL(mesh); - bool changed = meshGL.Merge(); - out.set("changed", changed); - out.set("mesh", changed ? MeshGL2JS(meshGL) : mesh); - return out; -} - -Manifold FromMeshJS(const val& mesh) { return Manifold(MeshJS2GL(mesh)); } - -Manifold Smooth(const val& mesh, - const std::vector& sharpenedEdges = {}) { - return Manifold::Smooth(MeshJS2GL(mesh), sharpenedEdges); -} - -Manifold Extrude(std::vector>& polygons, float height, - int nDivisions, float twistDegrees, glm::vec2 scaleTop) { - return Manifold::Extrude(ToPolygon(polygons), height, nDivisions, - twistDegrees, scaleTop); -} - -Manifold Revolve(std::vector>& polygons, - int circularSegments) { - return Manifold::Revolve(ToPolygon(polygons), circularSegments); -} - -Manifold Transform(Manifold& manifold, const val& mat) { - std::vector array = convertJSArrayToNumberVector(mat); - glm::mat4x3 matrix; - for (const int col : {0, 1, 2, 3}) - for (const int row : {0, 1, 2}) matrix[col][row] = array[col * 4 + row]; - return manifold.Transform(matrix); -} - -Manifold Warp(Manifold& manifold, uintptr_t funcPtr) { - void (*f)(glm::vec3&) = reinterpret_cast(funcPtr); - return manifold.Warp(f); -} +#include -Manifold SetProperties(Manifold& manifold, int numProp, uintptr_t funcPtr) { - void (*f)(float*, glm::vec3, const float*) = - reinterpret_cast(funcPtr); - return manifold.SetProperties(numProp, f); -} +#include "cross_section.h" +#include "helpers.cpp" -Manifold LevelSetJs(uintptr_t funcPtr, Box bounds, float edgeLength, - float level) { - float (*f)(const glm::vec3&) = - reinterpret_cast(funcPtr); - Mesh m = LevelSet(f, bounds, edgeLength, level); - return Manifold(m); -} +using namespace emscripten; +using namespace manifold; EMSCRIPTEN_BINDINGS(whatever) { value_object("vec2") @@ -224,6 +63,18 @@ EMSCRIPTEN_BINDINGS(whatever) { .value("FaceIDWrongLength", Manifold::Error::FaceIDWrongLength) .value("InvalidConstruction", Manifold::Error::InvalidConstruction); + enum_("fillrule") + .value("EvenOdd", CrossSection::FillRule::EvenOdd) + .value("NonZero", CrossSection::FillRule::NonZero) + .value("Positive", CrossSection::FillRule::Positive) + .value("Negative", CrossSection::FillRule::Negative); + + enum_("jointype") + .value("Square", CrossSection::JoinType::Square) + .value("Round", CrossSection::JoinType::Round) + .value("Miter", CrossSection::JoinType::Miter); + + value_object("rect").field("min", &Rect::min).field("max", &Rect::max); value_object("box").field("min", &Box::min).field("max", &Box::max); value_object("smoothness") @@ -247,21 +98,54 @@ EMSCRIPTEN_BINDINGS(whatever) { register_vector("Vector_vec2"); register_vector>("Vector2_vec2"); register_vector("Vector_f32"); + register_vector("Vector_crossSection"); register_vector("Vector_manifold"); register_vector("Vector_smoothness"); register_vector("Vector_vec4"); + class_("CrossSection") + .constructor(&cross_js::OfPolygons) + .function("_add", &cross_js::Union) + .function("_subtract", &cross_js::Difference) + .function("_intersect", &cross_js::Intersection) + .function("_Warp", &cross_js::Warp) + .function("transform", &cross_js::Transform) + .function("_Translate", &CrossSection::Translate) + .function("_Rotate", &CrossSection::Rotate) + .function("_Scale", &CrossSection::Scale) + .function("_Mirror", &CrossSection::Mirror) + .function("_Decompose", &CrossSection::Decompose) + .function("isEmpty", &CrossSection::IsEmpty) + .function("area", &CrossSection::Area) + .function("numVert", &CrossSection::NumVert) + .function("numContour", &CrossSection::NumContour) + .function("_Bounds", &CrossSection::Bounds) + .function("simplify", &CrossSection::Simplify) + .function("_Offset", &cross_js::Offset) + .function("_RectClip", &CrossSection::RectClip) + .function("_ToPolygons", &CrossSection::ToPolygons); + + // CrossSection Static Methods + function("_Square", &CrossSection::Square); + function("_Circle", &CrossSection::Circle); + function("_crossSectionCompose", &CrossSection::Compose); + function("_crossSectionUnionN", &cross_js::UnionN); + function("_crossSectionDifferenceN", &cross_js::DifferenceN); + function("_crossSectionIntersectionN", &cross_js::IntersectionN); + class_("Manifold") - .constructor(&FromMeshJS) - .function("add", &Union) - .function("subtract", &Difference) - .function("intersect", &Intersection) + .constructor(&man_js::FromMeshJS) + .function("add", &man_js::Union) + .function("subtract", &man_js::Difference) + .function("intersect", &man_js::Intersection) + .function("_Split", &man_js::Split) + .function("_SplitByPlane", &man_js::SplitByPlane) .function("_TrimByPlane", &Manifold::TrimByPlane) - .function("_GetMeshJS", &GetMeshJS) + .function("_GetMeshJS", &js::GetMeshJS) .function("refine", &Manifold::Refine) - .function("_Warp", &Warp) - .function("_SetProperties", &SetProperties) - .function("transform", &Transform) + .function("_Warp", &man_js::Warp) + .function("_SetProperties", &man_js::SetProperties) + .function("transform", &man_js::Transform) .function("_Translate", &Manifold::Translate) .function("_Rotate", &Manifold::Rotate) .function("_Scale", &Manifold::Scale) @@ -281,25 +165,26 @@ EMSCRIPTEN_BINDINGS(whatever) { .function("originalID", &Manifold::OriginalID) .function("asOriginal", &Manifold::AsOriginal); + // Manifold Static Methods function("_Cube", &Manifold::Cube); function("_Cylinder", &Manifold::Cylinder); function("_Sphere", &Manifold::Sphere); - function("tetrahedron", &Manifold::Tetrahedron); - function("_Smooth", &Smooth); - function("_Extrude", &Extrude); + function("_Tetrahedron", &Manifold::Tetrahedron); + function("_Smooth", &js::Smooth); + function("_Extrude", &Manifold::Extrude); function("_Triangulate", &Triangulate); - function("_Revolve", &Revolve); - function("_LevelSet", &LevelSetJs); - function("_Merge", &Merge); - - function("_unionN", &UnionN); - function("_differenceN", &DifferenceN); - function("_intersectionN", &IntersectionN); - function("_Compose", &Manifold::Compose); - + function("_Revolve", &Manifold::Revolve); + function("_LevelSet", &man_js::LevelSet); + function("_Merge", &js::Merge); + function("_ReserveIDs", &Manifold::ReserveIDs); + function("_manifoldCompose", &Manifold::Compose); + function("_manifoldUnionN", &man_js::UnionN); + function("_manifoldDifferenceN", &man_js::DifferenceN); + function("_manifoldIntersectionN", &man_js::IntersectionN); + + // Quality Globals function("setMinCircularAngle", &Quality::SetMinCircularAngle); function("setMinCircularEdgeLength", &Quality::SetMinCircularEdgeLength); function("setCircularSegments", &Quality::SetCircularSegments); function("getCircularSegments", &Quality::GetCircularSegments); - function("reserveIDs", &Manifold::ReserveIDs); } diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index 3cfdd6ac4..0c4da85d5 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -17,6 +17,8 @@ Module.setup = function() { if (_ManifoldInitialized) return; _ManifoldInitialized = true; + // conversion utilities + function toVec(vec, list, f = x => x) { if (list) { for (let x of list) { @@ -33,6 +35,21 @@ Module.setup = function() { return result; } + function vec2polygons(vec, f = x => x) { + const result = []; + const nPoly = vec.size(); + for (let i = 0; i < nPoly; i++) { + const v = vec.get(i); + const nPts = v.size(); + const poly = []; + for (let j = 0; j < nPts; j++) { + poly.push(f(v.get(j))); + } + result.push(poly); + } + return result; + } + function polygons2vec(polygons) { if (polygons[0].length < 3) { polygons = [polygons]; @@ -50,7 +67,15 @@ Module.setup = function() { polygonsVec.delete(); } - function vararg2vec(vec) { + function vararg2vec2(vec) { + if (vec[0] instanceof Array) return {x: vec[0][0], y: vec[0][1]}; + if (typeof (vec[0]) == 'number') + // default to 0 + return {x: vec[0] || 0, y: vec[1] || 0}; + return vec[0]; + } + + function vararg2vec3(vec) { if (vec[0] instanceof Array) return {x: vec[0][0], y: vec[0][1], z: vec[0][2]}; if (typeof (vec[0]) == 'number') @@ -59,6 +84,129 @@ Module.setup = function() { return vec[0]; } + function fillRuleToInt(fillRule) { + return fillRule == 'EvenOdd' ? 0 : + fillRule == 'NonZero' ? 1 : + fillRule == 'Negative' ? 3 : + /* Positive */ 2; + } + + function joinTypeToInt(joinType) { + return joinType == 'Round' ? 1 : joinType == 'Miter' ? 2 : /* Square */ 0; + } + + // CrossSection methods + + const CrossSectionCtor = Module.CrossSection; + + function cross(polygons, fillRule = 'Positive') { + if (polygons instanceof CrossSectionCtor) { + return polygons; + } else { + const polygonsVec = polygons2vec(polygons); + const cs = new CrossSectionCtor(polygonsVec, fillRuleToInt(fillRule)); + disposePolygons(polygonsVec); + return cs; + } + }; + + Module.CrossSection.prototype.translate = function(...vec) { + return this._Translate(vararg2vec2(vec)); + }; + + Module.CrossSection.prototype.rotate = function(vec) { + return this._Rotate(...vec); + }; + + Module.CrossSection.prototype.scale = function(vec) { + // if only one factor provided, scale both x and y with it + if (typeof vec == 'number') { + return this._Scale({x: vec, y: vec}); + } + return this._Scale(vararg2vec2([vec])); + }; + + Module.CrossSection.prototype.mirror = function(vec) { + return this._Mirror(vararg2vec2([vec])); + }; + + Module.CrossSection.prototype.warp = function(func) { + const wasmFuncPtr = addFunction(function(vec2Ptr) { + const x = getValue(vec2Ptr, 'float'); + const y = getValue(vec2Ptr + 4, 'float'); + const vert = [x, y]; + func(vert); + setValue(vec2Ptr, vert[0], 'float'); + setValue(vec2Ptr + 4, vert[1], 'float'); + }, 'vi'); + const out = this._Warp(wasmFuncPtr); + removeFunction(wasmFuncPtr); + return out; + }; + + Module.CrossSection.prototype.decompose = function() { + const vec = this._Decompose(); + const result = fromVec(vec); + vec.delete(); + return result; + }; + + Module.CrossSection.prototype.bounds = function() { + const result = this._Bounds(); + return { + min: ['x', 'y'].map(f => result.min[f]), + max: ['x', 'y'].map(f => result.max[f]), + }; + }; + + Module.CrossSection.prototype.offset = function( + delta, joinType = 'Square', miterLimit = 2.0, arcTolerance = 0.) { + return this._Offset( + delta, joinTypeToInt(joinType), miterLimit, arcTolerance); + }; + + Module.CrossSection.prototype.rectClip = function(rect) { + const rect2 = { + min: {x: rect.min[0], y: rect.min[1]}, + max: {x: rect.max[0], y: rect.max[1]}, + }; + return this._RectClip(rect2); + }; + + Module.CrossSection.prototype.extrude = function( + height, nDivisions = 0, twistDegrees = 0.0, scaleTop = [1.0, 1.0], + center = false) { + scaleTop = vararg2vec2([scaleTop]); + const man = + Module._Extrude(this, height, nDivisions, twistDegrees, scaleTop); + return (center ? man.translate([0., 0., -height / 2.]) : man); + }; + + Module.CrossSection.prototype.revolve = function(circularSegments = 0) { + return Module._Revolve(this, circularSegments); + }; + + Module.CrossSection.prototype.add = function(other) { + return this._add(cross(other)); + }; + + Module.CrossSection.prototype.subtract = function(other) { + return this._subtract(cross(other)); + }; + + Module.CrossSection.prototype.intersect = function(other) { + return this._intersect(cross(other)); + }; + + Module.CrossSection.prototype.toPolygons = function() { + const vec = this._ToPolygons(); + const result = vec2polygons(vec); + vec.delete(); + return result; + }; + + // Manifold methods + Module.Manifold.prototype.warp = function(func) { const wasmFuncPtr = addFunction(function(vec3Ptr) { const x = getValue(vec3Ptr, 'float'); @@ -108,7 +256,7 @@ Module.setup = function() { }; Module.Manifold.prototype.translate = function(...vec) { - return this._Translate(vararg2vec(vec)); + return this._Translate(vararg2vec3(vec)); }; Module.Manifold.prototype.rotate = function(vec) { @@ -116,18 +264,33 @@ Module.setup = function() { }; Module.Manifold.prototype.scale = function(vec) { + // if only one factor provided, scale all three dimensions (xyz) with it if (typeof vec == 'number') { return this._Scale({x: vec, y: vec, z: vec}); } - return this._Scale(vararg2vec([vec])); + return this._Scale(vararg2vec3([vec])); }; Module.Manifold.prototype.mirror = function(vec) { - return this._Mirror(vararg2vec([vec])); + return this._Mirror(vararg2vec3([vec])); }; - Module.Manifold.prototype.trimByPlane = function(normal, offset) { - return this._TrimByPlane(vararg2vec([normal]), offset); + Module.Manifold.prototype.trimByPlane = function(normal, offset = 0.) { + return this._TrimByPlane(vararg2vec3([normal]), offset); + }; + + Module.Manifold.prototype.split = function(manifold) { + const vec = this._split(manifold); + const result = fromVec(vec); + vec.delete(); + return result; + }; + + Module.Manifold.prototype.splitByPlane = function(normal, offset = 0.) { + const vec = this._splitByPlane(vararg2vec3([normal]), offset); + const result = fromVec(vec); + vec.delete(); + return result; }; Module.Manifold.prototype.decompose = function() { @@ -148,6 +311,14 @@ Module.setup = function() { return result; }; + Module.Manifold.prototype.boundingBox = function() { + const result = this._boundingBox(); + return { + min: ['x', 'y', 'z'].map(f => result.min[f]), + max: ['x', 'y', 'z'].map(f => result.max[f]), + }; + }; + class Mesh { constructor({ numProp = 3, @@ -228,14 +399,6 @@ Module.setup = function() { return new Mesh(this._GetMeshJS(normalIdx)); }; - Module.Manifold.prototype.boundingBox = function() { - const result = this._boundingBox(); - return { - min: ['x', 'y', 'z'].map(f => result.min[f]), - max: ['x', 'y', 'z'].map(f => result.max[f]), - }; - }; - Module.ManifoldError = function ManifoldError(code, ...args) { let message = 'Unknown error'; switch (code) { @@ -284,6 +447,65 @@ Module.setup = function() { {value: Module.ManifoldError, writable: true, configurable: true} }); + // CrossSection Constructors + + Module.CrossSection = function(polygons, fillRule = 'Positive') { + const polygonsVec = polygons2vec(polygons); + const cs = new CrossSectionCtor(polygonsVec, fillRuleToInt(fillRule)); + disposePolygons(polygonsVec); + return cs; + }; + + Module.CrossSection.ofPolygons = function(polygons, fillRule = 'Positive') { + return new Module.CrossSection(polygons, fillRule); + }; + + Module.CrossSection.square = function(...args) { + let size = undefined; + if (args.length == 0) + size = {x: 1, y: 1}; + else if (typeof args[0] == 'number') + size = {x: args[0], y: args[0]}; + else + size = vararg2vec2(args); + const center = args[1] || false; + return Module._Square(size, center); + }; + + Module.CrossSection.circle = function(radius, circularSegments = 0) { + return Module._Circle(radius, circularSegments); + }; + + // allows args to be either CrossSection or polygons (constructed with + // Positive fill) + function crossSectionBatchbool(name) { + return function(...args) { + if (args.length == 1) args = args[0]; + const v = new Module.Vector_crossSection(); + for (const cs of args) v.push_back(cross(cs)); + const result = Module['_crossSection' + name](v); + v.delete(); + return result; + }; + } + + Module.CrossSection.compose = crossSectionBatchbool('Compose'); + Module.CrossSection.union = crossSectionBatchbool('UnionN'); + Module.CrossSection.difference = crossSectionBatchbool('DifferenceN'); + Module.CrossSection.intersection = crossSectionBatchbool('IntersectionN'); + + Module.CrossSection.prototype = Object.create(CrossSectionCtor.prototype); + + // Because the constructor and prototype are being replaced, instanceof will + // not work as desired unless we refer back to the original like this + Object.defineProperty(Module.CrossSection, Symbol.hasInstance, { + get: () => (t) => { + return (t instanceof CrossSectionCtor); + } + }); + + // Manifold Constructors + const ManifoldCtor = Module.Manifold; Module.Manifold = function(mesh) { const manifold = new ManifoldCtor(mesh); @@ -296,32 +518,38 @@ Module.setup = function() { return manifold; }; - Module.Manifold.prototype = Object.create(ManifoldCtor.prototype); + Module.Manifold.ofMesh = function(mesh) { + return new Module.Manifold(mesh); + }; + + Module.Manifold.tetrahedron = function() { + return Module._Tetrahedron(); + }; - Module.cube = function(...args) { + Module.Manifold.cube = function(...args) { let size = undefined; if (args.length == 0) size = {x: 1, y: 1, z: 1}; else if (typeof args[0] == 'number') size = {x: args[0], y: args[0], z: args[0]}; else - size = vararg2vec(args); + size = vararg2vec3(args); const center = args[1] || false; return Module._Cube(size, center); }; - Module.cylinder = function( + Module.Manifold.cylinder = function( height, radiusLow, radiusHigh = -1.0, circularSegments = 0, center = false) { return Module._Cylinder( height, radiusLow, radiusHigh, circularSegments, center); }; - Module.sphere = function(radius, circularSegments = 0) { + Module.Manifold.sphere = function(radius, circularSegments = 0) { return Module._Sphere(radius, circularSegments); }; - Module.smooth = function(mesh, sharpenedEdges = []) { + Module.Manifold.smooth = function(mesh, sharpenedEdges = []) { const sharp = new Module.Vector_smoothness(); toVec(sharp, sharpenedEdges); const result = Module._Smooth(mesh, sharp); @@ -329,41 +557,50 @@ Module.setup = function() { return result; }; - Module.extrude = function( + Module.Manifold.extrude = function( polygons, height, nDivisions = 0, twistDegrees = 0.0, - scaleTop = [1.0, 1.0]) { - if (scaleTop instanceof Array) scaleTop = {x: scaleTop[0], y: scaleTop[1]}; - const polygonsVec = polygons2vec(polygons); - const result = Module._Extrude( - polygonsVec, height, nDivisions, twistDegrees, scaleTop); - disposePolygons(polygonsVec); - return result; + scaleTop = [1.0, 1.0], center = false) { + const cs = (polygons instanceof CrossSectionCtor) ? + polygons : + Module.CrossSection(polygons, 'Positive'); + return cs.extrude(height, nDivisions, twistDegrees, scaleTop, center); }; - Module.triangulate = function(polygons, precision = -1) { - const polygonsVec = polygons2vec(polygons); - const result = fromVec( - Module._Triangulate(polygonsVec, precision), (x) => [x[0], x[1], x[2]]); - disposePolygons(polygonsVec); - return result; + Module.Manifold.revolve = function(polygons, circularSegments = 0) { + const cs = (polygons instanceof CrossSectionCtor) ? + polygons : + Module.CrossSection(polygons, 'Positive'); + return cs.revolve(circularSegments); }; - Module.revolve = function(polygons, circularSegments = 0) { - const polygonsVec = polygons2vec(polygons); - const result = Module._Revolve(polygonsVec, circularSegments); - disposePolygons(polygonsVec); - return result; + Module.Manifold.reserveIDs = function(n) { + return Module._ReserveIDs(n); }; - Module.compose = function(manifolds) { + Module.Manifold.compose = function(manifolds) { const vec = new Module.Vector_manifold(); toVec(vec, manifolds); - const result = Module._Compose(vec); + const result = Module._manifoldCompose(vec); vec.delete(); return result; }; - Module.levelSet = function(sdf, bounds, edgeLength, level = 0) { + function manifoldBatchbool(name) { + return function(...args) { + if (args.length == 1) args = args[0]; + const v = new Module.Vector_manifold(); + for (const m of args) v.push_back(m); + const result = Module['_manifold' + name + 'N'](v); + v.delete(); + return result; + }; + } + + Module.Manifold.union = manifoldBatchbool('Union'); + Module.Manifold.difference = manifoldBatchbool('Difference'); + Module.Manifold.intersection = manifoldBatchbool('Intersection'); + + Module.Manifold.levelSet = function(sdf, bounds, edgeLength, level = 0) { const bounds2 = { min: {x: bounds.min[0], y: bounds.min[1], z: bounds.min[2]}, max: {x: bounds.max[0], y: bounds.max[1], z: bounds.max[2]}, @@ -380,18 +617,23 @@ Module.setup = function() { return out; }; - function batchbool(name) { - return function(...args) { - if (args.length == 1) args = args[0]; - const v = new Module.Vector_manifold(); - for (const m of args) v.push_back(m); - const result = Module['_' + name + 'N'](v); - v.delete(); - return result; - }; - } + Module.Manifold.prototype = Object.create(ManifoldCtor.prototype); - Module.union = batchbool('union'); - Module.difference = batchbool('difference'); - Module.intersection = batchbool('intersection'); + // Because the constructor and prototype are being replaced, instanceof will + // not work as desired unless we refer back to the original like this + Object.defineProperty(Module.Manifold, Symbol.hasInstance, { + get: () => (t) => { + return (t instanceof ManifoldCtor); + } + }); + + // Top-level functions + + Module.triangulate = function(polygons, precision = -1) { + const polygonsVec = polygons2vec(polygons); + const result = fromVec( + Module._Triangulate(polygonsVec, precision), (x) => [x[0], x[1], x[2]]); + disposePolygons(polygonsVec); + return result; + }; }; diff --git a/bindings/wasm/examples/README.md b/bindings/wasm/examples/README.md index 1c310a345..e3d1e5407 100644 --- a/bindings/wasm/examples/README.md +++ b/bindings/wasm/examples/README.md @@ -11,7 +11,7 @@ npm install npm test ``` -To develop the manifoldCAD.org editor as well as our other example pages, run +To develop the manifoldCAD.org editor as well as our other example pages, run ``` npm run dev ``` @@ -23,4 +23,17 @@ See `package.json` for other useful scripts. Note that the `emcmake` command automatically copies your WASM build into `built/`, (here, not just under the `buildWASM` directory) which is then packaged by Vite into `dist/assets/`. -When testing [ManifoldCAD.org](https://manifoldcad.org/) (either locally or the deployed version) note that it uses a service worker for faster loading. This means you need to open the page twice to see updates (the first time loads the old version and caches the new one, the second time loads the new version from cache). To see changes on each reload, open Chrome dev tools, go to the Application tab and check "update on reload". \ No newline at end of file +When testing [ManifoldCAD.org](https://manifoldcad.org/) (either locally or the +deployed version) note that it uses a service worker for faster loading. This +means you need to open the page twice to see updates (the first time loads the +old version and caches the new one, the second time loads the new version from +cache). To see changes on each reload, open Chrome dev tools, go to the +Application tab and check "update on reload". + +### Note for firefox users + +To use the manifoldCAD.org editor (`npm run dev`), you'll likely have to set +`dom.workers.modules.enabled: true` in your `about:config`, as mentioned in the +discussion of the +[issue#328](https://github.com/elalish/manifold/issues/328#issuecomment-1473847102) +of this repository. diff --git a/bindings/wasm/examples/editor.js b/bindings/wasm/examples/editor.js index 9a8888085..80a957351 100644 --- a/bindings/wasm/examples/editor.js +++ b/bindings/wasm/examples/editor.js @@ -231,26 +231,18 @@ async function getManifoldDTS() { return ` ${global.replaceAll('export', '')} ${encapsulated.replace(/^import.*$/gm, '').replaceAll('export', 'declare')} -declare interface ManifoldStatic { - cube: typeof cube; - cylinder: typeof cylinder; - sphere: typeof sphere; - smooth: typeof smooth; - tetrahedron: typeof tetrahedron; - extrude: typeof extrude; - revolve: typeof revolve; - union: typeof union; - difference: typeof difference; - intersection: typeof intersection; - compose: typeof compose; - levelSet: typeof levelSet; - setMinCircularAngle: typeof setMinCircularAngle; - setMinCircularEdgeLength: typeof setMinCircularEdgeLength; - setCircularSegments: typeof setCircularSegments; - getCircularSegments: typeof getCircularSegments; - reserveIDs: typeof reserveIDs; +declare interface ManifoldToplevel { + CrossSection: typeof T.CrossSection; + Manifold: typeof T.Manifold; + Mesh: typeof T.Mesh; + triangulate: typeof T.triangulate; + setMinCircularAngle: typeof T.setMinCircularAngle; + setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength; + setCircularSegments: typeof T.setCircularSegments; + getCircularSegments: typeof T.getCircularSegments; + setup: () => void; } -declare const module: ManifoldStatic; +declare const module: ManifoldToplevel; `; } diff --git a/bindings/wasm/examples/package.json b/bindings/wasm/examples/package.json index 3af49cb33..57273b4db 100644 --- a/bindings/wasm/examples/package.json +++ b/bindings/wasm/examples/package.json @@ -14,17 +14,17 @@ }, "dependencies": { "@gltf-transform/core": "^3.2.1", - "@gltf-transform/functions": "^3.2.1", "@gltf-transform/extensions": "^3.2.1", - "three": "0.151.2", + "@gltf-transform/functions": "^3.2.1", "gl-matrix": "^3.4.3", - "simple-dropzone": "0.8.3" + "simple-dropzone": "0.8.3", + "three": "0.151.2" }, "devDependencies": { - "@vitest/web-worker": "^0.31.1", "@vitest/ui": "^0.31.1", + "@vitest/web-worker": "^0.31.1", + "typescript": "5.0.4", "vite": "^4.3.2", - "vitest": "^0.31.1", - "typescript": "5.0.4" + "vitest": "^0.31.1" } -} \ No newline at end of file +} diff --git a/bindings/wasm/examples/public/editor.d.ts b/bindings/wasm/examples/public/editor.d.ts index 079605e8f..d6ad69478 100644 --- a/bindings/wasm/examples/public/editor.d.ts +++ b/bindings/wasm/examples/public/editor.d.ts @@ -46,23 +46,24 @@ declare function setMaterial( manifold: Manifold, material: GLTFMaterial): Manifold; /** - * Wrap any object with this method to display it and any copies in transparent - * red. This is particularly useful for debugging subtract() as it will allow - * you find the object even if it doesn't currently intersect the result. + * Wrap any shape object with this method to display it and any copies in + * transparent red. This is particularly useful for debugging subtract() as it + * will allow you find the object even if it doesn't currently intersect the + * result. * - * @param manifold The object to show - returned for chaining. + * @param shape The object to show - returned for chaining. */ -declare function show(manifold: Manifold): Manifold; +declare function show(shape: CrossSection|Manifold): Manifold; /** - * Wrap any object with this method to display it and any copies as the result, - * while ghosting out the final result in transparent gray. Helpful for + * Wrap any shape object with this method to display it and any copies as the + * result, while ghosting out the final result in transparent gray. Helpful for * debugging as it allows you to see objects that may be hidden in the interior * of the result. Multiple objects marked only() will all be shown. * - * @param manifold The object to show - returned for chaining. + * @param shape The object to show - returned for chaining. */ -declare function only(manifold: Manifold): Manifold; +declare function only(shape: CrossSection|Manifold): Manifold; // Type definitions for gl-matrix 3.4.3 Project: // https://github.com/toji/gl-matrix diff --git a/bindings/wasm/examples/public/examples.js b/bindings/wasm/examples/public/examples.js index 3b7571ba8..21c19c0a2 100644 --- a/bindings/wasm/examples/public/examples.js +++ b/bindings/wasm/examples/public/examples.js @@ -20,7 +20,7 @@ export const examples = { // see the static API - these functions can also be used bare. Use // console.log() to print output (lower-right). This editor defines Z as // up and units of mm. - + const {cube, sphere} = Manifold; const box = cube([100, 100, 100], true); const ball = sphere(60, 100); // You must name your final output "result", or create at least one @@ -60,7 +60,7 @@ export const examples = { const scale = edgeLength / (2 * Math.sqrt(2)); - const tet = tetrahedron().scale(scale); + const tet = Manifold.tetrahedron().scale(scale); const box = []; box.push([1, -1], [1, 1]); @@ -81,7 +81,7 @@ export const examples = { // channels as colors, and sets the factor to white, since our default is // yellow. const screw = setMaterial( - extrude(box, 1, nDivisions, 270).setProperties(3, fade), + Manifold.extrude(box, 1, nDivisions, 270).setProperties(3, fade), {baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']}); const result = tet.intersect( @@ -107,6 +107,7 @@ export const examples = { // Demonstrates how at 90-degree intersections, the sphere and cylinder // facets match up perfectly, for any choice of global resolution // parameters. + const {sphere, cylinder, union} = Manifold; function roundedFrame(edgeLength, radius, circularSegments = 0) { const edge = cylinder(edgeLength, radius, -1, circularSegments); @@ -171,7 +172,7 @@ export const examples = { v[2] *= r; }; - const ball = sphere(1, 200); + const ball = Manifold.sphere(1, 200); const heart = ball.warp(func); const box = heart.boundingBox(); const result = heart.scale(100 / (box.max[0] - box.min[0])); @@ -219,7 +220,7 @@ export const examples = { const triVerts = Uint32Array.from(triangles); const vertProperties = Float32Array.from(positions); const scallop = new Mesh({numProp: 3, triVerts, vertProperties}); - const result = smooth(scallop, sharpenedEdges).refine(n); + const result = Manifold.smooth(scallop, sharpenedEdges).refine(n); return result; }, @@ -259,12 +260,8 @@ export const examples = { const m = linearSegments > 2 ? linearSegments : n * q * majorRadius / threadRadius; - const circle = []; - const dPhi = 2 * 3.14159 / n; - const offset = 2; - for (let i = 0; i < n; ++i) { - circle.push([Math.cos(dPhi * i) + offset, Math.sin(dPhi * i)]); - } + const offset = 2 + const circle = CrossSection.circle(1, n).translate([offset, 0]); const func = (v) => { const psi = q * Math.atan2(v[0], v[1]); @@ -282,14 +279,14 @@ export const examples = { vec3.rotateZ(v, v, center, psi); }; - let knot = revolve(circle, m).warp(func); + let knot = Manifold.revolve(circle, m).warp(func); if (kLoops > 1) { const knots = []; for (let k = 0; k < kLoops; ++k) { knots.push(knot.rotate([0, 0, 360 * (k / kLoops) * (q / p)])); } - knot = compose(knots); + knot = Manifold.compose(knots); } return knot; @@ -325,16 +322,18 @@ export const examples = { } function mengerSponge(n) { - let result = cube([1, 1, 1], true); + let result = Manifold.cube([1, 1, 1], true); const holes = []; fractal(holes, result, 1.0, [0.0, 0.0], 1, n); - const hole = compose(holes); - - result = difference(result, hole); - result = difference(result, hole.rotate([90, 0, 0])); - result = difference(result, hole.rotate([0, 90, 0])); + const hole = Manifold.compose(holes); + result = Manifold.difference( + result, + hole, + hole.rotate([90, 0, 0]), + hole.rotate([0, 90, 0]), + ); return result; } @@ -363,7 +362,7 @@ export const examples = { function base( width, radius, decorRadius, twistRadius, nDecor, innerRadius, outerRadius, cut, nCut, nDivision) { - let b = cylinder(width, radius + twistRadius / 2); + let b = Manifold.cylinder(width, radius + twistRadius / 2); const circle = []; const dPhiDeg = 180 / nDivision; for (let i = 0; i < 2 * nDivision; ++i) { @@ -372,7 +371,7 @@ export const examples = { decorRadius * Math.sin(dPhiDeg * i * Math.PI / 180) ]); } - let decor = extrude(circle, width, nDivision, 180) + let decor = Manifold.extrude(circle, width, nDivision, 180) .scale([1, 0.5, 1]) .translate([0, radius, 0]); for (let i = 0; i < nDecor; i++) @@ -390,8 +389,9 @@ export const examples = { stretch.push(vec2.rotate([0, 0], p2, o, dPhiRad * i)); stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i)); } - b = intersection(extrude(stretch, width), b); - return b; + const result = + Manifold.intersection(Manifold.extrude(stretch, width), b); + return result; } function stretchyBracelet( @@ -404,7 +404,7 @@ export const examples = { const cut = 0.5 * (Math.PI * 2 * innerRadius / nCut - thickness); const adjThickness = 0.5 * thickness * height / cut; - return difference( + return Manifold.difference( base( width, radius, decorRadius, twistRadius, nDecor, innerRadius + thickness, outerRadius + adjThickness, @@ -449,11 +449,12 @@ export const examples = { min: vec3.fromValues(-period, -period, -period), max: vec3.fromValues(period, period, period) }; - return levelSet(gyroid, box, period / n, level).scale(size / period); + return Manifold.levelSet(gyroid, box, period / n, level) + .scale(size / period); }; function rhombicDodecahedron() { - const box = cube([1, 1, 2], true).scale(size * Math.sqrt(2)); + const box = Manifold.cube([1, 1, 2], true).scale(size * Math.sqrt(2)); const result = box.rotate([90, 45, 0]).intersect(box.rotate([90, 45, 90])); return result.intersect(box.rotate([0, 0, 45])); @@ -505,4 +506,4 @@ for (const [func, code] of Object.entries(examples.functions)) { if (typeof self !== 'undefined') { self.examples = examples; -} \ No newline at end of file +} diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 57b51f959..e4da34e09 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -20,9 +20,9 @@ import Module from './built/manifold'; //@ts-ignore import {setupIO, writeMesh} from './gltf-io'; import type {GLTFMaterial, Quat} from './public/editor'; -import type {Manifold, ManifoldStatic, Mesh, Vec3} from './public/manifold'; +import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './public/manifold'; -interface WorkerStatic extends ManifoldStatic { +interface WorkerStatic extends ManifoldToplevel { GLTFNode: typeof GLTFNode; show(manifold: Manifold): Manifold; only(manifold: Manifold): Manifold; @@ -39,6 +39,87 @@ glMatrix.glMatrix.setMatrixArrayType(Array); const io = setupIO(new WebIO()); io.registerExtensions(KHRONOS_EXTENSIONS); +// manifold static methods (that return a new manifold) +const manifoldStaticFunctions = [ + 'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'compose', + 'union', 'difference', 'intersection', 'levelSet', 'smooth', 'ofMesh' +]; +// manifold member functions (that return a new manifold) +const manifoldMemberFunctions = [ + 'add', 'subtract', 'intersect', 'decompose', 'warp', 'transform', 'translate', + 'rotate', 'scale', 'mirror', 'refine', 'setProperties', 'asOriginal', + 'trimByPlane', 'split', 'splitByPlane' +]; +// CrossSection static methods (that return a new cross-section) +const crossSectionStaticFunctions = [ + 'square', 'circle', 'union', 'difference', 'intersection', 'compose', + 'ofPolygons' +]; +// CrossSection member functions (that return a new cross-section) +const crossSectionMemberFunctions = [ + 'add', 'subtract', 'intersect', 'rectClip', 'decompose', 'transform', + 'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset' +]; +// top level functions that construct a new manifold/mesh +const toplevelConstructors = ['show', 'only', 'setMaterial']; +const toplevel = [ + 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', + 'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection' +]; +const exposedFunctions = toplevelConstructors.concat(toplevel); + +// Setup memory management, such that users don't have to care about +// calling `delete` manually. +// Note that this only fixes memory leak across different runs: the memory +// will only be freed when the compilation finishes. + +const memoryRegistry = new Array(); + +function addMembers( + className: string, methodNames: Array, areStatic: boolean) { + //@ts-ignore + const cls = module[className]; + const obj = areStatic ? cls : cls.prototype; + for (const name of methodNames) { + if (name != 'cylinder') { + const originalFn = obj[name]; + obj[name] = function(...args: any) { + //@ts-ignore + const result = originalFn(...args); + memoryRegistry.push(result); + return result; + }; + } + } +} + +addMembers('Manifold', manifoldMemberFunctions, false); +addMembers('Manifold', manifoldStaticFunctions, true); +addMembers('CrossSection', crossSectionMemberFunctions, false); +addMembers('CrossSection', crossSectionStaticFunctions, true); + +for (const name of toplevelConstructors) { + //@ts-ignore + const originalFn = module[name]; + //@ts-ignore + module[name] = function(...args: any) { + const result = originalFn(...args); + memoryRegistry.push(result); + return result; + }; +} + +module.cleanup = function() { + for (const obj of memoryRegistry) { + // decompose result is an array of manifolds + if (obj instanceof Array) + for (const elem of obj) elem.delete(); + else + obj.delete(); + } + memoryRegistry.length = 0; +}; + // Debug setup to show source meshes let ghost = false; const shown = new Map(); @@ -58,21 +139,6 @@ const GHOST = { metallic: 0 } as GLTFMaterial; -function debug(manifold: Manifold, map: Map) { - const result = manifold.asOriginal(); - map.set(result.originalID(), result.getMesh()); - return result; -}; - -module.show = (manifold) => { - return debug(manifold, shown); -}; - -module.only = (manifold) => { - ghost = true; - return debug(manifold, singles); -}; - const nodes = new Array(); const id2material = new Map(); const materialCache = new Map(); @@ -118,64 +184,19 @@ module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => { return out; }; -// manifold member functions that returns a new manifold -const memberFunctions = [ - 'add', 'subtract', 'intersect', 'trimByPlane', 'refine', 'warp', - 'setProperties', 'transform', 'translate', 'rotate', 'scale', 'mirror', - 'asOriginal', 'decompose' -]; -// top level functions that constructs a new manifold -const constructors = [ - 'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'union', - 'difference', 'intersection', 'compose', 'levelSet', 'smooth', 'show', 'only', - 'setMaterial' -]; -const utils = [ - 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', - 'getCircularSegments', 'Mesh', 'GLTFNode' -]; -const exposedFunctions = constructors.concat(utils); - -// Setup memory management, such that users don't have to care about -// calling `delete` manually. -// Note that this only fixes memory leak across different runs: the memory -// will only be freed when the compilation finishes. - -const manifoldRegistry = new Array(); -for (const name of memberFunctions) { - //@ts-ignore - const originalFn = module.Manifold.prototype[name]; - //@ts-ignore - module.Manifold.prototype['_' + name] = originalFn; - //@ts-ignore - module.Manifold.prototype[name] = function(...args: any) { - //@ts-ignore - const result = this['_' + name](...args); - manifoldRegistry.push(result); - return result; - }; -} +function debug(manifold: Manifold, map: Map) { + let result = manifold.asOriginal(); + map.set(result.originalID(), result.getMesh()); + return result; +}; -for (const name of constructors) { - //@ts-ignore - const originalFn = module[name]; - //@ts-ignore - module[name] = function(...args: any) { - const result = originalFn(...args); - manifoldRegistry.push(result); - return result; - }; -} +module.show = (manifold) => { + return debug(manifold, shown); +}; -module.cleanup = function() { - for (const obj of manifoldRegistry) { - // decompose result is an array of manifolds - if (obj instanceof Array) - for (const elem of obj) elem.delete(); - else - obj.delete(); - } - manifoldRegistry.length = 0; +module.only = (manifold) => { + ghost = true; + return debug(manifold, singles); }; // Setup complete @@ -440,4 +461,4 @@ async function exportGLB(manifold?: Manifold) { const blob = new Blob([glb], {type: 'application/octet-stream'}); self.postMessage({objectURL: URL.createObjectURL(blob)}); -} \ No newline at end of file +} diff --git a/bindings/wasm/helpers.cpp b/bindings/wasm/helpers.cpp new file mode 100644 index 000000000..50b3e9d51 --- /dev/null +++ b/bindings/wasm/helpers.cpp @@ -0,0 +1,222 @@ +#include +#include +#include +#include +#include + +#include + +#include "cross_section.h" + +using namespace emscripten; +using namespace manifold; + +namespace js { +val MeshGL2JS(const MeshGL& mesh) { + val meshJS = val::object(); + + meshJS.set("numProp", mesh.numProp); + meshJS.set("triVerts", + val(typed_memory_view(mesh.triVerts.size(), mesh.triVerts.data())) + .call("slice")); + meshJS.set("vertProperties", + val(typed_memory_view(mesh.vertProperties.size(), + mesh.vertProperties.data())) + .call("slice")); + meshJS.set("mergeFromVert", val(typed_memory_view(mesh.mergeFromVert.size(), + mesh.mergeFromVert.data())) + .call("slice")); + meshJS.set("mergeToVert", val(typed_memory_view(mesh.mergeToVert.size(), + mesh.mergeToVert.data())) + .call("slice")); + meshJS.set("runIndex", + val(typed_memory_view(mesh.runIndex.size(), mesh.runIndex.data())) + .call("slice")); + meshJS.set("runOriginalID", val(typed_memory_view(mesh.runOriginalID.size(), + mesh.runOriginalID.data())) + .call("slice")); + meshJS.set("faceID", + val(typed_memory_view(mesh.faceID.size(), mesh.faceID.data())) + .call("slice")); + meshJS.set("halfedgeTangent", + val(typed_memory_view(mesh.halfedgeTangent.size(), + mesh.halfedgeTangent.data())) + .call("slice")); + meshJS.set("runTransform", val(typed_memory_view(mesh.runTransform.size(), + mesh.runTransform.data())) + .call("slice")); + + return meshJS; +} + +MeshGL MeshJS2GL(const val& mesh) { + MeshGL out; + out.numProp = mesh["numProp"].as(); + out.triVerts = convertJSArrayToNumberVector(mesh["triVerts"]); + out.vertProperties = + convertJSArrayToNumberVector(mesh["vertProperties"]); + if (mesh["mergeFromVert"] != val::undefined()) { + out.mergeFromVert = + convertJSArrayToNumberVector(mesh["mergeFromVert"]); + } + if (mesh["mergeToVert"] != val::undefined()) { + out.mergeToVert = + convertJSArrayToNumberVector(mesh["mergeToVert"]); + } + if (mesh["runIndex"] != val::undefined()) { + out.runIndex = convertJSArrayToNumberVector(mesh["runIndex"]); + } + if (mesh["runOriginalID"] != val::undefined()) { + out.runOriginalID = + convertJSArrayToNumberVector(mesh["runOriginalID"]); + } + if (mesh["faceID"] != val::undefined()) { + out.faceID = convertJSArrayToNumberVector(mesh["faceID"]); + } + if (mesh["halfedgeTangent"] != val::undefined()) { + out.halfedgeTangent = + convertJSArrayToNumberVector(mesh["halfedgeTangent"]); + } + if (mesh["runTransform"] != val::undefined()) { + out.runTransform = + convertJSArrayToNumberVector(mesh["runTransform"]); + } + return out; +} + +val GetMeshJS(const Manifold& manifold, const glm::ivec3& normalIdx) { + MeshGL mesh = manifold.GetMeshGL(normalIdx); + return MeshGL2JS(mesh); +} + +val Merge(const val& mesh) { + val out = val::object(); + MeshGL meshGL = MeshJS2GL(mesh); + bool changed = meshGL.Merge(); + out.set("changed", changed); + out.set("mesh", changed ? MeshGL2JS(meshGL) : mesh); + return out; +} + +Manifold Smooth(const val& mesh, + const std::vector& sharpenedEdges = {}) { + return Manifold::Smooth(MeshJS2GL(mesh), sharpenedEdges); +} + +} // namespace js + +namespace cross_js { +CrossSection OfPolygons(std::vector> polygons, + int fill_rule) { + auto fr = fill_rule == 0 ? CrossSection::FillRule::EvenOdd + : fill_rule == 1 ? CrossSection::FillRule::NonZero + : fill_rule == 2 ? CrossSection::FillRule::Positive + : CrossSection::FillRule::Negative; + return CrossSection(polygons, fr); +} + +CrossSection Union(const CrossSection& a, const CrossSection& b) { + return a + b; +} + +CrossSection Difference(const CrossSection& a, const CrossSection& b) { + return a - b; +} + +CrossSection Intersection(const CrossSection& a, const CrossSection& b) { + return a ^ b; +} + +CrossSection UnionN(const std::vector& cross_sections) { + return CrossSection::BatchBoolean(cross_sections, OpType::Add); +} + +CrossSection DifferenceN(const std::vector& cross_sections) { + return CrossSection::BatchBoolean(cross_sections, OpType::Subtract); +} + +CrossSection IntersectionN(const std::vector& cross_sections) { + return CrossSection::BatchBoolean(cross_sections, OpType::Intersect); +} + +CrossSection Transform(CrossSection& cross_section, const val& mat) { + std::vector array = convertJSArrayToNumberVector(mat); + glm::mat3x2 matrix; + for (const int col : {0, 1, 2}) + for (const int row : {0, 1}) matrix[col][row] = array[col * 3 + row]; + return cross_section.Transform(matrix); +} + +CrossSection Warp(CrossSection& cross_section, uintptr_t funcPtr) { + void (*f)(glm::vec2&) = reinterpret_cast(funcPtr); + return cross_section.Warp(f); +} + +CrossSection Offset(CrossSection& cross_section, double delta, int join_type, + double miter_limit, double arc_tolerance) { + auto jt = join_type == 0 ? CrossSection::JoinType::Square + : join_type == 1 ? CrossSection::JoinType::Round + : CrossSection::JoinType::Miter; + return cross_section.Offset(delta, jt, miter_limit, arc_tolerance); +} +} // namespace cross_js + +namespace man_js { +Manifold FromMeshJS(const val& mesh) { return Manifold(js::MeshJS2GL(mesh)); } + +Manifold Union(const Manifold& a, const Manifold& b) { return a + b; } + +Manifold Difference(const Manifold& a, const Manifold& b) { return a - b; } + +Manifold Intersection(const Manifold& a, const Manifold& b) { return a ^ b; } + +Manifold UnionN(const std::vector& manifolds) { + return Manifold::BatchBoolean(manifolds, OpType::Add); +} + +Manifold DifferenceN(const std::vector& manifolds) { + return Manifold::BatchBoolean(manifolds, OpType::Subtract); +} + +Manifold IntersectionN(const std::vector& manifolds) { + return Manifold::BatchBoolean(manifolds, OpType::Intersect); +} + +Manifold Transform(Manifold& manifold, const val& mat) { + std::vector array = convertJSArrayToNumberVector(mat); + glm::mat4x3 matrix; + for (const int col : {0, 1, 2, 3}) + for (const int row : {0, 1, 2}) matrix[col][row] = array[col * 4 + row]; + return manifold.Transform(matrix); +} + +Manifold Warp(Manifold& manifold, uintptr_t funcPtr) { + void (*f)(glm::vec3&) = reinterpret_cast(funcPtr); + return manifold.Warp(f); +} + +Manifold SetProperties(Manifold& manifold, int numProp, uintptr_t funcPtr) { + void (*f)(float*, glm::vec3, const float*) = + reinterpret_cast(funcPtr); + return manifold.SetProperties(numProp, f); +} + +Manifold LevelSet(uintptr_t funcPtr, Box bounds, float edgeLength, + float level) { + float (*f)(const glm::vec3&) = + reinterpret_cast(funcPtr); + Mesh m = LevelSet(f, bounds, edgeLength, level); + return Manifold(m); +} + +std::vector Split(Manifold& a, Manifold& b) { + auto [r1, r2] = a.Split(b); + return {r1, r2}; +} + +std::vector SplitByPlane(Manifold& m, glm::vec3 normal, + float originOffset) { + auto [a, b] = m.SplitByPlane(normal, originOffset); + return {a, b}; +} +} // namespace man_js diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 667c8becd..b9acb7d08 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -12,101 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Box, Curvature, Mat4, Polygons, Properties, SealedFloat32Array, SealedUint32Array, Smoothness, Vec2, Vec3} from './manifold-global-types'; - -/** - * Constructs a unit cube (edge lengths all one), by default in the first - * octant, touching the origin. - * - * @param size The X, Y, and Z dimensions of the box. - * @param center Set to true to shift the center to the origin. - */ -export function cube(size?: Vec3|number, center?: boolean): Manifold; - -/** - * A convenience constructor for the common case of extruding a circle. Can also - * form cones if both radii are specified. - * - * @param height Z-extent - * @param radiusLow Radius of bottom circle. Must be positive. - * @param radiusHigh Radius of top circle. Can equal zero. Default is equal to - * radiusLow. - * @param circularSegments How many line segments to use around the circle. - * Default is calculated by the static Defaults. - * @param center Set to true to shift the center to the origin. Default is - * origin at the bottom. - */ -export function cylinder( - height: number, radiusLow: number, radiusHigh?: number, - circularSegments?: number, center?: boolean): Manifold; - -/** - * Constructs a geodesic sphere of a given radius. - * - * @param radius Radius of the sphere. Must be positive. - * @param circularSegments Number of segments along its - * diameter. This number will always be rounded up to the nearest factor of - * four, as this sphere is constructed by refining an octahedron. This means - * there are a circle of vertices on all three of the axis planes. Default is - * calculated by the static Defaults. - */ -export function sphere(radius: number, circularSegments?: number): Manifold; - - -/** - * Constructs a smooth version of the input mesh by creating tangents; this - * method will throw if you have supplied tangents with your mesh already. The - * actual triangle resolution is unchanged; use the Refine() method to - * interpolate to a higher-resolution curve. - * - * By default, every edge is calculated for maximum smoothness (very much - * approximately), attempting to minimize the maximum mean Curvature magnitude. - * No higher-order derivatives are considered, as the interpolation is - * independent per triangle, only sharing constraints on their boundaries. - * - * @param mesh input Mesh. - * @param sharpenedEdges If desired, you can supply a vector of sharpened - * halfedges, which should in general be a small subset of all halfedges. Order - * of entries doesn't matter, as each one specifies the desired smoothness - * (between zero and one, with one the default for all unspecified halfedges) - * and the halfedge index (3 * triangle index + [0,1,2] where 0 is the edge - * between triVert 0 and 1, etc). - * - * At a smoothness value of zero, a sharp crease is made. The smoothness is - * interpolated along each edge, so the specified value should be thought of as - * an average. Where exactly two sharpened edges meet at a vertex, their - * tangents are rotated to be colinear so that the sharpened edge can be - * continuous. Vertices with only one sharpened edge are completely smooth, - * allowing sharpened edges to smoothly vanish at termination. A single vertex - * can be sharpened by sharping all edges that are incident on it, allowing - * cones to be formed. - */ -export function smooth(mesh: Mesh, sharpenedEdges?: Smoothness[]): Manifold; - -/** - * Constructs a tetrahedron centered at the origin with one vertex at (1,1,1) - * and the rest at similarly symmetric points. - */ -export function tetrahedron(): Manifold; - -/** - * Constructs a manifold from a set of polygons by extruding them along the - * Z-axis. - * - * @param crossSection A set of non-overlapping polygons to extrude. - * @param height Z-extent of extrusion. - * @param nDivisions Number of extra copies of the crossSection to insert into - * the shape vertically; especially useful in combination with twistDegrees to - * avoid interpolation artifacts. Default is none. - * @param twistDegrees Amount to twist the top crossSection relative to the - * bottom, interpolated linearly for the divisions in between. - * @param scaleTop Amount to scale the top (independently in X and Y). If the - * scale is {0, 0}, a pure cone is formed with only a single vertex at the top. - * Default {1, 1}. - */ -export function extrude( - crossSection: Polygons, height: number, nDivisions?: number, - twistDegrees?: number, scaleTop?: Vec2): Manifold; +import {Box, Curvature, FillRule, JoinType, Mat4, Polygons, Properties, Rect, SealedFloat32Array, SealedUint32Array, SimplePolygon, Smoothness, Vec2, Vec3} from './manifold-global-types'; /** * Triangulates a set of /epsilon-valid polygons. @@ -118,58 +24,6 @@ export function extrude( */ export function triangulate(polygons: Polygons, precision?: number): Vec3[]; -/** - * Constructs a manifold from a set of polygons by revolving this cross-section - * around its Y-axis and then setting this as the Z-axis of the resulting - * manifold. If the polygons cross the Y-axis, only the part on the positive X - * side is used. Geometrically valid input will result in geometrically valid - * output. - * - * @param crossSection A set of non-overlapping polygons to revolve. - * @param circularSegments Number of segments along its diameter. Default is - * calculated by the static Defaults. - */ -export function revolve( - crossSection: Polygons, circularSegments?: number): Manifold; - -export function union(a: Manifold, b: Manifold): Manifold; -export function difference(a: Manifold, b: Manifold): Manifold; -export function intersection(a: Manifold, b: Manifold): Manifold; - -export function union(manifolds: Manifold[]): Manifold; -export function difference(manifolds: Manifold[]): Manifold; -export function intersection(manifolds: Manifold[]): Manifold; - -/** - * Constructs a new manifold from a list of other manifolds. This is a purely - * topological operation, so care should be taken to avoid creating - * overlapping results. It is the inverse operation of Decompose(). - * - * @param manifolds A list of Manifolds to lazy-union together. - */ -export function compose(manifolds: Manifold[]): Manifold; - -/** - * Constructs a level-set Mesh from the input Signed-Distance Function (SDF). - * This uses a form of Marching Tetrahedra (akin to Marching Cubes, but better - * for manifoldness). Instead of using a cubic grid, it uses a body-centered - * cubic grid (two shifted cubic grids). This means if your function's interior - * exceeds the given bounds, you will see a kind of egg-crate shape closing off - * the manifold, which is due to the underlying grid. - * - * @param sdf The signed-distance function which returns the signed distance of - * a given point in R^3. Positive values are inside, negative outside. - * @param bounds An axis-aligned box that defines the extent of the grid. - * @param edgeLength Approximate maximum edge length of the triangles in the - * final result. This affects grid spacing, and hence has a strong effect on - * performance. - * @param level You can inset your Mesh by using a positive value, or outset - * it with a negative value. - */ -export function levelSet( - sdf: (point: Vec3) => number, bounds: Box, edgeLength: number, - level?: number): Manifold; - /** * @name Defaults * These static properties control how circular shapes are quantized by @@ -186,12 +40,291 @@ export function setCircularSegments(segments: number): void; export function getCircularSegments(radius: number): number; ///@} -/** - * Returns the first of n sequential new unique mesh IDs for marking sets of - * triangles that can be looked up after further operations. Assign to - * Mesh.runOriginalID vector. - */ -export function reserveIDs(count: number): number; +export class CrossSection { + /** + * Create a 2d cross-section from a set of contours (complex polygons). A + * boolean union operation (with Positive filling rule by default) is + * performed to combine overlapping polygons and ensure the resulting + * CrossSection is free of intersections. + * + * @param contours A set of closed paths describing zero or more complex + * polygons. + * @param fillRule The filling rule used to interpret polygon sub-regions in + * contours. + */ + constructor(polygons: Polygons, fillRule?: FillRule); + + // Shapes + + static square(size?: Vec2|number, center?: boolean): CrossSection; + + static circle(radius: number, circularSegments?: number): CrossSection; + + // Extrusions (2d to 3d manifold) + + /** + * Constructs a manifold by extruding the cross-section along Z-axis. + * + * @param height Z-extent of extrusion. + * @param nDivisions Number of extra copies of the crossSection to insert into + * the shape vertically; especially useful in combination with twistDegrees to + * avoid interpolation artifacts. Default is none. + * @param twistDegrees Amount to twist the top crossSection relative to the + * bottom, interpolated linearly for the divisions in between. + * @param scaleTop Amount to scale the top (independently in X and Y). If the + * scale is {0, 0}, a pure cone is formed with only a single vertex at the + * top. Default {1, 1}. + * @param center If true, the extrusion is centered on the z-axis through the + * origin + * as opposed to resting on the XY plane as is default. + */ + extrude( + height: number, nDivisions?: number, twistDegrees?: number, + scaleTop?: Vec2|number, center?: boolean): Manifold; + + /** + * Constructs a manifold by revolving this cross-section around its Y-axis and + * then setting this as the Z-axis of the resulting manifold. If the contours + * cross the Y-axis, only the part on the positive X side is used. + * Geometrically valid input will result in geometrically valid output. + * + * @param circularSegments Number of segments along its diameter. Default is + * calculated by the static Defaults. + */ + revolve(circularSegments?: number): Manifold; + + // Transformations + + /** + * Transform this CrossSection in space. Stored in column-major order. This + * operation can be chained. Transforms are combined and applied lazily. + * + * @param m The affine transformation matrix to apply to all the vertices. The + * last row is ignored. + */ + transform(m: Mat3): CrossSection; + + /** + * Move this CrossSection in space. This operation can be chained. Transforms + * are combined and applied lazily. + * + * @param v The vector to add to every vertex. + */ + translate(v: Vec2): CrossSection; + + /** + * Applies an Euler angle rotation to the cross-section, first about the X + * axis, then Y, then Z, in degrees. We use degrees so that we can minimize + * rounding error, and eliminate it completely for any multiples of 90 + * degrees. Additionally, more efficient code paths are used to update the + * cross-section when the transforms only rotate by multiples of 90 degrees. + * This operation can be chained. Transforms are combined and applied lazily. + * + * @param v [X, Y, Z] rotation in degrees. + */ + rotate(v: Vec2): CrossSection; + + /** + * Scale this CrossSection in space. This operation can be chained. Transforms + * are combined and applied lazily. + * + * @param v The vector to multiply every vertex by per component. + */ + scale(v: Vec2|number): CrossSection; + + + /** + * Mirror this CrossSection over the arbitrary axis described by the unit form + * of the given vector. If the length of the vector is zero, an empty + * CrossSection is returned. This operation can be chained. Transforms are + * combined and applied lazily. + * + * @param ax the axis to be mirrored over + */ + mirror(v: Vec2): CrossSection; + + /** + * Move the vertices of this CrossSection (creating a new one) according to + * any arbitrary input function, followed by a union operation (with a + * Positive fill rule) that ensures any introduced intersections are not + * included in the result. + * + * @param warpFunc A function that modifies a given vertex position. + */ + warp(warpFunc: (vert: Vec2) => void): CrossSection; + + /** + * Inflate the contours in CrossSection by the specified delta, handling + * corners according to the given JoinType. + * + * @param delta Positive deltas will cause the expansion of outlining contours + * to expand, and retraction of inner (hole) contours. Negative deltas will + * have the opposite effect. + * @param joinType The join type specifying the treatment of contour joins + * (corners). + * @param miterLimit The maximum distance in multiples of delta that vertices + * can be offset from their original positions with before squaring is + * applied, **when the join type is Miter** (default is 2, which is the + * minimum allowed). See the [Clipper2 + * MiterLimit](http://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm) + * page for a visual example. + * @param arcTolerance The maximum acceptable imperfection for curves drawn + * (approximated with line segments) for Round joins (not relevant for other + * JoinTypes). By default (when undefined or =0), the allowable imprecision is + * scaled in inverse proportion to the offset delta. + */ + offset( + delta: number, joinType?: JoinType, miterLimit?: number, + arcTolerance?: number): CrossSection; + + /** + * Remove vertices from the contours in this CrossSection that are less than + * the specified distance epsilon from an imaginary line that passes through + * its two adjacent vertices. Near duplicate vertices and collinear points + * will be removed at lower epsilons, with elimination of line segments + * becoming increasingly aggressive with larger epsilons. + * + * It is recommended to apply this function following Offset, in order to + * clean up any spurious tiny line segments introduced that do not improve + * quality in any meaningful way. This is particularly important if further + * offseting operations are to be performed, which would compound the issue. + * + * @param epsilon minimum distance vertices must diverge from the hypothetical + * outline without them in order to be included in the output (default + * 1e-6) + */ + simplify(epsilon?: number): CrossSection; + + // Clipping Operations + + /** + * Boolean union + */ + add(other: CrossSection|Polygons): CrossSection; + + /** + * Boolean difference + */ + subtract(other: CrossSection|Polygons): CrossSection; + + /** + * Boolean intersection + */ + intersect(other: CrossSection|Polygons): CrossSection; + + /** + * Boolean union of the cross-sections a and b + */ + static union(a: CrossSection|Polygons, b: CrossSection|Polygons): + CrossSection; + + /** + * Boolean difference of the cross-section b from the cross-section a + */ + static difference(a: CrossSection|Polygons, b: CrossSection|Polygons): + CrossSection; + + /** + * Boolean intersection of the cross-sections a and b + */ + static intersection(a: CrossSection|Polygons, b: CrossSection|Polygons): + CrossSection; + + /** + * Boolean union of a list of cross-sections + */ + static union(polygons: (CrossSection|Polygons)[]): CrossSection; + + /** + * Boolean difference of the tail of a list of cross-sections from its head + */ + static difference(polygons: (CrossSection|Polygons)[]): CrossSection; + + /** + * Boolean intersection of a list of cross-sections + */ + static intersection(polygons: (CrossSection|Polygons)[]): CrossSection; + + /** + * Compute the intersection between a cross-section and an axis-aligned + * rectangle. This operation has much higher performance (O(n) vs + * >O(n^3)) than the general purpose intersection algorithm + * used for sets of cross-sections. + */ + rectClip(rect: Rect): CrossSection; + + // Topological Operations + + /** + * Construct a CrossSection from a vector of other Polygons (batch + * boolean union). + */ + static compose(polygons: (CrossSection|Polygons)[]): CrossSection; + + /** + * This operation returns a vector of CrossSections that are topologically + * disconnected, each containing one outline contour with zero or more + * holes. + */ + decompose(): CrossSection[]; + + // Polygon Conversion + + /** + * Create a 2d cross-section from a set of contours (complex polygons). A + * boolean union operation (with Positive filling rule by default) is + * performed to combine overlapping polygons and ensure the resulting + * CrossSection is free of intersections. + * + * @param contours A set of closed paths describing zero or more complex + * polygons. + * @param fillRule The filling rule used to interpret polygon sub-regions in + * contours. + */ + static ofPolygons(polygons: Polygons, fillRule?: FillRule): CrossSection; + + /** + * Return the contours of this CrossSection as a list of simple polygons. + */ + toPolygons(): SimplePolygon[]; + + // Properties + + /** + * Return the total area covered by complex polygons making up the + * CrossSection. + */ + area(): number; + + /** + * Does the CrossSection (not) have any contours? + */ + isEmpty(): boolean; + + /** + * The number of vertices in the CrossSection. + */ + numVert(): number; + + /** + * The number of contours in the CrossSection. + */ + numContour(): number; + + /** + * Returns the axis-aligned bounding rectangle of all the CrossSection's + * vertices. + */ + bounds(): Rect; + + // Memory + + /** + * Frees the WASM memory of this CrossSection, since these cannot be + * garbage-collected automatically. + */ + delete(): void; +} export class Manifold { /** @@ -206,6 +339,163 @@ export class Manifold { * materials into triangle runs. */ constructor(mesh: Mesh); + + // Shapes + + /** + * Constructs a tetrahedron centered at the origin with one vertex at (1,1,1) + * and the rest at similarly symmetric points. + */ + static tetrahedron(): Manifold; + + /** + * Constructs a unit cube (edge lengths all one), by default in the first + * octant, touching the origin. + * + * @param size The X, Y, and Z dimensions of the box. + * @param center Set to true to shift the center to the origin. + */ + static cube(size?: Vec3|number, center?: boolean): Manifold; + + /** + * A convenience constructor for the common case of extruding a circle. Can + * also form cones if both radii are specified. + * + * @param height Z-extent + * @param radiusLow Radius of bottom circle. Must be positive. + * @param radiusHigh Radius of top circle. Can equal zero. Default is equal to + * radiusLow. + * @param circularSegments How many line segments to use around the circle. + * Default is calculated by the static Defaults. + * @param center Set to true to shift the center to the origin. Default is + * origin at the bottom. + */ + static cylinder( + height: number, radiusLow: number, radiusHigh?: number, + circularSegments?: number, center?: boolean): Manifold; + + /** + * Constructs a geodesic sphere of a given radius. + * + * @param radius Radius of the sphere. Must be positive. + * @param circularSegments Number of segments along its + * diameter. This number will always be rounded up to the nearest factor of + * four, as this sphere is constructed by refining an octahedron. This means + * there are a circle of vertices on all three of the axis planes. Default is + * calculated by the static Defaults. + */ + static sphere(radius: number, circularSegments?: number): Manifold; + + // Extrusions from 2d shapes + + /** + * Constructs a manifold from a set of polygons/cross-section by extruding + * them along the Z-axis. + * + * @param polygons A set of non-overlapping polygons to extrude. + * @param height Z-extent of extrusion. + * @param nDivisions Number of extra copies of the crossSection to insert into + * the shape vertically; especially useful in combination with twistDegrees to + * avoid interpolation artifacts. Default is none. + * @param twistDegrees Amount to twist the top crossSection relative to the + * bottom, interpolated linearly for the divisions in between. + * @param scaleTop Amount to scale the top (independently in X and Y). If the + * scale is {0, 0}, a pure cone is formed with only a single vertex at the + * top. Default {1, 1}. + * @param center If true, the extrusion is centered on the z-axis through the + * origin + * as opposed to resting on the XY plane as is default. + */ + static extrude( + polygons: CrossSection|Polygons, height: number, nDivisions?: number, + twistDegrees?: number, scaleTop?: Vec2|number, + center?: boolean): Manifold; + + /** + * Constructs a manifold from a set of polygons/cross-section by revolving + * them around the Y-axis and then setting this as the Z-axis of the resulting + * manifold. If the polygons cross the Y-axis, only the part on the positive X + * side is used. Geometrically valid input will result in geometrically valid + * output. + * + * @param polygons A set of non-overlapping polygons to revolve. + * @param circularSegments Number of segments along its diameter. Default is + * calculated by the static Defaults. + */ + static revolve(polygons: CrossSection|Polygons, circularSegments?: number): + Manifold; + + // Mesh Conversion + + /** + * Convert a Mesh into a Manifold, retaining its properties and merging only + * the positions according to the merge vectors. Will throw an error if the + * result is not an oriented 2-manifold. Will collapse degenerate triangles + * and unnecessary vertices. + * + * All fields are read, making this structure suitable for a lossless + * round-trip of data from getMesh(). For multi-material input, use + * reserveIDs() to set a unique originalID for each material, and sort the + * materials into triangle runs. + */ + static ofMesh(mesh: Mesh): Manifold; + + /** + * Constructs a smooth version of the input mesh by creating tangents; this + * method will throw if you have supplied tangents with your mesh already. The + * actual triangle resolution is unchanged; use the Refine() method to + * interpolate to a higher-resolution curve. + * + * By default, every edge is calculated for maximum smoothness (very much + * approximately), attempting to minimize the maximum mean Curvature + * magnitude. No higher-order derivatives are considered, as the interpolation + * is independent per triangle, only sharing constraints on their boundaries. + * + * @param mesh input Mesh. + * @param sharpenedEdges If desired, you can supply a vector of sharpened + * halfedges, which should in general be a small subset of all halfedges. + * Order of entries doesn't matter, as each one specifies the desired + * smoothness (between zero and one, with one the default for all unspecified + * halfedges) and the halfedge index (3 * triangle index + [0,1,2] where 0 is + * the edge between triVert 0 and 1, etc). + * + * At a smoothness value of zero, a sharp crease is made. The smoothness is + * interpolated along each edge, so the specified value should be thought of + * as an average. Where exactly two sharpened edges meet at a vertex, their + * tangents are rotated to be colinear so that the sharpened edge can be + * continuous. Vertices with only one sharpened edge are completely smooth, + * allowing sharpened edges to smoothly vanish at termination. A single vertex + * can be sharpened by sharping all edges that are incident on it, allowing + * cones to be formed. + */ + static smooth(mesh: Mesh, sharpenedEdges?: Smoothness[]): Manifold; + + // Signed Distance Functions + + /** + * Constructs a level-set Mesh from the input Signed-Distance Function (SDF). + * This uses a form of Marching Tetrahedra (akin to Marching Cubes, but better + * for manifoldness). Instead of using a cubic grid, it uses a body-centered + * cubic grid (two shifted cubic grids). This means if your function's + * interior exceeds the given bounds, you will see a kind of egg-crate shape + * closing off the manifold, which is due to the underlying grid. + * + * @param sdf The signed-distance function which returns the signed distance + * of + * a given point in R^3. Positive values are inside, negative outside. + * @param bounds An axis-aligned box that defines the extent of the grid. + * @param edgeLength Approximate maximum edge length of the triangles in the + * final result. This affects grid spacing, and hence has a strong effect on + * performance. + * @param level You can inset your Mesh by using a positive value, or outset + * it with a negative value. + */ + static levelSet( + sdf: (point: Vec3) => number, bounds: Box, edgeLength: number, + level?: number): Manifold; + + // Transformations + /** * Transform this Manifold in space. Stored in column-major order. This * operation can be chained. Transforms are combined and applied lazily. @@ -254,30 +544,15 @@ export class Manifold { mirror(v: Vec3): Manifold; /** - * Boolean union - */ - add(other: Manifold): Manifold; - - /** - * Boolean difference - */ - subtract(other: Manifold): Manifold; - - /** - * Boolean intersection - */ - intersect(other: Manifold): Manifold; - - /** - * Removes everything behind the given half-space plane. + * This function does not change the topology, but allows the vertices to be + * moved according to any arbitrary input function. It is easy to create a + * function that warps a geometrically valid object into one which overlaps, + * but that is not checked here, so it is up to the user to choose their + * function with discretion. * - * @param normal This vector is normal to the cutting plane and its length - * does not matter. The result is in the direction of this vector from the - * plane. - * @param originOffset The distance of the plane from the origin in the - * direction of the normal vector. + * @param warpFunc A function that modifies a given vertex position. */ - trimByPlane(normal: Vec3, originOffset: number): Manifold; + warp(warpFunc: (vert: Vec3) => void): Manifold; /** * Increase the density of the mesh by splitting every edge into n pieces. For @@ -291,17 +566,6 @@ export class Manifold { */ refine(n: number): Manifold; - /** - * This function does not change the topology, but allows the vertices to be - * moved according to any arbitrary input function. It is easy to create a - * function that warps a geometrically valid object into one which overlaps, - * but that is not checked here, so it is up to the user to choose their - * function with discretion. - * - * @param warpFunc A function that modifies a given vertex position. - */ - warp(warpFunc: (vert: Vec3) => void): Manifold; - /** * Create a new copy of this manifold with updated vertex properties by * supplying a function that takes the existing position and properties as @@ -318,6 +582,96 @@ export class Manifold { propFunc: (newProp: number[], position: Vec3, oldProp: number[]) => void): Manifold; + // Boolean Operations + + /** + * Boolean union + */ + add(other: Manifold): Manifold; + + /** + * Boolean difference + */ + subtract(other: Manifold): Manifold; + + /** + * Boolean intersection + */ + intersect(other: Manifold): Manifold; + + /** + * Boolean union of the manifolds a and b + */ + static union(a: Manifold, b: Manifold): Manifold; + + /** + * Boolean difference of the manifold b from the manifold a + */ + static difference(a: Manifold, b: Manifold): Manifold; + + /** + * Boolean intersection of the manifolds a and b + */ + static intersection(a: Manifold, b: Manifold): Manifold; + + /** + * Boolean union of a list of manifolds + */ + static union(manifolds: Manifold[]): Manifold; + + /** + * Boolean difference of the tail of a list of manifolds from its head + */ + static difference(manifolds: Manifold[]): Manifold; + + /** + * Boolean intersection of a list of manifolds + */ + static intersection(manifolds: Manifold[]): Manifold; + + /** + * Split cuts this manifold in two using the cutter manifold. The first result + * is the intersection, second is the difference. This is more efficient than + * doing them separately. + * + * @param cutter + */ + split(cutter: Manifold): Manifold[]; + + /** + * Convenient version of Split() for a half-space. + * + * @param normal This vector is normal to the cutting plane and its length + * does + * not matter. The first result is in the direction of this vector, the second + * result is on the opposite side. + * @param originOffset The distance of the plane from the origin in the + * direction of the normal vector. + */ + splitByPlane(normal: Vec3, originOffset: number): Manifold[]; + + /** + * Removes everything behind the given half-space plane. + * + * @param normal This vector is normal to the cutting plane and its length + * does not matter. The result is in the direction of this vector from the + * plane. + * @param originOffset The distance of the plane from the origin in the + * direction of the normal vector. + */ + trimByPlane(normal: Vec3, originOffset: number): Manifold; + + // Topological Operations + + /** + * Constructs a new manifold from a list of other manifolds. This is a purely + * topological operation, so care should be taken to avoid creating + * overlapping results. It is the inverse operation of Decompose(). + * + * @param manifolds A list of Manifolds to lazy-union together. + */ + static compose(manifolds: Manifold[]): Manifold; + /** * This operation returns a vector of Manifolds that are topologically * disconnected. If everything is connected, the vector is length one, @@ -326,6 +680,8 @@ export class Manifold { */ decompose(): Manifold[]; + // Property Access + /** * Does the Manifold have any triangles? */ @@ -387,6 +743,8 @@ export class Manifold { */ getCurvature(): Curvature; + // Export + /** * Returns a Mesh that is designed to easily push into a renderer, including * all interleaved vertex properties that may have been input. It also @@ -402,6 +760,8 @@ export class Manifold { */ getMesh(normalIdx?: Vec3): Mesh; + // ID Management + /** * If you copy a manifold, but you want this new copy to have new properties * (e.g. a different UV mapping), you can reset its IDs to a new original, @@ -424,6 +784,15 @@ export class Manifold { */ originalID(): number; + /** + * Returns the first of n sequential new unique mesh IDs for marking sets of + * triangles that can be looked up after further operations. Assign to + * Mesh.runOriginalID vector. + */ + static reserveIDs(count: number): number; + + // Memory + /** * Frees the WASM memory of this Manifold, since these cannot be * garbage-collected automatically. @@ -463,4 +832,4 @@ export class Mesh { extras(vert: number): Float32Array; tangent(halfedge: number): SealedFloat32Array<4>; transform(run: number): Mat4; -} \ No newline at end of file +} diff --git a/bindings/wasm/manifold-global-types.d.ts b/bindings/wasm/manifold-global-types.d.ts index 0dd6eb11b..45c874514 100644 --- a/bindings/wasm/manifold-global-types.d.ts +++ b/bindings/wasm/manifold-global-types.d.ts @@ -43,6 +43,10 @@ export type Mat4 = [ ]; export type SimplePolygon = Vec2[]; export type Polygons = SimplePolygon|SimplePolygon[]; +export type Rect = { + min: Vec2, + max: Vec2 +}; export type Box = { min: Vec3, max: Vec3 @@ -62,4 +66,6 @@ export type Curvature = { minGaussianCurvature: number, vertMeanCurvature: number[], vertGaussianCurvature: number[] -}; \ No newline at end of file +}; +export type FillRule = 'EvenOdd'|'NonZero'|'Positive'|'Negative' +export type JoinType = 'Square'|'Round'|'Miter' diff --git a/bindings/wasm/manifold.d.ts b/bindings/wasm/manifold.d.ts index b4f6e49dc..be8a82d34 100644 --- a/bindings/wasm/manifold.d.ts +++ b/bindings/wasm/manifold.d.ts @@ -15,32 +15,21 @@ import * as T from './manifold-encapsulated-types'; export * from './manifold-global-types'; +export type CrossSection = T.CrossSection; export type Manifold = T.Manifold; export type Mesh = T.Mesh; -export interface ManifoldStatic { - cube: typeof T.cube; - cylinder: typeof T.cylinder; - sphere: typeof T.sphere; - smooth: typeof T.smooth; - tetrahedron: typeof T.tetrahedron; - extrude: typeof T.extrude; +export interface ManifoldToplevel { + CrossSection: typeof T.CrossSection; + Manifold: typeof T.Manifold; + Mesh: typeof T.Mesh; triangulate: typeof T.triangulate; - revolve: typeof T.revolve; - union: typeof T.union; - difference: typeof T.difference; - intersection: typeof T.intersection; - compose: typeof T.compose; - levelSet: typeof T.levelSet; setMinCircularAngle: typeof T.setMinCircularAngle; setMinCircularEdgeLength: typeof T.setMinCircularEdgeLength; setCircularSegments: typeof T.setCircularSegments; getCircularSegments: typeof T.getCircularSegments; - reserveIDs: typeof T.reserveIDs; - Manifold: typeof T.Manifold; - Mesh: typeof T.Mesh; setup: () => void; } export default function Module(config?: {locateFile: () => string}): - Promise; + Promise;