mirror of
https://github.com/blender/blender-addons.git
synced 2025-07-23 00:48:26 +00:00

- Remove time offset -- this feature was removed in Blender 2.61 - Cleanup Code - Rename groups to collections - Rename dupli to instancing - Remove BGE feature - Remove non existent `MESH_MT_CopyImagesFromLayer` - Update operator labels Addresses parts of T100240
946 lines
32 KiB
Python
946 lines
32 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
bl_info = {
|
|
"name": "Copy Attributes Menu",
|
|
"author": "Bassam Kurdali, Fabian Fricke, Adam Wiseman, Demeter Dzadik",
|
|
"version": (0, 6, 0),
|
|
"blender": (3, 4, 0),
|
|
"location": "View3D > Ctrl-C",
|
|
"description": "Copy Attributes Menu",
|
|
"doc_url": "{BLENDER_MANUAL_URL}/addons/interface/copy_attributes.html",
|
|
"category": "Interface",
|
|
}
|
|
|
|
import bpy
|
|
from mathutils import Matrix
|
|
from bpy.types import (
|
|
Operator,
|
|
Menu,
|
|
)
|
|
from bpy.props import (
|
|
BoolVectorProperty,
|
|
StringProperty,
|
|
)
|
|
|
|
# First part of the operator Info message
|
|
INFO_MESSAGE = "Copy Attributes: "
|
|
|
|
|
|
def build_exec(loopfunc, func):
|
|
"""Generator function that returns exec functions for operators """
|
|
|
|
def exec_func(self, context):
|
|
loopfunc(self, context, func)
|
|
return {'FINISHED'}
|
|
return exec_func
|
|
|
|
|
|
def build_invoke(loopfunc, func):
|
|
"""Generator function that returns invoke functions for operators"""
|
|
|
|
def invoke_func(self, context, event):
|
|
loopfunc(self, context, func)
|
|
return {'FINISHED'}
|
|
return invoke_func
|
|
|
|
|
|
def build_op(idname, label, description, fpoll, fexec, finvoke):
|
|
"""Generator function that returns the basic operator"""
|
|
|
|
class myopic(Operator):
|
|
bl_idname = idname
|
|
bl_label = label
|
|
bl_description = description
|
|
execute = fexec
|
|
poll = fpoll
|
|
invoke = finvoke
|
|
return myopic
|
|
|
|
|
|
def genops(copylist, oplist, prefix, poll_func, loopfunc):
|
|
"""Generate ops from the copy list and its associated functions"""
|
|
for op in copylist:
|
|
exec_func = build_exec(loopfunc, op[3])
|
|
invoke_func = build_invoke(loopfunc, op[3])
|
|
opclass = build_op(prefix + op[0], "Copy " + op[1], op[2],
|
|
poll_func, exec_func, invoke_func)
|
|
oplist.append(opclass)
|
|
|
|
|
|
def generic_copy(source, target, string=""):
|
|
"""Copy attributes from source to target that have string in them"""
|
|
for attr in dir(source):
|
|
if attr.find(string) > -1:
|
|
try:
|
|
setattr(target, attr, getattr(source, attr))
|
|
except:
|
|
pass
|
|
return
|
|
|
|
|
|
def getmat(bone, active, context, ignoreparent):
|
|
"""Helper function for visual transform copy,
|
|
gets the active transform in bone space
|
|
"""
|
|
obj_bone = bone.id_data
|
|
obj_active = active.id_data
|
|
data_bone = obj_bone.data.bones[bone.name]
|
|
# all matrices are in armature space unless commented otherwise
|
|
active_to_selected = obj_bone.matrix_world.inverted() @ obj_active.matrix_world
|
|
active_matrix = active_to_selected @ active.matrix
|
|
otherloc = active_matrix # final 4x4 mat of target, location.
|
|
bonemat_local = data_bone.matrix_local.copy() # self rest matrix
|
|
if data_bone.parent:
|
|
parentposemat = obj_bone.pose.bones[data_bone.parent.name].matrix.copy()
|
|
parentbonemat = data_bone.parent.matrix_local.copy()
|
|
else:
|
|
parentposemat = parentbonemat = Matrix()
|
|
if parentbonemat == parentposemat or ignoreparent:
|
|
newmat = bonemat_local.inverted() @ otherloc
|
|
else:
|
|
bonemat = parentbonemat.inverted() @ bonemat_local
|
|
|
|
newmat = bonemat.inverted() @ parentposemat.inverted() @ otherloc
|
|
return newmat
|
|
|
|
|
|
def rotcopy(item, mat):
|
|
"""Copy rotation to item from matrix mat depending on item.rotation_mode"""
|
|
if item.rotation_mode == 'QUATERNION':
|
|
item.rotation_quaternion = mat.to_3x3().to_quaternion()
|
|
elif item.rotation_mode == 'AXIS_ANGLE':
|
|
rot = mat.to_3x3().to_quaternion().to_axis_angle() # returns (Vector((x, y, z)), w)
|
|
axis_angle = rot[1], rot[0][0], rot[0][1], rot[0][2] # convert to w, x, y, z
|
|
item.rotation_axis_angle = axis_angle
|
|
else:
|
|
item.rotation_euler = mat.to_3x3().to_euler(item.rotation_mode)
|
|
|
|
|
|
def pLoopExec(self, context, funk):
|
|
"""Loop over selected bones and execute funk on them"""
|
|
active = context.active_pose_bone
|
|
selected = context.selected_pose_bones
|
|
selected.remove(active)
|
|
for bone in selected:
|
|
funk(bone, active, context)
|
|
|
|
|
|
# The following functions are used to copy attributes from active to bone
|
|
|
|
def pLocLocExec(bone, active, context):
|
|
bone.location = active.location
|
|
|
|
|
|
def pLocRotExec(bone, active, context):
|
|
rotcopy(bone, active.matrix_basis.to_3x3())
|
|
|
|
|
|
def pLocScaExec(bone, active, context):
|
|
bone.scale = active.scale
|
|
|
|
|
|
def pVisLocExec(bone, active, context):
|
|
bone.location = getmat(bone, active, context, False).to_translation()
|
|
|
|
|
|
def pVisRotExec(bone, active, context):
|
|
obj_bone = bone.id_data
|
|
rotcopy(bone, getmat(bone, active,
|
|
context, not obj_bone.data.bones[bone.name].use_inherit_rotation))
|
|
|
|
|
|
def pVisScaExec(bone, active, context):
|
|
obj_bone = bone.id_data
|
|
bone.scale = getmat(bone, active, context,
|
|
not obj_bone.data.bones[bone.name].use_inherit_scale)\
|
|
.to_scale()
|
|
|
|
|
|
def pDrwExec(bone, active, context):
|
|
bone.custom_shape = active.custom_shape
|
|
bone.use_custom_shape_bone_size = active.use_custom_shape_bone_size
|
|
bone.custom_shape_translation = active.custom_shape_translation
|
|
bone.custom_shape_rotation_euler = active.custom_shape_rotation_euler
|
|
bone.custom_shape_scale_xyz = active.custom_shape_scale_xyz
|
|
bone.bone.show_wire = active.bone.show_wire
|
|
|
|
|
|
def pLokExec(bone, active, context):
|
|
for index, state in enumerate(active.lock_location):
|
|
bone.lock_location[index] = state
|
|
for index, state in enumerate(active.lock_rotation):
|
|
bone.lock_rotation[index] = state
|
|
bone.lock_rotations_4d = active.lock_rotations_4d
|
|
bone.lock_rotation_w = active.lock_rotation_w
|
|
for index, state in enumerate(active.lock_scale):
|
|
bone.lock_scale[index] = state
|
|
|
|
|
|
def pConExec(bone, active, context):
|
|
for old_constraint in active.constraints.values():
|
|
new_constraint = bone.constraints.new(old_constraint.type)
|
|
generic_copy(old_constraint, new_constraint)
|
|
|
|
|
|
def pIKsExec(bone, active, context):
|
|
generic_copy(active, bone, "ik_")
|
|
|
|
|
|
def pBBonesExec(bone, active, context):
|
|
object = active.id_data
|
|
generic_copy(
|
|
object.data.bones[active.name],
|
|
object.data.bones[bone.name],
|
|
"bbone_")
|
|
|
|
|
|
pose_copies = (
|
|
('pose_loc_loc', "Local Location",
|
|
"Copy Location from Active to Selected", pLocLocExec),
|
|
('pose_loc_rot', "Local Rotation",
|
|
"Copy Rotation from Active to Selected", pLocRotExec),
|
|
('pose_loc_sca', "Local Scale",
|
|
"Copy Scale from Active to Selected", pLocScaExec),
|
|
('pose_vis_loc', "Visual Location",
|
|
"Copy Location from Active to Selected", pVisLocExec),
|
|
('pose_vis_rot', "Visual Rotation",
|
|
"Copy Rotation from Active to Selected", pVisRotExec),
|
|
('pose_vis_sca', "Visual Scale",
|
|
"Copy Scale from Active to Selected", pVisScaExec),
|
|
('pose_drw', "Bone Shape",
|
|
"Copy Bone Shape from Active to Selected", pDrwExec),
|
|
('pose_lok', "Protected Transform",
|
|
"Copy Protected Transforms from Active to Selected", pLokExec),
|
|
('pose_con', "Bone Constraints",
|
|
"Copy Object Constraints from Active to Selected", pConExec),
|
|
('pose_iks', "IK Limits",
|
|
"Copy IK Limits from Active to Selected", pIKsExec),
|
|
('bbone_settings', "BBone Settings",
|
|
"Copy BBone Settings from Active to Selected", pBBonesExec),
|
|
)
|
|
|
|
@classmethod
|
|
def pose_poll_func(cls, context):
|
|
return(context.mode == 'POSE')
|
|
|
|
|
|
def pose_invoke_func(self, context, event):
|
|
wm = context.window_manager
|
|
wm.invoke_props_dialog(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
|
CustomPropSelectionBoolsProperty = BoolVectorProperty(
|
|
size=32,
|
|
options={'SKIP_SAVE'}
|
|
)
|
|
|
|
class CopySelection:
|
|
"""Base class for copying properties from active to selected based on a selection."""
|
|
|
|
selection: CustomPropSelectionBoolsProperty
|
|
|
|
def draw_bools(self, button_names):
|
|
"""Draws the boolean toggle list with a list of strings for the button texts."""
|
|
layout = self.layout
|
|
for idx, name in enumerate(button_names):
|
|
layout.prop(self, "selection", index=idx, text=name,
|
|
toggle=True)
|
|
|
|
def copy_custom_property(source, destination, prop_name):
|
|
"""Copy a custom property called prop_name, from source to destination.
|
|
source and destination must be a Blender data type that can hold custom properties.
|
|
For a list of such data types, see:
|
|
https://docs.blender.org/manual/en/latest/files/data_blocks.html#files-data-blocks-custom-properties
|
|
"""
|
|
|
|
# Create the property.
|
|
destination[prop_name] = source[prop_name]
|
|
# Copy the settings of the property.
|
|
try:
|
|
dst_prop_manager = destination.id_properties_ui(prop_name)
|
|
except TypeError:
|
|
# Python values like lists or dictionaries don't have any settings to copy.
|
|
# They just consist of a value and nothing else.
|
|
return
|
|
|
|
src_prop_manager = source.id_properties_ui(prop_name)
|
|
assert src_prop_manager, f'Property "{prop_name}" not found in {source}'
|
|
|
|
dst_prop_manager.update_from(src_prop_manager)
|
|
|
|
# Copy the Library Overridable flag, which is stored elsewhere.
|
|
prop_rna_path = f'["{prop_name}"]'
|
|
is_lib_overridable = source.is_property_overridable_library(prop_rna_path)
|
|
destination.property_overridable_library_set(prop_rna_path, is_lib_overridable)
|
|
|
|
class CopyCustomProperties(CopySelection):
|
|
"""Base class for copying a selection of custom properties."""
|
|
|
|
def copy_selected_custom_props(self, active, selected):
|
|
keys = list(active.keys())
|
|
for item in selected:
|
|
if item == active:
|
|
continue
|
|
for index, is_selected in enumerate(self.selection):
|
|
if is_selected:
|
|
copy_custom_property(active, item, keys[index])
|
|
|
|
class CopySelectedBoneCustomProperties(CopyCustomProperties, Operator):
|
|
"""Copy Chosen custom properties from active to selected"""
|
|
bl_idname = "pose.copy_selected_custom_props"
|
|
bl_label = "Copy Selected Custom Properties"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
poll = pose_poll_func
|
|
invoke = pose_invoke_func
|
|
|
|
def draw(self, context):
|
|
self.draw_bools(context.active_pose_bone.keys())
|
|
|
|
def execute(self, context):
|
|
self.copy_selected_custom_props(context.active_pose_bone, context.selected_pose_bones)
|
|
return {'FINISHED'}
|
|
|
|
|
|
class CopySelectedPoseConstraints(Operator):
|
|
"""Copy Chosen constraints from active to selected"""
|
|
bl_idname = "pose.copy_selected_constraints"
|
|
bl_label = "Copy Selected Constraints"
|
|
|
|
selection: BoolVectorProperty(
|
|
size=32,
|
|
options={'SKIP_SAVE'}
|
|
)
|
|
|
|
poll = pose_poll_func
|
|
invoke = pose_invoke_func
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
for idx, const in enumerate(context.active_pose_bone.constraints):
|
|
layout.prop(self, "selection", index=idx, text=const.name,
|
|
toggle=True)
|
|
|
|
def execute(self, context):
|
|
active = context.active_pose_bone
|
|
selected = context.selected_pose_bones[:]
|
|
selected.remove(active)
|
|
for bone in selected:
|
|
for index, flag in enumerate(self.selection):
|
|
if flag:
|
|
bone.constraints.copy(active.constraints[index])
|
|
return {'FINISHED'}
|
|
|
|
|
|
pose_ops = [] # list of pose mode copy operators
|
|
genops(pose_copies, pose_ops, "pose.copy_", pose_poll_func, pLoopExec)
|
|
|
|
|
|
class VIEW3D_MT_posecopypopup(Menu):
|
|
bl_label = "Copy Attributes"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.operator_context = 'INVOKE_REGION_WIN'
|
|
for op in pose_copies:
|
|
layout.operator("pose.copy_" + op[0])
|
|
layout.operator("pose.copy_selected_constraints")
|
|
layout.operator("pose.copy_selected_custom_props")
|
|
layout.operator("pose.copy", text="copy pose")
|
|
|
|
|
|
def obLoopExec(self, context, funk):
|
|
"""Loop over selected objects and execute funk on them"""
|
|
active = context.active_object
|
|
selected = context.selected_objects[:]
|
|
selected.remove(active)
|
|
for obj in selected:
|
|
msg = funk(obj, active, context)
|
|
if msg:
|
|
self.report({msg[0]}, INFO_MESSAGE + msg[1])
|
|
|
|
|
|
def world_to_basis(active, ob, context):
|
|
"""put world coords of active as basis coords of ob"""
|
|
local = ob.parent.matrix_world.inverted() @ active.matrix_world
|
|
P = ob.matrix_basis @ ob.matrix_local.inverted()
|
|
mat = P @ local
|
|
return(mat)
|
|
|
|
|
|
# The following functions are used to copy attributes from
|
|
# active to selected object
|
|
|
|
def obLoc(ob, active, context):
|
|
ob.location = active.location
|
|
|
|
|
|
def obRot(ob, active, context):
|
|
rotcopy(ob, active.matrix_local.to_3x3())
|
|
|
|
|
|
def obSca(ob, active, context):
|
|
ob.scale = active.scale
|
|
|
|
|
|
def obVisLoc(ob, active, context):
|
|
if ob.parent:
|
|
mat = world_to_basis(active, ob, context)
|
|
ob.location = mat.to_translation()
|
|
else:
|
|
ob.location = active.matrix_world.to_translation()
|
|
return('INFO', "Object location copied")
|
|
|
|
|
|
def obVisRot(ob, active, context):
|
|
if ob.parent:
|
|
mat = world_to_basis(active, ob, context)
|
|
rotcopy(ob, mat.to_3x3())
|
|
else:
|
|
rotcopy(ob, active.matrix_world.to_3x3())
|
|
return('INFO', "Object rotation copied")
|
|
|
|
|
|
def obVisSca(ob, active, context):
|
|
if ob.parent:
|
|
mat = world_to_basis(active, ob, context)
|
|
ob.scale = mat.to_scale()
|
|
else:
|
|
ob.scale = active.matrix_world.to_scale()
|
|
return('INFO', "Object scale copied")
|
|
|
|
|
|
def obDrw(ob, active, context):
|
|
ob.display_type = active.display_type
|
|
ob.show_axis = active.show_axis
|
|
ob.show_bounds = active.show_bounds
|
|
ob.display_bounds_type = active.display_bounds_type
|
|
ob.show_name = active.show_name
|
|
ob.show_texture_space = active.show_texture_space
|
|
ob.show_transparent = active.show_transparent
|
|
ob.show_wire = active.show_wire
|
|
ob.show_in_front = active.show_in_front
|
|
ob.empty_display_type = active.empty_display_type
|
|
ob.empty_display_size = active.empty_display_size
|
|
|
|
|
|
def obDup(ob, active, context):
|
|
generic_copy(active, ob, "instance_type")
|
|
return('INFO', "Duplication method copied")
|
|
|
|
|
|
def obCol(ob, active, context):
|
|
ob.color = active.color
|
|
|
|
|
|
def obLok(ob, active, context):
|
|
for index, state in enumerate(active.lock_location):
|
|
ob.lock_location[index] = state
|
|
for index, state in enumerate(active.lock_rotation):
|
|
ob.lock_rotation[index] = state
|
|
ob.lock_rotations_4d = active.lock_rotations_4d
|
|
ob.lock_rotation_w = active.lock_rotation_w
|
|
for index, state in enumerate(active.lock_scale):
|
|
ob.lock_scale[index] = state
|
|
return('INFO', "Transform locks copied")
|
|
|
|
|
|
def obCon(ob, active, context):
|
|
# for consistency with 2.49, delete old constraints first
|
|
for removeconst in ob.constraints:
|
|
ob.constraints.remove(removeconst)
|
|
for old_constraint in active.constraints.values():
|
|
new_constraint = ob.constraints.new(old_constraint.type)
|
|
generic_copy(old_constraint, new_constraint)
|
|
return('INFO', "Constraints copied")
|
|
|
|
|
|
def obTex(ob, active, context):
|
|
if 'texspace_location' in dir(ob.data) and 'texspace_location' in dir(
|
|
active.data):
|
|
ob.data.texspace_location[:] = active.data.texspace_location[:]
|
|
if 'texspace_size' in dir(ob.data) and 'texspace_size' in dir(active.data):
|
|
ob.data.texspace_size[:] = active.data.texspace_size[:]
|
|
return('INFO', "Texture space copied")
|
|
|
|
|
|
def obIdx(ob, active, context):
|
|
ob.pass_index = active.pass_index
|
|
return('INFO', "Pass index copied")
|
|
|
|
|
|
def obMod(ob, active, context):
|
|
for modifier in ob.modifiers:
|
|
# remove existing before adding new:
|
|
ob.modifiers.remove(modifier)
|
|
for old_modifier in active.modifiers.values():
|
|
new_modifier = ob.modifiers.new(name=old_modifier.name,
|
|
type=old_modifier.type)
|
|
generic_copy(old_modifier, new_modifier)
|
|
return('INFO', "Modifiers copied")
|
|
|
|
|
|
def obCollections(ob, active, context):
|
|
for collection in bpy.data.collections:
|
|
if active.name in collection.objects and ob.name not in collection.objects:
|
|
collection.objects.link(ob)
|
|
return('INFO', "Collections copied")
|
|
|
|
|
|
def obWei(ob, active, context):
|
|
# sanity check: are source and target both mesh objects?
|
|
if ob.type != 'MESH' or active.type != 'MESH':
|
|
return('ERROR', "objects have to be of mesh type, doing nothing")
|
|
me_source = active.data
|
|
me_target = ob.data
|
|
# sanity check: do source and target have the same amount of verts?
|
|
if len(me_source.vertices) != len(me_target.vertices):
|
|
return('ERROR', "objects have different vertex counts, doing nothing")
|
|
vgroups_IndexName = {}
|
|
for i in range(0, len(active.vertex_groups)):
|
|
groups = active.vertex_groups[i]
|
|
vgroups_IndexName[groups.index] = groups.name
|
|
data = {} # vert_indices, [(vgroup_index, weights)]
|
|
for v in me_source.vertices:
|
|
vg = v.groups
|
|
vi = v.index
|
|
if len(vg) > 0:
|
|
vgroup_collect = []
|
|
for i in range(0, len(vg)):
|
|
vgroup_collect.append((vg[i].group, vg[i].weight))
|
|
data[vi] = vgroup_collect
|
|
# write data to target
|
|
if ob != active:
|
|
# add missing vertex groups
|
|
for vgroup_name in vgroups_IndexName.values():
|
|
# check if group already exists...
|
|
already_present = 0
|
|
for i in range(0, len(ob.vertex_groups)):
|
|
if ob.vertex_groups[i].name == vgroup_name:
|
|
already_present = 1
|
|
# ... if not, then add
|
|
if already_present == 0:
|
|
ob.vertex_groups.new(name=vgroup_name)
|
|
# write weights
|
|
for v in me_target.vertices:
|
|
for vi_source, vgroupIndex_weight in data.items():
|
|
if v.index == vi_source:
|
|
|
|
for i in range(0, len(vgroupIndex_weight)):
|
|
groupName = vgroups_IndexName[vgroupIndex_weight[i][0]]
|
|
groups = ob.vertex_groups
|
|
for vgs in range(0, len(groups)):
|
|
if groups[vgs].name == groupName:
|
|
groups[vgs].add((v.index,),
|
|
vgroupIndex_weight[i][1], "REPLACE")
|
|
return('INFO', "Weights copied")
|
|
|
|
|
|
object_copies = (
|
|
# ('obj_loc', "Location",
|
|
# "Copy Location from Active to Selected", obLoc),
|
|
# ('obj_rot', "Rotation",
|
|
# "Copy Rotation from Active to Selected", obRot),
|
|
# ('obj_sca', "Scale",
|
|
# "Copy Scale from Active to Selected", obSca),
|
|
('obj_vis_loc', "Location",
|
|
"Copy Location from Active to Selected", obVisLoc),
|
|
('obj_vis_rot', "Rotation",
|
|
"Copy Rotation from Active to Selected", obVisRot),
|
|
('obj_vis_sca', "Scale",
|
|
"Copy Scale from Active to Selected", obVisSca),
|
|
('obj_drw', "Draw Options",
|
|
"Copy Draw Options from Active to Selected", obDrw),
|
|
('obj_dup', "Instancing",
|
|
"Copy instancing properties from Active to Selected", obDup),
|
|
('obj_col', "Object Color",
|
|
"Copy Object Color from Active to Selected", obCol),
|
|
# ('obj_dmp', "Damping",
|
|
# "Copy Damping from Active to Selected"),
|
|
# ('obj_all', "All Physical Attributes",
|
|
# "Copy Physical Attributes from Active to Selected"),
|
|
# ('obj_prp', "Properties",
|
|
# "Copy Properties from Active to Selected"),
|
|
('obj_lok', "Protected Transform",
|
|
"Copy Protected Transforms from Active to Selected", obLok),
|
|
('obj_con', "Object Constraints",
|
|
"Copy Object Constraints from Active to Selected", obCon),
|
|
# ('obj_nla', "NLA Strips",
|
|
# "Copy NLA Strips from Active to Selected"),
|
|
# ('obj_tex', "Texture Space",
|
|
# "Copy Texture Space from Active to Selected", obTex),
|
|
# ('obj_sub', "Subdivision Surface Settings",
|
|
# "Copy Subdivision Surface Settings from Active to Selected"),
|
|
# ('obj_smo', "AutoSmooth",
|
|
# "Copy AutoSmooth from Active to Selected"),
|
|
('obj_idx', "Pass Index",
|
|
"Copy Pass Index from Active to Selected", obIdx),
|
|
('obj_mod', "Modifiers",
|
|
"Copy Modifiers from Active to Selected", obMod),
|
|
('obj_wei', "Vertex Weights",
|
|
"Copy vertex weights based on indices", obWei),
|
|
('obj_grp', "Collection Links",
|
|
"Copy selected into active object's collection", obCollections)
|
|
)
|
|
|
|
|
|
@classmethod
|
|
def object_poll_func(cls, context):
|
|
return (len(context.selected_objects) > 1)
|
|
|
|
|
|
def object_invoke_func(self, context, event):
|
|
wm = context.window_manager
|
|
wm.invoke_props_dialog(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
|
class CopySelectedObjectConstraints(Operator):
|
|
"""Copy Chosen constraints from active to selected"""
|
|
bl_idname = "object.copy_selected_constraints"
|
|
bl_label = "Copy Selected Constraints"
|
|
|
|
selection: BoolVectorProperty(
|
|
size=32,
|
|
options={'SKIP_SAVE'}
|
|
)
|
|
|
|
poll = object_poll_func
|
|
invoke = object_invoke_func
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
for idx, const in enumerate(context.active_object.constraints):
|
|
layout.prop(self, "selection", index=idx, text=const.name,
|
|
toggle=True)
|
|
|
|
def execute(self, context):
|
|
active = context.active_object
|
|
selected = context.selected_objects[:]
|
|
selected.remove(active)
|
|
for obj in selected:
|
|
for index, flag in enumerate(self.selection):
|
|
if flag:
|
|
obj.constraints.copy(active.constraints[index])
|
|
return{'FINISHED'}
|
|
|
|
|
|
class CopySelectedObjectModifiers(Operator):
|
|
"""Copy Chosen modifiers from active to selected"""
|
|
bl_idname = "object.copy_selected_modifiers"
|
|
bl_label = "Copy Selected Modifiers"
|
|
|
|
selection: BoolVectorProperty(
|
|
size=32,
|
|
options={'SKIP_SAVE'}
|
|
)
|
|
|
|
poll = object_poll_func
|
|
invoke = object_invoke_func
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
for idx, const in enumerate(context.active_object.modifiers):
|
|
layout.prop(self, 'selection', index=idx, text=const.name,
|
|
toggle=True)
|
|
|
|
def execute(self, context):
|
|
active = context.active_object
|
|
selected = context.selected_objects[:]
|
|
selected.remove(active)
|
|
for obj in selected:
|
|
for index, flag in enumerate(self.selection):
|
|
if flag:
|
|
old_modifier = active.modifiers[index]
|
|
new_modifier = obj.modifiers.new(
|
|
type=active.modifiers[index].type,
|
|
name=active.modifiers[index].name
|
|
)
|
|
generic_copy(old_modifier, new_modifier)
|
|
return{'FINISHED'}
|
|
|
|
|
|
class CopySelectedObjectCustomProperties(CopyCustomProperties, Operator):
|
|
"""Copy Chosen custom properties from active to selected objects"""
|
|
bl_idname = "object.copy_selected_custom_props"
|
|
bl_label = "Copy Selected Custom Properties"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
poll = object_poll_func
|
|
invoke = object_invoke_func
|
|
|
|
def draw(self, context):
|
|
self.draw_bools(context.object.keys())
|
|
|
|
def execute(self, context):
|
|
self.copy_selected_custom_props(context.object, context.selected_objects)
|
|
return {'FINISHED'}
|
|
|
|
object_ops = []
|
|
genops(object_copies, object_ops, "object.copy_", object_poll_func, obLoopExec)
|
|
|
|
|
|
class VIEW3D_MT_copypopup(Menu):
|
|
bl_label = "Copy Attributes"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.operator_context = 'INVOKE_REGION_WIN'
|
|
layout.operator("view3d.copybuffer", icon="COPY_ID")
|
|
|
|
if (len(context.selected_objects) <= 1):
|
|
layout.separator()
|
|
layout.label(text="Please select at least two objects", icon="INFO")
|
|
layout.separator()
|
|
|
|
for entry, op in enumerate(object_copies):
|
|
if entry and entry % 4 == 0:
|
|
layout.separator()
|
|
layout.operator("object.copy_" + op[0])
|
|
layout.operator("object.copy_selected_constraints")
|
|
layout.operator("object.copy_selected_modifiers")
|
|
layout.operator("object.copy_selected_custom_props")
|
|
|
|
|
|
# Begin Mesh copy settings:
|
|
|
|
class MESH_MT_CopyFaceSettings(Menu):
|
|
bl_label = "Copy Face Settings"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.mode == 'EDIT_MESH'
|
|
|
|
def draw(self, context):
|
|
mesh = context.object.data
|
|
uv = len(mesh.uv_layers) > 1
|
|
vc = len(mesh.vertex_colors) > 1
|
|
|
|
layout = self.layout
|
|
|
|
op = layout.operator(mesh.copy_face_settings, text="Copy Material")
|
|
op['layer'] = ''
|
|
op['mode'] = 'MAT'
|
|
|
|
if mesh.uv_layers.active:
|
|
op = layout.operator(mesh.copy_face_settings, text="Copy Active UV Coords")
|
|
op['layer'] = ''
|
|
op['mode'] = 'UV'
|
|
|
|
if mesh.vertex_colors.active:
|
|
op = layout.operator(mesh.copy_face_settings, text="Copy Active Vertex Colors")
|
|
op['layer'] = ''
|
|
op['mode'] = 'VCOL'
|
|
|
|
if uv or vc:
|
|
layout.separator()
|
|
if uv:
|
|
layout.menu("MESH_MT_CopyUVCoordsFromLayer")
|
|
if vc:
|
|
layout.menu("MESH_MT_CopyVertexColorsFromLayer")
|
|
|
|
|
|
# Data (UV map, Image and Vertex color) menus calling MESH_OT_CopyFaceSettings
|
|
# Explicitly defined as using the generator code was broken in case of Menus
|
|
# causing issues with access and registration
|
|
|
|
|
|
class MESH_MT_CopyUVCoordsFromLayer(Menu):
|
|
bl_label = "Copy UV Coordinates from Layer"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
obj = context.active_object
|
|
return obj and obj.mode == "EDIT_MESH" and len(
|
|
obj.data.uv_layers) > 1
|
|
|
|
def draw(self, context):
|
|
mesh = context.active_object.data
|
|
_buildmenu(self, mesh, 'UV', "GROUP_UVS")
|
|
|
|
|
|
class MESH_MT_CopyVertexColorsFromLayer(Menu):
|
|
bl_label = "Copy Vertex Colors from Layer"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
obj = context.active_object
|
|
return obj and obj.mode == "EDIT_MESH" and len(
|
|
obj.data.vertex_colors) > 1
|
|
|
|
def draw(self, context):
|
|
mesh = context.active_object.data
|
|
_buildmenu(self, mesh, 'VCOL', "GROUP_VCOL")
|
|
|
|
|
|
def _buildmenu(self, mesh, mode, icon):
|
|
layout = self.layout
|
|
if mode == 'VCOL':
|
|
layers = mesh.vertex_colors
|
|
else:
|
|
layers = mesh.uv_layers
|
|
for layer in layers:
|
|
if not layer.active:
|
|
op = layout.operator(mesh.copy_face_settings,
|
|
text=layer.name, icon=icon)
|
|
op['layer'] = layer.name
|
|
op['mode'] = mode
|
|
|
|
|
|
class MESH_OT_CopyFaceSettings(Operator):
|
|
"""Copy settings from active face to all selected faces"""
|
|
bl_idname = 'mesh.copy_face_settings'
|
|
bl_label = "Copy Face Settings"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
mode: StringProperty(
|
|
name="Mode",
|
|
options={"HIDDEN"},
|
|
)
|
|
layer: StringProperty(
|
|
name="Layer",
|
|
options={"HIDDEN"},
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.mode == 'EDIT_MESH'
|
|
|
|
def execute(self, context):
|
|
mode = getattr(self, 'mode', '')
|
|
if mode not in {'MAT', 'VCOL', 'UV'}:
|
|
self.report({'ERROR'}, "No mode specified or invalid mode")
|
|
return self._end(context, {'CANCELLED'})
|
|
layername = getattr(self, 'layer', '')
|
|
mesh = context.object.data
|
|
|
|
# Switching out of edit mode updates the selected state of faces and
|
|
# makes the data from the uv texture and vertex color layers available.
|
|
bpy.ops.object.editmode_toggle()
|
|
|
|
polys = mesh.polygons
|
|
if mode == 'MAT':
|
|
to_data = from_data = polys
|
|
else:
|
|
if mode == 'VCOL':
|
|
layers = mesh.vertex_colors
|
|
act_layer = mesh.vertex_colors.active
|
|
elif mode == 'UV':
|
|
layers = mesh.uv_layers
|
|
act_layer = mesh.uv_layers.active
|
|
if not layers or (layername and layername not in layers):
|
|
self.report({'ERROR'}, "Invalid UV or color layer. Operation Cancelled")
|
|
return self._end(context, {'CANCELLED'})
|
|
from_data = layers[layername or act_layer.name].data
|
|
to_data = act_layer.data
|
|
from_index = polys.active
|
|
|
|
for f in polys:
|
|
if f.select:
|
|
if to_data != from_data:
|
|
# Copying from another layer.
|
|
# from_face is to_face's counterpart from other layer.
|
|
from_index = f.index
|
|
elif f.index == from_index:
|
|
# Otherwise skip copying a face to itself.
|
|
continue
|
|
if mode == 'MAT':
|
|
f.material_index = polys[from_index].material_index
|
|
continue
|
|
if len(f.loop_indices) != len(polys[from_index].loop_indices):
|
|
self.report({'WARNING'}, "Different number of vertices.")
|
|
for i in range(len(f.loop_indices)):
|
|
to_vertex = f.loop_indices[i]
|
|
from_vertex = polys[from_index].loop_indices[i]
|
|
if mode == 'VCOL':
|
|
to_data[to_vertex].color = from_data[from_vertex].color
|
|
elif mode == 'UV':
|
|
to_data[to_vertex].uv = from_data[from_vertex].uv
|
|
|
|
return self._end(context, {'FINISHED'})
|
|
|
|
def _end(self, context, retval):
|
|
if context.mode != 'EDIT_MESH':
|
|
# Clean up by returning to edit mode like it was before.
|
|
bpy.ops.object.editmode_toggle()
|
|
return(retval)
|
|
|
|
|
|
classes = (
|
|
CopySelectedPoseConstraints,
|
|
CopySelectedBoneCustomProperties,
|
|
VIEW3D_MT_posecopypopup,
|
|
CopySelectedObjectConstraints,
|
|
CopySelectedObjectModifiers,
|
|
CopySelectedObjectCustomProperties,
|
|
VIEW3D_MT_copypopup,
|
|
MESH_MT_CopyFaceSettings,
|
|
MESH_MT_CopyUVCoordsFromLayer,
|
|
MESH_MT_CopyVertexColorsFromLayer,
|
|
MESH_OT_CopyFaceSettings,
|
|
*pose_ops,
|
|
*object_ops,
|
|
)
|
|
|
|
def register():
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|
|
|
|
# mostly to get the keymap working
|
|
kc = bpy.context.window_manager.keyconfigs.addon
|
|
if kc:
|
|
km = kc.keymaps.new(name="Object Mode")
|
|
kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS', ctrl=True)
|
|
kmi.properties.name = 'VIEW3D_MT_copypopup'
|
|
|
|
km = kc.keymaps.new(name="Pose")
|
|
kmi = km.keymap_items.get("pose.copy")
|
|
if kmi is not None:
|
|
kmi.idname = 'wm.call_menu'
|
|
else:
|
|
kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS', ctrl=True)
|
|
kmi.properties.name = 'VIEW3D_MT_posecopypopup'
|
|
|
|
km = kc.keymaps.new(name="Mesh")
|
|
kmi = km.keymap_items.new('wm.call_menu', 'C', 'PRESS')
|
|
kmi.ctrl = True
|
|
kmi.properties.name = 'MESH_MT_CopyFaceSettings'
|
|
|
|
|
|
def unregister():
|
|
# mostly to remove the keymap
|
|
kc = bpy.context.window_manager.keyconfigs.addon
|
|
if kc:
|
|
kms = kc.keymaps.get('Pose')
|
|
if kms is not None:
|
|
for item in kms.keymap_items:
|
|
if item.name == 'Call Menu' and item.idname == 'wm.call_menu' and \
|
|
item.properties.name == 'VIEW3D_MT_posecopypopup':
|
|
item.idname = 'pose.copy'
|
|
break
|
|
|
|
km = kc.keymaps.get('Mesh')
|
|
if km is not None:
|
|
for kmi in km.keymap_items:
|
|
if kmi.idname == 'wm.call_menu':
|
|
if kmi.properties.name == 'MESH_MT_CopyFaceSettings':
|
|
km.keymap_items.remove(kmi)
|
|
|
|
km = kc.keymaps.get('Object Mode')
|
|
if km is not None:
|
|
for kmi in km.keymap_items:
|
|
if kmi.idname == 'wm.call_menu':
|
|
if kmi.properties.name == 'VIEW3D_MT_copypopup':
|
|
km.keymap_items.remove(kmi)
|
|
|
|
from bpy.utils import unregister_class
|
|
for cls in classes:
|
|
unregister_class(cls)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|