How to Flatten a vertex path (a list of connected edges) into a straight line while keeping distances?

I'm trying to develop some custom close-body clothing for my characters. For that purpose I tried to "flatten" some half circle (meshes) into straight lines that can be easily extruded then sewed to other parts of the clothing. However the only method I have is the "Scale to Axis" method which is just a projection.

A projection does not preserve the original distance between the vertices. Is there a (simple) method for flattening a half curve into a straight line while preserving distances?

if not can someone suggest a python script that can achieve the same thing?

NB: here's an article on the subject: Addons to straighten edges in Blender However, the above article is old and the tools it mentions (LoopTools, for example) just perform the classic Scale to Axis flattening. EDIT 1 (some screenshots of the "Bad" Scale to Zero flattening technique: As you can see in the last picture, Scale to Zero DOES NOT preserve the path length. It's just a projection!

• Hello :). Just for clarity, could you please add some screenshots to illustrate? Mar 5, 2021 at 11:50
• See the edits (thanks for the comment)! Mar 5, 2021 at 12:25
• Hey :). This answer could help: Transform cylinder to flat plane Mar 5, 2021 at 13:02
• @JachymMichal was looking for that one... similarly with bones blender.stackexchange.com/questions/54134/… Kinda need to know which vert to keep in place and a vector to align to. Mar 5, 2021 at 13:22
• Hi@JachymMichal indeed, the script works ... only for a full circle. How can I modify it to be used for a half circle?[code]import bpy from math import atan2,sqrt cyl = bpy.context.object if cyl: flt = cyl.copy() me = flt.data.copy() flt.data = me bpy.context.collection.objects.link(flt) min_r = 100000 for v in me.vertices: [x,y,z] = v.co r = v.co.xy.length theta = atan2(x,y) v.co = (theta,r,z) min_r = min([r,min_r]) for v in me.vertices: v.co.x *= min_r v.co.y -= min_r flt.location.y += min_r[/code] Mar 5, 2021 at 13:48

I see I'm late, but here's my take on this problem:

import bpy, bmesh
from functools import reduce
from mathutils import Vector

mesh = bpy.context.edit_object.data
bm = bmesh.from_edit_mesh(mesh)
active = bm.select_history.active

found = {}  # key: vertex object, value: number of connected vertices, which are selected

found[found_vertex] = 0
for vertex_candidate in edge.verts:
found[found_vertex] += 1
if vertex_candidate not in found:

ends = [vertex for vertex, connections in found.items() if connections == 1]
origin = ends[0].co
offset = ends[1].co - origin
individual_offsets = [0]
current_path_length = 0

def analyze_path(a, b):
global current_path_length
distance = (a.co - b.co).length
individual_offsets.append(current_path_length + distance)
current_path_length += distance
return b

reduce(analyze_path, found)

# Edit: as @batFINGER mentioned, OP asks to keep the original length of the path:
ratio = current_path_length / offset.length
offset *= ratio

for v, individual_offset in zip(found, individual_offsets):
v.co = origin + (individual_offset / current_path_length) * offset

bmesh.update_edit_mesh(mesh)


at least one of selected vertices has to be active, and it only works in vertex selection mode:

Edit: as pointed by batFINGER, this didn't keep the length of the path, so I added a small update:

• Of opinion OP wanted to keep edge length. If that was half circle being collapsed to diameter, the semi circle would have length pi * r whereas the diameter is 2 * r. Mar 5, 2021 at 16:14
• @batFINGER, YEP, well said. But thank you for posting @MarkusvonBroady! Mar 5, 2021 at 16:18
• @batFINGER ah, indeed, I intended to ask if that's the case but forgot and assumed he was imprecise and intended to keep the proportions. I'll update the code in a moment. Mar 5, 2021 at 16:18
• still worth a UV. Makes sense to use two end points to get alignment. Easy enough to scale later. Mar 5, 2021 at 16:23
• Requires an active vert. Edit mode vert select mode, see in screenshot the lighter top vert is active. Mar 5, 2021 at 16:42

After long hours, here's an answer to my OWN question, thanks to comments from @Jachym Michal and @batFINGER.

import bpy
import math

def rotatePoint(centerPoint,point,angle):
"""Rotates a point around another centerPoint. Angle is in degrees.
Rotation is counter-clockwise"""
temp_point = point[0]-centerPoint[0] , point[1]-centerPoint[1]
temp_point = ( temp_point[0]*math.cos(angle)-temp_point[1]*math.sin(angle) , temp_point[0]*math.sin(angle)+temp_point[1]*math.cos(angle))
temp_point = temp_point[0]+centerPoint[0] , temp_point[1]+centerPoint[1]
return temp_point

def crossProduct(a, b):
c = [a[1]*b[2] - a[2]*b[1],
a[2]*b[0] - a[0]*b[2],
a[0]*b[1] - a[1]*b[0]]

return c

def dotProduct(a, b):
c = a[0]*b[0] + a[1]*b[1]+ a[2]*b[2]
return c

cyl = bpy.context.object
if cyl:
flt = cyl.copy()
me = flt.data.copy()
flt.data = me

assert(len(me.vertices) >= 3)

verts = me.vertices
Nvert = len(me.vertices)
print('Nvert=', Nvert)

nVertRots = 0

for n in range(Nvert - 1, 1, -1):
print('Vertex:',n)
v1 = verts[n - 1].co - verts[n - 2].co

v2 = verts[n].co - verts[n-1].co

