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/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/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/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/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/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/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/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/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/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/.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/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/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..4f6bd03dd78e
--- /dev/null
+++ b/examples/js-examples/LaplacianDeformation.dox.js
@@ -0,0 +1,46 @@
+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, ml.Vector3.add(p, ml.Vector3.mulScalar(n, 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..b0b60811e209
--- /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.packOptimally();
+
+// 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/MeshExtrude.dox.js b/examples/js-examples/MeshExtrude.dox.js
new file mode 100644
index 000000000000..c4c6eb4b3f06
--- /dev/null
+++ b/examples/js-examples/MeshExtrude.dox.js
@@ -0,0 +1,24 @@
+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())
+ coords.set(v, ml.Vector3.add(coords.get(v), ml.Vector3.plusZ()));
+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/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..f2f68c335122
--- /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(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
new file mode 100644
index 000000000000..46177f3a62a7
--- /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();
+
+// 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/MeshOffsetWeighted.dox.js b/examples/js-examples/MeshOffsetWeighted.dox.js
new file mode 100644
index 000000000000..2f95662414ac
--- /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();
+
+// 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/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/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/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/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/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/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/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/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/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 df2fe868d486..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"
@@ -19,19 +20,23 @@ 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 )
{
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(); } )
.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/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/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/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/MRWasmTorus.cpp b/source/MRWasmModule/MRWasmTorus.cpp
index 31a669f1a282..8212c7307bb0 100644
--- a/source/MRWasmModule/MRWasmTorus.cpp
+++ b/source/MRWasmModule/MRWasmTorus.cpp
@@ -11,10 +11,33 @@ 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 )
{
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/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 9be8660211db..93ae6087b797 100644
--- a/source/MRWasmModule/MRWasmVector.cpp
+++ b/source/MRWasmModule/MRWasmVector.cpp
@@ -16,7 +16,14 @@ 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 )]; } )
+ .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/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/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/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/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/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/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/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..cb1665b68525 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';
@@ -22,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/makers.test.mjs b/source/MRWasmTest/makers.test.mjs
index 9219d69c4cb0..7da8e896e2a7 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 );
@@ -50,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/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/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/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' );
+}
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();
+}
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();
+}