5

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.

N. Wells
  • 153
  • 7
  • Hello :). Just for clarity, could you please add some screenshots to illustrate? – jachym michal Mar 05 '21 at 11:50
  • See the edits (thanks for the comment)! – N. Wells Mar 05 '21 at 12:25
  • 1
  • @JachymMichal was looking for that one... similarly with bones https://blender.stackexchange.com/questions/54134/straighten-all-bone-chains-in-an-armature/54151#54151 Kinda need to know which vert to keep in place and a vector to align to. – batFINGER Mar 05 '21 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]

    – N. Wells Mar 05 '21 at 13:48

4 Answers4

6

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:

Markus von Broady
  • 36,563
  • 3
  • 30
  • 99
  • 1
    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. – batFINGER Mar 05 '21 at 16:14
  • @batFINGER, YEP, well said. But thank you for posting @MarkusvonBroady! – N. Wells Mar 05 '21 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. – Markus von Broady Mar 05 '21 at 16:18
  • still worth a UV. Makes sense to use two end points to get alignment. Easy enough to scale later. – batFINGER Mar 05 '21 at 16:23
  • @MarkusvonBroady in Blender 2.91 I get this: Traceback (most recent call last): File "/Text", line 21, in File "/Text", line 14, in recursive_add AttributeError: 'NoneType' object has no attribute 'link_edges' – N. Wells Mar 05 '21 at 16:40
  • 1
    Requires an active vert. Edit mode vert select mode, see in screenshot the lighter top vert is active. – batFINGER Mar 05 '21 at 16:42
  • @MarkusvonBroady, PLEASE, if you could adapt your code to work on HALF circles (or any open convex path), i'd love to accept your answer. – N. Wells Mar 05 '21 at 16:43
  • @N.Wells did you try the code? It works for me on half of a circle: https://i.imgur.com/tW9MJsf.gif here's without the other vertices: https://i.imgur.com/Iaa6THj.gif make sure one vertex is active (white) – Markus von Broady Mar 05 '21 at 16:46
  • @MarkusvonBroady, I meant an OPEN half circle. I'm sure I can adapt it myself. But i guess the others need to know too. – N. Wells Mar 05 '21 at 16:51
  • @N.Wells isn't the one here open? https://i.imgur.com/Iaa6THj.gif – Markus von Broady Mar 05 '21 at 16:53
  • @MarkusvonBroady, Indeed it works. I needed to have an "active vertex" (i.e. the last vertex). Thanks a lot for your answer and time. – N. Wells Mar 05 '21 at 16:54
  • @MarkusvonBroady, here are my observations (to make it more perfect): 1) User must make sure that ALL scale transforms have been applied (otherwise) the resulting (unwrapped) path will be scaled; 2) for simplification, the "offset" vector can just be the normalized vector between the two ends of the original path; in that case, the new vectors/coords are just (vector) multiples of this normalized (offset); no need for ratios; 3) (optional) the median point of the selection can be computed before and after the stretching, so that the resulting path can be "centered" just like the original path – N. Wells Mar 05 '21 at 23:28
  • @N.Wells if the ratios aren't needed, then all you need is summing the lengths of selected edges, creating a new edge with that length, and subdividing it. But I thought you need rations to preserve data like this: https://i.imgur.com/fEb9zZV.gif – Markus von Broady Mar 06 '21 at 08:40
  • @MarkusvonBroady, look, you already have the distances to the origin in the individual offsets array; then each vertex in the found array will get its new coordinates set as a vector multiple of its individual offset and the normalized offset vector thus preserving the distances. I did the test using this approach and it works. The code is much easier to read. – N. Wells Mar 06 '21 at 08:58
  • @N.Wells please paste updated code in pastebin/hastebin etc. and I'll look into it. – Markus von Broady Mar 06 '21 at 09:05
  • @MarkusvonBroady, here's the link: https://pastebin.pl/view/1ec49d31 – N. Wells Mar 06 '21 at 13:02
  • @MarkusvonBroady, would love to, but it's Saturday, and I need to think about other things than coding (you know what I mean ...). – N. Wells Mar 06 '21 at 14:15
5

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

N. Wells
  • 153
  • 7
  • 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. – batFINGER Mar 05 '21 at 15:31
  • @batFINGER thanks. I was in a hurry. Hopefully somebody will put it into an add-on (fingers crossed). – N. Wells Mar 05 '21 at 15:59
3

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.

Markus von Broady
  • 36,563
  • 3
  • 30
  • 99
  • That's a solution I was waiting for... I know there must be a way without scripting. Great – Gordon Brinkmann Mar 06 '21 at 09: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. – N. Wells Mar 06 '21 at 13:07
3

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'}, &quot;Select a vert&quot;)
    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),
        )[&quot;faces&quot;][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 &amp; move back so vert &quot;doesn't appear to move&quot;
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()

batFINGER
  • 84,216
  • 10
  • 108
  • 233