šŸ§µ Seams to Sewing Pattern (v 0.9, for 2.8 and 2.9)

http://thomaskole.nl/s2s/

This plugin serves two purposes:

  1. Transform meshes with seams into clothsim-ready sewing patterns, for easy iteration.
  2. Export the resulting sewing patterns for printing and sewing irl.

Version 0.7 brings with it remeshing, based on the Boundary Aligned Remesh plugin.
This means that even with very uneven geometry, your resulting sewing pattern with have nice, even triangular geometry. This is important for simulation.

A quick how to:

  1. Add seams to the mesh you wish to turn into a sewing pattern.
    Use the knife tool to add additional cuts for seams if needed.

  2. Go to Object > Seams to Sewing Pattern > Seams to Sewing Pattern

  3. Choose Remesh if you want, and set your target number of triangles.
    A small countdown appears next to the cursor.

  4. Go to Object > Seams to Sewing Pattern > Quick Clothsim to quickly setup a clothsim modifier with the standard parameters.

  5. Simulate, and watch your mesh assemble!

Development now happens on Gitlab. If you can contribute anything, feel free to do so!

SeamsToSewingPattern.zip (version 0.9, for Blender 2.8 and 2.9) (15.3 KB)

Changelog

v 0.7

Hooray, remeshing!


v 0.6
Script is neatly broken up into different parts, and now supports exporting sewing patterns to SVG

v 0.5:
Long (non-square-ish) UV islands would appear offset when unfolded.
This is now fixed.


v 0.4:
Should now work with both blender 2.8 and 2.9


v 0.3:
Added a ā€œprogress barā€ to avoid annoyance


v 0.2:
Actually a plugin you can install via a zip


v 0.1:
Initial version of a script


Old versions

v 0.6:

SeamsToSewingPattern.zip (version 0.6, for Blender 2.8 and 2.9) (6.0 KB)


v 0.5:
SeamsToSewingPattern.zip (version 0.5, for Blender 2.8 and 2.9) (2.7 KB)


v 0.4:
SeamsToSewingPattern.zip (version 0.4, for Blender 2.8 and 2.9) (2.7 KB)


v 0.3:

SeamsToSewingPattern.zip (version 0.3, for Blender 2.8) (2.1 KB)


v 0.2:
SeamsToSewingPattern.zip (version 0.2, for Blender 2.8) (2.0 KB)


v 0.1:

Hereā€™s the first iteration of a script I wrote that turns a mesh, with seams, into a sewing pattern that can be used for cloth sim.

This is especially handy for creating stuffed animals / shaped balloons

Version 0.1 is super dumb, and has no error handling.
Your input should be a mesh with UV seams. It will create a sewing pattern from a UV layout, and then put it back roughly where the original geometry was.

It will also create sewing lines, which you can use in your clothsim to stitch them together,

import bpy
import bmesh
import mathutils
import math

bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')

obj = bpy.context.edit_object
me = obj.data

bpy.ops.mesh.select_mode(type="EDGE")

# select all seams

bm = bmesh.from_edit_mesh(me)
for e in bm.edges:
    if e.seam:
        e.select = True
        
bpy.ops.mesh.bevel(vertex_only=False, offset=0.001)
bpy.ops.mesh.delete(type='ONLY_FACE')

bpy.ops.mesh.select_mode(type="FACE")
faceGroups = []

# isolate all face islands, and UV unwrap each island

faces = set(bm.faces[:])
while faces:
    bpy.ops.mesh.select_all(action='DESELECT')  
    face = faces.pop() 
    face.select = True
    bpy.ops.mesh.select_linked()
    selected_faces = {f for f in faces if f.select}
    selected_faces.add(face) # this or bm.faces above?
    faceGroups.append(selected_faces)
    faces -= selected_faces
    bpy.ops.uv.unwrap()    

bpy.ops.mesh.select_all(action='SELECT') 
bpy.ops.uv.select_all(action='SELECT')
bmesh.update_edit_mesh(me, False)    



uv_layer = bm.loops.layers.uv.active
    
