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.
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).
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…
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()
#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)