Script to Snap a mesh vertex to nearest mesh vertex of other mesh

I have two larger meshes which meet at one or more edges in such a way, that after joining there should be no naked edges. However, the vertex coordinates sometimes differ by an extremely small amount, so that sometimes the mesh is still seen as open (even though the displacement is like 1e-6 m at a tolerance value of 0.01). If I - by hand - drag the vertex away and snap it back on the target mesh, everything is fine. However, the number of vertices is far too large to do this by hand.

I have created a simple example model, which shows this problem (see image).

I tried to create a python script to make this adjustment automatically, but it seems the python functions for mesh manipulation are not fully fleshed out. For example, rs.MeshVertices(meshID) gives me a list of vertex coordinates, but I could not find a way to modify this list and update the vertex positions based on it. A different route over obj.Geometry.Vertices and using SetVertex also correctly identified the offending positions, but Rhino still shows the edge as open after joining
Join_Mesh_Example.3dm (184.1 KB)
.
I’m grateful for any suggestion. :slight_smile:

It seems you should try the _AlignVertices command.

That would work if I need one connected mesh. However, I need those meshes to remain separate, but fit together.
Of course I could temporarily join, then align and then try to separate the parts again, but I thought a simple mesh movement script should be possible (maybe a misjudgement on my part).

Yes, I would do that.

Absolutely. But I don’t know python nor I plan to learn it.
What you ask IS indeed trivial if done by grasshopper or c#, probably doable even via rhinoscript…
We have c# scripts in Rhino 8, I don’t know if you have it.
Otherwise… rhinoscript it is.
Or… someone else jump in and do it via python. I’m sure this is trivial there too.


“rs” is that rhinoscriptsyntax?
You might want to try your luck with “rc” , rhinocommon methods.
Updating a Mesh vertex location should be the same as editing an array/list at a specific index.
It works this way in c#…
But again, I don’t know python…

This works via c#, maybe you manage to replicate it with python…

    Rhino.DocObjects.ObjRef objref0;
    Rhino.Commands.Result result0 = Rhino.Input.RhinoGet.GetOneObject("Select mesh to edit", false, Rhino.DocObjects.ObjectType.Mesh, out objref0);
    Rhino.DocObjects.ObjRef objref1;
    Rhino.Commands.Result result1 = Rhino.Input.RhinoGet.GetOneObject("Select static mesh", false, Rhino.DocObjects.ObjectType.Mesh, out objref1);
    double tolerance = 0.1;
    Rhino.Commands.Result result2 = Rhino.Input.RhinoGet.GetNumber("Tolerance", false, ref tolerance, 0, 100);
    Rhino.Geometry.Mesh m0 = objref0.Mesh();
    Rhino.Geometry.Mesh m1 = objref1.Mesh();
    m0.Vertices.UseDoublePrecisionVertices = false;
    m1.Vertices.UseDoublePrecisionVertices = false;
    Point3d[] pts = m0.Vertices.ToPoint3dArray();
    Rhino.Geometry.PointCloud pc = new PointCloud(objref1.Mesh().Vertices.ToPoint3dArray());
    tolerance *= tolerance;
    for(int i = 0;i < pts.Length;i++){
      int j = pc.ClosestPoint(pts[i]);
      if(pc[j].Location.DistanceToSquared(pts[i]) < tolerance){
        m0.Vertices[i] = m1.Vertices[j];
      }
    }
    this.RhinoDocument.Objects.Replace(objref0, m0);
    this.RhinoDocument.Objects.Replace(objref1, m1);

Edit: edited the code.

Thanks a lot, I will try it this way! :+1:

Translated to Python 3.0 using ChatGPT and tested

import Rhino
from Rhino.Geometry import PointCloud