for g in faceGroups:
    previous_area = 0
    bpy.ops.mesh.select_mode(type='FACE')
    bpy.ops.mesh.select_all(action='DESELECT')
    average_position = mathutils.Vector((0,0,0))
    facenum = 0
    average_normal = mathutils.Vector((0,0,0))
    
    # calculate the area, average position, and average normal
    
    for f in g:
        f.select = True
        previous_area += f.calc_area()
        average_position += f.calc_center_median()
        average_normal += f.normal
        facenum += 1
                
    average_normal.normalize()
    
    average_position /= facenum

    average_tangent = mathutils.Vector((0,0,0))
    average_bitangent = mathutils.Vector((0,0,0))

    # calculate a rough tangent and a bitangent

    for face in g:
        for loop in face.loops:       
            uv = loop[uv_layer].uv
            delta = loop.vert.co - average_position
            average_tangent += delta * (uv.x - 0.5)
            average_bitangent += delta * (uv.y - 0.5)
            
    # reorient the tangent and bitangent
    
    average_normal = average_normal.normalized()
    average_tangent = average_tangent.normalized()
    average_bitangent = average_bitangent.normalized()
    halfvector = average_bitangent + average_tangent
    halfvector /= 2
    halfvector.normalize()
    #straighten out half vector
    halfvector = average_normal.cross(halfvector)
    halfvector = average_normal.cross(halfvector)
    cw = mathutils.Matrix.Rotation(math.radians(45.0), 4, average_normal)
    ccw = mathutils.Matrix.Rotation(math.radians(-45.0), 4, average_normal)
    
    average_tangent = mathutils.Vector(halfvector)
    average_tangent.rotate(ccw)
    
    average_bitangent = mathutils.Vector(halfvector)
    average_bitangent.rotate(cw)
    
    # offset each face island by their UV value, using the tangent and bitangent
        
    for face in g:
        for loop in face.loops:       
            uv = loop[uv_layer].uv
            vert = loop.vert
            pos = mathutils.Vector((0,0,0))
            pos += average_position
            pos += average_tangent * -(uv.x - 0.5)
            pos += average_bitangent * -(uv.y - 0.5)
            pos += average_normal * 0.3 #arbitrary - should probably depend on object scale?
            vert.co = pos;
      
    bmesh.update_edit_mesh(me, False)
    
    #resize to match previous area
    
    new_area = sum(f.calc_area() for f in g)
    
    area_ratio = previous_area / new_area
    area_ratio = math.sqrt(area_ratio)
    bpy.ops.transform.resize(value=(area_ratio, area_ratio, area_ratio))
    
# done
    
bmesh.update_edit_mesh(me, False)
bpy.ops.mesh.select_all(action='SELECT') 

Have fun with this first version, let me know if you make something with it!

Todo
Feature State Remarks
Align islands to average face tangent and bitangent Done So that they appear upright in place
Notify the user of progress Done
Make it zip-installable Done
Align islands to the center Done strange UV shapes can appear offset
Export Sewing Pattern in progress Similar to the Export UVā€™s plugin
Add options menu Todo
Create a temporary UV channel the process todo It overrides the active one for now
Make it an option to check stretching beforehand nth
Retopologize UV islands Done maybe using the ā€œtriangleā€ library
Twist seams nth offset each sewing edge by 1 or 2 to twist the part a bit, for realism
98 Likes

Beautiful, this evening I do some tests. you are a Great.

Hi! Thank you for sharing this! Looks awesome!

Thanks people.

Sneaky bugfix edit (in original post)

To make sure the newly placed sewing pattern has the same surface area as the original shape, I calculate the area before and after and scale the pattern by that number.

However, when the XYZ size doubles, the surface area quadruples, resulting in incorrect scale. This has been fixed with a simple sqrt()

5 Likes

This new version works well, I have tried whit older but no way but this new is working.

This is awesomeā€¦!!

greets. giving back. I setup an addon version of this for 2.90.

just adds a menu here : Object > Quick Effects > Quick Cloth from Seams

bl_info = {
        'name': 'SeamToCloth',
        'author': 'Thomas Kole',
        'version': (0, 1),
        'blender': (2, 90, 0),
        'category': 'Cloth',
        'location': 'Object > Quick Effects > Quick Cloth from Seams',
        'wiki_url': ''}

import bpy
import bmesh
import mathutils
import math


