From 69ebacd6dd2a8bdc707e8e5bbd6999bb648502fc Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Thu, 25 May 2023 14:09:25 -0700 Subject: [PATCH 01/41] Update JS bindings with CrossSection + reorganize - add CrossSection class - move static method (constructors) under their respective classes - add Manifold.{split, splitByPlane} --- bindings/wasm/bindings.cpp | 258 ++---- bindings/wasm/bindings.js | 382 +++++++-- bindings/wasm/examples/package.json | 14 +- bindings/wasm/examples/public/examples.js | 968 +++++++++++----------- bindings/wasm/examples/vite.config.js | 6 +- bindings/wasm/examples/worker.ts | 158 ++-- bindings/wasm/helpers.cpp | 203 +++++ bindings/wasm/manifold.d.ts | 5 +- 8 files changed, 1148 insertions(+), 846 deletions(-) create mode 100644 bindings/wasm/helpers.cpp diff --git a/bindings/wasm/bindings.cpp b/bindings/wasm/bindings.cpp index 06434a196..047d83e76 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>, + CrossSection::FillRule>() + .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", &CrossSection::Offset) + .function("_RectClip", &CrossSection::RectClip) + .function("_ToPolygons", &CrossSection::ToPolygons); + + 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) @@ -284,19 +168,21 @@ EMSCRIPTEN_BINDINGS(whatever) { 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("_manifoldCompose", &Manifold::Compose); + function("_manifoldUnionN", &man_js::UnionN); + function("_manifoldDifferenceN", &man_js::DifferenceN); + function("_manifoldIntersectionN", &man_js::IntersectionN); + + // TODO: these are ambiguous with addition of CrossSection + // should they be unified with a dynamic check that the input array + // isn't mixed? function("setMinCircularAngle", &Quality::SetMinCircularAngle); function("setMinCircularEdgeLength", &Quality::SetMinCircularEdgeLength); function("setCircularSegments", &Quality::SetCircularSegments); diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index 3cfdd6ac4..c0a03d3ac 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -13,10 +13,12 @@ // limitations under the License. var _ManifoldInitialized = false; -Module.setup = function() { +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,16 +35,30 @@ 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 nPts = vec[i].size(); + const poly = []; + for (let j = 0; j < nPts; j++) { + poly.push(f(vec[i].get(j))); + } + result.push(poly); + } + return result; + } + function polygons2vec(polygons) { if (polygons[0].length < 3) { polygons = [polygons]; } return toVec( - new Module.Vector2_vec2(), polygons, - poly => toVec(new Module.Vector_vec2(), poly, p => { - if (p instanceof Array) return {x: p[0], y: p[1]}; - return p; - })); + new Module.Vector2_vec2(), polygons, + poly => toVec(new Module.Vector_vec2(), poly, p => { + if (p instanceof Array) return { x: p[0], y: p[1] }; + return p; + })); } function disposePolygons(polygonsVec) { @@ -50,17 +66,131 @@ 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]}; + return { x: vec[0][0], y: vec[0][1], z: vec[0][2] }; if (typeof (vec[0]) == 'number') // default to 0 - return {x: vec[0] || 0, y: vec[1] || 0, z: vec[2] || 0}; + return { x: vec[0] || 0, y: vec[1] || 0, z: vec[2] || 0 }; return vec[0]; } - Module.Manifold.prototype.warp = function(func) { - const wasmFuncPtr = addFunction(function(vec3Ptr) { + // 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, fillrule = 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 (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, 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]) { + if (scaleTop instanceof Array) scaleTop = { x: scaleTop[0], y: scaleTop[1] }; + return Module._Extrude(this, height, nDivisions, twistDegrees, scaleTop); + }; + + 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'); const y = getValue(vec3Ptr + 4, 'float'); const z = getValue(vec3Ptr + 8, 'float'); @@ -80,9 +210,9 @@ Module.setup = function() { return out; }; - Module.Manifold.prototype.setProperties = function(numProp, func) { + Module.Manifold.prototype.setProperties = function (numProp, func) { const oldNumProp = this.numProp; - const wasmFuncPtr = addFunction(function(newPtr, vec3Ptr, oldPtr) { + const wasmFuncPtr = addFunction(function (newPtr, vec3Ptr, oldPtr) { const newProp = []; for (let i = 0; i < numProp; ++i) { newProp[i] = getValue(newPtr + 4 * i, 'float'); @@ -107,37 +237,51 @@ Module.setup = function() { return out; }; - Module.Manifold.prototype.translate = function(...vec) { - return this._Translate(vararg2vec(vec)); + Module.Manifold.prototype.translate = function (...vec) { + return this._Translate(vararg2vec3(vec)); }; - Module.Manifold.prototype.rotate = function(vec) { + Module.Manifold.prototype.rotate = function (vec) { return this._Rotate(...vec); }; - Module.Manifold.prototype.scale = function(vec) { + Module.Manifold.prototype.scale = function (vec) { if (typeof vec == 'number') { - return this._Scale({x: vec, y: vec, z: vec}); + 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])); + Module.Manifold.prototype.mirror = function (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.decompose = function() { + 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 () { const vec = this._Decompose(); const result = fromVec(vec); vec.delete(); return result; }; - Module.Manifold.prototype.getCurvature = function() { + Module.Manifold.prototype.getCurvature = function () { const result = this._getCurvature(); const oldMeanCurvature = result.vertMeanCurvature; const oldGaussianCurvature = result.vertGaussianCurvature; @@ -148,6 +292,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, @@ -186,8 +338,8 @@ Module.setup = function() { } merge() { - const {changed, mesh} = Module._Merge(this); - Object.assign(this, {...mesh}); + const { changed, mesh } = Module._Merge(this); + Object.assign(this, { ...mesh }); return changed; } @@ -201,7 +353,7 @@ Module.setup = function() { extras(vert) { return this.vertProperties.subarray( - numProp * vert + 3, numProp * (vert + 1)); + numProp * vert + 3, numProp * (vert + 1)); } tangent(halfedge) { @@ -222,20 +374,12 @@ Module.setup = function() { Module.Mesh = Mesh; - Module.Manifold.prototype.getMesh = function(normalIdx = [0, 0, 0]) { + Module.Manifold.prototype.getMesh = function (normalIdx = [0, 0, 0]) { if (normalIdx instanceof Array) - normalIdx = {0: normalIdx[0], 1: normalIdx[1], 2: normalIdx[2]}; + normalIdx = { 0: normalIdx[0], 1: normalIdx[1], 2: normalIdx[2] }; 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) { @@ -281,11 +425,61 @@ Module.setup = function() { Module.ManifoldError.prototype = Object.create(Error.prototype, { constructor: - {value: Module.ManifoldError, writable: true, configurable: true} + { value: Module.ManifoldError, writable: true, configurable: true } }); + // CrossSection Constructors + + Module.CrossSection = function (polygons, fillrule = "Positive") { + const polygonsVec = polygons2vec(polygons); + const cs = new CrossSectionCtor(polygonsVec, fillrule = fillrule); + disposePolygons(polygonsVec); + return cs; + }; + + Module.CrossSection.ofPolygons = function (polygons, fillrule = "Positive") { + return new Module.CrossSection(polygons, fillrule = 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); + + // Manifold Constructors + const ManifoldCtor = Module.Manifold; - Module.Manifold = function(mesh) { + Module.Manifold = function (mesh) { const manifold = new ManifoldCtor(mesh); const status = manifold.status(); @@ -296,32 +490,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}; + size = { x: 1, y: 1, z: 1 }; else if (typeof args[0] == 'number') - size = {x: args[0], y: args[0], z: args[0]}; + 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( - height, radiusLow, radiusHigh = -1.0, circularSegments = 0, - center = false) { + Module.Manifold.cylinder = function ( + height, radiusLow, radiusHigh = -1.0, circularSegments = 0, + center = false) { return Module._Cylinder( - height, radiusLow, radiusHigh, circularSegments, center); + 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,46 +529,47 @@ Module.setup = function() { return result; }; - Module.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; + Module.Manifold.extrude = function ( + polygons, height, nDivisions = 0, twistDegrees = 0.0, + scaleTop = [1.0, 1.0]) { + const cs = (polygons instanceof CrossSectionCtor) ? polygons : Module.CrossSection(polygons, "Positive"); + return cs.extrude(height, nDivisions, twistDegrees, scaleTop); }; - 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.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]}, + 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] }, }; - const wasmFuncPtr = addFunction(function(vec3Ptr) { + const wasmFuncPtr = addFunction(function (vec3Ptr) { const x = getValue(vec3Ptr, 'float'); const y = getValue(vec3Ptr + 4, 'float'); const z = getValue(vec3Ptr + 8, 'float'); @@ -380,18 +581,15 @@ 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'); + // 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/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/examples.js b/bindings/wasm/examples/public/examples.js index 3b7571ba8..f76beff29 100644 --- a/bindings/wasm/examples/public/examples.js +++ b/bindings/wasm/examples/public/examples.js @@ -13,496 +13,488 @@ // limitations under the License. export const examples = { - functions: { - Intro: function() { - // Write code in JavaScript or TypeScript and this editor will show the - // API docs. Type e.g. "box." to see the Manifold API. Type "module." to - // see the static API - these functions can also be used bare. Use - // console.log() to print output (lower-right). This editor defines Z as - // up and units of mm. - - const box = cube([100, 100, 100], true); - const ball = sphere(60, 100); - // You must name your final output "result", or create at least one - // GLTFNode - see Menger Sponge and Gyroid Module examples. - const result = box.subtract(ball); - - // For visual debug, wrap any shape with show() and it and all of its - // copies will be shown in transparent red, akin to # in OpenSCAD. Or try - // only() to ghost out everything else, akin to * in OpenSCAD. - - // All changes are automatically saved and restored between sessions. - // This PWA is purely local - there is no server communication. - // This means it will work equally well offline once loaded. - // Consider installing it (icon in the search bar) for easy access. - - // See the script drop-down above ("Intro") for usage examples. The - // gl-matrix package from npm is automatically imported for convenience - - // its API is available in the top-level glMatrix object. - - // Use GLTFNode for disjoint manifolds rather than compose(), as this will - // keep them better organized in the GLB. This will also allow you to - // specify material properties, and even vertex colors via - // setProperties(). See Tetrahedron Puzzle example. - return result; - }, - - 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: - // https://www.thingiverse.com/thing:186372 - - const edgeLength = 50; // Length of each edge of the overall tetrahedron. - const gap = 0.2; // Spacing between the two halves to allow sliding. - const nDivisions = 50; // Divisions (both ways) in the screw surface. - - const scale = edgeLength / (2 * Math.sqrt(2)); - - const tet = tetrahedron().scale(scale); - - const box = []; - box.push([1, -1], [1, 1]); - for (let i = 0; i <= nDivisions; ++i) { - box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]); - } - - const cyan = [0, 1, 1]; - const magenta = [1, 0, 1]; - const fade = (color, pos) => { - for (let i = 0; i < 3; ++i) { - color[i] = cyan[i] * pos[2] + magenta[i] * (1 - pos[2]); - } - }; - - // setProperties(3, fade) creates three channels of vertex properties - // according to the above fade function. setMaterial assigns these - // channels as colors, and sets the factor to white, since our default is - // yellow. - const screw = setMaterial( - extrude(box, 1, nDivisions, 270).setProperties(3, fade), - {baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']}); - - const result = tet.intersect( - screw.rotate([0, 0, -45]).translate([0, 0, -0.5]).scale(2 * scale)); - - // Assigned materials are only applied to a GLTFNode. Note that material - // definitions cascade, applying recursively to all child surfaces, but - // overridden by any materials defined lower down. Our default material: - // { - // roughness = 0.2, - // metallic = 1, - // baseColorFactor = [1, 1, 0], - // alpha = 1, - // unlit = false, - // name = '' - // } - const node = new GLTFNode(); - node.manifold = result; - return result; - }, - - RoundedFrame: function() { - // Demonstrates how at 90-degree intersections, the sphere and cylinder - // facets match up perfectly, for any choice of global resolution - // parameters. - - function roundedFrame(edgeLength, radius, circularSegments = 0) { - const edge = cylinder(edgeLength, radius, -1, circularSegments); - const corner = sphere(radius, circularSegments); - - const edge1 = union(corner, edge).rotate([-90, 0, 0]).translate([ - -edgeLength / 2, -edgeLength / 2, 0 - ]); - - const edge2 = union( - union(edge1, edge1.rotate([0, 0, 180])), - edge.translate([-edgeLength / 2, -edgeLength / 2, 0])); - - const edge4 = union(edge2, edge2.rotate([0, 0, 90])).translate([ - 0, 0, -edgeLength / 2 - ]); - - return union(edge4, edge4.rotate([180, 0, 0])); - } - - setMinCircularAngle(3); - setMinCircularEdgeLength(0.5); - const result = roundedFrame(100, 10); - return result; - }, - - Heart: function() { - // 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 - - const func = (v) => { - const x2 = v[0] * v[0]; - const y2 = v[1] * v[1]; - const z = v[2]; - const z2 = z * z; - const a = x2 + 9 / 4 * y2 + z2; - const b = z * z2 * (x2 + 9 / 80 * y2); - const a2 = a * a; - const a3 = a * a2; - - const step = (r) => { - const r2 = r * r; - const r4 = r2 * r2; - // Taubin's function: https://mathworld.wolfram.com/HeartSurface.html - const f = a3 * r4 * r2 - b * r4 * r - 3 * a2 * r4 + 3 * a * r2 - 1; - // Derivative - const df = - 6 * a3 * r4 * r - 5 * b * r4 - 12 * a2 * r2 * r + 6 * a * r; - return f / df; - }; - // Newton's method for root finding - let r = 1.5; - let dr = 1; - while (Math.abs(dr) > 0.0001) { - dr = step(r); - r -= dr; - } - // Update radius - v[0] *= r; - v[1] *= r; - v[2] *= r; - }; - - const ball = sphere(1, 200); - const heart = ball.warp(func); - const box = heart.boundingBox(); - const result = heart.scale(100 / (box.max[0] - box.min[0])); - return result; - }, - - Scallop: function() { - // 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 - - const height = 10; - const radius = 30; - const offset = 20; - const wiggles = 12; - const sharpness = 0.8; - const n = 50; - - const positions = []; - const triangles = []; - positions.push(-offset, 0, height, -offset, 0, -height); - const sharpenedEdges = []; - - const delta = 3.14159 / wiggles; - for (let i = 0; i < 2 * wiggles; ++i) { - const theta = (i - wiggles) * delta; - const amp = 0.5 * height * Math.max(Math.cos(0.8 * theta), 0); - - positions.push( - radius * Math.cos(theta), radius * Math.sin(theta), - amp * (i % 2 == 0 ? 1 : -1)); - let j = i + 1; - if (j == 2 * wiggles) j = 0; - - const smoothness = 1 - sharpness * Math.cos((theta + delta / 2) / 2); - let halfedge = triangles.length + 1; - sharpenedEdges.push({halfedge, smoothness}); - triangles.push(0, 2 + i, 2 + j); - - halfedge = triangles.length + 1; - sharpenedEdges.push({halfedge, smoothness}); - triangles.push(1, 2 + j, 2 + i); - } - - 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); - return result; - }, - - TorusKnot: function() { - // 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. - - // @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. - - 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 circle = []; - const dPhi = 2 * 3.14159 / n; - const offset = 2; - for (let i = 0; i < n; ++i) { - circle.push([Math.cos(dPhi * i) + offset, Math.sin(dPhi * i)]); - } - - const func = (v) => { - const psi = q * Math.atan2(v[0], v[1]); - 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 = 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); - } - - return knot; - } - - // This recreates Matlab Knot by Emmett Lalish: - // https://www.thingiverse.com/thing:7080 - - const result = torusKnot(1, 3, 25, 10, 3.75); - return result; - }, - - MengerSponge: function() { - // This example demonstrates how symbolic perturbation correctly creates - // holes even though the subtracted objects are exactly coplanar. - const {vec2} = glMatrix; - - function fractal(holes, hole, w, position, depth, maxDepth) { - w /= 3; - holes.push( - hole.scale([w, w, 1.0]).translate([position[0], position[1], 0.0])); - if (depth == maxDepth) return; - const offsets = [ - vec2.fromValues(-w, -w), vec2.fromValues(-w, 0.0), - vec2.fromValues(-w, w), vec2.fromValues(0.0, w), - vec2.fromValues(w, w), vec2.fromValues(w, 0.0), - vec2.fromValues(w, -w), vec2.fromValues(0.0, -w) - ]; - for (let offset of offsets) - fractal( - holes, hole, w, vec2.add(offset, position, offset), depth + 1, - maxDepth); - } - - function mengerSponge(n) { - let result = 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])); - - return result; - } - - const posColors = (newProp, pos) => { - for (let i = 0; i < 3; ++i) { - newProp[i] = (1 - pos[i]) / 2; - } - }; - - const result = mengerSponge(3) - .trimByPlane([1, 1, 1], 0) - .setProperties(3, posColors) - .scale(100); - - const node = new GLTFNode(); - node.manifold = result; - node.material = {baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']}; - return result; - }, - - StretchyBracelet: function() { - // Recreates Stretchy Bracelet by Emmett Lalish: - // https://www.thingiverse.com/thing:13505 - const {vec2} = glMatrix; - - function base( - width, radius, decorRadius, twistRadius, nDecor, innerRadius, - outerRadius, cut, nCut, nDivision) { - let b = cylinder(width, radius + twistRadius / 2); - const circle = []; - const dPhiDeg = 180 / nDivision; - for (let i = 0; i < 2 * nDivision; ++i) { - circle.push([ - decorRadius * Math.cos(dPhiDeg * i * Math.PI / 180) + twistRadius, - decorRadius * Math.sin(dPhiDeg * i * Math.PI / 180) - ]); - } - let decor = extrude(circle, width, nDivision, 180) - .scale([1, 0.5, 1]) - .translate([0, radius, 0]); - for (let i = 0; i < nDecor; i++) - b = b.add(decor.rotate([0, 0, (360.0 / nDecor) * i])); - const stretch = []; - const dPhiRad = 2 * Math.PI / nCut; - - const o = vec2.fromValues(0, 0); - const p0 = vec2.fromValues(outerRadius, 0); - const p1 = vec2.fromValues(innerRadius, -cut); - const p2 = vec2.fromValues(innerRadius, cut); - for (let i = 0; i < nCut; ++i) { - stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i)); - stretch.push(vec2.rotate([0, 0], p1, o, dPhiRad * i)); - 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; - } - - function stretchyBracelet( - radius = 30, height = 8, width = 15, thickness = 0.4, nDecor = 20, - nCut = 27, nDivision = 30) { - const twistRadius = Math.PI * radius / nDecor; - const decorRadius = twistRadius * 1.5; - const outerRadius = radius + (decorRadius + twistRadius) * 0.5; - const innerRadius = outerRadius - height; - const cut = 0.5 * (Math.PI * 2 * innerRadius / nCut - thickness); - const adjThickness = 0.5 * thickness * height / cut; - - return difference( - base( - width, radius, decorRadius, twistRadius, nDecor, - innerRadius + thickness, outerRadius + adjThickness, - cut - adjThickness, nCut, nDivision), - base( - width, radius - thickness, decorRadius, twistRadius, nDecor, - innerRadius, outerRadius + 3 * adjThickness, cut, nCut, - nDivision)); - } - - const result = stretchyBracelet(); - return result; - }, - - GyroidModule: function() { - // Recreates Modular Gyroid Puzzle by Emmett Lalish: - // https://www.thingiverse.com/thing:25477. This sample demonstrates the - // use of a Signed Distance Function (SDF) to create smooth, complex - // manifolds. - const {vec3} = glMatrix; - - // number of modules along pyramid edge (use 1 for print orientation) - const m = 4; - // module size - const size = 20; - // SDF resolution - const n = 20; - - const pi = 3.14159; - - function gyroid(p) { - const x = p[0] - pi / 4; - const y = p[1] - pi / 4; - const z = p[2] - pi / 4; - return Math.cos(x) * Math.sin(y) + Math.cos(y) * Math.sin(z) + - Math.cos(z) * Math.sin(x); - } - - function gyroidOffset(level) { - const period = 2 * pi; - const box = { - min: vec3.fromValues(-period, -period, -period), - max: vec3.fromValues(period, period, period) - }; - return levelSet(gyroid, box, period / n, level).scale(size / period); - }; - - function rhombicDodecahedron() { - const box = cube([1, 1, 2], true).scale(size * Math.sqrt(2)); - const result = - box.rotate([90, 45, 0]).intersect(box.rotate([90, 45, 90])); - return result.intersect(box.rotate([0, 0, 45])); - } - - const gyroidModule = rhombicDodecahedron() - .intersect(gyroidOffset(-0.4)) - .subtract(gyroidOffset(0.4)); - - if (m > 1) { - for (let i = 0; i < m; ++i) { - for (let j = i; j < m; ++j) { - for (let k = j; k < m; ++k) { - const node = new GLTFNode(); - node.manifold = gyroidModule; - node.translation = - [(k + i - j) * size, (k - i) * size, (-j) * size]; - node.material = { - baseColorFactor: - [(k + i - j + 1) / m, (k - i + 1) / m, (j + 1) / m] - }; + functions: { + Intro: function () { + // Write code in JavaScript or TypeScript and this editor will show the + // API docs. Type e.g. "box." to see the Manifold API. Type "module." to + // see the static API - these functions can also be used bare. Use + // console.log() to print output (lower-right). This editor defines Z as + // up and units of mm. + + const box = Manifold.cube([100, 100, 100], true); + const ball = Manifold.sphere(60, 100); + // You must name your final output "result", or create at least one + // GLTFNode - see Menger Sponge and Gyroid Module examples. + const result = box.subtract(ball); + + // For visual debug, wrap any shape with show() and it and all of its + // copies will be shown in transparent red, akin to # in OpenSCAD. Or try + // only() to ghost out everything else, akin to * in OpenSCAD. + + // All changes are automatically saved and restored between sessions. + // This PWA is purely local - there is no server communication. + // This means it will work equally well offline once loaded. + // Consider installing it (icon in the search bar) for easy access. + + // See the script drop-down above ("Intro") for usage examples. The + // gl-matrix package from npm is automatically imported for convenience - + // its API is available in the top-level glMatrix object. + + // Use GLTFNode for disjoint manifolds rather than compose(), as this will + // keep them better organized in the GLB. This will also allow you to + // specify material properties, and even vertex colors via + // setProperties(). See Tetrahedron Puzzle example. + return result; + }, + + 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: + // https://www.thingiverse.com/thing:186372 + + const edgeLength = 50; // Length of each edge of the overall tetrahedron. + const gap = 0.2; // Spacing between the two halves to allow sliding. + const nDivisions = 50; // Divisions (both ways) in the screw surface. + + const scale = edgeLength / (2 * Math.sqrt(2)); + + const tet = Manifold.tetrahedron().scale(scale); + + const box = []; + box.push([1, -1], [1, 1]); + for (let i = 0; i <= nDivisions; ++i) { + box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]); + } + + const cyan = [0, 1, 1]; + const magenta = [1, 0, 1]; + const fade = (color, pos) => { + for (let i = 0; i < 3; ++i) { + color[i] = cyan[i] * pos[2] + magenta[i] * (1 - pos[2]); + } + }; + + // setProperties(3, fade) creates three channels of vertex properties + // according to the above fade function. setMaterial assigns these + // channels as colors, and sets the factor to white, since our default is + // yellow. + const screw = setMaterial( + Manifold.extrude(box, 1, nDivisions, 270).setProperties(3, fade), + { baseColorFactor: [1, 1, 1], attributes: ['COLOR_0'] }); + + const result = tet.intersect( + screw.rotate([0, 0, -45]).translate([0, 0, -0.5]).scale(2 * scale)); + + // Assigned materials are only applied to a GLTFNode. Note that material + // definitions cascade, applying recursively to all child surfaces, but + // overridden by any materials defined lower down. Our default material: + // { + // roughness = 0.2, + // metallic = 1, + // baseColorFactor = [1, 1, 0], + // alpha = 1, + // unlit = false, + // name = '' + // } + const node = new GLTFNode(); + node.manifold = result; + return result; + }, + + RoundedFrame: function () { + // Demonstrates how at 90-degree intersections, the sphere and cylinder + // facets match up perfectly, for any choice of global resolution + // parameters. + + function roundedFrame(edgeLength, radius, circularSegments = 0) { + const edge = Manifold.cylinder(edgeLength, radius, -1, circularSegments); + const corner = Manifold.sphere(radius, circularSegments); + + const edge1 = Manifold.union(corner, edge).rotate([-90, 0, 0]).translate([ + -edgeLength / 2, -edgeLength / 2, 0 + ]); + + const edge2 = Manifold.union( + Manifold.union(edge1, edge1.rotate([0, 0, 180])), + edge.translate([-edgeLength / 2, -edgeLength / 2, 0])); + + const edge4 = Manifold.union(edge2, edge2.rotate([0, 0, 90])).translate([ + 0, 0, -edgeLength / 2 + ]); + + return Manifold.union(edge4, edge4.rotate([180, 0, 0])); + } + + setMinCircularAngle(3); + setMinCircularEdgeLength(0.5); + const result = roundedFrame(100, 10); + return result; + }, + + Heart: function () { + // 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 + + const func = (v) => { + const x2 = v[0] * v[0]; + const y2 = v[1] * v[1]; + const z = v[2]; + const z2 = z * z; + const a = x2 + 9 / 4 * y2 + z2; + const b = z * z2 * (x2 + 9 / 80 * y2); + const a2 = a * a; + const a3 = a * a2; + + const step = (r) => { + const r2 = r * r; + const r4 = r2 * r2; + // Taubin's function: https://mathworld.wolfram.com/HeartSurface.html + const f = a3 * r4 * r2 - b * r4 * r - 3 * a2 * r4 + 3 * a * r2 - 1; + // Derivative + const df = + 6 * a3 * r4 * r - 5 * b * r4 - 12 * a2 * r2 * r + 6 * a * r; + return f / df; + }; + // Newton's method for root finding + let r = 1.5; + let dr = 1; + while (Math.abs(dr) > 0.0001) { + dr = step(r); + r -= dr; + } + // Update radius + v[0] *= r; + v[1] *= r; + v[2] *= r; + }; + + 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])); + return result; + }, + + Scallop: function () { + // 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 + + const height = 10; + const radius = 30; + const offset = 20; + const wiggles = 12; + const sharpness = 0.8; + const n = 50; + + const positions = []; + const triangles = []; + positions.push(-offset, 0, height, -offset, 0, -height); + const sharpenedEdges = []; + + const delta = 3.14159 / wiggles; + for (let i = 0; i < 2 * wiggles; ++i) { + const theta = (i - wiggles) * delta; + const amp = 0.5 * height * Math.max(Math.cos(0.8 * theta), 0); + + positions.push( + radius * Math.cos(theta), radius * Math.sin(theta), + amp * (i % 2 == 0 ? 1 : -1)); + let j = i + 1; + if (j == 2 * wiggles) j = 0; + + const smoothness = 1 - sharpness * Math.cos((theta + delta / 2) / 2); + let halfedge = triangles.length + 1; + sharpenedEdges.push({ halfedge, smoothness }); + triangles.push(0, 2 + i, 2 + j); + + halfedge = triangles.length + 1; + sharpenedEdges.push({ halfedge, smoothness }); + triangles.push(1, 2 + j, 2 + i); + } + + const triVerts = Uint32Array.from(triangles); + const vertProperties = Float32Array.from(positions); + const scallop = new Mesh({ numProp: 3, triVerts, vertProperties }); + const result = Manifold.smooth(scallop, sharpenedEdges).refine(n); + return result; + }, + + TorusKnot: function () { + // 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. + + // @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. + + function torusKnot( + p, q, majorRadius, minorRadius, threadRadius, circularSegments = 0, + linearSegments = 0) { + const { vec3 } = glMatrix; + + function gcd(a, b) { + return b == 0 ? a : gcd(b, a % b); + } + + const kLoops = gcd(p, q); + p /= kLoops; + q /= kLoops; + const n = circularSegments > 2 ? circularSegments : + getCircularSegments(threadRadius); + const m = linearSegments > 2 ? linearSegments : + n * q * majorRadius / threadRadius; + + const offset = 2. + const circle = CrossSection.circle(1., n).translate([offset, 0.]); + + const func = (v) => { + const psi = q * Math.atan2(v[0], v[1]); + const theta = psi * p / q; + const x1 = Math.sqrt(v[0] * v[0] + v[1] * v[1]); + const phi = Math.atan2(x1 - offset, v[2]); + vec3.set( + v, threadRadius * Math.cos(phi), 0, threadRadius * Math.sin(phi)); + const center = vec3.fromValues(0, 0, 0); + const r = majorRadius + minorRadius * Math.cos(theta); + vec3.rotateX(v, v, center, -Math.atan2(p * minorRadius, q * r)); + v[0] += minorRadius; + vec3.rotateY(v, v, center, theta); + v[0] += majorRadius; + vec3.rotateZ(v, v, center, psi); + }; + + let knot = Manifold.revolve(circle, m).warp(func); + + if (kLoops > 1) { + const knots = []; + for (let k = 0; k < kLoops; ++k) { + knots.push(knot.rotate([0, 0, 360 * (k / kLoops) * (q / p)])); + } + knot = Manifold.compose(knots); + } + + return knot; + } + + // This recreates Matlab Knot by Emmett Lalish: + // https://www.thingiverse.com/thing:7080 + + const result = torusKnot(1, 3, 25, 10, 3.75); + return result; + }, + + MengerSponge: function () { + // This example demonstrates how symbolic perturbation correctly creates + // holes even though the subtracted objects are exactly coplanar. + const { vec2 } = glMatrix; + + function fractal(holes, hole, w, position, depth, maxDepth) { + w /= 3; + holes.push( + hole.scale([w, w, 1.0]).translate([position[0], position[1], 0.0])); + if (depth == maxDepth) return; + const offsets = [ + vec2.fromValues(-w, -w), vec2.fromValues(-w, 0.0), + vec2.fromValues(-w, w), vec2.fromValues(0.0, w), + vec2.fromValues(w, w), vec2.fromValues(w, 0.0), + vec2.fromValues(w, -w), vec2.fromValues(0.0, -w) + ]; + for (let offset of offsets) + fractal( + holes, hole, w, vec2.add(offset, position, offset), depth + 1, + maxDepth); + } + + function mengerSponge(n) { + let result = Manifold.cube([1, 1, 1], true); + const holes = []; + fractal(holes, result, 1.0, [0.0, 0.0], 1, n); + + const hole = Manifold.compose(holes); + + result = Manifold.difference(result, hole); + result = Manifold.difference(result, hole.rotate([90, 0, 0])); + result = Manifold.difference(result, hole.rotate([0, 90, 0])); + + return result; + } + + const posColors = (newProp, pos) => { + for (let i = 0; i < 3; ++i) { + newProp[i] = (1 - pos[i]) / 2; + } + }; + + const result = mengerSponge(3) + .trimByPlane([1, 1, 1], 0) + .setProperties(3, posColors) + .scale(100); + + const node = new GLTFNode(); + node.manifold = result; + node.material = { baseColorFactor: [1, 1, 1], attributes: ['COLOR_0'] }; + return result; + }, + + StretchyBracelet: function () { + // Recreates Stretchy Bracelet by Emmett Lalish: + // https://www.thingiverse.com/thing:13505 + const { vec2 } = glMatrix; + + function base( + width, radius, decorRadius, twistRadius, nDecor, innerRadius, + outerRadius, cut, nCut, nDivision) { + let b = Manifold.cylinder(width, radius + twistRadius / 2); + const circle = []; + const dPhiDeg = 180 / nDivision; + for (let i = 0; i < 2 * nDivision; ++i) { + circle.push([ + decorRadius * Math.cos(dPhiDeg * i * Math.PI / 180) + twistRadius, + decorRadius * Math.sin(dPhiDeg * i * Math.PI / 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++) + b = b.add(decor.rotate([0, 0, (360.0 / nDecor) * i])); + const stretch = []; + const dPhiRad = 2 * Math.PI / nCut; + + const o = vec2.fromValues(0, 0); + const p0 = vec2.fromValues(outerRadius, 0); + const p1 = vec2.fromValues(innerRadius, -cut); + const p2 = vec2.fromValues(innerRadius, cut); + for (let i = 0; i < nCut; ++i) { + stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i)); + stretch.push(vec2.rotate([0, 0], p1, o, dPhiRad * i)); + stretch.push(vec2.rotate([0, 0], p2, o, dPhiRad * i)); + stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i)); + } + b = Manifold.intersection(Manifold.extrude(stretch, width), b); + return b; + } + + function stretchyBracelet( + radius = 30, height = 8, width = 15, thickness = 0.4, nDecor = 20, + nCut = 27, nDivision = 30) { + const twistRadius = Math.PI * radius / nDecor; + const decorRadius = twistRadius * 1.5; + const outerRadius = radius + (decorRadius + twistRadius) * 0.5; + const innerRadius = outerRadius - height; + const cut = 0.5 * (Math.PI * 2 * innerRadius / nCut - thickness); + const adjThickness = 0.5 * thickness * height / cut; + + return Manifold.difference( + base( + width, radius, decorRadius, twistRadius, nDecor, + innerRadius + thickness, outerRadius + adjThickness, + cut - adjThickness, nCut, nDivision), + base( + width, radius - thickness, decorRadius, twistRadius, nDecor, + innerRadius, outerRadius + 3 * adjThickness, cut, nCut, + nDivision)); + } + + const result = stretchyBracelet(); + return result; + }, + + GyroidModule: function () { + // Recreates Modular Gyroid Puzzle by Emmett Lalish: + // https://www.thingiverse.com/thing:25477. This sample demonstrates the + // use of a Signed Distance Function (SDF) to create smooth, complex + // manifolds. + const { vec3 } = glMatrix; + + // number of modules along pyramid edge (use 1 for print orientation) + const m = 4; + // module size + const size = 20; + // SDF resolution + const n = 20; + + const pi = 3.14159; + + function gyroid(p) { + const x = p[0] - pi / 4; + const y = p[1] - pi / 4; + const z = p[2] - pi / 4; + return Math.cos(x) * Math.sin(y) + Math.cos(y) * Math.sin(z) + + Math.cos(z) * Math.sin(x); + } + + function gyroidOffset(level) { + const period = 2 * pi; + const box = { + min: vec3.fromValues(-period, -period, -period), + max: vec3.fromValues(period, period, period) + }; + return Manifold.levelSet(gyroid, box, period / n, level).scale(size / period); + }; + + function rhombicDodecahedron() { + const box = Manifold.cube([1, 1, 2], true).scale(size * Math.sqrt(2)); + const result = + box.rotate([90, 45, 0]).intersect(box.rotate([90, 45, 90])); + return result.intersect(box.rotate([0, 0, 45])); + } + + const gyroidModule = rhombicDodecahedron() + .intersect(gyroidOffset(-0.4)) + .subtract(gyroidOffset(0.4)); + + if (m > 1) { + for (let i = 0; i < m; ++i) { + for (let j = i; j < m; ++j) { + for (let k = j; k < m; ++k) { + const node = new GLTFNode(); + node.manifold = gyroidModule; + node.translation = + [(k + i - j) * size, (k - i) * size, (-j) * size]; + node.material = { + baseColorFactor: + [(k + i - j + 1) / m, (k - i + 1) / m, (j + 1) / m] + }; + } + } + } + } + + const result = gyroidModule.rotate([-45, 0, 90]).translate([ + 0, 0, size / Math.sqrt(2) + ]); + return result; } - } - } - } - - const result = gyroidModule.rotate([-45, 0, 90]).translate([ - 0, 0, size / Math.sqrt(2) - ]); - return result; - } - }, - - functionBodies: new Map() + }, + + functionBodies: new Map() }; for (const [func, code] of Object.entries(examples.functions)) { - const whole = code.toString(); - const lines = whole.split('\n'); - lines.splice(0, 1); // remove first line - lines.splice(-2, 2); // remove last two lines - // remove first six leading spaces - const body = '\n' + lines.map(l => l.slice(6)).join('\n'); - - const name = - func.replace(/([a-z])([A-Z])/g, '$1 $2'); // Add spaces between words - examples.functionBodies.set(name, body); + const whole = code.toString(); + const lines = whole.split('\n'); + lines.splice(0, 1); // remove first line + lines.splice(-2, 2); // remove last two lines + // remove first six leading spaces + const body = '\n' + lines.map(l => l.slice(6)).join('\n'); + + const name = + func.replace(/([a-z])([A-Z])/g, '$1 $2'); // Add spaces between words + examples.functionBodies.set(name, body); }; - -if (typeof self !== 'undefined') { - self.examples = examples; -} \ No newline at end of file diff --git a/bindings/wasm/examples/vite.config.js b/bindings/wasm/examples/vite.config.js index b9a7e0cb5..05c223750 100644 --- a/bindings/wasm/examples/vite.config.js +++ b/bindings/wasm/examples/vite.config.js @@ -1,9 +1,9 @@ // vite.config.js -import {resolve} from 'path' -import {defineConfig} from 'vite' +import { resolve } from 'path' +import { defineConfig } from 'vite' export default defineConfig({ - test: {testTimeout: 15000}, + test: { timeout: 10000 }, worker: { format: 'es', }, diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 57b51f959..765e6e5d3 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Document, Material, Node, WebIO} from '@gltf-transform/core'; -import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions'; +import { Document, Material, Node, WebIO } from '@gltf-transform/core'; +import { KHRMaterialsUnlit, KHRONOS_EXTENSIONS } from '@gltf-transform/extensions'; import * as glMatrix from 'gl-matrix'; import Module from './built/manifold'; //@ts-ignore -import {setupIO, writeMesh} from './gltf-io'; -import type {GLTFMaterial, Quat} from './public/editor'; -import type {Manifold, ManifoldStatic, Mesh, Vec3} from './public/manifold'; +import { setupIO, writeMesh } from './gltf-io'; +import type { GLTFMaterial, Quat } from './public/editor'; +import type { CrossSection, Manifold, ManifoldStatic, Mesh, Vec3 } from './public/manifold'; interface WorkerStatic extends ManifoldStatic { GLTFNode: typeof GLTFNode; @@ -100,7 +100,7 @@ class GLTFNode { nodes.push(this); } clone(parent?: GLTFNode) { - const copy = {...this}; + const copy = { ...this }; copy._parent = parent; nodes.push(copy); return copy; @@ -118,64 +118,88 @@ 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' +// manifold static methods +const manifoldStaticFunctions = [ + 'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'smooth', + 'compose', 'union', 'difference', 'intersection', 'levelSet', 'ofMesh' ]; -// 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' +// manifold member functions that return a new manifold +const manifoldMemberFunctions = [ + 'add', 'subtract', 'intersect', 'trimByPlane', 'refine', 'warp', 'transform', + 'setProperties', 'translate', 'rotate', 'scale', 'mirror', 'asOriginal', 'decompose', + 'split', 'splitByPlane' ]; -const utils = [ +// CrossSection static methods +const crossSectionStaticFunctions = [ + 'square', 'circle', 'union', 'difference', 'intersection', 'compose', 'ofPolygons' +]; +// CrossSection member functions that returns a new manifold +const crossSectionMemberFunctions = [ + 'add', 'subtract', 'intersect', 'transform', + 'translate', 'rotate', 'scale', 'mirror', 'decompose', 'simplify', 'offset', + 'rectClip', 'toPolygons' +]; +// top level functions that construct a new manifolds/meshes +const toplevelConstructors = [ + 'show', 'only', 'setMaterial' +]; +const toplevel = [ 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', - 'getCircularSegments', 'Mesh', 'GLTFNode' + 'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection' ]; -const exposedFunctions = constructors.concat(utils); +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 manifoldRegistry = new Array(); -for (const name of memberFunctions) { - //@ts-ignore - const originalFn = module.Manifold.prototype[name]; - //@ts-ignore - module.Manifold.prototype['_' + name] = originalFn; +const memoryRegistry = new Array(); + +function addMembers(className: string, methodNames: Array, areStatic: boolean) { //@ts-ignore - module.Manifold.prototype[name] = function(...args: any) { + const cls = module[className]; + const obj = areStatic ? cls : cls.prototype; + for (const name of methodNames) { //@ts-ignore - const result = this['_' + name](...args); - manifoldRegistry.push(result); - return result; - }; + const originalFn = obj[name]; + //@ts-ignore + obj['_' + name] = originalFn; + //@ts-ignore + obj[name] = function (...args: any) { + //@ts-ignore + const result = this['_' + name](...args); + memoryRegistry.push(result); + return result; + }; + } } -for (const name of constructors) { +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) { + module[name] = function (...args: any) { const result = originalFn(...args); - manifoldRegistry.push(result); + memoryRegistry.push(result); return result; }; } -module.cleanup = function() { - for (const obj of manifoldRegistry) { +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(); } - manifoldRegistry.length = 0; + memoryRegistry.length = 0; }; // Setup complete @@ -183,7 +207,7 @@ self.postMessage(null); if (self.console) { const oldLog = self.console.log; - self.console.log = function(...args) { + self.console.log = function (...args) { let message = ''; for (const arg of args) { if (arg == null) { @@ -194,7 +218,7 @@ if (self.console) { message += arg.toString(); } } - self.postMessage({log: message}); + self.postMessage({ log: message }); oldLog(...args); }; } @@ -208,16 +232,16 @@ function log(...args: any[]) { self.onmessage = async (e) => { const content = e.data + - '\nreturn exportGLB(typeof result === "undefined" ? undefined : result);\n'; + '\nreturn exportGLB(typeof result === "undefined" ? undefined : result);\n'; try { const f = new Function( - 'exportGLB', 'glMatrix', 'module', ...exposedFunctions, content); + 'exportGLB', 'glMatrix', 'module', ...exposedFunctions, content); await f( - exportGLB, glMatrix, module, //@ts-ignore - ...exposedFunctions.map(name => module[name])); + exportGLB, glMatrix, module, //@ts-ignore + ...exposedFunctions.map(name => module[name])); } catch (error: any) { console.log(error.toString()); - self.postMessage({objectURL: null}); + self.postMessage({ objectURL: null }); } finally { module.cleanup(); cleanup(); @@ -230,7 +254,7 @@ function createGLTFnode(doc: Document, node: GLTFNode) { out.setTranslation(node.translation); } if (node.rotation) { - const {quat} = glMatrix; + const { quat } = glMatrix; const deg2rad = Math.PI / 180; const q = quat.create() as Quat; quat.rotateX(q, q, deg2rad * node.rotation[0]); @@ -274,8 +298,8 @@ function makeDefaultedMaterial(doc: Document, { } return material.setRoughnessFactor(roughness) - .setMetallicFactor(metallic) - .setBaseColorFactor([...baseColorFactor, alpha]); + .setMetallicFactor(metallic) + .setBaseColorFactor([...baseColorFactor, alpha]); } function getCachedMaterial(doc: Document, matDef: GLTFMaterial): Material { @@ -286,8 +310,8 @@ function getCachedMaterial(doc: Document, matDef: GLTFMaterial): Material { } function addMesh( - doc: Document, node: Node, manifold: Manifold, - backupMaterial: GLTFMaterial = {}) { + doc: Document, node: Node, manifold: Manifold, + backupMaterial: GLTFMaterial = {}) { const numTri = manifold.numTri(); if (numTri == 0) { log('Empty manifold, skipping.'); @@ -300,11 +324,9 @@ function addMesh( for (let i = 0; i < 3; i++) { size[i] = Math.round((box.max[i] - box.min[i]) * 10) / 10; } - log(`Bounding Box: X = ${size[0].toLocaleString()} mm, Y = ${ - size[1].toLocaleString()} mm, Z = ${size[2].toLocaleString()} mm`); + log(`Bounding Box: X = ${size[0].toLocaleString()} mm, Y = ${size[1].toLocaleString()} mm, Z = ${size[2].toLocaleString()} mm`); const volume = Math.round(manifold.getProperties().volume / 10); - log(`Genus: ${manifold.genus().toLocaleString()}, Volume: ${ - (volume / 100).toLocaleString()} cm^3`); + log(`Genus: ${manifold.genus().toLocaleString()}, Volume: ${(volume / 100).toLocaleString()} cm^3`); // From Z-up to Y-up (glTF) const manifoldMesh = manifold.getMesh(); @@ -335,10 +357,10 @@ function addMesh( } const mat = single ? materials[run] : SHOW; const debugNode = - doc.createNode('debug') - .setMesh(writeMesh( - doc, inMesh, attributes, [getCachedMaterial(doc, mat)])) - .setMatrix(manifoldMesh.transform(run)); + doc.createNode('debug') + .setMesh(writeMesh( + doc, inMesh, attributes, [getCachedMaterial(doc, mat)])) + .setMatrix(manifoldMesh.transform(run)); node.addChild(debugNode); } } @@ -352,8 +374,8 @@ function cloneNode(toNode: Node, fromNode: Node) { } function cloneNodeNewMaterial( - doc: Document, toNode: Node, fromNode: Node, backupMaterial: Material, - oldBackupMaterial: Material) { + doc: Document, toNode: Node, fromNode: Node, backupMaterial: Material, + oldBackupMaterial: Material) { cloneNode(toNode, fromNode); const mesh = doc.createMesh(); toNode.setMesh(mesh); @@ -367,8 +389,8 @@ function cloneNodeNewMaterial( } function createNodeFromCache( - doc: Document, nodeDef: GLTFNode, - manifold2node: Map>): Node { + doc: Document, nodeDef: GLTFNode, + manifold2node: Map>): Node { const node = createGLTFnode(doc, nodeDef); if (nodeDef.manifold != null) { const backupMaterial = getBackupMaterial(nodeDef); @@ -383,8 +405,8 @@ function createNodeFromCache( if (cachedNode == null) { const [oldBackupMaterial, oldNode] = cachedNodes.entries().next().value; cloneNodeNewMaterial( - doc, node, oldNode, getCachedMaterial(doc, backupMaterial), - getCachedMaterial(doc, oldBackupMaterial)); + doc, node, oldNode, getCachedMaterial(doc, backupMaterial), + getCachedMaterial(doc, oldBackupMaterial)); cachedNodes.set(backupMaterial, node); } else { cloneNode(node, cachedNode); @@ -399,8 +421,8 @@ async function exportGLB(manifold?: Manifold) { const halfRoot2 = Math.sqrt(2) / 2; const mm2m = 1 / 1000; const wrapper = doc.createNode('wrapper') - .setRotation([-halfRoot2, 0, 0, halfRoot2]) - .setScale([mm2m, mm2m, mm2m]); + .setRotation([-halfRoot2, 0, 0, halfRoot2]) + .setScale([mm2m, mm2m, mm2m]); doc.createScene().addChild(wrapper); if (nodes.length > 0) { @@ -425,7 +447,7 @@ async function exportGLB(manifold?: Manifold) { } log('Total glTF nodes: ', nodes.length, - ', Total mesh references: ', leafNodes); + ', Total mesh references: ', leafNodes); } else { if (manifold == null) { log('No output because "result" is undefined and no "GLTFNode"s were created.'); @@ -438,6 +460,6 @@ async function exportGLB(manifold?: Manifold) { const glb = await io.writeBinary(doc); - const blob = new Blob([glb], {type: 'application/octet-stream'}); - self.postMessage({objectURL: URL.createObjectURL(blob)}); -} \ No newline at end of file + const blob = new Blob([glb], { type: 'application/octet-stream' }); + self.postMessage({ objectURL: URL.createObjectURL(blob) }); +} diff --git a/bindings/wasm/helpers.cpp b/bindings/wasm/helpers.cpp new file mode 100644 index 000000000..c94b38d5f --- /dev/null +++ b/bindings/wasm/helpers.cpp @@ -0,0 +1,203 @@ +#include +#include +#include +#include +#include + +#include + +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 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); +} +} // 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.d.ts b/bindings/wasm/manifold.d.ts index b4f6e49dc..6aa2b750d 100644 --- a/bindings/wasm/manifold.d.ts +++ b/bindings/wasm/manifold.d.ts @@ -15,6 +15,7 @@ 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; @@ -37,10 +38,10 @@ export interface ManifoldStatic { setCircularSegments: typeof T.setCircularSegments; getCircularSegments: typeof T.getCircularSegments; reserveIDs: typeof T.reserveIDs; + CrossSection: typeof T.CrossSection; Manifold: typeof T.Manifold; Mesh: typeof T.Mesh; setup: () => void; } -export default function Module(config?: {locateFile: () => string}): - Promise; +export default function Module(config: { locateFile: () => string }): Promise; From fdd6f569b357ec03140a953c9d2155f5569288c3 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Thu, 25 May 2023 14:37:59 -0700 Subject: [PATCH 02/41] ignore no-arg call of Module --- bindings/wasm/examples/worker.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 765e6e5d3..cdd326e2d 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -30,6 +30,7 @@ interface WorkerStatic extends ManifoldStatic { cleanup(): void; } +//@ts-ignore const module = await Module() as unknown as WorkerStatic; module.setup(); From 47c674e75b859ab7ebbf4da05bf4281677ee861d Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Thu, 25 May 2023 14:43:26 -0700 Subject: [PATCH 03/41] roughly group functions names by purpose --- bindings/wasm/examples/worker.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index cdd326e2d..97ddba214 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -121,32 +121,38 @@ module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => { // manifold static methods const manifoldStaticFunctions = [ - 'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'smooth', - 'compose', 'union', 'difference', 'intersection', 'levelSet', 'ofMesh' + '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', 'trimByPlane', 'refine', 'warp', 'transform', - 'setProperties', 'translate', 'rotate', 'scale', 'mirror', 'asOriginal', 'decompose', - 'split', 'splitByPlane' + 'add', 'subtract', 'intersect', 'decompose', + 'warp', 'transform', 'translate', 'rotate', 'scale', 'mirror', + 'refine', 'setProperties', 'asOriginal', + 'trimByPlane', 'split', 'splitByPlane' ]; // CrossSection static methods const crossSectionStaticFunctions = [ - 'square', 'circle', 'union', 'difference', 'intersection', 'compose', 'ofPolygons' + 'square', 'circle', + 'union', 'difference', 'intersection', 'compose', + 'ofPolygons' ]; // CrossSection member functions that returns a new manifold const crossSectionMemberFunctions = [ - 'add', 'subtract', 'intersect', 'transform', - 'translate', 'rotate', 'scale', 'mirror', 'decompose', 'simplify', 'offset', - 'rectClip', 'toPolygons' + 'add', 'subtract', 'intersect', 'rectClip', 'decompose', + 'transform', 'translate', 'rotate', 'scale', 'mirror', + 'simplify', 'offset', + 'toPolygons' ]; // top level functions that construct a new manifolds/meshes const toplevelConstructors = [ 'show', 'only', 'setMaterial' ]; const toplevel = [ - 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', - 'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection' + 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', 'getCircularSegments', + 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection' ]; const exposedFunctions = toplevelConstructors.concat(toplevel); From 356c9bfcfc0dcb20aec8f025393a2b09b65bae2b Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 00:48:28 -0700 Subject: [PATCH 04/41] clang-format pass over js files --- bindings/wasm/bindings.js | 180 ++-- bindings/wasm/examples/public/examples.js | 964 +++++++++++----------- bindings/wasm/examples/worker.ts | 113 ++- bindings/wasm/manifold.d.ts | 3 +- 4 files changed, 633 insertions(+), 627 deletions(-) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index c0a03d3ac..02ac33438 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -13,7 +13,7 @@ // limitations under the License. var _ManifoldInitialized = false; -Module.setup = function () { +Module.setup = function() { if (_ManifoldInitialized) return; _ManifoldInitialized = true; @@ -54,11 +54,11 @@ Module.setup = function () { polygons = [polygons]; } return toVec( - new Module.Vector2_vec2(), polygons, - poly => toVec(new Module.Vector_vec2(), poly, p => { - if (p instanceof Array) return { x: p[0], y: p[1] }; - return p; - })); + new Module.Vector2_vec2(), polygons, + poly => toVec(new Module.Vector_vec2(), poly, p => { + if (p instanceof Array) return {x: p[0], y: p[1]}; + return p; + })); } function disposePolygons(polygonsVec) { @@ -67,20 +67,19 @@ Module.setup = function () { } function vararg2vec2(vec) { - if (vec[0] instanceof Array) - return { x: vec[0][0], y: vec[0][1] }; + 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 {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] }; + return {x: vec[0][0], y: vec[0][1], z: vec[0][2]}; if (typeof (vec[0]) == 'number') // default to 0 - return { x: vec[0] || 0, y: vec[1] || 0, z: vec[2] || 0 }; + return {x: vec[0] || 0, y: vec[1] || 0, z: vec[2] || 0}; return vec[0]; } @@ -88,7 +87,7 @@ Module.setup = function () { const CrossSectionCtor = Module.CrossSection; - function cross(polygons, fillrule = "Positive") { + function cross(polygons, fillrule = 'Positive') { if (polygons instanceof CrossSectionCtor) { return polygons; } else { @@ -99,27 +98,27 @@ Module.setup = function () { } }; - Module.CrossSection.prototype.translate = function (...vec) { + Module.CrossSection.prototype.translate = function(...vec) { return this._Translate(vararg2vec2(vec)); }; - Module.CrossSection.prototype.rotate = function (vec) { + Module.CrossSection.prototype.rotate = function(vec) { return this._Rotate(...vec); }; - Module.CrossSection.prototype.scale = function (vec) { + Module.CrossSection.prototype.scale = function(vec) { if (typeof vec == 'number') { - return this._Scale({ x: vec, y: vec }); + return this._Scale({x: vec, y: vec}); } return this._Scale(vararg2vec2([vec])); }; - Module.CrossSection.prototype.mirror = function (vec) { + Module.CrossSection.prototype.mirror = function(vec) { return this._Mirror(vararg2vec2([vec])); }; - Module.CrossSection.prototype.warp = function (func) { - const wasmFuncPtr = addFunction(function (vec2Ptr) { + 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]; @@ -132,14 +131,14 @@ Module.setup = function () { return out; }; - Module.CrossSection.prototype.decompose = function () { + Module.CrossSection.prototype.decompose = function() { const vec = this._Decompose(); const result = fromVec(vec); vec.delete(); return result; }; - Module.CrossSection.prototype.bounds = function () { + Module.CrossSection.prototype.bounds = function() { const result = this._Bounds(); return { min: ['x', 'y'].map(f => result.min[f]), @@ -147,40 +146,42 @@ Module.setup = function () { }; }; - Module.CrossSection.prototype.offset = function (delta, jointype = "Square", miterLimit = 2.0, arcTolerance = 0.) { + Module.CrossSection.prototype.offset = function( + delta, jointype = 'Square', miterLimit = 2.0, arcTolerance = 0.) { return this._Offset(delta, jointype, miterLimit, arcTolerance); }; - Module.CrossSection.prototype.rectClip = function (rect) { + 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] }, + 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]) { - if (scaleTop instanceof Array) scaleTop = { x: scaleTop[0], y: scaleTop[1] }; + Module.CrossSection.prototype.extrude = function( + height, nDivisions = 0, twistDegrees = 0.0, scaleTop = [1.0, 1.0]) { + if (scaleTop instanceof Array) scaleTop = {x: scaleTop[0], y: scaleTop[1]}; return Module._Extrude(this, height, nDivisions, twistDegrees, scaleTop); }; - Module.CrossSection.prototype.revolve = function (circularSegments = 0) { + Module.CrossSection.prototype.revolve = function(circularSegments = 0) { return Module._Revolve(this, circularSegments); }; - Module.CrossSection.prototype.add = function (other) { + Module.CrossSection.prototype.add = function(other) { return this._add(cross(other)); }; - Module.CrossSection.prototype.subtract = function (other) { + Module.CrossSection.prototype.subtract = function(other) { return this._subtract(cross(other)); }; - Module.CrossSection.prototype.intersect = function (other) { + Module.CrossSection.prototype.intersect = function(other) { return this._intersect(cross(other)); }; - Module.CrossSection.prototype.toPolygons = function () { + Module.CrossSection.prototype.toPolygons = function() { const vec = this._ToPolygons(); const result = vec2polygons(vec); vec.delete(); @@ -189,8 +190,8 @@ Module.setup = function () { // Manifold methods - Module.Manifold.prototype.warp = function (func) { - const wasmFuncPtr = addFunction(function (vec3Ptr) { + Module.Manifold.prototype.warp = function(func) { + const wasmFuncPtr = addFunction(function(vec3Ptr) { const x = getValue(vec3Ptr, 'float'); const y = getValue(vec3Ptr + 4, 'float'); const z = getValue(vec3Ptr + 8, 'float'); @@ -210,9 +211,9 @@ Module.setup = function () { return out; }; - Module.Manifold.prototype.setProperties = function (numProp, func) { + Module.Manifold.prototype.setProperties = function(numProp, func) { const oldNumProp = this.numProp; - const wasmFuncPtr = addFunction(function (newPtr, vec3Ptr, oldPtr) { + const wasmFuncPtr = addFunction(function(newPtr, vec3Ptr, oldPtr) { const newProp = []; for (let i = 0; i < numProp; ++i) { newProp[i] = getValue(newPtr + 4 * i, 'float'); @@ -237,51 +238,51 @@ Module.setup = function () { return out; }; - Module.Manifold.prototype.translate = function (...vec) { + Module.Manifold.prototype.translate = function(...vec) { return this._Translate(vararg2vec3(vec)); }; - Module.Manifold.prototype.rotate = function (vec) { + Module.Manifold.prototype.rotate = function(vec) { return this._Rotate(...vec); }; - Module.Manifold.prototype.scale = function (vec) { + Module.Manifold.prototype.scale = function(vec) { if (typeof vec == 'number') { - return this._Scale({ x: vec, y: vec, z: vec }); + return this._Scale({x: vec, y: vec, z: vec}); } return this._Scale(vararg2vec3([vec])); }; - Module.Manifold.prototype.mirror = function (vec) { + Module.Manifold.prototype.mirror = function(vec) { return this._Mirror(vararg2vec3([vec])); }; - Module.Manifold.prototype.trimByPlane = function (normal, offset = 0.) { + Module.Manifold.prototype.trimByPlane = function(normal, offset = 0.) { return this._TrimByPlane(vararg2vec3([normal]), offset); }; - Module.Manifold.prototype.split = function (manifold) { + 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.) { + 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 () { + Module.Manifold.prototype.decompose = function() { const vec = this._Decompose(); const result = fromVec(vec); vec.delete(); return result; }; - Module.Manifold.prototype.getCurvature = function () { + Module.Manifold.prototype.getCurvature = function() { const result = this._getCurvature(); const oldMeanCurvature = result.vertMeanCurvature; const oldGaussianCurvature = result.vertGaussianCurvature; @@ -292,7 +293,7 @@ Module.setup = function () { return result; }; - Module.Manifold.prototype.boundingBox = function () { + Module.Manifold.prototype.boundingBox = function() { const result = this._boundingBox(); return { min: ['x', 'y', 'z'].map(f => result.min[f]), @@ -338,8 +339,8 @@ Module.setup = function () { } merge() { - const { changed, mesh } = Module._Merge(this); - Object.assign(this, { ...mesh }); + const {changed, mesh} = Module._Merge(this); + Object.assign(this, {...mesh}); return changed; } @@ -353,7 +354,7 @@ Module.setup = function () { extras(vert) { return this.vertProperties.subarray( - numProp * vert + 3, numProp * (vert + 1)); + numProp * vert + 3, numProp * (vert + 1)); } tangent(halfedge) { @@ -374,9 +375,9 @@ Module.setup = function () { Module.Mesh = Mesh; - Module.Manifold.prototype.getMesh = function (normalIdx = [0, 0, 0]) { + Module.Manifold.prototype.getMesh = function(normalIdx = [0, 0, 0]) { if (normalIdx instanceof Array) - normalIdx = { 0: normalIdx[0], 1: normalIdx[1], 2: normalIdx[2] }; + normalIdx = {0: normalIdx[0], 1: normalIdx[1], 2: normalIdx[2]}; return new Mesh(this._GetMeshJS(normalIdx)); }; @@ -425,41 +426,42 @@ Module.setup = function () { Module.ManifoldError.prototype = Object.create(Error.prototype, { constructor: - { value: Module.ManifoldError, writable: true, configurable: true } + {value: Module.ManifoldError, writable: true, configurable: true} }); // CrossSection Constructors - Module.CrossSection = function (polygons, fillrule = "Positive") { + Module.CrossSection = function(polygons, fillrule = 'Positive') { const polygonsVec = polygons2vec(polygons); const cs = new CrossSectionCtor(polygonsVec, fillrule = fillrule); disposePolygons(polygonsVec); return cs; }; - Module.CrossSection.ofPolygons = function (polygons, fillrule = "Positive") { + Module.CrossSection.ofPolygons = function(polygons, fillrule = 'Positive') { return new Module.CrossSection(polygons, fillrule = fillrule); }; - Module.CrossSection.square = function (...args) { + Module.CrossSection.square = function(...args) { let size = undefined; if (args.length == 0) - size = { x: 1, y: 1 }; + size = {x: 1, y: 1}; else if (typeof args[0] == 'number') - size = { x: args[0], y: args[0] }; + 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) { + Module.CrossSection.circle = function(radius, circularSegments = 0) { return Module._Circle(radius, circularSegments); }; - // allows args to be either CrossSection or polygons (constructed with Positive fill) + // allows args to be either CrossSection or polygons (constructed with + // Positive fill) function crossSectionBatchbool(name) { - return function (...args) { + 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)); @@ -479,7 +481,7 @@ Module.setup = function () { // Manifold Constructors const ManifoldCtor = Module.Manifold; - Module.Manifold = function (mesh) { + Module.Manifold = function(mesh) { const manifold = new ManifoldCtor(mesh); const status = manifold.status(); @@ -490,38 +492,38 @@ Module.setup = function () { return manifold; }; - Module.Manifold.ofMesh = function (mesh) { + Module.Manifold.ofMesh = function(mesh) { return new Module.Manifold(mesh); }; - Module.Manifold.tetrahedron = function () { + Module.Manifold.tetrahedron = function() { return Module._Tetrahedron(); }; - Module.Manifold.cube = function (...args) { + Module.Manifold.cube = function(...args) { let size = undefined; if (args.length == 0) - size = { x: 1, y: 1, z: 1 }; + size = {x: 1, y: 1, z: 1}; else if (typeof args[0] == 'number') - size = { x: args[0], y: args[0], z: args[0] }; + size = {x: args[0], y: args[0], z: args[0]}; else size = vararg2vec3(args); const center = args[1] || false; return Module._Cube(size, center); }; - Module.Manifold.cylinder = function ( - height, radiusLow, radiusHigh = -1.0, circularSegments = 0, - center = false) { + Module.Manifold.cylinder = function( + height, radiusLow, radiusHigh = -1.0, circularSegments = 0, + center = false) { return Module._Cylinder( - height, radiusLow, radiusHigh, circularSegments, center); + height, radiusLow, radiusHigh, circularSegments, center); }; - Module.Manifold.sphere = function (radius, circularSegments = 0) { + Module.Manifold.sphere = function(radius, circularSegments = 0) { return Module._Sphere(radius, circularSegments); }; - Module.Manifold.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); @@ -529,19 +531,23 @@ Module.setup = function () { return result; }; - Module.Manifold.extrude = function ( - polygons, height, nDivisions = 0, twistDegrees = 0.0, - scaleTop = [1.0, 1.0]) { - const cs = (polygons instanceof CrossSectionCtor) ? polygons : Module.CrossSection(polygons, "Positive"); + Module.Manifold.extrude = function( + polygons, height, nDivisions = 0, twistDegrees = 0.0, + scaleTop = [1.0, 1.0]) { + const cs = (polygons instanceof CrossSectionCtor) ? + polygons : + Module.CrossSection(polygons, 'Positive'); return cs.extrude(height, nDivisions, twistDegrees, scaleTop); }; - Module.Manifold.revolve = function (polygons, circularSegments = 0) { - const cs = (polygons instanceof CrossSectionCtor) ? polygons : Module.CrossSection(polygons, "Positive"); + Module.Manifold.revolve = function(polygons, circularSegments = 0) { + const cs = (polygons instanceof CrossSectionCtor) ? + polygons : + Module.CrossSection(polygons, 'Positive'); return cs.revolve(circularSegments); }; - Module.Manifold.compose = function (manifolds) { + Module.Manifold.compose = function(manifolds) { const vec = new Module.Vector_manifold(); toVec(vec, manifolds); const result = Module._manifoldCompose(vec); @@ -550,7 +556,7 @@ Module.setup = function () { }; function manifoldBatchbool(name) { - return function (...args) { + 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); @@ -564,12 +570,12 @@ Module.setup = function () { Module.Manifold.difference = manifoldBatchbool('Difference'); Module.Manifold.intersection = manifoldBatchbool('Intersection'); - Module.Manifold.levelSet = function (sdf, bounds, edgeLength, level = 0) { + 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] }, + 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]}, }; - const wasmFuncPtr = addFunction(function (vec3Ptr) { + const wasmFuncPtr = addFunction(function(vec3Ptr) { const x = getValue(vec3Ptr, 'float'); const y = getValue(vec3Ptr + 4, 'float'); const z = getValue(vec3Ptr + 8, 'float'); @@ -585,10 +591,10 @@ Module.setup = function () { // Top-level functions - Module.triangulate = function (polygons, precision = -1) { + Module.triangulate = function(polygons, precision = -1) { const polygonsVec = polygons2vec(polygons); const result = fromVec( - Module._Triangulate(polygonsVec, precision), (x) => [x[0], x[1], x[2]]); + Module._Triangulate(polygonsVec, precision), (x) => [x[0], x[1], x[2]]); disposePolygons(polygonsVec); return result; }; diff --git a/bindings/wasm/examples/public/examples.js b/bindings/wasm/examples/public/examples.js index f76beff29..a28ab143f 100644 --- a/bindings/wasm/examples/public/examples.js +++ b/bindings/wasm/examples/public/examples.js @@ -13,488 +13,492 @@ // limitations under the License. export const examples = { - functions: { - Intro: function () { - // Write code in JavaScript or TypeScript and this editor will show the - // API docs. Type e.g. "box." to see the Manifold API. Type "module." to - // see the static API - these functions can also be used bare. Use - // console.log() to print output (lower-right). This editor defines Z as - // up and units of mm. - - const box = Manifold.cube([100, 100, 100], true); - const ball = Manifold.sphere(60, 100); - // You must name your final output "result", or create at least one - // GLTFNode - see Menger Sponge and Gyroid Module examples. - const result = box.subtract(ball); - - // For visual debug, wrap any shape with show() and it and all of its - // copies will be shown in transparent red, akin to # in OpenSCAD. Or try - // only() to ghost out everything else, akin to * in OpenSCAD. - - // All changes are automatically saved and restored between sessions. - // This PWA is purely local - there is no server communication. - // This means it will work equally well offline once loaded. - // Consider installing it (icon in the search bar) for easy access. - - // See the script drop-down above ("Intro") for usage examples. The - // gl-matrix package from npm is automatically imported for convenience - - // its API is available in the top-level glMatrix object. - - // Use GLTFNode for disjoint manifolds rather than compose(), as this will - // keep them better organized in the GLB. This will also allow you to - // specify material properties, and even vertex colors via - // setProperties(). See Tetrahedron Puzzle example. - return result; - }, - - 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: - // https://www.thingiverse.com/thing:186372 - - const edgeLength = 50; // Length of each edge of the overall tetrahedron. - const gap = 0.2; // Spacing between the two halves to allow sliding. - const nDivisions = 50; // Divisions (both ways) in the screw surface. - - const scale = edgeLength / (2 * Math.sqrt(2)); - - const tet = Manifold.tetrahedron().scale(scale); - - const box = []; - box.push([1, -1], [1, 1]); - for (let i = 0; i <= nDivisions; ++i) { - box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]); - } - - const cyan = [0, 1, 1]; - const magenta = [1, 0, 1]; - const fade = (color, pos) => { - for (let i = 0; i < 3; ++i) { - color[i] = cyan[i] * pos[2] + magenta[i] * (1 - pos[2]); - } - }; - - // setProperties(3, fade) creates three channels of vertex properties - // according to the above fade function. setMaterial assigns these - // channels as colors, and sets the factor to white, since our default is - // yellow. - const screw = setMaterial( - Manifold.extrude(box, 1, nDivisions, 270).setProperties(3, fade), - { baseColorFactor: [1, 1, 1], attributes: ['COLOR_0'] }); - - const result = tet.intersect( - screw.rotate([0, 0, -45]).translate([0, 0, -0.5]).scale(2 * scale)); - - // Assigned materials are only applied to a GLTFNode. Note that material - // definitions cascade, applying recursively to all child surfaces, but - // overridden by any materials defined lower down. Our default material: - // { - // roughness = 0.2, - // metallic = 1, - // baseColorFactor = [1, 1, 0], - // alpha = 1, - // unlit = false, - // name = '' - // } - const node = new GLTFNode(); - node.manifold = result; - return result; - }, - - RoundedFrame: function () { - // Demonstrates how at 90-degree intersections, the sphere and cylinder - // facets match up perfectly, for any choice of global resolution - // parameters. - - function roundedFrame(edgeLength, radius, circularSegments = 0) { - const edge = Manifold.cylinder(edgeLength, radius, -1, circularSegments); - const corner = Manifold.sphere(radius, circularSegments); - - const edge1 = Manifold.union(corner, edge).rotate([-90, 0, 0]).translate([ - -edgeLength / 2, -edgeLength / 2, 0 - ]); - - const edge2 = Manifold.union( - Manifold.union(edge1, edge1.rotate([0, 0, 180])), - edge.translate([-edgeLength / 2, -edgeLength / 2, 0])); - - const edge4 = Manifold.union(edge2, edge2.rotate([0, 0, 90])).translate([ - 0, 0, -edgeLength / 2 - ]); - - return Manifold.union(edge4, edge4.rotate([180, 0, 0])); - } - - setMinCircularAngle(3); - setMinCircularEdgeLength(0.5); - const result = roundedFrame(100, 10); - return result; - }, - - Heart: function () { - // 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 - - const func = (v) => { - const x2 = v[0] * v[0]; - const y2 = v[1] * v[1]; - const z = v[2]; - const z2 = z * z; - const a = x2 + 9 / 4 * y2 + z2; - const b = z * z2 * (x2 + 9 / 80 * y2); - const a2 = a * a; - const a3 = a * a2; - - const step = (r) => { - const r2 = r * r; - const r4 = r2 * r2; - // Taubin's function: https://mathworld.wolfram.com/HeartSurface.html - const f = a3 * r4 * r2 - b * r4 * r - 3 * a2 * r4 + 3 * a * r2 - 1; - // Derivative - const df = - 6 * a3 * r4 * r - 5 * b * r4 - 12 * a2 * r2 * r + 6 * a * r; - return f / df; - }; - // Newton's method for root finding - let r = 1.5; - let dr = 1; - while (Math.abs(dr) > 0.0001) { - dr = step(r); - r -= dr; - } - // Update radius - v[0] *= r; - v[1] *= r; - v[2] *= r; - }; - - 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])); - return result; - }, - - Scallop: function () { - // 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 - - const height = 10; - const radius = 30; - const offset = 20; - const wiggles = 12; - const sharpness = 0.8; - const n = 50; - - const positions = []; - const triangles = []; - positions.push(-offset, 0, height, -offset, 0, -height); - const sharpenedEdges = []; - - const delta = 3.14159 / wiggles; - for (let i = 0; i < 2 * wiggles; ++i) { - const theta = (i - wiggles) * delta; - const amp = 0.5 * height * Math.max(Math.cos(0.8 * theta), 0); - - positions.push( - radius * Math.cos(theta), radius * Math.sin(theta), - amp * (i % 2 == 0 ? 1 : -1)); - let j = i + 1; - if (j == 2 * wiggles) j = 0; - - const smoothness = 1 - sharpness * Math.cos((theta + delta / 2) / 2); - let halfedge = triangles.length + 1; - sharpenedEdges.push({ halfedge, smoothness }); - triangles.push(0, 2 + i, 2 + j); - - halfedge = triangles.length + 1; - sharpenedEdges.push({ halfedge, smoothness }); - triangles.push(1, 2 + j, 2 + i); - } - - const triVerts = Uint32Array.from(triangles); - const vertProperties = Float32Array.from(positions); - const scallop = new Mesh({ numProp: 3, triVerts, vertProperties }); - const result = Manifold.smooth(scallop, sharpenedEdges).refine(n); - return result; - }, - - TorusKnot: function () { - // 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. - - // @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. - - function torusKnot( - p, q, majorRadius, minorRadius, threadRadius, circularSegments = 0, - linearSegments = 0) { - const { vec3 } = glMatrix; - - function gcd(a, b) { - return b == 0 ? a : gcd(b, a % b); - } - - const kLoops = gcd(p, q); - p /= kLoops; - q /= kLoops; - const n = circularSegments > 2 ? circularSegments : - getCircularSegments(threadRadius); - const m = linearSegments > 2 ? linearSegments : - n * q * majorRadius / threadRadius; - - const offset = 2. - const circle = CrossSection.circle(1., n).translate([offset, 0.]); - - const func = (v) => { - const psi = q * Math.atan2(v[0], v[1]); - const theta = psi * p / q; - const x1 = Math.sqrt(v[0] * v[0] + v[1] * v[1]); - const phi = Math.atan2(x1 - offset, v[2]); - vec3.set( - v, threadRadius * Math.cos(phi), 0, threadRadius * Math.sin(phi)); - const center = vec3.fromValues(0, 0, 0); - const r = majorRadius + minorRadius * Math.cos(theta); - vec3.rotateX(v, v, center, -Math.atan2(p * minorRadius, q * r)); - v[0] += minorRadius; - vec3.rotateY(v, v, center, theta); - v[0] += majorRadius; - vec3.rotateZ(v, v, center, psi); - }; - - let knot = Manifold.revolve(circle, m).warp(func); - - if (kLoops > 1) { - const knots = []; - for (let k = 0; k < kLoops; ++k) { - knots.push(knot.rotate([0, 0, 360 * (k / kLoops) * (q / p)])); - } - knot = Manifold.compose(knots); - } - - return knot; - } - - // This recreates Matlab Knot by Emmett Lalish: - // https://www.thingiverse.com/thing:7080 - - const result = torusKnot(1, 3, 25, 10, 3.75); - return result; - }, - - MengerSponge: function () { - // This example demonstrates how symbolic perturbation correctly creates - // holes even though the subtracted objects are exactly coplanar. - const { vec2 } = glMatrix; - - function fractal(holes, hole, w, position, depth, maxDepth) { - w /= 3; - holes.push( - hole.scale([w, w, 1.0]).translate([position[0], position[1], 0.0])); - if (depth == maxDepth) return; - const offsets = [ - vec2.fromValues(-w, -w), vec2.fromValues(-w, 0.0), - vec2.fromValues(-w, w), vec2.fromValues(0.0, w), - vec2.fromValues(w, w), vec2.fromValues(w, 0.0), - vec2.fromValues(w, -w), vec2.fromValues(0.0, -w) - ]; - for (let offset of offsets) - fractal( - holes, hole, w, vec2.add(offset, position, offset), depth + 1, - maxDepth); - } - - function mengerSponge(n) { - let result = Manifold.cube([1, 1, 1], true); - const holes = []; - fractal(holes, result, 1.0, [0.0, 0.0], 1, n); - - const hole = Manifold.compose(holes); - - result = Manifold.difference(result, hole); - result = Manifold.difference(result, hole.rotate([90, 0, 0])); - result = Manifold.difference(result, hole.rotate([0, 90, 0])); - - return result; - } - - const posColors = (newProp, pos) => { - for (let i = 0; i < 3; ++i) { - newProp[i] = (1 - pos[i]) / 2; - } - }; - - const result = mengerSponge(3) - .trimByPlane([1, 1, 1], 0) - .setProperties(3, posColors) - .scale(100); - - const node = new GLTFNode(); - node.manifold = result; - node.material = { baseColorFactor: [1, 1, 1], attributes: ['COLOR_0'] }; - return result; - }, - - StretchyBracelet: function () { - // Recreates Stretchy Bracelet by Emmett Lalish: - // https://www.thingiverse.com/thing:13505 - const { vec2 } = glMatrix; - - function base( - width, radius, decorRadius, twistRadius, nDecor, innerRadius, - outerRadius, cut, nCut, nDivision) { - let b = Manifold.cylinder(width, radius + twistRadius / 2); - const circle = []; - const dPhiDeg = 180 / nDivision; - for (let i = 0; i < 2 * nDivision; ++i) { - circle.push([ - decorRadius * Math.cos(dPhiDeg * i * Math.PI / 180) + twistRadius, - decorRadius * Math.sin(dPhiDeg * i * Math.PI / 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++) - b = b.add(decor.rotate([0, 0, (360.0 / nDecor) * i])); - const stretch = []; - const dPhiRad = 2 * Math.PI / nCut; - - const o = vec2.fromValues(0, 0); - const p0 = vec2.fromValues(outerRadius, 0); - const p1 = vec2.fromValues(innerRadius, -cut); - const p2 = vec2.fromValues(innerRadius, cut); - for (let i = 0; i < nCut; ++i) { - stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i)); - stretch.push(vec2.rotate([0, 0], p1, o, dPhiRad * i)); - stretch.push(vec2.rotate([0, 0], p2, o, dPhiRad * i)); - stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i)); - } - b = Manifold.intersection(Manifold.extrude(stretch, width), b); - return b; - } - - function stretchyBracelet( - radius = 30, height = 8, width = 15, thickness = 0.4, nDecor = 20, - nCut = 27, nDivision = 30) { - const twistRadius = Math.PI * radius / nDecor; - const decorRadius = twistRadius * 1.5; - const outerRadius = radius + (decorRadius + twistRadius) * 0.5; - const innerRadius = outerRadius - height; - const cut = 0.5 * (Math.PI * 2 * innerRadius / nCut - thickness); - const adjThickness = 0.5 * thickness * height / cut; - - return Manifold.difference( - base( - width, radius, decorRadius, twistRadius, nDecor, - innerRadius + thickness, outerRadius + adjThickness, - cut - adjThickness, nCut, nDivision), - base( - width, radius - thickness, decorRadius, twistRadius, nDecor, - innerRadius, outerRadius + 3 * adjThickness, cut, nCut, - nDivision)); - } - - const result = stretchyBracelet(); - return result; - }, - - GyroidModule: function () { - // Recreates Modular Gyroid Puzzle by Emmett Lalish: - // https://www.thingiverse.com/thing:25477. This sample demonstrates the - // use of a Signed Distance Function (SDF) to create smooth, complex - // manifolds. - const { vec3 } = glMatrix; - - // number of modules along pyramid edge (use 1 for print orientation) - const m = 4; - // module size - const size = 20; - // SDF resolution - const n = 20; - - const pi = 3.14159; - - function gyroid(p) { - const x = p[0] - pi / 4; - const y = p[1] - pi / 4; - const z = p[2] - pi / 4; - return Math.cos(x) * Math.sin(y) + Math.cos(y) * Math.sin(z) + - Math.cos(z) * Math.sin(x); - } - - function gyroidOffset(level) { - const period = 2 * pi; - const box = { - min: vec3.fromValues(-period, -period, -period), - max: vec3.fromValues(period, period, period) - }; - return Manifold.levelSet(gyroid, box, period / n, level).scale(size / period); - }; - - function rhombicDodecahedron() { - const box = Manifold.cube([1, 1, 2], true).scale(size * Math.sqrt(2)); - const result = - box.rotate([90, 45, 0]).intersect(box.rotate([90, 45, 90])); - return result.intersect(box.rotate([0, 0, 45])); - } - - const gyroidModule = rhombicDodecahedron() - .intersect(gyroidOffset(-0.4)) - .subtract(gyroidOffset(0.4)); - - if (m > 1) { - for (let i = 0; i < m; ++i) { - for (let j = i; j < m; ++j) { - for (let k = j; k < m; ++k) { - const node = new GLTFNode(); - node.manifold = gyroidModule; - node.translation = - [(k + i - j) * size, (k - i) * size, (-j) * size]; - node.material = { - baseColorFactor: - [(k + i - j + 1) / m, (k - i + 1) / m, (j + 1) / m] - }; - } - } - } - } - - const result = gyroidModule.rotate([-45, 0, 90]).translate([ - 0, 0, size / Math.sqrt(2) - ]); - return result; + functions: { + Intro: function() { + // Write code in JavaScript or TypeScript and this editor will show the + // API docs. Type e.g. "box." to see the Manifold API. Type "module." to + // see the static API - these functions can also be used bare. Use + // console.log() to print output (lower-right). This editor defines Z as + // up and units of mm. + + const box = Manifold.cube([100, 100, 100], true); + const ball = Manifold.sphere(60, 100); + // You must name your final output "result", or create at least one + // GLTFNode - see Menger Sponge and Gyroid Module examples. + const result = box.subtract(ball); + + // For visual debug, wrap any shape with show() and it and all of its + // copies will be shown in transparent red, akin to # in OpenSCAD. Or try + // only() to ghost out everything else, akin to * in OpenSCAD. + + // All changes are automatically saved and restored between sessions. + // This PWA is purely local - there is no server communication. + // This means it will work equally well offline once loaded. + // Consider installing it (icon in the search bar) for easy access. + + // See the script drop-down above ("Intro") for usage examples. The + // gl-matrix package from npm is automatically imported for convenience - + // its API is available in the top-level glMatrix object. + + // Use GLTFNode for disjoint manifolds rather than compose(), as this will + // keep them better organized in the GLB. This will also allow you to + // specify material properties, and even vertex colors via + // setProperties(). See Tetrahedron Puzzle example. + return result; + }, + + 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: + // https://www.thingiverse.com/thing:186372 + + const edgeLength = 50; // Length of each edge of the overall tetrahedron. + const gap = 0.2; // Spacing between the two halves to allow sliding. + const nDivisions = 50; // Divisions (both ways) in the screw surface. + + const scale = edgeLength / (2 * Math.sqrt(2)); + + const tet = Manifold.tetrahedron().scale(scale); + + const box = []; + box.push([1, -1], [1, 1]); + for (let i = 0; i <= nDivisions; ++i) { + box.push([gap / (4 * scale), 1 - i * 2 / nDivisions]); + } + + const cyan = [0, 1, 1]; + const magenta = [1, 0, 1]; + const fade = (color, pos) => { + for (let i = 0; i < 3; ++i) { + color[i] = cyan[i] * pos[2] + magenta[i] * (1 - pos[2]); + } + }; + + // setProperties(3, fade) creates three channels of vertex properties + // according to the above fade function. setMaterial assigns these + // channels as colors, and sets the factor to white, since our default is + // yellow. + const screw = setMaterial( + Manifold.extrude(box, 1, nDivisions, 270).setProperties(3, fade), + {baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']}); + + const result = tet.intersect( + screw.rotate([0, 0, -45]).translate([0, 0, -0.5]).scale(2 * scale)); + + // Assigned materials are only applied to a GLTFNode. Note that material + // definitions cascade, applying recursively to all child surfaces, but + // overridden by any materials defined lower down. Our default material: + // { + // roughness = 0.2, + // metallic = 1, + // baseColorFactor = [1, 1, 0], + // alpha = 1, + // unlit = false, + // name = '' + // } + const node = new GLTFNode(); + node.manifold = result; + return result; + }, + + RoundedFrame: function() { + // Demonstrates how at 90-degree intersections, the sphere and cylinder + // facets match up perfectly, for any choice of global resolution + // parameters. + + function roundedFrame(edgeLength, radius, circularSegments = 0) { + const edge = + Manifold.cylinder(edgeLength, radius, -1, circularSegments); + const corner = Manifold.sphere(radius, circularSegments); + + const edge1 = + Manifold.union(corner, edge).rotate([-90, 0, 0]).translate([ + -edgeLength / 2, -edgeLength / 2, 0 + ]); + + const edge2 = Manifold.union( + Manifold.union(edge1, edge1.rotate([0, 0, 180])), + edge.translate([-edgeLength / 2, -edgeLength / 2, 0])); + + const edge4 = + Manifold.union(edge2, edge2.rotate([0, 0, 90])).translate([ + 0, 0, -edgeLength / 2 + ]); + + return Manifold.union(edge4, edge4.rotate([180, 0, 0])); + } + + setMinCircularAngle(3); + setMinCircularEdgeLength(0.5); + const result = roundedFrame(100, 10); + return result; + }, + + Heart: function() { + // 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 + + const func = (v) => { + const x2 = v[0] * v[0]; + const y2 = v[1] * v[1]; + const z = v[2]; + const z2 = z * z; + const a = x2 + 9 / 4 * y2 + z2; + const b = z * z2 * (x2 + 9 / 80 * y2); + const a2 = a * a; + const a3 = a * a2; + + const step = (r) => { + const r2 = r * r; + const r4 = r2 * r2; + // Taubin's function: https://mathworld.wolfram.com/HeartSurface.html + const f = a3 * r4 * r2 - b * r4 * r - 3 * a2 * r4 + 3 * a * r2 - 1; + // Derivative + const df = + 6 * a3 * r4 * r - 5 * b * r4 - 12 * a2 * r2 * r + 6 * a * r; + return f / df; + }; + // Newton's method for root finding + let r = 1.5; + let dr = 1; + while (Math.abs(dr) > 0.0001) { + dr = step(r); + r -= dr; + } + // Update radius + v[0] *= r; + v[1] *= r; + v[2] *= r; + }; + + 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])); + return result; + }, + + Scallop: function() { + // 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 + + const height = 10; + const radius = 30; + const offset = 20; + const wiggles = 12; + const sharpness = 0.8; + const n = 50; + + const positions = []; + const triangles = []; + positions.push(-offset, 0, height, -offset, 0, -height); + const sharpenedEdges = []; + + const delta = 3.14159 / wiggles; + for (let i = 0; i < 2 * wiggles; ++i) { + const theta = (i - wiggles) * delta; + const amp = 0.5 * height * Math.max(Math.cos(0.8 * theta), 0); + + positions.push( + radius * Math.cos(theta), radius * Math.sin(theta), + amp * (i % 2 == 0 ? 1 : -1)); + let j = i + 1; + if (j == 2 * wiggles) j = 0; + + const smoothness = 1 - sharpness * Math.cos((theta + delta / 2) / 2); + let halfedge = triangles.length + 1; + sharpenedEdges.push({halfedge, smoothness}); + triangles.push(0, 2 + i, 2 + j); + + halfedge = triangles.length + 1; + sharpenedEdges.push({halfedge, smoothness}); + triangles.push(1, 2 + j, 2 + i); + } + + const triVerts = Uint32Array.from(triangles); + const vertProperties = Float32Array.from(positions); + const scallop = new Mesh({numProp: 3, triVerts, vertProperties}); + const result = Manifold.smooth(scallop, sharpenedEdges).refine(n); + return result; + }, + + TorusKnot: function() { + // 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. + + // @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. + + function torusKnot( + p, q, majorRadius, minorRadius, threadRadius, circularSegments = 0, + linearSegments = 0) { + const {vec3} = glMatrix; + + function gcd(a, b) { + return b == 0 ? a : gcd(b, a % b); + } + + const kLoops = gcd(p, q); + p /= kLoops; + q /= kLoops; + const n = circularSegments > 2 ? circularSegments : + getCircularSegments(threadRadius); + const m = linearSegments > 2 ? linearSegments : + n * q * majorRadius / threadRadius; + + const offset = 2. + const circle = CrossSection.circle(1., n).translate([offset, 0.]); + + const func = (v) => { + const psi = q * Math.atan2(v[0], v[1]); + const theta = psi * p / q; + const x1 = Math.sqrt(v[0] * v[0] + v[1] * v[1]); + const phi = Math.atan2(x1 - offset, v[2]); + vec3.set( + v, threadRadius * Math.cos(phi), 0, threadRadius * Math.sin(phi)); + const center = vec3.fromValues(0, 0, 0); + const r = majorRadius + minorRadius * Math.cos(theta); + vec3.rotateX(v, v, center, -Math.atan2(p * minorRadius, q * r)); + v[0] += minorRadius; + vec3.rotateY(v, v, center, theta); + v[0] += majorRadius; + vec3.rotateZ(v, v, center, psi); + }; + + let knot = Manifold.revolve(circle, m).warp(func); + + if (kLoops > 1) { + const knots = []; + for (let k = 0; k < kLoops; ++k) { + knots.push(knot.rotate([0, 0, 360 * (k / kLoops) * (q / p)])); + } + knot = Manifold.compose(knots); + } + + return knot; + } + + // This recreates Matlab Knot by Emmett Lalish: + // https://www.thingiverse.com/thing:7080 + + const result = torusKnot(1, 3, 25, 10, 3.75); + return result; + }, + + MengerSponge: function() { + // This example demonstrates how symbolic perturbation correctly creates + // holes even though the subtracted objects are exactly coplanar. + const {vec2} = glMatrix; + + function fractal(holes, hole, w, position, depth, maxDepth) { + w /= 3; + holes.push( + hole.scale([w, w, 1.0]).translate([position[0], position[1], 0.0])); + if (depth == maxDepth) return; + const offsets = [ + vec2.fromValues(-w, -w), vec2.fromValues(-w, 0.0), + vec2.fromValues(-w, w), vec2.fromValues(0.0, w), + vec2.fromValues(w, w), vec2.fromValues(w, 0.0), + vec2.fromValues(w, -w), vec2.fromValues(0.0, -w) + ]; + for (let offset of offsets) + fractal( + holes, hole, w, vec2.add(offset, position, offset), depth + 1, + maxDepth); + } + + function mengerSponge(n) { + let result = Manifold.cube([1, 1, 1], true); + const holes = []; + fractal(holes, result, 1.0, [0.0, 0.0], 1, n); + + const hole = Manifold.compose(holes); + + result = Manifold.difference(result, hole); + result = Manifold.difference(result, hole.rotate([90, 0, 0])); + result = Manifold.difference(result, hole.rotate([0, 90, 0])); + + return result; + } + + const posColors = (newProp, pos) => { + for (let i = 0; i < 3; ++i) { + newProp[i] = (1 - pos[i]) / 2; + } + }; + + const result = mengerSponge(3) + .trimByPlane([1, 1, 1], 0) + .setProperties(3, posColors) + .scale(100); + + const node = new GLTFNode(); + node.manifold = result; + node.material = {baseColorFactor: [1, 1, 1], attributes: ['COLOR_0']}; + return result; + }, + + StretchyBracelet: function() { + // Recreates Stretchy Bracelet by Emmett Lalish: + // https://www.thingiverse.com/thing:13505 + const {vec2} = glMatrix; + + function base( + width, radius, decorRadius, twistRadius, nDecor, innerRadius, + outerRadius, cut, nCut, nDivision) { + let b = Manifold.cylinder(width, radius + twistRadius / 2); + const circle = []; + const dPhiDeg = 180 / nDivision; + for (let i = 0; i < 2 * nDivision; ++i) { + circle.push([ + decorRadius * Math.cos(dPhiDeg * i * Math.PI / 180) + twistRadius, + decorRadius * Math.sin(dPhiDeg * i * Math.PI / 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++) + b = b.add(decor.rotate([0, 0, (360.0 / nDecor) * i])); + const stretch = []; + const dPhiRad = 2 * Math.PI / nCut; + + const o = vec2.fromValues(0, 0); + const p0 = vec2.fromValues(outerRadius, 0); + const p1 = vec2.fromValues(innerRadius, -cut); + const p2 = vec2.fromValues(innerRadius, cut); + for (let i = 0; i < nCut; ++i) { + stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i)); + stretch.push(vec2.rotate([0, 0], p1, o, dPhiRad * i)); + stretch.push(vec2.rotate([0, 0], p2, o, dPhiRad * i)); + stretch.push(vec2.rotate([0, 0], p0, o, dPhiRad * i)); + } + b = Manifold.intersection(Manifold.extrude(stretch, width), b); + return b; + } + + function stretchyBracelet( + radius = 30, height = 8, width = 15, thickness = 0.4, nDecor = 20, + nCut = 27, nDivision = 30) { + const twistRadius = Math.PI * radius / nDecor; + const decorRadius = twistRadius * 1.5; + const outerRadius = radius + (decorRadius + twistRadius) * 0.5; + const innerRadius = outerRadius - height; + const cut = 0.5 * (Math.PI * 2 * innerRadius / nCut - thickness); + const adjThickness = 0.5 * thickness * height / cut; + + return Manifold.difference( + base( + width, radius, decorRadius, twistRadius, nDecor, + innerRadius + thickness, outerRadius + adjThickness, + cut - adjThickness, nCut, nDivision), + base( + width, radius - thickness, decorRadius, twistRadius, nDecor, + innerRadius, outerRadius + 3 * adjThickness, cut, nCut, + nDivision)); + } + + const result = stretchyBracelet(); + return result; + }, + + GyroidModule: function() { + // Recreates Modular Gyroid Puzzle by Emmett Lalish: + // https://www.thingiverse.com/thing:25477. This sample demonstrates the + // use of a Signed Distance Function (SDF) to create smooth, complex + // manifolds. + const {vec3} = glMatrix; + + // number of modules along pyramid edge (use 1 for print orientation) + const m = 4; + // module size + const size = 20; + // SDF resolution + const n = 20; + + const pi = 3.14159; + + function gyroid(p) { + const x = p[0] - pi / 4; + const y = p[1] - pi / 4; + const z = p[2] - pi / 4; + return Math.cos(x) * Math.sin(y) + Math.cos(y) * Math.sin(z) + + Math.cos(z) * Math.sin(x); + } + + function gyroidOffset(level) { + const period = 2 * pi; + const box = { + min: vec3.fromValues(-period, -period, -period), + max: vec3.fromValues(period, period, period) + }; + return Manifold.levelSet(gyroid, box, period / n, level) + .scale(size / period); + }; + + function rhombicDodecahedron() { + const box = Manifold.cube([1, 1, 2], true).scale(size * Math.sqrt(2)); + const result = + box.rotate([90, 45, 0]).intersect(box.rotate([90, 45, 90])); + return result.intersect(box.rotate([0, 0, 45])); + } + + const gyroidModule = rhombicDodecahedron() + .intersect(gyroidOffset(-0.4)) + .subtract(gyroidOffset(0.4)); + + if (m > 1) { + for (let i = 0; i < m; ++i) { + for (let j = i; j < m; ++j) { + for (let k = j; k < m; ++k) { + const node = new GLTFNode(); + node.manifold = gyroidModule; + node.translation = + [(k + i - j) * size, (k - i) * size, (-j) * size]; + node.material = { + baseColorFactor: + [(k + i - j + 1) / m, (k - i + 1) / m, (j + 1) / m] + }; } - }, - - functionBodies: new Map() + } + } + } + + const result = gyroidModule.rotate([-45, 0, 90]).translate([ + 0, 0, size / Math.sqrt(2) + ]); + return result; + } + }, + + functionBodies: new Map() }; for (const [func, code] of Object.entries(examples.functions)) { - const whole = code.toString(); - const lines = whole.split('\n'); - lines.splice(0, 1); // remove first line - lines.splice(-2, 2); // remove last two lines - // remove first six leading spaces - const body = '\n' + lines.map(l => l.slice(6)).join('\n'); - - const name = - func.replace(/([a-z])([A-Z])/g, '$1 $2'); // Add spaces between words - examples.functionBodies.set(name, body); + const whole = code.toString(); + const lines = whole.split('\n'); + lines.splice(0, 1); // remove first line + lines.splice(-2, 2); // remove last two lines + // remove first six leading spaces + const body = '\n' + lines.map(l => l.slice(6)).join('\n'); + + const name = + func.replace(/([a-z])([A-Z])/g, '$1 $2'); // Add spaces between words + examples.functionBodies.set(name, body); }; diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 97ddba214..4be59ef66 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Document, Material, Node, WebIO } from '@gltf-transform/core'; -import { KHRMaterialsUnlit, KHRONOS_EXTENSIONS } from '@gltf-transform/extensions'; +import {Document, Material, Node, WebIO} from '@gltf-transform/core'; +import {KHRMaterialsUnlit, KHRONOS_EXTENSIONS} from '@gltf-transform/extensions'; import * as glMatrix from 'gl-matrix'; import Module from './built/manifold'; //@ts-ignore -import { setupIO, writeMesh } from './gltf-io'; -import type { GLTFMaterial, Quat } from './public/editor'; -import type { CrossSection, Manifold, ManifoldStatic, Mesh, Vec3 } from './public/manifold'; +import {setupIO, writeMesh} from './gltf-io'; +import type {GLTFMaterial, Quat} from './public/editor'; +import type {CrossSection, Manifold, ManifoldStatic, Mesh, Vec3} from './public/manifold'; interface WorkerStatic extends ManifoldStatic { GLTFNode: typeof GLTFNode; @@ -30,7 +30,6 @@ interface WorkerStatic extends ManifoldStatic { cleanup(): void; } -//@ts-ignore const module = await Module() as unknown as WorkerStatic; module.setup(); @@ -50,6 +49,7 @@ const SHOW = { alpha: 0.25, roughness: 1, metallic: 0 + } as GLTFMaterial; const GHOST = { @@ -101,7 +101,7 @@ class GLTFNode { nodes.push(this); } clone(parent?: GLTFNode) { - const copy = { ...this }; + const copy = {...this}; copy._parent = parent; nodes.push(copy); return copy; @@ -121,38 +121,30 @@ module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => { // manifold static methods const manifoldStaticFunctions = [ - 'cube', 'cylinder', 'sphere', 'tetrahedron', - 'extrude', 'revolve', - 'compose', 'union', 'difference', 'intersection', - 'levelSet', 'smooth', 'ofMesh' + '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', + 'add', 'subtract', 'intersect', 'decompose', 'warp', 'transform', 'translate', + 'rotate', 'scale', 'mirror', 'refine', 'setProperties', 'asOriginal', 'trimByPlane', 'split', 'splitByPlane' ]; // CrossSection static methods const crossSectionStaticFunctions = [ - 'square', 'circle', - 'union', 'difference', 'intersection', 'compose', + 'square', 'circle', 'union', 'difference', 'intersection', 'compose', 'ofPolygons' ]; // CrossSection member functions that returns a new manifold const crossSectionMemberFunctions = [ - 'add', 'subtract', 'intersect', 'rectClip', 'decompose', - 'transform', 'translate', 'rotate', 'scale', 'mirror', - 'simplify', 'offset', - 'toPolygons' + 'add', 'subtract', 'intersect', 'rectClip', 'decompose', 'transform', + 'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset', 'toPolygons' ]; // top level functions that construct a new manifolds/meshes -const toplevelConstructors = [ - 'show', 'only', 'setMaterial' -]; +const toplevelConstructors = ['show', 'only', 'setMaterial']; const toplevel = [ - 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', 'getCircularSegments', - 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection' + 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', + 'getCircularSegments', 'Mesh', 'GLTFNode', 'Manifold', 'CrossSection' ]; const exposedFunctions = toplevelConstructors.concat(toplevel); @@ -161,9 +153,10 @@ const exposedFunctions = toplevelConstructors.concat(toplevel); // Note that this only fixes memory leak across different runs: the memory // will only be freed when the compilation finishes. -const memoryRegistry = new Array(); +const memoryRegistry = new Array(); -function addMembers(className: string, methodNames: Array, areStatic: boolean) { +function addMembers( + className: string, methodNames: Array, areStatic: boolean) { //@ts-ignore const cls = module[className]; const obj = areStatic ? cls : cls.prototype; @@ -173,7 +166,7 @@ function addMembers(className: string, methodNames: Array, areStatic: bo //@ts-ignore obj['_' + name] = originalFn; //@ts-ignore - obj[name] = function (...args: any) { + obj[name] = function(...args: any) { //@ts-ignore const result = this['_' + name](...args); memoryRegistry.push(result); @@ -191,14 +184,14 @@ for (const name of toplevelConstructors) { //@ts-ignore const originalFn = module[name]; //@ts-ignore - module[name] = function (...args: any) { + module[name] = function(...args: any) { const result = originalFn(...args); memoryRegistry.push(result); return result; }; } -module.cleanup = function () { +module.cleanup = function() { for (const obj of memoryRegistry) { // decompose result is an array of manifolds if (obj instanceof Array) @@ -214,7 +207,7 @@ self.postMessage(null); if (self.console) { const oldLog = self.console.log; - self.console.log = function (...args) { + self.console.log = function(...args) { let message = ''; for (const arg of args) { if (arg == null) { @@ -225,7 +218,7 @@ if (self.console) { message += arg.toString(); } } - self.postMessage({ log: message }); + self.postMessage({log: message}); oldLog(...args); }; } @@ -239,16 +232,16 @@ function log(...args: any[]) { self.onmessage = async (e) => { const content = e.data + - '\nreturn exportGLB(typeof result === "undefined" ? undefined : result);\n'; + '\nreturn exportGLB(typeof result === "undefined" ? undefined : result);\n'; try { const f = new Function( - 'exportGLB', 'glMatrix', 'module', ...exposedFunctions, content); + 'exportGLB', 'glMatrix', 'module', ...exposedFunctions, content); await f( - exportGLB, glMatrix, module, //@ts-ignore - ...exposedFunctions.map(name => module[name])); + exportGLB, glMatrix, module, //@ts-ignore + ...exposedFunctions.map(name => module[name])); } catch (error: any) { console.log(error.toString()); - self.postMessage({ objectURL: null }); + self.postMessage({objectURL: null}); } finally { module.cleanup(); cleanup(); @@ -261,7 +254,7 @@ function createGLTFnode(doc: Document, node: GLTFNode) { out.setTranslation(node.translation); } if (node.rotation) { - const { quat } = glMatrix; + const {quat} = glMatrix; const deg2rad = Math.PI / 180; const q = quat.create() as Quat; quat.rotateX(q, q, deg2rad * node.rotation[0]); @@ -305,8 +298,8 @@ function makeDefaultedMaterial(doc: Document, { } return material.setRoughnessFactor(roughness) - .setMetallicFactor(metallic) - .setBaseColorFactor([...baseColorFactor, alpha]); + .setMetallicFactor(metallic) + .setBaseColorFactor([...baseColorFactor, alpha]); } function getCachedMaterial(doc: Document, matDef: GLTFMaterial): Material { @@ -317,8 +310,8 @@ function getCachedMaterial(doc: Document, matDef: GLTFMaterial): Material { } function addMesh( - doc: Document, node: Node, manifold: Manifold, - backupMaterial: GLTFMaterial = {}) { + doc: Document, node: Node, manifold: Manifold, + backupMaterial: GLTFMaterial = {}) { const numTri = manifold.numTri(); if (numTri == 0) { log('Empty manifold, skipping.'); @@ -331,9 +324,11 @@ function addMesh( for (let i = 0; i < 3; i++) { size[i] = Math.round((box.max[i] - box.min[i]) * 10) / 10; } - log(`Bounding Box: X = ${size[0].toLocaleString()} mm, Y = ${size[1].toLocaleString()} mm, Z = ${size[2].toLocaleString()} mm`); + log(`Bounding Box: X = ${size[0].toLocaleString()} mm, Y = ${ + size[1].toLocaleString()} mm, Z = ${size[2].toLocaleString()} mm`); const volume = Math.round(manifold.getProperties().volume / 10); - log(`Genus: ${manifold.genus().toLocaleString()}, Volume: ${(volume / 100).toLocaleString()} cm^3`); + log(`Genus: ${manifold.genus().toLocaleString()}, Volume: ${ + (volume / 100).toLocaleString()} cm^3`); // From Z-up to Y-up (glTF) const manifoldMesh = manifold.getMesh(); @@ -364,10 +359,10 @@ function addMesh( } const mat = single ? materials[run] : SHOW; const debugNode = - doc.createNode('debug') - .setMesh(writeMesh( - doc, inMesh, attributes, [getCachedMaterial(doc, mat)])) - .setMatrix(manifoldMesh.transform(run)); + doc.createNode('debug') + .setMesh(writeMesh( + doc, inMesh, attributes, [getCachedMaterial(doc, mat)])) + .setMatrix(manifoldMesh.transform(run)); node.addChild(debugNode); } } @@ -381,8 +376,8 @@ function cloneNode(toNode: Node, fromNode: Node) { } function cloneNodeNewMaterial( - doc: Document, toNode: Node, fromNode: Node, backupMaterial: Material, - oldBackupMaterial: Material) { + doc: Document, toNode: Node, fromNode: Node, backupMaterial: Material, + oldBackupMaterial: Material) { cloneNode(toNode, fromNode); const mesh = doc.createMesh(); toNode.setMesh(mesh); @@ -396,8 +391,8 @@ function cloneNodeNewMaterial( } function createNodeFromCache( - doc: Document, nodeDef: GLTFNode, - manifold2node: Map>): Node { + doc: Document, nodeDef: GLTFNode, + manifold2node: Map>): Node { const node = createGLTFnode(doc, nodeDef); if (nodeDef.manifold != null) { const backupMaterial = getBackupMaterial(nodeDef); @@ -412,8 +407,8 @@ function createNodeFromCache( if (cachedNode == null) { const [oldBackupMaterial, oldNode] = cachedNodes.entries().next().value; cloneNodeNewMaterial( - doc, node, oldNode, getCachedMaterial(doc, backupMaterial), - getCachedMaterial(doc, oldBackupMaterial)); + doc, node, oldNode, getCachedMaterial(doc, backupMaterial), + getCachedMaterial(doc, oldBackupMaterial)); cachedNodes.set(backupMaterial, node); } else { cloneNode(node, cachedNode); @@ -428,8 +423,8 @@ async function exportGLB(manifold?: Manifold) { const halfRoot2 = Math.sqrt(2) / 2; const mm2m = 1 / 1000; const wrapper = doc.createNode('wrapper') - .setRotation([-halfRoot2, 0, 0, halfRoot2]) - .setScale([mm2m, mm2m, mm2m]); + .setRotation([-halfRoot2, 0, 0, halfRoot2]) + .setScale([mm2m, mm2m, mm2m]); doc.createScene().addChild(wrapper); if (nodes.length > 0) { @@ -454,7 +449,7 @@ async function exportGLB(manifold?: Manifold) { } log('Total glTF nodes: ', nodes.length, - ', Total mesh references: ', leafNodes); + ', Total mesh references: ', leafNodes); } else { if (manifold == null) { log('No output because "result" is undefined and no "GLTFNode"s were created.'); @@ -467,6 +462,6 @@ async function exportGLB(manifold?: Manifold) { const glb = await io.writeBinary(doc); - const blob = new Blob([glb], { type: 'application/octet-stream' }); - self.postMessage({ objectURL: URL.createObjectURL(blob) }); + const blob = new Blob([glb], {type: 'application/octet-stream'}); + self.postMessage({objectURL: URL.createObjectURL(blob)}); } diff --git a/bindings/wasm/manifold.d.ts b/bindings/wasm/manifold.d.ts index 6aa2b750d..8b86d67a0 100644 --- a/bindings/wasm/manifold.d.ts +++ b/bindings/wasm/manifold.d.ts @@ -44,4 +44,5 @@ export interface ManifoldStatic { setup: () => void; } -export default function Module(config: { locateFile: () => string }): Promise; +export default function Module(config?: {locateFile: () => string}): + Promise; From 7c255831e35d9c4711cfe746c6ed513f1dc4bb4a Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 00:52:57 -0700 Subject: [PATCH 05/41] Move debug setup below memory registry indirection --- bindings/wasm/examples/worker.ts | 160 +++++++++++++++---------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 4be59ef66..b8e191177 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -39,86 +39,6 @@ glMatrix.glMatrix.setMatrixArrayType(Array); const io = setupIO(new WebIO()); io.registerExtensions(KHRONOS_EXTENSIONS); -// Debug setup to show source meshes -let ghost = false; -const shown = new Map(); -const singles = new Map(); - -const SHOW = { - baseColorFactor: [1, 0, 0], - alpha: 0.25, - roughness: 1, - metallic: 0 - -} as GLTFMaterial; - -const GHOST = { - baseColorFactor: [0.5, 0.5, 0.5], - alpha: 0.25, - roughness: 1, - metallic: 0 -} as GLTFMaterial; - -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(); - -function cleanup() { - ghost = false; - shown.clear(); - singles.clear(); - nodes.length = 0; - id2material.clear(); - materialCache.clear(); -} - -class GLTFNode { - private _parent?: GLTFNode; - manifold?: Manifold; - translation?: Vec3; - rotation?: Vec3; - scale?: Vec3; - material?: GLTFMaterial; - name?: string; - - constructor(parent?: GLTFNode) { - this._parent = parent; - nodes.push(this); - } - clone(parent?: GLTFNode) { - const copy = {...this}; - copy._parent = parent; - nodes.push(copy); - return copy; - } - get parent() { - return this._parent; - } -} - -module.GLTFNode = GLTFNode; - -module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => { - const out = manifold.asOriginal(); - id2material.set(out.originalID(), material); - return out; -}; - // manifold static methods const manifoldStaticFunctions = [ 'cube', 'cylinder', 'sphere', 'tetrahedron', 'extrude', 'revolve', 'compose', @@ -202,6 +122,86 @@ module.cleanup = function() { memoryRegistry.length = 0; }; +// Debug setup to show source meshes +let ghost = false; +const shown = new Map(); +const singles = new Map(); + +const SHOW = { + baseColorFactor: [1, 0, 0], + alpha: 0.25, + roughness: 1, + metallic: 0 + +} as GLTFMaterial; + +const GHOST = { + baseColorFactor: [0.5, 0.5, 0.5], + alpha: 0.25, + roughness: 1, + metallic: 0 +} as GLTFMaterial; + +const nodes = new Array(); +const id2material = new Map(); +const materialCache = new Map(); + +function cleanup() { + ghost = false; + shown.clear(); + singles.clear(); + nodes.length = 0; + id2material.clear(); + materialCache.clear(); +} + +class GLTFNode { + private _parent?: GLTFNode; + manifold?: Manifold; + translation?: Vec3; + rotation?: Vec3; + scale?: Vec3; + material?: GLTFMaterial; + name?: string; + + constructor(parent?: GLTFNode) { + this._parent = parent; + nodes.push(this); + } + clone(parent?: GLTFNode) { + const copy = {...this}; + copy._parent = parent; + nodes.push(copy); + return copy; + } + get parent() { + return this._parent; + } +} + +module.GLTFNode = GLTFNode; + +module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => { + const out = manifold.asOriginal(); + id2material.set(out.originalID(), material); + return out; +}; + +function debug(manifold: Manifold, map: Map) { + 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); +}; + // Setup complete self.postMessage(null); From 8a9b06260f5f530c1668f2ff1fe48aeb6c6407c9 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 12:26:22 -0700 Subject: [PATCH 06/41] Re-reformat --- bindings/wasm/examples/vite.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/wasm/examples/vite.config.js b/bindings/wasm/examples/vite.config.js index 05c223750..5872215b1 100644 --- a/bindings/wasm/examples/vite.config.js +++ b/bindings/wasm/examples/vite.config.js @@ -1,9 +1,9 @@ // vite.config.js -import { resolve } from 'path' -import { defineConfig } from 'vite' +import {resolve} from 'path' +import {defineConfig} from 'vite' export default defineConfig({ - test: { timeout: 10000 }, + test: {timeout: 10000}, worker: { format: 'es', }, From 623306b03b4541148f571faa7c9deb63079d1be7 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 12:26:34 -0700 Subject: [PATCH 07/41] Add back undefined check --- bindings/wasm/examples/public/examples.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bindings/wasm/examples/public/examples.js b/bindings/wasm/examples/public/examples.js index a28ab143f..1b33548ce 100644 --- a/bindings/wasm/examples/public/examples.js +++ b/bindings/wasm/examples/public/examples.js @@ -502,3 +502,7 @@ for (const [func, code] of Object.entries(examples.functions)) { func.replace(/([a-z])([A-Z])/g, '$1 $2'); // Add spaces between words examples.functionBodies.set(name, body); }; + +if (typeof self !== 'undefined') { + self.examples = examples; +} From 39ed5d73d2b28d5ca62832c8edb344ab2236098d Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 12:32:52 -0700 Subject: [PATCH 08/41] remove spurious newline --- bindings/wasm/examples/worker.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index b8e191177..3c755d458 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -132,7 +132,6 @@ const SHOW = { alpha: 0.25, roughness: 1, metallic: 0 - } as GLTFMaterial; const GHOST = { From b06c89eeeb1048d9e1b2af2fa039d9e615d58081 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 12:49:34 -0700 Subject: [PATCH 09/41] Move reserveIDs under Manifold --- bindings/wasm/bindings.cpp | 8 ++++---- bindings/wasm/bindings.js | 4 ++++ bindings/wasm/examples/worker.ts | 10 +++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/bindings/wasm/bindings.cpp b/bindings/wasm/bindings.cpp index 047d83e76..1abcb773e 100644 --- a/bindings/wasm/bindings.cpp +++ b/bindings/wasm/bindings.cpp @@ -126,6 +126,7 @@ EMSCRIPTEN_BINDINGS(whatever) { .function("_RectClip", &CrossSection::RectClip) .function("_ToPolygons", &CrossSection::ToPolygons); + // CrossSection Static Methods function("_Square", &CrossSection::Square); function("_Circle", &CrossSection::Circle); function("_crossSectionCompose", &CrossSection::Compose); @@ -165,6 +166,7 @@ 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); @@ -175,17 +177,15 @@ EMSCRIPTEN_BINDINGS(whatever) { 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); - // TODO: these are ambiguous with addition of CrossSection - // should they be unified with a dynamic check that the input array - // isn't mixed? + // 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 02ac33438..52789a498 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -547,6 +547,10 @@ Module.setup = function() { return cs.revolve(circularSegments); }; + Module.Manifold.reserveIDs = function(n) { + return Module._ReserveIDs(n); + }; + Module.Manifold.compose = function(manifolds) { const vec = new Module.Vector_manifold(); toVec(vec, manifolds); diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 3c755d458..8960475ef 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -39,28 +39,28 @@ glMatrix.glMatrix.setMatrixArrayType(Array); const io = setupIO(new WebIO()); io.registerExtensions(KHRONOS_EXTENSIONS); -// manifold static methods +// 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 +// 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 +// CrossSection static methods (that return a new cross-section) const crossSectionStaticFunctions = [ 'square', 'circle', 'union', 'difference', 'intersection', 'compose', 'ofPolygons' ]; -// CrossSection member functions that returns a new manifold +// CrossSection member functions (that return a new cross-section) const crossSectionMemberFunctions = [ 'add', 'subtract', 'intersect', 'rectClip', 'decompose', 'transform', 'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset', 'toPolygons' ]; -// top level functions that construct a new manifolds/meshes +// top level functions that construct a new manifold/mesh const toplevelConstructors = ['show', 'only', 'setMaterial']; const toplevel = [ 'setMinCircularAngle', 'setMinCircularEdgeLength', 'setCircularSegments', From c6b89f5d19b6bde4e50d728ba06bff509f75f367 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 15:47:19 -0700 Subject: [PATCH 10/41] Add center param to extrude --- bindings/wasm/bindings.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index 52789a498..af0716c9e 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -160,9 +160,11 @@ Module.setup = function() { }; Module.CrossSection.prototype.extrude = function( - height, nDivisions = 0, twistDegrees = 0.0, scaleTop = [1.0, 1.0]) { + height, nDivisions = 0, twistDegrees = 0.0, scaleTop = [1.0, 1.0], + center = false) { if (scaleTop instanceof Array) scaleTop = {x: scaleTop[0], y: scaleTop[1]}; - return Module._Extrude(this, height, nDivisions, twistDegrees, scaleTop); + const man = Module._Extrude(height, nDivisions, twistDegrees, scaleTop); + return center ? man.translate([0., 0., -height / 2.]) : man; }; Module.CrossSection.prototype.revolve = function(circularSegments = 0) { @@ -533,11 +535,11 @@ Module.setup = function() { Module.Manifold.extrude = function( polygons, height, nDivisions = 0, twistDegrees = 0.0, - scaleTop = [1.0, 1.0]) { + scaleTop = [1.0, 1.0], center = false) { const cs = (polygons instanceof CrossSectionCtor) ? polygons : Module.CrossSection(polygons, 'Positive'); - return cs.extrude(height, nDivisions, twistDegrees, scaleTop); + return cs.extrude(height, nDivisions, twistDegrees, scaleTop, center); }; Module.Manifold.revolve = function(polygons, circularSegments = 0) { From 2908a20348182ff459fad0e04af66b95d7275bbf Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 16:00:17 -0700 Subject: [PATCH 11/41] Add missing this argument back to _Extrude --- bindings/wasm/bindings.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index af0716c9e..ff33cf675 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -163,7 +163,8 @@ Module.setup = function() { height, nDivisions = 0, twistDegrees = 0.0, scaleTop = [1.0, 1.0], center = false) { if (scaleTop instanceof Array) scaleTop = {x: scaleTop[0], y: scaleTop[1]}; - const man = Module._Extrude(height, nDivisions, twistDegrees, scaleTop); + const man = + Module._Extrude(this, height, nDivisions, twistDegrees, scaleTop); return center ? man.translate([0., 0., -height / 2.]) : man; }; From 8423e6eedac5e654bc9d5532dfe415446918f994 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 21:38:12 -0700 Subject: [PATCH 12/41] Fix wrong timeout key --- bindings/wasm/examples/vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/wasm/examples/vite.config.js b/bindings/wasm/examples/vite.config.js index 5872215b1..b9a7e0cb5 100644 --- a/bindings/wasm/examples/vite.config.js +++ b/bindings/wasm/examples/vite.config.js @@ -3,7 +3,7 @@ import {resolve} from 'path' import {defineConfig} from 'vite' export default defineConfig({ - test: {timeout: 10000}, + test: {testTimeout: 15000}, worker: { format: 'es', }, From 2df7829af1a57f97d662e82cb105f9a948b376f3 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 21:38:27 -0700 Subject: [PATCH 13/41] rebuild on interface modification (ensure copying) --- bindings/wasm/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bindings/wasm/CMakeLists.txt b/bindings/wasm/CMakeLists.txt index 655394b13..b58bb8011 100644 --- a/bindings/wasm/CMakeLists.txt +++ b/bindings/wasm/CMakeLists.txt @@ -17,8 +17,12 @@ 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 + OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js + # FIXME: can we rewrite the custom command to run after these are modified when building + # without recompiling bindings.cpp? + OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/manifold.d.ts + OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/manifold-encapsulated-types.d.ts) 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 From bbd8ee1246f541694e58f161015e5b69cdcf10f2 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 21:40:53 -0700 Subject: [PATCH 14/41] Remove redundant checks in scale methods --- bindings/wasm/bindings.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index ff33cf675..e990e6265 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -107,9 +107,6 @@ Module.setup = function() { }; Module.CrossSection.prototype.scale = function(vec) { - if (typeof vec == 'number') { - return this._Scale({x: vec, y: vec}); - } return this._Scale(vararg2vec2([vec])); }; @@ -250,9 +247,6 @@ Module.setup = function() { }; Module.Manifold.prototype.scale = function(vec) { - if (typeof vec == 'number') { - return this._Scale({x: vec, y: vec, z: vec}); - } return this._Scale(vararg2vec3([vec])); }; From 618836cf36bcc72bf0eb65c77927811e9a637a1c Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 23:46:04 -0700 Subject: [PATCH 15/41] Undo scale change (vararg2vec not redundant) --- bindings/wasm/bindings.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index e990e6265..3b90a0d93 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -107,6 +107,10 @@ Module.setup = function() { }; 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])); }; @@ -247,6 +251,10 @@ Module.setup = function() { }; Module.Manifold.prototype.scale = function(vec) { + // if only one factor provided, scale all three dimensions (xyz) with it + if (typeof vec == 'number') { + return this._Scale({x: vec, y: vec, z: vec}); + } return this._Scale(vararg2vec3([vec])); }; From 4301220b620b8c5be9fdce72349c81fd359a9ca3 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 23:46:39 -0700 Subject: [PATCH 16/41] Fix bindings.cpp properties (js deps) --- bindings/wasm/CMakeLists.txt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bindings/wasm/CMakeLists.txt b/bindings/wasm/CMakeLists.txt index b58bb8011..cd6660732 100644 --- a/bindings/wasm/CMakeLists.txt +++ b/bindings/wasm/CMakeLists.txt @@ -16,13 +16,14 @@ project(wasm) add_executable(manifoldjs bindings.cpp) +set(JS_DEPS + ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js + ${CMAKE_CURRENT_SOURCE_DIR}/manifold.d.ts + ${CMAKE_CURRENT_SOURCE_DIR}/manifold-encapsulated-types.d.ts) +# FIXME: can we rewrite the custom command to run after these are modified when building +# without recompiling 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 - # FIXME: can we rewrite the custom command to run after these are modified when building - # without recompiling bindings.cpp? - OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/manifold.d.ts - OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/manifold-encapsulated-types.d.ts) +set_source_files_properties(bindings.cpp PROPERTIES OBJECT_DEPENDS ${JS_DEPS}) 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 From d8a9f189c0302c797d68b40bad824c74b3bce5c2 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Fri, 26 May 2023 23:47:14 -0700 Subject: [PATCH 17/41] Move reserveIDs into manifold intf as static This is the first of many (need to reorganize this interface and add CrossSection so that completion will work nicely again in editor) --- bindings/wasm/manifold-encapsulated-types.d.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 667c8becd..013fc7513 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -424,6 +424,13 @@ 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; + /** * Frees the WASM memory of this Manifold, since these cannot be * garbage-collected automatically. @@ -463,4 +470,4 @@ export class Mesh { extras(vert: number): Float32Array; tangent(halfedge: number): SealedFloat32Array<4>; transform(run: number): Mat4; -} \ No newline at end of file +} From 01820d773a4317456574674eceb3dd0198676d41 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Sat, 27 May 2023 00:12:39 -0700 Subject: [PATCH 18/41] Simplify memory registry closure The underscored method version did not allow destructuring methods from the classes for some reason (e.g. const {union} = Manifold) --- bindings/wasm/examples/worker.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 8960475ef..6355e2823 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -81,17 +81,15 @@ function addMembers( const cls = module[className]; const obj = areStatic ? cls : cls.prototype; for (const name of methodNames) { - //@ts-ignore - const originalFn = obj[name]; - //@ts-ignore - obj['_' + name] = originalFn; - //@ts-ignore - obj[name] = function(...args: any) { - //@ts-ignore - const result = this['_' + name](...args); - memoryRegistry.push(result); - return result; - }; + if (name != 'cylinder') { + const originalFn = obj[name]; + obj[name] = function(...args: any) { + //@ts-ignore + const result = originalFn(...args); + memoryRegistry.push(result); + return result; + }; + } } } From 98408f3e78e292f6546a95704d11ec72cfd05681 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Sat, 27 May 2023 00:25:33 -0700 Subject: [PATCH 19/41] Cleanup examples with local method opens --- bindings/wasm/examples/public/examples.js | 47 ++++++++++++----------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/bindings/wasm/examples/public/examples.js b/bindings/wasm/examples/public/examples.js index 1b33548ce..5edad6dd1 100644 --- a/bindings/wasm/examples/public/examples.js +++ b/bindings/wasm/examples/public/examples.js @@ -20,9 +20,9 @@ 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 box = Manifold.cube([100, 100, 100], true); - const ball = Manifold.sphere(60, 100); + 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 // GLTFNode - see Menger Sponge and Gyroid Module examples. const result = box.subtract(ball); @@ -107,27 +107,25 @@ 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 = - Manifold.cylinder(edgeLength, radius, -1, circularSegments); - const corner = Manifold.sphere(radius, circularSegments); + const edge = cylinder(edgeLength, radius, -1, circularSegments); + const corner = sphere(radius, circularSegments); - const edge1 = - Manifold.union(corner, edge).rotate([-90, 0, 0]).translate([ - -edgeLength / 2, -edgeLength / 2, 0 - ]); + const edge1 = union(corner, edge).rotate([-90, 0, 0]).translate([ + -edgeLength / 2, -edgeLength / 2, 0 + ]); - const edge2 = Manifold.union( - Manifold.union(edge1, edge1.rotate([0, 0, 180])), + const edge2 = union( + union(edge1, edge1.rotate([0, 0, 180])), edge.translate([-edgeLength / 2, -edgeLength / 2, 0])); - const edge4 = - Manifold.union(edge2, edge2.rotate([0, 0, 90])).translate([ - 0, 0, -edgeLength / 2 - ]); + const edge4 = union(edge2, edge2.rotate([0, 0, 90])).translate([ + 0, 0, -edgeLength / 2 + ]); - return Manifold.union(edge4, edge4.rotate([180, 0, 0])); + return union(edge4, edge4.rotate([180, 0, 0])); } setMinCircularAngle(3); @@ -330,10 +328,12 @@ export const examples = { const hole = Manifold.compose(holes); - result = Manifold.difference(result, hole); - result = Manifold.difference(result, hole.rotate([90, 0, 0])); - result = Manifold.difference(result, hole.rotate([0, 90, 0])); - + result = Manifold.difference( + result, + hole, + hole.rotate([90, 0, 0]), + hole.rotate([0, 90, 0]), + ); return result; } @@ -389,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 = Manifold.intersection(Manifold.extrude(stretch, width), b); - return b; + const result = + Manifold.intersection(Manifold.extrude(stretch, width), b); + return result; } function stretchyBracelet( From 5690a3a3ffd922bf83d90d055e97518de9160a8b Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Sat, 27 May 2023 10:03:10 -0700 Subject: [PATCH 20/41] Update Manifold interface (TODO add CrossSection) --- bindings/wasm/examples/editor.js | 4 +- bindings/wasm/examples/worker.ts | 4 +- .../wasm/manifold-encapsulated-types.d.ts | 337 ++++++++++-------- bindings/wasm/manifold.d.ts | 23 +- 4 files changed, 193 insertions(+), 175 deletions(-) diff --git a/bindings/wasm/examples/editor.js b/bindings/wasm/examples/editor.js index 9a8888085..51bf76ffe 100644 --- a/bindings/wasm/examples/editor.js +++ b/bindings/wasm/examples/editor.js @@ -231,7 +231,7 @@ async function getManifoldDTS() { return ` ${global.replaceAll('export', '')} ${encapsulated.replace(/^import.*$/gm, '').replaceAll('export', 'declare')} -declare interface ManifoldStatic { +declare interface ManifoldToplevel { cube: typeof cube; cylinder: typeof cylinder; sphere: typeof sphere; @@ -250,7 +250,7 @@ declare interface ManifoldStatic { getCircularSegments: typeof getCircularSegments; reserveIDs: typeof reserveIDs; } -declare const module: ManifoldStatic; +declare const module: ManifoldToplevel; `; } diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 6355e2823..ffd627d64 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 {CrossSection, 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; diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 013fc7513..e5c1b671f 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -14,100 +14,6 @@ import {Box, Curvature, Mat4, Polygons, Properties, SealedFloat32Array, SealedUint32Array, Smoothness, Vec2, Vec3} from './manifold-global-types'; -/** - * Constructs a unit cube (edge lengths all one), by default in the first - * octant, touching the origin. - * - * @param size The X, Y, and Z dimensions of the box. - * @param center Set to true to shift the center to the origin. - */ -export function cube(size?: Vec3|number, center?: boolean): Manifold; - -/** - * A convenience constructor for the common case of extruding a circle. Can also - * form cones if both radii are specified. - * - * @param height Z-extent - * @param radiusLow Radius of bottom circle. Must be positive. - * @param radiusHigh Radius of top circle. Can equal zero. Default is equal to - * radiusLow. - * @param circularSegments How many line segments to use around the circle. - * Default is calculated by the static Defaults. - * @param center Set to true to shift the center to the origin. Default is - * origin at the bottom. - */ -export function cylinder( - height: number, radiusLow: number, radiusHigh?: number, - circularSegments?: number, center?: boolean): Manifold; - -/** - * Constructs a geodesic sphere of a given radius. - * - * @param radius Radius of the sphere. Must be positive. - * @param circularSegments Number of segments along its - * diameter. This number will always be rounded up to the nearest factor of - * four, as this sphere is constructed by refining an octahedron. This means - * there are a circle of vertices on all three of the axis planes. Default is - * calculated by the static Defaults. - */ -export function sphere(radius: number, circularSegments?: number): Manifold; - - -/** - * Constructs a smooth version of the input mesh by creating tangents; this - * method will throw if you have supplied tangents with your mesh already. The - * actual triangle resolution is unchanged; use the Refine() method to - * interpolate to a higher-resolution curve. - * - * By default, every edge is calculated for maximum smoothness (very much - * approximately), attempting to minimize the maximum mean Curvature magnitude. - * No higher-order derivatives are considered, as the interpolation is - * independent per triangle, only sharing constraints on their boundaries. - * - * @param mesh input Mesh. - * @param sharpenedEdges If desired, you can supply a vector of sharpened - * halfedges, which should in general be a small subset of all halfedges. Order - * of entries doesn't matter, as each one specifies the desired smoothness - * (between zero and one, with one the default for all unspecified halfedges) - * and the halfedge index (3 * triangle index + [0,1,2] where 0 is the edge - * between triVert 0 and 1, etc). - * - * At a smoothness value of zero, a sharp crease is made. The smoothness is - * interpolated along each edge, so the specified value should be thought of as - * an average. Where exactly two sharpened edges meet at a vertex, their - * tangents are rotated to be colinear so that the sharpened edge can be - * continuous. Vertices with only one sharpened edge are completely smooth, - * allowing sharpened edges to smoothly vanish at termination. A single vertex - * can be sharpened by sharping all edges that are incident on it, allowing - * cones to be formed. - */ -export function smooth(mesh: Mesh, sharpenedEdges?: Smoothness[]): Manifold; - -/** - * Constructs a tetrahedron centered at the origin with one vertex at (1,1,1) - * and the rest at similarly symmetric points. - */ -export function tetrahedron(): Manifold; - -/** - * Constructs a manifold from a set of polygons by extruding them along the - * Z-axis. - * - * @param crossSection A set of non-overlapping polygons to extrude. - * @param height Z-extent of extrusion. - * @param nDivisions Number of extra copies of the crossSection to insert into - * the shape vertically; especially useful in combination with twistDegrees to - * avoid interpolation artifacts. Default is none. - * @param twistDegrees Amount to twist the top crossSection relative to the - * bottom, interpolated linearly for the divisions in between. - * @param scaleTop Amount to scale the top (independently in X and Y). If the - * scale is {0, 0}, a pure cone is formed with only a single vertex at the top. - * Default {1, 1}. - */ -export function extrude( - crossSection: Polygons, height: number, nDivisions?: number, - twistDegrees?: number, scaleTop?: Vec2): Manifold; - /** * Triangulates a set of /epsilon-valid polygons. * @@ -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,13 +40,6 @@ 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 Manifold { /** * Convert a Mesh into a Manifold, retaining its properties and merging only @@ -206,6 +53,151 @@ export class Manifold { * materials into triangle runs. */ constructor(mesh: Mesh); + + /** + * 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; + + /** + * Constructs a manifold from a set of polygons/cross-section 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}. + * @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( + crossSection: Polygons, height: number, nDivisions?: number, + twistDegrees?: number, scaleTop?: Vec2, center?: boolean): Manifold; + + /** + * Constructs a manifold from a set of polygons/cross-section 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. + */ + static revolve(crossSection: Polygons, circularSegments?: number): Manifold; + + /** + * 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; + + /** + * 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; + /** * Transform this Manifold in space. Stored in column-major order. This * operation can be chained. Transforms are combined and applied lazily. @@ -268,6 +260,36 @@ export class Manifold { */ intersect(other: Manifold): Manifold; + /** + * Boolean union of the manifolds a and b + */ + static union(a: Manifold, b: Manifold): Manifold; + + /** + * Boolean difference of the manifold b from the manifold a + */ + static difference(a: Manifold, b: Manifold): Manifold; + + /** + * Boolean intersection of the manifolds a and b + */ + static intersection(a: Manifold, b: Manifold): Manifold; + + /** + * Boolean union of a list of manifolds + */ + static union(manifolds: Manifold[]): Manifold; + + /** + * Boolean difference of the tail of a list of manifolds from its head + */ + static difference(manifolds: Manifold[]): Manifold; + + /** + * Boolean intersection of a list of manifolds + */ + static intersection(manifolds: Manifold[]): Manifold; + /** * Removes everything behind the given half-space plane. * @@ -318,6 +340,15 @@ export class Manifold { propFunc: (newProp: number[], position: Vec3, oldProp: number[]) => void): 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. + */ + 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, diff --git a/bindings/wasm/manifold.d.ts b/bindings/wasm/manifold.d.ts index 8b86d67a0..be8a82d34 100644 --- a/bindings/wasm/manifold.d.ts +++ b/bindings/wasm/manifold.d.ts @@ -19,30 +19,17 @@ 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; - CrossSection: typeof T.CrossSection; - Manifold: typeof T.Manifold; - Mesh: typeof T.Mesh; setup: () => void; } export default function Module(config?: {locateFile: () => string}): - Promise; + Promise; From 78d7509c114990b97b64b1e1a460d7e5c57f854b Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Sat, 27 May 2023 12:43:24 -0700 Subject: [PATCH 21/41] Copy js interface files on mod without recompiling --- bindings/wasm/CMakeLists.txt | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/bindings/wasm/CMakeLists.txt b/bindings/wasm/CMakeLists.txt index cd6660732..876428b54 100644 --- a/bindings/wasm/CMakeLists.txt +++ b/bindings/wasm/CMakeLists.txt @@ -16,14 +16,7 @@ project(wasm) add_executable(manifoldjs bindings.cpp) -set(JS_DEPS - ${CMAKE_CURRENT_SOURCE_DIR}/bindings.js - ${CMAKE_CURRENT_SOURCE_DIR}/manifold.d.ts - ${CMAKE_CURRENT_SOURCE_DIR}/manifold-encapsulated-types.d.ts) -# FIXME: can we rewrite the custom command to run after these are modified when building -# without recompiling bindings.cpp? -# make sure that we recompile the wasm when bindings.js is being modified -set_source_files_properties(bindings.cpp PROPERTIES OBJECT_DEPENDS ${JS_DEPS}) +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 @@ -34,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/) From 0f3e52a247d2f64199a11bc90a1210bb0a4c6640 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Sat, 27 May 2023 12:55:31 -0700 Subject: [PATCH 22/41] Add note to READMEs about Firefox fix --- README.md | 18 +++++++++++++----- bindings/wasm/examples/README.md | 17 +++++++++++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) 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/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. From 233e81fcd84335a9f47861bc49396bc78bd2c741 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Sat, 27 May 2023 13:49:45 -0700 Subject: [PATCH 23/41] Add CrossSection interface and associated types --- .../wasm/manifold-encapsulated-types.d.ts | 299 +++++++++++++++++- bindings/wasm/manifold-global-types.d.ts | 10 +- 2 files changed, 302 insertions(+), 7 deletions(-) diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index e5c1b671f..e9d45fdaa 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -12,7 +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'; +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. @@ -40,6 +40,274 @@ export function setCircularSegments(segments: number): void; export function getCircularSegments(radius: number): number; ///@} +export class CrossSection { + /** + * Create a 2d cross-section from a set of contours (complex polygons). A + * boolean union operation (with Positive filling rule by default) is + * performed to combine overlapping polygons and ensure the resulting + * CrossSection is free of intersections. + * + * @param contours A set of closed paths describing zero or more complex + * polygons. + * @param fillrule The filling rule used to interpret polygon sub-regions in + * contours. + */ + constructor(polygons: SimplePolygon|SimplePolygon[], fillrule?: FillRule); + + static square(size?: Vec2|number, center?: boolean): CrossSection; + + static circle(radius: number, circularSegments?: number): CrossSection; + + /** + * 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, center?: boolean): CrossSection; + + /** + * 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. + */ + static revolve(circularSegments?: number): CrossSection; + + /** + * 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 jt The join type specifying the treatment of contour joins + * (corners). + * @param miter_limit 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 arc_tolerance The maximum acceptable imperfection for curves drawn + * (approximated with line segments) for Round joins (not relevant for other + * JoinTypes). By default (when undefined or =0), the allowable imprecision is + * scaled in inverse proportion to the offset delta. + */ + 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; + + /** + * Boolean union + */ + add(other: Polygons): CrossSection; + + /** + * Boolean difference + */ + subtract(other: Polygons): CrossSection; + + /** + * Boolean intersection + */ + intersect(other: Polygons): CrossSection; + + /** + * Boolean union of the cross-sections a and b + */ + static union(a: Polygons, b: Polygons): CrossSection; + + /** + * Boolean difference of the cross-section b from the cross-section a + */ + static difference(a: Polygons, b: Polygons): CrossSection; + + /** + * Boolean intersection of the cross-sections a and b + */ + static intersection(a: Polygons, b: Polygons): CrossSection; + + /** + * Boolean union of a list of cross-sections + */ + static union(polygons: Polygons[]): CrossSection; + + /** + * Boolean difference of the tail of a list of cross-sections from its head + */ + static difference(polygons: Polygons[]): CrossSection; + + /** + * Boolean intersection of a list of cross-sections + */ + static intersection(polygons: 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; + + /** + * Construct a CrossSection from a vector of other Polygons (batch + * boolean union). + */ + static compose(polygons: 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[]; + + /** + * 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. + */ + ofPoygons(polygons: SimplePolygon|SimplePolygon[], fillrule?: FillRule): + CrossSection; + + /** + * Return the contours of this CrossSection as a list of simple polygons. + */ + toPolygons(): SimplePolygon[]; + + /** + * 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; + + /** + * Frees the WASM memory of this CrossSection, since these cannot be + * garbage-collected automatically. + */ + delete(): void; +} + export class Manifold { /** * Convert a Mesh into a Manifold, retaining its properties and merging only @@ -122,10 +390,10 @@ export class Manifold { /** * Constructs a manifold from a set of polygons/cross-section 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. + * 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 crossSection A set of non-overlapping polygons to revolve. * @param circularSegments Number of segments along its diameter. Default is @@ -290,6 +558,27 @@ export class Manifold { */ 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. * diff --git a/bindings/wasm/manifold-global-types.d.ts b/bindings/wasm/manifold-global-types.d.ts index 0dd6eb11b..7afd467a9 100644 --- a/bindings/wasm/manifold-global-types.d.ts +++ b/bindings/wasm/manifold-global-types.d.ts @@ -42,7 +42,11 @@ export type Mat4 = [ number, ]; export type SimplePolygon = Vec2[]; -export type Polygons = SimplePolygon|SimplePolygon[]; +export type Polygons = SimplePolygon|SimplePolygon[]|CrossSection; +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' From b30cc2530627b0c95d75ee91a81be9e1aebe8bda Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 11:13:29 -0700 Subject: [PATCH 24/41] Revert Polygons and fix signatures --- .../wasm/manifold-encapsulated-types.d.ts | 33 ++++++++++--------- bindings/wasm/manifold-global-types.d.ts | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index e9d45fdaa..8bd51b798 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -52,7 +52,7 @@ export class CrossSection { * @param fillrule The filling rule used to interpret polygon sub-regions in * contours. */ - constructor(polygons: SimplePolygon|SimplePolygon[], fillrule?: FillRule); + constructor(polygons: Polygons, fillrule?: FillRule); static square(size?: Vec2|number, center?: boolean): CrossSection; @@ -192,47 +192,50 @@ export class CrossSection { /** * Boolean union */ - add(other: Polygons): CrossSection; + add(other: CrossSection|Polygons): CrossSection; /** * Boolean difference */ - subtract(other: Polygons): CrossSection; + subtract(other: CrossSection|Polygons): CrossSection; /** * Boolean intersection */ - intersect(other: Polygons): CrossSection; + intersect(other: CrossSection|Polygons): CrossSection; /** * Boolean union of the cross-sections a and b */ - static union(a: Polygons, b: Polygons): CrossSection; + static union(a: CrossSection|Polygons, b: CrossSection|Polygons): + CrossSection; /** * Boolean difference of the cross-section b from the cross-section a */ - static difference(a: Polygons, b: Polygons): CrossSection; + static difference(a: CrossSection|Polygons, b: CrossSection|Polygons): + CrossSection; /** * Boolean intersection of the cross-sections a and b */ - static intersection(a: Polygons, b: Polygons): CrossSection; + static intersection(a: CrossSection|Polygons, b: CrossSection|Polygons): + CrossSection; /** * Boolean union of a list of cross-sections */ - static union(polygons: Polygons[]): CrossSection; + static union(polygons: (CrossSection|Polygons)[]): CrossSection; /** * Boolean difference of the tail of a list of cross-sections from its head */ - static difference(polygons: Polygons[]): CrossSection; + static difference(polygons: (CrossSection|Polygons)[]): CrossSection; /** * Boolean intersection of a list of cross-sections */ - static intersection(polygons: Polygons[]): CrossSection; + static intersection(polygons: (CrossSection|Polygons)[]): CrossSection; /** * Compute the intersection between a cross-section and an axis-aligned @@ -246,7 +249,7 @@ export class CrossSection { * Construct a CrossSection from a vector of other Polygons (batch * boolean union). */ - static compose(polygons: Polygons[]): CrossSection; + static compose(polygons: (CrossSection|Polygons)[]): CrossSection; /** * This operation returns a vector of CrossSections that are topologically @@ -266,8 +269,7 @@ export class CrossSection { * @param fillrule The filling rule used to interpret polygon sub-regions in * contours. */ - ofPoygons(polygons: SimplePolygon|SimplePolygon[], fillrule?: FillRule): - CrossSection; + ofPolygons(polygons: Polygons, fillrule?: FillRule): CrossSection; /** * Return the contours of this CrossSection as a list of simple polygons. @@ -385,7 +387,7 @@ export class Manifold { * as opposed to resting on the XY plane as is default. */ static extrude( - crossSection: Polygons, height: number, nDivisions?: number, + crossSection: CrossSection|Polygons, height: number, nDivisions?: number, twistDegrees?: number, scaleTop?: Vec2, center?: boolean): Manifold; /** @@ -399,7 +401,8 @@ export class Manifold { * @param circularSegments Number of segments along its diameter. Default is * calculated by the static Defaults. */ - static revolve(crossSection: Polygons, circularSegments?: number): Manifold; + static revolve( + crossSection: CrossSection|Polygons, circularSegments?: number): Manifold; /** * Convert a Mesh into a Manifold, retaining its properties and merging only diff --git a/bindings/wasm/manifold-global-types.d.ts b/bindings/wasm/manifold-global-types.d.ts index 7afd467a9..45c874514 100644 --- a/bindings/wasm/manifold-global-types.d.ts +++ b/bindings/wasm/manifold-global-types.d.ts @@ -42,7 +42,7 @@ export type Mat4 = [ number, ]; export type SimplePolygon = Vec2[]; -export type Polygons = SimplePolygon|SimplePolygon[]|CrossSection; +export type Polygons = SimplePolygon|SimplePolygon[]; export type Rect = { min: Vec2, max: Vec2 From 55320c7a1733aae3ebc67e07e7de93cad3465086 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 12:19:42 -0700 Subject: [PATCH 25/41] Fix instanceof for Manifold and CrossSection --- bindings/wasm/bindings.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index 3b90a0d93..ca771dd06 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -483,6 +483,14 @@ Module.setup = function() { 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; @@ -598,6 +606,14 @@ Module.setup = function() { Module.Manifold.prototype = Object.create(ManifoldCtor.prototype); + // Because the constructor and prototype are being replaced, instanceof will + // not work as desired unless we refer back to the original like this + Object.defineProperty(Module.Manifold, Symbol.hasInstance, { + get: () => (t) => { + return (t instanceof ManifoldCtor); + } + }); + // Top-level functions Module.triangulate = function(polygons, precision = -1) { From f3dc8056cd78ad0a0053b45f4f3c1d022659bf95 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 12:19:58 -0700 Subject: [PATCH 26/41] Fix signatures of extrude and revolve --- bindings/wasm/manifold-encapsulated-types.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 8bd51b798..48f380b04 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -76,7 +76,7 @@ export class CrossSection { */ extrude( height: number, nDivisions?: number, twistDegrees?: number, - scaleTop?: Vec2, center?: boolean): CrossSection; + scaleTop?: Vec2, center?: boolean): Manifold; /** * Constructs a manifold by revolving this cross-section around its Y-axis and @@ -87,7 +87,7 @@ export class CrossSection { * @param circularSegments Number of segments along its diameter. Default is * calculated by the static Defaults. */ - static revolve(circularSegments?: number): CrossSection; + revolve(circularSegments?: number): Manifold; /** * Transform this CrossSection in space. Stored in column-major order. This From 4d43bb6c19b508e42cb113c328b3e74ed619e706 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 12:21:47 -0700 Subject: [PATCH 27/41] Extend show and only to take CrossSections --- bindings/wasm/examples/worker.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index ffd627d64..96037bb3d 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -24,8 +24,8 @@ import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './publi interface WorkerStatic extends ManifoldToplevel { GLTFNode: typeof GLTFNode; - show(manifold: Manifold): Manifold; - only(manifold: Manifold): Manifold; + show(shape: CrossSection|Manifold): Manifold; + only(shape: CrossSection|Manifold): Manifold; setMaterial(manifold: Manifold, material: GLTFMaterial): void; cleanup(): void; } @@ -184,19 +184,28 @@ module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => { return out; }; -function debug(manifold: Manifold, map: Map) { - const result = manifold.asOriginal(); +function debug(shape: Manifold|CrossSection, map: Map) { + let result; + if (shape instanceof module.CrossSection) { + const box = shape.bounds(); + const x = box.max[0] - box.min[0]; + const y = box.max[1] - box.min[1]; + const h = Math.max(x, y) / 100; + result = shape.extrude(h).translate([0, 0, -h / 2]); + } else { + result = shape.asOriginal(); + } map.set(result.originalID(), result.getMesh()); return result; }; -module.show = (manifold) => { - return debug(manifold, shown); +module.show = (shape) => { + return debug(shape, shown); }; -module.only = (manifold) => { +module.only = (shape) => { ghost = true; - return debug(manifold, singles); + return debug(shape, singles); }; // Setup complete From 7aba193ca39d6402d2047f912ecb892fd4feed62 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 15:43:18 -0700 Subject: [PATCH 28/41] Copy ManifoldToplevel into editor.js (necessary?) --- bindings/wasm/examples/editor.js | 26 ++++++++--------------- bindings/wasm/examples/public/editor.d.ts | 19 +++++++++-------- bindings/wasm/examples/worker.ts | 7 +++--- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/bindings/wasm/examples/editor.js b/bindings/wasm/examples/editor.js index 51bf76ffe..80a957351 100644 --- a/bindings/wasm/examples/editor.js +++ b/bindings/wasm/examples/editor.js @@ -232,23 +232,15 @@ async function getManifoldDTS() { ${global.replaceAll('export', '')} ${encapsulated.replace(/^import.*$/gm, '').replaceAll('export', 'declare')} declare interface ManifoldToplevel { - 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; + 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: ManifoldToplevel; `; 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/worker.ts b/bindings/wasm/examples/worker.ts index 96037bb3d..6c791663c 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -185,16 +185,17 @@ module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => { }; function debug(shape: Manifold|CrossSection, map: Map) { - let result; + let manifold; if (shape instanceof module.CrossSection) { const box = shape.bounds(); const x = box.max[0] - box.min[0]; const y = box.max[1] - box.min[1]; const h = Math.max(x, y) / 100; - result = shape.extrude(h).translate([0, 0, -h / 2]); + manifold = shape.extrude(h).translate([0, 0, -h / 2]); } else { - result = shape.asOriginal(); + manifold = shape; } + let result = manifold.asOriginal(); map.set(result.originalID(), result.getMesh()); return result; }; From 817544aecda4b2dede0f48578d8fc77fbc677693 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 16:16:49 -0700 Subject: [PATCH 29/41] Fix enums in CrossSection constr and offset --- bindings/wasm/bindings.cpp | 5 ++--- bindings/wasm/bindings.js | 16 +++++++-------- bindings/wasm/helpers.cpp | 20 +++++++++++++++++++ .../wasm/manifold-encapsulated-types.d.ts | 16 +++++++-------- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/bindings/wasm/bindings.cpp b/bindings/wasm/bindings.cpp index 1abcb773e..7113af32b 100644 --- a/bindings/wasm/bindings.cpp +++ b/bindings/wasm/bindings.cpp @@ -104,8 +104,7 @@ EMSCRIPTEN_BINDINGS(whatever) { register_vector("Vector_vec4"); class_("CrossSection") - .constructor>, - CrossSection::FillRule>() + .constructor(&cross_js::OfPolygons) .function("_add", &cross_js::Union) .function("_subtract", &cross_js::Difference) .function("_intersect", &cross_js::Intersection) @@ -122,7 +121,7 @@ EMSCRIPTEN_BINDINGS(whatever) { .function("numContour", &CrossSection::NumContour) .function("_Bounds", &CrossSection::Bounds) .function("simplify", &CrossSection::Simplify) - .function("_Offset", &CrossSection::Offset) + .function("_Offset", &cross_js::Offset) .function("_RectClip", &CrossSection::RectClip) .function("_ToPolygons", &CrossSection::ToPolygons); diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index ca771dd06..d120c1b88 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -87,12 +87,12 @@ Module.setup = function() { const CrossSectionCtor = Module.CrossSection; - function cross(polygons, fillrule = 'Positive') { + function cross(polygons, fillRule = 'Positive') { if (polygons instanceof CrossSectionCtor) { return polygons; } else { const polygonsVec = polygons2vec(polygons); - const cs = new CrossSectionCtor(polygonsVec, fillrule = fillrule); + const cs = new CrossSectionCtor(polygonsVec, fillRule = fillRule); disposePolygons(polygonsVec); return cs; } @@ -148,8 +148,8 @@ Module.setup = function() { }; Module.CrossSection.prototype.offset = function( - delta, jointype = 'Square', miterLimit = 2.0, arcTolerance = 0.) { - return this._Offset(delta, jointype, miterLimit, arcTolerance); + delta, joinType = 'Square', miterLimit = 2.0, arcTolerance = 0.) { + return this._Offset(delta, joinType, miterLimit, arcTolerance); }; Module.CrossSection.prototype.rectClip = function(rect) { @@ -436,15 +436,15 @@ Module.setup = function() { // CrossSection Constructors - Module.CrossSection = function(polygons, fillrule = 'Positive') { + Module.CrossSection = function(polygons, fillRule = 'Positive') { const polygonsVec = polygons2vec(polygons); - const cs = new CrossSectionCtor(polygonsVec, fillrule = fillrule); + const cs = new CrossSectionCtor(polygonsVec, fillRule = fillRule); disposePolygons(polygonsVec); return cs; }; - Module.CrossSection.ofPolygons = function(polygons, fillrule = 'Positive') { - return new Module.CrossSection(polygons, fillrule = fillrule); + Module.CrossSection.ofPolygons = function(polygons, fillRule = 'Positive') { + return new Module.CrossSection(polygons, fillRule = fillRule); }; Module.CrossSection.square = function(...args) { diff --git a/bindings/wasm/helpers.cpp b/bindings/wasm/helpers.cpp index c94b38d5f..1be8d01f3 100644 --- a/bindings/wasm/helpers.cpp +++ b/bindings/wasm/helpers.cpp @@ -6,6 +6,8 @@ #include +#include "cross_section.h" + using namespace emscripten; using namespace manifold; @@ -104,6 +106,15 @@ Manifold Smooth(const val& mesh, } // namespace js namespace cross_js { +CrossSection OfPolygons(std::vector> polygons, + std::string fill_rule) { + auto fr = fill_rule == "EvenOdd" ? CrossSection::FillRule::EvenOdd + : fill_rule == "NonZero" ? CrossSection::FillRule::NonZero + : fill_rule == "Positive" ? CrossSection::FillRule::Positive + : CrossSection::FillRule::Negative; + return CrossSection(polygons, fr); +} + CrossSection Union(const CrossSection& a, const CrossSection& b) { return a + b; } @@ -140,6 +151,15 @@ 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, + std::string join_type, double miter_limit, + double arc_tolerance) { + auto jt = join_type == "Square" ? CrossSection::JoinType::Square + : join_type == "Round" ? CrossSection::JoinType::Round + : CrossSection::JoinType::Miter; + return cross_section.Offset(delta, jt, miter_limit, arc_tolerance); +} } // namespace cross_js namespace man_js { diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 48f380b04..f4f45a31d 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -49,10 +49,10 @@ export class CrossSection { * * @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 + * @param fillRule The filling rule used to interpret polygon sub-regions in * contours. */ - constructor(polygons: Polygons, fillrule?: FillRule); + constructor(polygons: Polygons, fillRule?: FillRule); static square(size?: Vec2|number, center?: boolean): CrossSection; @@ -154,21 +154,21 @@ export class CrossSection { * @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 jt The join type specifying the treatment of contour joins + * @param joinType The join type specifying the treatment of contour joins * (corners). - * @param miter_limit The maximum distance in multiples of delta that vertices + * @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 arc_tolerance The maximum acceptable imperfection for curves drawn + * @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, + delta: number, joinType?: JoinType, miterLimit?: number, arcTolerance?: number): CrossSection; /** @@ -266,10 +266,10 @@ export class CrossSection { * * @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 + * @param fillRule The filling rule used to interpret polygon sub-regions in * contours. */ - ofPolygons(polygons: Polygons, fillrule?: FillRule): CrossSection; + ofPolygons(polygons: Polygons, fillRule?: FillRule): CrossSection; /** * Return the contours of this CrossSection as a list of simple polygons. From 4871d54ecd5720e9d61daf1f1111c6b501d8e638 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 16:59:14 -0700 Subject: [PATCH 30/41] Doc comment fixes --- bindings/wasm/manifold-encapsulated-types.d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index f4f45a31d..814c2b4a6 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -158,7 +158,7 @@ export class CrossSection { * (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 + * 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. @@ -372,7 +372,7 @@ export class Manifold { * Constructs a manifold from a set of polygons/cross-section by extruding * them along the Z-axis. * - * @param crossSection A set of non-overlapping polygons to extrude. + * @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 @@ -387,7 +387,7 @@ export class Manifold { * as opposed to resting on the XY plane as is default. */ static extrude( - crossSection: CrossSection|Polygons, height: number, nDivisions?: number, + polygons: CrossSection|Polygons, height: number, nDivisions?: number, twistDegrees?: number, scaleTop?: Vec2, center?: boolean): Manifold; /** @@ -397,12 +397,12 @@ export class Manifold { * side is used. Geometrically valid input will result in geometrically valid * output. * - * @param crossSection A set of non-overlapping polygons to revolve. + * @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( - crossSection: CrossSection|Polygons, circularSegments?: number): Manifold; + static revolve(polygons: CrossSection|Polygons, circularSegments?: number): + Manifold; /** * Convert a Mesh into a Manifold, retaining its properties and merging only From 8a389cafc162bf3614ce97d611027d30d25ff169 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 21:59:09 -0700 Subject: [PATCH 31/41] allow scalar input to extrude's scaleTop parameter --- bindings/wasm/bindings.js | 4 ++-- bindings/wasm/manifold-encapsulated-types.d.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index d120c1b88..059a45c72 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -163,10 +163,10 @@ Module.setup = function() { Module.CrossSection.prototype.extrude = function( height, nDivisions = 0, twistDegrees = 0.0, scaleTop = [1.0, 1.0], center = false) { - if (scaleTop instanceof Array) scaleTop = {x: scaleTop[0], y: scaleTop[1]}; + scaleTop = vararg2vec2([scaleTop]); const man = Module._Extrude(this, height, nDivisions, twistDegrees, scaleTop); - return center ? man.translate([0., 0., -height / 2.]) : man; + return (center ? man.translate([0., 0., -height / 2.]) : man); }; Module.CrossSection.prototype.revolve = function(circularSegments = 0) { diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 814c2b4a6..2daf4291c 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -76,7 +76,7 @@ export class CrossSection { */ extrude( height: number, nDivisions?: number, twistDegrees?: number, - scaleTop?: Vec2, center?: boolean): Manifold; + scaleTop?: Vec2|number, center?: boolean): Manifold; /** * Constructs a manifold by revolving this cross-section around its Y-axis and @@ -388,7 +388,8 @@ export class Manifold { */ static extrude( polygons: CrossSection|Polygons, height: number, nDivisions?: number, - twistDegrees?: number, scaleTop?: Vec2, center?: boolean): Manifold; + twistDegrees?: number, scaleTop?: Vec2|number, + center?: boolean): Manifold; /** * Constructs a manifold from a set of polygons/cross-section by revolving From 2fa3f44bb226d56c73c12d82e25072b07fe87030 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 21:59:59 -0700 Subject: [PATCH 32/41] Use CrossSection a bit more in examples --- bindings/wasm/examples/public/examples.js | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/bindings/wasm/examples/public/examples.js b/bindings/wasm/examples/public/examples.js index 5edad6dd1..d4ee1d9a3 100644 --- a/bindings/wasm/examples/public/examples.js +++ b/bindings/wasm/examples/public/examples.js @@ -20,8 +20,11 @@ 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 {sphere} = Manifold; + const {square} = CrossSection; + const box = square([70, 70], true) + .offset(15, 'Round') + .extrude(100, 0, 0, [1, 1], true); const ball = sphere(60, 100); // You must name your final output "result", or create at least one // GLTFNode - see Menger Sponge and Gyroid Module examples. @@ -363,17 +366,10 @@ export const examples = { width, radius, decorRadius, twistRadius, nDecor, innerRadius, outerRadius, cut, nCut, nDivision) { let b = Manifold.cylinder(width, radius + twistRadius / 2); - const circle = []; - const dPhiDeg = 180 / nDivision; - for (let i = 0; i < 2 * nDivision; ++i) { - circle.push([ - decorRadius * Math.cos(dPhiDeg * i * Math.PI / 180) + twistRadius, - decorRadius * Math.sin(dPhiDeg * i * Math.PI / 180) - ]); - } - let decor = Manifold.extrude(circle, width, nDivision, 180) - .scale([1, 0.5, 1]) - .translate([0, radius, 0]); + const cyl = CrossSection.circle(decorRadius) + .translate([twistRadius, 0]) + .extrude(width, nDivision, 180); + const decor = cyl.scale([1, 0.5, 1]).translate([0, radius, 0]); for (let i = 0; i < nDecor; i++) b = b.add(decor.rotate([0, 0, (360.0 / nDecor) * i])); const stretch = []; From 97e43810894721907ef612f18e3aab2914857734 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 22:30:52 -0700 Subject: [PATCH 33/41] Remove std::string from enum handling --- bindings/wasm/bindings.js | 20 ++++++++++++++++---- bindings/wasm/helpers.cpp | 21 ++++++++++----------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index 059a45c72..22c05f4cc 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -83,6 +83,17 @@ 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; @@ -92,7 +103,7 @@ Module.setup = function() { return polygons; } else { const polygonsVec = polygons2vec(polygons); - const cs = new CrossSectionCtor(polygonsVec, fillRule = fillRule); + const cs = new CrossSectionCtor(polygonsVec, fillRuleToInt(fillRule)); disposePolygons(polygonsVec); return cs; } @@ -149,7 +160,8 @@ Module.setup = function() { Module.CrossSection.prototype.offset = function( delta, joinType = 'Square', miterLimit = 2.0, arcTolerance = 0.) { - return this._Offset(delta, joinType, miterLimit, arcTolerance); + return this._Offset( + delta, joinTypeToInt(joinType), miterLimit, arcTolerance); }; Module.CrossSection.prototype.rectClip = function(rect) { @@ -438,13 +450,13 @@ Module.setup = function() { Module.CrossSection = function(polygons, fillRule = 'Positive') { const polygonsVec = polygons2vec(polygons); - const cs = new CrossSectionCtor(polygonsVec, fillRule = fillRule); + const cs = new CrossSectionCtor(polygonsVec, fillRuleToInt(fillRule)); disposePolygons(polygonsVec); return cs; }; Module.CrossSection.ofPolygons = function(polygons, fillRule = 'Positive') { - return new Module.CrossSection(polygons, fillRule = fillRule); + return new Module.CrossSection(polygons, fillRule); }; Module.CrossSection.square = function(...args) { diff --git a/bindings/wasm/helpers.cpp b/bindings/wasm/helpers.cpp index 1be8d01f3..50b3e9d51 100644 --- a/bindings/wasm/helpers.cpp +++ b/bindings/wasm/helpers.cpp @@ -107,11 +107,11 @@ Manifold Smooth(const val& mesh, namespace cross_js { CrossSection OfPolygons(std::vector> polygons, - std::string fill_rule) { - auto fr = fill_rule == "EvenOdd" ? CrossSection::FillRule::EvenOdd - : fill_rule == "NonZero" ? CrossSection::FillRule::NonZero - : fill_rule == "Positive" ? CrossSection::FillRule::Positive - : CrossSection::FillRule::Negative; + 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); } @@ -152,12 +152,11 @@ CrossSection Warp(CrossSection& cross_section, uintptr_t funcPtr) { return cross_section.Warp(f); } -CrossSection Offset(CrossSection& cross_section, double delta, - std::string join_type, double miter_limit, - double arc_tolerance) { - auto jt = join_type == "Square" ? CrossSection::JoinType::Square - : join_type == "Round" ? CrossSection::JoinType::Round - : CrossSection::JoinType::Miter; +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 From f0a518ebc2d5ad893ab7d3c7806d17bd9f6a178f Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 22:49:28 -0700 Subject: [PATCH 34/41] Undo flubbed bracelet change, update intro test --- bindings/wasm/examples/public/examples.js | 15 +++++++++++---- bindings/wasm/examples/worker.test.js | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/bindings/wasm/examples/public/examples.js b/bindings/wasm/examples/public/examples.js index d4ee1d9a3..7fe0253b3 100644 --- a/bindings/wasm/examples/public/examples.js +++ b/bindings/wasm/examples/public/examples.js @@ -366,10 +366,17 @@ export const examples = { width, radius, decorRadius, twistRadius, nDecor, innerRadius, outerRadius, cut, nCut, nDivision) { let b = Manifold.cylinder(width, radius + twistRadius / 2); - const cyl = CrossSection.circle(decorRadius) - .translate([twistRadius, 0]) - .extrude(width, nDivision, 180); - const decor = cyl.scale([1, 0.5, 1]).translate([0, radius, 0]); + const circle = []; + const dPhiDeg = 180 / nDivision; + for (let i = 0; i < 2 * nDivision; ++i) { + circle.push([ + decorRadius * Math.cos(dPhiDeg * i * Math.PI / 180) + twistRadius, + decorRadius * Math.sin(dPhiDeg * i * Math.PI / 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++) b = b.add(decor.rotate([0, 0, (360.0 / nDecor) * i])); const stretch = []; diff --git a/bindings/wasm/examples/worker.test.js b/bindings/wasm/examples/worker.test.js index 4b870a230..aef3ccae7 100644 --- a/bindings/wasm/examples/worker.test.js +++ b/bindings/wasm/examples/worker.test.js @@ -90,8 +90,8 @@ suite('Examples', () => { test('Intro', async () => { const result = await runExample('Intro'); expect(result.genus).to.equal(5, 'Genus'); - expect(result.volume).to.be.closeTo(203164, 1, 'Volume'); - expect(result.surfaceArea).to.be.closeTo(62046, 1, 'Surface Area'); + expect(result.volume).to.be.closeTo(183836, 1, 'Volume'); + expect(result.surfaceArea).to.be.closeTo(59018, 1, 'Surface Area'); }); test('Tetrahedron Puzzle', async () => { From 2dc2b213234ac44d89b5763c47e69063ee88e35f Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 22:54:46 -0700 Subject: [PATCH 35/41] organizational comments --- bindings/wasm/manifold-encapsulated-types.d.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 2daf4291c..74e0366b3 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -54,10 +54,14 @@ export class CrossSection { */ constructor(polygons: Polygons, fillRule?: FillRule); + // Shapes + static square(size?: Vec2|number, center?: boolean): CrossSection; static circle(radius: number, circularSegments?: number): CrossSection; + // Extrusions + /** * Constructs a manifold by extruding the cross-section along Z-axis. * @@ -89,6 +93,8 @@ export class CrossSection { */ 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. @@ -189,6 +195,8 @@ export class CrossSection { */ simplify(epsilon?: number): CrossSection; + // Clipping Operations + /** * Boolean union */ @@ -245,6 +253,8 @@ export class CrossSection { */ rectClip(rect: Rect): CrossSection; + // Topological Operations + /** * Construct a CrossSection from a vector of other Polygons (batch * boolean union). @@ -258,6 +268,8 @@ export class CrossSection { */ 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 @@ -276,6 +288,8 @@ export class CrossSection { */ toPolygons(): SimplePolygon[]; + // Properties + /** * Return the total area covered by complex polygons making up the * CrossSection. @@ -303,6 +317,8 @@ export class CrossSection { */ bounds(): Rect; + // Memory + /** * Frees the WASM memory of this CrossSection, since these cannot be * garbage-collected automatically. From 11bc0c978e277c0486443b84561d60b6631fceef Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Mon, 29 May 2023 23:04:21 -0700 Subject: [PATCH 36/41] Revert show/only changes --- bindings/wasm/examples/worker.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index 6c791663c..dc7a8f019 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -24,8 +24,8 @@ import type {CrossSection, Manifold, ManifoldToplevel, Mesh, Vec3} from './publi interface WorkerStatic extends ManifoldToplevel { GLTFNode: typeof GLTFNode; - show(shape: CrossSection|Manifold): Manifold; - only(shape: CrossSection|Manifold): Manifold; + show(manifold: Manifold): Manifold; + only(manifold: Manifold): Manifold; setMaterial(manifold: Manifold, material: GLTFMaterial): void; cleanup(): void; } @@ -184,29 +184,19 @@ module.setMaterial = (manifold: Manifold, material: GLTFMaterial): Manifold => { return out; }; -function debug(shape: Manifold|CrossSection, map: Map) { - let manifold; - if (shape instanceof module.CrossSection) { - const box = shape.bounds(); - const x = box.max[0] - box.min[0]; - const y = box.max[1] - box.min[1]; - const h = Math.max(x, y) / 100; - manifold = shape.extrude(h).translate([0, 0, -h / 2]); - } else { - manifold = shape; - } +function debug(manifold: Manifold, map: Map) { let result = manifold.asOriginal(); map.set(result.originalID(), result.getMesh()); return result; }; -module.show = (shape) => { - return debug(shape, shown); +module.show = (manifold) => { + return debug(manifold, shown); }; -module.only = (shape) => { +module.only = (manifold) => { ghost = true; - return debug(shape, singles); + return debug(manifold, singles); }; // Setup complete From 5b05c42baad27c661f3dc215ce895f18ed8d7a6f Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Tue, 30 May 2023 11:38:58 -0700 Subject: [PATCH 37/41] Organizational comments --- .../wasm/manifold-encapsulated-types.d.ts | 100 +++++++++++------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 74e0366b3..685e4efa3 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -60,7 +60,7 @@ export class CrossSection { static circle(radius: number, circularSegments?: number): CrossSection; - // Extrusions + // Extrusions (2d to 3d manifold) /** * Constructs a manifold by extruding the cross-section along Z-axis. @@ -340,6 +340,8 @@ export class Manifold { */ 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. @@ -384,6 +386,8 @@ export class Manifold { */ 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. @@ -421,6 +425,8 @@ export class Manifold { 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 @@ -464,6 +470,8 @@ export class Manifold { */ 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 @@ -486,6 +494,8 @@ export class Manifold { 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. @@ -533,6 +543,47 @@ export class Manifold { */ mirror(v: Vec3): Manifold; + /** + * This function does not change the topology, but allows the vertices to be + * moved according to any arbitrary input function. It is easy to create a + * function that warps a geometrically valid object into one which overlaps, + * but that is not checked here, so it is up to the user to choose their + * function with discretion. + * + * @param warpFunc A function that modifies a given vertex position. + */ + warp(warpFunc: (vert: Vec3) => void): Manifold; + + /** + * Increase the density of the mesh by splitting every edge into n pieces. For + * instance, with n = 2, each triangle will be split into 4 triangles. These + * will all be coplanar (and will not be immediately collapsed) unless the + * Mesh/Manifold has halfedgeTangents specified (e.g. from the Smooth() + * constructor), in which case the new vertices will be moved to the + * interpolated surface according to their barycentric coordinates. + * + * @param n The number of pieces to split every edge into. Must be > 1. + */ + refine(n: number): Manifold; + + /** + * Create a new copy of this manifold with updated vertex properties by + * supplying a function that takes the existing position and properties as + * input. You may specify any number of output properties, allowing creation + * and removal of channels. Note: undefined behavior will result if you read + * past the number of input properties or write past the number of output + * properties. + * + * @param numProp The new number of properties per vertex. + * @param propFunc A function that modifies the properties of a given vertex. + */ + setProperties( + numProp: number, + propFunc: (newProp: number[], position: Vec3, oldProp: number[]) => void): + Manifold; + + // Boolean Operations + /** * Boolean union */ @@ -610,44 +661,7 @@ export class Manifold { */ trimByPlane(normal: Vec3, originOffset: 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 - * will all be coplanar (and will not be immediately collapsed) unless the - * Mesh/Manifold has halfedgeTangents specified (e.g. from the Smooth() - * constructor), in which case the new vertices will be moved to the - * interpolated surface according to their barycentric coordinates. - * - * @param n The number of pieces to split every edge into. Must be > 1. - */ - refine(n: number): Manifold; - - /** - * 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 - * input. You may specify any number of output properties, allowing creation - * and removal of channels. Note: undefined behavior will result if you read - * past the number of input properties or write past the number of output - * properties. - * - * @param numProp The new number of properties per vertex. - * @param propFunc A function that modifies the properties of a given vertex. - */ - setProperties( - numProp: number, - propFunc: (newProp: number[], position: Vec3, oldProp: number[]) => void): - Manifold; + // Topological Operations /** * Constructs a new manifold from a list of other manifolds. This is a purely @@ -666,6 +680,8 @@ export class Manifold { */ decompose(): Manifold[]; + // Property Access + /** * Does the Manifold have any triangles? */ @@ -727,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 @@ -742,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, @@ -771,6 +791,8 @@ export class Manifold { */ static reserveIDs(count: number): number; + // Memory + /** * Frees the WASM memory of this Manifold, since these cannot be * garbage-collected automatically. From 1612ff43cd8eb380415483ff31617b8694632152 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Tue, 30 May 2023 12:16:44 -0700 Subject: [PATCH 38/41] Fix vec to polygon conversion --- bindings/wasm/bindings.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bindings/wasm/bindings.js b/bindings/wasm/bindings.js index 22c05f4cc..0c4da85d5 100644 --- a/bindings/wasm/bindings.js +++ b/bindings/wasm/bindings.js @@ -39,10 +39,11 @@ Module.setup = function() { const result = []; const nPoly = vec.size(); for (let i = 0; i < nPoly; i++) { - const nPts = vec[i].size(); + const v = vec.get(i); + const nPts = v.size(); const poly = []; for (let j = 0; j < nPts; j++) { - poly.push(f(vec[i].get(j))); + poly.push(f(v.get(j))); } result.push(poly); } From 507b8448401ddb899be5cc6ecaaf78392b4da9e1 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Tue, 30 May 2023 12:17:17 -0700 Subject: [PATCH 39/41] add missing static to ofPolygons signature --- bindings/wasm/examples/worker.ts | 2 +- bindings/wasm/manifold-encapsulated-types.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/wasm/examples/worker.ts b/bindings/wasm/examples/worker.ts index dc7a8f019..e4da34e09 100644 --- a/bindings/wasm/examples/worker.ts +++ b/bindings/wasm/examples/worker.ts @@ -58,7 +58,7 @@ const crossSectionStaticFunctions = [ // CrossSection member functions (that return a new cross-section) const crossSectionMemberFunctions = [ 'add', 'subtract', 'intersect', 'rectClip', 'decompose', 'transform', - 'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset', 'toPolygons' + 'translate', 'rotate', 'scale', 'mirror', 'simplify', 'offset' ]; // top level functions that construct a new manifold/mesh const toplevelConstructors = ['show', 'only', 'setMaterial']; diff --git a/bindings/wasm/manifold-encapsulated-types.d.ts b/bindings/wasm/manifold-encapsulated-types.d.ts index 685e4efa3..b9acb7d08 100644 --- a/bindings/wasm/manifold-encapsulated-types.d.ts +++ b/bindings/wasm/manifold-encapsulated-types.d.ts @@ -281,7 +281,7 @@ export class CrossSection { * @param fillRule The filling rule used to interpret polygon sub-regions in * contours. */ - ofPolygons(polygons: Polygons, fillRule?: FillRule): CrossSection; + static ofPolygons(polygons: Polygons, fillRule?: FillRule): CrossSection; /** * Return the contours of this CrossSection as a list of simple polygons. From 0a3dfe1d9838dcb1bdafcb275cc1cd166db15acb Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Wed, 31 May 2023 22:36:16 -0700 Subject: [PATCH 40/41] Revert to old intro and drop unneeded decimal pts --- bindings/wasm/examples/public/examples.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/bindings/wasm/examples/public/examples.js b/bindings/wasm/examples/public/examples.js index 7fe0253b3..21c19c0a2 100644 --- a/bindings/wasm/examples/public/examples.js +++ b/bindings/wasm/examples/public/examples.js @@ -20,11 +20,8 @@ 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 {sphere} = Manifold; - const {square} = CrossSection; - const box = square([70, 70], true) - .offset(15, 'Round') - .extrude(100, 0, 0, [1, 1], true); + 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 // GLTFNode - see Menger Sponge and Gyroid Module examples. @@ -263,8 +260,8 @@ export const examples = { const m = linearSegments > 2 ? linearSegments : n * q * majorRadius / threadRadius; - const offset = 2. - const circle = CrossSection.circle(1., n).translate([offset, 0.]); + const offset = 2 + const circle = CrossSection.circle(1, n).translate([offset, 0]); const func = (v) => { const psi = q * Math.atan2(v[0], v[1]); From b4f260523362141e31b6dd80597cd15a9fd1c833 Mon Sep 17 00:00:00 2001 From: Geoff deRosenroll Date: Wed, 31 May 2023 22:57:29 -0700 Subject: [PATCH 41/41] missed intro example test reversion --- bindings/wasm/examples/worker.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/wasm/examples/worker.test.js b/bindings/wasm/examples/worker.test.js index aef3ccae7..4b870a230 100644 --- a/bindings/wasm/examples/worker.test.js +++ b/bindings/wasm/examples/worker.test.js @@ -90,8 +90,8 @@ suite('Examples', () => { test('Intro', async () => { const result = await runExample('Intro'); expect(result.genus).to.equal(5, 'Genus'); - expect(result.volume).to.be.closeTo(183836, 1, 'Volume'); - expect(result.surfaceArea).to.be.closeTo(59018, 1, 'Surface Area'); + expect(result.volume).to.be.closeTo(203164, 1, 'Volume'); + expect(result.surfaceArea).to.be.closeTo(62046, 1, 'Surface Area'); }); test('Tetrahedron Puzzle', async () => {