import bpy import bmesh import math import mathutils from mathutils import Vector from . import settings from . import utilities_ui precision = 5 multi_object_loop_stop = False def multi_object_loop(func, *args, need_results = False, **kwargs) : selected_obs = [ob for ob in bpy.context.selected_objects if ob.type == 'MESH'] if len(selected_obs) == 1: preactiv_name = bpy.context.view_layer.objects.active.name bpy.context.view_layer.objects.active = selected_obs[0] if not need_results: func(*args, **kwargs) if bpy.data.objects[preactiv_name]: bpy.context.view_layer.objects.active = bpy.data.objects[preactiv_name] else: result = func(*args, **kwargs) results = [result] if bpy.data.objects[preactiv_name]: bpy.context.view_layer.objects.active = bpy.data.objects[preactiv_name] return results else: global multi_object_loop_stop multi_object_loop_stop = False premode = bpy.context.active_object.mode preactiv_name = bpy.context.view_layer.objects.active.name bpy.ops.object.mode_set(mode='EDIT', toggle=False) unique_selected_obs = [ob for ob in bpy.context.objects_in_mode_unique_data if ob.type == 'MESH' and ob.select_get()] bpy.ops.object.mode_set(mode='OBJECT', toggle=False) bpy.ops.object.select_all(action='DESELECT') if need_results : results = [] for ob in unique_selected_obs: if multi_object_loop_stop: break bpy.context.view_layer.objects.active = ob bpy.ops.object.mode_set(mode='EDIT', toggle=False) if "ob_num" in kwargs : print("Operating on object " + str(kwargs["ob_num"])) if need_results : result = func(*args, **kwargs) #if result: 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') for ob in selected_obs: ob.select_set(True) bpy.context.view_layer.objects.active = bpy.data.objects[preactiv_name] bpy.ops.object.mode_set(mode=premode) if need_results : return results def selection_store(bm=None, uv_layers=None, return_selected_UV_faces=False, return_selected_faces_edges=False, return_selected_faces_loops=False): if bm is None: bm = bmesh.from_edit_mesh(bpy.context.active_object.data) uv_layers = bm.loops.layers.uv.verify() settings.use_uv_sync = bpy.context.scene.tool_settings.use_uv_select_sync settings.selection_uv_mode = bpy.context.scene.tool_settings.uv_select_mode contextViewUV = utilities_ui.GetContextViewUV() if contextViewUV: settings.selection_uv_pivot = contextViewUV['area'].spaces[0].pivot_point settings.selection_uv_pivot_pos = contextViewUV['area'].spaces[0].cursor_location.copy() # Clear previous selection settings.selection_vert_indexies.clear() settings.selection_edge_indexies.clear() settings.selection_face_indexies.clear() settings.seam_edges.clear() settings.selection_mode = tuple(bpy.context.scene.tool_settings.mesh_select_mode) if settings.selection_mode[0]: for vert in bm.verts: if vert.select: settings.selection_vert_indexies.add(vert.index) if settings.selection_mode[1]: for edge in bm.edges: if edge.select: settings.selection_edge_indexies.add(edge.index) # Face selections (Loops) settings.selection_uv_loops.clear() if return_selected_UV_faces: selected_faces = set() elif return_selected_faces_edges or return_selected_faces_loops: selected_faces_loops = {} for face in bm.faces: if face.select: settings.selection_face_indexies.add(face.index) n_selected_loops = 0 if return_selected_faces_edges or return_selected_faces_loops: face_selected_loops = [] for loop in face.loops: if loop.edge.seam == True: settings.seam_edges.add(loop.edge) if loop[uv_layers].select: n_selected_loops += 1 settings.selection_uv_loops.add( (face.index, loop.vert.index) ) if return_selected_faces_edges or return_selected_faces_loops: face_selected_loops.append(loop) if return_selected_UV_faces and n_selected_loops == len(face.loops) and face.select: selected_faces.add(face) elif return_selected_faces_edges and n_selected_loops == 2 and face.select: selected_faces_loops.update({face: face_selected_loops}) elif return_selected_faces_loops and n_selected_loops > 0 and face.select: selected_faces_loops.update({face: face_selected_loops}) if return_selected_UV_faces: return selected_faces elif return_selected_faces_edges or return_selected_faces_loops: return selected_faces_loops def selection_restore(bm = None, uv_layers = None, restore_seams=False): mode = bpy.context.object.mode if mode != 'EDIT': bpy.ops.object.mode_set(mode = 'EDIT') if bm is None: bm = bmesh.from_edit_mesh(bpy.context.active_object.data) uv_layers = bm.loops.layers.uv.verify() bpy.context.scene.tool_settings.use_uv_select_sync = settings.use_uv_sync bpy.context.scene.tool_settings.uv_select_mode = settings.selection_uv_mode contextViewUV = utilities_ui.GetContextViewUV() if contextViewUV: contextViewUV['area'].spaces[0].pivot_point = settings.selection_uv_pivot if settings.bversion >= 3.2: with bpy.context.temp_override(**contextViewUV): bpy.ops.uv.cursor_set(location=settings.selection_uv_pivot_pos) else: bpy.ops.uv.cursor_set(contextViewUV, location=settings.selection_uv_pivot_pos) #Restore seams if restore_seams: bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.mark_seam(clear=True) for edge in settings.seam_edges: edge.seam = True bpy.ops.mesh.select_all(action='DESELECT') #Selection Mode bpy.context.scene.tool_settings.mesh_select_mode = settings.selection_mode if settings.selection_mode[0]: bm.verts.ensure_lookup_table() for index in settings.selection_vert_indexies: if index < len(bm.verts): bm.verts[index].select = True if settings.selection_mode[1]: bm.edges.ensure_lookup_table() for index in settings.selection_edge_indexies: if index < len(bm.edges): bm.edges[index].select = True bm.faces.ensure_lookup_table() for index in settings.selection_face_indexies: if index < len(bm.faces): bm.faces[index].select = True #UV Face-UV Selections (Loops) if contextViewUV: if settings.bversion >= 3.2: with bpy.context.temp_override(**contextViewUV): bpy.ops.uv.select_all(action='DESELECT') else: bpy.ops.uv.select_all(contextViewUV, action='DESELECT') else: for face in bm.faces: for loop in face.loops: loop[uv_layers].select = False for uv_set in settings.selection_uv_loops: for loop in bm.faces[uv_set[0]].loops: if loop.vert.index == uv_set[1]: loop[uv_layers].select = True break # Workaround for selection not flushing properly from loops in EDGE or FACE UV Selection Mode, apparently since UV edge selection support was added to the UV space if settings.selection_uv_mode != "VERTEX": bpy.ops.uv.select_mode(type='VERTEX') bpy.context.scene.tool_settings.uv_select_mode = settings.selection_uv_mode bpy.context.view_layer.update() bpy.ops.object.mode_set(mode=mode) def selected_unique_objects_in_mode_with_uv(): return [obj for obj in bpy.context.objects_in_mode_unique_data if obj.type == 'MESH' and obj.data.uv_layers] def get_UDIM_tile_coords(obj): udim_tile = 1001 column = row = 0 if bpy.context.scene.texToolsSettings.UDIMs_source == 'OBJECT': if obj and obj.type == 'MESH' and obj.data.uv_layers: for i in range(len(obj.material_slots)): slot = obj.material_slots[i] if slot.material: if slot.material.use_nodes: nodes = slot.material.node_tree.nodes if nodes: for node in nodes: if node.type == 'TEX_IMAGE' and node.image and node.image.source =='TILED': udim_tile = node.image.tiles.active.number break else: continue break else: image = bpy.context.space_data.image if image: udim_tile = image.tiles.active.number if udim_tile != 1001: column = int(str(udim_tile - 1)[-1]) if udim_tile > 1010: row = int(str(udim_tile - 1001)[0:-1]) return udim_tile, column, row def get_UDIM_tiles(objs): tiles = set() for obj in objs: for i in range(len(obj.material_slots)): slot = obj.material_slots[i] if slot.material: if slot.material.use_nodes: nodes = slot.material.node_tree.nodes if nodes: for node in nodes: if node.type == 'TEX_IMAGE' and node.image and node.image.source =='TILED': tiles.update({tile.number for tile in node.image.tiles}) return tiles def move_island(island, dx, dy): me = bpy.context.active_object.data bm = bmesh.from_edit_mesh(me) uv_layer = bm.loops.layers.uv.verify() # adjust uv coordinates for face in island: for loop in face.loops: loop_uv = loop[uv_layer] loop_uv.uv[0] += dx loop_uv.uv[1] += dy bmesh.update_edit_mesh(me) def translate_island(island, uv_layer, delta): for face in island: for loop in face.loops: loop[uv_layer].uv += delta def rotate_island(island, uv_layer=None, angle=0, pivot=None): '''Rotate a list of faces by angle (in radians) around a center''' rot_matrix = mathutils.Matrix.Rotation(-angle, 2) if uv_layer is None: me = bpy.context.active_object.data bm = bmesh.from_edit_mesh(me) uv_layer = bm.loops.layers.uv.verify() if pivot: for face in island: for loop in face.loops: uv = loop[uv_layer] uv.uv = rot_matrix @ (uv.uv - pivot) + pivot return for face in island: for loop in face.loops: uv = loop[uv_layer] uv.uv = uv.uv @ rot_matrix def scale_island(island, uv_layer, scale_x, scale_y, pivot=None): """Scale a list of faces by 'scale_x, scale_y'. """ if not pivot: bbox = get_BBOX(island, None, uv_layer) pivot = bbox['center'] for face in island: for loop in face.loops: x, y = loop[uv_layer].uv xt = x - pivot.x yt = y - pivot.y xs = xt * scale_x ys = yt * scale_y loop[uv_layer].uv.x = xs + pivot.x loop[uv_layer].uv.y = ys + pivot.y def set_selected_faces(faces, bm, uv_layers): for face in faces: for loop in face.loops: loop[uv_layers].select = True def get_selected_uvs(bm, uv_layers): """Returns selected mesh vertices of selected UV's""" uvs = set() for face in bm.faces: if face.select: for loop in face.loops: if loop[uv_layers].select: uvs.add( loop[uv_layers] ) return uvs def get_selected_uv_verts(bm, uv_layers, selected=None): """Returns selected mesh vertices of selected UV's""" verts = set() if selected is None: for face in bm.faces: if face.select: for loop in face.loops: if loop[uv_layers].select: verts.add( loop.vert ) else: for loop in selected: verts.add( loop.vert ) return verts def get_selected_uv_edges(bm, uv_layers, selected=None): """Returns selected mesh edges of selected UV's""" verts = get_selected_uv_verts(bm, uv_layers, selected) edges = set() for edge in bm.edges: if edge.verts[0] in verts and edge.verts[1] in verts: edges.add(edge) return edges def get_selected_uv_faces(bm, uv_layers): """Returns selected mesh faces of selected UV's""" faces = [face for face in bm.faces if all([loop[uv_layers].select for loop in face.loops]) and face.select] return faces def get_vert_to_uv(bm, uv_layers): vert_to_uv = {} for face in bm.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) return vert_to_uv def get_uv_to_vert(bm, uv_layers): uv_to_vert = {} for face in bm.faces: for loop in face.loops: vert = loop.vert uv = loop[uv_layers] if uv not in uv_to_vert: uv_to_vert[ uv ] = vert return uv_to_vert def getSelectionBBox(bm=None, uv_layers=None): if bm is None: bm = bmesh.from_edit_mesh(bpy.context.active_object.data) uv_layers = bm.loops.layers.uv.verify() sync = bpy.context.scene.tool_settings.use_uv_select_sync bbox = {} xmin = math.inf xmax = -math.inf ymin = math.inf ymax = -math.inf select = False for face in bm.faces: if face.select: for loop in face.loops: if sync or loop[uv_layers].select: select = True x, y = loop[uv_layers].uv if xmin > x: xmin = x if xmax < x: xmax = x if ymin > y: ymin = y if ymax < y: ymax = y if not select: return bbox bbox['min'] = Vector((xmin, ymin)) bbox['max'] = Vector((xmax, ymax)) bbox['width'] = xmax - xmin bbox['height'] = ymax - ymin xcenter = (xmax + xmin)*0.5 ycenter = (ymax + ymin)*0.5 bbox['center'] = Vector((xcenter, ycenter)) bbox['area'] = bbox['width'] * bbox['height'] bbox['minLength'] = min(bbox['width'], bbox['height']) return bbox def get_BBOX(group, bm, uv_layers, are_loops=False): bbox = {} xmin = math.inf xmax = -math.inf ymin = math.inf ymax = -math.inf if not are_loops: for face in group: for loop in face.loops: x, y = loop[uv_layers].uv if xmin > x: xmin = x if xmax < x: xmax = x if ymin > y: ymin = y if ymax < y: ymax = y else: for loop in group: x, y = loop[uv_layers].uv if xmin > x: xmin = x if xmax < x: xmax = x if ymin > y: ymin = y if ymax < y: ymax = y bbox['min'] = Vector((xmin, ymin)) bbox['max'] = Vector((xmax, ymax)) bbox['width'] = xmax - xmin bbox['height'] = ymax - ymin xcenter = (xmax + xmin) * 0.5 ycenter = (ymax + ymin) * 0.5 bbox['center'] = Vector((xcenter, ycenter)) bbox['area'] = bbox['width'] * bbox['height'] bbox['minLength'] = min(bbox['width'], bbox['height']) return bbox def get_BBOX_multi(all_ob_bounds): multibbox = {} boundsMin = Vector((math.inf, math.inf)) boundsMax = Vector((-math.inf, -math.inf)) 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 get_center(group, bm, uv_layers, are_loops=False): n = 0 total = Vector((0.0, 0.0)) if not are_loops: for face in group: for loop in face.loops: total += loop[uv_layers].uv n += 1 else: for loop in group: total += loop[uv_layers].uv n += 1 return total / n def get_selected_islands(bm, uv_layers, selected=True, extend_selection_to_islands=False): islands = [] island = set() sync = bpy.context.scene.tool_settings.use_uv_select_sync faces = bm.faces # Reset tags for unselected (if tag is False - skip) if selected: if sync: for face in faces: face.tag = face.select else: for face in faces: if face.select: face.tag = all(l[uv_layers].select for l in face.loops) continue face.tag = False else: if sync: for face in faces: face.tag = not face.hide else: for face in faces: face.tag = not face.hide and face.select for face in faces: # Skip unselected and appended faces if not face.tag: # if is False: continue # Tag first element in island (dont add again) face.tag = False # Container collector of island elements parts_of_island = [face] # Container for get elements from loop from parts_of_island temp = [] # Blank list == all faces of the island taken while parts_of_island: for f in parts_of_island: # Running through all the neighboring faces for l in f.loops: link_face = l.link_loop_radial_next.face # Skip appended if not link_face.tag: # if is False: continue for ll in link_face.loops: if not ll.face.tag: # if is False: continue # If the coordinates of the vertices of adjacent # faces on the uv match, then this is part of the # island and we append face to the list if ll[uv_layers].uv != l[uv_layers].uv: continue # Skip non-manifold if (l.link_loop_next[uv_layers].uv == ll.link_loop_prev[uv_layers].uv) or \ (ll.link_loop_next[uv_layers].uv == l.link_loop_prev[uv_layers].uv): temp.append(ll.face) ll.face.tag = False island.update(parts_of_island) parts_of_island = temp temp = [] # Skip the islands that don't have a single selected face. if selected is False and extend_selection_to_islands is True: if sync: for face in island: if face.select: break else: island = set() continue else: for face in island: if all(l[uv_layers].select for l in face.loops): break else: island = set() continue islands.append(island) island = set() return islands def getFacesIslands(bm, uv_layers, faces, islands, disordered_island_faces): for face in faces: if face in disordered_island_faces: bpy.ops.uv.select_all(action='DESELECT') face.loops[0][uv_layers].select = True bpy.ops.uv.select_linked() islandFaces = {f for f in disordered_island_faces if f.loops[0][uv_layers].select} disordered_island_faces.difference_update(islandFaces) islands.append(islandFaces) if not disordered_island_faces: break def getAllIslands(bm, uv_layers): faces = {f for f in bm.faces if f.select} if not faces: return [] islands = [] faces_unparsed = faces.copy() getFacesIslands(bm, uv_layers, faces, islands, faces_unparsed) return islands def getSelectionIslands(bm, uv_layers, extend_selection_to_islands=False, selected_faces=None, need_faces_selected=True, restore_selected=True): if selected_faces is None: if need_faces_selected: selected_faces = {f for f in bm.faces if all([l[uv_layers].select for l in f.loops]) and f.select} else: selected_faces = {f for f in bm.faces if any([l[uv_layers].select for l in f.loops]) and f.select} if not selected_faces: return [] # Select islands if extend_selection_to_islands: bpy.ops.uv.select_linked() disordered_island_faces = {f for f in bm.faces if f.loops[0][uv_layers].select and f.select} else: disordered_island_faces = selected_faces.copy() # Collect UV islands islands = [] getFacesIslands(bm, uv_layers, selected_faces, islands, disordered_island_faces) # Restore selection if restore_selected: bpy.ops.uv.select_all(action='DESELECT') set_selected_faces(selected_faces, bm, uv_layers) return islands def getSelectedUnselectedIslands(bm, uv_layers, selected_faces=None, target_faces=None, restore_selected=False): if selected_faces is None: return [], [] # Collect selected UV islands selected_islands = [] bpy.ops.uv.select_linked() disordered_islands_selected = {f for f in bm.faces if f.loops[0][uv_layers].select and f.select} getFacesIslands(bm, uv_layers, selected_faces, selected_islands, disordered_islands_selected) # Collect target UV islands if target_faces is None: return selected_islands, [] target_islands = [] target_faces.difference_update(disordered_islands_selected) bpy.ops.uv.select_all(action='DESELECT') for f in target_faces: f.loops[0][uv_layers].select = True bpy.ops.uv.select_linked() disordered_islands_targets = {f for f in bm.faces if f.loops[0][uv_layers].select and f.select} getFacesIslands(bm, uv_layers, target_faces, target_islands, disordered_islands_targets) if restore_selected: bpy.ops.uv.select_all(action='DESELECT') set_selected_faces(selected_faces, bm, uv_layers) return selected_islands, target_islands def getSelectionFacesIslands(bm, uv_layers, selected_faces_loops): # Select islands bpy.ops.uv.select_linked() disordered_island_faces = {f for f in bm.faces if f.loops[0][uv_layers].select and f.select} # Collect UV islands selected_faces_islands = {} to_remove = set() for face in selected_faces_loops.keys(): if face not in disordered_island_faces: to_remove.add(face) else: bpy.ops.uv.select_all(action='DESELECT') face.loops[0][uv_layers].select = True bpy.ops.uv.select_linked() face_island = {f for f in disordered_island_faces if f.loops[0][uv_layers].select} disordered_island_faces.difference_update(face_island) selected_faces_islands.update({face: face_island}) for face in to_remove: selected_faces_loops.pop(face) return selected_faces_islands, selected_faces_loops ''' def getSelectionLoopsIslands(bm, uv_layers, selected_loops): # Select islands bpy.ops.uv.select_linked() disordered_loops_islands = {loop for face in bm.faces for loop in face.loops if loop[uv_layers].select and loop.edge.select} selected_loops_islands = [] for loop in selected_loops: if loop in disordered_loops_islands: bpy.ops.uv.select_all(action='DESELECT') loop[uv_layers].select = True bpy.ops.uv.select_linked() loops_island = {l for l in disordered_loops_islands if l[uv_layers].select} disordered_loops_islands.difference_update(loops_island) selected_loops_islands.append(loops_island) if not disordered_loops_islands: break return selected_loops_islands ''' def find_min_rotate_angle(angle): angle = math.degrees(angle) x = math.fmod(angle, 90) if angle > 45: y = 90 - x angle = -y if y < x else x elif angle < -45: y = -90 - x angle = -y if y > x else x return math.radians(angle) def calc_min_align_angle(selected_faces, uv_layers): points = [l[uv_layers].uv for f in selected_faces for l in f.loops] align_angle_pre = mathutils.geometry.box_fit_2d(points) return find_min_rotate_angle(align_angle_pre) def calc_min_align_angle_pt(points): align_angle_pre = mathutils.geometry.box_fit_2d(points) return find_min_rotate_angle(align_angle_pre)