def main(self, context):
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='DESELECT')

        obj = bpy.context.edit_object
        me = obj.data

        bpy.ops.mesh.select_mode(type="EDGE")

        # select all seams

        bm = bmesh.from_edit_mesh(me)
        for e in bm.edges:
            if e.seam:
                e.select = True
                
        bpy.ops.mesh.bevel(affect='EDGES', offset=0.001)
        bpy.ops.mesh.delete(type='ONLY_FACE')

        bpy.ops.mesh.select_mode(type="FACE")
        faceGroups = []

        # isolate all face islands, and UV unwrap each island

        faces = set(bm.faces[:])
        while faces:
            bpy.ops.mesh.select_all(action='DESELECT')  
            face = faces.pop() 
            face.select = True
            bpy.ops.mesh.select_linked()
            selected_faces = {f for f in faces if f.select}
            selected_faces.add(face) # this or bm.faces above?
            faceGroups.append(selected_faces)
            faces -= selected_faces
            # bpy.ops.uv.unwrap()    
            bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.05)

        bpy.ops.mesh.select_all(action='SELECT') 
        bpy.ops.uv.select_all(action='SELECT')
        bmesh.update_edit_mesh(me, False)    



        uv_layer = bm.loops.layers.uv.active
            
        for g in faceGroups:
            previous_area = 0
            bpy.ops.mesh.select_mode(type='FACE')
            bpy.ops.mesh.select_all(action='DESELECT')
            average_position = mathutils.Vector((0,0,0))
            facenum = 0
            average_normal = mathutils.Vector((0,0,0))
            
            # calculate the area, average position, and average normal
            
            for f in g:
                f.select = True
                previous_area += f.calc_area()
                average_position += f.calc_center_median()
                average_normal += f.normal
                facenum += 1
                        
            average_normal.normalize()
            
            average_position /= facenum

            average_tangent = mathutils.Vector((0,0,0))
            average_bitangent = mathutils.Vector((0,0,0))

            # calculate a rough tangent and a bitangent

            for face in g:
                for loop in face.loops:       
                    uv = loop[uv_layer].uv
                    delta = loop.vert.co - average_position
                    average_tangent += delta * (uv.x - 0.5)
                    average_bitangent += delta * (uv.y - 0.5)
                    
            # reorient the tangent and bitangent
            
            average_normal = average_normal.normalized()
            average_tangent = average_tangent.normalized()
            average_bitangent = average_bitangent.normalized()
            halfvector = average_bitangent + average_tangent
            halfvector /= 2
            halfvector.normalize()
            #straighten out half vector
            halfvector = average_normal.cross(halfvector)
            halfvector = average_normal.cross(halfvector)
            cw = mathutils.Matrix.Rotation(math.radians(45.0), 4, average_normal)
            ccw = mathutils.Matrix.Rotation(math.radians(-45.0), 4, average_normal)
            
            average_tangent = mathutils.Vector(halfvector)
            average_tangent.rotate(ccw)
            
            average_bitangent = mathutils.Vector(halfvector)
            average_bitangent.rotate(cw)
            
            # offset each face island by their UV value, using the tangent and bitangent
                
            for face in g:
                for loop in face.loops:       
                    uv = loop[uv_layer].uv
                    vert = loop.vert
                    pos = mathutils.Vector((0,0,0))
                    pos += average_position
                    pos += average_tangent * -(uv.x - 0.5)
                    pos += average_bitangent * -(uv.y - 0.5)
                    pos += average_normal * 0.3 #arbitrary - should probably depend on object scale?
                    vert.co = pos;
            
            bmesh.update_edit_mesh(me, False)
            
            #resize to match previous area
            
            new_area = sum(f.calc_area() for f in g)
            
            area_ratio = previous_area / new_area
            area_ratio = math.sqrt(area_ratio)
            bpy.ops.transform.resize(value=(area_ratio, area_ratio, area_ratio))
            
        # done
            
        bmesh.update_edit_mesh(me, False)
        bpy.ops.mesh.select_all(action='SELECT') 

        bpy.ops.object.mode_set(mode='OBJECT', toggle=False)

        objects = bpy.context.selected_objects
        if objects is not None :
            for obj in objects:
                    cloth_mod = obj.modifiers.new(name = 'Cloth', type = 'CLOTH')
                    cloth_mod.settings.use_pressure = True
                    cloth_mod.settings.uniform_pressure_force = 10
                    cloth_mod.settings.use_sewing_springs = True
                    cloth_mod.settings.sewing_force_max = 5
                    cloth_mod.settings.fluid_density = 0.25
                    cloth_mod.settings.effector_weights.gravity = 0




        return{'FINISHED'}


