diff --git a/op_island_align_world.py b/op_island_align_world.py index a552d4d..007fb0c 100644 --- a/op_island_align_world.py +++ b/op_island_align_world.py @@ -19,6 +19,10 @@ class op(bpy.types.Operator): bl_description = "Align selected UV islands to world / gravity directions" bl_options = {'REGISTER', 'UNDO'} + bool_face = bpy.props.BoolProperty(name="Per face", default=False, description="Use if every face is an island in uv space; this speeds up the script dramatically.") + bool_simple = bpy.props.BoolProperty(name="Simple align", default=False, description="Only process one edge per island, enough for nearly undistorted uvs.") + steps = bpy.props.IntProperty(name="Iterations", min=1, max=100, soft_min=1, soft_max=5, default=1, description="Using multiple steps (up to 5, usually 2 or 3) is useful in certain cases, especially uv hulls with high localized distortion.") + # is_global = bpy.props.BoolProperty( # name = "Global Axis", # description = "Global or local object axis alignment", @@ -51,14 +55,16 @@ class op(bpy.types.Operator): return True - def execute(self, context): - main(self) + main(self, context) return {'FINISHED'} + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) -def main(context): +def main(self, context): print("\n________________________\nis_global") #Store selection @@ -75,18 +81,20 @@ def main(context): bm = bmesh.from_edit_mesh(bpy.context.active_object.data); uv_layers = bm.loops.layers.uv.verify(); - islands = utilities_uv.getSelectionIslands() - + if self.bool_face: + islands = [[f] for f in bm.faces if f.select and f.loops[0][uv_layers].select] + else: + islands = utilities_uv.getSelectionIslands() - for faces in islands: - # Get average viewport normal of UV island avg_normal = Vector((0,0,0)) - for face in faces: - avg_normal+=face.normal - avg_normal/=len(faces) - - # avg_normal = (obj.matrix_world*avg_normal).normalized() + if self.bool_face: + avg_normal = faces[0].normal + else: + # Get average viewport normal of UV island + for face in faces: + avg_normal+=face.normal + avg_normal/=len(faces) # Which Side x = 0 @@ -94,29 +102,32 @@ def main(context): z = 2 max_size = max(abs(avg_normal.x), abs(avg_normal.y), abs(avg_normal.z)) - # Use multiple steps - for i in range(3): + for i in range(self.steps): # Use multiple steps if(abs(avg_normal.x) == max_size): print("x normal") - align_island(obj, bm, uv_layers, faces, y, z, avg_normal.x < 0, False) - + if self.bool_simple: + align_island_simple(obj, bm, uv_layers, faces, y, z, avg_normal.x < 0, False) + else: + align_island(obj, bm, uv_layers, faces, y, z, avg_normal.x < 0, False) elif(abs(avg_normal.y) == max_size): print("y normal") - align_island(obj, bm, uv_layers, faces, x, z, avg_normal.y > 0, False) - + if self.bool_simple: + align_island_simple(obj, bm, uv_layers, faces, x, z, avg_normal.y > 0, False) + else: + align_island(obj, bm, uv_layers, faces, x, z, avg_normal.y > 0, False) elif(abs(avg_normal.z) == max_size): print("z normal") - align_island(obj, bm, uv_layers, faces, x, y, False, avg_normal.z < 0) + if self.bool_simple: + align_island_simple(obj, bm, uv_layers, faces, x, z, avg_normal.y > 0, False) + else: + align_island(obj, bm, uv_layers, faces, x, y, False, avg_normal.z < 0) print("align island: faces {}x n:{}, max:{}".format(len(faces), avg_normal, max_size)) - - #Restore selection utilities_uv.selection_restore() - def align_island(obj, bm, uv_layers, faces, x=0, y=1, flip_x=False, flip_y=False): # Find lowest and highest verts @@ -126,166 +137,135 @@ def align_island(obj, bm, uv_layers, faces, x=0, y=1, flip_x=False, flip_y=False axis_names = ['x', 'y', 'z'] print("Align shell {}x at {},{} flip {},{}".format(len(faces), axis_names[x], axis_names[y], flip_x, flip_y)) - # print(" Min #{} , Max #{} along '{}'".format(minmax_vert[0].index, minmax_vert[1].index, axis_names[y] )) # print(" A1 {:.1f} , A2 {:.1f} along ".format(minmax_val[0], minmax_val[1] )) - - - + # Collect UV to Vert - vert_to_uv = utilities_uv.get_vert_to_uv(bm, uv_layers) - uv_to_vert = utilities_uv.get_uv_to_vert(bm, uv_layers) - + vert_to_uv = {} + for face in faces: + for loop in face.loops: + vert = loop.vert + uv = loop[uv_layers] + if vert not in vert_to_uv: + vert_to_uv[vert] = [uv]; + else: + vert_to_uv[vert].append(uv) + #uv_to_vert = utilities_uv.get_uv_to_vert(bm, uv_layers) processed_edges = [] - edges = [] + n_edges = 0 + avg_angle = 0 for face in faces: for edge in face.edges: if edge not in processed_edges: processed_edges.append(edge) delta = edge.verts[0].co -edge.verts[1].co max_side = max(abs(delta.x), abs(delta.y), abs(delta.z)) - # Check edges dominant in active axis if( abs(delta[x]) == max_side or abs(delta[y]) == max_side): - # if( abs(delta[y]) == max_side): - edges.append(edge) + n_edges += 1 + uv0 = vert_to_uv[ edge.verts[0] ][0] + uv1 = vert_to_uv[ edge.verts[1] ][0] + + delta_verts = Vector(( + edge.verts[1].co[x] - edge.verts[0].co[x], + edge.verts[1].co[y] - edge.verts[0].co[y] + )) + if flip_x: + delta_verts.x = -edge.verts[1].co[x] + edge.verts[0].co[x] + if flip_y: + delta_verts.y = -edge.verts[1].co[y] + edge.verts[0].co[y] + + delta_uvs = Vector(( + uv1.uv.x - uv0.uv.x, + uv1.uv.y - uv0.uv.y + )) - print("Edges {}x".format(len(edges))) + a0 = math.atan2(delta_verts.y, delta_verts.x) #- math.pi/2 + a1 = math.atan2(delta_uvs.y, delta_uvs.x) #- math.pi/2 + + a_delta = math.atan2(math.sin(a0-a1), math.cos(a0-a1)) + + # Consolidation (math.atan2 gives the lower angle between -Pi and Pi, this triggers errors when using the average avg_angle /= n_edges for rotation angles close to Pi) + if n_edges > 1: + if abs((avg_angle / (n_edges-1)) - a_delta) > 2.8: + if a_delta > 0: + avg_angle+=(a_delta-math.pi*2) + else: + avg_angle+=(a_delta+math.pi*2) + else: + avg_angle+=a_delta + else: + avg_angle+=a_delta + + avg_angle /= n_edges + + print("Edges {}x".format(n_edges)) + print("Turn {:.1f}".format(avg_angle * 180/math.pi)) + + bpy.ops.uv.select_all(action='DESELECT') + for face in faces: + for loop in face.loops: + loop[uv_layers].select = True - avg_angle = 0 - for edge in edges: + bpy.context.tool_settings.transform_pivot_point = 'MEDIAN_POINT' + bpy.ops.transform.rotate(value=-avg_angle, orient_axis='Z') # minus angle; Blender uses unconventional rotation notation (positive for clockwise) + + +def align_island_simple(obj, bm, uv_layers, faces, x=0, y=1, flip_x=False, flip_y=False): + + # Find lowest and highest verts + minmax_val = [0,0] + minmax_vert = [None, None] + + axis_names = ['x', 'y', 'z'] + print("Align shell {}x at {},{} flip {},{}".format(len(faces), axis_names[x], axis_names[y], flip_x, flip_y)) + + # Collect UV to Vert + vert_to_uv = {} + face = faces[0] + for loop in face.loops: + vert = loop.vert + uv = loop[uv_layers] + vert_to_uv[vert] = [uv] + uv.select = True + + edge = faces[0].edges[0] + delta = edge.verts[0].co -edge.verts[1].co + max_side = max(abs(delta.x), abs(delta.y), abs(delta.z)) + + # Check edges dominant in active axis + if abs(delta[x]) == max_side or abs(delta[y]) == max_side : uv0 = vert_to_uv[ edge.verts[0] ][0] uv1 = vert_to_uv[ edge.verts[1] ][0] + delta_verts = Vector(( edge.verts[1].co[x] - edge.verts[0].co[x], edge.verts[1].co[y] - edge.verts[0].co[y] )) - - if flip_x: delta_verts.x = -edge.verts[1].co[x] + edge.verts[0].co[x] if flip_y: delta_verts.y = -edge.verts[1].co[y] + edge.verts[0].co[y] - # delta_verts.y = edge.verts[0].co[y] - edge.verts[1].co[y] - - delta_uvs = Vector(( uv1.uv.x - uv0.uv.x, uv1.uv.y - uv0.uv.y )) - a0 = math.atan2(delta_verts.y, delta_verts.x) - math.pi/2 - a1 = math.atan2(delta_uvs.y, delta_uvs.x) - math.pi/2 + a0 = math.atan2(delta_verts.y, delta_verts.x) + a1 = math.atan2(delta_uvs.y, delta_uvs.x) - - - a_delta = math.atan2(math.sin(a0-a1), math.cos(a0-a1)) - # edge.verts[0].index, edge.verts[1].index - # print(" turn {:.1f} .. {:.1f} , {:.1f}".format(a_delta*180/math.pi, a0*180/math.pi,a1*180/math.pi)) - avg_angle+=a_delta - - - avg_angle/=len(edges) # - math.pi/2 - print("Turn {:.1f}".format(avg_angle * 180/math.pi)) + a_delta = math.atan2(math.sin(a0-a1), math.cos(a0-a1)) - bpy.ops.uv.select_all(action='DESELECT') - for face in faces: - for loop in face.loops: - loop[uv_layers].select = True - + print("Turn {:.1f}".format(a_delta * 180/math.pi)) - bpy.context.tool_settings.transform_pivot_point = 'MEDIAN_POINT' - bpy.ops.transform.rotate(value=avg_angle, orient_axis='Z') - # bpy.ops.transform.rotate(value=0.58191, axis=(-0, -0, -1), constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SPHERE', proportional_size=0.0267348) - - - # processed = [] - - - ''' bpy.ops.uv.select_all(action='DESELECT') for face in faces: - - # Collect UV to Vert for loop in face.loops: loop[uv_layers].select = True - vert = loop.vert - uv = loop[uv_layers] - # vert_to_uv - if vert not in vert_to_uv: - vert_to_uv[vert] = [uv]; - else: - vert_to_uv[vert].append(uv) - # uv_to_vert - if uv not in uv_to_vert: - uv_to_vert[ uv ] = vert; - - - for vert in face.verts: - if vert not in processed: - processed.append(vert) - - vert_y = (vert.co)[y] #obj.matrix_world * - - print("idx {} = {}".format(vert.index, vert_y)) - - if not minmax_vert[0] or not minmax_vert[1]: - minmax_vert[0] = vert - minmax_vert[1] = vert - minmax_val[0] = vert_y - minmax_val[1] = vert_y - continue - - if vert_y < minmax_val[0]: - # Not yet defined or smaller - minmax_vert[0] = vert - minmax_val[0] = vert_y - - elif vert_y > minmax_val[1]: - minmax_vert[1] = vert - minmax_val[1] = vert_y - - - if minmax_vert[0] and minmax_vert[1]: - axis_names = ['x', 'y', 'z'] - print(" Min #{} , Max #{} along '{}'".format(minmax_vert[0].index, minmax_vert[1].index, axis_names[y] )) - # print(" A1 {:.1f} , A2 {:.1f} along ".format(minmax_val[0], minmax_val[1] )) - - vert_A = minmax_vert[0] - vert_B = minmax_vert[1] - uv_A = vert_to_uv[vert_A][0] - uv_B = vert_to_uv[vert_B][0] - - delta_verts = Vector(( - vert_B.co[x] - vert_A.co[x], - vert_B.co[y] - vert_A.co[y] - )) - - delta_uvs = Vector(( - uv_B.uv.x - uv_A.uv.x, - uv_B.uv.y - uv_A.uv.y, - - )) - # Get angles - angle_vert = math.atan2(delta_verts.y, delta_verts.x) - math.pi/2 - angle_uv = math.atan2(delta_uvs.y, delta_uvs.x) - math.pi/2 - - angle_delta = math.atan2(math.sin(angle_vert-angle_uv), math.cos(angle_vert-angle_uv)) - - print(" Angles {:.2f} | {:.2f}".format(angle_vert*180/math.pi, angle_uv*180/math.pi)) - print(" Angle Diff {:.2f}".format(angle_delta*180/math.pi)) - - bpy.context.tool_settings.transform_pivot_point = 'MEDIAN_POINT' - bpy.ops.transform.rotate(value=angle_delta, axis='Z') - # bpy.ops.transform.rotate(value=0.58191, axis=(-0, -0, -1), constraint_axis=(False, False, False), orient_type='GLOBAL', mirror=False, use_proportional_edit=False, proportional_edit_falloff='SPHERE', proportional_size=0.0267348) - - - # bpy.ops.mesh.select_all(action='DESELECT') - # vert_A.select = True - # vert_B.select = True + + bpy.context.tool_settings.transform_pivot_point = 'MEDIAN_POINT' + bpy.ops.transform.rotate(value=-a_delta, orient_axis='Z') # minus angle; Blender uses unconventional rotation notation (positive for clockwise) - # return - ''' bpy.utils.register_class(op)