From ff036d66ce91660dd0980116493929485140db6c Mon Sep 17 00:00:00 2001 From: franMarz <58062362+franMarz@users.noreply.github.com> Date: Tue, 15 Dec 2020 21:12:17 +0100 Subject: [PATCH] Extended support for multi-object UV editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Align, Sort, Crop, Fill and Select Similar now work with multiple objects while editing the UVs. - Now, Select Overlap behaves like in previous versions: all but one of the overlapping islands are selected, making it posible to manually fix the overlapping by using this operator in conjunction with manually moving the selected islands. - Further optimization of the 90ยบ Rotate Island operator. - Some cleanup and minor corrections in op_smoothing_uv_islands, op_select_islands_flipped, op_select_islands_outline, op_rectify, settings, __init__ and LICENSE.txt --- LICENSE.txt | 2 +- __init__.py | 2 +- op_align.py | 50 ++++----- op_island_align_sort.py | 102 ++++++------------- op_island_rotate_90.py | 40 ++------ op_rectify.py | 12 +-- op_select_islands_flipped.py | 10 +- op_select_islands_identical.py | 27 +++-- op_select_islands_outline.py | 11 +- op_select_islands_overlap.py | 12 +++ op_smoothing_uv_islands.py | 12 +-- op_uv_crop.py | 50 +++++---- op_uv_fill.py | 86 +++------------- settings.py | 6 +- utilities_uv.py | 179 ++++++++++++++++++++++++--------- 15 files changed, 297 insertions(+), 304 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 118e934..2116261 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -23,6 +23,6 @@ # Copyright (C) <2014> # https://github.com/JoseConseco/UvSquares/blob/master/uv_squares.py # -# Current maintainers Sav Martin, FranMarz. +# Current maintainers Sav Martin, franMarz. # # Icons art. DavidRivera (activemotionpictures) diff --git a/__init__.py b/__init__.py index 3b6ec02..86178b7 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "TexTools", "description": "Professional UV and Texture tools for Blender.", - "author": "renderhjs, (Port to 2.80 by Sav Martin), FranMarz", + "author": "renderhjs, (Port to 2.80 by Sav Martin), franMarz", "version": (1, 4, 00), "blender": (2, 80, 0), "category": "UV", diff --git a/op_align.py b/op_align.py index ddf99eb..080b3e1 100644 --- a/op_align.py +++ b/op_align.py @@ -4,8 +4,6 @@ from mathutils import Vector from collections import defaultdict from math import pi, sqrt -from numpy import median - from . import utilities_uv @@ -34,7 +32,7 @@ def poll(cls, context): #Requires UV map if not bpy.context.object.data.uv_layers: - return False #self.report({'WARNING'}, "Object must have more than one UV map") + return False # Not in Synced mode if bpy.context.scene.tool_settings.use_uv_select_sync: @@ -44,29 +42,32 @@ def poll(cls, context): def execute(self, context): + all_ob_bounds = utilities_uv.multi_object_loop(utilities_uv.getSelectionBBox, need_results=True) + + select = False + for ob_bounds in all_ob_bounds: + if len(ob_bounds) > 0 : + select = True + break + if not select: + return {'CANCELLED'} + + boundsAll = utilities_uv.getMultiObjectSelectionBBox(all_ob_bounds) + utilities_uv.multi_object_loop(align, context, self.direction, boundsAll) - align(context, self.direction) return {'FINISHED'} -def align(context, direction): - #Store selection - utilities_uv.selection_store() +def align(context, direction, boundsAll): - if bpy.context.tool_settings.transform_pivot_point != 'CURSOR': - bpy.context.tool_settings.transform_pivot_point = 'CURSOR' + prepivot = bpy.context.space_data.pivot_point + bpy.context.space_data.pivot_point = 'CURSOR' #B-Mesh obj = bpy.context.active_object - bm = bmesh.from_edit_mesh(obj.data); - uv_layers = bm.loops.layers.uv.verify(); - - if len(obj.data.uv_layers) == 0: - print("There is no UV channel or UV data set") - return + bm = bmesh.from_edit_mesh(obj.data) + uv_layers = bm.loops.layers.uv.verify() - # Collect BBox sizes - boundsAll = utilities_uv.getSelectionBBox() center_all = boundsAll['center'] mode = bpy.context.scene.tool_settings.uv_select_mode @@ -80,19 +81,19 @@ def align(context, direction): if direction == "bottom": delta = boundsAll['min'] - bounds['min'] - utilities_uv.move_island(island, 0,delta.y) + utilities_uv.move_island(island, 0, delta.y) elif direction == "top": delta = boundsAll['max'] - bounds['max'] - utilities_uv.move_island(island, 0,delta.y) + utilities_uv.move_island(island, 0, delta.y) elif direction == "left": delta = boundsAll['min'] - bounds['min'] - utilities_uv.move_island(island, delta.x,0) + utilities_uv.move_island(island, delta.x, 0) elif direction == "right": delta = boundsAll['max'] - bounds['max'] - utilities_uv.move_island(island, delta.x,0) + utilities_uv.move_island(island, delta.x, 0) elif direction == "center": delta = Vector((center_all - center)) @@ -106,9 +107,8 @@ def align(context, direction): delta = Vector((center_all - center)) utilities_uv.move_island(island, delta.x, 0) - else: - print("Unkown direction: "+str(direction)) + print("Unknown direction: "+str(direction)) elif mode == 'EDGE' or mode == 'VERTEX': @@ -132,8 +132,8 @@ def align(context, direction): bmesh.update_edit_mesh(obj.data) - #Restore selection - # utilities_uv.selection_restore() + bpy.context.space_data.pivot_point = prepivot + bpy.utils.register_class(op) diff --git a/op_island_align_sort.py b/op_island_align_sort.py index 6b9df02..59d82be 100644 --- a/op_island_align_sort.py +++ b/op_island_align_sort.py @@ -51,55 +51,46 @@ def poll(cls, context): def execute(self, context): - main(context, self.is_vertical, self.padding) + utilities_uv.multi_object_loop(main, context, self.is_vertical, self.padding) + + all_ob_bounds = utilities_uv.multi_object_loop(utilities_uv.getSelectionBBox, need_results=True) + utilities_uv.multi_object_loop(relocate, context, self.is_vertical, self.padding, all_ob_bounds, ob_num=0) + return {'FINISHED'} def main(context, isVertical, padding): print("Executing IslandsAlignSort main {}".format(padding)) - + #Store selection utilities_uv.selection_store() - if bpy.context.tool_settings.transform_pivot_point != 'CURSOR': - bpy.context.tool_settings.transform_pivot_point = 'CURSOR' - - #Only in Face or Island mode - if bpy.context.scene.tool_settings.uv_select_mode is not 'FACE' or 'ISLAND': - bpy.context.scene.tool_settings.uv_select_mode = 'FACE' + bpy.context.tool_settings.transform_pivot_point = 'CURSOR' + bpy.context.scene.tool_settings.uv_select_mode = 'FACE' - bm = bmesh.from_edit_mesh(bpy.context.active_object.data); - uv_layers = bm.loops.layers.uv.verify(); + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() - boundsAll = utilities_uv.getSelectionBBox() - - islands = utilities_uv.getSelectionIslands() allSizes = {} #https://stackoverflow.com/questions/613183/sort-a-python-dictionary-by-value allBounds = {} - print("Islands: "+str(len(islands))+"x") - - bpy.context.window_manager.progress_begin(0, len(islands)) - #Rotate to minimal bounds for i in range(0, len(islands)): - alignIslandMinimalBounds(uv_layers, islands[i]) + # Select Island + bpy.ops.uv.select_all(action='DESELECT') + utilities_uv.set_selected_faces(islands[i]) + + utilities_uv.alignMinimalBounds(uv_layers=uv_layers) # Collect BBox sizes bounds = utilities_uv.getSelectionBBox() - allSizes[i] = max(bounds['width'], bounds['height']) + i*0.000001;#Make each size unique - allBounds[i] = bounds; - print("Rotate compact: "+str(allSizes[i])) - - bpy.context.window_manager.progress_update(i) - - bpy.context.window_manager.progress_end() - + allSizes[i] = max(bounds['width'], bounds['height']) + i*0.000001 #Make each size unique + allBounds[i] = bounds #Position by sorted size in row - sortedSizes = sorted(allSizes.items(), key=operator.itemgetter(1))#Sort by values, store tuples + sortedSizes = sorted(allSizes.items(), key=operator.itemgetter(1)) #Sort by values, store tuples sortedSizes.reverse() offset = 0.0 for sortedSize in sortedSizes: @@ -113,59 +104,32 @@ def main(context, isVertical, padding): #Offset Island if(isVertical): - delta = Vector((boundsAll['min'].x - bounds['min'].x, boundsAll['max'].y - bounds['max'].y)); + delta = Vector((boundsAll['min'].x - bounds['min'].x, boundsAll['max'].y - bounds['max'].y)) bpy.ops.transform.translate(value=(delta.x, delta.y-offset, 0)) offset += bounds['height']+padding else: - print("Horizontal") - delta = Vector((boundsAll['min'].x - bounds['min'].x, boundsAll['max'].y - bounds['max'].y)); + delta = Vector((boundsAll['min'].x - bounds['min'].x, boundsAll['max'].y - bounds['max'].y)) bpy.ops.transform.translate(value=(delta.x+offset, delta.y, 0)) offset += bounds['width']+padding - #Restore selection utilities_uv.selection_restore() -def alignIslandMinimalBounds(uv_layers, faces): - # Select Island - bpy.ops.uv.select_all(action='DESELECT') - utilities_uv.set_selected_faces(faces) - - steps = 8 - angle = 45; # Starting Angle, half each step - - bboxPrevious = utilities_uv.getSelectionBBox() - - for i in range(0, steps): - # Rotate right - bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z') - bbox = utilities_uv.getSelectionBBox() - - if i == 0: - sizeA = bboxPrevious['width'] * bboxPrevious['height'] - sizeB = bbox['width'] * bbox['height'] - if abs(bbox['width'] - bbox['height']) <= 0.0001 and sizeA < sizeB: - # print("Already squared") - bpy.ops.transform.rotate(value=(-angle * math.pi / 180), orient_axis='Z') - break; - - - if bbox['minLength'] < bboxPrevious['minLength']: - bboxPrevious = bbox; # Success - else: - # Rotate Left - bpy.ops.transform.rotate(value=(-angle*2 * math.pi / 180), orient_axis='Z') - bbox = utilities_uv.getSelectionBBox() - if bbox['minLength'] < bboxPrevious['minLength']: - bboxPrevious = bbox; # Success +def relocate(context, isVertical, padding, all_ob_bounds, ob_num=0): + if ob_num > 0 : + offset = 0.0 + for i in range(0, ob_num): + if isVertical: + offset += all_ob_bounds[i]['height']+padding else: - # Restore angle of this iteration - bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z') - - angle = angle / 2 + offset += all_ob_bounds[i]['width']+padding + if isVertical: + delta = Vector((all_ob_bounds[0]['min'].x - all_ob_bounds[ob_num]['min'].x, all_ob_bounds[0]['max'].y - all_ob_bounds[ob_num]['max'].y)) + bpy.ops.transform.translate(value=(delta.x, delta.y-offset, 0)) + else: + delta = Vector((all_ob_bounds[0]['min'].x - all_ob_bounds[ob_num]['min'].x, all_ob_bounds[0]['max'].y - all_ob_bounds[ob_num]['max'].y)) + bpy.ops.transform.translate(value=(delta.x+offset, delta.y, 0)) - if bboxPrevious['width'] < bboxPrevious['height']: - bpy.ops.transform.rotate(value=(90 * math.pi / 180), orient_axis='Z') bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_island_rotate_90.py b/op_island_rotate_90.py index c1d66a3..c5a8ec6 100644 --- a/op_island_rotate_90.py +++ b/op_island_rotate_90.py @@ -45,41 +45,15 @@ def poll(cls, context): def execute(self, context): - utilities_uv.multi_object_loop(main, context, self.angle) - return {'FINISHED'} - - -def main(context, angle): + bpy.ops.uv.select_linked() - #Store selection - utilities_uv.selection_store() - - bm = bmesh.from_edit_mesh(bpy.context.active_object.data) - uv_layers = bm.loops.layers.uv.verify() - - bpy.ops.uv.select_linked() + angle = self.angle + bversion = float(bpy.app.version_string[0:4]) + if bversion == 2.80 or bversion == 2.81 or bversion == 2.82 or bversion == 2.90: + angle = -angle + bpy.ops.transform.rotate(value=-angle, orient_axis='Z', constraint_axis=(False, False, False), use_proportional_edit=False) - #Bounds - #bounds_initial = utilities_uv.getSelectionBBox() - - # bpy.ops.transform.rotate behaves differently based on the version of Blender on the UV Editor. Not expected to be fixed for every version of master - bversion = float(bpy.app.version_string[0:4]) - if bversion == 2.80 or bversion == 2.81 or bversion == 2.82 or bversion == 2.90: - angle = -angle - bpy.ops.transform.rotate(value=-angle, orient_axis='Z', constraint_axis=(False, False, False), use_proportional_edit=False) - - #Align rotation to top left|right - # bounds_post = utilities_uv.getSelectionBBox() - # dy = bounds_post['max'].y - bounds_initial['max'].y - # dx = 0 - # if angle > 0: - # dx = bounds_post['max'].x - bounds_initial['max'].x - # else: - # dx = bounds_post['min'].x - bounds_initial['min'].x - # bpy.ops.transform.translate(value=(-dx, -dy, 0), constraint_axis=(False, False, False), use_proportional_edit=False) - - #Restore selection - utilities_uv.selection_restore() + return {'FINISHED'} bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_rectify.py b/op_rectify.py index 4454f7d..de14656 100644 --- a/op_rectify.py +++ b/op_rectify.py @@ -66,10 +66,10 @@ def main(square = False, snapToClosest = False): face_act = bm.faces.active targetFace = face_act - + #if len(bm.faces) > allowedFaces: - # operator.report({'ERROR'}, "selected more than " +str(allowedFaces) +" allowed faces.") - # return + # operator.report({'ERROR'}, "selected more than " +str(allowedFaces) +" allowed faces.") + # return edgeVerts, filteredVerts, selFaces, nonQuadFaces, vertsDict, noEdge = ListsOfVerts(uv_layers, bm) @@ -79,8 +79,8 @@ def main(square = False, snapToClosest = False): return cursorClosestTo = CursorClosestTo(filteredVerts) + #line is selected - if len(selFaces) is 0: if snapToClosest is True: SnapCursorToClosestSelected(filteredVerts) @@ -154,7 +154,7 @@ def ListsOfVerts(uv_layers, bm): else: isFaceSel = False allEdgeVerts.extend(facesEdgeVerts) - if isFaceSel: + if isFaceSel: if len(f.verts) is not 4: nonQuadFaces.append(f) edgeVerts.extend(facesEdgeVerts) @@ -168,7 +168,7 @@ def ListsOfVerts(uv_layers, bm): vertsDict[(x, y)].append(luv) else: edgeVerts.extend(facesEdgeVerts) - + noEdge = False if len(edgeVerts) is 0: noEdge = True diff --git a/op_select_islands_flipped.py b/op_select_islands_flipped.py index e38edec..f9c5272 100644 --- a/op_select_islands_flipped.py +++ b/op_select_islands_flipped.py @@ -61,7 +61,6 @@ def select_flipped(context): bpy.context.scene.tool_settings.use_uv_select_sync = False bpy.ops.uv.select_all(action='DESELECT') - for island in islands: is_flipped = False @@ -90,7 +89,6 @@ def select_flipped(context): - class Island_bounds: faces = [] center = Vector([0,0]) @@ -98,8 +96,8 @@ class Island_bounds: max = Vector([0,0]) def __init__(self, faces): - bm = bmesh.from_edit_mesh(bpy.context.active_object.data); - uv_layers = bm.loops.layers.uv.verify(); + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() # Collect topology stats self.faces = faces @@ -113,8 +111,6 @@ def __init__(self, faces): self.min = bounds['min'] self.max = bounds['max'] - - def isEqual(A, B): # Bounding Box AABB intersection? @@ -125,7 +121,7 @@ def isEqual(A, B): if not (max_x < min_x or max_y < min_y): return True - return False + bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_select_islands_identical.py b/op_select_islands_identical.py index 20b4c3b..5edfa04 100644 --- a/op_select_islands_identical.py +++ b/op_select_islands_identical.py @@ -42,23 +42,30 @@ def poll(cls, context): def execute(self, context): - swap(self, context) + island_stats_source_list = utilities_uv.multi_object_loop(island_find, self, context, need_results = True) + island_stats_source = next((island_stats_source for island_stats_source in island_stats_source_list if island_stats_source is not None), None) + if island_stats_source is None: + self.report({'ERROR_INVALID_INPUT'}, "Please select only 1 UV Island") + return {'CANCELLED'} + utilities_uv.multi_object_loop(swap, self, context, island_stats_source) return {'FINISHED'} -def swap(self, context): - +def island_find(self, context): bm = bmesh.from_edit_mesh(bpy.context.active_object.data) uv_layers = bm.loops.layers.uv.verify() - # Get selected island islands = utilities_uv.getSelectionIslands() + if len(islands) > 0 : + island_stats_source = Island_stats(islands[0]) + utilities_uv.multi_object_loop_stop = True + return island_stats_source - if len(islands) != 1: - self.report({'ERROR_INVALID_INPUT'}, "Please select only 1 UV Island") - return - - island_stats_source = Island_stats(islands[0]) + +def swap(self, context, island_stats_source): + + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() bpy.context.scene.tool_settings.uv_select_mode = 'FACE' bpy.ops.uv.select_all(action='SELECT') @@ -71,8 +78,6 @@ def swap(self, context): if island_stats_source.isEqual(island_stats): islands_equal.append(island_stats.faces) - print("Islands: "+str(len(islands_equal))+"x") - bpy.ops.uv.select_all(action='DESELECT') for island in islands_equal: for face in island: diff --git a/op_select_islands_outline.py b/op_select_islands_outline.py index 364beb9..0a246d5 100644 --- a/op_select_islands_outline.py +++ b/op_select_islands_outline.py @@ -38,14 +38,14 @@ def poll(cls, context): def execute(self, context): - utilities_uv.multi_object_loop(select_outline, context) + utilities_uv.multi_object_loop(select_outline, self, context) return {'FINISHED'} -def select_outline(context): +def select_outline(self, context): - bm = bmesh.from_edit_mesh(bpy.context.active_object.data); - uv_layers = bm.loops.layers.uv.verify(); + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() # Store previous edge seams edges_seam = [edge for edge in bm.edges if edge.seam] @@ -54,7 +54,7 @@ def select_outline(context): contextViewUV = utilities_ui.GetContextViewUV() if not contextViewUV: self.report({'ERROR_INVALID_INPUT'}, "This tool requires an available UV/Image view.") - return + return {'CANCELLED'} # Create seams from islands bpy.ops.uv.seams_from_islands(contextViewUV) @@ -97,7 +97,6 @@ def select_outline(context): if edge.is_boundary or edge in edges_seams_from_islands: edges.extend([e for e in edge.verts[0].link_loops]) edges.extend([e for e in edge.verts[1].link_loops]) - #edges.append(edge) bpy.context.scene.tool_settings.uv_select_mode = 'EDGE' for face in faces_islands: diff --git a/op_select_islands_overlap.py b/op_select_islands_overlap.py index b5e7731..14c0534 100644 --- a/op_select_islands_overlap.py +++ b/op_select_islands_overlap.py @@ -45,9 +45,21 @@ def poll(cls, context): def execute(self, context): bpy.ops.uv.select_overlap() bpy.ops.uv.select_linked() + utilities_uv.multi_object_loop(deselect, self, context) return {'FINISHED'} +def deselect(self, context): + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() + + islands = utilities_uv.getSelectionIslands() + if len(islands) > 0 : + for face in islands[0]: + for loop in face.loops: + loop[uv_layers].select = False + utilities_uv.multi_object_loop_stop = True + """ def selectOverlap(context): print("Execute op_select_islands_overlap") diff --git a/op_smoothing_uv_islands.py b/op_smoothing_uv_islands.py index d852d2e..1e42a3d 100644 --- a/op_smoothing_uv_islands.py +++ b/op_smoothing_uv_islands.py @@ -5,10 +5,10 @@ from mathutils import Vector from collections import defaultdict - from . import utilities_uv from . import utilities_ui + class op(bpy.types.Operator): bl_idname = "uv.textools_smoothing_uv_islands" bl_label = "Apply smooth normals and hard edges for UV Island borders." @@ -34,8 +34,9 @@ def execute(self, context): return {'FINISHED'} - def smooth_uv_islands(self, context): + + premode = (bpy.context.active_object.mode) bpy.ops.object.mode_set(mode='EDIT') #Store selection @@ -65,13 +66,10 @@ def smooth_uv_islands(self, context): bpy.context.object.data.use_auto_smooth = True bpy.context.object.data.auto_smooth_angle = math.pi - # Apply Edge split modifier - # bpy.ops.object.modifier_add(type='EDGE_SPLIT') - # bpy.context.object.modifiers["EdgeSplit"].use_edge_angle = False - # Restore selection utilities_uv.selection_restore() - + + bpy.ops.object.mode_set(mode=premode) bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_uv_crop.py b/op_uv_crop.py index 842cd50..24680ac 100644 --- a/op_uv_crop.py +++ b/op_uv_crop.py @@ -33,34 +33,48 @@ def poll(cls, context): #Requires UV map if not bpy.context.object.data.uv_layers: return False - + + #Not in Synced mode + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + return True def execute(self, context): - crop(self, context) - return {'FINISHED'} + all_ob_bounds = utilities_uv.multi_object_loop(utilities_uv.getSelectionBBox, need_results=True) + select = False + for ob_bounds in all_ob_bounds: + if len(ob_bounds) > 0 : + select = True + break + if not select: + return {'CANCELLED'} + + boundsAll = utilities_uv.getMultiObjectSelectionBBox(all_ob_bounds) + prepivot = bpy.context.space_data.pivot_point + precursor = tuple(bpy.context.space_data.cursor_location) + bpy.context.space_data.pivot_point = 'CURSOR' + bpy.context.space_data.cursor_location = (0.0, 0.0) -def crop(self, context): - bm = bmesh.from_edit_mesh(bpy.context.active_object.data); - uv_layers = bm.loops.layers.uv.verify(); + padding = utilities_ui.get_padding() - padding = utilities_ui.get_padding() + # Scale to fit bounds + scale_u = (1.0-padding) / boundsAll['width'] + scale_v = (1.0-padding) / boundsAll['height'] + scale = min(scale_u, scale_v) - - # Scale to fit bounds - bbox = utilities_uv.getSelectionBBox() - scale_u = (1.0-padding) / bbox['width'] - scale_v = (1.0-padding) / bbox['height'] - scale = min(scale_u, scale_v) + bpy.ops.transform.resize(value=(scale, scale, 1), constraint_axis=(False, False, False), mirror=False, use_proportional_edit=False) - bpy.ops.transform.resize(value=(scale, scale, scale), constraint_axis=(False, False, False), mirror=False, use_proportional_edit=False) + # Reposition + delta_position = Vector((padding/2,1-padding/2)) - Vector((scale*boundsAll['min'].x, scale*boundsAll['min'].y + scale*boundsAll['height'])) + bpy.ops.transform.translate(value=(delta_position.x, delta_position.y, 0)) - # Reposition - bbox = utilities_uv.getSelectionBBox() + bpy.context.space_data.pivot_point = prepivot + bpy.context.space_data.cursor_location = precursor + + return {'FINISHED'} - delta_position = Vector((padding/2,1-padding/2)) - Vector((bbox['min'].x, bbox['min'].y + bbox['height'])) - bpy.ops.transform.translate(value=(delta_position.x, delta_position.y, 0)) bpy.utils.register_class(op) \ No newline at end of file diff --git a/op_uv_fill.py b/op_uv_fill.py index 683bda4..b90a4e1 100644 --- a/op_uv_fill.py +++ b/op_uv_fill.py @@ -9,6 +9,8 @@ from . import utilities_uv from . import utilities_ui +from . import op_uv_crop + class op(bpy.types.Operator): bl_idname = "uv.textools_uv_fill" @@ -35,83 +37,27 @@ def poll(cls, context): #Requires UV map if not bpy.context.object.data.uv_layers: return False - + + #Not in Synced mode + if bpy.context.scene.tool_settings.use_uv_select_sync: + return False + return True def execute(self, context): - fill(self, context) - return {'FINISHED'} - - - -def fill(self, context): - - - #Store selection - utilities_uv.selection_store() - - bm = bmesh.from_edit_mesh(bpy.context.active_object.data); - uv_layers = bm.loops.layers.uv.verify(); - - - # 1.) Rotate minimal bounds (less than 45 degrees rotation) - steps = 8 - angle = 45; # Starting Angle, half each step - bboxPrevious = utilities_uv.getSelectionBBox() - - for i in range(0, steps): - # Rotate right - bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z') - bbox = utilities_uv.getSelectionBBox() - - - print("Rotate {}, diff le: {}".format(angle, bbox['height'] - bboxPrevious['height'])) - - - if i == 0: - # Check if already squared - sizeA = bboxPrevious['width'] * bboxPrevious['height'] - sizeB = bbox['width'] * bbox['height'] - if abs(bbox['width'] - bbox['height']) <= 0.0001 and sizeA < sizeB: - bpy.ops.transform.rotate(value=(-angle * math.pi / 180), orient_axis='Z') - break; - - if bbox['minLength'] < bboxPrevious['minLength']: - bboxPrevious = bbox; # Success - else: - # Rotate Left - bpy.ops.transform.rotate(value=(-angle*2 * math.pi / 180), orient_axis='Z') - bbox = utilities_uv.getSelectionBBox() - if bbox['minLength'] < bboxPrevious['minLength']: - bboxPrevious = bbox; # Success - else: - # Restore angle of this iteration - bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z') + prepivot = bpy.context.space_data.pivot_point + precursor = tuple(bpy.context.space_data.cursor_location) + bpy.context.space_data.pivot_point = 'CURSOR' + bpy.context.space_data.cursor_location = (0.0, 0.0) - angle = angle / 2 - - if bboxPrevious['width'] < bboxPrevious['height']: - bpy.ops.transform.rotate(value=(90 * math.pi / 180), orient_axis='Z') - - # 2.) Match width and height to UV bounds - bbox = utilities_uv.getSelectionBBox() - - scale_x = 1.0 / bbox['width'] - scale_y = 1.0 / bbox['height'] + utilities_uv.alignMinimalBounds() - print("Scale {} | {}".format(scale_x, scale_y)) + bpy.context.space_data.pivot_point = prepivot + bpy.context.space_data.cursor_location = precursor - bpy.context.tool_settings.transform_pivot_point = 'BOUNDING_BOX_CENTER' - bpy.ops.transform.resize(value=(scale_x, scale_y, 1), constraint_axis=(False, False, False), orient_type='GLOBAL', use_proportional_edit=False) + bpy.ops.uv.textools_uv_crop() + return {'FINISHED'} - bbox = utilities_uv.getSelectionBBox() - offset_x = -bbox['min'].x - offset_y = -bbox['min'].y - - bpy.ops.transform.translate(value=(offset_x, offset_y, 0), constraint_axis=(False, False, False), orient_type='GLOBAL', use_proportional_edit=False) - - #Restore selection - utilities_uv.selection_restore() bpy.utils.register_class(op) \ No newline at end of file diff --git a/settings.py b/settings.py index 083256b..c45d99f 100644 --- a/settings.py +++ b/settings.py @@ -2,12 +2,12 @@ import bmesh import operator -selection_uv_mode = ''; +selection_uv_mode = '' selection_uv_loops = [] -selection_uv_pivot = ''; +selection_uv_pivot = '' selection_uv_pivot_pos = (0,0) -selection_mode = [False, False, True]; +selection_mode = [False, False, True] selection_vert_indexies = [] selection_face_indexies = [] diff --git a/utilities_uv.py b/utilities_uv.py index 1327c52..612d318 100644 --- a/utilities_uv.py +++ b/utilities_uv.py @@ -4,26 +4,42 @@ import time from mathutils import Vector from collections import defaultdict -from math import pi -from numpy import median +import math from . import settings from . import utilities_ui +multi_object_loop_stop = False + +def multi_object_loop(func, *args, need_results = False, **kwargs) : -def multi_object_loop(func, *args, **ob_num) : - premode = bpy.context.active_object.mode selected_obs = [ob for ob in bpy.context.selected_objects if ob.type == 'MESH'] + if len(selected_obs) > 1: + global multi_object_loop_stop + multi_object_loop_stop = False + + premode = (bpy.context.active_object.mode) bpy.ops.object.mode_set(mode='OBJECT', toggle=False) bpy.ops.object.select_all(action='DESELECT') + + if need_results : + results = [] + for ob in selected_obs: + if multi_object_loop_stop: break bpy.context.view_layer.objects.active = ob bpy.ops.object.mode_set(mode='EDIT', toggle=False) - func(*args, **ob_num) - if len(ob_num) > 0 : - ob_num["ob_num"] += 1 + if "ob_num" in kwargs : + print("Operating on object " + str(kwargs["ob_num"])) + if need_results : + result = func(*args, **kwargs) + results.append(result) + else: + func(*args, **kwargs) + if "ob_num" in kwargs : + kwargs["ob_num"] += 1 bpy.ops.object.mode_set(mode='OBJECT', toggle=False) bpy.ops.object.select_all(action='DESELECT') @@ -31,8 +47,17 @@ def multi_object_loop(func, *args, **ob_num) : ob.select_set(True) bpy.ops.object.mode_set(mode=premode) + + if need_results : + return results + else: - func(*args) + if need_results : + result = func(*args, **kwargs) + results = [result] + return results + else: + func(*args, **kwargs) @@ -142,7 +167,6 @@ def move_island(island, dx,dy): - def get_selected_faces(): bm = bmesh.from_edit_mesh(bpy.context.active_object.data) faces = [] @@ -243,14 +267,13 @@ def getSelectionBBox(): uv_layers = bm.loops.layers.uv.verify() bbox = {} - uvs = [] boundsMin = Vector((99999999.0,99999999.0)) boundsMax = Vector((-99999999.0,-99999999.0)) boundsCenter = Vector((0.0,0.0)) - countFaces = 0 for face in bm.faces: if face.select: + select = True for loop in face.loops: if loop[uv_layers].select is True: uv = loop[uv_layers].uv @@ -258,70 +281,83 @@ def getSelectionBBox(): boundsMin.y = min(boundsMin.y, uv.y) boundsMax.x = max(boundsMax.x, uv.x) boundsMax.y = max(boundsMax.y, uv.y) - - boundsCenter += uv - countFaces+=1 - uvs.append([uv.x,uv.y]) + if not select: + return bbox bbox['min'] = boundsMin bbox['max'] = boundsMax bbox['width'] = (boundsMax - boundsMin).x bbox['height'] = (boundsMax - boundsMin).y - - if countFaces == 0: - bbox['center'] = boundsMin - else: - bbox['center'] = boundsCenter / countFaces - bbox['median'] = median(uvs) + boundsCenter.x = (boundsMax.x + boundsMin.x)/2 + boundsCenter.y = (boundsMax.y + boundsMin.y)/2 + + bbox['center'] = boundsCenter bbox['area'] = bbox['width'] * bbox['height'] - bbox['minLength'] = min(bbox['width'], bbox['height']) + bbox['minLength'] = min(bbox['width'], bbox['height']) + return bbox + def get_island_BBOX(island): - points = [] - obj = bpy.context.active_object - me = obj.data - bm = bmesh.from_edit_mesh(me) + bbox = {} + bm = bmesh.from_edit_mesh(bpy.context.active_object.data) + uv_layers = bm.loops.layers.uv.verify() - uv_layer = bm.loops.layers.uv.verify() boundsMin = Vector((99999999.0,99999999.0)) boundsMax = Vector((-99999999.0,-99999999.0)) boundsCenter = Vector((0.0,0.0)) - countFaces = 0 for face in island: for loop in face.loops: - loop_uv = loop[uv_layer] - points.append(loop_uv.uv) - uv = loop[uv_layer].uv + uv = loop[uv_layers].uv boundsMin.x = min(boundsMin.x, uv.x) boundsMin.y = min(boundsMin.y, uv.y) boundsMax.x = max(boundsMax.x, uv.x) boundsMax.y = max(boundsMax.y, uv.y) - boundsCenter += uv - countFaces+=1 + bbox['min'] = Vector((boundsMin)) + bbox['max'] = Vector((boundsMax)) - x_coordinates, y_coordinates = zip(*points) - island_bbox = [(min(x_coordinates), min(y_coordinates)), (max(x_coordinates), max(y_coordinates))] - - bbox = {} + boundsCenter.x = (boundsMax.x + boundsMin.x)/2 + boundsCenter.y = (boundsMax.y + boundsMin.y)/2 + + bbox['center'] = boundsCenter - if countFaces == 0: - bbox['center'] = boundsMin - else: - bbox['center'] = boundsCenter / countFaces - - bbox['min'] = Vector((island_bbox[0])) - bbox['max'] = Vector((island_bbox[1])) - bbox['median'] = Vector((median(x_coordinates),median(y_coordinates))) - print ("Island bbox", island_bbox) return bbox +def getMultiObjectSelectionBBox(all_ob_bounds): + multibbox = {} + boundsMin = Vector((99999999.0,99999999.0)) + boundsMax = Vector((-99999999.0,-99999999.0)) + boundsCenter = Vector((0.0,0.0)) + + for ob_bounds in all_ob_bounds: + if len(ob_bounds) > 1 : + boundsMin.x = min(boundsMin.x, ob_bounds['min'].x) + boundsMin.y = min(boundsMin.y, ob_bounds['min'].y) + boundsMax.x = max(boundsMax.x, ob_bounds['max'].x) + boundsMax.y = max(boundsMax.y, ob_bounds['max'].y) + + multibbox['min'] = boundsMin + multibbox['max'] = boundsMax + multibbox['width'] = (boundsMax - boundsMin).x + multibbox['height'] = (boundsMax - boundsMin).y + + boundsCenter.x = (boundsMax.x + boundsMin.x)/2 + boundsCenter.y = (boundsMax.y + boundsMin.y)/2 + + multibbox['center'] = boundsCenter + multibbox['area'] = multibbox['width'] * multibbox['height'] + multibbox['minLength'] = min(multibbox['width'], multibbox['height']) + + return multibbox + + + def getSelectionIslands(bm=None, uv_layers=None): if bm == None: bm = bmesh.from_edit_mesh(bpy.context.active_object.data) @@ -370,6 +406,55 @@ def getSelectionIslands(bm=None, uv_layers=None): for loop in face.loops: loop[uv_layers].select = True + # print("Islands: {}x".format(len(islands))) - print("Islands: {}x".format(len(islands))) return islands + + + +def alignMinimalBounds(uv_layers=None): + steps = 8 + angle = 45 # Starting Angle, half each step + + all_ob_bounds = multi_object_loop(getSelectionBBox, need_results=True) + + select = False + for ob_bounds in all_ob_bounds: + if len(ob_bounds) > 0 : + select = True + break + if not select: + return {'CANCELLED'} + + bboxPrevious = getMultiObjectSelectionBBox(all_ob_bounds) + + for i in range(0, steps): + # Rotate right + bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z', constraint_axis=(False, False, False), use_proportional_edit=False) + all_ob_bounds = multi_object_loop(getSelectionBBox, need_results=True) + bbox = getMultiObjectSelectionBBox(all_ob_bounds) + + if i == 0: + sizeA = bboxPrevious['width'] * bboxPrevious['height'] + sizeB = bbox['width'] * bbox['height'] + if abs(bbox['width'] - bbox['height']) <= 0.0001 and sizeA < sizeB: + bpy.ops.transform.rotate(value=(-angle * math.pi / 180), orient_axis='Z', constraint_axis=(False, False, False), use_proportional_edit=False) + break + + if bbox['minLength'] < bboxPrevious['minLength']: + bboxPrevious = bbox # Success + else: + # Rotate Left + bpy.ops.transform.rotate(value=(-angle*2 * math.pi / 180), orient_axis='Z', constraint_axis=(False, False, False), use_proportional_edit=False) + all_ob_bounds = multi_object_loop(getSelectionBBox, need_results=True) + bbox = getMultiObjectSelectionBBox(all_ob_bounds) + if bbox['minLength'] < bboxPrevious['minLength']: + bboxPrevious = bbox # Success + else: + # Restore angle of this iteration + bpy.ops.transform.rotate(value=(angle * math.pi / 180), orient_axis='Z', constraint_axis=(False, False, False), use_proportional_edit=False) + + angle = angle / 2 + + # if bboxPrevious['width'] < bboxPrevious['height']: + # bpy.ops.transform.rotate(value=(90 * math.pi / 180), orient_axis='Z') \ No newline at end of file