# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### bl_info = { "name": "传递顶点顺序", "author": "Jose Conseco based on UV Transfer from MagicUV Nutti", "version": (2, 4), "blender": (4, 2, 0), "location": "侧边栏 (N) -> 工具面板", "description": "通过顶点位置或选定面传递顶点ID", "warning": "", "wiki_url": "", "category": "Object", "doc_url": "https://bartoszstyperek.gumroad.com/l/copy_verts_ids", } from collections import OrderedDict import bpy import bmesh from bpy.props import BoolProperty from mathutils import kdtree, Vector class CopyIDs(): def __init__(self): self.transuv = ID_DATA() class ID_DATA(): face_vert_ids = [] face_edge_ids = [] faces_id = [] face_loop_ids = [] class VOT_PT_CopyVertIds(bpy.types.Panel): bl_idname = "VOT_PT_copyvertids" bl_label = "传递顶点顺序" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'Tools' def draw(self, context): layout = self.layout if context.mode == 'OBJECT': layout.label(text = '编辑模式下有更多选项') layout.operator("object.vert_id_transfer_proximity") layout.operator("object.vert_id_transfer_uv") layout.operator("object.preview_vertex_order") layout.label(text="选中 -> 活动") elif context.mode == 'EDIT_MESH': layout.separator() layout.operator("object.copy_vert_id") layout.operator("object.paste_vert_id") layout.operator("object.copy_paste_vert_id") layout.operator("object.select_not_transfered_verts") layout.operator("object.preview_vertex_order") layout.label(text="选中 -> 活动") class VOT_OT_TransferVertId(bpy.types.Operator): """通过顶点位置传递顶点ID""" bl_label = "使用位置传递ID" bl_idname = "object.vert_id_transfer_proximity" bl_description = "通过顶点位置传递顶点ID(适用于形状完全相同的网格)\n需要选择两个网格物体" bl_options = {'REGISTER'} delta: bpy.props.FloatProperty(name="误差", description="搜索距离", default=0.1, min=0, max=1, precision = 4) def execute(self, context): sourceObj = context.active_object TargetObjs = [obj for obj in context.selected_objects if obj!=sourceObj and obj.type=='MESH'] if not TargetObjs: self.report({'ERROR'}, '你需要选择两个网格物体(源物体和接收顶点顺序的目标物体)!取消操作') return {'CANCELLED'} bm = bmesh.new() # load mesh bm.from_mesh(sourceObj.data) src_obj_kd_verts = kdtree.KDTree(len(bm.verts)) for i, v in enumerate(bm.verts): src_obj_kd_verts.insert(v.co, i) src_obj_kd_verts.balance() src_obj_kd_edges = kdtree.KDTree(len(bm.edges)) for i, edge in enumerate(bm.edges): src_obj_kd_edges.insert((edge.verts[0].co+edge.verts[1].co)/2, i) src_obj_kd_edges.balance() src_obj_kd_faces = kdtree.KDTree(len(bm.faces)) for i, f in enumerate(bm.faces): src_obj_kd_faces.insert(f.calc_center_median(), i) src_obj_kd_faces.balance() bm.free() processedVertsIdDict = {} processedEdgesIdDict = {} processedFacesIdDict = {} for target in TargetObjs: copiedCount = 0 processedVertsIdDict.clear() bm = bmesh.new() # load mesh bm.from_mesh(target.data) for vert in bm.verts: co, index, dist = src_obj_kd_verts.find(vert.co) if dist < self.delta: # delta copiedCount += 1 vert.index = index processedVertsIdDict[vert] = index for edge in bm.edges: co, index, dist = src_obj_kd_edges.find((edge.verts[0].co+edge.verts[1].co)/2) if dist no other Islands -> quit def fix_islands(processed_items, bm_element): #face, verts, or edges processedItems = {item: id for (item, id) in processed_items.items()} # dicts instead of lists processedIDs = {id: 1 for (item, id) in processed_items.items()} # dicts instead of lists notProcessedItemsIds = {ele.index: 1 for ele in bm_element if ele not in processedItems} # it will have duplicated ids from processedIDs that have to be spareIDS = [i for i in range(len(bm_element)) if (i not in processedIDs and i not in notProcessedItemsIds)] notProcessedElements = [item for item in bm_element if item not in processedItems] for item in notProcessedElements: if item.index in processedIDs: # if duplicated id found in not processed verts item.index = spareIDS.pop(0) # what if list is empty?? fix_islands(processedVertsIdDict, bm.verts) fix_islands(preocessedEdgesIsDict, bm.edges) fix_islands(preocessedFaceIsDict, bm.faces) def execute(self, context): props = context.scene.copy_indices.transuv active_obj = context.active_object bm = bmesh.from_edit_mesh(active_obj.data) bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() color_layer = get_color_layer(bm) # get selection history all_sel_faces = [ e for e in bm.select_history if isinstance(e, bmesh.types.BMFace) and e.select] if len(all_sel_faces) % 2 != 0: self.report({'WARNING'}, "必须选择两个面") return {'CANCELLED'} # parse selection history loopID_dict = {} vertID_dict = {} edgeID_dict = {} faceID_dict = {} for i, _ in enumerate(all_sel_faces): if (i == 0) or (i % 2 == 0): continue sel_faces = [all_sel_faces[i - 1], all_sel_faces[i]] active_face = all_sel_faces[i] # parse all faces according to selection history active_face_nor = active_face.normal.copy() if self.invert_normals: active_face_nor.negate() all_sorted_faces = main_parse(self, sel_faces, active_face, active_face_nor) # ipdb.set_trace() if all_sorted_faces: # check amount of copied/pasted faces if len(all_sorted_faces) != len(props.face_vert_ids): self.report( {'WARNING'}, "网格的面数量不同" ) return {'FINISHED'} for j,(face, face_data) in enumerate(all_sorted_faces.items()): loop_ids_cache = props.face_loop_ids[j] vert_ids_cache = props.face_vert_ids[j] edge_ids_cache = props.face_edge_ids[j] face_id_cache = props.faces_id[j] # check amount of copied/pasted verts if len(vert_ids_cache) != len(face_data[1]): bpy.ops.mesh.select_all(action='DESELECT') # select problematic face list(all_sorted_faces.keys())[j].select = True self.report( {'WARNING'}, "面的顶点数量不同" ) return {'FINISHED'} for k, vert in enumerate(face_data[1]): vert.index = vert_ids_cache[k] #index vertID_dict[vert] = vert.index for k, loop in enumerate(face_data[0]): loop.index = loop_ids_cache[k] #index loop[color_layer] = Vector((0,1,0,1)) loopID_dict[loop] = loop.index face.index = face_id_cache faceID_dict[face] = face_id_cache for k, edge in enumerate(face_data[2]): #edges edge.index = edge_ids_cache[k] # index edgeID_dict[edge] = edge.index self.sortOtherVerts(vertID_dict, edgeID_dict, faceID_dict, bm) #? does not exist bm.loops.sort() bm.verts.sort() bm.edges.sort() bm.faces.sort() #! !!! for faces in bm.fa bmesh.update_edit_mesh(active_obj.data) activate_color_layer(active_obj) self.report({"INFO"}, "顶点顺序已粘贴。") return {'FINISHED'} class VOT_OT_CopyPasteVertID(bpy.types.Operator): bl_idname = "object.copy_paste_vert_id" bl_label = "复制并粘贴顶点ID" bl_description = ( "从选中物体复制顶点顺序到活动物体。" "两个物体都应处于编辑模式并选择两个相似的面" ) bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): selected_objects = context.selected_objects if len(selected_objects) != 2 or any(obj.mode != "EDIT" for obj in selected_objects): cls.poll_message_set("必须选择两个物体,且都需要处于编辑模式") return False return True def execute(self, context): selected_objects = context.selected_objects target_obj = context.active_object source_obj = next(obj for obj in selected_objects if obj != target_obj) with context.temp_override(active_object=source_obj): bpy.ops.object.copy_vert_id() with context.temp_override(active_object=target_obj): bpy.ops.object.paste_vert_id() return {"FINISHED"} def main_parse(self, sel_faces, active_face, active_face_nor): all_sorted_faces = OrderedDict() # This is the main stuff used_verts = set() used_edges = set() faces_to_parse = [] # get shared edge of two faces cross_edges = [] for edge in active_face.edges: if edge in sel_faces[0].edges and edge in sel_faces[1].edges: cross_edges.append(edge) # parse two selected faces if cross_edges and len(cross_edges) == 1: shared_edge = cross_edges[0] vert1 = None vert2 = None dot_n = active_face_nor.normalized() edge_vec_1 = (shared_edge.verts[1].co - shared_edge.verts[0].co) edge_vec_len = edge_vec_1.length edge_vec_1 = edge_vec_1.normalized() af_center = active_face.calc_center_median() af_vec = shared_edge.verts[0].co + (edge_vec_1 * (edge_vec_len * 0.5)) af_vec = (af_vec - af_center).normalized() if af_vec.cross(edge_vec_1).dot(dot_n) > 0: vert1 = shared_edge.verts[0] vert2 = shared_edge.verts[1] else: vert1 = shared_edge.verts[1] vert2 = shared_edge.verts[0] # get active face stuff and uvs # ipdb.set_trace() face_stuff = get_other_verts_edges(active_face, vert1, vert2, shared_edge) all_sorted_faces[active_face] = face_stuff used_verts.update(active_face.verts) used_edges.update(active_face.edges) # get first selected face stuff and uvs as they share shared_edge second_face = sel_faces[0] if second_face is active_face: second_face = sel_faces[1] face_stuff = get_other_verts_edges(second_face, vert1, vert2, shared_edge) all_sorted_faces[second_face] = face_stuff used_verts.update(second_face.verts) used_edges.update(second_face.edges) # first Grow faces_to_parse.append(active_face) faces_to_parse.append(second_face) else: self.report({'WARNING'}, "两个面应该共享一条边") return None # parse all faces while True: new_parsed_faces = [] if not faces_to_parse: break for face in faces_to_parse: face_stuff = all_sorted_faces.get(face) new_faces = parse_faces(face, face_stuff, used_verts, used_edges, all_sorted_faces) if new_faces == 'CANCELLED': self.report({'WARNING'}, "超过2个面共享边") return None new_parsed_faces += new_faces faces_to_parse = new_parsed_faces return all_sorted_faces def parse_faces(check_face, face_stuff, used_verts, used_edges, all_sorted_faces): """recurse faces around the new_grow only""" new_shared_faces = [] for sorted_edge in face_stuff[2]: shared_faces = sorted_edge.link_faces if shared_faces: if len(shared_faces) > 2: bpy.ops.mesh.select_all(action='DESELECT') for face_sel in shared_faces: face_sel.select = True shared_faces = [] return 'CANCELLED' clear_shared_faces = get_new_shared_faces(check_face, sorted_edge, shared_faces, all_sorted_faces.keys()) if clear_shared_faces: shared_face = clear_shared_faces[0] # get vertices of the edge vert1 = sorted_edge.verts[0] vert2 = sorted_edge.verts[1] if face_stuff[1].index(vert1) > face_stuff[1].index(vert2): vert1 = sorted_edge.verts[1] vert2 = sorted_edge.verts[0] new_face_stuff = get_other_verts_edges(shared_face, vert1, vert2, sorted_edge) all_sorted_faces[shared_face] = new_face_stuff used_verts.update(shared_face.verts) used_edges.update(shared_face.edges) new_shared_faces.append(shared_face) return new_shared_faces def get_new_shared_faces(orig_face, shared_edge, check_faces, used_faces): shared_faces = [] for face in check_faces: is_shared_edge = shared_edge in face.edges not_used = face not in used_faces not_orig = face is not orig_face not_hide = face.hide is False if is_shared_edge and not_used and not_orig and not_hide: shared_faces.append(face) return shared_faces def get_other_verts_edges(face, vert1, vert2, first_edge): face_edges = [first_edge] face_verts = [vert1, vert2] other_edges = [edge for edge in face.edges if edge not in face_edges] face_loops = [] # ! move to vert processing above? To maintain order ids? def add_vert_loop(ver): #add vert link_loops for loop in ver.link_loops: if loop.face == face: face_loops.append(loop) break add_vert_loop(vert1) #add loops in same order as verts add_vert_loop(vert2) for _ in range(len(other_edges)): found_edge = None # get sorted verts and edges for edge in other_edges: if face_verts[-1] in edge.verts: other_vert = edge.other_vert(face_verts[-1]) if other_vert not in face_verts: face_verts.append(other_vert) add_vert_loop(other_vert) found_edge = edge if found_edge not in face_edges: face_edges.append(edge) break other_edges.remove(found_edge) return [face_loops, face_verts, face_edges] panels = ( VOT_PT_CopyVertIds, ) def update_panel(self, context): message = "Vertex Order: Updating Panel locations has failed" try: for panel in panels: if "bl_rna" in panel.__dict__: bpy.utils.unregister_class(panel) for panel in panels: panel.bl_category = context.preferences.addons[__name__].preferences.category bpy.utils.register_class(panel) except Exception as e: print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e)) pass class WertOrderPreferences(bpy.types.AddonPreferences): # this must match the addon name, use '__package__' # when defining this in a submodule of a python package. bl_idname = __name__ category: bpy.props.StringProperty( name="标签类别", description="为面板类别选择一个名称", default="Tools", update=update_panel ) def draw(self, context): layout = self.layout row = layout.row() col = row.column() col.label(text="标签类别:") col.prop(self, "category", text="") def get_color_layer(bm): color_layer = bm.loops.layers.color.get("VTX_TRANSFER", None) if not color_layer: color_layer = bm.loops.layers.color.new("VTX_TRANSFER") return color_layer def activate_color_layer(obj): obj.data.vertex_colors["VTX_TRANSFER"].active = True class VOT_OT_SelectNotTransfered(bpy.types.Operator): bl_idname = "object.select_not_transfered_verts" bl_label = "选择未传递的顶点" bl_description = "选择所有尚未传递顶点顺序的面" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): active_obj = context.active_object self.obj = active_obj bm = bmesh.from_edit_mesh(active_obj.data) color_layer = get_color_layer(bm) something_left = False for face in bm.faces: for loop in face.loops: if loop[color_layer].z > 0.01: face.select = True something_left = True break else: face.select = False bmesh.update_edit_mesh(active_obj.data) activate_color_layer(active_obj) if something_left: self.report({"INFO"}, "已选择面") else: self.report({"INFO"}, "顶点顺序传递完成,没有可选择的内容") return {"FINISHED"} class VOT_OT_PreviewVertexOrder(bpy.types.Operator): bl_idname = "object.preview_vertex_order" bl_label = "预览顶点顺序" bl_description = "通过从选中物体在活动物体上创建形态键来预览顶点顺序" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): if not len(context.selected_objects) == 2: cls.poll_message_set("需要选择2个物体") return False return True def execute(self, context): selected_objects = context.selected_objects target_obj = context.active_object source_obj = next(obj for obj in selected_objects if obj != target_obj) if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode = "OBJECT") shape_keys = target_obj.data.shape_keys if not shape_keys: target_obj.shape_key_add(name="Basis") shape_keys = target_obj.data.shape_keys key_blocks = shape_keys.key_blocks # if we already have that shape key, then we remove it first if "VTX_TRANSFER" in key_blocks: previous_shape_key = key_blocks["VTX_TRANSFER"] shape_key_index = key_blocks[:].index(previous_shape_key) current_shape_key_index = target_obj.active_shape_key_index target_obj.active_shape_key_index = shape_key_index bpy.ops.object.shape_key_remove() target_obj.active_shape_key_index = current_shape_key_index bpy.ops.object.join_shapes() shape_key = key_blocks[:][-1] shape_key.name = 'VTX_TRANSFER' shape_key.value = 1.0 return {"FINISHED"} classes = ( WertOrderPreferences, VOT_OT_TransferVertId, VOT_OT_TransferVertIdByUV, VOT_OT_CopyVertID, VOT_OT_PasteVertID, VOT_OT_SelectNotTransfered, VOT_PT_CopyVertIds, VOT_OT_CopyPasteVertID, VOT_OT_PreviewVertexOrder ) def register(): bpy.types.Scene.copy_indices = CopyIDs() # bpy.ops.wm.addon_enable(module="space_view3d_copy_attributes") from bpy.utils import register_class for cls in classes: register_class(cls) update_panel(None, bpy.context) def unregister(): from bpy.utils import unregister_class for cls in reversed(classes): unregister_class(cls) del bpy.types.Scene.copy_indices if __name__ == "__main__": register()