Fix io_anim_camera error exporting cameras with quotes in their name
[blender-addons.git] / space_view3d_copy_attributes.py
blob0a8705e28e06bc6bde3c92eac9e707c331c8602d
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 bl_info = {
6 "name": "Copy Attributes Menu",
7 "author": "Bassam Kurdali, Fabian Fricke, Adam Wiseman, Demeter Dzadik",
8 "version": (0, 5, 0),
9 "blender": (3, 0, 0),
10 "location": "View3D > Ctrl-C",
11 "description": "Copy Attributes Menu",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/interface/copy_attributes.html",
13 "category": "Interface",
16 import bpy
17 from mathutils import Matrix
18 from bpy.types import (
19 Operator,
20 Menu,
22 from bpy.props import (
23 BoolVectorProperty,
24 StringProperty,
27 # First part of the operator Info message
28 INFO_MESSAGE = "Copy Attributes: "
31 def build_exec(loopfunc, func):
32 """Generator function that returns exec functions for operators """
34 def exec_func(self, context):
35 loopfunc(self, context, func)
36 return {'FINISHED'}
37 return exec_func
40 def build_invoke(loopfunc, func):
41 """Generator function that returns invoke functions for operators"""
43 def invoke_func(self, context, event):
44 loopfunc(self, context, func)
45 return {'FINISHED'}
46 return invoke_func
49 def build_op(idname, label, description, fpoll, fexec, finvoke):
50 """Generator function that returns the basic operator"""
52 class myopic(Operator):
53 bl_idname = idname
54 bl_label = label
55 bl_description = description
56 execute = fexec
57 poll = fpoll
58 invoke = finvoke
59 return myopic
62 def genops(copylist, oplist, prefix, poll_func, loopfunc):
63 """Generate ops from the copy list and its associated functions"""
64 for op in copylist:
65 exec_func = build_exec(loopfunc, op[3])
66 invoke_func = build_invoke(loopfunc, op[3])
67 opclass = build_op(prefix + op[0], "Copy " + op[1], op[2],
68 poll_func, exec_func, invoke_func)
69 oplist.append(opclass)
72 def generic_copy(source, target, string=""):
73 """Copy attributes from source to target that have string in them"""
74 for attr in dir(source):
75 if attr.find(string) > -1:
76 try:
77 setattr(target, attr, getattr(source, attr))
78 except:
79 pass
80 return
83 def getmat(bone, active, context, ignoreparent):
84 """Helper function for visual transform copy,
85 gets the active transform in bone space
86 """
87 obj_bone = bone.id_data
88 obj_active = active.id_data
89 data_bone = obj_bone.data.bones[bone.name]
90 # all matrices are in armature space unless commented otherwise
91 active_to_selected = obj_bone.matrix_world.inverted() @ obj_active.matrix_world
92 active_matrix = active_to_selected @ active.matrix
93 otherloc = active_matrix # final 4x4 mat of target, location.
94 bonemat_local = data_bone.matrix_local.copy() # self rest matrix
95 if data_bone.parent:
96 parentposemat = obj_bone.pose.bones[data_bone.parent.name].matrix.copy()
97 parentbonemat = data_bone.parent.matrix_local.copy()
98 else:
99 parentposemat = parentbonemat = Matrix()
100 if parentbonemat == parentposemat or ignoreparent:
101 newmat = bonemat_local.inverted() @ otherloc
102 else:
103 bonemat = parentbonemat.inverted() @ bonemat_local
105 newmat = bonemat.inverted() @ parentposemat.inverted() @ otherloc
106 return newmat
109 def rotcopy(item, mat):
110 """Copy rotation to item from matrix mat depending on item.rotation_mode"""
111 if item.rotation_mode == 'QUATERNION':
112 item.rotation_quaternion = mat.to_3x3().to_quaternion()
113 elif item.rotation_mode == 'AXIS_ANGLE':
114 rot = mat.to_3x3().to_quaternion().to_axis_angle() # returns (Vector((x, y, z)), w)
115 axis_angle = rot[1], rot[0][0], rot[0][1], rot[0][2] # convert to w, x, y, z
116 item.rotation_axis_angle = axis_angle
117 else:
118 item.rotation_euler = mat.to_3x3().to_euler(item.rotation_mode)
121 def pLoopExec(self, context, funk):
122 """Loop over selected bones and execute funk on them"""
123 active = context.active_pose_bone
124 selected = context.selected_pose_bones
125 selected.remove(active)
126 for bone in selected:
127 funk(bone, active, context)
130 # The following functions are used to copy attributes from active to bone
132 def pLocLocExec(bone, active, context):
133 bone.location = active.location
136 def pLocRotExec(bone, active, context):
137 rotcopy(bone, active.matrix_basis.to_3x3())
140 def pLocScaExec(bone, active, context):
141 bone.scale = active.scale
144 def pVisLocExec(bone, active, context):
145 bone.location = getmat(bone, active, context, False).to_translation()
148 def pVisRotExec(bone, active, context):
149 obj_bone = bone.id_data
150 rotcopy(bone, getmat(bone, active,
151 context, not obj_bone.data.bones[bone.name].use_inherit_rotation))
154 def pVisScaExec(bone, active, context):
155 obj_bone = bone.id_data
156 bone.scale = getmat(bone, active, context,
157 not obj_bone.data.bones[bone.name].use_inherit_scale)\
158 .to_scale()
161 def pDrwExec(bone, active, context):
162 bone.custom_shape = active.custom_shape
163 bone.use_custom_shape_bone_size = active.use_custom_shape_bone_size
164 bone.custom_shape_translation = active.custom_shape_translation
165 bone.custom_shape_rotation_euler = active.custom_shape_rotation_euler
166 bone.custom_shape_scale_xyz = active.custom_shape_scale_xyz
167 bone.bone.show_wire = active.bone.show_wire
170 def pLokExec(bone, active, context):
171 for index, state in enumerate(active.lock_location):
172 bone.lock_location[index] = state
173 for index, state in enumerate(active.lock_rotation):
174 bone.lock_rotation[index] = state
175 bone.lock_rotations_4d = active.lock_rotations_4d
176 bone.lock_rotation_w = active.lock_rotation_w
177 for index, state in enumerate(active.lock_scale):
178 bone.lock_scale[index] = state
181 def pConExec(bone, active, context):
182 for old_constraint in active.constraints.values():
183 new_constraint = bone.constraints.new(old_constraint.type)
184 generic_copy(old_constraint, new_constraint)
187 def pIKsExec(bone, active, context):
188 generic_copy(active, bone, "ik_")
191 def pBBonesExec(bone, active, context):
192 object = active.id_data
193 generic_copy(
194 object.data.bones[active.name],
195 object.data.bones[bone.name],
196 "bbone_")
199 pose_copies = (
200 ('pose_loc_loc', "Local Location",
201 "Copy Location from Active to Selected", pLocLocExec),
202 ('pose_loc_rot', "Local Rotation",
203 "Copy Rotation from Active to Selected", pLocRotExec),
204 ('pose_loc_sca', "Local Scale",
205 "Copy Scale from Active to Selected", pLocScaExec),
206 ('pose_vis_loc', "Visual Location",
207 "Copy Location from Active to Selected", pVisLocExec),
208 ('pose_vis_rot', "Visual Rotation",
209 "Copy Rotation from Active to Selected", pVisRotExec),
210 ('pose_vis_sca', "Visual Scale",
211 "Copy Scale from Active to Selected", pVisScaExec),
212 ('pose_drw', "Bone Shape",
213 "Copy Bone Shape from Active to Selected", pDrwExec),
214 ('pose_lok', "Protected Transform",
215 "Copy Protected Transforms from Active to Selected", pLokExec),
216 ('pose_con', "Bone Constraints",
217 "Copy Object Constraints from Active to Selected", pConExec),
218 ('pose_iks', "IK Limits",
219 "Copy IK Limits from Active to Selected", pIKsExec),
220 ('bbone_settings', "BBone Settings",
221 "Copy BBone Settings from Active to Selected", pBBonesExec),
224 @classmethod
225 def pose_poll_func(cls, context):
226 return(context.mode == 'POSE')
229 def pose_invoke_func(self, context, event):
230 wm = context.window_manager
231 wm.invoke_props_dialog(self)
232 return {'RUNNING_MODAL'}
235 CustomPropSelectionBoolsProperty = BoolVectorProperty(
236 size=32,
237 options={'SKIP_SAVE'}
240 class CopySelection:
241 """Base class for copying properties from active to selected based on a selection."""
243 selection: CustomPropSelectionBoolsProperty
245 def draw_bools(self, button_names):
246 """Draws the boolean toggle list with a list of strings for the button texts."""
247 layout = self.layout
248 for idx, name in enumerate(button_names):
249 layout.prop(self, "selection", index=idx, text=name,
250 toggle=True)
252 def copy_custom_property(source, destination, prop_name):
253 """Copy a custom property called prop_name, from source to destination.
254 source and destination must be a Blender data type that can hold custom properties.
255 For a list of such data types, see:
256 https://docs.blender.org/manual/en/latest/files/data_blocks.html#files-data-blocks-custom-properties
259 # Create the property.
260 destination[prop_name] = source[prop_name]
261 # Copy the settings of the property.
262 try:
263 dst_prop_manager = destination.id_properties_ui(prop_name)
264 except TypeError:
265 # Python values like lists or dictionaries don't have any settings to copy.
266 # They just consist of a value and nothing else.
267 return
269 src_prop_manager = source.id_properties_ui(prop_name)
270 assert src_prop_manager, f'Property "{prop_name}" not found in {source}'
272 dst_prop_manager.update_from(src_prop_manager)
274 # Copy the Library Overridable flag, which is stored elsewhere.
275 prop_rna_path = f'["{prop_name}"]'
276 is_lib_overridable = source.is_property_overridable_library(prop_rna_path)
277 destination.property_overridable_library_set(prop_rna_path, is_lib_overridable)
279 class CopyCustomProperties(CopySelection):
280 """Base class for copying a selection of custom properties."""
282 def copy_selected_custom_props(self, active, selected):
283 keys = list(active.keys())
284 for item in selected:
285 if item == active:
286 continue
287 for index, is_selected in enumerate(self.selection):
288 if is_selected:
289 copy_custom_property(active, item, keys[index])
291 class CopySelectedBoneCustomProperties(CopyCustomProperties, Operator):
292 """Copy Chosen custom properties from active to selected"""
293 bl_idname = "pose.copy_selected_custom_props"
294 bl_label = "Copy Selected Custom Properties"
295 bl_options = {'REGISTER', 'UNDO'}
297 poll = pose_poll_func
298 invoke = pose_invoke_func
300 def draw(self, context):
301 self.draw_bools(context.active_pose_bone.keys())
303 def execute(self, context):
304 self.copy_selected_custom_props(context.active_pose_bone, context.selected_pose_bones)
305 return {'FINISHED'}
308 class CopySelectedPoseConstraints(Operator):
309 """Copy Chosen constraints from active to selected"""
310 bl_idname = "pose.copy_selected_constraints"
311 bl_label = "Copy Selected Constraints"
313 selection: BoolVectorProperty(
314 size=32,
315 options={'SKIP_SAVE'}
318 poll = pose_poll_func
319 invoke = pose_invoke_func
321 def draw(self, context):
322 layout = self.layout
323 for idx, const in enumerate(context.active_pose_bone.constraints):
324 layout.prop(self, "selection", index=idx, text=const.name,
325 toggle=True)
327 def execute(self, context):
328 active = context.active_pose_bone
329 selected = context.selected_pose_bones[:]
330 selected.remove(active)
331 for bone in selected:
332 for index, flag in enumerate(self.selection):
333 if flag:
334 bone.constraints.copy(active.constraints[index])
335 return {'FINISHED'}
338 pose_ops = [] # list of pose mode copy operators
339 genops(pose_copies, pose_ops, "pose.copy_", pose_poll_func, pLoopExec)
342 class VIEW3D_MT_posecopypopup(Menu):
343 bl_label = "Copy Attributes"
345 def draw(self, context):
346 layout = self.layout
347 layout.operator_context = 'INVOKE_REGION_WIN'
348 for op in pose_copies:
349 layout.operator("pose.copy_" + op[0])
350 layout.operator("pose.copy_selected_constraints")
351 layout.operator("pose.copy_selected_custom_props")
352 layout.operator("pose.copy", text="copy pose")
355 def obLoopExec(self, context, funk):
356 """Loop over selected objects and execute funk on them"""
357 active = context.active_object
358 selected = context.selected_objects[:]
359 selected.remove(active)
360 for obj in selected:
361 msg = funk(obj, active, context)
362 if msg:
363 self.report({msg[0]}, INFO_MESSAGE + msg[1])
366 def world_to_basis(active, ob, context):
367 """put world coords of active as basis coords of ob"""
368 local = ob.parent.matrix_world.inverted() @ active.matrix_world
369 P = ob.matrix_basis @ ob.matrix_local.inverted()
370 mat = P @ local
371 return(mat)
374 # The following functions are used to copy attributes from
375 # active to selected object
377 def obLoc(ob, active, context):
378 ob.location = active.location
381 def obRot(ob, active, context):
382 rotcopy(ob, active.matrix_local.to_3x3())
385 def obSca(ob, active, context):
386 ob.scale = active.scale
389 def obVisLoc(ob, active, context):
390 if ob.parent:
391 mat = world_to_basis(active, ob, context)
392 ob.location = mat.to_translation()
393 else:
394 ob.location = active.matrix_world.to_translation()
395 return('INFO', "Object location copied")
398 def obVisRot(ob, active, context):
399 if ob.parent:
400 mat = world_to_basis(active, ob, context)
401 rotcopy(ob, mat.to_3x3())
402 else:
403 rotcopy(ob, active.matrix_world.to_3x3())
404 return('INFO', "Object rotation copied")
407 def obVisSca(ob, active, context):
408 if ob.parent:
409 mat = world_to_basis(active, ob, context)
410 ob.scale = mat.to_scale()
411 else:
412 ob.scale = active.matrix_world.to_scale()
413 return('INFO', "Object scale copied")
416 def obDrw(ob, active, context):
417 ob.display_type = active.display_type
418 ob.show_axis = active.show_axis
419 ob.show_bounds = active.show_bounds
420 ob.display_bounds_type = active.display_bounds_type
421 ob.show_name = active.show_name
422 ob.show_texture_space = active.show_texture_space
423 ob.show_transparent = active.show_transparent
424 ob.show_wire = active.show_wire
425 ob.show_in_front = active.show_in_front
426 ob.empty_display_type = active.empty_display_type
427 ob.empty_display_size = active.empty_display_size
430 def obOfs(ob, active, context):
431 ob.time_offset = active.time_offset
432 return('INFO', "Time offset copied")
435 def obDup(ob, active, context):
436 generic_copy(active, ob, "dupli")
437 return('INFO', "Duplication method copied")
440 def obCol(ob, active, context):
441 ob.color = active.color
444 def obLok(ob, active, context):
445 for index, state in enumerate(active.lock_location):
446 ob.lock_location[index] = state
447 for index, state in enumerate(active.lock_rotation):
448 ob.lock_rotation[index] = state
449 ob.lock_rotations_4d = active.lock_rotations_4d
450 ob.lock_rotation_w = active.lock_rotation_w
451 for index, state in enumerate(active.lock_scale):
452 ob.lock_scale[index] = state
453 return('INFO', "Transform locks copied")
456 def obCon(ob, active, context):
457 # for consistency with 2.49, delete old constraints first
458 for removeconst in ob.constraints:
459 ob.constraints.remove(removeconst)
460 for old_constraint in active.constraints.values():
461 new_constraint = ob.constraints.new(old_constraint.type)
462 generic_copy(old_constraint, new_constraint)
463 return('INFO', "Constraints copied")
466 def obTex(ob, active, context):
467 if 'texspace_location' in dir(ob.data) and 'texspace_location' in dir(
468 active.data):
469 ob.data.texspace_location[:] = active.data.texspace_location[:]
470 if 'texspace_size' in dir(ob.data) and 'texspace_size' in dir(active.data):
471 ob.data.texspace_size[:] = active.data.texspace_size[:]
472 return('INFO', "Texture space copied")
475 def obIdx(ob, active, context):
476 ob.pass_index = active.pass_index
477 return('INFO', "Pass index copied")
480 def obMod(ob, active, context):
481 for modifier in ob.modifiers:
482 # remove existing before adding new:
483 ob.modifiers.remove(modifier)
484 for old_modifier in active.modifiers.values():
485 new_modifier = ob.modifiers.new(name=old_modifier.name,
486 type=old_modifier.type)
487 generic_copy(old_modifier, new_modifier)
488 return('INFO', "Modifiers copied")
491 def obGrp(ob, active, context):
492 for grp in bpy.data.collections:
493 if active.name in grp.objects and ob.name not in grp.objects:
494 grp.objects.link(ob)
495 return('INFO', "Groups copied")
498 def obWei(ob, active, context):
499 # sanity check: are source and target both mesh objects?
500 if ob.type != 'MESH' or active.type != 'MESH':
501 return('ERROR', "objects have to be of mesh type, doing nothing")
502 me_source = active.data
503 me_target = ob.data
504 # sanity check: do source and target have the same amount of verts?
505 if len(me_source.vertices) != len(me_target.vertices):
506 return('ERROR', "objects have different vertex counts, doing nothing")
507 vgroups_IndexName = {}
508 for i in range(0, len(active.vertex_groups)):
509 groups = active.vertex_groups[i]
510 vgroups_IndexName[groups.index] = groups.name
511 data = {} # vert_indices, [(vgroup_index, weights)]
512 for v in me_source.vertices:
513 vg = v.groups
514 vi = v.index
515 if len(vg) > 0:
516 vgroup_collect = []
517 for i in range(0, len(vg)):
518 vgroup_collect.append((vg[i].group, vg[i].weight))
519 data[vi] = vgroup_collect
520 # write data to target
521 if ob != active:
522 # add missing vertex groups
523 for vgroup_name in vgroups_IndexName.values():
524 # check if group already exists...
525 already_present = 0
526 for i in range(0, len(ob.vertex_groups)):
527 if ob.vertex_groups[i].name == vgroup_name:
528 already_present = 1
529 # ... if not, then add
530 if already_present == 0:
531 ob.vertex_groups.new(name=vgroup_name)
532 # write weights
533 for v in me_target.vertices:
534 for vi_source, vgroupIndex_weight in data.items():
535 if v.index == vi_source:
537 for i in range(0, len(vgroupIndex_weight)):
538 groupName = vgroups_IndexName[vgroupIndex_weight[i][0]]
539 groups = ob.vertex_groups
540 for vgs in range(0, len(groups)):
541 if groups[vgs].name == groupName:
542 groups[vgs].add((v.index,),
543 vgroupIndex_weight[i][1], "REPLACE")
544 return('INFO', "Weights copied")
547 object_copies = (
548 # ('obj_loc', "Location",
549 # "Copy Location from Active to Selected", obLoc),
550 # ('obj_rot', "Rotation",
551 # "Copy Rotation from Active to Selected", obRot),
552 # ('obj_sca', "Scale",
553 # "Copy Scale from Active to Selected", obSca),
554 ('obj_vis_loc', "Location",
555 "Copy Location from Active to Selected", obVisLoc),
556 ('obj_vis_rot', "Rotation",
557 "Copy Rotation from Active to Selected", obVisRot),
558 ('obj_vis_sca', "Scale",
559 "Copy Scale from Active to Selected", obVisSca),
560 ('obj_drw', "Draw Options",
561 "Copy Draw Options from Active to Selected", obDrw),
562 ('obj_ofs', "Time Offset",
563 "Copy Time Offset from Active to Selected", obOfs),
564 ('obj_dup', "Dupli",
565 "Copy Dupli from Active to Selected", obDup),
566 ('obj_col', "Object Color",
567 "Copy Object Color from Active to Selected", obCol),
568 # ('obj_dmp', "Damping",
569 # "Copy Damping from Active to Selected"),
570 # ('obj_all', "All Physical Attributes",
571 # "Copy Physical Attributes from Active to Selected"),
572 # ('obj_prp', "Properties",
573 # "Copy Properties from Active to Selected"),
574 # ('obj_log', "Logic Bricks",
575 # "Copy Logic Bricks from Active to Selected"),
576 ('obj_lok', "Protected Transform",
577 "Copy Protected Transforms from Active to Selected", obLok),
578 ('obj_con', "Object Constraints",
579 "Copy Object Constraints from Active to Selected", obCon),
580 # ('obj_nla', "NLA Strips",
581 # "Copy NLA Strips from Active to Selected"),
582 # ('obj_tex', "Texture Space",
583 # "Copy Texture Space from Active to Selected", obTex),
584 # ('obj_sub', "Subdivision Surface Settings",
585 # "Copy Subdivision Surface Settings from Active to Selected"),
586 # ('obj_smo', "AutoSmooth",
587 # "Copy AutoSmooth from Active to Selected"),
588 ('obj_idx', "Pass Index",
589 "Copy Pass Index from Active to Selected", obIdx),
590 ('obj_mod', "Modifiers",
591 "Copy Modifiers from Active to Selected", obMod),
592 ('obj_wei', "Vertex Weights",
593 "Copy vertex weights based on indices", obWei),
594 ('obj_grp', "Group Links",
595 "Copy selected into active object's groups", obGrp)
599 @classmethod
600 def object_poll_func(cls, context):
601 return (len(context.selected_objects) > 1)
604 def object_invoke_func(self, context, event):
605 wm = context.window_manager
606 wm.invoke_props_dialog(self)
607 return {'RUNNING_MODAL'}
610 class CopySelectedObjectConstraints(Operator):
611 """Copy Chosen constraints from active to selected"""
612 bl_idname = "object.copy_selected_constraints"
613 bl_label = "Copy Selected Constraints"
615 selection: BoolVectorProperty(
616 size=32,
617 options={'SKIP_SAVE'}
620 poll = object_poll_func
621 invoke = object_invoke_func
623 def draw(self, context):
624 layout = self.layout
625 for idx, const in enumerate(context.active_object.constraints):
626 layout.prop(self, "selection", index=idx, text=const.name,
627 toggle=True)
629 def execute(self, context):
630 active = context.active_object
631 selected = context.selected_objects[:]
632 selected.remove(active)
633 for obj in selected:
634 for index, flag in enumerate(self.selection):
635 if flag:
636 obj.constraints.copy(active.constraints[index])
637 return{'FINISHED'}
640 class CopySelectedObjectModifiers(Operator):
641 """Copy Chosen modifiers from active to selected"""
642 bl_idname = "object.copy_selected_modifiers"
643 bl_label = "Copy Selected Modifiers"
645 selection: BoolVectorProperty(
646 size=32,
647 options={'SKIP_SAVE'}
650 poll = object_poll_func
651 invoke = object_invoke_func
653 def draw(self, context):
654 layout = self.layout
655 for idx, const in enumerate(context.active_object.modifiers):
656 layout.prop(self, 'selection', index=idx, text=const.name,
657 toggle=True)
659 def execute(self, context):
660 active = context.active_object
661 selected = context.selected_objects[:]
662 selected.remove(active)
663 for obj in selected:
664 for index, flag in enumerate(self.selection):
665 if flag:
666 old_modifier = active.modifiers[index]
667 new_modifier = obj.modifiers.new(
668 type=active.modifiers[index].type,
669 name=active.modifiers[index].name
671 generic_copy(old_modifier, new_modifier)
672 return{'FINISHED'}
675 class CopySelectedObjectCustomProperties(CopyCustomProperties, Operator):
676 """Copy Chosen custom properties from active to selected objects"""
677 bl_idname = "object.copy_selected_custom_props"
678 bl_label = "Copy Selected Custom Properties"
679 bl_options = {'REGISTER', 'UNDO'}
681 poll = object_poll_func
682 invoke = object_invoke_func
684 def draw(self, context):
685 self.draw_bools(context.object.keys())
687 def execute(self, context):
688 self.copy_selected_custom_props(context.object, context.selected_objects)
689 return {'FINISHED'}
691 object_ops = []
692 genops(object_copies, object_ops, "object.copy_", object_poll_func, obLoopExec)
695 class VIEW3D_MT_copypopup(Menu):
696 bl_label = "Copy Attributes"
698 def draw(self, context):
699 layout = self.layout
701 layout.operator_context = 'INVOKE_REGION_WIN'
702 layout.operator("view3d.copybuffer", icon="COPY_ID")
704 if (len(context.selected_objects) <= 1):
705 layout.separator()
706 layout.label(text="Please select at least two objects", icon="INFO")
707 layout.separator()
709 for entry, op in enumerate(object_copies):
710 if entry and entry % 4 == 0:
711 layout.separator()
712 layout.operator("object.copy_" + op[0])
713 layout.operator("object.copy_selected_constraints")
714 layout.operator("object.copy_selected_modifiers")
715 layout.operator("object.copy_selected_custom_props")
718 # Begin Mesh copy settings:
720 class MESH_MT_CopyFaceSettings(Menu):
721 bl_label = "Copy Face Settings"
723 @classmethod
724 def poll(cls, context):
725 return context.mode == 'EDIT_MESH'
727 def draw(self, context):
728 mesh = context.object.data
729 uv = len(mesh.uv_layers) > 1
730 vc = len(mesh.vertex_colors) > 1
732 layout = self.layout
734 op = layout.operator(MESH_OT_CopyFaceSettings.bl_idname,
735 text="Copy Material")
736 op['layer'] = ''
737 op['mode'] = 'MAT'
739 if mesh.uv_layers.active:
740 op = layout.operator(MESH_OT_CopyFaceSettings.bl_idname,
741 text="Copy Active UV Coords")
742 op['layer'] = ''
743 op['mode'] = 'UV'
745 if mesh.vertex_colors.active:
746 op = layout.operator(MESH_OT_CopyFaceSettings.bl_idname,
747 text="Copy Active Vertex Colors")
748 op['layer'] = ''
749 op['mode'] = 'VCOL'
751 if uv or vc:
752 layout.separator()
753 if uv:
754 layout.menu("MESH_MT_CopyImagesFromLayer")
755 layout.menu("MESH_MT_CopyUVCoordsFromLayer")
756 if vc:
757 layout.menu("MESH_MT_CopyVertexColorsFromLayer")
760 # Data (UV map, Image and Vertex color) menus calling MESH_OT_CopyFaceSettings
761 # Explicitly defined as using the generator code was broken in case of Menus
762 # causing issues with access and registration
765 class MESH_MT_CopyUVCoordsFromLayer(Menu):
766 bl_label = "Copy Other UV Coord Layers"
768 @classmethod
769 def poll(cls, context):
770 obj = context.active_object
771 return obj and obj.mode == "EDIT_MESH" and len(
772 obj.data.uv_layers) > 1
774 def draw(self, context):
775 mesh = context.active_object.data
776 _buildmenu(self, mesh, 'UV', "GROUP_UVS")
779 class MESH_MT_CopyVertexColorsFromLayer(Menu):
780 bl_label = "Copy Other Vertex Colors Layers"
782 @classmethod
783 def poll(cls, context):
784 obj = context.active_object
785 return obj and obj.mode == "EDIT_MESH" and len(
786 obj.data.vertex_colors) > 1
788 def draw(self, context):
789 mesh = context.active_object.data
790 _buildmenu(self, mesh, 'VCOL', "GROUP_VCOL")
793 def _buildmenu(self, mesh, mode, icon):
794 layout = self.layout
795 if mode == 'VCOL':
796 layers = mesh.vertex_colors
797 else:
798 layers = mesh.uv_layers
799 for layer in layers:
800 if not layer.active:
801 op = layout.operator(MESH_OT_CopyFaceSettings.bl_idname,
802 text=layer.name, icon=icon)
803 op['layer'] = layer.name
804 op['mode'] = mode
807 class MESH_OT_CopyFaceSettings(Operator):
808 """Copy settings from active face to all selected faces"""
809 bl_idname = 'mesh.copy_face_settings'
810 bl_label = "Copy Face Settings"
811 bl_options = {'REGISTER', 'UNDO'}
813 mode: StringProperty(
814 name="Mode",
815 options={"HIDDEN"},
817 layer: StringProperty(
818 name="Layer",
819 options={"HIDDEN"},
822 @classmethod
823 def poll(cls, context):
824 return context.mode == 'EDIT_MESH'
826 def execute(self, context):
827 mode = getattr(self, 'mode', '')
828 if mode not in {'MAT', 'VCOL', 'UV'}:
829 self.report({'ERROR'}, "No mode specified or invalid mode")
830 return self._end(context, {'CANCELLED'})
831 layername = getattr(self, 'layer', '')
832 mesh = context.object.data
834 # Switching out of edit mode updates the selected state of faces and
835 # makes the data from the uv texture and vertex color layers available.
836 bpy.ops.object.editmode_toggle()
838 polys = mesh.polygons
839 if mode == 'MAT':
840 to_data = from_data = polys
841 else:
842 if mode == 'VCOL':
843 layers = mesh.vertex_colors
844 act_layer = mesh.vertex_colors.active
845 elif mode == 'UV':
846 layers = mesh.uv_layers
847 act_layer = mesh.uv_layers.active
848 if not layers or (layername and layername not in layers):
849 self.report({'ERROR'}, "Invalid UV or color layer. Operation Cancelled")
850 return self._end(context, {'CANCELLED'})
851 from_data = layers[layername or act_layer.name].data
852 to_data = act_layer.data
853 from_index = polys.active
855 for f in polys:
856 if f.select:
857 if to_data != from_data:
858 # Copying from another layer.
859 # from_face is to_face's counterpart from other layer.
860 from_index = f.index
861 elif f.index == from_index:
862 # Otherwise skip copying a face to itself.
863 continue
864 if mode == 'MAT':
865 f.material_index = polys[from_index].material_index
866 continue
867 if len(f.loop_indices) != len(polys[from_index].loop_indices):
868 self.report({'WARNING'}, "Different number of vertices.")
869 for i in range(len(f.loop_indices)):
870 to_vertex = f.loop_indices[i]
871 from_vertex = polys[from_index].loop_indices[i]
872 if mode == 'VCOL':
873 to_data[to_vertex].color = from_data[from_vertex].color
874 elif mode == 'UV':
875 to_data[to_vertex].uv = from_data[from_vertex].uv
877 return self._end(context, {'FINISHED'})
879 def _end(self, context, retval):
880 if context.mode != 'EDIT_MESH':
881 # Clean up by returning to edit mode like it was before.
882 bpy.ops.object.editmode_toggle()
883 return(retval)
886 classes = (
887 CopySelectedPoseConstraints,
888 CopySelectedBoneCustomProperties,
889 VIEW3D_MT_posecopypopup,
890 CopySelectedObjectConstraints,
891 CopySelectedObjectModifiers,
892 CopySelectedObjectCustomProperties,
893 VIEW3D_MT_copypopup,
894 MESH_MT_CopyFaceSettings,
895 MESH_MT_CopyUVCoordsFromLayer,
896 MESH_MT_CopyVertexColorsFromLayer,
897 MESH_OT_CopyFaceSettings,
898 *pose_ops,
899 *object_ops,
902 def register():
903 from bpy.utils import register_class
904 for cls in classes:
905 register_class(cls)
907 # mostly to get the keymap working
908 kc = bpy.context.window_manager.keyconfigs.addon
909 if kc:
910 km = kc.keymaps.new(name="Object Mode")
911 kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS', ctrl=True)
912 kmi.properties.name = 'VIEW3D_MT_copypopup'
914 km = kc.keymaps.new(name="Pose")
915 kmi = km.keymap_items.get("pose.copy")
916 if kmi is not None:
917 kmi.idname = 'wm.call_menu'
918 else:
919 kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS', ctrl=True)
920 kmi.properties.name = 'VIEW3D_MT_posecopypopup'
922 km = kc.keymaps.new(name="Mesh")
923 kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS')
924 kmi.ctrl = True
925 kmi.properties.name = 'MESH_MT_CopyFaceSettings'
928 def unregister():
929 # mostly to remove the keymap
930 kc = bpy.context.window_manager.keyconfigs.addon
931 if kc:
932 kms = kc.keymaps.get('Pose')
933 if kms is not None:
934 for item in kms.keymap_items:
935 if item.name == 'Call Menu' and item.idname == 'wm.call_menu' and \
936 item.properties.name == 'VIEW3D_MT_posecopypopup':
937 item.idname = 'pose.copy'
938 break
940 km = kc.keymaps.get('Mesh')
941 if km is not None:
942 for kmi in km.keymap_items:
943 if kmi.idname == 'wm.call_menu':
944 if kmi.properties.name == 'MESH_MT_CopyFaceSettings':
945 km.keymap_items.remove(kmi)
947 km = kc.keymaps.get('Object Mode')
948 if km is not None:
949 for kmi in km.keymap_items:
950 if kmi.idname == 'wm.call_menu':
951 if kmi.properties.name == 'VIEW3D_MT_copypopup':
952 km.keymap_items.remove(kmi)
954 from bpy.utils import unregister_class
955 for cls in classes:
956 unregister_class(cls)
959 if __name__ == "__main__":
960 register()