2021-01-10 16:47:17 +01:00
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2021-01-10 19:12:09 +01:00
## based on GPclipboard 1.3.2 (just stripped addon prefs)
2021-01-10 16:47:17 +01:00
bl_info = {
" name " : " GP clipboard " ,
" description " : " Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends " ,
" author " : " Samuel Bernou " ,
2021-01-10 19:12:09 +01:00
" version " : ( 1 , 3 , 2 ) ,
2021-01-10 16:47:17 +01:00
" blender " : ( 2 , 83 , 0 ) ,
" location " : " View3D > Toolbar > Gpencil > GP clipboard " ,
" warning " : " " ,
" doc_url " : " https://github.com/Pullusb/GP_clipboard " ,
" category " : " Object " }
import bpy
import os
import mathutils
from mathutils import Vector
import json
from time import time
from operator import itemgetter
from itertools import groupby
# from pprint import pprint
def convertAttr ( Attr ) :
''' Convert given value to a Json serializable format '''
if isinstance ( Attr , ( mathutils . Vector , mathutils . Color ) ) :
return Attr [ : ]
elif isinstance ( Attr , mathutils . Matrix ) :
return [ v [ : ] for v in Attr ]
elif isinstance ( Attr , bpy . types . bpy_prop_array ) :
return [ Attr [ i ] for i in range ( 0 , len ( Attr ) ) ]
else :
return ( Attr )
def getMatrix ( layer ) :
matrix = mathutils . Matrix . Identity ( 4 )
if layer . is_parented :
if layer . parent_type == ' BONE ' :
object = layer . parent
bone = object . pose . bones [ layer . parent_bone ]
matrix = bone . matrix @ object . matrix_world
matrix = matrix . copy ( ) @ layer . matrix_inverse
else :
matrix = layer . parent . matrix_world @ layer . matrix_inverse
return matrix . copy ( )
def dump_gp_point ( p , l , obj ) :
''' add properties of a given points to a dic and return it '''
pdic = { }
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
#for att in point_attr_list:
# pdic[att] = convertAttr(getattr(p, att))
if l . parent :
mat = getMatrix ( l )
pdic [ ' co ' ] = convertAttr ( obj . matrix_world @ mat @ getattr ( p , ' co ' ) )
else :
pdic [ ' co ' ] = convertAttr ( obj . matrix_world @ getattr ( p , ' co ' ) )
pdic [ ' pressure ' ] = convertAttr ( getattr ( p , ' pressure ' ) )
# pdic['select'] = convertAttr(getattr(p,'select'))# need selection ?
pdic [ ' strength ' ] = convertAttr ( getattr ( p , ' strength ' ) )
## get vertex color (long...)
if p . vertex_color [ : ] != ( 0.0 , 0.0 , 0.0 , 0.0 ) :
pdic [ ' vertex_color ' ] = convertAttr ( getattr ( p , ' vertex_color ' ) )
return pdic
def dump_gp_stroke_range ( s , sid , l , obj ) :
''' Get a grease pencil stroke and return a dic with attribute
( points attribute being a dic of dics to store points and their attributes )
'''
sdic = { }
stroke_attr_list = ( ' line_width ' , ) #'select'#read-only: 'triangles'
for att in stroke_attr_list :
sdic [ att ] = getattr ( s , att )
## Dump following these value only if they are non default
if s . material_index != 0 :
sdic [ ' material_index ' ] = s . material_index
2021-01-10 19:12:09 +01:00
if getattr ( s , ' draw_cyclic ' , None ) : # pre-2.92
2021-01-10 16:47:17 +01:00
sdic [ ' draw_cyclic ' ] = s . draw_cyclic
2021-01-10 19:12:09 +01:00
if getattr ( s , ' use_cyclic ' , None ) : # from 2.92
sdic [ ' use_cyclic ' ] = s . use_cyclic
2021-01-10 16:47:17 +01:00
if s . uv_scale != 1.0 :
sdic [ ' uv_scale ' ] = s . uv_scale
if s . uv_rotation != 0.0 :
sdic [ ' uv_rotation ' ] = s . uv_rotation
if s . hardness != 1.0 :
sdic [ ' hardness ' ] = s . hardness
if s . uv_translation != Vector ( ( 0.0 , 0.0 ) ) :
sdic [ ' uv_translation ' ] = convertAttr ( s . uv_translation )
if s . vertex_color_fill [ : ] != ( 0 , 0 , 0 , 0 ) :
sdic [ ' vertex_color_fill ' ] = convertAttr ( s . vertex_color_fill )
points = [ ]
if sid is None : #no ids, just full points...
for p in s . points :
points . append ( dump_gp_point ( p , l , obj ) )
else :
for pid in sid :
points . append ( dump_gp_point ( s . points [ pid ] , l , obj ) )
sdic [ ' points ' ] = points
return sdic
def copycut_strokes ( layers = None , copy = True , keep_empty = True ) : # (mayber allow filter)
'''
copy all visibles selected strokes on active frame
layers can be None , a single layer object or list of layer object as filter
if keep_empty is False the frame is deleted when all strokes are cutted
'''
t0 = time ( )
### must iterate in all layers ! (since all layers are selectable / visible !)
obj = bpy . context . object
gp = obj . data
gpl = gp . layers
# if not color:#get active color name
# color = gp.palettes.active.colors.active.name
if not layers :
#by default all visible layers
layers = [ l for l in gpl if not l . hide and not l . lock ] #[]
if not isinstance ( layers , list ) :
#if a single layer object is send put in a list
layers = [ layers ]
stroke_list = [ ] #one stroke list for all layers.
for l in layers :
f = l . active_frame
if f : #active frame can be None
if not copy :
staylist = [ ] #init part of strokes that must survive on this layer
for s in f . strokes :
if s . select :
# separate in multiple stroke if parts of the strokes a selected.
sel = [ i for i , p in enumerate ( s . points ) if p . select ]
substrokes = [ ] # list of list containing isolated selection
for k , g in groupby ( enumerate ( sel ) , lambda x : x [ 0 ] - x [ 1 ] ) : # continuity stroke have same substract result between point index and enumerator
group = list ( map ( itemgetter ( 1 ) , g ) )
substrokes . append ( group )
for ss in substrokes :
if len ( ss ) > 1 : #avoid copy isolated points
stroke_list . append ( dump_gp_stroke_range ( s , ss , l , obj ) )
#Cutting operation
if not copy :
maxindex = len ( s . points ) - 1
if len ( substrokes ) == maxindex + 1 : #si un seul substroke, c'est le stroke entier
f . strokes . remove ( s )
else :
neg = [ i for i , p in enumerate ( s . points ) if not p . select ]
staying = [ ]
for k , g in groupby ( enumerate ( neg ) , lambda x : x [ 0 ] - x [ 1 ] ) :
group = list ( map ( itemgetter ( 1 ) , g ) )
#extend group to avoid gap when cut, a bit dirty
if group [ 0 ] > 0 :
group . insert ( 0 , group [ 0 ] - 1 )
if group [ - 1 ] < maxindex :
group . append ( group [ - 1 ] + 1 )
staying . append ( group )
for ns in staying :
if len ( ns ) > 1 :
staylist . append ( dump_gp_stroke_range ( s , ns , l , obj ) )
#make a negative list containing all last index
''' #full stroke version
# if s.colorname == color: #line for future filters
stroke_list . append ( dump_gp_stroke ( s , l ) )
#delete stroke on the fly
if not copy :
f . strokes . remove ( s )
'''
if not copy :
# delete all selected strokes...
for s in f . strokes :
if s . select :
f . strokes . remove ( s )
# ...recreate these uncutted ones
#pprint(staylist)
if staylist :
add_multiple_strokes ( staylist , l )
#for ns in staylist:#weirdly recreate the stroke twice !
# add_stroke(ns, f, l)
#if nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
if not copy and not keep_empty : #
if not len ( f . strokes ) :
l . frames . remove ( f )
print ( len ( stroke_list ) , ' strokes copied in ' , time ( ) - t0 , ' seconds ' )
#print(stroke_list)
return stroke_list
""" # Unused
def copy_all_strokes ( layers = None ) :
'''
copy all stroke , not affected by selection on active frame
layers can be None , a single layer object or list of layer object as filter
if keep_empty is False the frame is deleted when all strokes are cutted
'''
t0 = time ( )
scene = bpy . context . scene
obj = bpy . context . object
gp = obj . data
gpl = gp . layers
if not layers :
# by default all visible layers
layers = [ l for l in gpl if not l . hide and not l . lock ] # include locked ?
if not isinstance ( layers , list ) :
# if a single layer object is send put in a list
layers = [ layers ]
stroke_list = [ ] # one stroke list for all layers.
for l in layers :
f = l . active_frame
if not f :
continue # active frame can be None
for s in f . strokes :
## full stroke version
# if s.select:
stroke_list . append ( dump_gp_stroke_range ( s , None , l , obj ) )
print ( len ( stroke_list ) , ' strokes copied in ' , time ( ) - t0 , ' seconds ' )
#print(stroke_list)
return stroke_list
"""
def copy_all_strokes_in_frame ( frame = None , layers = None , obj = None ) :
'''
copy all stroke , not affected by selection on active frame
layers can be None , a single layer object or list of layer object as filter
if keep_empty is False the frame is deleted when all strokes are cutted
'''
t0 = time ( )
scene = bpy . context . scene
obj = bpy . context . object
gp = obj . data
gpl = gp . layers
if not frame or not obj :
return
if not layers :
# by default all visible layers
layers = [ l for l in gpl if not l . hide and not l . lock ] # include locked ?
if not isinstance ( layers , list ) :
# if a single layer object is send put in a list
layers = [ layers ]
stroke_list = [ ]
for l in layers :
f = l . active_frame
if not f :
continue # active frame can be None
for s in f . strokes :
## full stroke version
# if s.select:
# send index of all points to get the whole stroke with "range"
stroke_list . append ( dump_gp_stroke_range ( s , [ i for i in range ( len ( s . points ) ) ] , l , obj ) )
print ( len ( stroke_list ) , ' strokes copied in ' , time ( ) - t0 , ' seconds ' )
#print(stroke_list)
return stroke_list
def add_stroke ( s , frame , layer , obj ) :
''' add stroke on a given frame, (layer is for parentage setting) '''
# print(3*'-',s)
ns = frame . strokes . new ( )
for att , val in s . items ( ) :
if att not in ( ' points ' ) :
setattr ( ns , att , val )
pts_to_add = len ( s [ ' points ' ] )
# print(pts_to_add, 'points')#dbg
ns . points . add ( pts_to_add )
ob_mat_inv = obj . matrix_world . inverted ( )
## patch pressure 1
# pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened
if layer . is_parented :
mat = getMatrix ( layer ) . inverted ( )
for i , pt in enumerate ( s [ ' points ' ] ) :
for k , v in pt . items ( ) :
if k == ' co ' :
setattr ( ns . points [ i ] , k , v )
ns . points [ i ] . co = ob_mat_inv @ mat @ ns . points [ i ] . co # invert of object * invert of layer * coordinate
else :
setattr ( ns . points [ i ] , k , v )
else :
for i , pt in enumerate ( s [ ' points ' ] ) :
for k , v in pt . items ( ) :
if k == ' co ' :
setattr ( ns . points [ i ] , k , v )
ns . points [ i ] . co = ob_mat_inv @ ns . points [ i ] . co # invert of object * coordinate
else :
setattr ( ns . points [ i ] , k , v )
## patch pressure 2
# ns.points.foreach_set('pressure', pressure_flat_list)
def add_multiple_strokes ( stroke_list , layer = None , use_current_frame = True ) :
'''
add a list of strokes to active frame of given layer
if no layer specified , active layer is used
if use_current_frame is True , a new frame will be created only if needed
'''
scene = bpy . context . scene
obj = bpy . context . object
gp = obj . data
gpl = gp . layers
#default: active
if not layer :
layer = gpl . active
fnum = scene . frame_current
target_frame = False
act = layer . active_frame
for s in stroke_list :
if act :
if use_current_frame or act . frame_number == fnum :
#work on current frame if exists
# use current frame anyway if one key exist at this scene.frame
target_frame = act
if not target_frame :
#no active frame
#or active exists but not aligned scene.current with use_current_frame disabled
target_frame = layer . frames . new ( fnum )
add_stroke ( s , target_frame , layer , obj )
'''
for s in stroke_data :
add_stroke ( s , target_frame )
'''
print ( len ( stroke_list ) , ' strokes pasted ' )
### OPERATORS
class GPCLIP_OT_copy_strokes ( bpy . types . Operator ) :
bl_idname = " gp.copy_strokes "
bl_label = " GP Copy strokes "
bl_description = " Copy strokes to str in paperclip "
bl_options = { " REGISTER " }
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll ( cls , context ) :
return context . object and context . object . type == ' GPENCIL '
def execute ( self , context ) :
# if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
t0 = time ( )
#ct = check_pressure()
strokelist = copycut_strokes ( copy = True , keep_empty = True )
if not strokelist :
self . report ( { ' ERROR ' } , ' rien a copier ' )
return { " CANCELLED " }
bpy . context . window_manager . clipboard = json . dumps ( strokelist ) #copy=self.copy
#if ct:
# self.report({'ERROR'}, "Copie OK\n{} points ont une épaisseur supérieure a 1.0 (max = {:.2f})\nCes épaisseurs seront plafonnées à 1 au 'coller'".format(ct[0], ct[1]))
self . report ( { ' INFO ' } , f ' Copied (time : { time ( ) - t0 : .4f } ) ' )
# print('copy total time:', time() - t0)
return { " FINISHED " }
class GPCLIP_OT_cut_strokes ( bpy . types . Operator ) :
bl_idname = " gp.cut_strokes "
bl_label = " GP Cut strokes "
bl_description = " Cut strokes to str in paperclip "
bl_options = { " REGISTER " }
@classmethod
def poll ( cls , context ) :
return context . object and context . object . type == ' GPENCIL '
def execute ( self , context ) :
# if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
t0 = time ( )
strokelist = copycut_strokes ( copy = False , keep_empty = True ) #ct = check_pressure()
if not strokelist :
self . report ( { ' ERROR ' } , ' Nothing to cut ' )
return { " CANCELLED " }
bpy . context . window_manager . clipboard = json . dumps ( strokelist )
self . report ( { ' INFO ' } , f ' Cutted (time : { time ( ) - t0 : .4f } ) ' )
return { " FINISHED " }
class GPCLIP_OT_paste_strokes ( bpy . types . Operator ) :
bl_idname = " gp.paste_strokes "
bl_label = " GP Paste strokes "
bl_description = " paste stroke from paperclip "
bl_options = { " REGISTER " }
@classmethod
def poll ( cls , context ) :
return context . object and context . object . type == ' GPENCIL '
def execute ( self , context ) :
# if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected to paste on')
# return {"CANCELLED"}
t0 = time ( )
#add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
try :
data = json . loads ( bpy . context . window_manager . clipboard )
except :
mess = ' Clipboard does not contain drawing data (load error) '
self . report ( { ' ERROR ' } , mess )
return { " CANCELLED " }
print ( ' data loaded ' , time ( ) - t0 )
add_multiple_strokes ( data , use_current_frame = True )
print ( ' total_time ' , time ( ) - t0 )
return { " FINISHED " }
### --- multi copy
class GPCLIP_OT_copy_multi_strokes ( bpy . types . Operator ) :
bl_idname = " gp.copy_multi_strokes "
bl_label = " GP Copy multi strokes "
bl_description = " Copy multiple layers>frames>strokes (unlocked and unhided ones) to str in paperclip "
bl_options = { " REGISTER " }
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll ( cls , context ) :
return context . object and context . object . type == ' GPENCIL '
def execute ( self , context ) :
bake_moves = True
skip_empty_frame = False
org_frame = context . scene . frame_current
obj = context . object
gpl = obj . data . layers
t0 = time ( )
#ct = check_pressure()
layerdic = { }
layerpool = [ l for l in gpl if not l . hide and l . select ] # and not l.lock
if not layerpool :
self . report ( { ' ERROR ' } , ' No layers selected in GP dopesheet (needs to be visible and selected to be copied) \n Hint: Changing active layer reset selection to active only ' )
return { " CANCELLED " }
if not bake_moves : # copy only drawed frames as is.
for l in layerpool :
if not l . frames :
continue # skip empty layers
frame_dic = { }
for f in l . frames :
if skip_empty_frame and not len ( f . strokes ) :
continue
context . scene . frame_set ( f . frame_number ) #use matrix of this frame
strokelist = copy_all_strokes_in_frame ( frame = f , layers = l , obj = obj )
frame_dic [ f . frame_number ] = strokelist
layerdic [ l . info ] = frame_dic
else : # bake position: copy frame where object as moved even if frame is unchanged
for l in layerpool :
if not l . frames :
continue # skip empty layers
frame_dic = { }
fnums_dic = { f . frame_number : f for f in l . frames }
context . scene . frame_set ( context . scene . frame_start )
curmat = prevmat = obj . matrix_world . copy ( )
for i in range ( context . scene . frame_start , context . scene . frame_end ) :
context . scene . frame_set ( i ) #use matrix of this frame
curmat = obj . matrix_world . copy ( )
# if object has moved or current time is on a draw key
if prevmat != curmat or i in fnums_dic . keys ( ) :
# get the current used frame
for j in fnums_dic . keys ( ) :
if j > = i :
f = fnums_dic [ j ]
break
## skip empty frame if specified
if skip_empty_frame and not len ( f . strokes ) :
continue
strokelist = copy_all_strokes_in_frame ( frame = f , layers = l , obj = obj )
frame_dic [ i ] = strokelist
prevmat = curmat
layerdic [ l . info ] = frame_dic
## All to clipboard manager
bpy . context . window_manager . clipboard = json . dumps ( layerdic )
# reset original frame.
context . scene . frame_set ( org_frame )
self . report ( { ' INFO ' } , f ' Copied layers (time : { time ( ) - t0 : .4f } ) ' )
# print('copy total time:', time() - t0)
return { " FINISHED " }
class GPCLIP_OT_paste_multi_strokes ( bpy . types . Operator ) :
bl_idname = " gp.paste_multi_strokes "
bl_label = " GP paste multi strokes "
bl_description = " Paste multiple layers>frames>strokes from paperclip "
bl_options = { " REGISTER " }
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll ( cls , context ) :
return context . object and context . object . type == ' GPENCIL '
def execute ( self , context ) :
org_frame = context . scene . frame_current
obj = context . object
gpl = obj . data . layers
t0 = time ( )
#add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
try :
data = json . loads ( bpy . context . window_manager . clipboard )
except :
mess = ' Clipboard does not contain drawing data (load error) '
self . report ( { ' ERROR ' } , mess )
return { " CANCELLED " }
print ( ' data loaded ' , time ( ) - t0 )
# add layers (or merge with existing names ?)
### structure
# {layername :
# {1: [strokelist of frame 1], 3: [strokelist of frame 3]}
# }
for layname , allframes in data . items ( ) :
layer = gpl . get ( layname )
if not layer :
layer = gpl . new ( layname )
for fnum , fstrokes in allframes . items ( ) :
context . scene . frame_set ( int ( fnum ) ) #use matrix of this frame for copying (maybe just evaluate depsgraph for object
add_multiple_strokes ( fstrokes , use_current_frame = False ) #create a new frame at each encoutered
print ( ' total_time ' , time ( ) - t0 )
# reset original frame.
context . scene . frame_set ( org_frame )
self . report ( { ' INFO ' } , f ' Copied layers (time : { time ( ) - t0 : .4f } ) ' )
# print('copy total time:', time() - t0)
return { " FINISHED " }
##--PANEL
class GPCLIP_PT_clipboard_ui ( bpy . types . Panel ) :
# bl_idname = "gp_clipboard_panel"
bl_label = " GP Clipboard "
bl_space_type = " VIEW_3D "
bl_region_type = " UI "
bl_category = " Gpencil "
bl_options = { ' DEFAULT_CLOSED ' }
def draw ( self , context ) :
layout = self . layout
row = layout . row ( align = True )
row . operator ( ' gp.copy_strokes ' , text = ' Copy strokes ' , icon = ' COPYDOWN ' )
row . operator ( ' gp.cut_strokes ' , text = ' Cut strokes ' , icon = ' PASTEFLIPUP ' )
layout . operator ( ' gp.paste_strokes ' , text = ' Paste strokes ' , icon = ' PASTEDOWN ' )
layout . separator ( )
layout . operator ( ' gp.copy_multi_strokes ' , text = ' Copy layers ' , icon = ' COPYDOWN ' )
layout . operator ( ' gp.paste_multi_strokes ' , text = ' Paste layers ' , icon = ' PASTEDOWN ' )
###---TEST zone
"""
##main defs
def copy_strokes_to_paperclip ( ) :
bpy . context . window_manager . clipboard = json . dumps ( copycut_strokes ( copy = True , keep_empty = True ) ) #default layers are visible one
def cut_strokes_to_paperclip ( ) :
bpy . context . window_manager . clipboard = json . dumps ( copycut_strokes ( copy = False , keep_empty = True ) )
def paste_strokes_from_paperclip ( ) :
#add condition to detect if clipboard contains loadable values
add_multiple_strokes ( json . loads ( bpy . context . window_manager . clipboard ) , use_current_frame = True ) #layer= layers.active
#copy_strokes_to_paperclip()
#paste_strokes_from_paperclip()
#test direct
#li = copycut_strokes(copy=True)
#add_multiple_strokes(li, bpy.context.scene.grease_pencil.layers['correct'])
"""
#use directly operator idname in shortcut settings :
# gp.copy_strokes
# gp.cut_strokes
# gp.paste_strokes
# gp.copy_multi_strokes
# gp.paste_multi_strokes
###---REGISTER + copy cut paste keymapping
addon_keymaps = [ ]
def register_keymaps ( ) :
addon = bpy . context . window_manager . keyconfigs . addon
km = addon . keymaps . new ( name = " Grease Pencil " , space_type = " EMPTY " , region_type = ' WINDOW ' ) # in Grease context
# km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")# in 3D context
# km = addon.keymaps.new(name = "Window", space_type = "EMPTY")# from everywhere
kmi = km . keymap_items . new ( " gp.copy_strokes " , type = " C " , value = " PRESS " , ctrl = True , shift = True )
kmi . repeat = False
addon_keymaps . append ( ( km , kmi ) )
kmi = km . keymap_items . new ( " gp.cut_strokes " , type = " X " , value = " PRESS " , ctrl = True , shift = True )
kmi . repeat = False
addon_keymaps . append ( ( km , kmi ) )
kmi = km . keymap_items . new ( " gp.paste_strokes " , type = " V " , value = " PRESS " , ctrl = True , shift = True )
kmi . repeat = False
addon_keymaps . append ( ( km , kmi ) )
def unregister_keymaps ( ) :
for km , kmi in addon_keymaps :
km . keymap_items . remove ( kmi )
addon_keymaps . clear ( )
classes = (
GPCLIP_OT_copy_strokes ,
GPCLIP_OT_cut_strokes ,
GPCLIP_OT_paste_strokes ,
GPCLIP_OT_copy_multi_strokes ,
GPCLIP_OT_paste_multi_strokes ,
GPCLIP_PT_clipboard_ui ,
)
def register ( ) :
for cl in classes :
bpy . utils . register_class ( cl )
## make scene property for empty key preservation and bake movement for layers...
register_keymaps ( )
def unregister ( ) :
unregister_keymaps ( )
for cl in reversed ( classes ) :
bpy . utils . unregister_class ( cl )
if __name__ == " __main__ " :
register ( )