755 lines
27 KiB
Python
755 lines
27 KiB
Python
## 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
|
|
|
|
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)
|
|
|
|
def getMatrix(layer) :
|
|
matrix = mathutils.Matrix.Identity(4)
|
|
|
|
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
|
|
|
|
return matrix.copy()
|
|
|
|
# default_pt_uv_fill = Vector((0.5, 0.5))
|
|
|
|
def dump_gp_point(p, l, obj,
|
|
radius=True, opacity=True, vertex_color=True, fill_color=True, uv_factor=True, rotation=True):
|
|
'''add properties of a given points to a dic and return it'''
|
|
point_dict = {}
|
|
#point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type'
|
|
#for att in point_attr_list:
|
|
# point_dict[att] = convertAttr(getattr(p, att))
|
|
if l.parent:
|
|
mat = getMatrix(l)
|
|
point_dict['position'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'position'))
|
|
else:
|
|
point_dict['position'] = convertAttr(obj.matrix_world @ getattr(p,'position'))
|
|
|
|
# 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'))
|
|
|
|
## get vertex color (long...)
|
|
if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
|
|
point_dict['vertex_color'] = convertAttr(p.vertex_color)
|
|
|
|
if rotation and p.rotation != 0.0:
|
|
point_dict['rotation'] = convertAttr(p.rotation)
|
|
|
|
## No time infos
|
|
# if delta_time and p.delta_time != 0.0:
|
|
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
|
|
|
|
return point_dict
|
|
|
|
def dump_gp_stroke_range(s, sid, l, obj,
|
|
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
|
'''Get a grease pencil stroke and return a dic with attribute
|
|
(points attribute being a dic of dics to store points and their attributes)
|
|
'''
|
|
|
|
stroke_dict = {}
|
|
# stroke_attr_list = ('line_width',)
|
|
# for att in stroke_attr_list:
|
|
# stroke_dict[att] = getattr(s, att)
|
|
|
|
## Dump following these value only if they are non default
|
|
if s.material_index != 0:
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
points = []
|
|
if sid is None: # no ids, just full points...
|
|
for p in s.points:
|
|
points.append(dump_gp_point(p, l, obj,
|
|
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
|
|
else:
|
|
for pid in sid:
|
|
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
|
|
|
|
|
|
def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
|
'''
|
|
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.current_frame()
|
|
|
|
if f: # active frame can be None
|
|
if not copy:
|
|
staylist = [] # init part of strokes that must survive on this layer
|
|
|
|
rm_list = [] # init strokes that must be removed from this layer
|
|
for s_index, stroke in enumerate(f.drawing.strokes):
|
|
if stroke.select:
|
|
# separate in multiple stroke if parts of the strokes a selected.
|
|
sel = [i for i, p in enumerate(stroke.points) if p.select]
|
|
substrokes = [] # list of list containing isolated selection
|
|
|
|
# continuity stroke have same substract result between point index and enumerator
|
|
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):
|
|
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(stroke, ss, l, obj))
|
|
|
|
# Cutting operation
|
|
if not copy:
|
|
maxindex = len(stroke.points)-1
|
|
if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
|
|
# f.drawing.strokes.remove(stroke) # gpv2
|
|
rm_list.append(s_index)
|
|
else:
|
|
neg = [i for i, p in enumerate(stroke.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(stroke, ns, l, obj))
|
|
# make a negative list containing all last index
|
|
|
|
if rm_list:
|
|
f.drawing.remove_strokes(indices=rm_list)
|
|
|
|
if not copy:
|
|
selected_ids = [i for i, s in enumerate(f.drawing.strokes) if s.select]
|
|
|
|
# delete all selected strokes...
|
|
if selected_ids:
|
|
f.drawing.remove_strokes(indices=selected_ids)
|
|
|
|
# ...recreate these uncutted ones
|
|
#pprint(staylist)
|
|
if staylist:
|
|
add_multiple_strokes(staylist, 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.drawing.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.current_frame()
|
|
|
|
if not f:
|
|
continue# active frame can be None
|
|
|
|
for s in f.drawing.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,
|
|
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
|
'''
|
|
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
|
|
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.current_frame()
|
|
|
|
if not f:
|
|
continue# active frame can be None
|
|
|
|
for s in f.drawing.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,
|
|
radius=radius, opacity=opacity, vertex_color=vertex_color, fill_color=fill_color, fill_opacity=fill_opacity, rotation=rotation))
|
|
|
|
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
|
return stroke_list
|
|
|
|
def add_stroke(s, frame, layer, obj, select=False):
|
|
'''add stroke on a given frame, (layer is for parentage setting)'''
|
|
# print(3*'-',s)
|
|
pts_to_add = len(s['points'])
|
|
frame.drawing.add_strokes([pts_to_add])
|
|
|
|
ns = frame.drawing.strokes[-1]
|
|
|
|
## set strokes atrributes
|
|
for att, val in s.items():
|
|
if att not in ('points'):
|
|
setattr(ns, att, val)
|
|
|
|
ob_mat_inv = obj.matrix_world.inverted()
|
|
|
|
if layer.parent:
|
|
layer_matrix = getMatrix(layer).inverted()
|
|
transform_matrix = ob_mat_inv @ layer_matrix
|
|
else:
|
|
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
|
|
|
|
|
|
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False):
|
|
'''
|
|
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
|
|
if select is True, newly added strokes are set selected
|
|
if stroke list is empty create an empty frame at current frame
|
|
'''
|
|
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.current_frame()
|
|
## 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)
|
|
|
|
for s in stroke_list:
|
|
add_stroke(s, target_frame, layer, obj, select=select)
|
|
'''
|
|
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 text in paperclip"
|
|
bl_options = {"REGISTER"}
|
|
|
|
#copy = bpy.props.BoolProperty(default=True)
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
|
|
|
def execute(self, context):
|
|
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
|
# self.report({'ERROR'},'No GP object selected')
|
|
# return {"CANCELLED"}
|
|
|
|
t0 = time()
|
|
#ct = check_radius()
|
|
strokelist = copycut_strokes(copy=True, keep_empty=True)
|
|
if not strokelist:
|
|
self.report({'ERROR'}, 'Nothing to copy')
|
|
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 text in paperclip"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
|
|
|
def execute(self, context):
|
|
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
|
# self.report({'ERROR'},'No GP object selected')
|
|
# return {"CANCELLED"}
|
|
|
|
t0 = time()
|
|
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_radius()
|
|
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 == 'GREASEPENCIL'
|
|
|
|
def execute(self, context):
|
|
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
|
# 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, select=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 from selected layers 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 == 'GREASEPENCIL'
|
|
|
|
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)')
|
|
vertex_color : bpy.props.BoolProperty(name='vertex color', default=True,
|
|
description='Dump point vertex_color attribute (already skipped if at default value)')
|
|
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)')
|
|
uv_factor : bpy.props.BoolProperty(name='uv factor', default=True,
|
|
description='Dump point uv_factor attribute (already skipped if at default value)')
|
|
rotation : bpy.props.BoolProperty(name='rotation', default=True,
|
|
description='Dump point rotation attribute (already skipped if at default value)')
|
|
|
|
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:')
|
|
col.prop(self, 'radius')
|
|
col.prop(self, 'opacity')
|
|
col.prop(self, 'vertex_color')
|
|
col.prop(self, 'fill_color')
|
|
col.prop(self, 'fill_opacity')
|
|
col.prop(self, 'rotation')
|
|
return
|
|
|
|
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_radius()
|
|
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)\nHint: 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.drawing.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,
|
|
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
|
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
|
|
|
frame_dic[f.frame_number] = strokelist
|
|
|
|
layerdic[l.name] = frame_dic
|
|
|
|
else: # bake position: copy frame where object as moved even if frame is unchanged
|
|
for l in layerpool:
|
|
print('dump layer:', l.name)
|
|
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.drawing.strokes):
|
|
continue
|
|
|
|
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
|
|
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
|
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
|
|
|
frame_dic[i] = strokelist
|
|
|
|
prevmat = curmat
|
|
layerdic[l.name] = 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 on active layer"
|
|
bl_options = {"REGISTER"}
|
|
|
|
#copy = bpy.props.BoolProperty(default=True)
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object and context.object.type == 'GREASEPENCIL'
|
|
|
|
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 occurence
|
|
|
|
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
|
|
|
|
col = layout.column(align=True)
|
|
row = col.row(align=True)
|
|
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')
|
|
# layout.separator()
|
|
col = layout.column(align=True)
|
|
col.operator('gp.copy_multi_strokes', text='Copy Layers', icon='COPYDOWN')
|
|
col.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():
|
|
if bpy.app.background:
|
|
return
|
|
|
|
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():
|
|
if bpy.app.background:
|
|
return
|
|
|
|
unregister_keymaps()
|
|
for cl in reversed(classes):
|
|
bpy.utils.unregister_class(cl)
|
|
|
|
if __name__ == "__main__":
|
|
register()
|