600 lines
21 KiB
Python
600 lines
21 KiB
Python
import bpy
|
|
import mathutils
|
|
import blf
|
|
from .common import *
|
|
|
|
|
|
# Operator to create a new pose library
|
|
|
|
|
|
class DSPL_OT_CreatePoseLibrary(bpy.types.Operator):
|
|
bl_idname = "dspl.create_pose_library"
|
|
bl_label = "Create Pose Library"
|
|
bl_description = "Create Pose Library"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
arm_object = getArmatureObject(context)
|
|
arm_object.dspl.pose_library = bpy.data.actions.new(
|
|
name=arm_object.name + "_PoseLib")
|
|
arm_object.dspl.pose_library.use_fake_user = True
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Operator to convert a pose library to dspl property
|
|
|
|
|
|
class DSPL_OT_ConvertPoseLibrary(bpy.types.Operator):
|
|
bl_idname = "dspl.convert_pose_library"
|
|
bl_label = "Convert Pose Library"
|
|
bl_description = "Convert Pose Library"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
arm_object = getArmatureObject(context)
|
|
arm_object.dspl.pose_library = arm_object.animation_data.action
|
|
arm_object.animation_data.action = None
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Operator to to draw a menu for new poses
|
|
|
|
|
|
class DSPL_OT_DrawNewPoseMenu(bpy.types.Operator):
|
|
bl_idname = "dspl.draw_new_pose_menu"
|
|
bl_label = "New Pose Menu"
|
|
bl_description = "New Pose Menu"
|
|
bl_options = {'INTERNAL'}
|
|
|
|
def execute(self, context):
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_popup(self, width=200)
|
|
|
|
def draw(self, context):
|
|
dspl_create_popup_layout = self.layout
|
|
dspl_new_pose_menu = dspl_create_popup_layout.box()
|
|
|
|
arm_object = getArmatureObject(context)
|
|
pose_library = getPoseLib(context)
|
|
dspl_new_pose_menu.prop(
|
|
arm_object.dsplvars, "pose_new_name", text="Name")
|
|
dspl_new_pose_menu.prop(
|
|
arm_object.dsplvars,
|
|
"only_selected", icon='GROUP_BONE', text="Selected", toggle=True)
|
|
dspl_new_pose_menu.operator(
|
|
"dspl.add_pose", icon='ADD', text="Add New Pose").posename = arm_object.dsplvars.pose_new_name
|
|
|
|
|
|
# Operator to call up popup menus by ID
|
|
|
|
|
|
class DSPL_OT_CallPopupMenu(bpy.types.Operator):
|
|
bl_idname = "dspl.call_popup_menu"
|
|
bl_label = "Popup menu"
|
|
bl_description = "Popup menu"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
menuID: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
bpy.ops.wm.call_menu(name=self.menuID)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Operator to manage the big menu buttons
|
|
|
|
|
|
class DSPL_OT_MenuButtonHandler(bpy.types.Operator):
|
|
bl_idname = "dspl.menu_button_handler"
|
|
bl_label = "Button Handler"
|
|
bl_description = "Button Handler"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
posename: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
bpy.ops.wm.call_menu(name=self.menuID)
|
|
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
if event.ctrl:
|
|
# Select
|
|
action_object = getPoseLib(context)
|
|
action_object.pose_markers.active_index = searchPoseMarker(context, posename=self.posename, type="index")
|
|
return {'FINISHED'}
|
|
elif event.alt:
|
|
# Remove
|
|
bpy.ops.dspl.remove_pose(posename = self.posename)
|
|
return {'FINISHED'}
|
|
elif event.shift:
|
|
# Rename
|
|
bpy.ops.dspl.rename_pose(posename = self.posename)
|
|
return {'FINISHED'}
|
|
else:
|
|
return self.execute(context)
|
|
|
|
# Operator to add keyframes and marker to pose library
|
|
|
|
|
|
class DSPL_OT_AddPose(bpy.types.Operator):
|
|
bl_idname = "dspl.add_pose"
|
|
bl_label = "Add Pose"
|
|
bl_description = "Add Pose"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
posename: bpy.props.StringProperty()
|
|
replace: bpy.props.BoolProperty(name="Replace", description="Replace existing pose", default=False, options={'SKIP_SAVE'})
|
|
|
|
def execute(self, context):
|
|
|
|
if self.replace == False:
|
|
arm_object = getArmatureObject(context)
|
|
action_object = getPoseLib(context)
|
|
pose_markers = action_object.pose_markers
|
|
new_name = self.posename
|
|
counter = 1
|
|
|
|
# Find first unused marker frame
|
|
for f in range(0, len(pose_markers) + 1):
|
|
f += 1
|
|
for pm in pose_markers:
|
|
name_check = pm.name
|
|
frame_check = pm.frame
|
|
if frame_check == f:
|
|
break
|
|
else:
|
|
new_marker = f
|
|
break
|
|
|
|
# Check for duplicate names
|
|
while pose_markers.find(new_name) > -1:
|
|
new_name = self.posename + ".{:03d}".format(counter)
|
|
counter += 1
|
|
else:
|
|
pose_name = new_name
|
|
|
|
pose_markers.new(name=pose_name)
|
|
pose_markers[pose_name].frame = new_marker
|
|
|
|
setKeyframesFromBones(context, new_marker)
|
|
|
|
action_object.pose_markers.active = pose_markers[pose_name]
|
|
bpy.context.area.tag_redraw()
|
|
|
|
self.report({'INFO'}, "DSPL: Added " + pose_markers[new_name].name + " to frame " + str(pose_markers[new_name].frame))
|
|
|
|
else:
|
|
arm_object = getArmatureObject(context)
|
|
action_object = getPoseLib(context)
|
|
pose_markers = action_object.pose_markers
|
|
active_marker = pose_markers.active
|
|
new_name = active_marker.name
|
|
counter = 1
|
|
|
|
for pm in pose_markers:
|
|
name_check = pm.name
|
|
frame_check = pm.frame
|
|
if name_check == new_name:
|
|
target_name = name_check
|
|
target_frame = frame_check
|
|
break
|
|
else:
|
|
return
|
|
|
|
new_marker = target_frame
|
|
|
|
setKeyframesFromBones(context, new_marker)
|
|
|
|
self.report({'INFO'}, "DSPL: Replaced " + pose_markers[new_name].name + " on frame " + str(pose_markers[new_name].frame))
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Operator to remove keyframes and marker
|
|
|
|
|
|
class DSPL_OT_RemovePose(bpy.types.Operator):
|
|
bl_idname = "dspl.remove_pose"
|
|
bl_label = "Remove Pose"
|
|
bl_description = "Remove Pose"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
posename: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
arm_object = getArmatureObject(context)
|
|
action_object = getPoseLib(context)
|
|
pose_markers = action_object.pose_markers
|
|
if self.posename:
|
|
action_object.pose_markers.active_index = searchPoseMarker(context, posename=self.posename, type="index")
|
|
active_index = action_object.pose_markers.active_index
|
|
else:
|
|
active_index = pose_markers.active_index
|
|
active_marker = pose_markers.active
|
|
active_frame = active_marker.frame
|
|
|
|
fcurves = action_object.fcurves
|
|
for fcu in fcurves:
|
|
for kf in fcu.keyframe_points.values():
|
|
if kf.co.x == active_frame:
|
|
fcu.keyframe_points.remove(kf)
|
|
|
|
if active_index <= 0:
|
|
next_index = active_index
|
|
next_marker = pose_markers[next_index]
|
|
elif (active_index + 1) >= len(pose_markers):
|
|
next_index = active_index - 1
|
|
next_marker = pose_markers[next_index]
|
|
else:
|
|
next_index = active_index
|
|
next_marker = pose_markers[next_index]
|
|
|
|
pose_markers.remove(marker=active_marker)
|
|
|
|
action_object.pose_markers.active = next_marker
|
|
action_object.pose_markers.active_index = next_index
|
|
|
|
self.report({'INFO'}, "DSPL: Removed " + self.posename)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Operator to rename the current pose
|
|
|
|
|
|
class DSPL_OT_RenamePose(bpy.types.Operator):
|
|
bl_idname = "dspl.rename_pose"
|
|
bl_label = "Rename Pose"
|
|
bl_description = "Rename Pose"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
posename: bpy.props.StringProperty()
|
|
pose_new_name: bpy.props.StringProperty()
|
|
|
|
def execute(self, context):
|
|
|
|
arm_object = getArmatureObject(context)
|
|
action_object = getPoseLib(context)
|
|
pose_markers = action_object.pose_markers
|
|
active_marker = pose_markers.active
|
|
|
|
if self.posename:
|
|
target_marker = searchPoseMarker(context, posename=self.posename, type="marker")
|
|
else:
|
|
target_marker = active_marker
|
|
|
|
if self.pose_new_name:
|
|
target_marker.name = self.pose_new_name
|
|
context.area.tag_redraw()
|
|
return {'FINISHED'}
|
|
else:
|
|
return {'FINISHED'}
|
|
|
|
|
|
def invoke(self, context, event):
|
|
self.pose_new_name = self.posename
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
|
|
# Operator to reorder pose markers
|
|
|
|
|
|
class DSPL_OT_MovePose(bpy.types.Operator):
|
|
bl_idname = "dspl.move_pose"
|
|
bl_label = "Move Pose"
|
|
bl_description = "Move pose"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
direction: bpy.props.StringProperty(name="Direction", default="DOWN")
|
|
posename: bpy.props.StringProperty(name="Pose Name", default="", options={'SKIP_SAVE'})
|
|
|
|
def execute(self, context):
|
|
arm_object = getArmatureObject(context)
|
|
action_object = getPoseLib(context)
|
|
pose_lib = getPoseLib(context)
|
|
pose_markers = action_object.pose_markers
|
|
|
|
if self.posename:
|
|
active_index = searchPoseMarker(context, posename=self.posename, type="index")
|
|
active_marker = searchPoseMarker(context, posename=self.posename, type="marker")
|
|
active_frame = active_marker.frame
|
|
active_posename = active_marker.name
|
|
else:
|
|
arm_object = getArmatureObject(context)
|
|
active_marker = pose_markers.active
|
|
active_index = pose_markers.active_index
|
|
|
|
if self.direction == "UP":
|
|
move_dir = -1
|
|
elif self.direction == "DOWN":
|
|
move_dir = 1
|
|
|
|
swap_index = active_index + move_dir
|
|
|
|
if swap_index < 0:
|
|
# swap_index = len(pose_lib.pose_markers) - 1
|
|
return {'FINISHED'}
|
|
elif swap_index >= len(pose_lib.pose_markers):
|
|
# swap_index = 0
|
|
return {'FINISHED'}
|
|
|
|
swap_marker = pose_markers[swap_index]
|
|
|
|
tmp_marker_name = swap_marker.name
|
|
tmp_marker_frame = swap_marker.frame
|
|
action_object.pose_markers[swap_index].name = active_marker.name
|
|
action_object.pose_markers[swap_index].frame = active_marker.frame
|
|
action_object.pose_markers[active_index].name = tmp_marker_name
|
|
action_object.pose_markers[active_index].frame = tmp_marker_frame
|
|
action_object.pose_markers.active_index = swap_index
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Operator to apply a pose from active marker
|
|
|
|
|
|
class DSPL_OT_ApplyPose(bpy.types.Operator):
|
|
bl_idname = "dspl.apply_pose"
|
|
bl_label = "Apply Pose"
|
|
bl_description = "Apply Pose (Ctrl+Click to select, Alt+Click to remove)"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
posename: bpy.props.StringProperty()
|
|
|
|
|
|
def execute(self, context):
|
|
arm_object = getArmatureObject(context)
|
|
action_object = getPoseLib(context)
|
|
pose_markers = action_object.pose_markers
|
|
|
|
if self.posename:
|
|
active_marker = searchPoseMarker(context, posename=self.posename, type="marker")
|
|
active_frame = active_marker.frame
|
|
active_posename = active_marker.name
|
|
action_object.pose_markers.active_index = searchPoseMarker(context, posename=self.posename, type="index")
|
|
else:
|
|
active_index = pose_markers.active_index
|
|
active_marker = pose_markers.active
|
|
active_frame = active_marker.frame
|
|
active_posename = active_marker.name
|
|
|
|
for bone in arm_object.pose.bones:
|
|
if bone.bone.select or arm_object.dsplvars.only_selected == False:
|
|
bone_name = bone.name
|
|
|
|
if bone.rotation_mode == "XYZ":
|
|
rot_mode = "rotation_euler"
|
|
elif bone.rotation_mode == "YZX":
|
|
rot_mode = "rotation_euler"
|
|
elif bone.rotation_mode == "ZXY":
|
|
rot_mode = "rotation_euler"
|
|
elif bone.rotation_mode == "QUATERNION":
|
|
rot_mode = "rotation_quaternion"
|
|
else:
|
|
self.report({'WARNING'}, "DSPL: Unsupported bone: " + bone.name + ": " + bone.rotation_mode)
|
|
rot_mode = None
|
|
|
|
loc_x = findFcurve(context, bone_name, "location", 0) or 0.0
|
|
loc_y = findFcurve(context, bone_name, "location", 1) or 0.0
|
|
loc_z = findFcurve(context, bone_name, "location", 2) or 0.0
|
|
if rot_mode == "rotation_quaternion":
|
|
rot_w = findFcurve(context, bone_name, rot_mode, 0) or 1.0
|
|
rot_x = findFcurve(context, bone_name, rot_mode, 1) or 0.0
|
|
rot_y = findFcurve(context, bone_name, rot_mode, 2) or 0.0
|
|
rot_z = findFcurve(context, bone_name, rot_mode, 3) or 0.0
|
|
elif rot_mode == "rotation_euler":
|
|
rot_x = findFcurve(context, bone_name, rot_mode, 0) or 0.0
|
|
rot_y = findFcurve(context, bone_name, rot_mode, 1) or 0.0
|
|
rot_z = findFcurve(context, bone_name, rot_mode, 2) or 0.0
|
|
scl_x = findFcurve(context, bone_name, "scale", 0) or 1.0
|
|
scl_y = findFcurve(context, bone_name, "scale", 1) or 1.0
|
|
scl_z = findFcurve(context, bone_name, "scale", 2) or 1.0
|
|
|
|
bone.location = mathutils.Vector((loc_x, loc_y, loc_z))
|
|
if bone.rotation_mode == "XYZ":
|
|
bone.rotation_euler = mathutils.Euler(
|
|
(rot_x, rot_y, rot_z))
|
|
elif bone.rotation_mode == "YZX":
|
|
bone.rotation_euler = mathutils.Euler(
|
|
(rot_x, rot_y, rot_z))
|
|
elif bone.rotation_mode == "ZXY":
|
|
bone.rotation_euler = mathutils.Euler(
|
|
(rot_z, rot_x, rot_y))
|
|
elif bone.rotation_mode == "YXZ":
|
|
bone.rotation_euler = mathutils.Euler(
|
|
(rot_y, rot_x, rot_z))
|
|
elif bone.rotation_mode == "XZY":
|
|
bone.rotation_euler = mathutils.Euler(
|
|
(rot_x, rot_z, rot_y))
|
|
elif rot_mode == "rotation_quaternion":
|
|
bone.rotation_quaternion = mathutils.Quaternion(
|
|
(rot_w, rot_x, rot_y, rot_z))
|
|
bone.scale = mathutils.Vector((scl_x, scl_y, scl_z))
|
|
|
|
self.report({'INFO'}, "DSPL: Applied " + active_posename)
|
|
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
if event.ctrl:
|
|
# Select
|
|
action_object = getPoseLib(context)
|
|
action_object.pose_markers.active_index = searchPoseMarker(context, posename=self.posename, type="index")
|
|
return {'FINISHED'}
|
|
elif event.alt:
|
|
# Remove
|
|
bpy.ops.dspl.remove_pose(posename = self.posename)
|
|
return {'FINISHED'}
|
|
elif event.shift:
|
|
# Rename
|
|
bpy.ops.dspl.rename_pose(posename = self.posename)
|
|
return {'FINISHED'}
|
|
else:
|
|
return self.execute(context)
|
|
|
|
|
|
# Operator to preview up and down pose list
|
|
|
|
|
|
class DSPL_OT_BrowsePoses(bpy.types.Operator):
|
|
bl_idname = "dspl.browse_poses"
|
|
bl_label = "Browse Poses"
|
|
bl_description = "Browse Poses"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
|
|
def draw_callback_px(self, context, test):
|
|
font_id = 1
|
|
font_size = 24
|
|
font_dpi = 72
|
|
|
|
blf.position(font_id, 15, 30, 0)
|
|
mod_var = self.pose_lib.pose_markers.active.name
|
|
blf.color(font_id, 1.0, 1.0, 1.0, 1.0)
|
|
blf.size(font_id, font_size)
|
|
blf.draw(font_id, "Previewing pose: " + mod_var)
|
|
|
|
|
|
def modal(self, context, event):
|
|
context.area.tag_redraw()
|
|
|
|
if event.value == 'PRESS':
|
|
if event.type in {'LEFT_ARROW', 'UP_ARROW'}:
|
|
if self.pose_lib.pose_markers.active_index <= 0:
|
|
self.pose_lib.pose_markers.active_index = len(self.pose_lib.pose_markers) - 1
|
|
else:
|
|
self.pose_lib.pose_markers.active_index = self.pose_lib.pose_markers.active_index - 1
|
|
bpy.ops.dspl.apply_pose()
|
|
elif event.type in {'RIGHT_ARROW', 'DOWN_ARROW'}:
|
|
if self.pose_lib.pose_markers.active_index + 1 >= len(self.pose_lib.pose_markers):
|
|
self.pose_lib.pose_markers.active_index = 0
|
|
else:
|
|
self.pose_lib.pose_markers.active_index = self.pose_lib.pose_markers.active_index + 1
|
|
bpy.ops.dspl.apply_pose()
|
|
|
|
if event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER'}:
|
|
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
|
return {'FINISHED'}
|
|
|
|
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
|
self.arm_object.pose.backup_restore()
|
|
self.pose_lib.pose_markers.active_index = self.backup_index
|
|
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
|
return {'CANCELLED'}
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
|
def invoke(self, context, event):
|
|
bpy.context.area.tag_redraw()
|
|
|
|
self.arm_object = getArmatureObject(context)
|
|
self.pose_lib = getPoseLib(context)
|
|
|
|
if self.pose_lib is None:
|
|
self.report({'WARNING'}, "DSPL: Pose Library not active")
|
|
return {'CANCELLED'}
|
|
|
|
self.arm_object.pose.backup_create(self.pose_lib)
|
|
self.backup_index = self.pose_lib.pose_markers.active_index
|
|
bpy.ops.dspl.apply_pose()
|
|
|
|
if context.area.type == 'VIEW_3D':
|
|
self.report({'INFO'}, "DSPL: Browsing Poses")
|
|
args = (self, context)
|
|
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
return {'CANCELLED'}
|
|
|
|
|
|
# Operator to unlink a pose library and mark for removal
|
|
|
|
|
|
class DSPL_OT_UnlinkPoseLibrary(bpy.types.Operator):
|
|
bl_idname = "dspl.unlink_pose_library"
|
|
bl_label = "Unlink Pose Library"
|
|
bl_description = "Unlink Pose Library"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
arm_object = getArmatureObject(context)
|
|
pose_library = getPoseLib(context)
|
|
|
|
arm_object.dspl.pose_library.name = "del_" + arm_object.dspl.pose_library.name
|
|
arm_object.dspl.pose_library = None
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Operator to protect orphaned legacy pose libraries
|
|
|
|
|
|
class DSPL_OT_ProtectOrphanPoseLibrary(bpy.types.Operator):
|
|
bl_idname = "dspl.protect_orphan_pose_library"
|
|
bl_label = "Protect Orphaned Pose Libraries"
|
|
bl_description = "Protect Orphaned Pose Libraries"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
only_poselibs: bpy.props.BoolProperty(name="Only Poselibs", description="Only named poselibs", default=False, options={'SKIP_SAVE'})
|
|
protect: bpy.props.BoolProperty(name="Protect", description="Or not", default=True, options={'SKIP_SAVE'})
|
|
|
|
def check(self, context):
|
|
return True
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
orphaned_act = [act for act in bpy.data.actions if act.users == 0]
|
|
if orphaned_act:
|
|
|
|
# bpy.types.Scene.my_prop = bpy.props.BoolVectorProperty(name="boo", size = len(orphaned_act), default=True)
|
|
col = layout.column()
|
|
col.label(text="Orphaned actions", icon="ORPHAN_DATA")
|
|
|
|
for act in orphaned_act:
|
|
entryrow = col.row()
|
|
protectbutton = entryrow.prop(self, "protect", text=act.name)
|
|
|
|
col.split()
|
|
else:
|
|
layout.label(text="No orphans here")
|
|
|
|
def execute(self, context):
|
|
orphaned_act = [act for act in bpy.data.actions if act.users == 0]
|
|
if orphaned_act:
|
|
for act in orphaned_act:
|
|
if "_loc" in act.name or "PoseLib" in act.name:
|
|
self.report({'INFO'}, "DSPL: Protecting orphaned action: " + act.name)
|
|
act.use_fake_user = True
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
classes = (
|
|
DSPL_OT_CreatePoseLibrary,
|
|
DSPL_OT_ConvertPoseLibrary,
|
|
DSPL_OT_CallPopupMenu,
|
|
DSPL_OT_DrawNewPoseMenu,
|
|
DSPL_OT_MenuButtonHandler,
|
|
DSPL_OT_AddPose,
|
|
DSPL_OT_RemovePose,
|
|
DSPL_OT_RenamePose,
|
|
DSPL_OT_MovePose,
|
|
DSPL_OT_ApplyPose,
|
|
DSPL_OT_BrowsePoses,
|
|
DSPL_OT_UnlinkPoseLibrary,
|
|
DSPL_OT_ProtectOrphanPoseLibrary
|
|
)
|
|
|
|
register, unregister = bpy.utils.register_classes_factory(classes)
|