gp_toolbox/OP_copy_paste.py

752 lines
27 KiB
Python
Raw Normal View History

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
2024-12-16 17:29:27 +01:00
from .utils import is_locked, is_hidden
2021-01-10 16:47:17 +01:00
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)
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))
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'))
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...)
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)
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
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 = []
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
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:
# by default all visible layers
2024-12-16 17:29:27 +01:00
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # []
2021-01-10 16:47:17 +01:00
if not isinstance(layers, list):
# if a single layer object is send put in a list
2021-01-10 16:47:17 +01:00
layers = [layers]
stroke_list = [] # one stroke list for all layers.
2021-01-10 16:47:17 +01:00
for l in layers:
f = l.current_frame()
2021-01-10 16:47:17 +01:00
if f: # active frame can be None
2021-01-10 16:47:17 +01:00
if not copy:
staylist = [] # init part of strokes that must survive on this layer
2021-01-10 16:47:17 +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):
if stroke.select:
2021-01-10 16:47:17 +01:00
# 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]):
2021-01-10 16:47:17 +01:00
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))
2021-01-10 16:47:17 +01:00
# Cutting operation
2021-01-10 16:47:17 +01:00
if not copy:
maxindex = len(stroke.points)-1
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:
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))
# 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:
staylist.append(dump_gp_stroke_range(stroke, ns, l, obj))
# make a negative list containing all last index
2021-01-10 16:47:17 +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:
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)
2021-01-10 16:47:17 +01:00
# ...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):
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
2024-12-16 17:29:27 +01:00
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# 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 = []# one stroke list for all layers.
for l in layers:
f = l.current_frame()
2021-01-10 16:47:17 +01:00
if not f:
continue# active frame can be None
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
"""
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
2024-12-16 17:29:27 +01:00
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # 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:
f = l.current_frame()
2021-01-10 16:47:17 +01:00
if not f:
continue# active frame can be None
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"
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
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
2021-01-10 16:47:17 +01:00
return stroke_list
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
## 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:
layer_matrix = getMatrix(layer).inverted()
transform_matrix = ob_mat_inv @ layer_matrix
2021-01-10 16:47:17 +01:00
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
2021-01-10 16:47:17 +01: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
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
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)
2021-01-10 16:47:17 +01:00
for s in stroke_list:
add_stroke(s, target_frame, layer, obj, select=select)
# 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"
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:
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"
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)
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"
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)')
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)')
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)')
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')
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')
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 = {}
2024-12-16 17:29:27 +01:00
layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l)
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)\nHint: Changing active layer reset selection to active only')
return {"CANCELLED"}
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:
if skip_empty_frame and not len(f.drawing.strokes):
2021-01-10 16:47:17 +01:00
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,
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
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):
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
if skip_empty_frame and not len(f.drawing.strokes):
2021-01-10 16:47:17 +01:00
continue
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[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"
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():
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
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')
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():
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():
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()