def main():
    # Prompt user to select the first mesh
    result0, objref0 = Rhino.Input.RhinoGet.GetOneObject("Select mesh to edit", False, Rhino.DocObjects.ObjectType.Mesh)
    if result0 != Rhino.Commands.Result.Success:
        print("Failed to select the first mesh.")
        return

    # Prompt user to select the second mesh
    result1, objref1 = Rhino.Input.RhinoGet.GetOneObject("Select static mesh", False, Rhino.DocObjects.ObjectType.Mesh)
    if result1 != Rhino.Commands.Result.Success:
        print("Failed to select the second mesh.")
        return

    # Get tolerance input
    tolerance = 0.1
    result2, tolerance = Rhino.Input.RhinoGet.GetNumber("Tolerance", False, tolerance, 0, 500)
    if result2 != Rhino.Commands.Result.Success:
        print("Failed to get tolerance input.")
        return

    # Get the meshes
    mesh0 = objref0.Mesh()
    mesh1 = objref1.Mesh()

    # Set single-precision vertices
    mesh0.Vertices.UseDoublePrecisionVertices = False
    mesh1.Vertices.UseDoublePrecisionVertices = False

    # Convert vertices to point arrays
    pts = mesh0.Vertices.ToPoint3dArray()
    mesh1_pts = mesh1.Vertices.ToPoint3dArray()

    # Create a PointCloud from the second mesh's vertices
    pc = PointCloud()
    for pt in mesh1_pts:
        pc.Add(pt)

    # Use squared tolerance for efficiency
    tolerance_squared = tolerance * tolerance

    # Iterate through the vertices of the first mesh
    for i in range(len(pts)):
        # Find the closest point in the point cloud
        j = pc.ClosestPoint(pts[i])
        if pc[j].Location.DistanceToSquared(pts[i]) < tolerance_squared:
            # Replace the vertex in mesh0 with the corresponding vertex from mesh1
            mesh0.Vertices.SetVertex(i, mesh1.Vertices[j])

    # Replace the original objects in the document
    doc = Rhino.RhinoDoc.ActiveDoc
    doc.Objects.Replace(objref0.ObjectId, mesh0)
    doc.Objects.Replace(objref1.ObjectId, mesh1)

    # Notify user of success
    print("Meshes updated successfully.")

# Call the main function
if __name__ == "__main__":
    main()

Variation where only the points found in the same plane are moved.

import Rhino
from Rhino.Geometry import PointCloud, Plane

def main():
    # Prompt user to select the first mesh
    result0, objref0 = Rhino.Input.RhinoGet.GetOneObject("Select mesh to edit", False, Rhino.DocObjects.ObjectType.Mesh)
    if result0 != Rhino.Commands.Result.Success:
        print("Failed to select the first mesh.")
        return

    # Prompt user to select the second mesh
    result1, objref1 = Rhino.Input.RhinoGet.GetOneObject("Select static mesh", False, Rhino.DocObjects.ObjectType.Mesh)
    if result1 != Rhino.Commands.Result.Success:
        print("Failed to select the second mesh.")
        return

    # Get tolerance input
    tolerance = 0.1
    result2, tolerance = Rhino.Input.RhinoGet.GetNumber("Tolerance", False, tolerance, 0, 1000)
    if result2 != Rhino.Commands.Result.Success:
        print("Failed to get tolerance input.")
        return

    # Get the meshes
    mesh0 = objref0.Mesh()
    mesh1 = objref1.Mesh()

    # Set single-precision vertices
    mesh0.Vertices.UseDoublePrecisionVertices = False
    mesh1.Vertices.UseDoublePrecisionVertices = False

    # Convert vertices to point arrays
    pts = mesh0.Vertices.ToPoint3dArray()
    mesh1_pts = mesh1.Vertices.ToPoint3dArray()

    # Create a PointCloud from the second mesh's vertices
    pc = PointCloud()
    for pt in mesh1_pts:
        pc.Add(pt)

    # Use squared tolerance for efficiency
    tolerance_squared = tolerance * tolerance

    # Iterate through the vertices of the first mesh
    for i in range(len(pts)):
        # Find the closest point in the point cloud
        j = pc.ClosestPoint(pts[i])
        
        # Check if points are in the same plane (assuming Z-coordinates must match)
        if abs(pc[j].Location.Z - pts[i].Z) < 1e-6:  # Allow for slight floating-point inaccuracies
            # Check if the points are within the given tolerance
            if pc[j].Location.DistanceToSquared(pts[i]) < tolerance_squared:
                # Replace the vertex in mesh0 with the corresponding vertex from mesh1
                mesh0.Vertices.SetVertex(i, mesh1.Vertices[j])

    # Replace the original objects in the document
    doc = Rhino.RhinoDoc.ActiveDoc
    doc.Objects.Replace(objref0.ObjectId, mesh0)
    doc.Objects.Replace(objref1.ObjectId, mesh1)

    # Notify user of success
    print("Meshes updated successfully, but only points in the same plane were moved.")

# Call the main function
if __name__ == "__main__":
    main()


@Ricardo_Luz ,
Some points simply don’t want to move, even though they are within tolerance.
Could there be a bug somewhere ?

Variation working on SubD meshes:

#SubD mesh snapping
import Rhino
from Rhino.Geometry import PointCloud, Mesh

