object_print3d_utils: replace f-strings by str.format() for I18n
[blender-addons.git] / space_view3d_copy_attributes.py
blob3ceaf1990636e57563d12697b783c27cb69961a0
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Copy Attributes Menu",
5 "author": "Bassam Kurdali, Fabian Fricke, Adam Wiseman, Demeter Dzadik",
6 "version": (0, 5, 0),
7 "blender": (3, 0, 0),
8 "location": "View3D > Ctrl-C",
9 "description": "Copy Attributes Menu",
10 "doc_url": "{BLENDER_MANUAL_URL}/addons/interface/copy_attributes.html",
11 "category": "Interface",
14 import bpy
15 from mathutils import Matrix
16 from bpy.types import (
17 Operator,
18 Menu,
20 from bpy.props import (
21 BoolVectorProperty,
22 StringProperty,
25 # First part of the operator Info message
26 INFO_MESSAGE = "Copy Attributes: "
29 def build_exec(loopfunc, func):
30 """Generator function that returns exec functions for operators """
32 def exec_func(self, context):
33 loopfunc(self, context, func)
34 return {'FINISHED'}
35 return exec_func
38 def build_invoke(loopfunc, func):
39 """Generator function that returns invoke functions for operators"""
41 def invoke_func(self, context, event):
42 loopfunc(self, context, func)
43 return {'FINISHED'}
44 return invoke_func
47 def build_op(idname, label, description, fpoll, fexec, finvoke):
48 """Generator function that returns the basic operator"""
50 class myopic(Operator):
51 bl_idname = idname
52 bl_label = label
53 bl_description = description
54 execute = fexec
55 poll = fpoll
56 invoke = finvoke
57 return myopic
60 def genops(copylist, oplist, prefix, poll_func, loopfunc):
61 """Generate ops from the copy list and its associated functions"""
62 for op in copylist:
63 exec_func = build_exec(loopfunc, op[3])
64 invoke_func = build_invoke(loopfunc, op[3])
65 opclass = build_op(prefix + op[0], "Copy " + op[1], op[2],
66 poll_func, exec_func, invoke_func)
67 oplist.append(opclass)
70 def generic_copy(source, target, string=""):
71 """Copy attributes from source to target that have string in them"""
72 for attr in dir(source):
73 if attr.find(string) > -1:
74 try:
75 setattr(target, attr, getattr(source, attr))
76 except:
77 pass
78 return
81 def getmat(bone, active, context, ignoreparent):
82 """Helper function for visual transform copy,
83 gets the active transform in bone space
84 """
85 obj_bone = bone.id_data
86 obj_active = active.id_data
87 data_bone = obj_bone.data.bones[bone.name]
88 # all matrices are in armature space unless commented otherwise
89 active_to_selected = obj_bone.matrix_world.inverted() @ obj_active.matrix_world
90 active_matrix = active_to_selected @ active.matrix
91 otherloc = active_matrix # final 4x4 mat of target, location.
92 bonemat_local = data_bone.matrix_local.copy() # self rest matrix
93 if data_bone.parent:
94 parentposemat = obj_bone.pose.bones[data_bone.parent.name].matrix.copy()
95 parentbonemat = data_bone.parent.matrix_local.copy()
96 else:
97 parentposemat = parentbonemat = Matrix()
98 if parentbonemat == parentposemat or ignoreparent:
99 newmat = bonemat_local.inverted() @ otherloc
100 else:
101 bonemat = parentbonemat.inverted() @ bonemat_local
103 newmat = bonemat.inverted() @ parentposemat.inverted() @ otherloc
104 return newmat
107 def rotcopy(item, mat):
108 """Copy rotation to item from matrix mat depending on item.rotation_mode"""
109 if item.rotation_mode == 'QUATERNION':
110 item.rotation_quaternion = mat.to_3x3().to_quaternion()
111 elif item.rotation_mode == 'AXIS_ANGLE':
112 rot = mat.to_3x3().to_quaternion().to_axis_angle() # returns (Vector((x, y, z)), w)
113 axis_angle = rot[1], rot[0][0], rot[0][1], rot[0][2] # convert to w, x, y, z
114 item.rotation_axis_angle = axis_angle
115 else:
116 item.rotation_euler = mat.to_3x3().to_euler(item.rotation_mode)
119 def pLoopExec(self, context, funk):
120 """Loop over selected bones and execute funk on them"""
121 active = context.active_pose_bone
122 selected = context.selected_pose_bones
123 selected.remove(active)
124 for bone in selected:
125 funk(bone, active, context)
128 # The following functions are used to copy attributes from active to bone
130 def pLocLocExec(bone, active, context):
131 bone.location = active.location
134 def pLocRotExec(bone, active, context):
135 rotcopy(bone, active.matrix_basis.to_3x3())
138 def pLocScaExec(bone, active, context):
139 bone.scale = active.scale
142 def pVisLocExec(bone, active, context):
143 bone.location = getmat(bone, active, context, False).to_translation()
146 def pVisRotExec(bone, active, context):
147 obj_bone = bone.id_data
148 rotcopy(bone, getmat(bone, active,
149 context, not obj_bone.data.bones[bone.name].use_inherit_rotation))
152 def pVisScaExec(bone, active, context):
153 obj_bone = bone.id_data
154 bone.scale = getmat(bone, active, context,
155 not obj_bone.data.bones[bone.name].use_inherit_scale)\
156 .to_scale()
159 def pDrwExec(bone, active, context):
160 bone.custom_shape = active.custom_shape
161 bone.use_custom_shape_bone_size = active.use_custom_shape_bone_size
162 bone.custom_shape_translation = active.custom_shape_translation
163 bone.custom_shape_rotation_euler = active.custom_shape_rotation_euler
164 bone.custom_shape_scale_xyz = active.custom_shape_scale_xyz
165 bone.bone.show_wire = active.bone.show_wire
168 def pLokExec(bone, active, context):
169 for index, state in enumerate(active.lock_location):
170 bone.lock_location[index] = state
171 for index, state in enumerate(active.lock_rotation):
172 bone.lock_rotation[index] = state
173 bone.lock_rotations_4d = active.lock_rotations_4d
174 bone.lock_rotation_w = active.lock_rotation_w
175 for index, state in enumerate(active.lock_scale):
176 bone.lock_scale[index] = state
179 def pConExec(bone, active, context):
180 for old_constraint in active.constraints.values():
181 new_constraint = bone.constraints.new(old_constraint.type)
182 generic_copy(old_constraint, new_constraint)
185 def pIKsExec(bone, active, context):
186 generic_copy(active, bone, "ik_")
189 def pBBonesExec(bone, active, context):
190 object = active.id_data
191 generic_copy(
192 object.data.bones[active.name],
193 object.data.bones[bone.name],
194 "bbone_")
197 pose_copies = (
198 ('pose_loc_loc', "Local Location",
199 "Copy Location from Active to Selected", pLocLocExec),
200 ('pose_loc_rot', "Local Rotation",
201 "Copy Rotation from Active to Selected", pLocRotExec),
202 ('pose_loc_sca', "Local Scale",
203 "Copy Scale from Active to Selected", pLocScaExec),
204 ('pose_vis_loc', "Visual Location",
205 "Copy Location from Active to Selected", pVisLocExec),
206 ('pose_vis_rot', "Visual Rotation",
207 "Copy Rotation from Active to Selected", pVisRotExec),
208 ('pose_vis_sca', "Visual Scale",
209 "Copy Scale from Active to Selected", pVisScaExec),
210 ('pose_drw', "Bone Shape",
211 "Copy Bone Shape from Active to Selected", pDrwExec),
212 ('pose_lok', "Protected Transform",
213 "Copy Protected Transforms from Active to Selected", pLokExec),
214 ('pose_con', "Bone Constraints",
215 "Copy Object Constraints from Active to Selected", pConExec),
216 ('pose_iks', "IK Limits",
217 "Copy IK Limits from Active to Selected", pIKsExec),
218 ('bbone_settings', "BBone Settings",
219 "Copy BBone Settings from Active to Selected", pBBonesExec),
222 @classmethod
223 def pose_poll_func(cls, context):
224 return(context.mode == 'POSE')
227 def pose_invoke_func(self, context, event):
228 wm = context.window_manager
229 wm.invoke_props_dialog(self)
230 return {'RUNNING_MODAL'}
233 CustomPropSelectionBoolsProperty = BoolVectorProperty(
234 size=32,
235 options={'SKIP_SAVE'}
238 class CopySelection:
239 """Base class for copying properties from active to selected based on a selection."""
241 selection: CustomPropSelectionBoolsProperty
243 def draw_bools(self, button_names):
244 """Draws the boolean toggle list with a list of strings for the button texts."""
245 layout = self.layout
246 for idx, name in enumerate(button_names):
247 layout.prop(self, "selection", index=idx, text=name,
248 toggle=True)
250 def copy_custom_property(source, destination, prop_name):
251 """Copy a custom property called prop_name, from source to destination.
252 source and destination must be a Blender data type that can hold custom properties.
253 For a list of such data types, see:
254 https://docs.blender.org/manual/en/latest/files/data_blocks.html#files-data-blocks-custom-properties
257 # Create the property.
258 destination[prop_name] = source[prop_name]
259 # Copy the settings of the property.
260 try:
261 dst_prop_manager = destination.id_properties_ui(prop_name)
262 except TypeError:
263 # Python values like lists or dictionaries don't have any settings to copy.
264 # They just consist of a value and nothing else.
265 return
267 src_prop_manager = source.id_properties_ui(prop_name)
268 assert src_prop_manager, f'Property "{prop_name}" not found in {source}'
270 dst_prop_manager.update_from(src_prop_manager)
272 # Copy the Library Overridable flag, which is stored elsewhere.
273 prop_rna_path = f'["{prop_name}"]'
274 is_lib_overridable = source.is_property_overridable_library(prop_rna_path)
275 destination.property_overridable_library_set(prop_rna_path, is_lib_overridable)
277 class CopyCustomProperties(CopySelection):
278 """Base class for copying a selection of custom properties."""
280 def copy_selected_custom_props(self, active, selected):
281 keys = list(active.keys())
282 for item in selected:
283 if item == active:
284 continue
285 for index, is_selected in enumerate(self.selection):
286 if is_selected:
287 copy_custom_property(active, item, keys[index])
289 class CopySelectedBoneCustomProperties(CopyCustomProperties, Operator):
290 """Copy Chosen custom properties from active to selected"""
291 bl_idname = "pose.copy_selected_custom_props"
292 bl_label = "Copy Selected Custom Properties"
293 bl_options = {'REGISTER', 'UNDO'}
295 poll = pose_poll_func
296 invoke = pose_invoke_func
298 def draw(self, context):
299 self.draw_bools(context.active_pose_bone.keys())
301 def execute(self, context):
302 self.copy_selected_custom_props(context.active_pose_bone, context.selected_pose_bones)
303 return {'FINISHED'}
306 class CopySelectedPoseConstraints(Operator):
307 """Copy Chosen constraints from active to selected"""
308 bl_idname = "pose.copy_selected_constraints"
309 bl_label = "Copy Selected Constraints"
311 selection: BoolVectorProperty(
312 size=32,
313 options={'SKIP_SAVE'}
316 poll = pose_poll_func
317 invoke = pose_invoke_func
319 def draw(self, context):
320 layout = self.layout
321 for idx, const in enumerate(context.active_pose_bone.constraints):
322 layout.prop(self, "selection", index=idx, text=const.name,
323 toggle=True)
325 def execute(self, context):
326 active = context.active_pose_bone
327 selected = context.selected_pose_bones[:]
328 selected.remove(active)
329 for bone in selected:
330 for index, flag in enumerate(self.selection):
331 if flag:
332 bone.constraints.copy(active.constraints[index])
333 return {'FINISHED'}
336 pose_ops = [] # list of pose mode copy operators
337 genops(pose_copies, pose_ops, "pose.copy_", pose_poll_func, pLoopExec)
340 class VIEW3D_MT_posecopypopup(Menu):
341 bl_label = "Copy Attributes"
343 def draw(self, context):
344 layout = self.layout
345 layout.operator_context = 'INVOKE_REGION_WIN'
346 for op in pose_copies:
347 layout.operator("pose.copy_" + op[0])
348 layout.operator("pose.copy_selected_constraints")
349 layout.operator("pose.copy_selected_custom_props")
350 layout.operator("pose.copy", text="copy pose")
353 def obLoopExec(self, context, funk):
354 """Loop over selected objects and execute funk on them"""
355 active = context.active_object
356 selected = context.selected_objects[:]
357 selected.remove(active)
358 for obj in selected:
359 msg = funk(obj, active, context)
360 if msg:
361 self.report({msg[0]}, INFO_MESSAGE + msg[1])
364 def world_to_basis(active, ob, context):
365 """put world coords of active as basis coords of ob"""
366 local = ob.parent.matrix_world.inverted() @ active.matrix_world
367 P = ob.matrix_basis @ ob.matrix_local.inverted()
368 mat = P @ local
369 return(mat)
372 # The following functions are used to copy attributes from
373 # active to selected object
375 def obLoc(ob, active, context):
376 ob.location = active.location
379 def obRot(ob, active, context):
380 rotcopy(ob, active.matrix_local.to_3x3())
383 def obSca(ob, active, context):
384 ob.scale = active.scale
387 def obVisLoc(ob, active, context):
388 if ob.parent:
389 mat = world_to_basis(active, ob, context)
390 ob.location = mat.to_translation()
391 else:
392 ob.location = active.matrix_world.to_translation()
393 return('INFO', "Object location copied")
396 def obVisRot(ob, active, context):
397 if ob.parent:
398 mat = world_to_basis(active, ob, context)
399 rotcopy(ob, mat.to_3x3())
400 else:
401 rotcopy(ob, active.matrix_world.to_3x3())
402 return('INFO', "Object rotation copied")
405 def obVisSca(ob, active, context):
406 if ob.parent:
407 mat = world_to_basis(active, ob, context)
408 ob.scale = mat.to_scale()
409 else:
410 ob.scale = active.matrix_world.to_scale()
411 return('INFO', "Object scale copied")
414 def obDrw(ob, active, context):
415 ob.display_type = active.display_type
416 ob.show_axis = active.show_axis
417 ob.show_bounds = active.show_bounds
418 ob.display_bounds_type = active.display_bounds_type
419 ob.show_name = active.show_name
420 ob.show_texture_space = active.show_texture_space
421 ob.show_transparent = active.show_transparent
422 ob.show_wire = active.show_wire
423 ob.show_in_front = active.show_in_front
424 ob.empty_display_type = active.empty_display_type
425 ob.empty_display_size = active.empty_display_size
428 def obOfs(ob, active, context):
429 ob.time_offset = active.time_offset
430 return('INFO', "Time offset copied")
433 def obDup(ob, active, context):
434 generic_copy(active, ob, "dupli")
435 return('INFO', "Duplication method copied")
438 def obCol(ob, active, context):
439 ob.color = active.color
442 def obLok(ob, active, context):
443 for index, state in enumerate(active.lock_location):
444 ob.lock_location[index] = state
445 for index, state in enumerate(active.lock_rotation):
446 ob.lock_rotation[index] = state
447 ob.lock_rotations_4d = active.lock_rotations_4d
448 ob.lock_rotation_w = active.lock_rotation_w
449 for index, state in enumerate(active.lock_scale):
450 ob.lock_scale[index] = state
451 return('INFO', "Transform locks copied")
454 def obCon(ob, active, context):
455 # for consistency with 2.49, delete old constraints first
456 for removeconst in ob.constraints:
457 ob.constraints.remove(removeconst)
458 for old_constraint in active.constraints.values():
459 new_constraint = ob.constraints.new(old_constraint.type)
460 generic_copy(old_constraint, new_constraint)
461 return('INFO', "Constraints copied")
464 def obTex(ob, active, context):
465 if 'texspace_location' in dir(ob.data) and 'texspace_location' in dir(
466 active.data):
467 ob.data.texspace_location[:] = active.data.texspace_location[:]
468 if 'texspace_size' in dir(ob.data) and 'texspace_size' in dir(active.data):
469 ob.data.texspace_size[:] = active.data.texspace_size[:]
470 return('INFO', "Texture space copied")
473 def obIdx(ob, active, context):
474 ob.pass_index = active.pass_index
475 return('INFO', "Pass index copied")
478 def obMod(ob, active, context):
479 for modifier in ob.modifiers:
480 # remove existing before adding new:
481 ob.modifiers.remove(modifier)
482 for old_modifier in active.modifiers.values():
483 new_modifier = ob.modifiers.new(name=old_modifier.name,
484 type=old_modifier.type)
485 generic_copy(old_modifier, new_modifier)
486 return('INFO', "Modifiers copied")
489 def obGrp(ob, active, context):
490 for grp in bpy.data.collections:
491 if active.name in grp.objects and ob.name not in grp.objects:
492 grp.objects.link(ob)
493 return('INFO', "Groups copied")
496 def obWei(ob, active, context):
497 # sanity check: are source and target both mesh objects?
498 if ob.type != 'MESH' or active.type != 'MESH':
499 return('ERROR', "objects have to be of mesh type, doing nothing")
500 me_source = active.data
501 me_target = ob.data
502 # sanity check: do source and target have the same amount of verts?
503 if len(me_source.vertices) != len(me_target.vertices):
504 return('ERROR', "objects have different vertex counts, doing nothing")
505 vgroups_IndexName = {}
506 for i in range(0, len(active.vertex_groups)):
507 groups = active.vertex_groups[i]
508 vgroups_IndexName[groups.index] = groups.name
509 data = {} # vert_indices, [(vgroup_index, weights)]
510 for v in me_source.vertices:
511 vg = v.groups
512 vi = v.index
513 if len(vg) > 0:
514 vgroup_collect = []
515 for i in range(0, len(vg)):
516 vgroup_collect.append((vg[i].group, vg[i].weight))
517 data[vi] = vgroup_collect
518 # write data to target
519 if ob != active:
520 # add missing vertex groups
521 for vgroup_name in vgroups_IndexName.values():
522 # check if group already exists...
523 already_present = 0
524 for i in range(0, len(ob.vertex_groups)):
525 if ob.vertex_groups[i].name == vgroup_name:
526 already_present = 1
527 # ... if not, then add
528 if already_present == 0:
529 ob.vertex_groups.new(name=vgroup_name)
530 # write weights
531 for v in me_target.vertices:
532 for vi_source, vgroupIndex_weight in data.items():
533 if v.index == vi_source:
535 for i in range(0, len(vgroupIndex_weight)):
536 groupName = vgroups_IndexName[vgroupIndex_weight[i][0]]
537 groups = ob.vertex_groups
538 for vgs in range(0, len(groups)):
539 if groups[vgs].name == groupName:
540 groups[vgs].add((v.index,),
541 vgroupIndex_weight[i][1], "REPLACE")
542 return('INFO', "Weights copied")
545 object_copies = (
546 # ('obj_loc', "Location",
547 # "Copy Location from Active to Selected", obLoc),
548 # ('obj_rot', "Rotation",
549 # "Copy Rotation from Active to Selected", obRot),
550 # ('obj_sca', "Scale",
551 # "Copy Scale from Active to Selected", obSca),
552 ('obj_vis_loc', "Location",
553 "Copy Location from Active to Selected", obVisLoc),
554 ('obj_vis_rot', "Rotation",
555 "Copy Rotation from Active to Selected", obVisRot),
556 ('obj_vis_sca', "Scale",
557 "Copy Scale from Active to Selected", obVisSca),
558 ('obj_drw', "Draw Options",
559 "Copy Draw Options from Active to Selected", obDrw),
560 ('obj_ofs', "Time Offset",
561 "Copy Time Offset from Active to Selected", obOfs),
562 ('obj_dup', "Dupli",
563 "Copy Dupli from Active to Selected", obDup),
564 ('obj_col', "Object Color",
565 "Copy Object Color from Active to Selected", obCol),
566 # ('obj_dmp', "Damping",
567 # "Copy Damping from Active to Selected"),
568 # ('obj_all', "All Physical Attributes",
569 # "Copy Physical Attributes from Active to Selected"),
570 # ('obj_prp', "Properties",
571 # "Copy Properties from Active to Selected"),
572 # ('obj_log', "Logic Bricks",
573 # "Copy Logic Bricks from Active to Selected"),
574 ('obj_lok', "Protected Transform",
575 "Copy Protected Transforms from Active to Selected", obLok),
576 ('obj_con', "Object Constraints",
577 "Copy Object Constraints from Active to Selected", obCon),
578 # ('obj_nla', "NLA Strips",
579 # "Copy NLA Strips from Active to Selected"),
580 # ('obj_tex', "Texture Space",
581 # "Copy Texture Space from Active to Selected", obTex),
582 # ('obj_sub', "Subdivision Surface Settings",
583 # "Copy Subdivision Surface Settings from Active to Selected"),
584 # ('obj_smo', "AutoSmooth",
585 # "Copy AutoSmooth from Active to Selected"),
586 ('obj_idx', "Pass Index",
587 "Copy Pass Index from Active to Selected", obIdx),
588 ('obj_mod', "Modifiers",
589 "Copy Modifiers from Active to Selected", obMod),
590 ('obj_wei', "Vertex Weights",
591 "Copy vertex weights based on indices", obWei),
592 ('obj_grp', "Group Links",
593 "Copy selected into active object's groups", obGrp)
597 @classmethod
598 def object_poll_func(cls, context):
599 return (len(context.selected_objects) > 1)
602 def object_invoke_func(self, context, event):
603 wm = context.window_manager
604 wm.invoke_props_dialog(self)
605 return {'RUNNING_MODAL'}
608 class CopySelectedObjectConstraints(Operator):
609 """Copy Chosen constraints from active to selected"""
610 bl_idname = "object.copy_selected_constraints"
611 bl_label = "Copy Selected Constraints"
613 selection: BoolVectorProperty(
614 size=32,
615 options={'SKIP_SAVE'}
618 poll = object_poll_func
619 invoke = object_invoke_func
621 def draw(self, context):
622 layout = self.layout
623 for idx, const in enumerate(context.active_object.constraints):
624 layout.prop(self, "selection", index=idx, text=const.name,
625 toggle=True)
627 def execute(self, context):
628 active = context.active_object
629 selected = context.selected_objects[:]
630 selected.remove(active)
631 for obj in selected:
632 for index, flag in enumerate(self.selection):
633 if flag:
634 obj.constraints.copy(active.constraints[index])
635 return{'FINISHED'}
638 class CopySelectedObjectModifiers(Operator):
639 """Copy Chosen modifiers from active to selected"""
640 bl_idname = "object.copy_selected_modifiers"
641 bl_label = "Copy Selected Modifiers"
643 selection: BoolVectorProperty(
644 size=32,
645 options={'SKIP_SAVE'}
648 poll = object_poll_func
649 invoke = object_invoke_func
651 def draw(self, context):
652 layout = self.layout
653 for idx, const in enumerate(context.active_object.modifiers):
654 layout.prop(self, 'selection', index=idx, text=const.name,
655 toggle=True)
657 def execute(self, context):
658 active = context.active_object
659 selected = context.selected_objects[:]
660 selected.remove(active)
661 for obj in selected:
662 for index, flag in enumerate(self.selection):
663 if flag:
664 old_modifier = active.modifiers[index]
665 new_modifier = obj.modifiers.new(
666 type=active.modifiers[index].type,
667 name=active.modifiers[index].name
669 generic_copy(old_modifier, new_modifier)
670 return{'FINISHED'}
673 class CopySelectedObjectCustomProperties(CopyCustomProperties, Operator):
674 """Copy Chosen custom properties from active to selected objects"""
675 bl_idname = "object.copy_selected_custom_props"
676 bl_label = "Copy Selected Custom Properties"
677 bl_options = {'REGISTER', 'UNDO'}
679 poll = object_poll_func
680 invoke = object_invoke_func
682 def draw(self, context):
683 self.draw_bools(context.object.keys())
685 def execute(self, context):
686 self.copy_selected_custom_props(context.object, context.selected_objects)
687 return {'FINISHED'}
689 object_ops = []
690 genops(object_copies, object_ops, "object.copy_", object_poll_func, obLoopExec)
693 class VIEW3D_MT_copypopup(Menu):
694 bl_label = "Copy Attributes"
696 def draw(self, context):
697 layout = self.layout
699 layout.operator_context = 'INVOKE_REGION_WIN'
700 layout.operator("view3d.copybuffer", icon="COPY_ID")
702 if (len(context.selected_objects) <= 1):
703 layout.separator()
704 layout.label(text="Please select at least two objects", icon="INFO")
705 layout.separator()
707 for entry, op in enumerate(object_copies):
708 if entry and entry % 4 == 0:
709 layout.separator()
710 layout.operator("object.copy_" + op[0])
711 layout.operator("object.copy_selected_constraints")
712 layout.operator("object.copy_selected_modifiers")
713 layout.operator("object.copy_selected_custom_props")
716 # Begin Mesh copy settings:
718 class MESH_MT_CopyFaceSettings(Menu):
719 bl_label = "Copy Face Settings"
721 @classmethod
722 def poll(cls, context):
723 return context.mode == 'EDIT_MESH'
725 def draw(self, context):
726 mesh = context.object.data
727 uv = len(mesh.uv_layers) > 1
728 vc = len(mesh.vertex_colors) > 1
730 layout = self.layout
732 op = layout.operator(MESH_OT_CopyFaceSettings.bl_idname,
733 text="Copy Material")
734 op['layer'] = ''
735 op['mode'] = 'MAT'
737 if mesh.uv_layers.active:
738 op = layout.operator(MESH_OT_CopyFaceSettings.bl_idname,
739 text="Copy Active UV Coords")
740 op['layer'] = ''
741 op['mode'] = 'UV'
743 if mesh.vertex_colors.active:
744 op = layout.operator(MESH_OT_CopyFaceSettings.bl_idname,
745 text="Copy Active Vertex Colors")
746 op['layer'] = ''
747 op['mode'] = 'VCOL'
749 if uv or vc:
750 layout.separator()
751 if uv:
752 layout.menu("MESH_MT_CopyImagesFromLayer")
753 layout.menu("MESH_MT_CopyUVCoordsFromLayer")
754 if vc:
755 layout.menu("MESH_MT_CopyVertexColorsFromLayer")
758 # Data (UV map, Image and Vertex color) menus calling MESH_OT_CopyFaceSettings
759 # Explicitly defined as using the generator code was broken in case of Menus
760 # causing issues with access and registration
763 class MESH_MT_CopyUVCoordsFromLayer(Menu):
764 bl_label = "Copy Other UV Coord Layers"
766 @classmethod
767 def poll(cls, context):
768 obj = context.active_object
769 return obj and obj.mode == "EDIT_MESH" and len(
770 obj.data.uv_layers) > 1
772 def draw(self, context):
773 mesh = context.active_object.data
774 _buildmenu(self, mesh, 'UV', "GROUP_UVS")
777 class MESH_MT_CopyVertexColorsFromLayer(Menu):
778 bl_label = "Copy Other Vertex Colors Layers"
780 @classmethod
781 def poll(cls, context):
782 obj = context.active_object
783 return obj and obj.mode == "EDIT_MESH" and len(
784 obj.data.vertex_colors) > 1
786 def draw(self, context):
787 mesh = context.active_object.data
788 _buildmenu(self, mesh, 'VCOL', "GROUP_VCOL")
791 def _buildmenu(self, mesh, mode, icon):
792 layout = self.layout
793 if mode == 'VCOL':
794 layers = mesh.vertex_colors
795 else:
796 layers = mesh.uv_layers
797 for layer in layers:
798 if not layer.active:
799 op = layout.operator(MESH_OT_CopyFaceSettings.bl_idname,
800 text=layer.name, icon=icon)
801 op['layer'] = layer.name
802 op['mode'] = mode
805 class MESH_OT_CopyFaceSettings(Operator):
806 """Copy settings from active face to all selected faces"""
807 bl_idname = 'mesh.copy_face_settings'
808 bl_label = "Copy Face Settings"
809 bl_options = {'REGISTER', 'UNDO'}
811 mode: StringProperty(
812 name="Mode",
813 options={"HIDDEN"},
815 layer: StringProperty(
816 name="Layer",
817 options={"HIDDEN"},
820 @classmethod
821 def poll(cls, context):
822 return context.mode == 'EDIT_MESH'
824 def execute(self, context):
825 mode = getattr(self, 'mode', '')
826 if mode not in {'MAT', 'VCOL', 'UV'}:
827 self.report({'ERROR'}, "No mode specified or invalid mode")
828 return self._end(context, {'CANCELLED'})
829 layername = getattr(self, 'layer', '')
830 mesh = context.object.data
832 # Switching out of edit mode updates the selected state of faces and
833 # makes the data from the uv texture and vertex color layers available.
834 bpy.ops.object.editmode_toggle()
836 polys = mesh.polygons
837 if mode == 'MAT':
838 to_data = from_data = polys
839 else:
840 if mode == 'VCOL':
841 layers = mesh.vertex_colors
842 act_layer = mesh.vertex_colors.active
843 elif mode == 'UV':
844 layers = mesh.uv_layers
845 act_layer = mesh.uv_layers.active
846 if not layers or (layername and layername not in layers):
847 self.report({'ERROR'}, "Invalid UV or color layer. Operation Cancelled")
848 return self._end(context, {'CANCELLED'})
849 from_data = layers[layername or act_layer.name].data
850 to_data = act_layer.data
851 from_index = polys.active
853 for f in polys:
854 if f.select:
855 if to_data != from_data:
856 # Copying from another layer.
857 # from_face is to_face's counterpart from other layer.
858 from_index = f.index
859 elif f.index == from_index:
860 # Otherwise skip copying a face to itself.
861 continue
862 if mode == 'MAT':
863 f.material_index = polys[from_index].material_index
864 continue
865 if len(f.loop_indices) != len(polys[from_index].loop_indices):
866 self.report({'WARNING'}, "Different number of vertices.")
867 for i in range(len(f.loop_indices)):
868 to_vertex = f.loop_indices[i]
869 from_vertex = polys[from_index].loop_indices[i]
870 if mode == 'VCOL':
871 to_data[to_vertex].color = from_data[from_vertex].color
872 elif mode == 'UV':
873 to_data[to_vertex].uv = from_data[from_vertex].uv
875 return self._end(context, {'FINISHED'})
877 def _end(self, context, retval):
878 if context.mode != 'EDIT_MESH':
879 # Clean up by returning to edit mode like it was before.
880 bpy.ops.object.editmode_toggle()
881 return(retval)
884 classes = (
885 CopySelectedPoseConstraints,
886 CopySelectedBoneCustomProperties,
887 VIEW3D_MT_posecopypopup,
888 CopySelectedObjectConstraints,
889 CopySelectedObjectModifiers,
890 CopySelectedObjectCustomProperties,
891 VIEW3D_MT_copypopup,
892 MESH_MT_CopyFaceSettings,
893 MESH_MT_CopyUVCoordsFromLayer,
894 MESH_MT_CopyVertexColorsFromLayer,
895 MESH_OT_CopyFaceSettings,
896 *pose_ops,
897 *object_ops,
900 def register():
901 from bpy.utils import register_class
902 for cls in classes:
903 register_class(cls)
905 # mostly to get the keymap working
906 kc = bpy.context.window_manager.keyconfigs.addon
907 if kc:
908 km = kc.keymaps.new(name="Object Mode")
909 kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS', ctrl=True)
910 kmi.properties.name = 'VIEW3D_MT_copypopup'
912 km = kc.keymaps.new(name="Pose")
913 kmi = km.keymap_items.get("pose.copy")
914 if kmi is not None:
915 kmi.idname = 'wm.call_menu'
916 else:
917 kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS', ctrl=True)
918 kmi.properties.name = 'VIEW3D_MT_posecopypopup'
920 km = kc.keymaps.new(name="Mesh")
921 kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS')
922 kmi.ctrl = True
923 kmi.properties.name = 'MESH_MT_CopyFaceSettings'
926 def unregister():
927 # mostly to remove the keymap
928 kc = bpy.context.window_manager.keyconfigs.addon
929 if kc:
930 kms = kc.keymaps.get('Pose')
931 if kms is not None:
932 for item in kms.keymap_items:
933 if item.name == 'Call Menu' and item.idname == 'wm.call_menu' and \
934 item.properties.name == 'VIEW3D_MT_posecopypopup':
935 item.idname = 'pose.copy'
936 break
938 km = kc.keymaps.get('Mesh')
939 if km is not None:
940 for kmi in km.keymap_items:
941 if kmi.idname == 'wm.call_menu':
942 if kmi.properties.name == 'MESH_MT_CopyFaceSettings':
943 km.keymap_items.remove(kmi)
945 km = kc.keymaps.get('Object Mode')
946 if km is not None:
947 for kmi in km.keymap_items:
948 if kmi.idname == 'wm.call_menu':
949 if kmi.properties.name == 'VIEW3D_MT_copypopup':
950 km.keymap_items.remove(kmi)
952 from bpy.utils import unregister_class
953 for cls in classes:
954 unregister_class(cls)
957 if __name__ == "__main__":
958 register()