v1mag = math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z)

v1norm = [v1.x/v1mag , v1.y/v1mag , v1.z/v1mag]

v2mag = math.sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z)

v2norm = [v2.x/v2mag , v2.y/v2mag , v2.z/v2mag]

res = v1norm[0] * v2norm[0] + v1norm[1] * v2norm[1] + v1norm[2] * v2norm[2]

angle = math.acos(res)

cross = crossProduct(v1norm, v2norm)
sign = dotProduct([0,0,1], cross)
if sign < 0:
angle = -angle

print('Angle(1,2):', angle)

nVertRots += 1
for m in range(n, Nvert, +1):
v2rot = rotatePoint([verts[n - 1].co.x, verts[n - 1].co.y],[verts[m].co.x, verts[m].co.y], -angle)
verts[m].co = (v2rot[0], v2rot[1], verts[0].co.z)

print('nVertRots:',nVertRots)
if nVertRots > 0:


• A heads up that vectors in blender eg v = ob.location have vector operations built in... eg v.dot((1, 1, 0)) and v.length and v.cross(v) and v.normalized() etc. Mar 5, 2021 at 15:31
• @batFINGER thanks. I was in a hurry. Hopefully somebody will put it into an add-on (fingers crossed). Mar 5, 2021 at 15:59

Scriptless solution

Go to Edit -> Preferences... Add-ons, and enable MeasureIt and LoopTools

Now select a path that interests you, press N for Numbers Panel, click on View tab, then expand MeasureIt panel at the bottom. Choose a Sum group and click on Segment button:

Then scroll down to the Totals section to see the total length:

You can scroll back up to Items section to change the precision if needed:

Right-click your mesh, and from LoopTools choose Gstretch:

Change from Spread evenly to just Spread to keep proportions, then press S for scaling, / to divide current length, and type the length you see on the right on the MeasureIt panel. Press Enter to confirm, then press S again and this time type the length we had before Gstretching: in this case 3.14. Press Enter to confirm.

• That's a solution I was waiting for... I know there must be a way without scripting. Great Mar 6, 2021 at 9:51
• @MarkusvonBroady, great post. However, when you have to do it on dozens of path (at different angles), you'll tire fast. I think someone may contact the LoopTools team to include your script in the next release. Mar 6, 2021 at 13:07

YAF one.

_Also late to the party, cooked a GPU, can report 2.91 runs with a Gforce 220 lol _

For a string of edges can use the method from How can I sort vertex positions sequentially indices in a closed area? to fill the edges. The new face will have edges in its winding order. The new edge to close path is used to set order.

Face edges angle - python

Added a simple UI to also make it "without scripting" 8)_ (Be a quick task re-jigging to use code from @MVB's excellent answer)

To use, select a single vert of the edge string, and the axis to align to. Keeps the selected vert in place.

import bpy
import bmesh
from mathutils import Matrix
from itertools import cycle, islice

def main(op, context, align):
def con_edges(vert):
while s - x: # edges growing
x, s  = s, s.union(
e for le in s
for v in le.verts
return list(x)

ob = context.edit_object
me = ob.data
bm = bmesh.from_edit_mesh(me)
av = bm.select_history.active
if not isinstance(av, bmesh.types.BMVert):
op.report({'INFO'}, "Select a vert")
return {'CANCELLED'}
# keep at active vert location.
loc = av.co.copy()
edges = set(bm.edges)

f = bmesh.ops.contextual_create(
bm,
geom=con_edges(av),
)["faces"][0]
# will throw error if cyclic.
new_edge =  (set(bm.edges) - edges).pop()
edges = f.edges[:]
for e in bm.edges:
e.select_set(e in edges)

i = edges.index(new_edge) + 1
# direction
if align == 'ENDS':
vec = (new_edge.verts[1].co - new_edge.verts[0].co).normalized()
elif align in 'XYZ':
vec = Matrix.Identity(3)['XYZ'.index(align)]
# edge loop around face
loop = [(v, e, e.calc_length()) for v, e in islice(cycle(zip(f.verts, f.edges)),i, i + len(f.edges) - 1)]
for v0, e, l in loop:
#v0 = f.verts[edges.index(e)]
v1 = e.other_vert(v0)
v1.co = v0.co + l * vec
#remove face link edge & move back so vert "doesn't appear to move"
bm.edges.remove(new_edge)
bm.transform(
Matrix.Translation((loc - av.co))
)

bmesh.update_edit_mesh(me)
return {'FINISHED'}

class StraightenEdgeOperator(bpy.types.Operator):
"""Straighten Edges"""
bl_idname = "mesh.straighten_edges"
bl_label = "Straighten Edges"
bl_options = {'REGISTER', 'UNDO'}
align : bpy.props.EnumProperty(
items=[
('X', 'X Axis', ''),
('Y', 'Y Axis', ''),
('Z', 'Z Axis', ''),
('ENDS', 'Segment Ends', ''),
],
name="Align",
default = 'ENDS',
)
def draw(self, context):
layout = self.layout
layout.prop(self, "align", expand=True)
@classmethod
def poll(cls, context):
return context.edit_object is not None and context.mode.startswith('EDIT_MESH')

def execute(self, context):
return main(self, context, self.align)

def register():
bpy.utils.register_class(StraightenEdgeOperator)

def unregister():
bpy.utils.unregister_class(StraightenEdgeOperator)

if __name__ == "__main__":
register()