From 3948820a667bd1ec44a69283ccf538d9be13a983 Mon Sep 17 00:00:00 2001 From: Artur Sharafutdinov Date: Fri, 17 Jul 2026 21:25:35 +0200 Subject: [PATCH 1/4] WIP: Add JavaScript examples --- doxygen/examples/Collision.dox | 3 + doxygen/examples/CollisionPrecise.dox | 3 + doxygen/examples/CollisionSelf.dox | 3 + doxygen/examples/ExamplePointsToMesh.dox | 3 + doxygen/examples/FreeFormDeformation.dox | 3 + doxygen/examples/GlobalRegistration.dox | 2 + doxygen/examples/LaplacianExample.dox | 3 + doxygen/examples/MeshBoolean.dox | 7 +++ doxygen/examples/MeshDecimate.dox | 7 +++ doxygen/examples/MeshExport.dox | 3 + doxygen/examples/MeshFillHole.dox | 2 + doxygen/examples/MeshICP.dox | 2 + doxygen/examples/MeshLoadSave.dox | 3 + doxygen/examples/MeshModification.dox | 2 + doxygen/examples/MeshOffset.dox | 7 +++ doxygen/examples/MeshResolveDegenerations.dox | 2 + doxygen/examples/MeshStitchHole.dox | 2 + doxygen/examples/SignedDistance.dox | 3 + .../examples/SignedDistancePointToMesh.dox | 3 + doxygen/examples/SignedDistances.dox | 3 + examples/js-examples/.gitignore | 1 + examples/js-examples/Collision.dox.js | 25 ++++++++ examples/js-examples/CollisionPrecise.dox.js | 21 +++++++ examples/js-examples/CollisionSelf.dox.js | 14 +++++ .../js-examples/FreeFormDeformation.dox.js | 33 ++++++++++ .../js-examples/GlobalRegistration.dox.js | 63 +++++++++++++++++++ .../js-examples/LaplacianDeformation.dox.js | 49 +++++++++++++++ examples/js-examples/MeshBoolean.dox.js | 21 +++++++ examples/js-examples/MeshDecimate.dox.js | 26 ++++++++ examples/js-examples/MeshExport.dox.js | 18 ++++++ examples/js-examples/MeshFillHole.dox.js | 21 +++++++ .../js-examples/MeshFixDegeneracies.dox.js | 15 +++++ examples/js-examples/MeshICP.dox.js | 38 +++++++++++ examples/js-examples/MeshLoadSave.dox.js | 9 +++ examples/js-examples/MeshModification.dox.js | 20 ++++++ examples/js-examples/MeshOffset.dox.js | 22 +++++++ examples/js-examples/MeshStitchHole.dox.js | 25 ++++++++ examples/js-examples/PointsToMesh.dox.js | 14 +++++ examples/js-examples/SignedDistance.dox.js | 10 +++ .../SignedDistancePointToMesh.dox.js | 10 +++ examples/js-examples/SignedDistances.dox.js | 22 +++++++ examples/js-examples/package.json | 11 ++++ scripts/npm/index.d.mts | 25 +++++++- source/MRWasmModule/MRWasmBitSet.cpp | 4 ++ source/MRWasmModule/MRWasmMesh.cpp | 1 + source/MRWasmModule/MRWasmMeshCollide.cpp | 51 +++++++++++++++ .../MRWasmModule/MRWasmMeshCollidePrecise.cpp | 10 ++- source/MRWasmModule/MRWasmMeshFillHole.cpp | 12 +++- .../MRWasmModule/MRWasmMeshMeshDistance.cpp | 28 +++++++++ source/MRWasmModule/MRWasmMeshProject.cpp | 2 +- source/MRWasmModule/MRWasmMeshSubdivide.cpp | 33 ++++++++++ source/MRWasmModule/MRWasmMultiwayICP.cpp | 7 ++- source/MRWasmModule/MRWasmTorus.cpp | 7 +++ source/MRWasmModule/MRWasmVector.cpp | 4 +- 54 files changed, 731 insertions(+), 7 deletions(-) create mode 100644 examples/js-examples/.gitignore create mode 100644 examples/js-examples/Collision.dox.js create mode 100644 examples/js-examples/CollisionPrecise.dox.js create mode 100644 examples/js-examples/CollisionSelf.dox.js create mode 100644 examples/js-examples/FreeFormDeformation.dox.js create mode 100644 examples/js-examples/GlobalRegistration.dox.js create mode 100644 examples/js-examples/LaplacianDeformation.dox.js create mode 100644 examples/js-examples/MeshBoolean.dox.js create mode 100644 examples/js-examples/MeshDecimate.dox.js create mode 100644 examples/js-examples/MeshExport.dox.js create mode 100644 examples/js-examples/MeshFillHole.dox.js create mode 100644 examples/js-examples/MeshFixDegeneracies.dox.js create mode 100644 examples/js-examples/MeshICP.dox.js create mode 100644 examples/js-examples/MeshLoadSave.dox.js create mode 100644 examples/js-examples/MeshModification.dox.js create mode 100644 examples/js-examples/MeshOffset.dox.js create mode 100644 examples/js-examples/MeshStitchHole.dox.js create mode 100644 examples/js-examples/PointsToMesh.dox.js create mode 100644 examples/js-examples/SignedDistance.dox.js create mode 100644 examples/js-examples/SignedDistancePointToMesh.dox.js create mode 100644 examples/js-examples/SignedDistances.dox.js create mode 100644 examples/js-examples/package.json create mode 100644 source/MRWasmModule/MRWasmMeshSubdivide.cpp diff --git a/doxygen/examples/Collision.dox b/doxygen/examples/Collision.dox index 24aa992a84f5..ba25e3bc647c 100644 --- a/doxygen/examples/Collision.dox +++ b/doxygen/examples/Collision.dox @@ -17,6 +17,9 @@ Following code presents example of finding collision triangles of two meshes - C# \include Collision.dox.cs +- JavaScript +\include Collision.dox.js + */ diff --git a/doxygen/examples/CollisionPrecise.dox b/doxygen/examples/CollisionPrecise.dox index 4718cc7ed1af..084c89357611 100644 --- a/doxygen/examples/CollisionPrecise.dox +++ b/doxygen/examples/CollisionPrecise.dox @@ -17,6 +17,9 @@ Following code presents example of finding preceise collision triangles and edge - C# \include CollisionPrecise.dox.cs +- JavaScript +\include CollisionPrecise.dox.js + */ diff --git a/doxygen/examples/CollisionSelf.dox b/doxygen/examples/CollisionSelf.dox index 80d928a37b3c..f693ca02c112 100644 --- a/doxygen/examples/CollisionSelf.dox +++ b/doxygen/examples/CollisionSelf.dox @@ -17,6 +17,9 @@ Following code presents example of finding self-intersecting triangles of single - C# \include CollisionSelf.dox.cs +- JavaScript +\include CollisionSelf.dox.js + */ diff --git a/doxygen/examples/ExamplePointsToMesh.dox b/doxygen/examples/ExamplePointsToMesh.dox index 744f0240ffd0..dac4d34f66ef 100644 --- a/doxygen/examples/ExamplePointsToMesh.dox +++ b/doxygen/examples/ExamplePointsToMesh.dox @@ -14,6 +14,9 @@ Example of generating a triangulation from a pointcloud. - C \include PointsToMesh.dox.c +- JavaScript +\include PointsToMesh.dox.js + */ diff --git a/doxygen/examples/FreeFormDeformation.dox b/doxygen/examples/FreeFormDeformation.dox index f4ad4a29bc90..2686637bd9c3 100644 --- a/doxygen/examples/FreeFormDeformation.dox +++ b/doxygen/examples/FreeFormDeformation.dox @@ -19,6 +19,9 @@ Example of using Free Form deformation of the mesh - C# \include FreeFormDeformation.dox.cs +- JavaScript +\include FreeFormDeformation.dox.js + */ \ No newline at end of file diff --git a/doxygen/examples/GlobalRegistration.dox b/doxygen/examples/GlobalRegistration.dox index 9d3ad458d56d..a7baa7b58a47 100644 --- a/doxygen/examples/GlobalRegistration.dox +++ b/doxygen/examples/GlobalRegistration.dox @@ -13,6 +13,8 @@ Example of Global Registration \include GlobalRegistration.dox.c - C# \include GlobalRegistration.dox.cs +- JavaScript +\include GlobalRegistration.dox.js diff --git a/doxygen/examples/LaplacianExample.dox b/doxygen/examples/LaplacianExample.dox index 874bdfba11a4..2b5ac317e382 100644 --- a/doxygen/examples/LaplacianExample.dox +++ b/doxygen/examples/LaplacianExample.dox @@ -16,6 +16,9 @@ Example of using Laplacian deformer - C \include LaplacianDeformation.dox.c +- JavaScript +\include LaplacianDeformation.dox.js + */ \ No newline at end of file diff --git a/doxygen/examples/MeshBoolean.dox b/doxygen/examples/MeshBoolean.dox index 9c82c9c72f30..8749922cb6b3 100644 --- a/doxygen/examples/MeshBoolean.dox +++ b/doxygen/examples/MeshBoolean.dox @@ -33,6 +33,13 @@ Source meshes Boolean intersection \image html boolean_after.png +- JavaScript +\include MeshBoolean.dox.js +Source meshes +\image html boolean_before.png +Boolean intersection +\image html boolean_after.png + */ \ No newline at end of file diff --git a/doxygen/examples/MeshDecimate.dox b/doxygen/examples/MeshDecimate.dox index dce9352ff25c..d012c412c2f1 100644 --- a/doxygen/examples/MeshDecimate.dox +++ b/doxygen/examples/MeshDecimate.dox @@ -33,6 +33,13 @@ Source mesh After decimate \image html decimate_after.png +- JavaScript +\include MeshDecimate.dox.js +Source mesh +\image html decimate_before.png +After decimate +\image html decimate_after.png + */ \ No newline at end of file diff --git a/doxygen/examples/MeshExport.dox b/doxygen/examples/MeshExport.dox index c7d1137068dd..dc6b429d12f6 100644 --- a/doxygen/examples/MeshExport.dox +++ b/doxygen/examples/MeshExport.dox @@ -17,6 +17,9 @@ See a more relevant example \ref ExampleNumpy - C# \include MeshExport.dox.cs +- JavaScript +\include MeshExport.dox.js + */ \ No newline at end of file diff --git a/doxygen/examples/MeshFillHole.dox b/doxygen/examples/MeshFillHole.dox index f43a92721e89..9d4a28e11123 100644 --- a/doxygen/examples/MeshFillHole.dox +++ b/doxygen/examples/MeshFillHole.dox @@ -13,6 +13,8 @@ Example of filling holes \include MeshFillHole.dox.c - C# \include MeshFillHole.dox.cs +- JavaScript +\include MeshFillHole.dox.js diff --git a/doxygen/examples/MeshICP.dox b/doxygen/examples/MeshICP.dox index 4e720f3a10a9..c5ad2076d511 100644 --- a/doxygen/examples/MeshICP.dox +++ b/doxygen/examples/MeshICP.dox @@ -13,6 +13,8 @@ Example of mesh ICP (finding transformation to match objects) \include MeshICP.dox.c - C# \include MeshICP.dox.cs +- JavaScript +\include MeshICP.dox.js diff --git a/doxygen/examples/MeshLoadSave.dox b/doxygen/examples/MeshLoadSave.dox index df0ac3e10548..9a8f755278f9 100644 --- a/doxygen/examples/MeshLoadSave.dox +++ b/doxygen/examples/MeshLoadSave.dox @@ -20,6 +20,9 @@ Further examples won't check return values for sake of clarity - C# \include MeshLoadSave.dox.cs +- JavaScript +\include MeshLoadSave.dox.js + */ \ No newline at end of file diff --git a/doxygen/examples/MeshModification.dox b/doxygen/examples/MeshModification.dox index ecad4c0afdbf..1f89c0ebc358 100644 --- a/doxygen/examples/MeshModification.dox +++ b/doxygen/examples/MeshModification.dox @@ -11,6 +11,8 @@ Some examples of mesh modification are presented here \include MeshModification.dox.py - C \include MeshModification.dox.c +- JavaScript +\include MeshModification.dox.js diff --git a/doxygen/examples/MeshOffset.dox b/doxygen/examples/MeshOffset.dox index ba1080465436..684648c152dc 100644 --- a/doxygen/examples/MeshOffset.dox +++ b/doxygen/examples/MeshOffset.dox @@ -33,6 +33,13 @@ Source mesh After offset \image html offset_after.png +- JavaScript +\include MeshOffset.dox.js +Source mesh +\image html offset_before.png +After offset +\image html offset_after.png + */ \ No newline at end of file diff --git a/doxygen/examples/MeshResolveDegenerations.dox b/doxygen/examples/MeshResolveDegenerations.dox index ed8a9a55a638..159b7a706dd9 100644 --- a/doxygen/examples/MeshResolveDegenerations.dox +++ b/doxygen/examples/MeshResolveDegenerations.dox @@ -13,6 +13,8 @@ Example of fixing degenerated faces \include MeshFixDegeneracies.dox.c - C# \include MeshFixDegeneracies.dox.cs +- JavaScript +\include MeshFixDegeneracies.dox.js diff --git a/doxygen/examples/MeshStitchHole.dox b/doxygen/examples/MeshStitchHole.dox index 3c6e1ea3a599..db4e09580940 100644 --- a/doxygen/examples/MeshStitchHole.dox +++ b/doxygen/examples/MeshStitchHole.dox @@ -11,6 +11,8 @@ Example of stitching holes (connect two holes with a cylinder) \include MeshStitchHole.dox.py - C \include MeshStitchHole.dox.c +- JavaScript +\include MeshStitchHole.dox.js diff --git a/doxygen/examples/SignedDistance.dox b/doxygen/examples/SignedDistance.dox index c26a25ad01d2..37fc105f3417 100644 --- a/doxygen/examples/SignedDistance.dox +++ b/doxygen/examples/SignedDistance.dox @@ -16,6 +16,9 @@ Example of usage to get signed distance from mesh to mesh - C \include SignedDistance.dox.c +- JavaScript +\include SignedDistance.dox.js + */ \ No newline at end of file diff --git a/doxygen/examples/SignedDistancePointToMesh.dox b/doxygen/examples/SignedDistancePointToMesh.dox index ec5e89a2a6f5..b7a936c02947 100644 --- a/doxygen/examples/SignedDistancePointToMesh.dox +++ b/doxygen/examples/SignedDistancePointToMesh.dox @@ -14,6 +14,9 @@ Example of usage to get signed distance from point to mesh - C \include SignedDistancePointToMesh.dox.c +- JavaScript +\include SignedDistancePointToMesh.dox.js + */ \ No newline at end of file diff --git a/doxygen/examples/SignedDistances.dox b/doxygen/examples/SignedDistances.dox index 9c50cc2cfa15..0aa04f6e82c1 100644 --- a/doxygen/examples/SignedDistances.dox +++ b/doxygen/examples/SignedDistances.dox @@ -16,6 +16,9 @@ Example of usage to get signed distances from points of mesh to another referenc - C \include SignedDistances.dox.c +- JavaScript +\include SignedDistances.dox.js + */ \ No newline at end of file diff --git a/examples/js-examples/.gitignore b/examples/js-examples/.gitignore new file mode 100644 index 000000000000..c2658d7d1b31 --- /dev/null +++ b/examples/js-examples/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/examples/js-examples/Collision.dox.js b/examples/js-examples/Collision.dox.js new file mode 100644 index 000000000000..a78c228d2248 --- /dev/null +++ b/examples/js-examples/Collision.dox.js @@ -0,0 +1,25 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using meshA = ml.makeUVSphere(1.0, 16, 16); // make mesh A +using meshB = ml.makeUVSphere(1.0, 16, 16); // make mesh B +// shift mesh B for better demonstration +using shift = ml.AffineXf3f.translation({ x: 0.1, y: 0.1, z: 0.1 }); +meshB.transform(shift); + +// find each pair of colliding faces +const collidingFacePairs = ml.findCollidingTriangles(meshA, meshB); +for (const { aFace, bFace } of collidingFacePairs) + console.log(`${aFace} ${bFace}`); + +// find bitsets of colliding faces +const bitsets = ml.findCollidingTriangleBitsets(meshA, meshB); +using collidingFacesA = bitsets.a; +using collidingFacesB = bitsets.b; +console.log(collidingFacesA.count()); // number of colliding faces from mesh A +console.log(collidingFacesB.count()); // number of colliding faces from mesh B + +// fast check whether mesh A and mesh B collide +const isColliding = ml.findCollidingTriangles(meshA, meshB, true).length > 0; +console.log(isColliding); diff --git a/examples/js-examples/CollisionPrecise.dox.js b/examples/js-examples/CollisionPrecise.dox.js new file mode 100644 index 000000000000..254ed56e8464 --- /dev/null +++ b/examples/js-examples/CollisionPrecise.dox.js @@ -0,0 +1,21 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using meshA = ml.makeUVSphere(1.0, 16, 16); // make mesh A +using meshB = ml.makeUVSphere(1.0, 16, 16); // make mesh B +// shift mesh B for better demonstration +using shift = ml.AffineXf3f.translation({ x: 0.1, y: 0.1, z: 0.1 }); +meshB.transform(shift); + +// create converters to integer field (needed for absolute precision predicates) +using converters = ml.getVectorConverters(meshA, meshB); +// find each intersecting edge/triangle pair +using collidingFaceEdges = ml.findCollidingEdgeTrisPrecise(meshA, meshB, converters); +for (let i = 0; i < collidingFaceEdges.size(); ++i) { + using vet = collidingFaceEdges.get(i); + if (vet.isEdgeATriB()) + console.log(`edgeA: ${vet.edge}, triB: ${vet.tri()}`); + else + console.log(`triA: ${vet.tri()}, edgeB: ${vet.edge}`); +} diff --git a/examples/js-examples/CollisionSelf.dox.js b/examples/js-examples/CollisionSelf.dox.js new file mode 100644 index 000000000000..341b0e31c7ea --- /dev/null +++ b/examples/js-examples/CollisionSelf.dox.js @@ -0,0 +1,14 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using mesh = ml.makeTorusWithSelfIntersections(1.0, 0.1, 16, 16); // make torus with self-intersections + +// find self-intersecting face pairs (throws if the search is cancelled) +const selfCollidingPairs = ml.findSelfCollidingTriangles(mesh); +for (const { aFace, bFace } of selfCollidingPairs) + console.log(`${aFace} ${bFace}`); + +// find the union of self-intersecting faces +using selfCollidingFaces = ml.findSelfCollidingTrianglesBS(mesh); +console.log(selfCollidingFaces.count()); // number of self-intersecting faces diff --git a/examples/js-examples/FreeFormDeformation.dox.js b/examples/js-examples/FreeFormDeformation.dox.js new file mode 100644 index 000000000000..ac68b9143744 --- /dev/null +++ b/examples/js-examples/FreeFormDeformation.dox.js @@ -0,0 +1,33 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// Load mesh +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); + +// Construct the deformer on the mesh (the JS binding takes the mesh directly) +using ffDeformer = new ml.FreeFormDeformer(mesh); + +// Compute mesh bounding box +using box = mesh.computeBoundingBox(); + +// Init deformer with a 3x3x3 grid on the mesh box +ffDeformer.init({ x: 3, y: 3, z: 3 }, box); + +// Move some control points of the grid to the center +const center = box.center(); +ffDeformer.setRefGridPointPosition({ x: 1, y: 1, z: 0 }, center); +ffDeformer.setRefGridPointPosition({ x: 1, y: 1, z: 2 }, center); +ffDeformer.setRefGridPointPosition({ x: 0, y: 1, z: 1 }, center); +ffDeformer.setRefGridPointPosition({ x: 2, y: 1, z: 1 }, center); +ffDeformer.setRefGridPointPosition({ x: 1, y: 0, z: 1 }, center); +ffDeformer.setRefGridPointPosition({ x: 1, y: 2, z: 1 }, center); + +// Apply the deformation to the mesh vertices +ffDeformer.apply(); + +// Invalidate the mesh because of the external vertex changes +mesh.invalidateCaches(); + +// Save deformed mesh +ml.MeshSave.toAnySupportedFormat(mesh, 'deformed_mesh.stl'); diff --git a/examples/js-examples/GlobalRegistration.dox.js b/examples/js-examples/GlobalRegistration.dox.js new file mode 100644 index 000000000000..dfec8f58248e --- /dev/null +++ b/examples/js-examples/GlobalRegistration.dox.js @@ -0,0 +1,63 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// the global registration can be applied to meshes and point clouds; +// to keep the sample simple, we work with point clouds only +const inputPaths = ['cloud1.ply', 'cloud2.ply', 'cloud3.ply']; +const outputPath = 'registered.ply'; + +// a DisposableStack frees the per-input WASM handles (there is a variable number of them) at the +// end of the scope, in reverse order, the same way `using` frees a single handle +using stack = new DisposableStack(); + +// as ICP and MultiwayICP accept both meshes and point clouds, each input is wrapped in a +// MeshOrPointsXf; the wrapper references the source cloud, so the clouds must outlive it (the +// stack, declared first, is disposed last) +using identityXf = new ml.AffineXf3f(); +const inputs = []; +const objects = []; +let maxVolume = -Infinity; +let baseDiagonal = 0; +for (const path of inputPaths) { + const cloud = stack.use(ml.PointsLoad.fromAnySupportedFormat(path)); + inputs.push(cloud); + + using bbox = cloud.computeBoundingBox(); + const volume = bbox.volume(); + if (volume > maxVolume) { + maxVolume = volume; + baseDiagonal = bbox.diagonal(); + } + + const mop = stack.use(ml.MeshOrPoints.fromPoints(cloud)); + objects.push(stack.use(new ml.MeshOrPointsXf(mop, identityXf))); +} + +// set the sampling voxel size relative to the largest input +using samplingParams = new ml.MultiwayICPSamplingParameters(); +samplingParams.samplingVoxelSize = baseDiagonal * 0.03; + +using icp = new ml.MultiwayICP(objects, samplingParams); +using icpProps = new ml.ICPProperties(); +icp.setParams(icpProps); + +// gather statistics +icp.updateAllPointPairs(); +console.log(`Samples: ${icp.getNumSamples()}`); +console.log(`Active point pairs: ${icp.getNumActivePairs()}`); + +// one transformation per input object +const xfs = icp.calculateTransformations(); + +// apply each transformation to its cloud and merge everything into one output cloud +using output = new ml.PointCloud(); +for (let i = 0; i < inputs.length; ++i) { + using xf = xfs[i]; + using srcPoints = inputs[i].points; + const coords = srcPoints.toArray(); + for (let p = 0; p < coords.length; p += 3) + output.addPoint(xf.apply({ x: coords[p], y: coords[p + 1], z: coords[p + 2] })); +} + +ml.PointsSave.toAnySupportedFormat(output, outputPath); diff --git a/examples/js-examples/LaplacianDeformation.dox.js b/examples/js-examples/LaplacianDeformation.dox.js new file mode 100644 index 000000000000..f676588d7388 --- /dev/null +++ b/examples/js-examples/LaplacianDeformation.dox.js @@ -0,0 +1,49 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// Load mesh +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); + +// Construct deformer on the mesh vertices (it references the mesh, which is declared first so it +// outlives the deformer) +using deformer = new ml.Laplacian(mesh); + +// Find an area for the deformation anchor points +using topology = mesh.topology; +using validVerts = topology.getValidVerts(); +const ancV0 = validVerts.find_first(); +const ancV1 = validVerts.find_last(); + +// Mark the anchor points in the free area +using freeVerts = new ml.VertBitSet(); +freeVerts.resize(validVerts.size(), false); +freeVerts.set(ancV0, true); +freeVerts.set(ancV1, true); +// Expand the free area +using expandedFreeVerts = ml.expandVerts(topology, freeVerts, 5); + +// Initialize laplacian +deformer.init(expandedFreeVerts, ml.EdgeWeights.Cotan, ml.VertexMass.NeiArea, ml.RememberShape.Yes); + +using box = mesh.computeBoundingBox(); +const shiftAmount = box.diagonal() * 0.01; +using points = mesh.points; +// Fix the anchor vertices in the required position (shifted along the vertex normal) +for (const anchor of [ancV0, ancV1]) { + const p = points.get(anchor); + const n = mesh.normal(anchor); + deformer.fixVertex( + anchor, + { x: p.x + n.x * shiftAmount, y: p.y + n.y * shiftAmount, z: p.z + n.z * shiftAmount }, + true ); +} + +// Move the free vertices according to the anchor ones +deformer.apply(); + +// Invalidate the mesh because of the external vertex changes +mesh.invalidateCaches(); + +// Save the deformed mesh +ml.MeshSave.toAnySupportedFormat(mesh, 'deformed_mesh.stl'); diff --git a/examples/js-examples/MeshBoolean.dox.js b/examples/js-examples/MeshBoolean.dox.js new file mode 100644 index 000000000000..9adfb90cb435 --- /dev/null +++ b/examples/js-examples/MeshBoolean.dox.js @@ -0,0 +1,21 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// create first sphere with radius of 1 unit +using sphere1 = ml.makeUVSphere(1.0, 64, 64); + +// create second sphere (the JS bindings expose no cheap mesh copy, so regenerate an +// identical sphere) and move it in the X direction +using sphere2 = ml.makeUVSphere(1.0, 64, 64); +using xf = ml.AffineXf3f.translation({ x: 0.7, y: 0.0, z: 0.0 }); +sphere2.transform(xf); + +// perform boolean operation +using result = ml.boolean(sphere1, sphere2, ml.BooleanOperation.Intersection); +if (!result.valid()) + throw new Error(result.errorString); + +// save result to STL file +using resultMesh = result.mesh; +ml.MeshSave.toAnySupportedFormat(resultMesh, 'out_boolean.stl'); diff --git a/examples/js-examples/MeshDecimate.dox.js b/examples/js-examples/MeshDecimate.dox.js new file mode 100644 index 000000000000..bbed14dae228 --- /dev/null +++ b/examples/js-examples/MeshDecimate.dox.js @@ -0,0 +1,26 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// Load mesh +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); + +// Repack the mesh. +// Not necessary, but highly recommended to achieve the best performance in parallel processing. +mesh.pack(); + +// Setup decimate parameters +using settings = new ml.DecimateSettings(); + +// Decimation stop thresholds, you may specify one or both +settings.maxDeletedFaces = 1000; // Number of faces to be deleted +settings.maxError = 0.05; // Maximum error when decimation stops + +// Number of parts for simultaneous processing, greatly improves performance by cost of minor quality loss +settings.subdivideParts = 64; + +// Decimate mesh +ml.decimateMesh(mesh, settings); + +// Save result +ml.MeshSave.toAnySupportedFormat(mesh, 'decimated_mesh.stl'); diff --git a/examples/js-examples/MeshExport.dox.js b/examples/js-examples/MeshExport.dox.js new file mode 100644 index 000000000000..b648c1a695a1 --- /dev/null +++ b/examples/js-examples/MeshExport.dox.js @@ -0,0 +1,18 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// create some mesh +using mesh = ml.makeCube({ x: 1, y: 1, z: 1 }, { x: -0.5, y: -0.5, z: -0.5 }); + +// all point coordinates, as a flat Float32Array of x, y, z triples +using points = mesh.points; +const vertexCoordinates = points.toArray(); + +// triangle vertices, as a flat Uint32Array of triples of indices into the points array +using topology = mesh.topology; +using triangulation = topology.getTriangulation(); +const vertexTriples = triangulation.toArray(); + +// TODO: export in your format +console.log(`${vertexCoordinates.length / 3} vertices, ${vertexTriples.length / 3} triangles`); diff --git a/examples/js-examples/MeshFillHole.dox.js b/examples/js-examples/MeshFillHole.dox.js new file mode 100644 index 000000000000..5014c4c902f4 --- /dev/null +++ b/examples/js-examples/MeshFillHole.dox.js @@ -0,0 +1,21 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// Load mesh +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); + +// Find a single representative edge for each hole in the mesh +using topology = mesh.topology; +const holeEdges = topology.findHoleRepresentiveEdges(); + +// Setup filling parameters +using params = new ml.FillHoleParams(); +using metric = ml.getUniversalMetric(mesh); +params.metric = metric; + +// Fill all holes at once (ml.fillHole( mesh, edge, params ) fills a single hole) +ml.fillHoles(mesh, holeEdges, params); + +// Save result +ml.MeshSave.toAnySupportedFormat(mesh, 'filledMesh.stl'); diff --git a/examples/js-examples/MeshFixDegeneracies.dox.js b/examples/js-examples/MeshFixDegeneracies.dox.js new file mode 100644 index 000000000000..359ee9b22ee9 --- /dev/null +++ b/examples/js-examples/MeshFixDegeneracies.dox.js @@ -0,0 +1,15 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); + +// you can set various parameters for the resolving process; see the documentation for more info +using box = mesh.computeBoundingBox(); +using params = new ml.FixMeshDegeneraciesParams(); +params.maxDeviation = 1e-5 * box.diagonal(); +params.tinyEdgeLength = 1e-3; +ml.fixMeshDegeneracies(mesh, params); + +// Save result +ml.MeshSave.toAnySupportedFormat(mesh, 'fixedMesh.stl'); diff --git a/examples/js-examples/MeshICP.dox.js b/examples/js-examples/MeshICP.dox.js new file mode 100644 index 000000000000..852fdbea91a3 --- /dev/null +++ b/examples/js-examples/MeshICP.dox.js @@ -0,0 +1,38 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// Load meshes +using meshFloating = ml.MeshLoad.fromAnySupportedFormat('meshA.stl'); +using meshFixed = ml.MeshLoad.fromAnySupportedFormat('meshB.stl'); + +// Prepare ICP parameters +using box = meshFixed.computeBoundingBox(); +const diagonal = box.diagonal(); +const icpSamplingVoxelSize = diagonal * 0.01; // To sample points from object +using icpParams = new ml.ICPProperties(); +icpParams.distThresholdSq = (diagonal * 0.1) ** 2; // Use point pairs with maximum distance specified +icpParams.exitVal = diagonal * 0.003; // Stop when distance reached + +// Pair each mesh with an initial transformation (identity here). The JS ICP takes +// MeshOrPointsXf; the source meshes back these wrappers, and the reverse-order disposal +// of `using` guarantees the meshes outlive them. +using floatingMop = ml.MeshOrPoints.fromMesh(meshFloating); +using fixedMop = ml.MeshOrPoints.fromMesh(meshFixed); +using identityXf = new ml.AffineXf3f(); +using floating = new ml.MeshOrPointsXf(floatingMop, identityXf); +using fixed = new ml.MeshOrPointsXf(fixedMop, identityXf); + +// Calculate transformation +using icp = new ml.ICP(floating, fixed, icpSamplingVoxelSize); +icp.setParams(icpParams); +using xf = icp.calculateTransformation(); + +// Transform floating mesh +meshFloating.transform(xf); + +// Output information string +console.log(icp.getStatusInfo()); + +// Save result +ml.MeshSave.toAnySupportedFormat(meshFloating, 'meshA_icp.stl'); diff --git a/examples/js-examples/MeshLoadSave.dox.js b/examples/js-examples/MeshLoadSave.dox.js new file mode 100644 index 000000000000..d7822ea56c7d --- /dev/null +++ b/examples/js-examples/MeshLoadSave.dox.js @@ -0,0 +1,9 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// Load mesh +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); + +// Save mesh (the format is chosen from the file extension) +ml.MeshSave.toAnySupportedFormat(mesh, 'mesh.ply'); diff --git a/examples/js-examples/MeshModification.dox.js b/examples/js-examples/MeshModification.dox.js new file mode 100644 index 000000000000..78d10f7f25a8 --- /dev/null +++ b/examples/js-examples/MeshModification.dox.js @@ -0,0 +1,20 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using mesh = ml.makeTorus(1.0, 0.1, 16, 16); + +// Relax mesh (5 iterations) +using relaxParams = new ml.MeshRelaxParams(); +relaxParams.iterations = 5; +ml.relax(mesh, relaxParams); + +// Subdivide mesh +using subdivideSettings = new ml.SubdivideSettings(); +subdivideSettings.maxDeviationAfterFlip = 0.5; +ml.subdivideMesh(mesh, subdivideSettings); + +// Rotate mesh +using rotation = ml.Matrix3f.rotation({ x: 0, y: 0, z: 1 }, Math.PI * 0.5); +using rotationXf = ml.AffineXf3f.linear(rotation); +mesh.transform(rotationXf); diff --git a/examples/js-examples/MeshOffset.dox.js b/examples/js-examples/MeshOffset.dox.js new file mode 100644 index 000000000000..91f0c9552ab6 --- /dev/null +++ b/examples/js-examples/MeshOffset.dox.js @@ -0,0 +1,22 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// Create mesh +using mesh = ml.makeCube({ x: 1, y: 1, z: 1 }, { x: -0.5, y: -0.5, z: -0.5 }); + +// Setup parameters +using params = new ml.GeneralOffsetParameters(); +// calculate voxel size depending on desired accuracy and/or memory consumption +params.voxelSize = ml.suggestVoxelSize(mesh, 10000000.0); +using topology = mesh.topology; +if (ml.findRightBoundary(topology).length > 0) + params.signDetectionMode = ml.SignDetectionMode.HoleWindingRule; // use if you have holes in mesh + +// Make offset mesh +using box = mesh.computeBoundingBox(); +const offset = box.diagonal() * 0.1; +using result = ml.generalOffsetMesh(mesh, offset, params); + +// Save result +ml.MeshSave.toAnySupportedFormat(result, 'mesh_offset.stl'); diff --git a/examples/js-examples/MeshStitchHole.dox.js b/examples/js-examples/MeshStitchHole.dox.js new file mode 100644 index 000000000000..3370af4aceed --- /dev/null +++ b/examples/js-examples/MeshStitchHole.dox.js @@ -0,0 +1,25 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// Load meshes +using mesh = ml.MeshLoad.fromAnySupportedFormat('meshAwithHole.stl'); +using meshB = ml.MeshLoad.fromAnySupportedFormat('meshBwithHole.stl'); + +// Unite meshes +mesh.addMesh(meshB); + +// Find holes (expect that there are exactly 2 holes) +using topology = mesh.topology; +const edges = topology.findHoleRepresentiveEdges(); +if (edges.length !== 2) + throw new Error(`expected exactly 2 holes, found ${edges.length}`); + +// Connect two holes +using params = new ml.StitchHolesParams(); +using metric = ml.getUniversalMetric(mesh); +params.metric = metric; +ml.stitchHoles(mesh, edges[0], edges[1], params); + +// Save result +ml.MeshSave.toAnySupportedFormat(mesh, 'stitchedMesh.stl'); diff --git a/examples/js-examples/PointsToMesh.dox.js b/examples/js-examples/PointsToMesh.dox.js new file mode 100644 index 000000000000..f62633abe2de --- /dev/null +++ b/examples/js-examples/PointsToMesh.dox.js @@ -0,0 +1,14 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// load points +using pointCloud = ml.PointsLoad.fromAnySupportedFormat('Points.ply'); + +// triangulate (the JS binding requires a parameters object) +using params = new ml.TriangulationParameters(); +using mesh = ml.triangulatePointCloud(pointCloud, params); +if (!mesh) + throw new Error('triangulation was cancelled'); + +ml.MeshSave.toAnySupportedFormat(mesh, 'Mesh.ply'); diff --git a/examples/js-examples/SignedDistance.dox.js b/examples/js-examples/SignedDistance.dox.js new file mode 100644 index 000000000000..cfb2df358723 --- /dev/null +++ b/examples/js-examples/SignedDistance.dox.js @@ -0,0 +1,10 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using mesh1 = ml.MeshLoad.fromAnySupportedFormat('mesh1.ply'); +using mesh2 = ml.MeshLoad.fromAnySupportedFormat('mesh2.ply'); + +// unlike findSignedDistances, this returns the single minimal signed distance between two meshes +const dist = ml.findSignedDistanceFromMesh(mesh1, mesh2); +console.log(`Signed distance between meshes is ${dist.signedDist}`); diff --git a/examples/js-examples/SignedDistancePointToMesh.dox.js b/examples/js-examples/SignedDistancePointToMesh.dox.js new file mode 100644 index 000000000000..9f42d9dc22b3 --- /dev/null +++ b/examples/js-examples/SignedDistancePointToMesh.dox.js @@ -0,0 +1,10 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh1.ply'); +const point = { x: 1.5, y: 1.0, z: 0.5 }; + +const result = ml.findSignedDistanceFromPoint(point, mesh); +if (result) + console.log(`Signed distance from point to mesh ${result.dist}`); diff --git a/examples/js-examples/SignedDistances.dox.js b/examples/js-examples/SignedDistances.dox.js new file mode 100644 index 000000000000..0e45269ef9b3 --- /dev/null +++ b/examples/js-examples/SignedDistances.dox.js @@ -0,0 +1,22 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using refMesh = ml.MeshLoad.fromAnySupportedFormat('mesh1.ply'); +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh2.ply'); + +// distances between the points of the target mesh and the reference mesh +// (the JS binding requires a parameters object; its defaults match the C++ overload) +using params = new ml.MeshProjectionParameters(); +const vertDistances = ml.findSignedDistances(refMesh, mesh, params); + +// find the closest and farthest distances (a loop, not Math.min(...arr), because the +// typed array may be large enough to overflow the call stack when spread) +let closest = Infinity; +let farthest = -Infinity; +for (const d of vertDistances) { + if (d < closest) closest = d; + if (d > farthest) farthest = d; +} +console.log(`Distance between reference mesh and the closest point of target mesh is ${closest}`); +console.log(`Distance between reference mesh and the farthest point of target mesh is ${farthest}`); diff --git a/examples/js-examples/package.json b/examples/js-examples/package.json new file mode 100644 index 000000000000..782497ea0000 --- /dev/null +++ b/examples/js-examples/package.json @@ -0,0 +1,11 @@ +{ + "name": "meshlib-js-examples", + "private": true, + "type": "module", + "engines": { + "node": ">=24" + }, + "dependencies": { + "@meshinspector/meshlib": "*" + } +} diff --git a/scripts/npm/index.d.mts b/scripts/npm/index.d.mts index 9a1a423d21ae..8d13ca58332c 100644 --- a/scripts/npm/index.d.mts +++ b/scripts/npm/index.d.mts @@ -53,6 +53,19 @@ export interface SignedMeshProjectionResult { dist: number; } +export interface MeshMeshSignedDistanceResult { + a: PointOnFace; + b: PointOnFace; + /** value of the MeshMeshCollisionStatus enum */ + status: number; + signedDist: number; +} + +export interface FaceFace { + aFace: number; + bFace: number; +} + export interface MeshDistanceResult { a: PointOnFace; b: PointOnFace; @@ -147,8 +160,18 @@ type Converters = { // Top-level free functions that return / accept a raw `val`. type Functions = { findProjection( pt: Base.Vector3f, m: Base.Mesh ): MeshProjectionResult; - findSignedDistance( pt: Base.Vector3f, m: Base.Mesh ): SignedMeshProjectionResult | null; + // Overloaded in C++ by operand type; the JS bindings take the operand suffix (cf. + // makeConvexHullFromMesh / getIncidentVertsFromFaces). + findSignedDistanceFromPoint( pt: Base.Vector3f, m: Base.Mesh ): SignedMeshProjectionResult | null; + findSignedDistanceFromMesh( a: Base.Mesh, b: Base.Mesh ): MeshMeshSignedDistanceResult; findDistance( a: Base.Mesh, b: Base.Mesh ): MeshDistanceResult; + findCollidingTriangles: { + ( a: Base.Mesh, b: Base.Mesh ): FaceFace[]; + ( a: Base.Mesh, b: Base.Mesh, firstIntersectionOnly: boolean ): FaceFace[]; + }; + findCollidingTriangleBitsets( a: Base.Mesh, b: Base.Mesh ): { a: Base.FaceBitSet; b: Base.FaceBitSet }; + findSelfCollidingTriangles( mp: Base.Mesh ): FaceFace[]; + findSelfCollidingTrianglesBS( mp: Base.Mesh ): Base.FaceBitSet; cutMesh( mesh: Base.Mesh, contours: Base.OneMeshContours, params: Base.CutMeshParameters ): CutMeshResult; cutMeshByProjection( mesh: Base.Mesh, contours: readonly Float32Array[], settings: Base.CutByProjectionSettings ): Uint32Array[]; findRightBoundary( topology: Base.MeshTopology ): Uint32Array[]; diff --git a/source/MRWasmModule/MRWasmBitSet.cpp b/source/MRWasmModule/MRWasmBitSet.cpp index aba68164f331..b7287816e2ae 100644 --- a/source/MRWasmModule/MRWasmBitSet.cpp +++ b/source/MRWasmModule/MRWasmBitSet.cpp @@ -55,6 +55,10 @@ EMSCRIPTEN_BINDINGS( meshlib_bitset ) .function( "count", &BitSet::count ) .function( "empty", &BitSet::empty ) .function( "test", &BitSet::test ) + .function( "set", +[]( BitSet& bs, int n, bool val ) { bs.set( BitSet::IndexType( n ), val ); } ) + .function( "resize", +[]( BitSet& bs, int numBits, bool fillValue ) { bs.resize( BitSet::size_type( numBits ), fillValue ); } ) + .function( "find_first", +[]( const BitSet& bs ) { return (int)bs.find_first(); } ) + .function( "find_last", +[]( const BitSet& bs ) { return (int)bs.find_last(); } ) .function( "toIndices", &bitSetToIndices ) .class_function( "fromIndices", &bitSetFromIndices ); diff --git a/source/MRWasmModule/MRWasmMesh.cpp b/source/MRWasmModule/MRWasmMesh.cpp index df2fe868d486..61a9cb6ac393 100644 --- a/source/MRWasmModule/MRWasmMesh.cpp +++ b/source/MRWasmModule/MRWasmMesh.cpp @@ -32,6 +32,7 @@ EMSCRIPTEN_BINDINGS( meshlib_mesh ) .function( "invalidateCaches", +[]( Mesh& m ) { m.invalidateCaches(); } ) .function( "transform", +[]( Mesh& m, const AffineXf3f& xf ) { m.transform( xf ); } ) .function( "averageEdgeLength", +[]( const Mesh& m ) { return m.averageEdgeLength(); } ) + .function( "normal", +[]( const Mesh& m, int v ) { return m.normal( VertId( v ) ); } ) .function( "findCenterFromPoints", +[]( const Mesh& m ) { return m.findCenterFromPoints(); } ) .function( "findCenterFromFaces", +[]( const Mesh& m ) { return m.findCenterFromFaces(); } ) .function( "findCenterFromBBox", +[]( const Mesh& m ) { return m.findCenterFromBBox(); } ) diff --git a/source/MRWasmModule/MRWasmMeshCollide.cpp b/source/MRWasmModule/MRWasmMeshCollide.cpp index 23fd3da2279c..f10b5ea6baac 100644 --- a/source/MRWasmModule/MRWasmMeshCollide.cpp +++ b/source/MRWasmModule/MRWasmMeshCollide.cpp @@ -3,17 +3,68 @@ #include "MRMesh/MRMeshCollide.h" #include "MRMesh/MRMesh.h" #include "MRMesh/MRMeshPart.h" +#include "MRMesh/MRFaceFace.h" +#include "MRMesh/MRBitSet.h" #include +#include #include +#include using namespace MR; +namespace +{ + +emscripten::val faceFacesToArray( const std::vector& pairs ) +{ + emscripten::val arr = emscripten::val::array(); + for ( const FaceFace& ff : pairs ) + { + emscripten::val o = emscripten::val::object(); + o.set( "aFace", (int)ff.aFace ); + o.set( "bFace", (int)ff.bFace ); + arr.call( "push", o ); + } + return arr; +} + +} + EMSCRIPTEN_BINDINGS( meshlib_mesh_collide ) { emscripten::function( "isInside", +[]( std::shared_ptr a, std::shared_ptr b ) { return isInside( *a, *b ); } ); + + emscripten::function( "findCollidingTriangles", +[]( std::shared_ptr a, std::shared_ptr b ) + { + return faceFacesToArray( findCollidingTriangles( *a, *b ) ); + } ); + + emscripten::function( "findCollidingTriangles", +[]( std::shared_ptr a, std::shared_ptr b, bool firstIntersectionOnly ) + { + return faceFacesToArray( findCollidingTriangles( *a, *b, nullptr, firstIntersectionOnly ) ); + } ); + + emscripten::function( "findCollidingTriangleBitsets", +[]( std::shared_ptr a, std::shared_ptr b ) + { + auto bitsets = findCollidingTriangleBitsets( *a, *b ); + emscripten::val out = emscripten::val::object(); + out.set( "a", bitsets.first ); + out.set( "b", bitsets.second ); + return out; + } ); + + emscripten::function( "findSelfCollidingTriangles", +[]( std::shared_ptr mp ) + { + return faceFacesToArray( Wasm::unwrap( findSelfCollidingTriangles( *mp ) ) ); + } ); + + emscripten::function( "findSelfCollidingTrianglesBS", +[]( std::shared_ptr mp ) + { + return Wasm::unwrap( findSelfCollidingTrianglesBS( *mp ) ); + } ); } diff --git a/source/MRWasmModule/MRWasmMeshCollidePrecise.cpp b/source/MRWasmModule/MRWasmMeshCollidePrecise.cpp index 6eab456d59bb..dce36e161dc8 100644 --- a/source/MRWasmModule/MRWasmMeshCollidePrecise.cpp +++ b/source/MRWasmModule/MRWasmMeshCollidePrecise.cpp @@ -10,8 +10,14 @@ using namespace MR; EMSCRIPTEN_BINDINGS( meshlib_mesh_collide_precise ) { - // Opaque carrier: std::vector; produced here, consumed by orderIntersectionContours. - emscripten::class_( "PreciseCollisionResult" ); + emscripten::class_( "VarEdgeTri" ) + .property( "edge", +[]( const VarEdgeTri& v ) { return (int)v.edge; } ) + .function( "tri", +[]( const VarEdgeTri& v ) { return (int)v.tri(); } ) + .function( "isEdgeATriB", +[]( const VarEdgeTri& v ) { return v.isEdgeATriB(); } ); + + emscripten::class_( "PreciseCollisionResult" ) + .function( "size", +[]( const PreciseCollisionResult& r ) { return (int)r.size(); } ) + .function( "get", +[]( const PreciseCollisionResult& r, int i ) { return r[i]; } ); emscripten::function( "getVectorConverters", +[]( const Mesh& a, const Mesh& b ) { diff --git a/source/MRWasmModule/MRWasmMeshFillHole.cpp b/source/MRWasmModule/MRWasmMeshFillHole.cpp index 11f55c632f4e..9bb6a2c21b46 100644 --- a/source/MRWasmModule/MRWasmMeshFillHole.cpp +++ b/source/MRWasmModule/MRWasmMeshFillHole.cpp @@ -24,7 +24,12 @@ EMSCRIPTEN_BINDINGS( meshlib_mesh_fill_hole ) .property( "smoothBd", &FillHoleParams::smoothBd ) .property( "multipleEdgesResolveMode", &FillHoleParams::multipleEdgesResolveMode ) .property( "makeDegenerateBand", &FillHoleParams::makeDegenerateBand ) - .property( "maxPolygonSubdivisions", &FillHoleParams::maxPolygonSubdivisions ); + .property( "maxPolygonSubdivisions", &FillHoleParams::maxPolygonSubdivisions ) + .property( "metric", &FillHoleParams::metric ); + + emscripten::class_( "StitchHolesParams" ) + .constructor<>() + .property( "metric", &StitchHolesParams::metric ); emscripten::function( "fillHole", +[]( std::shared_ptr mesh, int a, const FillHoleParams& params ) { @@ -39,4 +44,9 @@ EMSCRIPTEN_BINDINGS( meshlib_mesh_fill_hole ) es[i] = EdgeId( as[ i ].as() ); fillHoles( *mesh, es, params ); } ); + + emscripten::function( "stitchHoles", +[]( std::shared_ptr mesh, int a, int b, const StitchHolesParams& params ) + { + stitchHoles( *mesh, EdgeId( a ), EdgeId( b ), params ); + } ); } diff --git a/source/MRWasmModule/MRWasmMeshMeshDistance.cpp b/source/MRWasmModule/MRWasmMeshMeshDistance.cpp index 30f6b54816fb..73f7ea224185 100644 --- a/source/MRWasmModule/MRWasmMeshMeshDistance.cpp +++ b/source/MRWasmModule/MRWasmMeshMeshDistance.cpp @@ -14,6 +14,15 @@ using namespace MR; EMSCRIPTEN_BINDINGS( meshlib_mesh_mesh_distance ) { + emscripten::enum_( "MeshMeshCollisionStatus" ) + .value( "BothOutside", MeshMeshCollisionStatus::BothOutside ) + .value( "BothInside", MeshMeshCollisionStatus::BothInside ) + .value( "AInside", MeshMeshCollisionStatus::AInside ) + .value( "BInside", MeshMeshCollisionStatus::BInside ) + .value( "Colliding", MeshMeshCollisionStatus::Colliding ) + .value( "Touching", MeshMeshCollisionStatus::Touching ) + .value( "NotColliding", MeshMeshCollisionStatus::NotColliding ); + emscripten::function( "findDistance", +[]( std::shared_ptr a, std::shared_ptr b ) { const MeshMeshDistanceResult r = findDistance( *a, *b ); @@ -31,4 +40,23 @@ EMSCRIPTEN_BINDINGS( meshlib_mesh_mesh_distance ) out.set( "distSq", r.distSq ); return out; } ); + + emscripten::function( "findSignedDistanceFromMesh", +[]( std::shared_ptr a, std::shared_ptr b ) + { + const MeshMeshSignedDistanceResult r = findSignedDistance( *a, *b ); + + emscripten::val va = emscripten::val::object(); + va.set( "face", (int)r.a.face ); + va.set( "point", r.a.point ); + emscripten::val vb = emscripten::val::object(); + vb.set( "face", (int)r.b.face ); + vb.set( "point", r.b.point ); + + emscripten::val out = emscripten::val::object(); + out.set( "a", va ); + out.set( "b", vb ); + out.set( "status", (int)r.status ); + out.set( "signedDist", r.signedDist ); + return out; + } ); } diff --git a/source/MRWasmModule/MRWasmMeshProject.cpp b/source/MRWasmModule/MRWasmMeshProject.cpp index 52bbc0a5f4ff..37eb21adeeb5 100644 --- a/source/MRWasmModule/MRWasmMeshProject.cpp +++ b/source/MRWasmModule/MRWasmMeshProject.cpp @@ -38,7 +38,7 @@ EMSCRIPTEN_BINDINGS( meshlib_mesh_project ) return out; } ); - emscripten::function( "findSignedDistance", +[]( const Vector3f& pt, std::shared_ptr mp ) + emscripten::function( "findSignedDistanceFromPoint", +[]( const Vector3f& pt, std::shared_ptr mp ) { const auto res = findSignedDistance( pt, *mp ); if ( !res ) diff --git a/source/MRWasmModule/MRWasmMeshSubdivide.cpp b/source/MRWasmModule/MRWasmMeshSubdivide.cpp new file mode 100644 index 000000000000..6bc25035adb1 --- /dev/null +++ b/source/MRWasmModule/MRWasmMeshSubdivide.cpp @@ -0,0 +1,33 @@ +#include "MRWasmBindings.h" + +#include "MRMesh/MRMesh.h" +#include "MRMesh/MRMeshSubdivide.h" + +#include + +#include + +using namespace MR; + +EMSCRIPTEN_BINDINGS( meshlib_subdivide ) +{ + emscripten::class_( "SubdivideSettings" ) + .constructor<>() + .property( "maxEdgeLen", &SubdivideSettings::maxEdgeLen ) + .property( "curvaturePriority", &SubdivideSettings::curvaturePriority ) + .property( "maxEdgeSplits", &SubdivideSettings::maxEdgeSplits ) + .property( "maxDeviationAfterFlip", &SubdivideSettings::maxDeviationAfterFlip ) + .property( "maxAngleChangeAfterFlip", &SubdivideSettings::maxAngleChangeAfterFlip ) + .property( "criticalAspectRatioFlip", &SubdivideSettings::criticalAspectRatioFlip ) + .property( "subdivideBorder", &SubdivideSettings::subdivideBorder ) + .property( "maxTriAspectRatio", &SubdivideSettings::maxTriAspectRatio ) + .property( "maxSplittableTriAspectRatio", &SubdivideSettings::maxSplittableTriAspectRatio ) + .property( "smoothMode", &SubdivideSettings::smoothMode ) + .property( "minSharpDihedralAngle", &SubdivideSettings::minSharpDihedralAngle ) + .property( "projectOnOriginalMesh", &SubdivideSettings::projectOnOriginalMesh ); + + emscripten::function( "subdivideMesh", +[]( std::shared_ptr mesh, const SubdivideSettings& settings ) + { + return subdivideMesh( *mesh, settings ); + } ); +} diff --git a/source/MRWasmModule/MRWasmMultiwayICP.cpp b/source/MRWasmModule/MRWasmMultiwayICP.cpp index 81fbb042033a..0db6ac6e14b6 100644 --- a/source/MRWasmModule/MRWasmMultiwayICP.cpp +++ b/source/MRWasmModule/MRWasmMultiwayICP.cpp @@ -7,6 +7,7 @@ #include "MRMesh/MRVector.h" #include +#include using namespace MR; @@ -34,7 +35,11 @@ EMSCRIPTEN_BINDINGS( meshlib_multiway_icp ) }, emscripten::allow_raw_pointers() ) .function( "calculateTransformations", +[]( MultiwayICP& self ) { - return Wasm::packedToTypedArray, float, 12>( self.calculateTransformations() ); + const auto xfs = self.calculateTransformations(); + emscripten::val arr = emscripten::val::array(); + for ( const auto& xf : xfs ) + arr.call( "push", xf ); + return arr; } ) .function( "resamplePoints", &MultiwayICP::resamplePoints ) .function( "updateAllPointPairs", +[]( MultiwayICP& self ) { return self.updateAllPointPairs(); } ) diff --git a/source/MRWasmModule/MRWasmTorus.cpp b/source/MRWasmModule/MRWasmTorus.cpp index 31a669f1a282..45e843404de3 100644 --- a/source/MRWasmModule/MRWasmTorus.cpp +++ b/source/MRWasmModule/MRWasmTorus.cpp @@ -17,4 +17,11 @@ EMSCRIPTEN_BINDINGS( meshlib_torus ) return std::make_shared( makeTorus( primaryRadius, secondaryRadius, primaryResolution, secondaryResolution ) ); } ); + + emscripten::function( "makeTorusWithSelfIntersections", + +[]( float primaryRadius, float secondaryRadius, int primaryResolution, int secondaryResolution ) + { + return std::make_shared( + makeTorusWithSelfIntersections( primaryRadius, secondaryRadius, primaryResolution, secondaryResolution ) ); + } ); } diff --git a/source/MRWasmModule/MRWasmVector.cpp b/source/MRWasmModule/MRWasmVector.cpp index 9be8660211db..bff1e9eb179b 100644 --- a/source/MRWasmModule/MRWasmVector.cpp +++ b/source/MRWasmModule/MRWasmVector.cpp @@ -16,7 +16,9 @@ EMSCRIPTEN_BINDINGS( meshlib_vector ) { emscripten::class_( "VertCoords" ) .class_function( "fromArray", &Wasm::packedFromTypedArray ) - .function( "toArray", &Wasm::packedToTypedArray ); + .function( "toArray", &Wasm::packedToTypedArray ) + .function( "size", +[]( const VertCoords& v ) { return (int)v.size(); } ) + .function( "get", +[]( const VertCoords& v, int i ) { return v[VertId( i )]; } ); emscripten::class_( "Triangulation" ) .class_function( "fromArray", &Wasm::packedFromTypedArray ) From 3c38e3f499aea2041f8a6b514d6776e3c00431bf Mon Sep 17 00:00:00 2001 From: Artur Sharafutdinov Date: Sat, 18 Jul 2026 12:38:28 +0200 Subject: [PATCH 2/4] Update tests --- source/MRWasmTest/bitset.test.mjs | 19 ++++++ source/MRWasmTest/collisions.test.mjs | 79 ++++++++++++++++++++++++ source/MRWasmTest/fillHole.test.mjs | 51 +++++++++++++++ source/MRWasmTest/icp.test.mjs | 6 +- source/MRWasmTest/index.mjs | 1 + source/MRWasmTest/makers.test.mjs | 7 +++ source/MRWasmTest/meshAccessors.test.mjs | 18 ++++++ source/MRWasmTest/meshQueries.test.mjs | 20 +++++- source/MRWasmTest/processing.test.mjs | 16 +++++ 9 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 source/MRWasmTest/collisions.test.mjs diff --git a/source/MRWasmTest/bitset.test.mjs b/source/MRWasmTest/bitset.test.mjs index 1363fba8dad9..2be8da90f8f3 100644 --- a/source/MRWasmTest/bitset.test.mjs +++ b/source/MRWasmTest/bitset.test.mjs @@ -33,3 +33,22 @@ import { ml, cube, meshFromGeometry } from './helpers.mjs'; assert.deepEqual( Array.from( fbs.toIndices() ), [ 1, 3, 5 ], 'round-trips the indices' ); fbs.delete(); } + +// set / resize / find_first / find_last +{ + const bs = new ml.VertBitSet(); + bs.resize( 10, false ); + assert.equal( bs.size(), 10, 'resize sets the size' ); + assert.equal( bs.count(), 0, 'resize(…, false) leaves every bit clear' ); + + bs.set( 3, true ); + bs.set( 7, true ); + assert.equal( bs.count(), 2, 'two bits set' ); + assert.equal( bs.find_first(), 3, 'find_first returns the lowest set bit' ); + assert.equal( bs.find_last(), 7, 'find_last returns the highest set bit' ); + + bs.set( 3, false ); + assert.equal( bs.find_first(), 7, 'clearing bit 3 advances find_first to 7' ); + + bs.delete(); +} diff --git a/source/MRWasmTest/collisions.test.mjs b/source/MRWasmTest/collisions.test.mjs new file mode 100644 index 000000000000..93bb4681096b --- /dev/null +++ b/source/MRWasmTest/collisions.test.mjs @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import { ml } from './helpers.mjs'; + +// findCollidingTriangles / findCollidingTriangleBitsets — two overlapping spheres +{ + const a = ml.makeUVSphere( 1, 16, 16 ); + const b = ml.makeUVSphere( 1, 16, 16 ); + const shift = ml.AffineXf3f.translation( { x: 0.5, y: 0, z: 0 } ); + b.transform( shift ); + + const pairs = ml.findCollidingTriangles( a, b ); + assert.ok( pairs.length > 0, 'overlapping spheres have colliding triangle pairs' ); + assert.ok( Number.isInteger( pairs[ 0 ].aFace ) && Number.isInteger( pairs[ 0 ].bFace ), + 'each pair carries aFace / bFace face indices' ); + + const bitsets = ml.findCollidingTriangleBitsets( a, b ); + assert.ok( bitsets.a.count() > 0 && bitsets.b.count() > 0, + 'bitsets mark colliding faces on both meshes' ); + bitsets.a.delete(); + bitsets.b.delete(); + + // fast path: firstIntersectionOnly still reports the collision + assert.ok( ml.findCollidingTriangles( a, b, true ).length > 0, 'firstIntersectionOnly reports a hit' ); + + shift.delete(); + a.delete(); + b.delete(); +} + +// no collision — far-apart spheres +{ + const a = ml.makeUVSphere( 1, 16, 16 ); + const b = ml.makeUVSphere( 1, 16, 16 ); + const shift = ml.AffineXf3f.translation( { x: 5, y: 0, z: 0 } ); + b.transform( shift ); + + assert.equal( ml.findCollidingTriangles( a, b ).length, 0, 'far-apart spheres do not collide' ); + + shift.delete(); + a.delete(); + b.delete(); +} + +// self-collisions — a deliberately self-intersecting torus +{ + const torus = ml.makeTorusWithSelfIntersections( 1, 0.1, 16, 16 ); + + const pairs = ml.findSelfCollidingTriangles( torus ); + assert.ok( pairs.length > 0, 'the self-intersecting torus reports self-colliding pairs' ); + + const bs = ml.findSelfCollidingTrianglesBS( torus ); + assert.ok( bs.count() > 0, 'the self-intersecting faces form a non-empty set' ); + bs.delete(); + + torus.delete(); +} + +// precise collisions — enumerable VarEdgeTri entries +{ + const a = ml.makeUVSphere( 1, 16, 16 ); + const b = ml.makeUVSphere( 1, 16, 16 ); + const shift = ml.AffineXf3f.translation( { x: 0.5, y: 0, z: 0 } ); + b.transform( shift ); + + const conv = ml.getVectorConverters( a, b ); + const res = ml.findCollidingEdgeTrisPrecise( a, b, conv ); + assert.ok( res.size() > 0, 'precise collision finds edge/triangle intersections' ); + + const vet = res.get( 0 ); + assert.equal( typeof vet.isEdgeATriB(), 'boolean', 'VarEdgeTri.isEdgeATriB() is a boolean' ); + assert.ok( Number.isInteger( vet.edge ) && Number.isInteger( vet.tri() ), 'VarEdgeTri exposes edge and tri()' ); + vet.delete(); + res.delete(); + conv.delete(); + + shift.delete(); + a.delete(); + b.delete(); +} diff --git a/source/MRWasmTest/fillHole.test.mjs b/source/MRWasmTest/fillHole.test.mjs index 97e61dd82bda..8e32b7e2a00f 100644 --- a/source/MRWasmTest/fillHole.test.mjs +++ b/source/MRWasmTest/fillHole.test.mjs @@ -44,3 +44,54 @@ import { ml, cube, meshFromGeometry } from './helpers.mjs'; metric.delete(); m.delete(); } + +// FillHoleParams.metric accepts a FillHoleMetric +{ + const c = cube( 0, 0, 0, 2 ); + const idx = Array.from( c.indices ); + const holed = new Uint32Array( [ ...idx.slice( 0, 6 ), ...idx.slice( 12 ) ] ); + const m = meshFromGeometry( c.positions, holed ); + + const topo = m.topology; + const holeEdges = topo.findHoleRepresentiveEdges(); + topo.delete(); + + const params = new ml.FillHoleParams(); + const metric = ml.getUniversalMetric( m ); + params.metric = metric; + ml.fillHole( m, holeEdges[ 0 ], params ); + metric.delete(); + params.delete(); + + const topo2 = m.topology; + assert.equal( topo2.findNumHoles(), 0, 'the hole is filled using the universal metric' ); + topo2.delete(); + m.delete(); +} + +// stitchHoles — connect the two open ends of a tube with a cylindrical band +{ + const c = cube( 0, 0, 0, 2 ); + const idx = Array.from( c.indices ); + // keep only the four side faces (drop the -z face 0..5 and the +z face 6..11): an open tube + const tube = new Uint32Array( idx.slice( 12 ) ); + const m = meshFromGeometry( c.positions, tube ); + + let topo = m.topology; + assert.equal( topo.findNumHoles(), 2, 'the open tube has two holes' ); + const holeEdges = topo.findHoleRepresentiveEdges(); + assert.equal( holeEdges.length, 2, 'two hole representative edges' ); + topo.delete(); + + const params = new ml.StitchHolesParams(); + const metric = ml.getUniversalMetric( m ); + params.metric = metric; + ml.stitchHoles( m, holeEdges[ 0 ], holeEdges[ 1 ], params ); + metric.delete(); + params.delete(); + + topo = m.topology; + assert.equal( topo.findNumHoles(), 0, 'stitching connects the two holes (none remain)' ); + topo.delete(); + m.delete(); +} diff --git a/source/MRWasmTest/icp.test.mjs b/source/MRWasmTest/icp.test.mjs index 1f990a559b59..547d85abae8a 100644 --- a/source/MRWasmTest/icp.test.mjs +++ b/source/MRWasmTest/icp.test.mjs @@ -75,11 +75,13 @@ import { ml } from './helpers.mjs'; params.samplingVoxelSize = 0.1; const mw = new ml.MultiwayICP( objs, params ); - const results = mw.calculateTransformations(); // Float32Array, 12 floats (Matrix3f A + Vector3f b) per object - assert.equal( results.length, offsets.length * 12, 'a 12-float transform per object' ); + const results = mw.calculateTransformations(); // one AffineXf3f per object + assert.equal( results.length, offsets.length, 'one transform per object' ); + assert.ok( results[0].A !== undefined && results[0].b !== undefined, 'each result is an AffineXf3f' ); assert.ok( mw.getNumSamples() > 0, 'MultiwayICP formed sample pairs' ); assert.ok( mw.getMeanSqDistToPoint() < 0.1, 'MultiwayICP converged' ); + for ( const r of results ) r.delete(); mw.delete(); params.delete(); for ( const o of objs ) o.delete(); diff --git a/source/MRWasmTest/index.mjs b/source/MRWasmTest/index.mjs index dc3a18ac4a0d..65f3c904d0e5 100644 --- a/source/MRWasmTest/index.mjs +++ b/source/MRWasmTest/index.mjs @@ -13,6 +13,7 @@ import './expandShrink.test.mjs'; import './meshProject.test.mjs'; import './meshAccessors.test.mjs'; import './meshQueries.test.mjs'; +import './collisions.test.mjs'; import './maps.test.mjs'; import './booleanMapper.test.mjs'; import './fillHole.test.mjs'; diff --git a/source/MRWasmTest/makers.test.mjs b/source/MRWasmTest/makers.test.mjs index 9219d69c4cb0..5380db3052de 100644 --- a/source/MRWasmTest/makers.test.mjs +++ b/source/MRWasmTest/makers.test.mjs @@ -42,6 +42,13 @@ import { ml, meshToGeometry } from './helpers.mjs'; m.delete(); } +{ + const m = ml.makeTorusWithSelfIntersections( 1, 0.1, 16, 16 ); + const g = meshToGeometry( m, false ); + assert.ok( g.positions.length > 0 && g.indices.length > 0, 'self-intersecting torus is non-empty' ); + m.delete(); +} + { const c = ml.makeCube( { x: 2, y: 2, z: 2 }, { x: -1, y: -1, z: -1 } ); const hull = ml.makeConvexHullFromMesh( c ); diff --git a/source/MRWasmTest/meshAccessors.test.mjs b/source/MRWasmTest/meshAccessors.test.mjs index 8b3f856f0925..f1c0f66e8a72 100644 --- a/source/MRWasmTest/meshAccessors.test.mjs +++ b/source/MRWasmTest/meshAccessors.test.mjs @@ -44,3 +44,21 @@ import { ml, cube, meshFromGeometry } from './helpers.mjs'; topo.delete(); m.delete(); } + +// Mesh.normal and VertCoords.get / size +{ + const c = cube( 0, 0, 0, 2 ); + const m = meshFromGeometry( c.positions, c.indices ); + const points = m.points; + + assert.equal( points.size(), 8, 'a size-2 cube has 8 vertices' ); + const p0 = points.get( 0 ); + assert.ok( Math.abs( p0.x + 1 ) < 1e-4 && Math.abs( p0.y + 1 ) < 1e-4 && Math.abs( p0.z + 1 ) < 1e-4, + 'vertex 0 is the (-1,-1,-1) corner' ); + + const n = m.normal( 0 ); + assert.ok( Math.abs( Math.hypot( n.x, n.y, n.z ) - 1 ) < 1e-3, 'the vertex normal is unit length' ); + + points.delete(); + m.delete(); +} diff --git a/source/MRWasmTest/meshQueries.test.mjs b/source/MRWasmTest/meshQueries.test.mjs index 82e9a53d97d1..2a84a199d947 100644 --- a/source/MRWasmTest/meshQueries.test.mjs +++ b/source/MRWasmTest/meshQueries.test.mjs @@ -31,22 +31,36 @@ const eq = ( a, b ) => Math.abs( a - b ) < 1e-3; big.delete(); } -// findSignedDistance — sign flips inside vs outside +// findSignedDistanceFromPoint — sign flips inside vs outside { const c = cube( 0, 0, 0, 2 ); const m = meshFromGeometry( c.positions, c.indices ); - const outside = ml.findSignedDistance( { x: 5, y: 0, z: 0 }, m ); + const outside = ml.findSignedDistanceFromPoint( { x: 5, y: 0, z: 0 }, m ); assert.notEqual( outside, null, 'a projection exists' ); assert.ok( eq( outside.dist, 4 ), 'point (5,0,0) is 4 outside the +x face' ); assert.ok( eq( outside.proj.point.x, 1 ), 'closest point on the +x face' ); - const inside = ml.findSignedDistance( { x: 0, y: 0, z: 0 }, m ); + const inside = ml.findSignedDistanceFromPoint( { x: 0, y: 0, z: 0 }, m ); assert.ok( inside.dist < 0 && eq( inside.dist, -1 ), 'the centre is 1 unit inside (negative distance)' ); m.delete(); } +// findSignedDistanceFromMesh — signed distance between two separated cubes +{ + const ca = cube( 0, 0, 0, 2 ), cb = cube( 5, 0, 0, 2 ); + const a = meshFromGeometry( ca.positions, ca.indices ); + const b = meshFromGeometry( cb.positions, cb.indices ); + + const r = ml.findSignedDistanceFromMesh( a, b ); + assert.ok( eq( r.signedDist, 3 ), 'the [-1,1] and [4,6] cubes are 3 apart (positive: not colliding)' ); + assert.ok( Number.isInteger( r.status ), 'a MeshMeshCollisionStatus value is returned' ); + + a.delete(); + b.delete(); +} + // findSignedDistances — verts of a mesh lie on its own surface { const c = cube( 0, 0, 0, 2 ); diff --git a/source/MRWasmTest/processing.test.mjs b/source/MRWasmTest/processing.test.mjs index 24fc08be7d2d..00522e707d63 100644 --- a/source/MRWasmTest/processing.test.mjs +++ b/source/MRWasmTest/processing.test.mjs @@ -43,3 +43,19 @@ import { ml, cube, meshFromGeometry, meshToGeometry } from './helpers.mjs'; p.delete(); m.delete(); } + +{ + const c = cube( 0, 0, 0, 2 ); + const m = meshFromGeometry( c.positions, c.indices ); + const before = meshToGeometry( m, false ).indices.length / 3; + + const s = new ml.SubdivideSettings(); + s.maxEdgeLen = 0.5; + s.maxEdgeSplits = 10000; + const splits = ml.subdivideMesh( m, s ); + assert.ok( splits > 0, 'subdivideMesh performs edge splits' ); + assert.ok( meshToGeometry( m, false ).indices.length / 3 > before, 'subdivideMesh increases the face count' ); + + s.delete(); + m.delete(); +} From 8610534825c904688ee58f4d1869c5afab2ce739 Mon Sep 17 00:00:00 2001 From: Artur Sharafutdinov Date: Sat, 18 Jul 2026 22:33:39 +0200 Subject: [PATCH 3/4] Add more examples --- doxygen/examples/ExampleFusion.dox | 3 ++ doxygen/examples/MeshExtrude.dox | 2 + doxygen/examples/MeshOffsetWeighted.dox | 3 ++ doxygen/examples/NoiseDenoiseExample.dox | 3 ++ doxygen/examples/Triangulation.dox | 3 ++ examples/js-examples/Fusion.dox.js | 15 +++++++ examples/js-examples/MeshExtrude.dox.js | 27 ++++++++++++ .../js-examples/MeshOffsetWeighted.dox.js | 30 +++++++++++++ examples/js-examples/NoiseDenoise.dox.js | 18 ++++++++ examples/js-examples/Triangulation.dox.js | 36 +++++++++++++++ source/MRWasmModule/MRWasmAddNoise.cpp | 23 ++++++++++ .../MRWasmClosestWeightedPoint.cpp | 17 +++++++ source/MRWasmModule/MRWasmMesh.cpp | 4 +- source/MRWasmModule/MRWasmMeshExtrude.cpp | 19 ++++++++ source/MRWasmModule/MRWasmNormalDenoising.cpp | 33 ++++++++++++++ source/MRWasmModule/MRWasmPointCloud.cpp | 7 ++- .../MRWasmModule/MRWasmPointCloudRadius.cpp | 20 +++++++++ .../MRWasmModule/MRWasmPointsToMeshFusion.cpp | 27 ++++++++++++ source/MRWasmModule/MRWasmUniformSampling.cpp | 29 ++++++++++++ source/MRWasmModule/MRWasmVector.cpp | 7 ++- .../MRWasmWeightedPointsShell.cpp | 34 ++++++++++++++ source/MRWasmTest/denoise.test.mjs | 28 ++++++++++++ source/MRWasmTest/extrude.test.mjs | 44 +++++++++++++++++++ source/MRWasmTest/fusion.test.mjs | 27 ++++++++++++ source/MRWasmTest/index.mjs | 4 ++ source/MRWasmTest/pointCloud.test.mjs | 26 +++++++++++ source/MRWasmTest/weightedShell.test.mjs | 43 ++++++++++++++++++ 27 files changed, 528 insertions(+), 4 deletions(-) create mode 100644 examples/js-examples/Fusion.dox.js create mode 100644 examples/js-examples/MeshExtrude.dox.js create mode 100644 examples/js-examples/MeshOffsetWeighted.dox.js create mode 100644 examples/js-examples/NoiseDenoise.dox.js create mode 100644 examples/js-examples/Triangulation.dox.js create mode 100644 source/MRWasmModule/MRWasmAddNoise.cpp create mode 100644 source/MRWasmModule/MRWasmClosestWeightedPoint.cpp create mode 100644 source/MRWasmModule/MRWasmMeshExtrude.cpp create mode 100644 source/MRWasmModule/MRWasmNormalDenoising.cpp create mode 100644 source/MRWasmModule/MRWasmPointCloudRadius.cpp create mode 100644 source/MRWasmModule/MRWasmPointsToMeshFusion.cpp create mode 100644 source/MRWasmModule/MRWasmUniformSampling.cpp create mode 100644 source/MRWasmModule/MRWasmWeightedPointsShell.cpp create mode 100644 source/MRWasmTest/denoise.test.mjs create mode 100644 source/MRWasmTest/extrude.test.mjs create mode 100644 source/MRWasmTest/fusion.test.mjs create mode 100644 source/MRWasmTest/weightedShell.test.mjs diff --git a/doxygen/examples/ExampleFusion.dox b/doxygen/examples/ExampleFusion.dox index 6299d29431f4..23c825c13aa7 100644 --- a/doxygen/examples/ExampleFusion.dox +++ b/doxygen/examples/ExampleFusion.dox @@ -14,6 +14,9 @@ Example of Fusion. - C \include Fusion.dox.c +- JavaScript +\include Fusion.dox.js + */ diff --git a/doxygen/examples/MeshExtrude.dox b/doxygen/examples/MeshExtrude.dox index 792d21a05284..22d8ad696afa 100644 --- a/doxygen/examples/MeshExtrude.dox +++ b/doxygen/examples/MeshExtrude.dox @@ -11,6 +11,8 @@ Example of extrude faces on mesh \include MeshExtrude.dox.py - C \include MeshExtrude.dox.c +- JavaScript +\include MeshExtrude.dox.js diff --git a/doxygen/examples/MeshOffsetWeighted.dox b/doxygen/examples/MeshOffsetWeighted.dox index b3a25b84e716..202061b3143b 100644 --- a/doxygen/examples/MeshOffsetWeighted.dox +++ b/doxygen/examples/MeshOffsetWeighted.dox @@ -14,6 +14,9 @@ Example of mesh weighted offset - C \include MeshOffsetWeighted.dox.c +- JavaScript +\include MeshOffsetWeighted.dox.js + */ \ No newline at end of file diff --git a/doxygen/examples/NoiseDenoiseExample.dox b/doxygen/examples/NoiseDenoiseExample.dox index efd45b87d7ea..e1f7825888bc 100644 --- a/doxygen/examples/NoiseDenoiseExample.dox +++ b/doxygen/examples/NoiseDenoiseExample.dox @@ -16,6 +16,9 @@ - C \include NoiseDenoise.dox.c +- JavaScript +\include NoiseDenoise.dox.js + */ \ No newline at end of file diff --git a/doxygen/examples/Triangulation.dox b/doxygen/examples/Triangulation.dox index 4d0543128c74..55693feac79c 100644 --- a/doxygen/examples/Triangulation.dox +++ b/doxygen/examples/Triangulation.dox @@ -17,6 +17,9 @@ Simple triangulation - C \include Triangulation.dox.c +- JavaScript +\include Triangulation.dox.js + */ \ No newline at end of file diff --git a/examples/js-examples/Fusion.dox.js b/examples/js-examples/Fusion.dox.js new file mode 100644 index 000000000000..f0146027ca4d --- /dev/null +++ b/examples/js-examples/Fusion.dox.js @@ -0,0 +1,15 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using pointCloud = ml.PointsLoad.fromAnySupportedFormat('Points.ply'); + +using bbox = pointCloud.computeBoundingBox(); +using params = new ml.PointsToMeshParameters(); +params.voxelSize = bbox.diagonal() * 0.01; +params.sigma = Math.max(params.voxelSize, ml.findAvgPointsRadius(pointCloud, 50)); +params.minWeight = 1; + +using mesh = ml.pointsToMeshFusion(pointCloud, params); + +ml.MeshSave.toAnySupportedFormat(mesh, 'Mesh.ply'); diff --git a/examples/js-examples/MeshExtrude.dox.js b/examples/js-examples/MeshExtrude.dox.js new file mode 100644 index 000000000000..37716c179025 --- /dev/null +++ b/examples/js-examples/MeshExtrude.dox.js @@ -0,0 +1,27 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); + +// select the faces to extrude +using facesToExtrude = ml.FaceBitSet.fromIndices(new Uint32Array([1, 2])); + +// create duplicated vertices along the region boundary +ml.makeDegenerateBandAroundRegion(mesh, facesToExtrude); + +// find the vertices to move and shift them along +Z +using topology = mesh.topology; +using vertsForMove = ml.getIncidentVertsFromFaces(topology, facesToExtrude); +using coords = mesh.points; +for (const v of vertsForMove.toIndices()) { + const p = coords.get(v); + p.z += 1; + coords.set(v, p); +} +mesh.points = coords; + +// invalidate internal caches after the manual vertex changes +mesh.invalidateCaches(); + +ml.MeshSave.toAnySupportedFormat(mesh, 'extrudedMesh.stl'); diff --git a/examples/js-examples/MeshOffsetWeighted.dox.js b/examples/js-examples/MeshOffsetWeighted.dox.js new file mode 100644 index 000000000000..7f286f8f867e --- /dev/null +++ b/examples/js-examples/MeshOffsetWeighted.dox.js @@ -0,0 +1,30 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using mesh = ml.makeTorus(1.0, 0.1, 16, 16); + +// build a per-vertex weight (extra offset) from each vertex's x coordinate +using coords = mesh.points; +const positions = coords.toArray(); +const weights = new Float32Array(positions.length / 3); +let maxWeight = 0; +for (let v = 0; v < weights.length; ++v) { + weights[v] = Math.abs(positions[3 * v] / 5); + maxWeight = Math.max(maxWeight, weights[v]); +} +using scalars = ml.VertScalars.fromArray(weights); + +using params = new ml.WeightedShellParametersMetric(); +// the algorithm is voxel-based; the voxel size affects performance and the result shape +params.voxelSize = ml.suggestVoxelSize(mesh, 1000); +// a common basic offset applied to every vertex; the weighted offsets are added on top +params.offset = 0.2; +// the maximum weight must be an upper bound of the provided weights +using dist = params.dist; +dist.maxWeight = maxWeight; +params.dist = dist; + +using result = ml.WeightedShell.meshShell(mesh, scalars, params); + +ml.MeshSave.toAnySupportedFormat(result, 'offset_weighted.ply'); diff --git a/examples/js-examples/NoiseDenoise.dox.js b/examples/js-examples/NoiseDenoise.dox.js new file mode 100644 index 000000000000..1baf2a35ce08 --- /dev/null +++ b/examples/js-examples/NoiseDenoise.dox.js @@ -0,0 +1,18 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); + +// add noise to the mesh, scaled to its bounding box +using bbox = mesh.computeBoundingBox(); +using noiseSettings = new ml.NoiseSettings(); +noiseSettings.sigma = bbox.diagonal() * 0.0001; +ml.addNoise(mesh, noiseSettings); +mesh.invalidateCaches(); +ml.MeshSave.toAnySupportedFormat(mesh, 'noised_mesh.stl'); + +// denoise the mesh, keeping sharp edges sharp +// see the article "Mesh Denoising via a Novel Mumford-Shah Framework" +ml.meshDenoiseViaNormals(mesh); +ml.MeshSave.toAnySupportedFormat(mesh, 'denoised_mesh.stl'); diff --git a/examples/js-examples/Triangulation.dox.js b/examples/js-examples/Triangulation.dox.js new file mode 100644 index 000000000000..1dd0b0ebcbb1 --- /dev/null +++ b/examples/js-examples/Triangulation.dox.js @@ -0,0 +1,36 @@ +import createMeshLib from '@meshinspector/meshlib'; + +const ml = await createMeshLib(); + +// generate a point cloud sampling a unit sphere +using pointCloud = new ml.PointCloud(); +const resolution = 100; +for (let i = 0; i < resolution; ++i) { + const u = 2 * Math.PI * i / (resolution - 1); + for (let j = 0; j < resolution; ++j) { + const v = Math.PI * j / (resolution - 1); + pointCloud.addPoint({ + x: Math.cos(u) * Math.sin(v), + y: Math.sin(u) * Math.sin(v), + z: Math.cos(v), + }); + } +} + +// remove the points that are too close (e.g. the duplicated poles) +using samplingSettings = new ml.UniformSamplingSettings(); +samplingSettings.distance = 1e-3; +using samples = ml.pointUniformSampling(pointCloud, samplingSettings); +pointCloud.validPoints = samples; +pointCloud.invalidateCaches(); + +// triangulate the sampled cloud +using triangulationParams = new ml.TriangulationParameters(); +using triangulated = ml.triangulatePointCloud(pointCloud, triangulationParams); + +// fix possible issues with a zero offset +using offsetParams = new ml.OffsetParameters(); +offsetParams.voxelSize = ml.suggestVoxelSize(triangulated, 5e6); +using mesh = ml.offsetMesh(triangulated, 0, offsetParams); + +ml.MeshSave.toAnySupportedFormat(mesh, 'result.stl'); diff --git a/source/MRWasmModule/MRWasmAddNoise.cpp b/source/MRWasmModule/MRWasmAddNoise.cpp new file mode 100644 index 000000000000..8436cec22868 --- /dev/null +++ b/source/MRWasmModule/MRWasmAddNoise.cpp @@ -0,0 +1,23 @@ +#include "MRWasmBindings.h" + +#include "MRMesh/MRAddNoise.h" +#include "MRMesh/MRMesh.h" + +#include + +#include + +using namespace MR; + +EMSCRIPTEN_BINDINGS( meshlib_add_noise ) +{ + emscripten::class_( "NoiseSettings" ) + .constructor<>() + .property( "sigma", &NoiseSettings::sigma ) + .property( "seed", &NoiseSettings::seed ); + + emscripten::function( "addNoise", +[]( std::shared_ptr mesh, const NoiseSettings& settings ) + { + Wasm::unwrap( addNoise( *mesh, nullptr, settings ) ); + } ); +} diff --git a/source/MRWasmModule/MRWasmClosestWeightedPoint.cpp b/source/MRWasmModule/MRWasmClosestWeightedPoint.cpp new file mode 100644 index 000000000000..486ca3b32139 --- /dev/null +++ b/source/MRWasmModule/MRWasmClosestWeightedPoint.cpp @@ -0,0 +1,17 @@ +#include "MRWasmBindings.h" + +#include "MRMesh/MRClosestWeightedPoint.h" + +#include + +using namespace MR; + +EMSCRIPTEN_BINDINGS( meshlib_closest_weighted_point ) +{ + emscripten::class_( "DistanceFromWeightedPointsParams" ) + .constructor<>() + .property( "minWeight", &DistanceFromWeightedPointsParams::minWeight ) + .property( "maxWeight", &DistanceFromWeightedPointsParams::maxWeight ) + .property( "maxWeightGrad", &DistanceFromWeightedPointsParams::maxWeightGrad ) + .property( "bidirectionalMode", &DistanceFromWeightedPointsParams::bidirectionalMode ); +} diff --git a/source/MRWasmModule/MRWasmMesh.cpp b/source/MRWasmModule/MRWasmMesh.cpp index 61a9cb6ac393..13d47c078bb3 100644 --- a/source/MRWasmModule/MRWasmMesh.cpp +++ b/source/MRWasmModule/MRWasmMesh.cpp @@ -19,7 +19,9 @@ EMSCRIPTEN_BINDINGS( meshlib_mesh ) { emscripten::class_( "Mesh" ) .smart_ptr>( "MeshPtr" ) - .property( "points", +[]( const Mesh& m ) { return m.points; } ) + .property( "points", + +[]( const Mesh& m ) { return m.points; }, + +[]( Mesh& m, const VertCoords& c ) { m.points = c; } ) .property( "topology", +[]( const Mesh& m ) { return m.topology; } ) .class_function( "fromTriangles", +[]( const VertCoords& vertexCoordinates, const Triangulation& t ) { diff --git a/source/MRWasmModule/MRWasmMeshExtrude.cpp b/source/MRWasmModule/MRWasmMeshExtrude.cpp new file mode 100644 index 000000000000..140b22a0bcae --- /dev/null +++ b/source/MRWasmModule/MRWasmMeshExtrude.cpp @@ -0,0 +1,19 @@ +#include "MRWasmBindings.h" + +#include "MRMesh/MRMeshExtrude.h" +#include "MRMesh/MRMesh.h" +#include "MRMesh/MRBitSet.h" + +#include + +#include + +using namespace MR; + +EMSCRIPTEN_BINDINGS( meshlib_mesh_extrude ) +{ + emscripten::function( "makeDegenerateBandAroundRegion", +[]( std::shared_ptr mesh, const FaceBitSet& region ) + { + makeDegenerateBandAroundRegion( *mesh, region ); + } ); +} diff --git a/source/MRWasmModule/MRWasmNormalDenoising.cpp b/source/MRWasmModule/MRWasmNormalDenoising.cpp new file mode 100644 index 000000000000..d00ba8af894c --- /dev/null +++ b/source/MRWasmModule/MRWasmNormalDenoising.cpp @@ -0,0 +1,33 @@ +#include "MRWasmBindings.h" + +#include "MRMesh/MRNormalDenoising.h" +#include "MRMesh/MRMesh.h" + +#include + +#include + +using namespace MR; + +EMSCRIPTEN_BINDINGS( meshlib_normal_denoising ) +{ + emscripten::class_( "DenoiseViaNormalsSettings" ) + .constructor<>() + .property( "fastIndicatorComputation", &DenoiseViaNormalsSettings::fastIndicatorComputation ) + .property( "beta", &DenoiseViaNormalsSettings::beta ) + .property( "gamma", &DenoiseViaNormalsSettings::gamma ) + .property( "normalIters", &DenoiseViaNormalsSettings::normalIters ) + .property( "pointIters", &DenoiseViaNormalsSettings::pointIters ) + .property( "guideWeight", &DenoiseViaNormalsSettings::guideWeight ) + .property( "limitNearInitial", &DenoiseViaNormalsSettings::limitNearInitial ) + .property( "maxInitialDist", &DenoiseViaNormalsSettings::maxInitialDist ); + + emscripten::function( "meshDenoiseViaNormals", +[]( std::shared_ptr mesh ) + { + Wasm::unwrap( meshDenoiseViaNormals( *mesh ) ); + } ); + emscripten::function( "meshDenoiseViaNormals", +[]( std::shared_ptr mesh, const DenoiseViaNormalsSettings& settings ) + { + Wasm::unwrap( meshDenoiseViaNormals( *mesh, settings ) ); + } ); +} diff --git a/source/MRWasmModule/MRWasmPointCloud.cpp b/source/MRWasmModule/MRWasmPointCloud.cpp index 9b24bd81fdf1..2391618853b2 100644 --- a/source/MRWasmModule/MRWasmPointCloud.cpp +++ b/source/MRWasmModule/MRWasmPointCloud.cpp @@ -17,7 +17,10 @@ EMSCRIPTEN_BINDINGS( meshlib_point_cloud ) .constructor<>() .property( "points", +[]( const PointCloud& pc ) { return pc.points; } ) .property( "normals", +[]( const PointCloud& pc ) { return pc.normals; } ) - .property( "validPoints", +[]( const PointCloud& pc ) { return pc.validPoints; } ) + .property( "validPoints", + +[]( const PointCloud& pc ) { return pc.validPoints; }, + +[]( PointCloud& pc, const VertBitSet& bs ) { pc.validPoints = bs; } ) .function( "addPoint", +[]( PointCloud& pc, const Vector3f& point ) { return (int)pc.addPoint( point ); } ) - .function( "computeBoundingBox", +[]( const PointCloud& pc ) { return pc.computeBoundingBox(); } ); + .function( "computeBoundingBox", +[]( const PointCloud& pc ) { return pc.computeBoundingBox(); } ) + .function( "invalidateCaches", +[]( PointCloud& pc ) { pc.invalidateCaches(); } ); } diff --git a/source/MRWasmModule/MRWasmPointCloudRadius.cpp b/source/MRWasmModule/MRWasmPointCloudRadius.cpp new file mode 100644 index 000000000000..dbe3992f75f3 --- /dev/null +++ b/source/MRWasmModule/MRWasmPointCloudRadius.cpp @@ -0,0 +1,20 @@ +#include "MRWasmBindings.h" + +#include "MRMesh/MRPointCloudRadius.h" +#include "MRMesh/MRPointCloud.h" + +#include + +using namespace MR; + +EMSCRIPTEN_BINDINGS( meshlib_point_cloud_radius ) +{ + emscripten::function( "findAvgPointsRadius", +[]( const PointCloud& pointCloud, int avgPoints ) + { + return findAvgPointsRadius( pointCloud, avgPoints ); + } ); + emscripten::function( "findAvgPointsRadius", +[]( const PointCloud& pointCloud, int avgPoints, int samples ) + { + return findAvgPointsRadius( pointCloud, avgPoints, samples ); + } ); +} diff --git a/source/MRWasmModule/MRWasmPointsToMeshFusion.cpp b/source/MRWasmModule/MRWasmPointsToMeshFusion.cpp new file mode 100644 index 000000000000..d4fc04e69f97 --- /dev/null +++ b/source/MRWasmModule/MRWasmPointsToMeshFusion.cpp @@ -0,0 +1,27 @@ +#include "MRWasmBindings.h" + +#include "MRVoxels/MRPointsToMeshFusion.h" +#include "MRMesh/MRPointCloud.h" +#include "MRMesh/MRMesh.h" + +#include + +#include + +using namespace MR; + +EMSCRIPTEN_BINDINGS( meshlib_points_to_mesh_fusion ) +{ + emscripten::class_( "PointsToMeshParameters" ) + .constructor<>() + .property( "sigma", &PointsToMeshParameters::sigma ) + .property( "minWeight", &PointsToMeshParameters::minWeight ) + .property( "invSigmaModifier", &PointsToMeshParameters::invSigmaModifier ) + .property( "sqrtAngleWeight", &PointsToMeshParameters::sqrtAngleWeight ) + .property( "voxelSize", &PointsToMeshParameters::voxelSize ); + + emscripten::function( "pointsToMeshFusion", +[]( const PointCloud& cloud, const PointsToMeshParameters& params ) + { + return std::make_shared( Wasm::unwrap( pointsToMeshFusion( cloud, params ) ) ); + } ); +} diff --git a/source/MRWasmModule/MRWasmUniformSampling.cpp b/source/MRWasmModule/MRWasmUniformSampling.cpp new file mode 100644 index 000000000000..7f57c2361ea8 --- /dev/null +++ b/source/MRWasmModule/MRWasmUniformSampling.cpp @@ -0,0 +1,29 @@ +#include "MRWasmBindings.h" + +#include "MRMesh/MRUniformSampling.h" +#include "MRMesh/MRPointCloud.h" +#include "MRMesh/MRBitSet.h" + +#include + +#include +#include + +using namespace MR; + +EMSCRIPTEN_BINDINGS( meshlib_uniform_sampling ) +{ + emscripten::class_( "UniformSamplingSettings" ) + .constructor<>() + .property( "distance", &UniformSamplingSettings::distance ) + .property( "minNormalDot", &UniformSamplingSettings::minNormalDot ) + .property( "lexicographicalOrder", &UniformSamplingSettings::lexicographicalOrder ); + + emscripten::function( "pointUniformSampling", +[]( const PointCloud& pointCloud, const UniformSamplingSettings& settings ) + { + auto res = pointUniformSampling( pointCloud, settings ); + if ( !res ) + Wasm::throwJsError( "pointUniformSampling was cancelled" ); + return std::move( *res ); + } ); +} diff --git a/source/MRWasmModule/MRWasmVector.cpp b/source/MRWasmModule/MRWasmVector.cpp index bff1e9eb179b..93ae6087b797 100644 --- a/source/MRWasmModule/MRWasmVector.cpp +++ b/source/MRWasmModule/MRWasmVector.cpp @@ -18,7 +18,12 @@ EMSCRIPTEN_BINDINGS( meshlib_vector ) .class_function( "fromArray", &Wasm::packedFromTypedArray ) .function( "toArray", &Wasm::packedToTypedArray ) .function( "size", +[]( const VertCoords& v ) { return (int)v.size(); } ) - .function( "get", +[]( const VertCoords& v, int i ) { return v[VertId( i )]; } ); + .function( "get", +[]( const VertCoords& v, int i ) { return v[VertId( i )]; } ) + .function( "set", +[]( VertCoords& v, int i, const Vector3f& p ) { v[VertId( i )] = p; } ); + + emscripten::class_( "VertScalars" ) + .class_function( "fromArray", &Wasm::packedFromTypedArray ) + .function( "toArray", &Wasm::packedToTypedArray ); emscripten::class_( "Triangulation" ) .class_function( "fromArray", &Wasm::packedFromTypedArray ) diff --git a/source/MRWasmModule/MRWasmWeightedPointsShell.cpp b/source/MRWasmModule/MRWasmWeightedPointsShell.cpp new file mode 100644 index 000000000000..d5db3cd714d6 --- /dev/null +++ b/source/MRWasmModule/MRWasmWeightedPointsShell.cpp @@ -0,0 +1,34 @@ +#include "MRWasmBindings.h" + +#include "MRVoxels/MRWeightedPointsShell.h" +#include "MRMesh/MRMesh.h" + +#include + +#include + +using namespace MR; + +namespace +{ +struct WeightedShellModule {}; +} + +EMSCRIPTEN_BINDINGS( meshlib_weighted_points_shell ) +{ + emscripten::class_( "WeightedShellParametersBase" ) + .constructor<>() + .property( "offset", &WeightedShell::ParametersBase::offset ) + .property( "voxelSize", &WeightedShell::ParametersBase::voxelSize ) + .property( "numLayers", &WeightedShell::ParametersBase::numLayers ); + + emscripten::class_>( "WeightedShellParametersMetric" ) + .constructor<>() + .property( "dist", &WeightedShell::ParametersMetric::dist ); + + emscripten::class_( "WeightedShell" ) + .class_function( "meshShell", +[]( std::shared_ptr mesh, const VertScalars& vertWeights, const WeightedShell::ParametersMetric& params ) + { + return std::make_shared( Wasm::unwrap( WeightedShell::meshShell( *mesh, vertWeights, params ) ) ); + } ); +} diff --git a/source/MRWasmTest/denoise.test.mjs b/source/MRWasmTest/denoise.test.mjs new file mode 100644 index 000000000000..9a1144912425 --- /dev/null +++ b/source/MRWasmTest/denoise.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import { ml, meshToGeometry } from './helpers.mjs'; + +// addNoise perturbs the vertices; meshDenoiseViaNormals then smooths them +{ + const sphere = ml.makeUVSphere( 1, 16, 16 ); + + const before = meshToGeometry( sphere, false ).positions; + + const noise = new ml.NoiseSettings(); + noise.sigma = 0.01; + noise.seed = 1; + ml.addNoise( sphere, noise ); + sphere.invalidateCaches(); + noise.delete(); + + const noised = meshToGeometry( sphere, false ).positions; + assert.equal( noised.length, before.length, 'noise keeps the vertex count' ); + let moved = 0; + for ( let i = 0; i < before.length; ++i ) + if ( Math.abs( before[ i ] - noised[ i ] ) > 1e-6 ) ++moved; + assert.ok( moved > 0, 'addNoise perturbs the vertices' ); + + ml.meshDenoiseViaNormals( sphere ); + assert.ok( meshToGeometry( sphere, false ).positions.length > 0, 'the mesh survives denoising' ); + + sphere.delete(); +} diff --git a/source/MRWasmTest/extrude.test.mjs b/source/MRWasmTest/extrude.test.mjs new file mode 100644 index 000000000000..c23a749b3954 --- /dev/null +++ b/source/MRWasmTest/extrude.test.mjs @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import { ml, cube, meshFromGeometry } from './helpers.mjs'; + +{ + const c = cube( 0, 0, 0, 2 ); + const m = meshFromGeometry( c.positions, c.indices ); + + const coordsBefore = m.points; + const beforeVerts = coordsBefore.size(); + coordsBefore.delete(); + + // a degenerate band duplicates the boundary vertices of the extruded region + const faces = ml.FaceBitSet.fromIndices( new Uint32Array( [ 1, 2 ] ) ); + ml.makeDegenerateBandAroundRegion( m, faces ); + + const coords = m.points; + assert.ok( coords.size() > beforeVerts, 'the band adds duplicated boundary vertices' ); + + // shift the incident vertices via the writable points accessors + const topo = m.topology; + const verts = ml.getIncidentVertsFromFaces( topo, faces ); + const idx = verts.toIndices(); + assert.ok( idx.length > 0, 'the extruded faces have incident vertices' ); + + const v0 = idx[ 0 ]; + const zBefore = coords.get( v0 ).z; + for ( const v of idx ) { + const p = coords.get( v ); + p.z += 1; + coords.set( v, p ); + } + m.points = coords; + m.invalidateCaches(); + + const after = m.points; + assert.ok( Math.abs( after.get( v0 ).z - ( zBefore + 1 ) ) < 1e-5, 'the moved vertex is written back to the mesh' ); + + after.delete(); + coords.delete(); + verts.delete(); + topo.delete(); + faces.delete(); + m.delete(); +} diff --git a/source/MRWasmTest/fusion.test.mjs b/source/MRWasmTest/fusion.test.mjs new file mode 100644 index 000000000000..ffad4b521f7b --- /dev/null +++ b/source/MRWasmTest/fusion.test.mjs @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import { ml, meshToGeometry } from './helpers.mjs'; + +// build a point cloud from a sphere, then reconstruct a mesh via fusion +{ + const sphere = ml.makeUVSphere( 1, 32, 32 ); + const pc = ml.meshToPointCloud( sphere, true ); + + const radius = ml.findAvgPointsRadius( pc, 50 ); + assert.ok( radius > 0, 'findAvgPointsRadius returns a positive radius' ); + + const bbox = pc.computeBoundingBox(); + const params = new ml.PointsToMeshParameters(); + params.voxelSize = bbox.diagonal() * 0.02; + params.sigma = Math.max( params.voxelSize, radius ); + params.minWeight = 1; + bbox.delete(); + + const mesh = ml.pointsToMeshFusion( pc, params ); + const g = meshToGeometry( mesh, false ); + assert.ok( g.positions.length > 0 && g.indices.length > 0, 'fusion reconstructs a non-empty mesh' ); + + mesh.delete(); + params.delete(); + pc.delete(); + sphere.delete(); +} diff --git a/source/MRWasmTest/index.mjs b/source/MRWasmTest/index.mjs index 65f3c904d0e5..cb1665b68525 100644 --- a/source/MRWasmTest/index.mjs +++ b/source/MRWasmTest/index.mjs @@ -23,6 +23,10 @@ import './icp.test.mjs'; import './contoursCut.test.mjs'; import './deformers.test.mjs'; import './voxels.test.mjs'; +import './extrude.test.mjs'; +import './denoise.test.mjs'; +import './fusion.test.mjs'; +import './weightedShell.test.mjs'; import './mopup.test.mjs'; console.log( 'OK' ); diff --git a/source/MRWasmTest/pointCloud.test.mjs b/source/MRWasmTest/pointCloud.test.mjs index 788843a036d7..20564828d4b4 100644 --- a/source/MRWasmTest/pointCloud.test.mjs +++ b/source/MRWasmTest/pointCloud.test.mjs @@ -45,3 +45,29 @@ import { ml, cube, meshFromGeometry, meshToGeometry } from './helpers.mjs'; pc.delete(); sphere.delete(); } + +// pointUniformSampling + writable validPoints + invalidateCaches +{ + const sphere = ml.makeUVSphere( 1, 32, 32 ); + const pc = ml.meshToPointCloud( sphere, true ); + + const initial = pc.validPoints; + const total = initial.count(); + initial.delete(); + + const settings = new ml.UniformSamplingSettings(); + settings.distance = 0.2; + const samples = ml.pointUniformSampling( pc, settings ); + assert.ok( samples.count() > 0 && samples.count() < total, 'uniform sampling selects a proper subset' ); + + pc.validPoints = samples; + pc.invalidateCaches(); + const after = pc.validPoints; + assert.equal( after.count(), samples.count(), 'the sampled subset becomes the valid points' ); + + after.delete(); + samples.delete(); + settings.delete(); + pc.delete(); + sphere.delete(); +} diff --git a/source/MRWasmTest/weightedShell.test.mjs b/source/MRWasmTest/weightedShell.test.mjs new file mode 100644 index 000000000000..0eb46e8a07d8 --- /dev/null +++ b/source/MRWasmTest/weightedShell.test.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import { ml, meshToGeometry } from './helpers.mjs'; + +// VertScalars round-trips a Float32Array +{ + const src = new Float32Array( [ 0.1, 0.2, 0.3, 0.4 ] ); + const scalars = ml.VertScalars.fromArray( src ); + assert.deepEqual( Array.from( scalars.toArray() ), Array.from( src ), 'VertScalars round-trips the weights' ); + scalars.delete(); +} + +// weighted shell offsets a torus using per-vertex weights +{ + const mesh = ml.makeTorus( 1.0, 0.1, 16, 16 ); + + const coords = mesh.points; + const positions = coords.toArray(); + const weights = new Float32Array( positions.length / 3 ); + let maxWeight = 0; + for ( let v = 0; v < weights.length; ++v ) { + weights[ v ] = Math.abs( positions[ 3 * v ] / 5 ); + maxWeight = Math.max( maxWeight, weights[ v ] ); + } + coords.delete(); + const scalars = ml.VertScalars.fromArray( weights ); + + const params = new ml.WeightedShellParametersMetric(); + params.voxelSize = ml.suggestVoxelSize( mesh, 1000 ); + params.offset = 0.2; + const dist = params.dist; + dist.maxWeight = maxWeight; + params.dist = dist; + dist.delete(); + + const result = ml.WeightedShell.meshShell( mesh, scalars, params ); + const g = meshToGeometry( result, false ); + assert.ok( g.positions.length > 0 && g.indices.length > 0, 'weighted shell produces a non-empty mesh' ); + + result.delete(); + params.delete(); + scalars.delete(); + mesh.delete(); +} From 9af9ef314f607f5e04da0026ca3b9a6e7d349357 Mon Sep 17 00:00:00 2001 From: Artur Sharafutdinov Date: Tue, 21 Jul 2026 11:24:19 +0200 Subject: [PATCH 4/4] Update bindings --- .../js-examples/LaplacianDeformation.dox.js | 5 +---- examples/js-examples/MeshDecimate.dox.js | 2 +- examples/js-examples/MeshExtrude.dox.js | 7 ++---- examples/js-examples/MeshModification.dox.js | 2 +- examples/js-examples/MeshOffset.dox.js | 2 +- .../js-examples/MeshOffsetWeighted.dox.js | 2 +- source/MRWasmModule/MRWasmCube.cpp | 8 +++++++ source/MRWasmModule/MRWasmMesh.cpp | 2 ++ source/MRWasmModule/MRWasmTorus.cpp | 16 ++++++++++++++ source/MRWasmModule/MRWasmVector3.cpp | 11 ++++++++++ source/MRWasmTest/makers.test.mjs | 22 +++++++++++++++++++ source/MRWasmTest/primitives.test.mjs | 15 +++++++++++++ 12 files changed, 81 insertions(+), 13 deletions(-) diff --git a/examples/js-examples/LaplacianDeformation.dox.js b/examples/js-examples/LaplacianDeformation.dox.js index f676588d7388..4f6bd03dd78e 100644 --- a/examples/js-examples/LaplacianDeformation.dox.js +++ b/examples/js-examples/LaplacianDeformation.dox.js @@ -33,10 +33,7 @@ using points = mesh.points; for (const anchor of [ancV0, ancV1]) { const p = points.get(anchor); const n = mesh.normal(anchor); - deformer.fixVertex( - anchor, - { x: p.x + n.x * shiftAmount, y: p.y + n.y * shiftAmount, z: p.z + n.z * shiftAmount }, - true ); + deformer.fixVertex(anchor, ml.Vector3.add(p, ml.Vector3.mulScalar(n, shiftAmount)), true); } // Move the free vertices according to the anchor ones diff --git a/examples/js-examples/MeshDecimate.dox.js b/examples/js-examples/MeshDecimate.dox.js index bbed14dae228..b0b60811e209 100644 --- a/examples/js-examples/MeshDecimate.dox.js +++ b/examples/js-examples/MeshDecimate.dox.js @@ -7,7 +7,7 @@ using mesh = ml.MeshLoad.fromAnySupportedFormat('mesh.stl'); // Repack the mesh. // Not necessary, but highly recommended to achieve the best performance in parallel processing. -mesh.pack(); +mesh.packOptimally(); // Setup decimate parameters using settings = new ml.DecimateSettings(); diff --git a/examples/js-examples/MeshExtrude.dox.js b/examples/js-examples/MeshExtrude.dox.js index 37716c179025..c4c6eb4b3f06 100644 --- a/examples/js-examples/MeshExtrude.dox.js +++ b/examples/js-examples/MeshExtrude.dox.js @@ -14,11 +14,8 @@ ml.makeDegenerateBandAroundRegion(mesh, facesToExtrude); using topology = mesh.topology; using vertsForMove = ml.getIncidentVertsFromFaces(topology, facesToExtrude); using coords = mesh.points; -for (const v of vertsForMove.toIndices()) { - const p = coords.get(v); - p.z += 1; - coords.set(v, p); -} +for (const v of vertsForMove.toIndices()) + coords.set(v, ml.Vector3.add(coords.get(v), ml.Vector3.plusZ())); mesh.points = coords; // invalidate internal caches after the manual vertex changes diff --git a/examples/js-examples/MeshModification.dox.js b/examples/js-examples/MeshModification.dox.js index 78d10f7f25a8..f2f68c335122 100644 --- a/examples/js-examples/MeshModification.dox.js +++ b/examples/js-examples/MeshModification.dox.js @@ -15,6 +15,6 @@ subdivideSettings.maxDeviationAfterFlip = 0.5; ml.subdivideMesh(mesh, subdivideSettings); // Rotate mesh -using rotation = ml.Matrix3f.rotation({ x: 0, y: 0, z: 1 }, Math.PI * 0.5); +using rotation = ml.Matrix3f.rotation(ml.Vector3.plusZ(), Math.PI * 0.5); using rotationXf = ml.AffineXf3f.linear(rotation); mesh.transform(rotationXf); diff --git a/examples/js-examples/MeshOffset.dox.js b/examples/js-examples/MeshOffset.dox.js index 91f0c9552ab6..46177f3a62a7 100644 --- a/examples/js-examples/MeshOffset.dox.js +++ b/examples/js-examples/MeshOffset.dox.js @@ -3,7 +3,7 @@ import createMeshLib from '@meshinspector/meshlib'; const ml = await createMeshLib(); // Create mesh -using mesh = ml.makeCube({ x: 1, y: 1, z: 1 }, { x: -0.5, y: -0.5, z: -0.5 }); +using mesh = ml.makeCube(); // Setup parameters using params = new ml.GeneralOffsetParameters(); diff --git a/examples/js-examples/MeshOffsetWeighted.dox.js b/examples/js-examples/MeshOffsetWeighted.dox.js index 7f286f8f867e..2f95662414ac 100644 --- a/examples/js-examples/MeshOffsetWeighted.dox.js +++ b/examples/js-examples/MeshOffsetWeighted.dox.js @@ -2,7 +2,7 @@ import createMeshLib from '@meshinspector/meshlib'; const ml = await createMeshLib(); -using mesh = ml.makeTorus(1.0, 0.1, 16, 16); +using mesh = ml.makeTorus(); // build a per-vertex weight (extra offset) from each vertex's x coordinate using coords = mesh.points; diff --git a/source/MRWasmModule/MRWasmCube.cpp b/source/MRWasmModule/MRWasmCube.cpp index 8dff13fbc20d..7af6d00c532f 100644 --- a/source/MRWasmModule/MRWasmCube.cpp +++ b/source/MRWasmModule/MRWasmCube.cpp @@ -12,6 +12,14 @@ using namespace MR; EMSCRIPTEN_BINDINGS( meshlib_cube ) { + emscripten::function( "makeCube", +[]() + { + return std::make_shared( makeCube() ); + } ); + emscripten::function( "makeCube", +[]( const Vector3f& size ) + { + return std::make_shared( makeCube( size ) ); + } ); emscripten::function( "makeCube", +[]( const Vector3f& size, const Vector3f& base ) { return std::make_shared( makeCube( size, base ) ); diff --git a/source/MRWasmModule/MRWasmMesh.cpp b/source/MRWasmModule/MRWasmMesh.cpp index 13d47c078bb3..3acb6137f98c 100644 --- a/source/MRWasmModule/MRWasmMesh.cpp +++ b/source/MRWasmModule/MRWasmMesh.cpp @@ -2,6 +2,7 @@ #include "MRMesh/MRMesh.h" #include "MRMesh/MRMeshTopology.h" +#include "MRMesh/MRBuffer.h" #include "MRMesh/MRBox.h" #include "MRMesh/MRAffineXf3.h" #include "MRMesh/MRVector3.h" @@ -28,6 +29,7 @@ EMSCRIPTEN_BINDINGS( meshlib_mesh ) return std::make_shared( Mesh::fromTriangles( vertexCoordinates, t ) ); } ) .function( "pack", +[]( Mesh& m ) { m.pack(); } ) + .function( "packOptimally", +[]( Mesh& m ) { m.packOptimally(); } ) .function( "volume", +[]( const Mesh& m ) { return m.volume(); } ) .function( "area", +[]( const Mesh& m ) { return m.area(); } ) .function( "computeBoundingBox", +[]( const Mesh& m ) { return m.computeBoundingBox(); } ) diff --git a/source/MRWasmModule/MRWasmTorus.cpp b/source/MRWasmModule/MRWasmTorus.cpp index 45e843404de3..8212c7307bb0 100644 --- a/source/MRWasmModule/MRWasmTorus.cpp +++ b/source/MRWasmModule/MRWasmTorus.cpp @@ -11,6 +11,22 @@ using namespace MR; EMSCRIPTEN_BINDINGS( meshlib_torus ) { + emscripten::function( "makeTorus", +[]() + { + return std::make_shared( makeTorus() ); + } ); + emscripten::function( "makeTorus", +[]( float primaryRadius ) + { + return std::make_shared( makeTorus( primaryRadius ) ); + } ); + emscripten::function( "makeTorus", +[]( float primaryRadius, float secondaryRadius ) + { + return std::make_shared( makeTorus( primaryRadius, secondaryRadius ) ); + } ); + emscripten::function( "makeTorus", +[]( float primaryRadius, float secondaryRadius, int primaryResolution ) + { + return std::make_shared( makeTorus( primaryRadius, secondaryRadius, primaryResolution ) ); + } ); emscripten::function( "makeTorus", +[]( float primaryRadius, float secondaryRadius, int primaryResolution, int secondaryResolution ) { diff --git a/source/MRWasmModule/MRWasmVector3.cpp b/source/MRWasmModule/MRWasmVector3.cpp index f5b3e98f828b..3d1a6fe61e6d 100644 --- a/source/MRWasmModule/MRWasmVector3.cpp +++ b/source/MRWasmModule/MRWasmVector3.cpp @@ -6,6 +6,11 @@ using namespace MR; +namespace +{ +struct Vector3Module {}; +} + EMSCRIPTEN_BINDINGS( meshlib_vector3 ) { emscripten::value_object( "Vector3f" ) @@ -17,4 +22,10 @@ EMSCRIPTEN_BINDINGS( meshlib_vector3 ) .field( "x", &Vector3i::x ) .field( "y", &Vector3i::y ) .field( "z", &Vector3i::z ); + + emscripten::class_( "Vector3" ) + .class_function( "plusZ", +[]() { return Vector3f::plusZ(); } ) + .class_function( "diagonal", +[]( float a ) { return Vector3f::diagonal( a ); } ) + .class_function( "add", +[]( const Vector3f& a, const Vector3f& b ) { return a + b; } ) + .class_function( "mulScalar", +[]( const Vector3f& v, float s ) { return v * s; } ); } diff --git a/source/MRWasmTest/makers.test.mjs b/source/MRWasmTest/makers.test.mjs index 5380db3052de..7da8e896e2a7 100644 --- a/source/MRWasmTest/makers.test.mjs +++ b/source/MRWasmTest/makers.test.mjs @@ -57,3 +57,25 @@ import { ml, meshToGeometry } from './helpers.mjs'; c.delete(); hull.delete(); } + +// default-argument overloads +{ + const m = ml.makeCube(); + assert.ok( Math.abs( m.volume() - 1 ) < 1e-4, 'default cube (no args) has volume 1' ); + m.delete(); +} + +{ + const m = ml.makeTorus(); + const g = meshToGeometry( m, false ); + assert.ok( g.positions.length > 0 && g.indices.length > 0, 'default torus (no args) is non-empty' ); + m.delete(); +} + +// packOptimally reorders storage without changing the geometry +{ + const m = ml.makeCube(); + m.packOptimally(); + assert.ok( Math.abs( m.volume() - 1 ) < 1e-4, 'packOptimally preserves the mesh' ); + m.delete(); +} diff --git a/source/MRWasmTest/primitives.test.mjs b/source/MRWasmTest/primitives.test.mjs index fda1c41a85c3..6c48a9487711 100644 --- a/source/MRWasmTest/primitives.test.mjs +++ b/source/MRWasmTest/primitives.test.mjs @@ -26,3 +26,18 @@ import { ml } from './helpers.mjs'; assert.ok( bsz.x === 2 && bsz.y === 3 && bsz.z === 4, 'integer box size is (2,3,4)' ); box.delete(); } + +// Vector3 helpers operate on plain {x,y,z} value objects +{ + const z = ml.Vector3.plusZ(); + assert.ok( z.x === 0 && z.y === 0 && z.z === 1, 'plusZ is (0,0,1)' ); + + const d = ml.Vector3.diagonal( 2 ); + assert.ok( d.x === 2 && d.y === 2 && d.z === 2, 'diagonal(2) is (2,2,2)' ); + + const s = ml.Vector3.add( { x: 1, y: 2, z: 3 }, { x: 10, y: 20, z: 30 } ); + assert.ok( s.x === 11 && s.y === 22 && s.z === 33, 'add sums componentwise' ); + + const m = ml.Vector3.mulScalar( { x: 1, y: -2, z: 3 }, 2 ); + assert.ok( m.x === 2 && m.y === -4 && m.z === 6, 'mulScalar scales componentwise' ); +}