Created
September 16, 2023 17:13
-
-
Save Andrej730/7cb16bbb13f198a4e6451e951e70b471 to your computer and use it in GitHub Desktop.
transfer_vertex_order.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ##### 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": "Transfer Vert Order", | |
"author": "Jose Conseco based on UV Transfer from MagicUV Nutti", | |
"version": (2, 4), | |
"blender": (2, 82, 0), | |
"location": "Sidebar (N) -> Tools panel", | |
"description": "Transfer Verts IDs by verts proximity or by selected faces", | |
"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 = "Transfer vertex order" | |
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 = 'More options in EDIT mode') | |
layout.operator("object.vert_id_transfer_proximity") | |
layout.operator("object.vert_id_transfer_uv") | |
layout.operator("object.preview_vertex_order") | |
layout.label(text="SELECTED -> ACTIVE") | |
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="SELECTED -> ACTIVE") | |
class VOT_OT_TransferVertId(bpy.types.Operator): | |
"""Transfer vert ID by vert proximity""" | |
bl_label = "Transfer IDs using location" | |
bl_idname = "object.vert_id_transfer_proximity" | |
bl_description = "Transfer verts IDs by vert positions (for meshes with exactly same shape)\nTwo mesh objects have to be selected" | |
bl_options = {'REGISTER'} | |
delta: bpy.props.FloatProperty(name="Delta", description="SearchDistance", 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'}, 'You seed to select two mesh objects (source then target that will receive vert order)! Cancelling') | |
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<self.delta: #delta | |
copiedCount+=1 | |
edge.index = index | |
processedEdgesIdDict[edge] = index | |
for face in bm.faces: | |
co, index, dist = src_obj_kd_faces.find(face.calc_center_median()) | |
if dist<self.delta: #delta | |
copiedCount+=1 | |
face.index = index | |
processedFacesIdDict[face] = index | |
VOT_OT_PasteVertID.sortOtherVerts(processedVertsIdDict, processedEdgesIdDict, processedFacesIdDict, bm) | |
bm.verts.sort() | |
bm.edges.sort() | |
bm.faces.sort() | |
bm.to_mesh(target.data) | |
bm.free() | |
self.report({'INFO'}, 'Pasted '+str(copiedCount)+' vert id\'s ') | |
return {"FINISHED"} | |
class VOT_OT_TransferVertIdByUV(bpy.types.Operator): | |
"""Transfer vert ID by vert UVs""" | |
bl_label = "Transfer IDs using UVs" | |
bl_idname = "object.vert_id_transfer_uv" | |
bl_description = "Transfer verts IDs from selected to active object using UVs (for meshes with different shape but same UVs)\nTwo mesh objects have to be selected" | |
bl_options = {'REGISTER'} | |
delta: bpy.props.FloatProperty(name="Delta", description="SearchDistance", default=0.01, min=0, max=0.1, precision = 5) | |
@staticmethod | |
def find_face_uv_center(face: bmesh.types.BMFace, uv_layer): | |
uv_ctr = Vector((0.0, 0.0)) | |
uv_cnt = 0 | |
for loop in face.loops: | |
uv_ctr += loop[uv_layer].uv | |
uv_cnt += 1 | |
# calculate winding value to better deal with multiple faces with the same UV center | |
# (the main thing this is for is mirrored meshes that have the same UVs for both sides) | |
winding_1: Vector = (face.loops[1][uv_layer].uv - face.loops[0][uv_layer].uv).to_3d() | |
winding_2: Vector = (face.loops[2][uv_layer].uv - face.loops[0][uv_layer].uv).to_3d() | |
# by not normalizing, it also serves to differentiate by face size centers that otherwise might match | |
# will only be a z value since the input vectors were 2d on xy | |
winding = winding_1.cross(winding_2) | |
return (uv_ctr / uv_cnt).to_3d() + winding | |
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'}, 'You seed to select two mesh objects (source then target that will receive vert order)! Cancelling') | |
return {'CANCELLED'} | |
bm_src = bmesh.new() # load mesh | |
bm_src.from_mesh(sourceObj.data) | |
bm_src.faces.ensure_lookup_table() | |
src_obj_kd_faces = kdtree.KDTree(len(bm_src.faces)) | |
for f in bm_src.faces: | |
src_obj_kd_faces.insert(self.find_face_uv_center(f, bm_src.loops.layers.uv.active), f.index) | |
src_obj_kd_faces.balance() | |
processedVertsIdDict = {} | |
processedEdgesIdDict = {} | |
processedFacesIdDict = {} | |
for target in TargetObjs: | |
processedVertsIdDict.clear() | |
bm = bmesh.new() # load mesh | |
bm.from_mesh(target.data) | |
for face in bm.faces: | |
co, index, dist = src_obj_kd_faces.find(self.find_face_uv_center(face, bm.loops.layers.uv.active)) | |
if dist<self.delta: #delta | |
face.index = index | |
processedFacesIdDict[face] = index | |
for loop_src, loop_dst in zip(bm_src.faces[index].loops, face.loops): | |
# will be copied over to the actual data later (so as to only be done once per vert/edge) | |
processedEdgesIdDict[loop_dst.edge] = loop_src.edge.index | |
loop_dst.edge.index = loop_src.edge.index | |
processedVertsIdDict[loop_dst.vert] = loop_src.vert.index | |
loop_dst.vert.index = loop_src.vert.index | |
copiedCount = len(processedVertsIdDict) | |
copiedCount += len(processedEdgesIdDict) | |
copiedCount += len(processedFacesIdDict) | |
VOT_OT_PasteVertID.sortOtherVerts(processedVertsIdDict, processedEdgesIdDict, processedFacesIdDict, bm) | |
bm.verts.sort() | |
bm.edges.sort() | |
bm.faces.sort() | |
bm.to_mesh(target.data) | |
bm.free() | |
self.report({'INFO'}, 'Pasted '+str(copiedCount)+' vert id\'s ') | |
bm_src.free() | |
return {"FINISHED"} | |
class VOT_OT_CopyVertID(bpy.types.Operator): | |
bl_idname = "object.copy_vert_id" | |
bl_label = "Copy Vert IDs" | |
bl_description = "Copy verts IDs by topology (you need to selected two faces)\nMesh shape can be different, bu topology must be the same" | |
bl_options = {'REGISTER', 'UNDO'} | |
def execute(self, context): | |
props = context.scene.copy_indices.transuv | |
active_obj = context.active_object | |
self.obj = active_obj | |
bm = bmesh.from_edit_mesh(active_obj.data) | |
bm.edges.ensure_lookup_table() | |
bm.faces.ensure_lookup_table() | |
color_layer = get_color_layer(bm) | |
props.face_loop_ids.clear() | |
props.face_vert_ids.clear() | |
props.face_edge_ids.clear() | |
props.faces_id.clear() | |
# get selected faces | |
active_face = bm.faces.active | |
sel_faces = [face for face in bm.faces if face.select] | |
if len(sel_faces) != 2: | |
self.report({'WARNING'}, "Two faces must be selected") | |
return {'CANCELLED'} | |
if not active_face or active_face not in sel_faces: | |
self.report({'WARNING'}, "Two faces must be active") | |
return {'CANCELLED'} | |
# parse all faces according to selection | |
active_face_nor = active_face.normal.copy() | |
all_sorted_faces = main_parse(self, sel_faces, active_face, active_face_nor) | |
if all_sorted_faces: | |
for face, face_data in all_sorted_faces.items(): | |
loops = face_data[0] | |
verts = face_data[1] | |
edges = face_data[2] | |
props.face_loop_ids.append([loop.index for loop in loops]) | |
props.face_vert_ids.append([vert.index for vert in verts]) | |
props.face_edge_ids.append([e.index for e in edges]) | |
props.faces_id.append(face.index) | |
for loop in loops: | |
loop[color_layer] = Vector((1,0,0,1)) | |
bmesh.update_edit_mesh(active_obj.data) | |
activate_color_layer(active_obj) | |
self.report({"INFO"}, "Vertex order copied.") | |
return {'FINISHED'} | |
class VOT_OT_PasteVertID(bpy.types.Operator): | |
bl_idname = "object.paste_vert_id" | |
bl_label = "Paste Verts Ids" | |
bl_description = ( | |
"Paste verts ID by topology (you need selected two faces matching source obj topology)\n" | |
"Mesh shape can be different, bu topology must be the same" | |
) | |
bl_options = {'REGISTER', 'UNDO'} | |
invert_normals: BoolProperty(name="Invert Normals", description="Invert Normals", default=False) | |
@staticmethod | |
def sortOtherVerts(processedVertsIdDict, preocessedEdgesIsDict, preocessedFaceIsDict, bm): | |
"""Prevet verts on other islands from being all shuffled""" | |
# dicts instead of lists - faster search 4x? | |
if len(bm.verts) == len(processedVertsIdDict) and len(bm.faces) == len(preocessedFaceIsDict): | |
return #all verts, and faces were processed - > 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'}, "Two faces must be selected") | |
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'}, | |
"Mesh has different amount of faces" | |
) | |
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'}, | |
"Face have different amount of vertices" | |
) | |
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"}, "Vertex order pasted.") | |
return {'FINISHED'} | |
class VOT_OT_CopyPasteVertID(bpy.types.Operator): | |
bl_idname = "object.copy_paste_vert_id" | |
bl_label = "Copy & Paste Verts Ids" | |
bl_description = ( | |
"Copies vertex order from selected object to active. " | |
"Both should be in EDIT mode and have two similar faces selected" | |
) | |
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("Two objects have to be selected and both need to be in EDIT mode") | |
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'}, "Two faces should share one edge") | |
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'}, "More than 2 faces share edge") | |
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="Tab Category", description="Choose a name for the category of the panel", default="Tools", update=update_panel ) | |
def draw(self, context): | |
layout = self.layout | |
row = layout.row() | |
col = row.column() | |
col.label(text="Tab Category:") | |
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 = "Select Not Transfered Verts" | |
bl_description = "Select all faces which have vertex order not yet transfered" | |
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"}, "Faces are selected") | |
else: | |
self.report({"INFO"}, "Vertex order transfer complete, nothing to select") | |
return {"FINISHED"} | |
class VOT_OT_PreviewVertexOrder(bpy.types.Operator): | |
bl_idname = "object.preview_vertex_order" | |
bl_label = "Preview Vertex Order" | |
bl_description = "Preview vertex order by creating shape key from selected object on active" | |
bl_options = {'REGISTER', 'UNDO'} | |
@classmethod | |
def poll(cls, context): | |
if not len(context.selected_objects) == 2: | |
cls.poll_message_set("2 objects need to be selected") | |
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.key_blocks | |
# if we already have that shape key, then we remove it first | |
if "VTX_TRANSFER" in shape_keys: | |
previous_shape_key = shape_keys["VTX_TRANSFER"] | |
shape_key_index = shape_keys[:].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 = shape_keys[:][-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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@JoseConseco hello! Thanks for the great addon copy_verts_ids! Wanted to share my fork of it, maybe you'd fine anything useful for the main version (the first revision on this gist is official 2.3 version, so it's easy to see the changes).
The main problem with the addon for me was making sure vertex order transfered for all mesh parts (sometimes meshes have some separated parts like eyes, teeth, etc) and I've optimized it for that workflow.
Workflow example now:
CcLubZz8Jt.mp4