745 lines
28 KiB
Python
745 lines
28 KiB
Python
# ##### 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<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'}, '已粘贴 '+str(copiedCount)+' 个顶点ID')
|
||
return {"FINISHED"}
|
||
|
||
class VOT_OT_TransferVertIdByUV(bpy.types.Operator):
|
||
"""通过UV传递顶点ID"""
|
||
bl_label = "使用UV传递ID"
|
||
bl_idname = "object.vert_id_transfer_uv"
|
||
bl_description = "使用UV从选中物体传递顶点ID到活动物体(适用于形状不同但UV相同的网格)\n需要选择两个网格物体"
|
||
bl_options = {'REGISTER'}
|
||
|
||
delta: bpy.props.FloatProperty(name="误差", description="搜索距离", 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'}, '你需要选择两个网格物体(源物体和接收顶点顺序的目标物体)!取消操作')
|
||
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'}, '已粘贴 '+str(copiedCount)+' 个顶点ID')
|
||
bm_src.free()
|
||
return {"FINISHED"}
|
||
|
||
|
||
class VOT_OT_CopyVertID(bpy.types.Operator):
|
||
bl_idname = "object.copy_vert_id"
|
||
bl_label = "复制顶点ID"
|
||
bl_description = "通过拓扑复制顶点ID(需要选择两个面)\n网格形状可以不同,但拓扑必须相同"
|
||
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'}, "必须选择两个面")
|
||
return {'CANCELLED'}
|
||
if not active_face or active_face not in sel_faces:
|
||
self.report({'WARNING'}, "必须有一个活动面")
|
||
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"}, "顶点顺序已复制。")
|
||
return {'FINISHED'}
|
||
|
||
|
||
class VOT_OT_PasteVertID(bpy.types.Operator):
|
||
bl_idname = "object.paste_vert_id"
|
||
bl_label = "粘贴顶点ID"
|
||
bl_description = (
|
||
"通过拓扑粘贴顶点ID(需要选择两个与源物体拓扑匹配的面)\n"
|
||
"网格形状可以不同,但拓扑必须相同"
|
||
)
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
invert_normals: BoolProperty(name="反转法线", description="反转法线", 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'}, "必须选择两个面")
|
||
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()
|