class BR_OT_seam_to_cloth(bpy.types.Operator):
    """Convert closed mesh seams to cloth """
    bl_idname = "view3d.seam_to_cloth"
    bl_label = "Quick Cloth from Seams"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        main(self, context)
        return {'FINISHED'}


def menu_draw_seam_to_cloth(self, context):
    self.layout.operator(BR_OT_seam_to_cloth.bl_idname)


classes = (
    BR_OT_seam_to_cloth,
)

def register():

    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.VIEW3D_MT_object_quick_effects.append(menu_draw_seam_to_cloth)


def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)
    
    bpy.types.VIEW3D_MT_object_quick_effects.remove(menu_draw_seam_to_cloth)

    if __name__ != "__main__":
        bpy.types.VIEW3D_MT_object_quick_effects.remove(menu_draw_seam_to_cloth)

if __name__ == "__main__":
    register()


6 Likes

Wow, thatā€™s awesome!
Thanks!

I was a bit lost on where to put the effect, but Quick Effects is a very smart place.

What about this makes it not work in 2.8?

1 Like


Another test.
Input mesh:

17 Likes

This is awesome.

Used it for my own test. It works so well. I just threw something together and it isnā€™t perfect, but it works so well.

Thanks for sharing it.

8 Likes

2.90 changed the api

this line
bpy.ops.mesh.bevel(vertex_only=False, offset=0.001)

became this
bpy.ops.mesh.bevel(affect=ā€˜EDGESā€™, offset=0.001)

This looks really interesting and I look forward to trying it.

Thanks for the comments so far all.

Iā€™ve updated the script to include a ā€œprogress barā€, the classic black box around your cursor with numbers.
Big meshes with many islands can take a few seconds, now you know it hasnā€™t crashed. You still canā€™t interrupt it though.

The download link is in the top post.

Enjoy!

4 Likes

Many thanks, Thomas and Bay! :slight_smile:

Iā€™ve tried the last update but I canā€™t use it. Both 2.83.5 and 2.9 give me errors.
2.83.5

Error: Traceback (most recent call last):
  File "C:\Users\username\AppData\Roaming\Blender Foundation\Blender\2.
83\scripts\addons\SeamsToSewingPattern\__init__.py", line 165, in execute
    main(self, context)
  File "C:\Users\username\AppData\Roaming\Blender Foundation\Blender\2.
83\scripts\addons\SeamsToSewingPattern\__init__.py", line 154, in main
    win.progress_end()
NameError: name 'win' is not defined

2.9

Error: Traceback (most recent call last):
  File "C:\Users\username\AppData\Roaming\Blender Foundation\Blender\2.
90\scripts\addons\SeamsToSewingPattern\__init__.py", line 165, in execute
    main(self, context)
  File "C:\Users\username\AppData\Roaming\Blender Foundation\Blender\2.
90\scripts\addons\SeamsToSewingPattern\__init__.py", line 34, in main
    bpy.ops.mesh.bevel(vertex_only=False, offset=0.001)
  File "C:\Apps\Blender_Test_Builds\blender-2.90.0-1bced5884c3d-windows64\2.90\s
cripts\modules\bpy\ops.py", line 201, in __call__
    ret = op_call(self.idname_py(), None, kw)
TypeError: Converting py args to operator properties: : keyword "vertex_only" un
recognized

What should be ?

Hi, the 2.83.5 error is harmless, Iā€™ll fix it really quick.
Iā€™m not running 2.9 myself yet but Iā€™ll see if I can make a workaround for that error.

This error should now be gone, and Iā€™ve updated the plugin so that it works with 2.9 as well.
Let me know if you run into anything!

1 Like

Before go onā€¦ How shoould I use it?
Iā€™ve added a Suzanne, select it, went to Quick Effect and then I can see just this
blender_2020-08-27_10-10-01|347x500

Am I wrong?

Make sure you have enough seams to cut along, as you can see in the video in the top post.
That should help you along.