5
$\begingroup$

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: Two half circles of the same length (units) The result of Applying S -> X -> 0 (Scale to Zero) As you can see in the last picture, Scale to Zero DOES NOT preserve the path length. It's just a projection!

Thanks in advance.

$\endgroup$
5
  • $\begingroup$ Hello :). Just for clarity, could you please add some screenshots to illustrate? $\endgroup$ Mar 5, 2021 at 11:50
  • $\begingroup$ See the edits (thanks for the comment)! $\endgroup$
    – N. Wells
    Mar 5, 2021 at 12:25
  • 1
    $\begingroup$ Hey :). This answer could help: Transform cylinder to flat plane $\endgroup$ Mar 5, 2021 at 13:02
  • $\begingroup$ @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. $\endgroup$
    – batFINGER
    Mar 5, 2021 at 13:22
  • $\begingroup$ 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] $\endgroup$
    – N. Wells
    Mar 5, 2021 at 13:48

4 Answers 4

6
$\begingroup$

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

def recursive_add(found_vertex):
    found[found_vertex] = 0
    for edge in found_vertex.link_edges:
        for vertex_candidate in edge.verts:
            if vertex_candidate is not found_vertex and vertex_candidate.select:
                found[found_vertex] += 1
                if vertex_candidate not in found:
                    recursive_add(vertex_candidate)

recursive_add(active)
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:

$\endgroup$
18
  • 1
    $\begingroup$ 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. $\endgroup$
    – batFINGER
    Mar 5, 2021 at 16:14
  • $\begingroup$ @batFINGER, YEP, well said. But thank you for posting @MarkusvonBroady! $\endgroup$
    – N. Wells
    Mar 5, 2021 at 16:18
  • $\begingroup$ @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. $\endgroup$ Mar 5, 2021 at 16:18
  • $\begingroup$ still worth a UV. Makes sense to use two end points to get alignment. Easy enough to scale later. $\endgroup$
    – batFINGER
    Mar 5, 2021 at 16:23
  • 1
    $\begingroup$ Requires an active vert. Edit mode vert select mode, see in screenshot the lighter top vert is active. $\endgroup$
    – batFINGER
    Mar 5, 2021 at 16:42
5
$\begingroup$

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:
    bpy.context.collection.objects.link(flt)

Initial half Circle

Flatten path

$\endgroup$
2
  • $\begingroup$ 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. $\endgroup$
    – batFINGER
    Mar 5, 2021 at 15:31
  • $\begingroup$ @batFINGER thanks. I was in a hurry. Hopefully somebody will put it into an add-on (fingers crossed). $\endgroup$
    – N. Wells
    Mar 5, 2021 at 15:59
3
$\begingroup$

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.

$\endgroup$
2
  • $\begingroup$ That's a solution I was waiting for... I know there must be a way without scripting. Great $\endgroup$ Mar 6, 2021 at 9:51
  • $\begingroup$ @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. $\endgroup$
    – N. Wells
    Mar 6, 2021 at 13:07
3
$\begingroup$

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

enter image description here

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):
        x, s = set(), set(vert.link_edges)
        while s - x: # edges growing
            x, s  = s, s.union(
                e for le in s 
                for v in le.verts 
                for e in v.link_edges
                if len(v.link_edges) < 3)
        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()
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.