def main():
    # Prompt user to select the first SubD
    result0, objref0 = Rhino.Input.RhinoGet.GetOneObject("Select SubD to edit", False, Rhino.DocObjects.ObjectType.SubD)
    if result0 != Rhino.Commands.Result.Success:
        print("Failed to select the first SubD.")
        return

    # Prompt user to select the second SubD
    result1, objref1 = Rhino.Input.RhinoGet.GetOneObject("Select static SubD", False, Rhino.DocObjects.ObjectType.SubD)
    if result1 != Rhino.Commands.Result.Success:
        print("Failed to select the second SubD.")
        return

    # Get tolerance input
    tolerance = 1000
    result2, tolerance = Rhino.Input.RhinoGet.GetNumber("Tolerance", False, tolerance, 0, 1000)
    if result2 != Rhino.Commands.Result.Success:
        print("Failed to get tolerance input.")
        return

    # Get the SubD objects
    subd0 = objref0.Geometry()
    subd1 = objref1.Geometry()

    # Convert SubD to control-net meshes
    mesh0 = Mesh.CreateFromSubDControlNet(subd0)
    mesh1 = Mesh.CreateFromSubDControlNet(subd1)

    # Use single-precision vertices
    mesh0.Vertices.UseDoublePrecisionVertices = False
    mesh1.Vertices.UseDoublePrecisionVertices = False

    # Build a PointCloud from the second mesh
    pc = PointCloud()
    for pt in mesh1.Vertices:
        pc.Add(pt)

    # Tolerance squared
    tolerance_sq = tolerance * tolerance

    # Snap the vertices of mesh0
    for i in range(len(mesh0.Vertices)):
        pt = mesh0.Vertices[i]
        j = pc.ClosestPoint(pt)
        if pc[j].Location.DistanceToSquared(pt) < tolerance_sq:
            mesh0.Vertices.SetVertex(i, pc[j].Location)

    # Create a new SubD from the updated mesh
    updated_subd0 = Rhino.Geometry.SubD.CreateFromMesh(mesh0)

    # Replace the original SubD with this new one
    Rhino.RhinoDoc.ActiveDoc.Objects.Replace(objref0.ObjectId, updated_subd0)

    print("SubD updated successfully (but creases not preserved).")

# Call the main function
if __name__ == "__main__":
    main()

Conclusion

  • If your Rhino version lacks the SubD APIs to detect or manipulate creases and also lacks a way to iterate or set the original SubD’s control vertices, then it is not possible to preserve creases while snapping.
  • The script that simply rebuilds a new SubD from a snapped mesh works for moving geometry, but loses creases.

No further workaround is possible unless you upgrade to a Rhino version that provides these SubD APIs (like GetEnumerator(), IsCreased, CreaseType, or direct control net manipulation).

Hence the final answer is:

You can snap the geometry with the script you already have, but you cannot preserve the crease data in your version of Rhino.

""" Snaps vertices of 'mesh_in' to the closest vertices of 'mesh_ref' if within 'tolerance'.

IN:
    mesh_in   (Mesh)    -- Mesh to modify
    mesh_ref  (Mesh)    -- Reference mesh whose vertices are the snap targets
    tolerance (float)   -- Distance threshold for snapping

OUT:
    mesh_out  (Mesh)    -- The modified mesh with snapped vertices
"""

import Rhino
from Rhino.Geometry import PointCloud

def snap_mesh_vertices(mesh_in, mesh_ref, tolerance):
    # Validate inputs
    if not mesh_in or not mesh_ref:
        return None

    # Build a PointCloud from mesh_ref’s vertices for fast nearest-point lookups
    pc = PointCloud()
    for i in range(mesh_ref.Vertices.Count):
        pt = mesh_ref.Vertices[i]
        pc.Add(pt)

    # Create a *copy* of mesh_in so we can modify it safely
    new_mesh = mesh_in.DuplicateMesh()
    if not new_mesh:
        return None

    # We'll compare squared distances to avoid repeated sqrt
    tol_sq = tolerance * tolerance

    # Snap each vertex in new_mesh if within tolerance
    for i in range(new_mesh.Vertices.Count):
        pt = new_mesh.Vertices[i]
        idx_closest = pc.ClosestPoint(pt)  # index in the point cloud
        dist_sq = pt.DistanceToSquared(pc[idx_closest].Location)
        if dist_sq < tol_sq:
            # Snap this vertex to the reference vertex
            new_mesh.Vertices.SetVertex(i, pc[idx_closest].Location)

    # (Optional) Rebuild normals to keep them consistent after vertex changes
    new_mesh.RebuildNormals()

    return new_mesh


def main(mesh_in, mesh_ref, tolerance):
    return snap_mesh_vertices(mesh_in, mesh_ref, tolerance)

mesh_out = main(mesh_in, mesh_ref, tolerance)