Needed to modify hair cards and wanted to remesh them with the fancy geometry node hair tools now available. Problem was that I needed to convert the hair card meshes into lines that follow the flow of the cards. With a bit of trial and error and a lot oh help from our old friend Chargpt, got this script working. It uses shortest edges to determine which lines should be intersected, idk if that’s gonna work for everyone, but it works well enouph for my models. Enjoy, and I hope this helps someone!
import bpy
import bmesh
def find_shortest_edges(face):
"""
Identify the shortest pair of opposite edges in a quad face.
"""
if len(face.edges) != 4:
return None
edges = list(face.edges)
edge_lengths = [(edge, (edge.verts[0].co - edge.verts[1].co).length) for edge in edges]
edge_lengths.sort(key=lambda x: x[1]) # Sort by length, ascending order
# Shortest two edges are most likely opposing
edge_1, edge_2 = edge_lengths[0][0], edge_lengths[1][0]
return edge_1, edge_2
def bisect_and_connect_lines(midpoint_a, midpoint_b):
"""
Bisect the center line and return the midpoint of the bisected line.
"""
bisect_point = (midpoint_a + midpoint_b) / 2.0
return bisect_point
def clean_mesh(bm, merge_distance=0.0001):
"""
Clean the mesh by merging vertices by distance and removing vertices
connected to one or fewer edges, including isolated vertex pairs.
"""
# Merge vertices by distance
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=merge_distance)
# Delete vertices connected to one or fewer edges
for vert in bm.verts:
if len(vert.link_edges) <= 1: # If a vertex has one or fewer edges
bm.verts.remove(vert)
# Check for bi-connected vertices (vertices connected exactly to two other vertices)
for vert in bm.verts:
if len(vert.link_edges) == 2: # Exactly two connections
edge1, edge2 = vert.link_edges
# If both edges connect only back to the original vertex, we can remove it
other_vert_1 = edge1.other_vert(vert)
other_vert_2 = edge2.other_vert(vert)
if len(other_vert_1.link_edges) == 1 and len(other_vert_2.link_edges) == 1:
bm.verts.remove(vert)
break # Only remove the first found bi-connected vertex
def convert_hair_cards_to_center_lines(obj):
if not obj or obj.type != 'MESH':
print(f"Skipping {obj.name}: Not a mesh object.")
return
print(f"Operating on: {obj.name}")
# Access mesh data and create bmesh
mesh = obj.data
bm = bmesh.new()
bm.from_mesh(mesh)
center_lines = []
# Process each quad face
for face in bm.faces:
if len(face.edges) != 4:
continue # Skip non-quad faces
# Find the shortest pair of opposite edges
shortest_edges = find_shortest_edges(face)
if not shortest_edges:
continue
edge_1, edge_2 = shortest_edges
# Calculate midpoints for the shortest edges
midpoint_a = (edge_1.verts[0].co + edge_1.verts[1].co) / 2.0
midpoint_b = (edge_2.verts[0].co + edge_2.verts[1].co) / 2.0
# Bisect the center line
bisected_point = bisect_and_connect_lines(midpoint_a, midpoint_b)
# Create a centerline segment with the bisected point
center_lines.append((midpoint_a, bisected_point))
center_lines.append((midpoint_b, bisected_point))
# Create a new object for the center lines
line_mesh = bpy.data.meshes.new(f"{obj.name}_HairCardCenterLines")
line_obj = bpy.data.objects.new(f"{obj.name}_HairCardCenterLines", line_mesh)
bpy.context.collection.objects.link(line_obj)
# Generate the new mesh data
vertices = [point for line in center_lines for point in line]
edges = [(i * 2, i * 2 + 1) for i in range(len(center_lines))]
line_mesh.from_pydata(vertices, edges, [])
line_mesh.update()
# Now clean up the mesh
bm = bmesh.new()
bm.from_mesh(line_mesh)
clean_mesh(bm)
bm.to_mesh(line_mesh)
bm.free()
print(f"Converted hair cards to center lines and cleaned up for {obj.name}.")
# Iterate over all selected objects
for obj in bpy.context.selected_objects:
convert_hair_cards_to_center_lines(obj)