2024-11-14 16:11:31 +01:00
## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends
## View3D > Toolbar > Gpencil > GP clipboard
## in 4.2- existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
2021-01-10 16:47:17 +01:00
import bpy
import mathutils
from mathutils import Vector
import json
from time import time
from operator import itemgetter
from itertools import groupby
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 )
2024-11-13 18:58:28 +01:00
def getMatrix ( layer ) :
2021-01-10 16:47:17 +01:00
matrix = mathutils . Matrix . Identity ( 4 )
2024-11-26 16:30:21 +01:00
if parent := layer . parent :
if parent . type == ' ARMATURE ' and layer . parent_bone :
bone = parent . pose . bones [ layer . parent_bone ]
matrix = bone . matrix @ parent . matrix_world
matrix = matrix . copy ( ) @ layer . matrix_parent_inverse
else :
matrix = parent . matrix_world @ layer . matrix_parent_inverse
2021-01-10 16:47:17 +01:00
return matrix . copy ( )
2024-11-13 18:58:28 +01:00
# default_pt_uv_fill = Vector((0.5, 0.5))
2021-02-21 20:15:01 +01:00
2022-07-01 18:14:07 +02:00
def dump_gp_point ( p , l , obj ,
2024-11-13 18:58:28 +01:00
radius = True , opacity = True , vertex_color = True , fill_color = True , uv_factor = True , rotation = True ) :
2021-01-10 16:47:17 +01:00
''' add properties of a given points to a dic and return it '''
2024-11-13 18:58:28 +01:00
point_dict = { }
#point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type'
2021-01-10 16:47:17 +01:00
#for att in point_attr_list:
2024-11-13 18:58:28 +01:00
# point_dict[att] = convertAttr(getattr(p, att))
2021-01-10 16:47:17 +01:00
if l . parent :
mat = getMatrix ( l )
2024-11-13 18:58:28 +01:00
point_dict [ ' position ' ] = convertAttr ( obj . matrix_world @ mat @ getattr ( p , ' position ' ) )
2021-01-10 16:47:17 +01:00
else :
2024-11-13 18:58:28 +01:00
point_dict [ ' position ' ] = convertAttr ( obj . matrix_world @ getattr ( p , ' position ' ) )
2022-07-01 18:14:07 +02:00
2024-11-13 18:58:28 +01:00
# point_dict['select'] = convertAttr(getattr(p,'select')) # need selection ?
if radius and p . radius != 1.0 :
point_dict [ ' radius ' ] = convertAttr ( getattr ( p , ' radius ' ) )
if opacity and p . opacity != 1.0 :
point_dict [ ' opacity ' ] = convertAttr ( getattr ( p , ' opacity ' ) )
2021-01-10 16:47:17 +01:00
## get vertex color (long...)
2022-07-01 18:14:07 +02:00
if vertex_color and p . vertex_color [ : ] != ( 0.0 , 0.0 , 0.0 , 0.0 ) :
2024-11-13 18:58:28 +01:00
point_dict [ ' vertex_color ' ] = convertAttr ( p . vertex_color )
2021-02-21 20:15:01 +01:00
2024-11-13 18:58:28 +01:00
if rotation and p . rotation != 0.0 :
point_dict [ ' rotation ' ] = convertAttr ( p . rotation )
2021-01-10 16:47:17 +01:00
2024-11-13 18:58:28 +01:00
## No time infos
# if delta_time and p.delta_time != 0.0:
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
2021-01-10 16:47:17 +01:00
2024-11-13 18:58:28 +01:00
return point_dict
2021-01-10 16:47:17 +01:00
2022-07-01 18:14:07 +02:00
def dump_gp_stroke_range ( s , sid , l , obj ,
2024-11-13 18:58:28 +01:00
radius = True , opacity = True , vertex_color = True , fill_color = True , fill_opacity = True , rotation = True ) :
2021-01-10 16:47:17 +01:00
''' Get a grease pencil stroke and return a dic with attribute
( points attribute being a dic of dics to store points and their attributes )
'''
2024-11-13 18:58:28 +01:00
stroke_dict = { }
# stroke_attr_list = ('line_width',)
# for att in stroke_attr_list:
# stroke_dict[att] = getattr(s, att)
2021-01-10 16:47:17 +01:00
## Dump following these value only if they are non default
if s . material_index != 0 :
2024-11-13 18:58:28 +01:00
stroke_dict [ ' material_index ' ] = s . material_index
if s . cyclic :
stroke_dict [ ' cyclic ' ] = s . cyclic
if s . softness != 0.0 :
stroke_dict [ ' softness ' ] = s . softness
2021-01-10 16:47:17 +01:00
2024-11-13 18:58:28 +01:00
if s . aspect_ratio != 1.0 :
stroke_dict [ ' aspect_ratio ' ] = s . aspect_ratio
if s . start_cap != 0 :
stroke_dict [ ' start_cap ' ] = s . start_cap
if s . end_cap != 0 :
stroke_dict [ ' end_cap ' ] = s . end_cap
2021-01-10 16:47:17 +01:00
2024-11-13 18:58:28 +01:00
if fill_color and s . fill_color [ : ] != ( 0 , 0 , 0 , 0 ) :
stroke_dict [ ' fill_color ' ] = convertAttr ( s . fill_color )
if fill_opacity and s . fill_opacity != 0.0 :
stroke_dict [ ' fill_opacity ' ] = s . fill_opacity
## No time infos
# if s.time_start != 0.0:
# stroke_dict['time_start'] = s.time_start
2021-01-10 16:47:17 +01:00
points = [ ]
2022-07-01 18:14:07 +02:00
if sid is None : # no ids, just full points...
2021-01-10 16:47:17 +01:00
for p in s . points :
2024-11-13 18:58:28 +01:00
points . append ( dump_gp_point ( p , l , obj ,
radius = radius , opacity = opacity , vertex_color = vertex_color , rotation = rotation ) )
2021-01-10 16:47:17 +01:00
else :
for pid in sid :
2024-11-13 18:58:28 +01:00
points . append ( dump_gp_point ( s . points [ pid ] , l , obj ,
radius = radius , opacity = opacity , vertex_color = vertex_color , rotation = rotation ) )
stroke_dict [ ' points ' ] = points
return stroke_dict
2021-01-10 16:47:17 +01:00
2022-07-01 18:14:07 +02:00
def copycut_strokes ( layers = None , copy = True , keep_empty = True ) :
2021-01-10 16:47:17 +01:00
'''
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 :
2021-10-28 14:33:37 +02:00
# by default all visible layers
layers = [ l for l in gpl if not l . hide and not l . lock ] # []
2021-01-10 16:47:17 +01:00
if not isinstance ( layers , list ) :
2021-10-28 14:33:37 +02:00
# if a single layer object is send put in a list
2021-01-10 16:47:17 +01:00
layers = [ layers ]
2021-10-28 14:33:37 +02:00
stroke_list = [ ] # one stroke list for all layers.
2021-01-10 16:47:17 +01:00
for l in layers :
2024-11-11 17:30:33 +01:00
f = l . current_frame ( )
2021-01-10 16:47:17 +01:00
2021-10-28 14:33:37 +02:00
if f : # active frame can be None
2021-01-10 16:47:17 +01:00
if not copy :
2021-10-28 14:33:37 +02:00
staylist = [ ] # init part of strokes that must survive on this layer
2021-01-10 16:47:17 +01:00
2024-11-12 19:06:57 +01:00
rm_list = [ ] # init strokes that must be removed from this layer
2024-11-13 18:58:28 +01:00
for s_index , stroke in enumerate ( f . drawing . strokes ) :
2024-11-12 19:06:57 +01:00
if stroke . select :
2021-01-10 16:47:17 +01:00
# separate in multiple stroke if parts of the strokes a selected.
2024-11-12 19:06:57 +01:00
sel = [ i for i , p in enumerate ( stroke . points ) if p . select ]
2021-10-28 14:33:37 +02:00
substrokes = [ ] # list of list containing isolated selection
2024-11-12 19:06:57 +01:00
# continuity stroke have same substract result between point index and enumerator
for k , g in groupby ( enumerate ( sel ) , lambda x : x [ 0 ] - x [ 1 ] ) :
2021-01-10 16:47:17 +01:00
group = list ( map ( itemgetter ( 1 ) , g ) )
substrokes . append ( group )
for ss in substrokes :
2021-10-28 14:33:37 +02:00
if len ( ss ) > 1 : # avoid copy isolated points
2024-11-12 19:06:57 +01:00
stroke_list . append ( dump_gp_stroke_range ( stroke , ss , l , obj ) )
2021-01-10 16:47:17 +01:00
2021-10-28 14:33:37 +02:00
# Cutting operation
2021-01-10 16:47:17 +01:00
if not copy :
2024-11-12 19:06:57 +01:00
maxindex = len ( stroke . points ) - 1
2021-10-28 14:33:37 +02:00
if len ( substrokes ) == maxindex + 1 : # if only one substroke, then it's the full stroke
2024-11-13 18:58:28 +01:00
# f.drawing.strokes.remove(stroke) # gpv2
rm_list . append ( s_index )
2021-01-10 16:47:17 +01:00
else :
2024-11-12 19:06:57 +01:00
neg = [ i for i , p in enumerate ( stroke . points ) if not p . select ]
2021-01-10 16:47:17 +01:00
staying = [ ]
for k , g in groupby ( enumerate ( neg ) , lambda x : x [ 0 ] - x [ 1 ] ) :
group = list ( map ( itemgetter ( 1 ) , g ) )
2021-10-28 14:33:37 +02:00
# extend group to avoid gap when cut, a bit dirty
2021-01-10 16:47:17 +01:00
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 :
2024-11-12 19:06:57 +01:00
staylist . append ( dump_gp_stroke_range ( stroke , ns , l , obj ) )
2021-10-28 14:33:37 +02:00
# make a negative list containing all last index
2021-01-10 16:47:17 +01:00
2024-11-12 19:06:57 +01:00
if rm_list :
f . drawing . remove_strokes ( indices = rm_list )
2024-11-14 16:11:31 +01:00
2021-01-10 16:47:17 +01:00
if not copy :
2024-11-12 19:06:57 +01:00
selected_ids = [ i for i , s in enumerate ( f . drawing . strokes ) if s . select ]
2024-11-13 18:58:28 +01:00
2021-01-10 16:47:17 +01:00
# delete all selected strokes...
2024-11-13 18:58:28 +01:00
if selected_ids :
f . drawing . remove_strokes ( indices = selected_ids )
2024-11-12 19:06:57 +01:00
2021-01-10 16:47:17 +01:00
# ...recreate these uncutted ones
#pprint(staylist)
if staylist :
add_multiple_strokes ( staylist , l )
2024-11-12 19:06:57 +01:00
# 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 :
2024-11-11 17:48:11 +01:00
if not len ( f . drawing . strokes ) :
2021-01-10 16:47:17 +01:00
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 :
2024-11-11 17:30:33 +01:00
f = l . current_frame ( )
2021-01-10 16:47:17 +01:00
if not f :
continue # active frame can be None
2024-11-11 17:48:11 +01:00
for s in f . drawing . strokes :
2021-01-10 16:47:17 +01:00
## 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
"""
2022-07-01 18:14:07 +02:00
def copy_all_strokes_in_frame ( frame = None , layers = None , obj = None ,
2024-11-13 18:58:28 +01:00
radius = True , opacity = True , vertex_color = True , fill_color = True , fill_opacity = True , rotation = True ) :
2021-01-10 16:47:17 +01:00
'''
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
'''
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
2022-07-01 18:14:07 +02:00
layers = [ l for l in gpl if not l . hide and not l . lock ] # include locked ?
2021-01-10 16:47:17 +01:00
if not isinstance ( layers , list ) :
# if a single layer object is send put in a list
layers = [ layers ]
stroke_list = [ ]
for l in layers :
2024-11-11 17:30:33 +01:00
f = l . current_frame ( )
2021-01-10 16:47:17 +01:00
if not f :
continue # active frame can be None
2024-11-11 17:48:11 +01:00
for s in f . drawing . strokes :
2021-01-10 16:47:17 +01:00
## full stroke version
# if s.select:
# send index of all points to get the whole stroke with "range"
2022-07-01 18:14:07 +02:00
stroke_list . append ( dump_gp_stroke_range ( s , [ i for i in range ( len ( s . points ) ) ] , l , obj ,
2024-11-13 18:58:28 +01:00
radius = radius , opacity = opacity , vertex_color = vertex_color , fill_color = fill_color , fill_opacity = fill_opacity , rotation = rotation ) )
2021-01-10 16:47:17 +01:00
2021-12-17 23:26:48 +01:00
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
2021-01-10 16:47:17 +01:00
return stroke_list
2021-04-01 18:20:24 +02:00
def add_stroke ( s , frame , layer , obj , select = False ) :
2021-01-10 16:47:17 +01:00
''' add stroke on a given frame, (layer is for parentage setting) '''
# print(3*'-',s)
2024-11-13 18:58:28 +01:00
pts_to_add = len ( s [ ' points ' ] )
frame . drawing . add_strokes ( [ pts_to_add ] )
ns = frame . drawing . strokes [ - 1 ]
2021-01-10 16:47:17 +01:00
2024-11-26 16:30:21 +01:00
## set strokes atrributes
2021-01-10 16:47:17 +01:00
for att , val in s . items ( ) :
if att not in ( ' points ' ) :
setattr ( ns , att , val )
ob_mat_inv = obj . matrix_world . inverted ( )
2024-11-13 18:58:28 +01:00
if layer . parent :
2024-11-26 16:30:21 +01:00
layer_matrix = getMatrix ( layer ) . inverted ( )
transform_matrix = ob_mat_inv @ layer_matrix
2021-01-10 16:47:17 +01:00
else :
2024-11-26 16:30:21 +01:00
transform_matrix = ob_mat_inv
## Set points attributes
for i , pt in enumerate ( s [ ' points ' ] ) :
for k , v in pt . items ( ) :
if k == ' position ' :
setattr ( ns . points [ i ] , k , v )
ns . points [ i ] . position = transform_matrix @ ns . points [ i ] . position # invert of object * invert of layer * coordinate
else :
setattr ( ns . points [ i ] , k , v )
if select :
ns . points [ i ] . select = True
## Opacity initialized at 0.0 (should be 1.0)
if not ' opacity ' in pt :
ns . points [ i ] . opacity = 1.0
## Radius initialized at 0.0 (should probably be 0.01)
if not ' radius ' in pt :
ns . points [ i ] . radius = 0.01
2021-01-10 16:47:17 +01:00
2021-04-01 18:20:24 +02:00
def add_multiple_strokes ( stroke_list , layer = None , use_current_frame = True , select = False ) :
2021-01-10 16:47:17 +01:00
'''
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
2021-12-17 23:26:48 +01:00
if select is True , newly added strokes are set selected
if stroke list is empty create an empty frame at current frame
2021-01-10 16:47:17 +01:00
'''
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
2024-11-11 17:30:33 +01:00
act = layer . current_frame ( )
2021-12-17 23:26:48 +01:00
## set frame if needed
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 )
2021-01-10 16:47:17 +01:00
2021-12-17 23:26:48 +01:00
for s in stroke_list :
2021-04-01 18:20:24 +02:00
add_stroke ( s , target_frame , layer , obj , select = select )
2024-12-02 14:51:43 +01:00
2021-12-17 23:26:48 +01:00
# print(len(stroke_list), 'strokes pasted')
2021-01-10 16:47:17 +01:00
### OPERATORS
class GPCLIP_OT_copy_strokes ( bpy . types . Operator ) :
bl_idname = " gp.copy_strokes "
bl_label = " GP Copy strokes "
2022-07-01 18:14:07 +02:00
bl_description = " Copy strokes to text in paperclip "
2021-01-10 16:47:17 +01:00
bl_options = { " REGISTER " }
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll ( cls , context ) :
2024-11-11 15:35:39 +01:00
return context . object and context . object . type == ' GREASEPENCIL '
2021-01-10 16:47:17 +01:00
def execute ( self , context ) :
2024-11-11 15:35:39 +01:00
# if not context.object or not context.object.type == 'GREASEPENCIL':
2021-01-10 16:47:17 +01:00
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
t0 = time ( )
2024-11-13 18:58:28 +01:00
#ct = check_radius()
2021-01-10 16:47:17 +01:00
strokelist = copycut_strokes ( copy = True , keep_empty = True )
if not strokelist :
2022-07-01 18:14:07 +02:00
self . report ( { ' ERROR ' } , ' Nothing to copy ' )
2021-01-10 16:47:17 +01:00
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 "
2022-07-01 18:14:07 +02:00
bl_description = " Cut strokes to text in paperclip "
2024-11-14 16:11:31 +01:00
bl_options = { " REGISTER " , " UNDO " }
2021-01-10 16:47:17 +01:00
@classmethod
def poll ( cls , context ) :
2024-11-11 15:35:39 +01:00
return context . object and context . object . type == ' GREASEPENCIL '
2021-01-10 16:47:17 +01:00
def execute ( self , context ) :
2024-11-11 15:35:39 +01:00
# if not context.object or not context.object.type == 'GREASEPENCIL':
2021-01-10 16:47:17 +01:00
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
t0 = time ( )
2024-11-13 18:58:28 +01:00
strokelist = copycut_strokes ( copy = False , keep_empty = True ) # ct = check_radius()
2021-01-10 16:47:17 +01:00
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 ) :
2024-11-11 15:35:39 +01:00
return context . object and context . object . type == ' GREASEPENCIL '
2021-01-10 16:47:17 +01:00
def execute ( self , context ) :
2024-11-11 15:35:39 +01:00
# if not context.object or not context.object.type == 'GREASEPENCIL':
2021-01-10 16:47:17 +01:00
# 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 )
2021-04-01 18:20:24 +02:00
add_multiple_strokes ( data , use_current_frame = True , select = True )
2021-01-10 16:47:17 +01:00
print ( ' total_time ' , time ( ) - t0 )
return { " FINISHED " }
### --- multi copy
class GPCLIP_OT_copy_multi_strokes ( bpy . types . Operator ) :
bl_idname = " gp.copy_multi_strokes "
2021-12-17 23:26:48 +01:00
bl_label = " GP Copy Multi Strokes "
bl_description = " Copy multiple layers>frames>strokes from selected layers to str in paperclip "
2021-01-10 16:47:17 +01:00
bl_options = { " REGISTER " }
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll ( cls , context ) :
2024-11-11 15:35:39 +01:00
return context . object and context . object . type == ' GREASEPENCIL '
2021-01-10 16:47:17 +01:00
2024-11-13 18:58:28 +01:00
radius : bpy . props . BoolProperty ( name = ' radius ' , default = True ,
description = ' Dump point radius attribute (already skipped if at default value) ' )
opacity : bpy . props . BoolProperty ( name = ' opacity ' , default = True ,
description = ' Dump point opacity attribute (already skipped if at default value) ' )
2022-07-01 18:14:07 +02:00
vertex_color : bpy . props . BoolProperty ( name = ' vertex color ' , default = True ,
description = ' Dump point vertex_color attribute (already skipped if at default value) ' )
2024-11-13 18:58:28 +01:00
fill_color : bpy . props . BoolProperty ( name = ' fill color ' , default = True ,
description = ' Dump point fill_color attribute (already skipped if at default value) ' )
fill_opacity : bpy . props . BoolProperty ( name = ' fill opacity ' , default = True ,
description = ' Dump point fill_opacity attribute (already skipped if at default value) ' )
2022-07-01 18:14:07 +02:00
uv_factor : bpy . props . BoolProperty ( name = ' uv factor ' , default = True ,
description = ' Dump point uv_factor attribute (already skipped if at default value) ' )
2024-11-13 18:58:28 +01:00
rotation : bpy . props . BoolProperty ( name = ' rotation ' , default = True ,
description = ' Dump point rotation attribute (already skipped if at default value) ' )
2022-07-01 18:14:07 +02:00
def invoke ( self , context , event ) :
# self.file_dump = event.ctrl
return context . window_manager . invoke_props_dialog ( self ) # , width=400
# return self.execute(context)
def draw ( self , context ) :
layout = self . layout
layout . use_property_split = True
col = layout . column ( )
col . label ( text = ' Keep following point attributes: ' )
2024-11-13 18:58:28 +01:00
col . prop ( self , ' radius ' )
col . prop ( self , ' opacity ' )
2022-07-01 18:14:07 +02:00
col . prop ( self , ' vertex_color ' )
2024-11-13 18:58:28 +01:00
col . prop ( self , ' fill_color ' )
col . prop ( self , ' fill_opacity ' )
col . prop ( self , ' rotation ' )
2022-07-01 18:14:07 +02:00
return
2021-01-10 16:47:17 +01:00
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 ( )
2024-11-13 18:58:28 +01:00
#ct = check_radius()
2021-01-10 16:47:17 +01:00
layerdic = { }
2021-12-17 23:26:48 +01:00
layerpool = [ l for l in gpl if not l . hide and l . select ] # and not l.lock
2021-01-10 16:47:17 +01:00
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 " }
2021-12-17 23:26:48 +01:00
if not bake_moves : # copy only drawed frames as is.
2021-01-10 16:47:17 +01:00
for l in layerpool :
if not l . frames :
continue # skip empty layers
frame_dic = { }
for f in l . frames :
2024-11-11 17:48:11 +01:00
if skip_empty_frame and not len ( f . drawing . strokes ) :
2021-01-10 16:47:17 +01:00
continue
2022-07-01 18:14:07 +02:00
context . scene . frame_set ( f . frame_number ) # use matrix of this frame
strokelist = copy_all_strokes_in_frame ( frame = f , layers = l , obj = obj ,
2024-11-13 18:58:28 +01:00
radius = self . radius , opacity = self . opacity , vertex_color = self . vertex_color ,
2024-11-14 16:11:31 +01:00
fill_color = self . fill_color , fill_opacity = self . fill_opacity , rotation = self . rotation )
2021-01-10 16:47:17 +01:00
frame_dic [ f . frame_number ] = strokelist
2024-11-11 15:56:43 +01:00
layerdic [ l . name ] = frame_dic
2021-01-10 16:47:17 +01:00
2022-07-01 18:14:07 +02:00
else : # bake position: copy frame where object as moved even if frame is unchanged
2021-01-10 16:47:17 +01:00
for l in layerpool :
2024-11-11 15:56:43 +01:00
print ( ' dump layer: ' , l . name )
2021-01-10 16:47:17 +01:00
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 ) :
2022-07-01 18:14:07 +02:00
context . scene . frame_set ( i ) # use matrix of this frame
2021-01-10 16:47:17 +01:00
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
2024-11-11 17:48:11 +01:00
if skip_empty_frame and not len ( f . drawing . strokes ) :
2021-01-10 16:47:17 +01:00
continue
2022-07-01 18:14:07 +02:00
strokelist = copy_all_strokes_in_frame ( frame = f , layers = l , obj = obj ,
2024-11-13 18:58:28 +01:00
radius = self . radius , opacity = self . opacity , vertex_color = self . vertex_color ,
2024-11-14 16:11:31 +01:00
fill_color = self . fill_color , fill_opacity = self . fill_opacity , rotation = self . rotation )
2022-07-01 18:14:07 +02:00
2021-01-10 16:47:17 +01:00
frame_dic [ i ] = strokelist
prevmat = curmat
2024-11-11 15:56:43 +01:00
layerdic [ l . name ] = frame_dic
2021-01-10 16:47:17 +01:00
## 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 "
2021-12-17 23:26:48 +01:00
bl_label = " GP Paste Multi Strokes "
bl_description = " Paste multiple layers>frames>strokes from paperclip on active layer "
2021-01-10 16:47:17 +01:00
bl_options = { " REGISTER " }
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll ( cls , context ) :
2024-11-11 15:35:39 +01:00
return context . object and context . object . type == ' GREASEPENCIL '
2021-01-10 16:47:17 +01:00
def execute ( self , context ) :
org_frame = context . scene . frame_current
obj = context . object
gpl = obj . data . layers
t0 = time ( )
2024-11-14 16:11:31 +01:00
# add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
2021-01-10 16:47:17 +01:00
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 ( ) :
2021-12-17 23:26:48 +01:00
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 occurence
2021-01-10 16:47:17 +01:00
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
2021-10-20 13:56:01 +02:00
col = layout . column ( align = True )
row = col . row ( align = True )
2021-12-17 23:26:48 +01:00
row . operator ( ' gp.copy_strokes ' , text = ' Copy Strokes ' , icon = ' COPYDOWN ' )
row . operator ( ' gp.cut_strokes ' , text = ' Cut Strokes ' , icon = ' PASTEFLIPUP ' )
col . operator ( ' gp.paste_strokes ' , text = ' Paste Strokes ' , icon = ' PASTEDOWN ' )
2021-10-20 13:56:01 +02:00
# layout.separator()
col = layout . column ( align = True )
2021-12-17 23:26:48 +01:00
col . operator ( ' gp.copy_multi_strokes ' , text = ' Copy Layers ' , icon = ' COPYDOWN ' )
col . operator ( ' gp.paste_multi_strokes ' , text = ' Paste Layers ' , icon = ' PASTEDOWN ' )
2021-01-10 16:47:17 +01:00
###---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 ( ) :
2021-10-26 02:24:19 +02:00
if bpy . app . background :
return
2021-01-10 16:47:17 +01:00
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 ( ) :
2021-10-26 02:24:19 +02:00
if bpy . app . background :
return
2021-01-10 16:47:17 +01:00
unregister_keymaps ( )
for cl in reversed ( classes ) :
bpy . utils . unregister_class ( cl )
if __name__ == " __main__ " :
register ( )