738 lines
26 KiB
Python
738 lines
26 KiB
Python
# 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/>.
|
|
|
|
## based on GPclipboard 1.3.2 (just stripped addon prefs)
|
|
|
|
bl_info = {
|
|
"name": "GP clipboard",
|
|
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
|
|
"author": "Samuel Bernou",
|
|
"version": (1, 3, 3),
|
|
"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()
|
|
|
|
default_pt_uv_fill = Vector((0.5, 0.5))
|
|
|
|
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(p.vertex_color)
|
|
|
|
## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...)
|
|
if p.uv_fill != default_pt_uv_fill:
|
|
pdic['uv_fill'] = convertAttr(p.uv_fill)
|
|
if p.uv_factor != 0.0:
|
|
pdic['uv_factor'] = convertAttr(p.uv_factor)
|
|
if p.uv_rotation != 0.0:
|
|
pdic['uv_rotation'] = convertAttr(p.uv_rotation)
|
|
|
|
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
|
|
|
|
if getattr(s, 'draw_cyclic', None):# pre-2.92
|
|
sdic['draw_cyclic'] = s.draw_cyclic
|
|
|
|
if getattr(s, 'use_cyclic', None):# from 2.92
|
|
sdic['use_cyclic'] = s.use_cyclic
|
|
|
|
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: # if only one substroke, then it's the full stroke
|
|
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
|
|
'''
|
|
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')
|
|
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)
|
|
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)
|
|
if select:
|
|
ns.points[i].select = True
|
|
|
|
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)
|
|
if select:
|
|
ns.points[i].select = True
|
|
|
|
## trigger updapte (in 2.93 fix some drawing problem with fills and UVs)
|
|
ns.points.update()
|
|
|
|
## patch pressure 2
|
|
# ns.points.foreach_set('pressure', pressure_flat_list)
|
|
|
|
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.active_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 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, 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 == '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)\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.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 on active layer"
|
|
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 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()
|