20 Commits
Author SHA1 Message Date
pullusb 94fc926f7a Gp modifier to unified modifier 2024-11-11 18:44:35 +01:00
pullusb 0160b4eae4 keymap paint mode name 2024-11-11 17:49:22 +01:00
pullusb 1dfb8cff9c add intermediate stroke container - strokes to drawing.strokes 2024-11-11 17:48:11 +01:00
pullusb 4732110b93 multi_frame editing changed to scene toolsetting property 2024-11-11 17:34:47 +01:00
pullusb 7bc7d5d9ff fix brush context menu 2024-11-11 17:32:58 +01:00
pullusb 998bd4b0cb active_frame attribute to current_frame method 2024-11-11 17:30:33 +01:00
pullusb 25adb5beb6 partial update of GP menu types 2024-11-11 17:27:57 +01:00
pullusb 6e94ee270d point attribute co to position 2024-11-11 16:23:11 +01:00
pullusb 3ece64e517 gpv3 update info to name property 2024-11-11 15:56:43 +01:00
pullusb eae69b6f75 update to gpv3 context modes 2024-11-11 15:47:33 +01:00
pullusb f5a78601b6 Begin port to GPv3 - change GP type 2024-11-11 15:35:39 +01:00
pullusb 98ed92afe2 add link to last gpv2 release 2024-11-11 15:35:27 +01:00
pullusb c02b890915 Add Copy material to layer
3.3.0

- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
2024-07-16 17:57:27 +02:00
pullusb 19e26f8cee Fix batch project bug and expose placemnt options
3.2.0

- added: UI settings to show GP tool settings placement and orientation
- fixed: Bug with reproject orientation settings
- added: show current orientation in batch reproject popup UI (if current is selected)
2024-06-04 14:33:31 +02:00
pullusb 01ce06201e move material to layer feature
3.1.0

- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
2024-05-30 18:33:05 +02:00
pullusb 92e53f8368 update urls to gitea repository 2024-03-28 17:10:32 +01:00
pullusb 386be46251 update changelog 3.0.2 2024-03-27 12:10:15 +01:00
pullusb 47b9b68e9e Expose ops copy-move keys to layer in menus
3.0.2

- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
2024-03-27 12:09:57 +01:00
pullusb 810256f5cb fix crash after empty frame creation
3.0.1

- fixed: Crash when drawing directly after generating empty frames
2024-02-22 11:09:26 +01:00
pullusb cf2ba8448a replace bgl with gpu calls update for 4.0
3.0.0

- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
- fixed: openGL draw camera frame and passepartout
2024-02-20 16:07:20 +01:00
31 changed files with 762 additions and 535 deletions
+27
View File
@@ -1,5 +1,32 @@
# Changelog
3.3.0
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
3.2.0
- added: UI settings to show GP tool settings placement and orientation
- fixed: Bug with reproject orientation settings
- added: show current orientation in batch reproject popup UI (if current is selected)
3.1.0
- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
3.0.2
- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
3.0.1
- fixed: Crash after generating empty frames
3.0.0
- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
- fixed: openGL draw camera frame and passepartout
2.5.0
- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)
+5 -4
View File
@@ -9,7 +9,7 @@ from bpy.props import (FloatProperty,
## copied from OP_key_duplicate_send
def get_layer_list(self, context):
'''return (identifier, name, description) of enum content'''
return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
class GP_OT_create_empty_frames(bpy.types.Operator):
bl_idname = "gp.create_empty_frames"
@@ -57,7 +57,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
def invoke(self, context, event):
# Possible preset with shortcut
@@ -113,7 +113,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
if not self.layers_enum:
self.report({'ERROR'}, f"No chosen layers, aborted")
return {'CANCELLED'}
tgt_layers = [l for l in gpl if l.info == self.layers_enum]
tgt_layers = [l for l in gpl if l.name == self.layers_enum]
elif self.targeted_layers == 'NUMBER':
if self.number == 0:
@@ -166,8 +166,9 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
gpl.active.frames.new(num, active=False)
fct += 1
gpl.update()
if fct:
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.name}")
else:
self.report({'WARNING'}, f"No frames to create !")
+40 -37
View File
@@ -53,11 +53,14 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
encounter = defaultdict(list)
plist = []
matrix = ob.matrix_world
for s in f.strokes:#add first and last
for s in f.drawing.strokes: #add first and last
smat = ob.material_slots[s.material_index].material
if not smat:continue#no material on line
if smat.grease_pencil.show_fill:continue# skip fill lines -> #smat.grease_pencil.show_stroke
if len(s.points) < 2:continue#avoid 0 or 1 points
if not smat:
continue #no material on line
if smat.grease_pencil.show_fill:
continue # skip fill lines -> #smat.grease_pencil.show_stroke
if len(s.points) < 2:
continue #avoid 0 or 1 points
plist.append(s.points[0])
plist.append(s.points[-1])
# plist.extend([s.points[0], s.points[-1])# is extend faster ?
@@ -70,7 +73,7 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
for op in plist:#other points
if p == op:# print('same point')
continue
gap2d = vector_length_2d(location_to_region(matrix @ p.co), location_to_region(matrix @ op.co))
gap2d = vector_length_2d(location_to_region(matrix @ p.position), location_to_region(matrix @ op.position))
# print('gap2d: ', gap2d)
if gap2d > tol:
continue
@@ -102,16 +105,16 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
encounter[p].append(op)
simple_draw_gp_stroke([p.co, op.co], f, width = 2, mat_id = mat_id)
simple_draw_gp_stroke([p.position, op.position], f, width = 2, mat_id = mat_id)
ctl += 1
print(f'{ctl} line created')
##test_call: #create_gap_stroke(C.object.data.layers.active.active_frame, C.object, mat_id=C.object.active_material_index)
##test_call: #create_gap_stroke(C.object.data.layers.active.current_frame(), C.object, mat_id=C.object.active_material_index)
def create_closing_line(tolerance=0.2):
for ob in bpy.context.selected_objects:
if ob.type != 'GPENCIL':
if ob.type != 'GREASEPENCIL':
continue
mat_id = get_closeline_mat(ob)# get a the closing material
@@ -128,7 +131,7 @@ def create_closing_line(tolerance=0.2):
## filter on selected
if not l.select:continue# comment this line for all
# for f in l.frames:#not all for now
f = l.active_frame
f = l.current_frame()
## create gap stroke
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
@@ -143,9 +146,9 @@ def is_deviating_by(s, deviation=0.75):
pb = s.points[-2]
pc = s.points[-3]
a = location_to_region(pa.co)
b = location_to_region(pb.co)
c = location_to_region(pc.co)
a = location_to_region(pa.position)
b = location_to_region(pb.position)
c = location_to_region(pc.position)
#cb-> compare angle with ba->
angle = (b-c).angle(a-b)
@@ -158,16 +161,16 @@ def extend_stroke_tips(s,f,ob,length, mat_id):
'''extend line boundary by given length'''
for id_pair in [ [1,0], [-2,-1] ]:# start and end pair
## 2D mode
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].co)
# b_loc = ob.matrix_world @ s.points[id_pair[1]].co
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].position)
# b_loc = ob.matrix_world @ s.points[id_pair[1]].position
# b = location_to_region(b_loc)
# c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c))
# c_loc = region_to_location(c, b_loc)
# simple_draw_gp_stroke([ob.matrix_world.inverted() @ b_loc, ob.matrix_world.inverted() @ c_loc], f, width=2, mat_id=mat_id)
## 3D
a = s.points[id_pair[0]].co# ob.matrix_world @
b = s.points[id_pair[1]].co# ob.matrix_world @
a = s.points[id_pair[0]].position# ob.matrix_world @
b = s.points[id_pair[1]].position# ob.matrix_world @
c = extrapolate_points_by_length(a,b,length)#print(vector_length(b,c))
simple_draw_gp_stroke([b,c], f, width=2, mat_id=mat_id)
@@ -188,15 +191,15 @@ def change_extension_length(ob, strokelist, length, selected=False):
## Change length of current length to designated
# Vector point A to point B (direction), push point B in this direction
a = s.points[-2].co
a = s.points[-2].position
bp = s.points[-1]#end-point
b = bp.co
b = bp.position
ab = b - a
if not ab:
continue
# new pos of B is A + new length in the AB direction
newb = a + (ab.normalized() * length)
bp.co = newb
bp.position = newb
ct += 1
return ct
@@ -210,14 +213,14 @@ def extend_all_strokes_tips(ob, frame, length=10, selected=False):
return
# TODO need custom filters or go in GP refine strokes...
# frame = ob.data.layers.active.active_frame
# frame = ob.data.layers.active.current_frame()
if not frame: return
ct = 0
#TODO need to delete previous closing lines on frame before launching
# iterate in a copy of stroke list to avoid growing frame.strokes as we loop in !
for s in list(frame.strokes):
# iterate in a copy of stroke list to avoid growing frame.drawing.strokes as we loop in !
for s in list(frame.drawing.strokes):
if s.material_index == mat_id:#is a closeline
continue
if len(s.points) < 2:#not enough point to evaluate
@@ -241,7 +244,7 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
# mode : bpy.props.StringProperty(
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
@@ -277,14 +280,14 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide]
else:
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
fct = 0
for l in lays:
if not l.active_frame:
print(f'{l.info} has no active frame')
if not l.current_frame():
print(f'{l.name} has no active frame')
continue
fct += extend_all_strokes_tips(ob, l.active_frame, length = self.length, selected = self.selected)
fct += extend_all_strokes_tips(ob, l.current_frame(), length = self.length, selected = self.selected)
if not fct:
mess = "No strokes extended... see console"
@@ -306,7 +309,7 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
layer_tgt : bpy.props.EnumProperty(
name="Extend layers", description="Choose which layer to target",
@@ -338,14 +341,14 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide]
else:
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
fct = 0
for l in lays:
if not l.active_frame:
print(f'{l.info} has no active frame')
if not l.current_frame():
print(f'{l.name} has no active frame')
continue
fct += change_extension_length(ob, [s for s in l.active_frame.strokes], length = self.length, selected = self.selected)
fct += change_extension_length(ob, [s for s in l.current_frame().drawing.strokes], length = self.length, selected = self.selected)
if not fct:
mess = "No extension modified... see console"
@@ -367,15 +370,15 @@ class GPSTK_OT_comma_finder(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GPENCIL'
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
def execute(self, context):
ct = 0
ob = context.object
lays = [l for l in ob.data.layers if not l.hide and not l.lock]
for l in lays:
if not l.active_frame:continue
for s in l.active_frame.strokes:
if not l.current_frame():continue
for s in l.current_frame().drawing.strokes:
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
ct+=1
@@ -397,7 +400,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
@classmethod
def poll(cls, context):
return (context.object is not None)# and context.object.type == 'GPENCIL'
return (context.object is not None)# and context.object.type == 'GREASEPENCIL'
## draw stuff inside the header (place before main label)
# def draw_header(self, context):
@@ -414,7 +417,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT')
#diplay closeline visibility
if context.object.type == 'GPENCIL' and context.object.data.materials.get('closeline'):
if context.object.type == 'GREASEPENCIL' and context.object.data.materials.get('closeline'):
row=layout.row()
row.prop(context.object.data.materials['closeline'].grease_pencil, 'hide', text='Stop lines')
row.operator("gp.change_close_lines_extension", text='Length', icon = 'DRIVER_DISTANCE')
+6 -6
View File
@@ -27,7 +27,7 @@ class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
# @classmethod
# def poll(cls, context):
# return context.object and context.object.type == 'GPENCIL'
# return context.object and context.object.type == 'GREASEPENCIL'
filename_ext = '.blend'
@@ -58,7 +58,7 @@ class GPTB_OT_brush_set(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.mode == 'PAINT_GPENCIL'
return context.object and context.mode == 'PAINT_GREASE_PENCIL'
def execute(self, context):
brush = bpy.data.brushes.get(self.brush_name)
@@ -72,12 +72,12 @@ class GPTB_OT_brush_set(bpy.types.Operator):
def load_brush_ui(self, context):
prefs = get_addon_prefs()
if context.mode == 'PAINT_GPENCIL':
if context.mode == 'PAINT_GREASE_PENCIL':
self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path
def load_brush_top_bar_ui(self, context):
prefs = get_addon_prefs()
if context.mode == 'PAINT_GPENCIL':
if context.mode == 'PAINT_GREASE_PENCIL':
self.layout.operator('gp.load_brushes').filepath = prefs.brush_path
classes = (
@@ -89,12 +89,12 @@ def register():
for cl in classes:
bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_brush_gpencil_context_menu.append(load_brush_ui)
bpy.types.VIEW3D_MT_brush_context_menu.append(load_brush_ui)
bpy.types.VIEW3D_HT_tool_header.append(load_brush_top_bar_ui)
def unregister():
bpy.types.VIEW3D_HT_tool_header.remove(load_brush_top_bar_ui)
bpy.types.VIEW3D_MT_brush_gpencil_context_menu.remove(load_brush_ui)
bpy.types.VIEW3D_MT_brush_context_menu.remove(load_brush_ui)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
+28 -28
View File
@@ -169,13 +169,13 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
stroke_list = [] # one stroke list for all layers.
for l in layers:
f = l.active_frame
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
for s in f.strokes:
for s in f.drawing.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]
@@ -192,7 +192,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
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)
f.drawing.strokes.remove(s)
else:
neg = [i for i, p in enumerate(s.points) if not p.select]
@@ -217,14 +217,14 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
stroke_list.append(dump_gp_stroke(s,l))
#delete stroke on the fly
if not copy:
f.strokes.remove(s)
f.drawing.strokes.remove(s)
'''
if not copy:
# delete all selected strokes...
for s in f.strokes:
for s in f.drawing.strokes:
if s.select:
f.strokes.remove(s)
f.drawing.strokes.remove(s)
# ...recreate these uncutted ones
#pprint(staylist)
if staylist:
@@ -234,7 +234,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
#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):
if not len(f.drawing.strokes):
l.frames.remove(f)
@@ -268,12 +268,12 @@ def copy_all_strokes(layers=None):
stroke_list = []# one stroke list for all layers.
for l in layers:
f = l.active_frame
f = l.current_frame()
if not f:
continue# active frame can be None
for s in f.strokes:
for s in f.drawing.strokes:
## full stroke version
# if s.select:
stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
@@ -308,12 +308,12 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
stroke_list = []
for l in layers:
f = l.active_frame
f = l.current_frame()
if not f:
continue# active frame can be None
for s in f.strokes:
for s in f.drawing.strokes:
## full stroke version
# if s.select:
# send index of all points to get the whole stroke with "range"
@@ -326,7 +326,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
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()
ns = frame.drawing.strokes.new()
for att, val in s.items():
if att not in ('points'):
@@ -347,7 +347,7 @@ def add_stroke(s, frame, layer, obj, select=False):
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
ns.points[i].position = ob_mat_inv @ mat @ ns.points[i].position # invert of object * invert of layer * coordinate
else:
setattr(ns.points[i], k, v)
if select:
@@ -358,7 +358,7 @@ def add_stroke(s, frame, layer, obj, select=False):
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
ns.points[i].position = ob_mat_inv @ ns.points[i].position# invert of object * coordinate
else:
setattr(ns.points[i], k, v)
if select:
@@ -389,7 +389,7 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
fnum = scene.frame_current
target_frame = False
act = layer.active_frame
act = layer.current_frame()
## set frame if needed
if act:
if use_current_frame or act.frame_number == fnum:
@@ -423,10 +423,10 @@ class GPCLIP_OT_copy_strokes(bpy.types.Operator):
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# if not context.object or not context.object.type == 'GREASEPENCIL':
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
@@ -452,10 +452,10 @@ class GPCLIP_OT_cut_strokes(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# if not context.object or not context.object.type == 'GREASEPENCIL':
# self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"}
@@ -477,10 +477,10 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
# if not context.object or not context.object.type == 'GPENCIL':
# if not context.object or not context.object.type == 'GREASEPENCIL':
# self.report({'ERROR'},'No GP object selected to paste on')
# return {"CANCELLED"}
@@ -510,7 +510,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
pressure : bpy.props.BoolProperty(name='pressure', default=True,
description='Dump point pressure attribute (already skipped if at default value)')
@@ -566,7 +566,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
frame_dic = {}
for f in l.frames:
if skip_empty_frame and not len(f.strokes):
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,
@@ -575,11 +575,11 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
frame_dic[f.frame_number] = strokelist
layerdic[l.info] = frame_dic
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.info)
print('dump layer:', l.name)
if not l.frames:
continue# skip empty layers
@@ -603,7 +603,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
break
## skip empty frame if specified
if skip_empty_frame and not len(f.strokes):
if skip_empty_frame and not len(f.drawing.strokes):
continue
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
@@ -613,7 +613,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
frame_dic[i] = strokelist
prevmat = curmat
layerdic[l.info] = frame_dic
layerdic[l.name] = frame_dic
## All to clipboard manager
bpy.context.window_manager.clipboard = json.dumps(layerdic)
@@ -633,7 +633,7 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
#copy = bpy.props.BoolProperty(default=True)
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
org_frame = context.scene.frame_current
+2 -2
View File
@@ -13,7 +13,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
# @classmethod
# def poll(cls, context):
# return context.object and context.object.type == 'GPENCIL'
# return context.object and context.object.type == 'GREASEPENCIL'
def invoke(self, context, event):
#print('-!SNAP!-')
@@ -23,7 +23,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
return {"FINISHED"}
def execute(self, context):
if not context.object or context.object.type != 'GPENCIL':
if not context.object or context.object.type != 'GREASEPENCIL':
self.report({'INFO'}, 'Not GP, Cursor surface project')
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
return {"FINISHED"}
+22 -23
View File
@@ -1,6 +1,5 @@
import bpy
from bpy.types import Operator
import bgl
from gpu_extras.presets import draw_circle_2d
from gpu_extras.batch import batch_for_shader
import gpu
@@ -190,7 +189,7 @@ class GPTB_OT_eraser(Operator):
bl_options = {'REGISTER', 'UNDO'}
def draw_callback_px(self):
bgl.glEnable(bgl.GL_BLEND)
gpu.state.blend_set('ALPHA')
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
@@ -201,7 +200,7 @@ class GPTB_OT_eraser(Operator):
bg_color = area.spaces.active.shading.background_color
#print(bg_color)
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
shader.bind()
shader.uniform_float("color", (1, 1, 1, 1))
for mouse, radius in self.mouse_path:
@@ -210,7 +209,7 @@ class GPTB_OT_eraser(Operator):
batch.draw(shader)
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
bgl.glDisable(bgl.GL_BLEND)
gpu.state.blend_set('NONE')
@@ -232,7 +231,7 @@ class GPTB_OT_eraser(Operator):
hld_stroke.points.add(count=1)
p = hld_stroke.points[-1]
p.co = mat_inv @ mouse_3d
p.position = mat_inv @ mouse_3d
p.pressure = search_radius * 2000
#context.scene.cursor.location = mouse_3d
@@ -253,14 +252,14 @@ class GPTB_OT_eraser(Operator):
#print(self.cuts_data)
# for f in self.gp_frames:
# for s in [s for s in f.strokes if s.material_index==self.hld_index]:
# f.strokes.remove(s)
# for s in [s for s in f.drawing.strokes if s.material_index==self.hld_index]:
# f.drawing.strokes.remove(s)
#gp.data.materials.pop(index=self.hld_index)
#bpy.data.materials.remove(self.hld_mat)
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
bpy.ops.object.mode_set(mode='EDIT')
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
@@ -282,7 +281,7 @@ class GPTB_OT_eraser(Operator):
bpy.ops.gpencil.select_all(action='DESELECT')
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
strokes = [s for f in self.gp_frames for s in f.strokes]
strokes = [s for f in self.gp_frames for s in f.drawing.strokes]
#print('select_circle', time()-t1)
t2 = time()
@@ -310,18 +309,18 @@ class GPTB_OT_eraser(Operator):
bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True)
new_p1 = stroke.points[p1_index+1]
new_p1.co = mat_inv@intersects[0]
new_p1.position = mat_inv@intersects[0]
new_points += [(stroke, p1_index+1)]
#print('number_cuts', number_cuts)
if number_cuts == 2:
new_p2 = stroke.points[p1_index+2]
new_p2.co = mat_inv@( (intersects[0] + intersects[1])/2 )
new_p2.position = mat_inv@( (intersects[0] + intersects[1])/2 )
#new_points += [new_p2]
new_p3 = stroke.points[p1_index+3]
new_p3.co = mat_inv@intersects[1]
new_p3.position = mat_inv@intersects[1]
new_points += [(stroke, p1_index+3)]
#print('subdivide', time() - t3)
@@ -330,7 +329,7 @@ class GPTB_OT_eraser(Operator):
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
'''
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
tip_points = [p for s in selected_strokes for i, p in enumerate(s.points) if p.select and (i==0 or i == len(s.points)-1)]
bpy.ops.gpencil.select_less()
@@ -342,7 +341,7 @@ class GPTB_OT_eraser(Operator):
'''
t4 = time()
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
if selected_strokes:
bpy.ops.gpencil.delete(type='POINTS')
@@ -359,9 +358,9 @@ class GPTB_OT_eraser(Operator):
#bpy.ops.object.mode_set(mode='OBJECT')
context.scene.tool_settings.gpencil_selectmode_edit = self.gpencil_selectmode_edit
bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
#selected_strokes = [s for s in self.gp_frame.strokes if s.select]
#bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
#selected_strokes = [s for s in self.gp_frame.drawing.strokes if s.select]
#bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
def modal(self, context, event):
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
@@ -442,22 +441,22 @@ class GPTB_OT_eraser(Operator):
t0 = time()
gp_mats = gp.data.materials
gp_layers = [l for l in gp.data.layers if not l.lock or l.hide]
self.gp_frames = [l.active_frame for l in gp_layers]
self.gp_frames = [l.current_frame() for l in gp_layers]
'''
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.strokes]
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes]
points_data = [(s, f, m) for s, f, m in points_data if not m.grease_pencil.hide or m.grease_pencil.lock]
print('get_gp_points', time()-t0)
t0 = time()
#points_data = [(s, f, m, p, get_screen_co(p.co, matrix)) for s, f, m in points_data for p in reversed(s.points)]
points_data = [(s, f, m, p, org + ((matrix @ p.co)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
#points_data = [(s, f, m, p, get_screen_co(p.position, matrix)) for s, f, m in points_data for p in reversed(s.points)]
points_data = [(s, f, m, p, org + ((matrix @ p.position)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
print('points_to_2d', time()-t0)
#print(points_data)
self.points_data = [(s, f, m, p, co) for s, f, m, p, co in points_data if co is not None]
#for s, f, m, p, co in self.points_data:
# p.co = co
# p.position = co
t0 = time()
@@ -482,7 +481,7 @@ class GPTB_OT_eraser(Operator):
self.hld_strokes = []
for f in self.gp_frames:
hld_stroke = f.strokes.new()
hld_stroke = f.drawing.strokes.new()
hld_stroke.start_cap_mode = 'ROUND'
hld_stroke.end_cap_mode = 'ROUND'
hld_stroke.material_index = self.hld_index
+26 -84
View File
@@ -16,15 +16,15 @@ def remove_stroke_exact_duplications(apply=True):
for l in gp.layers:
for f in l.frames:
stroke_list = []
for s in reversed(f.strokes):
for s in reversed(f.drawing.strokes):
point_list = [p.co for p in s.points]
point_list = [p.position for p in s.points]
if point_list in stroke_list:
ct += 1
if apply:
# Remove redundancy
f.strokes.remove(s)
f.drawing.strokes.remove(s)
else:
stroke_list.append(point_list)
return ct
@@ -120,7 +120,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
setattr(area.spaces[0], 'show_locked_time', True)
## set cursor type
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
if context.mode in ("EDIT_GREASE_PENCIL", "SCULPT_GREASE_PENCIL"):
tool = fix.select_active_tool
if tool != 'none':
if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool:
@@ -145,7 +145,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
## GP Use light disable
if fix.set_gp_use_lights_off:
gp_with_lights = [o for o in context.scene.objects if o.type == 'GPENCIL' and o.use_grease_pencil_lights]
gp_with_lights = [o for o in context.scene.objects if o.type == 'GREASEPENCIL' and o.use_grease_pencil_lights]
if gp_with_lights:
problems.append(f'Disable "Use Lights" on {len(gp_with_lights)} Gpencil objects')
if apply:
@@ -181,33 +181,25 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if fix.list_gp_mod_vis_conflict:
mod_viz_ct = 0
for o in context.scene.objects:
if o.type == 'GPENCIL':
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
vp = 'Yes' if m.show_viewport else 'No'
rd = 'Yes' if m.show_render else 'No'
mod_viz_ct += 1
print(f'{o.name} - GP modifier {m.name}: viewport {vp} != render {rd}')
else:
for m in o.modifiers:
if m.show_viewport != m.show_render:
vp = 'Yes' if m.show_viewport else 'No'
rd = 'Yes' if m.show_render else 'No'
mod_viz_ct += 1
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
for m in o.modifiers:
if m.show_viewport != m.show_render:
vp = 'Yes' if m.show_viewport else 'No'
rd = 'Yes' if m.show_render else 'No'
mod_viz_ct += 1
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
if mod_viz_ct:
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA'])
## check if GP modifier have broken layer targets
if fix.list_broken_mod_targets:
for o in [o for o in bpy.context.scene.objects if o.type == 'GPENCIL']:
lay_name_list = [l.info for l in o.data.layers]
for m in o.grease_pencil_modifiers:
if not hasattr(m, 'layer'):
for o in [o for o in bpy.context.scene.objects if o.type == 'GREASEPENCIL']:
lay_name_list = [l.name for l in o.data.layers]
for m in o.modifiers:
if not hasattr(m, 'layer_filter'):
continue
if m.layer != '' and not m.layer in lay_name_list:
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer}'
if m.layer_filter != '' and not m.layer_filter in lay_name_list:
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer_filter}'
print(mess)
problems.append(mess)
@@ -277,7 +269,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
# for ob in context.scene.objects:#from object
# if ob.type == 'GPENCIL':
# if ob.type == 'GREASEPENCIL':
# ob.data.onion_keyframe_type = 'ALL'
#### --- print fix/problems report
@@ -562,46 +554,6 @@ class GPTB_OT_list_object_visibility(bpy.types.Operator):
# self.report({'INFO'}, f"No Object visibility conflict on current scene")
# return {'FINISHED'}
## Only GP modifier
'''
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
bl_idname = "gp.list_modifier_visibility"
bl_label = "List GP Modifiers Visibility Conflicts"
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
def invoke(self, context, event):
self.ob_list = []
for o in context.scene.objects:
if o.type != 'GPENCIL':
continue
if not len(o.grease_pencil_modifiers):
continue
mods = []
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods])
mods.append(m)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
for o in self.ob_list:
layout.label(text=o[0].name, icon='OUTLINER_OB_GREASEPENCIL')
for m in o[1]:
row = layout.row()
row.label(text='')
row.label(text=m.name, icon='MODIFIER_ON')
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
def execute(self, context):
return {'FINISHED'}
'''
## not exposed in UI, Check is performed in Check file (can be called in popped menu)
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
bl_idname = "gp.list_modifier_visibility"
@@ -612,24 +564,14 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
def invoke(self, context, event):
self.ob_list = []
for o in context.scene.objects:
if o.type == 'GPENCIL':
if not len(o.grease_pencil_modifiers):
continue
mods = []
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, 'OUTLINER_OB_GREASEPENCIL'])
mods.append(m)
else:
if not len(o.modifiers):
continue
mods = []
for m in o.modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
mods.append(m)
if not len(o.modifiers):
continue
mods = []
for m in o.modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
mods.append(m)
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
return context.window_manager.invoke_props_dialog(self, width=250)
+17 -17
View File
@@ -19,7 +19,7 @@ def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=
oframe = bpy.context.scene.frame_current
omode = bpy.context.mode
# frame_list = [ f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)]
# frame_list = [ f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
# frame_list = list(set(frame_list))
# frame_list.sort()
# for fnum in frame_list:
@@ -32,15 +32,15 @@ def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=
fnum = len(l.frames)
zf = len(str(fnum))
for j, f in enumerate(reversed(l.frames)): # whynot...
print(f'{obj.name} : {i+1}/{laynum} : {l.info} : {str(j+1).zfill(zf)}/{fnum}{" "*30}', end='\r')
print(f'{obj.name} : {i+1}/{laynum} : {l.name} : {str(j+1).zfill(zf)}/{fnum}{" "*30}', end='\r')
scn.frame_set(f.frame_number) # more chance to update the matrix
bpy.context.view_layer.update() # update the matrix ?
bpy.context.scene.camera.location = bpy.context.scene.camera.location
scn.frame_current = f.frame_number
for s in f.strokes:
for s in f.drawing.strokes:
for p in s.points:
p.co = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.co), scn.cursor.location)
p.position = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.position), scn.cursor.location)
if restore_frame:
bpy.context.scene.frame_current = oframe
@@ -67,8 +67,8 @@ def batch_flat_reproject(obj):
plane_no.rotate(cam_mat)
plane_co = scn.cursor.location
for s in f.strokes:
points_co = [obj.matrix_world @ p.co for p in s.points]
for s in f.drawing.strokes:
points_co = [obj.matrix_world @ p.position for p in s.points]
points_co = [mat_inv @ intersect_line_plane(origin, p, plane_co, plane_no) for p in points_co]
points_co = [co for vector in points_co for co in vector]
@@ -76,8 +76,8 @@ def batch_flat_reproject(obj):
s.points.add(1) # update
s.points.pop() # update
#for p in s.points:
# loc_2d = location_to_region(obj.matrix_world @ p.co)
# p.co = obj.matrix_world.inverted() @ region_to_location(loc_2d, scn.cursor.location)
# loc_2d = location_to_region(obj.matrix_world @ p.position)
# p.position = obj.matrix_world.inverted() @ region_to_location(loc_2d, scn.cursor.location)
"""
def batch_flat_reproject(obj):
@@ -96,14 +96,14 @@ def batch_flat_reproject(obj):
plane_co = scn.cursor.location
for l in obj.data.layers:
f = l.active_frame
f = l.current_frame()
if not f: # No active frame
continue
if f.frame_number != scn.frame_current:
f = l.frames.copy(f) # duplicate content of the previous frame
for s in f.strokes:
points_co = [obj.matrix_world @ p.co for p in s.points]
for s in f.drawing.strokes:
points_co = [obj.matrix_world @ p.position for p in s.points]
points_co = [mat_inv @ intersect_line_plane(origin, p, plane_co, plane_no) for p in points_co]
points_co = [co for vector in points_co for co in vector]
@@ -119,11 +119,11 @@ class GPTB_OT_batch_flat_reproject(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
for o in context.selected_objects:
if o.type != 'GPENCIL' or not o.select_get():
if o.type != 'GREASEPENCIL' or not o.select_get():
continue
batch_flat_reproject(o)
@@ -132,12 +132,12 @@ class GPTB_OT_batch_flat_reproject(bpy.types.Operator):
### -- MENU ENTRY --
def flat_reproject_clean_menu(self, context):
if context.mode == 'EDIT_GPENCIL':
if context.mode == 'EDIT_GREASE_PENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
def flat_reproject_context_menu(self, context):
if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
@@ -149,12 +149,12 @@ def register():
for cl in classes:
bpy.utils.register_class(cl)
# bpy.types.VIEW3D_MT_gpencil_edit_context_menu.append(flat_reproject_context_menu)
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.append(flat_reproject_context_menu)
# bpy.types.GPENCIL_MT_cleanup.append(flat_reproject_clean_menu)
def unregister():
# bpy.types.GPENCIL_MT_cleanup.remove(flat_reproject_clean_menu)
# bpy.types.VIEW3D_MT_gpencil_edit_context_menu.remove(flat_reproject_context_menu)
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.remove(flat_reproject_context_menu)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
+2 -2
View File
@@ -135,8 +135,8 @@ class GPTB_OT_go_to_object(bpy.types.Operator):
bpy.ops.object.mode_set(mode='POSE', toggle=False)
self.report({'INFO'}, f'Back to pose mode, {obj.name}')
elif obj.type == 'GPENCIL':
bpy.ops.object.mode_set(mode='PAINT_GPENCIL', toggle=False)
elif obj.type == 'GREASEPENCIL':
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL', toggle=False)
else:
self.report({'INFO'}, f'Back to object mode, {obj.name}')
+18 -18
View File
@@ -79,7 +79,7 @@ class GPTB_OT_rename_data_from_obj(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
if not self.rename_all:
@@ -93,7 +93,7 @@ class GPTB_OT_rename_data_from_obj(Operator):
else:
oblist = []
for o in context.scene.objects:
if o.type == 'GPENCIL':
if o.type == 'GREASEPENCIL':
if o.name == o.data.name:
continue
oblist.append(f'{o.data.name} -> {o.name}')
@@ -250,7 +250,7 @@ class GPTB_OT_draw_cam(Operator):
drawcam.parent = act
vec = Vector((0,1,0))
if act.type == 'GPENCIL':
if act.type == 'GREASEPENCIL':
#change vector according to alignement
vec = get_gp_alignement_vector(context)
@@ -427,15 +427,15 @@ class GPTB_OT_toggle_mute_animation(Operator):
pool = context.scene.objects
for o in pool:
if self.mode == 'GPENCIL' and o.type != 'GPENCIL':
if self.mode == 'GREASEPENCIL' and o.type != 'GREASEPENCIL':
continue
if self.mode == 'OBJECT' and o.type in ('GPENCIL', 'CAMERA'):
if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'):
continue
if self.mode == 'CAMERA' and o.type != 'CAMERA':
continue
# mute attribute animation for GP and cameras
if o.type in ('GPENCIL', 'CAMERA') and o.data.animation_data:
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
gp_act = o.data.animation_data.action
if gp_act:
print(f'\n---{o.name} data:')
@@ -473,9 +473,9 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
else:
pool = context.scene.objects
for o in pool:
if o.type != 'GPENCIL':
if o.type != 'GREASEPENCIL':
continue
for m in o.grease_pencil_modifiers:
for m in o.modifier:
# skip modifier that are not visible in render
if not m.show_render:
continue
@@ -506,12 +506,12 @@ class GPTB_OT_list_disabled_anims(Operator):
pool = context.scene.objects
for o in pool:
# if self.skip_gp and o.type == 'GPENCIL':
# if self.skip_gp and o.type == 'GREASEPENCIL':
# continue
# if self.skip_obj and o.type != 'GPENCIL':
# if self.skip_obj and o.type != 'GREASEPENCIL':
# continue
if o.type == 'GPENCIL':
if o.type == 'GREASEPENCIL':
if o.data.animation_data:
gp_act = o.data.animation_data.action
if gp_act:
@@ -611,7 +611,7 @@ class GPTB_OT_clear_active_frame(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
obj = context.object
@@ -619,18 +619,18 @@ class GPTB_OT_clear_active_frame(Operator):
if not l:
self.report({'ERROR'}, 'No layers')
return {'CANCELLED'}
f = l.active_frame
f = l.current_frame()
if not f:
self.report({'ERROR'}, 'No active frame')
return {'CANCELLED'}
ct = len(f.strokes)
ct = len(f.drawing.strokes)
if not ct:
self.report({'ERROR'}, 'Active frame already empty')
return {'CANCELLED'}
for s in reversed(f.strokes):
f.strokes.remove(s)
for s in reversed(f.drawing.strokes):
f.drawing.strokes.remove(s)
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
return {'FINISHED'}
@@ -644,7 +644,7 @@ class GPTB_OT_check_canvas_alignement(Operator):
@classmethod
def poll(cls, context):
# if lock_axis is 'VIEW' then the draw axis is always aligned
return context.object and context.object.type == 'GPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
def execute(self, context):
if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW':
@@ -673,7 +673,7 @@ class GPTB_OT_step_select_frames(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
start : bpy.props.IntProperty(name='Start Frame',
description='Start frame of the step animation',
+29 -15
View File
@@ -8,7 +8,7 @@ def get_layer_list(self, context):
return [('None', 'None','None')]
if not context.object:
return [('None', 'None','None')]
return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
# try:
# except:
# return [("", "", "")]
@@ -17,24 +17,31 @@ def get_layer_list(self, context):
class GPTB_OT_duplicate_send_to_layer(Operator) :
bl_idname = "gp.duplicate_send_to_layer"
bl_label = 'Duplicate and send to layer'
bl_label = 'Duplicate Send To Layer'
bl_description = 'Duplicate selected keys in active layer and send to chosen layer'
# important to have the updated enum here as bl_property
bl_property = "layers_enum"
layers_enum : bpy.props.EnumProperty(
name="Duplicate to layers",
description="Duplicate selected keys in active layer and send them to choosen layer",
description="Duplicate selected keys in active layer and send them to chosen layer",
items=get_layer_list,
options={'HIDDEN'},
)
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
@classmethod
def description(cls, context, properties):
if properties.delete_source:
return f"Move selected keys in active layer to chosen layer"
else:
return f"Copy selected keys in active layer and send to chosen layer"
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'\
and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'\
and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GREASEPENCIL'
# history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self)
@@ -56,16 +63,16 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
replaced = len(to_replace)
## remove overlapping frames
## Remove overlapping frames
for f in reversed(to_replace):
target_layer.frames.remove(f)
## copy original frames
## Copy original frames
for f in selected_frames:
target_layer.frames.copy(f)
sent = len(selected_frames)
## delete original frames as an option
## Delete original frames as an option
if self.delete_source:
for f in reversed(selected_frames):
act_layer.frames.remove(f)
@@ -74,9 +81,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
if replaced:
mess += f' ({replaced} replaced)'
# context.view_layer.update()
# bpy.ops.gpencil.editmode_toggle()
mod = context.mode
bpy.ops.gpencil.editmode_toggle()
bpy.ops.object.mode_set(mode=mod)
@@ -109,9 +113,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
addon_keymaps = []
def register_keymaps():
# pref = get_addon_prefs()
# if not pref.kfj_use_shortcut:
# return
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR")
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True)
@@ -129,6 +130,12 @@ def unregister_keymaps():
addon_keymaps.clear()
def menu_duplicate_and_send_to_layer(self, context):
if context.space_data.ui_mode == 'GREASEPENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN'
self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
classes = (
GPTB_OT_duplicate_send_to_layer,
)
@@ -139,12 +146,19 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
register_keymaps()
bpy.types.DOPESHEET_MT_key.append(menu_duplicate_and_send_to_layer)
bpy.types.DOPESHEET_MT_context_menu.append(menu_duplicate_and_send_to_layer)
def unregister():
if bpy.app.background:
return
bpy.types.DOPESHEET_MT_context_menu.remove(menu_duplicate_and_send_to_layer)
bpy.types.DOPESHEET_MT_key.remove(menu_duplicate_and_send_to_layer)
unregister_keymaps()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+1 -1
View File
@@ -10,7 +10,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
next : BoolProperty(
name="Next GP keyframe", description="Go to next active GP keyframe",
+53 -53
View File
@@ -30,7 +30,7 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
prefs = get_addon_prefs()
sep = prefs.separator
name = old = layer.info
name = old = layer.name
pattern = PATTERN.replace('_', sep) # set separator
@@ -69,7 +69,7 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
# check if name is available without the increment ending
new = f'{grp}{tag}{name}{sfix}'
layer.info = new
layer.name = new
## update name in modifier targets
if old != new:
@@ -78,11 +78,11 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
# maybe a more elegant way exists to find all objects users ?
# update Gpencil modifier targets
for mod in ob_user.grease_pencil_modifiers:
if not hasattr(mod, 'layer'):
for mod in ob_user.modifier:
if not hasattr(mod, 'layer_filter'):
continue
if mod.layer == old:
mod.layer = new
if mod.layer_filter == old:
mod.layer_filter = new
"""
def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
@@ -93,7 +93,7 @@ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
prefs = get_addon_prefs()
sep = prefs.separator
name = layer.info
name = layer.name
pattern = pattern.replace('_', sep) # set separator
@@ -122,7 +122,7 @@ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
p4 = sep + suffix.upper().strip()
new = f'{p1}{p2}{p3}{p4}'
layer.info = new
layer.name = new
"""
## multi-prefix solution (Caps letters)
@@ -172,14 +172,14 @@ class GPTB_OT_layer_name_build(Operator):
def grp_toggle(l, mode='TOGGLE'):
'''take mode in (TOGGLE, GROUP, UNGROUP) '''
grp_item_id = ' - '
res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.info)
res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.name)
if not res and mode in ('TOGGLE', 'GROUP'):
# No gpr : add group prefix after stripping all space and dash
l.info = grp_item_id + l.info.lstrip(' -')
l.name = grp_item_id + l.name.lstrip(' -')
elif res and mode in ('TOGGLE', 'UNGROUP'):
# found : delete group prefix
l.info = res.group(2)
l.name = res.group(2)
class GPTB_OT_layer_group_toggle(Operator):
@@ -220,18 +220,18 @@ class GPTB_OT_layer_new_group(Operator):
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
res = re.search(PATTERN, act.info)
res = re.search(PATTERN, act.name)
if not res:
self.report({'ERROR'}, 'Could not create a group name, create a layer manually')
return {"CANCELLED"}
name = res.group('name').strip(' -')
if not name:
self.report({'ERROR'}, f'No name found in {act.info}')
self.report({'ERROR'}, f'No name found in {act.name}')
return {"CANCELLED"}
if name in [l.info.strip(' -') for l in gpl]:
self.report({'WARNING'}, f'Name already exists: {act.info}')
if name in [l.name.strip(' -') for l in gpl]:
self.report({'WARNING'}, f'Name already exists: {act.name}')
return {"FINISHED"}
grp_toggle(act, mode='GROUP')
@@ -261,9 +261,9 @@ def build_layers_targets_from_dopesheet(context):
if dopeset.show_only_selected:
pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
pool = [o for o in context.selected_objects if o.type == 'GREASEPENCIL']
else:
pool = [o for o in context.scene.objects if o.type == 'GPENCIL']
pool = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
if not dopeset.show_hidden:
pool = [o for o in pool if o.visible_get()]
@@ -272,7 +272,7 @@ def build_layers_targets_from_dopesheet(context):
# apply search filter
if dopeset.filter_text:
layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.info.lower()) ^ dopeset.use_filter_invert]
layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.name.lower()) ^ dopeset.use_filter_invert]
return layer_pool
@@ -292,7 +292,7 @@ class GPTB_OT_select_set_same_prefix(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
items=(
@@ -330,35 +330,35 @@ class GPTB_OT_select_set_same_prefix(Operator):
self.report({'ERROR'}, 'No active layer to base action')
return {"CANCELLED"}
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}')
res = re.search(PATTERN, act.info)
res = re.search(PATTERN, act.name)
if not res:
self.report({'ERROR'}, f'Error scanning {act.info}')
self.report({'ERROR'}, f'Error scanning {act.name}')
return {"CANCELLED"}
namespace = res.group('tag')
if not namespace:
self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}')
self.report({'WARNING'}, f'No prefix detected in {act.name} with separator {sep}')
return {"CANCELLED"}
if self.mode == 'SELECT':
## with split
# namespace = act.info.split(sep,1)[0]
# namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl]
# namespace = act.name.split(sep,1)[0]
# namespace_bool_list = [l.name.split(sep,1)[0] == namespace for l in gpl]
## with reg # only active
# namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl]
# namespace_bool_list = [l.name.split(sep,1)[0] + sep == namespace for l in gpl]
# gpl.foreach_set('select', namespace_bool_list)
## don't work Need Foreach set per gp
# for l in pool:
# l.select = l.info.split(sep,1)[0] + sep == namespace
# l.select = l.name.split(sep,1)[0] + sep == namespace
for gp, layers in gp_dic.items():
# check namespace + restrict selection to visible layers according to filters
# TODO : Should use the regex pattern to detect and compare r.group('tag')
namespace_bool_list = [(l in layers) and (l.info.split(sep,1)[0] + sep == namespace) for l in gp.layers]
namespace_bool_list = [(l in layers) and (l.name.split(sep,1)[0] + sep == namespace) for l in gp.layers]
gp.layers.foreach_set('select', namespace_bool_list)
elif self.mode == 'SET':
@@ -380,7 +380,7 @@ class GPTB_OT_select_set_same_color(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
items=(
@@ -415,7 +415,7 @@ class GPTB_OT_select_set_same_color(Operator):
self.report({'ERROR'}, 'No active layer to base action')
return {"CANCELLED"}
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}')
color = act.channel_color
if self.mode == 'SELECT':
## NEED FOREACH TO APPLY SELECT
@@ -426,7 +426,7 @@ class GPTB_OT_select_set_same_color(Operator):
# On multiple objects -- don't work, need foreach
# for l in pool:
# print(l.id_data.name, l.info, l.channel_color == act.channel_color)
# print(l.id_data.name, l.name, l.channel_color == act.channel_color)
# l.select = l.channel_color == act.channel_color
"""
@@ -463,38 +463,38 @@ def replace_layer_name(target, replacement, selected_only=True, prefix_only=True
gpl = bpy.context.object.data.layers
if selected_only:
lays = [l for l in gpl if l.select] # exclude : l.info != 'background'
lays = [l for l in gpl if l.select] # exclude : l.name != 'background'
else:
lays = [l for l in gpl] # exclude : if l.info != 'background'
lays = [l for l in gpl] # exclude : if l.name != 'background'
ct = 0
for l in lays:
old = l.info
old = l.name
if regex:
new = re.sub(target, replacement, l.info)
new = re.sub(target, replacement, l.name)
if old != new:
l.info = new
l.name = new
print('rename:', old, '-->', new)
ct += 1
continue
if prefix_only:
if not sep in l.info:
if not sep in l.name:
# only if separator exists
continue
splited = l.info.split(sep)
splited = l.name.split(sep)
prefix = splited[0]
new_prefix = prefix.replace(target, replacement)
if prefix != new_prefix:
splited[0] = new_prefix
l.info = sep.join(splited)
print('rename:', old, '-->', l.info)
l.name = sep.join(splited)
print('rename:', old, '-->', l.name)
ct += 1
else:
new = l.info.replace(target, replacement)
new = l.name.replace(target, replacement)
if old != new:
l.info = new
l.name = new
print('rename:', old, '-->', new)
ct += 1
return ct
@@ -507,7 +507,7 @@ class GPTB_OT_rename_gp_layer(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
@@ -548,7 +548,7 @@ class GPTB_OT_rename_gp_layer(Operator):
## --- UI layer panel---
def layer_name_builder_ui(self, context):
'''appended to DATA_PT_gpencil_layers'''
'''appended to DATA_PT_grease_pencil_layers'''
prefs = get_addon_prefs()
if not prefs.show_prefix_buttons:
@@ -557,7 +557,7 @@ def layer_name_builder_ui(self, context):
return
layout = self.layout
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
# layout.separator()
col = layout.column()
@@ -633,7 +633,7 @@ def gpencil_dopesheet_header(self, context):
'''to append in DOPESHEET_HT_header'''
layout = self.layout
st = context.space_data
if st.mode != 'GPENCIL':
if st.mode != 'GREASEPENCIL':
return
row = layout.row(align=True)
@@ -653,7 +653,7 @@ def gpencil_layer_dropdown_menu(self, context):
def obj_layer_name_callback():
'''assign layer name properties so user an tweak it'''
ob = bpy.context.object
if not ob or ob.type != 'GPENCIL':
if not ob or ob.type != 'GREASEPENCIL':
return
if not ob.data.layers.active:
return
@@ -663,7 +663,7 @@ def obj_layer_name_callback():
for l in ob.data.layers:
l.select = l == ob.data.layers.active
res = re.search(PATTERN, ob.data.layers.active.info.strip())
res = re.search(PATTERN, ob.data.layers.active.name.strip())
if not res:
return
if not res.group('name'):
@@ -736,7 +736,7 @@ class GPTB_OT_add_gp_layer_with_rename(Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
add_layer(context)
@@ -750,7 +750,7 @@ class GPTB_OT_add_gp_layer(Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
add_layer(context)
@@ -776,7 +776,7 @@ def register_keymaps():
##---# F2 rename calls
## Direct rename active layer in Paint mode
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY")
kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
kmi.properties.name = 'GPTB_PT_layer_name_ui'
kmi.properties.keep_open = False
@@ -817,7 +817,7 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder_ui)
bpy.types.DATA_PT_grease_pencil_layers.prepend(layer_name_builder_ui)
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu)
bpy.app.handlers.load_post.append(subscribe_layer_change_handler)
@@ -831,7 +831,7 @@ def unregister():
bpy.app.handlers.load_post.remove(subscribe_layer_change_handler)
bpy.types.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu)
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder_ui)
bpy.types.DATA_PT_grease_pencil_layers.remove(layer_name_builder_ui)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+1 -1
View File
@@ -127,7 +127,7 @@ addon_keymaps = []
def register_keymap():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY")
kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_UP', value='PRESS')
kmi.properties.direction = 'UP'
+12 -12
View File
@@ -14,7 +14,7 @@ class GP_OT_pick_closest_layer(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
stroke_filter : bpy.props.EnumProperty(name='Target',
default='STROKE',
@@ -47,7 +47,7 @@ class GP_OT_pick_closest_layer(Operator):
def draw(self, context):
layout = self.layout
if context.object.data.layers.active:
layout.label(text=f'Layer: {context.object.data.layers.active.info}')
layout.label(text=f'Layer: {context.object.data.layers.active.name}')
layout.prop(self, 'stroke_filter')
def modal(self, context, event):
@@ -74,31 +74,31 @@ class GP_OT_pick_closest_layer(Operator):
self.inv_mat = self.ob.matrix_world.inverted()
self.point_pair = []
if gp.use_multiedit:
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for layer_id, l in enumerate(gp.layers):
if l.hide:# l.lock or
if l.hide: # l.lock or
continue
for f in l.frames:
if not f.select:
continue
for s in f.strokes:
for s in f.drawing.strokes:
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
continue
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
continue
self.point_pair += [(Vector((*location_to_region(mat @ p.co), 0)), layer_id) for p in s.points]
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer_id) for p in s.points]
else:
# [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
# [s for l in gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes]
for layer_id, l in enumerate(gp.layers):
if l.hide or not l.active_frame:# l.lock or
if l.hide or not l.current_frame(): # l.lock or
continue
for s in l.active_frame.strokes:
for s in l.current_frame().drawing.strokes:
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
continue
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
continue
self.point_pair += [(Vector((*location_to_region(mat @ p.co), 0)), layer_id) for p in s.points]
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer_id) for p in s.points]
if not self.point_pair:
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
@@ -117,7 +117,7 @@ class GP_OT_pick_closest_layer(Operator):
## debug show trigger time
# print(f'Trigger time {time() - self.t0:.3f}')
# print(f'Search time {time() - t1:.3f}')
self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.info}')
self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.name}')
return {'FINISHED'}
@@ -125,7 +125,7 @@ class GP_OT_pick_closest_layer(Operator):
addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW')
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new(
# name="",
+183
View File
@@ -0,0 +1,183 @@
import bpy
from bpy.types import Operator
import mathutils
from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils
from . import utils
# def get_layer_list(self, context):
# '''return (identifier, name, description) of enum content'''
# if not context:
# return [('None', 'None','None')]
# if not context.object:
# return [('None', 'None','None')]
# return [(l.name, l.name, '') for l in context.object.data.layers] # if l != context.object.data.layers.active
## in Class
# bl_property = "layers_enum"
# layers_enum : bpy.props.EnumProperty(
# name="Send Material To Layer",
# description="Send active material to layer",
# items=get_layer_list,
# options={'HIDDEN'},
# )
class GPTB_OT_move_material_to_layer(Operator) :
bl_idname = "gp.move_material_to_layer"
bl_label = 'Move Material To Layer'
bl_description = 'Move active material to an existing or new layer'
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
layer_name : bpy.props.StringProperty(
name='Layer Name', default='', options={'SKIP_SAVE'})
copy : bpy.props.BoolProperty(
name='Copy to layer', default=False,
description='Copy strokes to layer instead of moving',
options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'
def invoke(self, context, event):
if self.layer_name:
return self.execute(context)
if not len(context.object.data.layers):
self.report({'WARNING'}, 'No layers on current GP object')
return {'CANCELLED'}
mat = context.object.data.materials[context.object.active_material_index]
self.mat_name = mat.name
# wm.invoke_search_popup(self)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
# layout.operator_context = "INVOKE_DEFAULT"
layout.prop(self, 'copy', text='Copy Strokes')
action_label = 'Copy' if self.copy else 'Move'
layout.label(text=f'{action_label} material "{self.mat_name}" to layer:', icon='MATERIAL')
col = layout.column()
col.prop(self, 'layer_name', text='', icon='ADD')
# if self.layer_name:
# col.label(text='Ok/Enter to create new layer', icon='INFO')
col.separator()
for l in reversed(context.object.data.layers):
icon = 'GREASEPENCIL' if l == context.object.data.layers.active else 'BLANK1'
row = col.row()
row.alignment = 'LEFT'
op = col.operator('gp.move_material_to_layer', text=l.name, icon=icon, emboss=False)
op.layer_name = l.name
op.copy = self.copy
def execute(self, context):
if not self.layer_name:
print('Out')
return {'CANCELLED'}
## Active + selection
pool = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
if not context.object in pool:
pool.append(context.object)
mat = context.object.data.materials[context.object.active_material_index]
print(f'Moving strokes using material "{mat.name}" on {len(pool)} object(s)')
# import time
# t = time.time() # Dbg
total = 0
oct = 0
for ob in pool:
mat_index = next((i for i, ms in enumerate(ob.material_slots) if ms.material and ms.material == mat), None)
if mat_index is None:
print(f'/!\ {ob.name} has no Material {mat.name} in stack')
continue
gpl = ob.data.layers
if not (target_layer := gpl.get(self.layer_name)):
target_layer = gpl.new(self.layer_name)
## List existing frames
key_dict = {f.frame_number : f for f in target_layer.frames}
### Move Strokes to a new key (or existing key if comming for yet another layer)
fct = 0
sct = 0
for l in gpl:
if l == target_layer:
## ! infinite loop if target layer is included
continue
for f in l.frames:
## skip if no stroke has active material
if not next((s for s in f.drawing.strokes if s.material_index == mat_index), None):
continue
## Get/Create a destination frame and keep a reference to it
if not (dest_key := key_dict.get(f.frame_number)):
dest_key = target_layer.frames.new(f.frame_number)
key_dict[dest_key.frame_number] = dest_key
print(f'{ob.name} : frame {f.frame_number}')
## Replicate strokes in dest_keys
stroke_to_delete = []
for s in f.drawing.strokes:
if s.material_index == mat_index:
utils.copy_stroke_to_frame(s, dest_key)
stroke_to_delete.append(s)
## Debug
# if time.time() - t > 10:
# print('TIMEOUT')
# return {'CANCELLED'}
sct += len(stroke_to_delete)
# print('Removing frames') # Dbg
## Remove from source frame (f)
if not self.copy:
for s in reversed(stroke_to_delete):
f.drawing.strokes.remove(s)
## ? Remove frame if layer is empty ? -> probably not, will show previous frame
fct += 1
l.frames.update()
if fct:
oct += 1
print(f'{ob.name}: Moved {fct} frames -> {sct} Strokes') # Dbg
total += fct
report_type = 'INFO' if total else 'WARNING'
if self.copy:
self.report({report_type}, f'Copied {total} frames accross {oct} object(s)')
else:
self.report({report_type}, f'Moved {total} frames accross {oct} object(s)')
return {'FINISHED'}
# def menu_duplicate_and_send_to_layer(self, context):
# if context.space_data.ui_mode == 'GREASEPENCIL':
# self.layout.operator_context = 'INVOKE_REGION_WIN'
# self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
# self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
classes = (
GPTB_OT_move_material_to_layer,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+23 -23
View File
@@ -33,20 +33,20 @@ class GP_OT_pick_closest_material(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
def filter_stroke(self, context):
# get stroke under mouse using kdtree
point_pair = [(p.co, s) for s in self.stroke_list for p in s.points] # local space
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
kd = mathutils.kdtree.KDTree(len(point_pair))
for i, pair in enumerate(point_pair):
kd.insert(pair[0], i)
kd.balance()
## Get 3D coordinate on drawing plane according to mouse 2d.co on flat 2d drawing
## Get 3D coordinate on drawing plane according to mouse 2d.position on flat 2d drawing
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
if not hit:
@@ -62,7 +62,7 @@ class GP_OT_pick_closest_material(Operator):
## find point index in stroke
self.idx = None
for i, p in enumerate(s.points):
if p.co == co:
if p.position == co:
self.idx = i
break
@@ -77,22 +77,22 @@ class GP_OT_pick_closest_material(Operator):
self.stroke_list = []
self.inv_mat = self.ob.matrix_world.inverted()
if self.gp.use_multiedit:
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for l in self.gp.layers:
if l.hide:# l.lock or
continue
for f in l.frames:
if not f.select:
continue
for s in f.strokes:
for s in f.drawing.strokes:
self.stroke_list.append(s)
else:
# [s for l in self.gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
# [s for l in self.gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes]
for l in self.gp.layers:
if l.hide or not l.active_frame:# l.lock or
if l.hide or not l.current_frame():# l.lock or
continue
for s in l.active_frame.strokes:
for s in l.current_frame().drawing.strokes:
self.stroke_list.append(s)
if self.fill_only:
@@ -116,8 +116,8 @@ class GP_OT_pick_closest_material(Operator):
self.report({'WARNING'}, 'No coord found')
return {'CANCELLED'}
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].co
self.init_pos = [p.co.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].position
self.init_pos = [p.position.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
## directly use world position ?
# self.pos_world = [self.ob.matrix_world @ co for co in self.init_pos]
self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
@@ -144,7 +144,7 @@ class GP_OT_pick_closest_material(Operator):
# if event.type in {'RIGHTMOUSE', 'ESC'}:
# # for i, p in enumerate(self.stroke.points): # reset position
# # self.stroke.points[i].co = self.init_pos[i]
# # self.stroke.points[i].position = self.init_pos[i]
# context.area.tag_redraw()
# return {'CANCELLED'}
@@ -159,7 +159,7 @@ class GP_OT_pick_closest_material(Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
# fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
stroke_filter : bpy.props.EnumProperty(default='FILL',
@@ -172,7 +172,7 @@ class GP_OT_pick_closest_material(Operator):
def filter_stroke(self, context):
# get stroke under mouse using kdtree
point_pair = [(p.co, s) for s in self.stroke_list for p in s.points] # local space
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
kd = mathutils.kdtree.KDTree(len(point_pair))
for i, pair in enumerate(point_pair):
@@ -195,7 +195,7 @@ class GP_OT_pick_closest_material(Operator):
## find point index in stroke
self.idx = None
for i, p in enumerate(s.points):
if p.co == co:
if p.position == co:
self.idx = i
break
@@ -233,22 +233,22 @@ class GP_OT_pick_closest_material(Operator):
self.stroke_list = []
self.inv_mat = self.ob.matrix_world.inverted()
if gp.use_multiedit:
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for l in gp.layers:
if l.hide:# l.lock or
continue
for f in l.frames:
if not f.select:
continue
for s in f.strokes:
for s in f.drawing.strokes:
self.stroke_list.append(s)
else:
# [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
# [s for l in gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes]
for l in gp.layers:
if l.hide or not l.active_frame:# l.lock or
if l.hide or not l.current_frame():# l.lock or
continue
for s in l.active_frame.strokes:
for s in l.current_frame().drawing.strokes:
self.stroke_list.append(s)
if self.stroke_filter == 'FILL':
@@ -274,8 +274,8 @@ class GP_OT_pick_closest_material(Operator):
self.report({'WARNING'}, 'No coord found')
return {'CANCELLED'}
# self.depth = self.ob.matrix_world @ stroke.points[self.idx].co
# self.init_pos = [p.co.copy() for p in stroke.points] # need a copy otherwise vector is updated
# self.depth = self.ob.matrix_world @ stroke.points[self.idx].position
# self.init_pos = [p.position.copy() for p in stroke.points] # need a copy otherwise vector is updated
# self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
# self.plen = len(stroke.points)
@@ -291,7 +291,7 @@ addon_keymaps = []
def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Draw brush)", space_type = "EMPTY", region_type='WINDOW')
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW')
# km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Fill)", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new(
# name="",
+11 -11
View File
@@ -42,7 +42,7 @@ class GPTB_OT_load_default_palette(bpy.types.Operator):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
# Start Clean (delete unuesed sh*t)
@@ -82,7 +82,7 @@ class GPTB_OT_load_palette(bpy.types.Operator, ImportHelper):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
filename_ext = '.json'
@@ -110,7 +110,7 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
@@ -169,7 +169,7 @@ def load_blend_palette(context, filepath):
print(f'-- import palette from : {filepath} --')
for ob in context.selected_objects:
if ob.type != 'GPENCIL':
if ob.type != 'GREASEPENCIL':
print(f'{ob.name} not a GP object')
continue
@@ -224,7 +224,7 @@ class GPTB_OT_load_blend_palette(bpy.types.Operator, ImportHelper):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
filename_ext = '.blend'
@@ -252,7 +252,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def execute(self, context):
ob = context.object
@@ -260,7 +260,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
self.report({'ERROR'}, 'No materials to transfer')
return {"CANCELLED"}
selection = [o for o in context.selected_objects if o.type == 'GPENCIL' and o != ob]
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL' and o != ob]
if not selection:
self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials')
@@ -313,7 +313,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
def invoke(self, context, event):
self.ob = context.object
@@ -354,7 +354,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
import re
diff_ct = 0
todel = []
if ob.type != 'GPENCIL':
if ob.type != 'GREASEPENCIL':
return
if not hasattr(ob, 'material_slots'):
return
@@ -410,7 +410,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
# iterate in all strokes and replace with new_mat_id
for l in ob.data.layers:
for f in l.frames:
for s in f.strokes:
for s in f.drawing.strokes:
if s.material_index == i:
s.material_index = new_mat_id
@@ -427,7 +427,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
# if self.skip_binded_empty_slots:
# for l in ob.data.layers:
# for f in l.frames:
# for s in f.strokes:
# for s in f.drawing.strokes:
# if s.material_index == i:
# is_binded = True
# break
+2 -2
View File
@@ -45,7 +45,7 @@ class GPTB_OT_import_obj_palette(Operator):
def execute(self, context):
## get targets
selection = [o for o in context.selected_objects if o.type == 'GPENCIL']
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL']
if not selection:
self.report({'ERROR'}, 'Need to have at least one GP object selected in scene')
return {"CANCELLED"}
@@ -98,7 +98,7 @@ class GPTB_OT_import_obj_palette(Operator):
return {"CANCELLED"}
for i in range(len(linked_objs))[::-1]: # reversed(range(len(l))) / range(len(l))[::-1]
if linked_objs[i].type != 'GPENCIL':
if linked_objs[i].type != 'GREASEPENCIL':
print(f'{linked_objs[i].name} type is "{linked_objs[i].type}"')
bpy.data.objects.remove(linked_objs.pop(i))
+5 -5
View File
@@ -74,7 +74,7 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
# namespace_order
namespaces=[]
for l in gpl:
ns= l.info.lower().split(separator, 1)[0]
ns= l.name.lower().split(separator, 1)[0]
if ns not in namespaces:
namespaces.append(ns)
@@ -88,14 +88,14 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
### step from 0.1 to 0.9
for i, l in enumerate(gpl):
if l.info.lower() not in ('background',):
if l.name.lower() not in ('background',):
print()
print('>', l.info)
ns= l.info.lower().split(separator, 1)[0]#get namespace from separator
print('>', l.name)
ns= l.name.lower().split(separator, 1)[0]#get namespace from separator
print("namespace", ns)#Dbg
if context.scene.gptoolprops.autotint_namespace:
h = get_hue_by_name(ns, hue_offset)#l.info == individuels
h = get_hue_by_name(ns, hue_offset)#l.name == individuels
else:
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)
+75 -53
View File
@@ -15,6 +15,35 @@ def get_scale_matrix(scale):
matscale = matscale_x @ matscale_y @ matscale_z
return matscale
'''
## Old reproject method using Operators:
omode = bpy.context.mode
if all_strokes:
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
for l in obj.data.layers:
l.hide = False
l.lock = False
l.lock_frame = False
bpy.ops.object.mode_set(mode='EDIT')
for fnum in frame_list:
bpy.context.scene.frame_current = fnum
bpy.ops.gpencil.select_all(action='SELECT')
bpy.ops.gpencil.reproject(type=proj_type) # 'INVOKE_DEFAULT'
bpy.ops.gpencil.select_all(action='DESELECT')
# restore
if all_strokes:
for layer, hide, lock, lock_frame in layers_state:
layer.hide = hide
layer.lock = lock
layer.lock_frame = lock_frame
bpy.ops.object.mode_set(mode=omode)
'''
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject - ops method
:all_stroke: affect hided, locked layers
@@ -25,7 +54,7 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
plan_co, plane_no = utils.get_gp_draw_plane(obj, orient=proj_type)
frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)]
frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
frame_list = list(set(frame_list))
frame_list.sort()
@@ -49,7 +78,7 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
f = next((f for f in l.frames if f.frame_number == i), None)
if f is None:
continue
for s in f.strokes:
for s in f.drawing.strokes:
## Batch matrix apply (Here is slower than list comprehension).
# nb_points = len(s.points)
# coords = np.empty(nb_points * 3, dtype='float64')
@@ -57,13 +86,13 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
# world_co_3d = utils.matrix_transform(coords.reshape((nb_points, 3)), matrix)
## list comprehension method
world_co_3d = [obj.matrix_world @ p.co for p in s.points]
world_co_3d = [obj.matrix_world @ p.position for p in s.points]
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d]
## Basic method (Slower than foreach_set)
# for i, p in enumerate(s.points):
# p.co = obj.matrix_world.inverted() @ new_world_co_3d[i]
# p.position = obj.matrix_world.inverted() @ new_world_co_3d[i]
## Ravel new coordinate on the fly
new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
@@ -72,40 +101,13 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
# new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
s.points.foreach_set('co', new_local_coords)
bpy.context.area.tag_redraw()
'''
## Old method using Operators:
omode = bpy.context.mode
if all_strokes:
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
for l in obj.data.layers:
l.hide = False
l.lock = False
l.lock_frame = False
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
for fnum in frame_list:
bpy.context.scene.frame_current = fnum
bpy.ops.gpencil.select_all(action='SELECT')
bpy.ops.gpencil.reproject(type=proj_type) # 'INVOKE_DEFAULT'
bpy.ops.gpencil.select_all(action='DESELECT')
# restore
if all_strokes:
for layer, hide, lock, lock_frame in layers_state:
layer.hide = hide
layer.lock = lock
layer.lock_frame = lock_frame
bpy.ops.object.mode_set(mode=omode)
'''
if restore_frame:
bpy.context.scene.frame_current = oframe
## Update the layer and redraw all viewports
obj.data.layers.update()
utils.refresh_areas()
def align_global(reproject=True, ref=None, all_strokes=True):
@@ -137,9 +139,9 @@ def align_global(reproject=True, ref=None, all_strokes=True):
# world_coords = []
for l in o.data.layers:
for f in l.frames:
for s in f.strokes:
for s in f.drawing.strokes:
## foreach
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
# print('coords: ', coords)
# print([co for v in coords for co in v])
s.points.foreach_set('co', [co for v in coords for co in v])
@@ -150,11 +152,11 @@ def align_global(reproject=True, ref=None, all_strokes=True):
# for p in s.points:
## GOOD :
# world_co = mat @ p.co
# p.co = new_mat.inverted() @ world_co
# world_co = mat @ p.position
# p.position = new_mat.inverted() @ world_co
## GOOD :
# p.co = p.co @ mat.inverted() @ new_mat
# p.position = p.position @ mat.inverted() @ new_mat
if o.parent:
o.matrix_world = new_mat
@@ -214,9 +216,9 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
scale_mat = get_scale_matrix(o_scale)
new_mat = loc_mat @ rot_mat @ scale_mat
for s in f.strokes:
for s in f.drawing.strokes:
## foreach
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
# print('coords: ', coords)
# print([co for v in coords for co in v])
s.points.foreach_set('co', [co for v in coords for co in v])
@@ -265,7 +267,7 @@ class GPTB_OT_realign(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
reproject : bpy.props.BoolProperty(
name='Reproject', default=True,
@@ -281,7 +283,7 @@ class GPTB_OT_realign(bpy.types.Operator):
## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes
def invoke(self, context, event):
if context.object.data.use_multiedit:
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
self.report({'ERROR'}, 'Does not work in Multiframe mode')
return {"CANCELLED"}
@@ -363,25 +365,25 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
return context.object and context.object.type == 'GREASEPENCIL'
all_strokes : bpy.props.BoolProperty(
name='All Strokes', default=True,
description='Hided and locked layer will also be reprojected')
type: bpy.props.EnumProperty(name='Type',
type : bpy.props.EnumProperty(name='Type',
items=(('CURRENT', "Current", ""),
('FRONT', "Front", ""),
('SIDE', "Side", ""),
('TOP', "Top", ""),
('VIEW', "View", ""),
('SURFACE', "Surface", ""),
('CURSOR', "Cursor", ""),
# ('SURFACE', "Surface", ""),
),
default='CURRENT')
def invoke(self, context, event):
if context.object.data.use_multiedit:
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
self.report({'ERROR'}, 'Does not work in Multi-edit')
return {"CANCELLED"}
return context.window_manager.invoke_props_dialog(self)
@@ -390,9 +392,29 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
layout = self.layout
if not context.region_data.view_perspective == 'CAMERA':
# layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
layout.label(text='Reprojection is made from camera, not current view', icon='ERROR')
layout.label(text='Reprojection is made from camera', icon='ERROR')
layout.prop(self, "all_strokes")
layout.prop(self, "type")
layout.prop(self, "type", text='Project Axis')
## Hint show axis
if self.type == 'CURRENT':
## Show as prop
# row = layout.row()
# row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='Current', icon='INFO')
# row.enabled = False
orient = {
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], # AXIS_Y
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'], # AXIS_X
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'], # AXIS_Z
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
}
box = layout.box()
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
box.label(text=orient[axis][0], icon=orient[axis][1])
def execute(self, context):
@@ -410,12 +432,12 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
### -- MENU ENTRY --
def reproject_clean_menu(self, context):
if context.mode == 'EDIT_GPENCIL':
if context.mode == 'EDIT_GREASE_PENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
def reproject_context_menu(self, context):
if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
@@ -428,12 +450,12 @@ def register():
for cl in classes:
bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_gpencil_edit_context_menu.append(reproject_context_menu)
bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.append(reproject_context_menu)
bpy.types.GPENCIL_MT_cleanup.append(reproject_clean_menu)
def unregister():
bpy.types.GPENCIL_MT_cleanup.remove(reproject_clean_menu)
bpy.types.VIEW3D_MT_gpencil_edit_context_menu.remove(reproject_context_menu)
bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.remove(reproject_context_menu)
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
+3 -1
View File
@@ -2,7 +2,9 @@
Blender addon - Various tool to help with grease pencil in animation productions.
**[Download latest](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
**[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Download for Blender 4.2 and below](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/v3.3.0.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+3 -1
View File
@@ -2,7 +2,9 @@
Blender addon - Boîte à outils de grease pencil pour la production d'animation.
**[Télécharger la dernière version](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
**[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Télécharger pour Blender 4.2 ou inférieure](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/v3.3.0.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+1 -1
View File
@@ -7,7 +7,7 @@ from .utils import get_addon_prefs
class GPTB_WT_eraser(WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'PAINT_GPENCIL'
bl_context_mode = 'PAINT_GREASE_PENCIL'
# The prefix of the idname should be your add-on name.
bl_idname = "gp.eraser_tool"
+29 -14
View File
@@ -20,7 +20,7 @@ class GPTB_PT_dataprop_panel(Panel):
# bl_category = "Tool"
# bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class.
bl_label = "Pseudo color"# title
bl_parent_id = "DATA_PT_gpencil_layers"#subpanel of this ID
bl_parent_id = "DATA_PT_grease_pencil_layers"#subpanel of this ID
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
@@ -58,6 +58,11 @@ class GPTB_PT_sidebar_panel(Panel):
## flip X cam
# layout.label(text='! Flipped !')
row = col.row(align=True)
row.prop(context.scene.tool_settings, 'gpencil_stroke_placement_view3d', text='')
row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='')
row = col.row(align=True)
row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
@@ -139,7 +144,7 @@ class GPTB_PT_sidebar_panel(Panel):
col.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
# col.prop(context.space_data.overlay, 'use_gpencil_onion_skin') # not often used
if context.object and context.object.type == 'GPENCIL':
if context.object and context.object.type == 'GREASEPENCIL':
# col.prop(context.object.data, 'use_autolock_layers') # not often used
col.prop(context.object, 'show_in_front') # text='In Front'
@@ -186,23 +191,23 @@ class GPTB_PT_anim_manager(Panel):
# import time
# t0 = time.perf_counter()
# objs = [o for o in context.scene.objects if o.type not in ('GPENCIL', 'CAMERA')]
# gps = [o for o in context.scene.objects if o.type == 'GPENCIL']
# objs = [o for o in context.scene.objects if o.type not in ('GREASEPENCIL', 'CAMERA')]
# gps = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
# cams = [o for o in context.scene.objects if o.type == 'CAMERA']
objs = []
gps = []
cams = []
for o in context.scene.objects:
if o.type not in ('GPENCIL', 'CAMERA'):
if o.type not in ('GREASEPENCIL', 'CAMERA'):
objs.append(o)
elif o.type == 'GPENCIL':
elif o.type == 'GREASEPENCIL':
gps.append(o)
elif o.type == 'CAMERA':
cams.append(o)
# print(f'{time.perf_counter() - t0:.8f}s')
return {'OBJECT': objs, 'GPENCIL': gps, 'CAMERA': cams}
return {'OBJECT': objs, 'GREASEPENCIL': gps, 'CAMERA': cams}
def draw(self, context):
@@ -216,7 +221,7 @@ class GPTB_PT_anim_manager(Panel):
col.operator('gp.list_disabled_anims')
## Show Enable / Disable anims
for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GPENCIL')]:
for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GREASEPENCIL')]:
on_icon, off_icon = anim_status(obj_types[cat_type])
subcol = col.column()
@@ -237,7 +242,7 @@ class GPTB_PT_anim_manager(Panel):
row = subcol.row(align=True)
row.label(text='Gp modifiers:')
on_icon, off_icon = gp_modifier_status(obj_types['GPENCIL'])
on_icon, off_icon = gp_modifier_status(obj_types['GREASEPENCIL'])
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
row.operator('gp.toggle_hide_gp_modifier', text='ON', icon=on_icon).show = True
row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False
@@ -420,7 +425,7 @@ def palette_manager_menu(self, context):
"""Palette menu to append in existing menu"""
# GPENCIL_MT_material_context_menu
layout = self.layout
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
layout.separator()
prefs = get_addon_prefs()
@@ -432,6 +437,8 @@ def palette_manager_menu(self, context):
layout.separator()
layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path
layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path
layout.separator()
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
def expose_use_channel_color_pref(self, context):
@@ -651,7 +658,7 @@ class GPTB_PT_tools_grease_pencil_interpolate(Panel):
# settings = context.tool_settings.gpencil_interpolate # old 2.92 global settings
## access active tool settings
# settings = context.workspace.tools[0].operator_properties('gpencil.interpolate')
settings = context.workspace.tools.from_space_view3d_mode('PAINT_GPENCIL').operator_properties('gpencil.interpolate')
settings = context.workspace.tools.from_space_view3d_mode('PAINT_GREASE_PENCIL').operator_properties('gpencil.interpolate')
## custom curve access (still in gp interpolate tools)
interpolate_settings = context.tool_settings.gpencil_interpolate
@@ -727,7 +734,7 @@ def interpolate_header_ui(self, context):
layout = self.layout
obj = context.active_object
if obj and obj.type == 'GPENCIL' and context.gpencil_data:
if obj and obj.type == 'GREASEPENCIL' and context.gpencil_data:
gpd = context.gpencil_data
else:
return
@@ -761,7 +768,10 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref)
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
# if bpy.app.version >= (3,0,0):
@@ -770,8 +780,13 @@ def register():
def unregister():
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref)
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# if bpy.app.version >= (3,0,0):
# bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui)
+6 -4
View File
@@ -4,12 +4,12 @@ bl_info = {
"name": "GP toolbox",
"description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux",
"version": (2, 5, 0),
"blender": (3, 0, 0),
"version": (4, 0, 0),
"blender": (4, 3, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "",
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues",
"category": "3D View",
}
@@ -46,6 +46,7 @@ from . import OP_git_update
from . import OP_layer_namespace
from . import OP_pseudo_tint
from . import OP_follow_curve
from . import OP_material_move_to_layer
# from . import OP_eraser_brush
# from . import TOOL_eraser_brush
from . import handler_draw_cam
@@ -803,6 +804,7 @@ addon_modules = (
OP_layer_picker,
OP_layer_nav,
OP_follow_curve,
OP_material_move_to_layer,
# OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam,
+1 -21
View File
@@ -99,7 +99,7 @@ def gp_stroke_angle_split (frame, strokes, angle):
splitted_loops = bm_angle_split(bm,angle)
frame.strokes.remove(stroke_info['stroke'])
frame.drawing.strokes.remove(stroke_info['stroke'])
for loop in splitted_loops :
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
@@ -165,7 +165,6 @@ def randomise_points(mat, points, attr, strength) :
setattr(point,attr,value+random*strength)
def zoom_to_object(cam, resolution, box, margin=0.01) :
min_x= box[0]
max_x= box[1]
@@ -216,25 +215,6 @@ def zoom_to_object(cam, resolution, box, margin=0.01) :
#print(matrix,resolution)
return modelview_matrix,projection_matrix,frame,resolution
def set_viewport_matrix(width, height, mat):
from bgl import glViewport,glMatrixMode,GL_PROJECTION,glLoadMatrixf,Buffer,GL_FLOAT,glMatrixMode,GL_MODELVIEW,glLoadIdentity
glViewport(0,0,width,height)
#glLoadIdentity()
glMatrixMode(GL_PROJECTION)
projection = [mat[j][i] for i in range(4) for j in range(4)]
glLoadMatrixf(Buffer(GL_FLOAT, 16, projection))
#glMatrixMode( GL_MODELVIEW )
#glLoadIdentity()
# get object info
def get_object_info(mesh_groups, order_list = []) :
scene = bpy.context.scene
+22 -9
View File
@@ -1,6 +1,5 @@
import bpy
import gpu
import bgl
# import blf
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -30,6 +29,20 @@ def view3d_camera_border_2d(context, cam):
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
return frame_px
def vertices_to_line_loop(v_list, closed=True) -> list:
'''Take a sequence of vertices
return a position lists of segments to create a line loop passing in all points
the result is usable with gpu_shader 'LINES'
ex: vlist = [a,b,c] -> closed=True return [a,b,b,c,c,a], closed=False return [a,b,b,c]
'''
loop = []
for i in range(len(v_list) - 1):
loop += [v_list[i], v_list[i + 1]]
if closed:
# Add segment between last and first to close loop
loop += [v_list[-1], v_list[0]]
return loop
def draw_cam_frame_callback_2d():
context = bpy.context
if context.region_data.view_perspective != 'CAMERA':
@@ -41,11 +54,12 @@ def draw_cam_frame_callback_2d():
if not main_cam:
return
bgl.glEnable(bgl.GL_BLEND)
gpu.state.blend_set('ALPHA')
frame_point = view3d_camera_border_2d(
context, context.scene.camera.parent)
shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') # POLYLINE_FLAT_COLOR
# gpu.shader.from_builtin('2D_UNIFORM_COLOR')
if context.scene.gptoolprops.drawcam_passepartout:
### PASSEPARTOUT
@@ -109,8 +123,8 @@ def draw_cam_frame_callback_2d():
### Camera framing trace over
bgl.glLineWidth(1)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
gpu.state.line_width_set(1.0)
# bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth
"""
## need to accurately detect viewport background color (difficult)
@@ -135,15 +149,14 @@ def draw_cam_frame_callback_2d():
frame_color = (0.0, 0.0, 0.25, 1.0)
screen_framing = batch_for_shader(
shader_2d, 'LINE_LOOP', {"pos": frame_point})
shader_2d, 'LINES', {"pos": vertices_to_line_loop(frame_point)})
shader_2d.bind()
shader_2d.uniform_float("color", frame_color)
screen_framing.draw(shader_2d)
# bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_LINE_SMOOTH)
bgl.glDisable(bgl.GL_BLEND)
# bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth
gpu.state.blend_set('NONE')
draw_handle = None
+2 -2
View File
@@ -21,12 +21,12 @@ def update_layer_name(self, context):
if not self.layer_name:
# never replace by nothing (since there should be prefix/suffix)
return
if not context.object or context.object.type != 'GPENCIL':
if not context.object or context.object.type != 'GREASEPENCIL':
return
if not context.object.data.layers.active:
return
layer_name_build(context.object.data.layers.active, desc=self.layer_name)
# context.object.data.layers.active.info = self.layer_name
# context.object.data.layers.active.name = self.layer_name
class GP_PG_FixSettings(PropertyGroup):
+106 -84
View File
@@ -2,12 +2,13 @@ import bpy, os
import numpy as np
import bmesh
import mathutils
from mathutils import Vector
import math
from math import sqrt
from sys import platform
import subprocess
from math import sqrt
from mathutils import Vector
from sys import platform
""" def get_gp_parent(layer) :
@@ -28,9 +29,9 @@ def get_matrix(ob) :
return ob.matrix_world.copy()
def set_matrix(gp_frame,mat):
for stroke in gp_frame.strokes :
for stroke in gp_frame.drawing.strokes :
for point in stroke.points :
point.co = mat @ point.co
point.position = mat @ point.position
# get view vector location (the 2 methods work fine)
def get_view_origin_position():
@@ -159,7 +160,7 @@ def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0):
draw basic stroke by passing list of point 3D coordinate
the frame to draw on and optional width parameter (default = 2)
'''
stroke = frame.strokes.new()
stroke = frame.drawing.strokes.new()
stroke.line_width = width
stroke.display_mode = '3DSPACE'
stroke.material_index = mat_id
@@ -172,12 +173,12 @@ def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0):
# for i, pt in enumerate(pts):
# stroke.points.add()
# dest_point = stroke.points[i]
# dest_point.co = pt
# dest_point.position = pt
return stroke
## OLD - need update
def draw_gp_stroke(loop_info, frame, palette, width = 2) :
stroke = frame.strokes.new(palette)
stroke = frame.drawing.strokes.new(palette)
stroke.line_width = width
stroke.display_mode = '3DSPACE'# old -> draw_mode
@@ -263,55 +264,6 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
### GP funcs
# -----------------
""" V1
def get_gp_draw_plane(obj=None):
''' return tuple with plane coordinate and normal
of the curent drawing accordign to geometry'''
context = bpy.context
settings = context.scene.tool_settings
orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
if obj:
mat = obj.matrix_world
else:
mat = context.object.matrix_world if context.object else None
# -> placement
if loc == "CURSOR":
plane_co = context.scene.cursor.location
else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE')
if not context.object:
plane_co = None
else:
plane_co = context.object.matrix_world.to_translation()# context.object.location
# -> orientation
if orient == 'VIEW':
#only depth is important, no need to get view vector
plane_no = None
elif orient == 'AXIS_Y':#front (X-Z)
plane_no = Vector((0,1,0))
plane_no.rotate(mat)
elif orient == 'AXIS_X':#side (Y-Z)
plane_no = Vector((1,0,0))
plane_no.rotate(mat)
elif orient == 'AXIS_Z':#top (X-Y)
plane_no = Vector((0,0,1))
plane_no.rotate(mat)
elif orient == 'CURSOR':
plane_no = Vector((0,0,1))
plane_no.rotate(context.scene.cursor.matrix)
return plane_co, plane_no
"""
## V2
def get_gp_draw_plane(obj=None, orient=None):
''' return tuple with plane coordinate and normal
of the curent drawing according to geometry'''
@@ -336,13 +288,13 @@ def get_gp_draw_plane(obj=None, orient=None):
plane_co = bpy.context.scene.cursor.location
mat = bpy.context.scene.cursor.matrix
elif orient == 'AXIS_Y':#front (X-Z)
elif orient in ('AXIS_Y', 'FRONT'): # front (X-Z)
plane_no = Vector((0,1,0))
elif orient == 'AXIS_X':#side (Y-Z)
elif orient in ('AXIS_X', 'SIDE'): # side (Y-Z)
plane_no = Vector((1,0,0))
elif orient == 'AXIS_Z':#top (X-Y)
elif orient in ('AXIS_Z', 'TOP'): # top (X-Y)
plane_no = Vector((0,0,1))
plane_no.rotate(mat)
@@ -420,24 +372,24 @@ def create_gp_palette(gp_data_block,info) :
def get_gp_objects(selection=True):
'''return selected objects or only the active one'''
if not bpy.context.active_object or bpy.context.active_object.type != 'GPENCIL':
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL':
print('No active GP object')
return []
active = bpy.context.active_object
if selection:
selection = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
selection = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
if not active in selection:
selection += [active]
return selection
if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL':
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL':
return [active]
return []
def get_gp_datas(selection=True):
'''return selected objects or only the active one'''
if not bpy.context.active_object or bpy.context.active_object.type != 'GPENCIL':
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL':
print('No active GP object')
return []
@@ -445,28 +397,28 @@ def get_gp_datas(selection=True):
if selection:
selected = []
for o in bpy.context.selected_objects:
if o.type == 'GPENCIL':
if o.type == 'GREASEPENCIL':
if o.data not in selected:
selected.append(o.data)
# selected = [o.data for o in bpy.context.selected_objects if o.type == 'GPENCIL']
# selected = [o.data for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
if not active_data in selected:
selected += [active_data]
return selected
if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL':
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL':
return [active_data]
print('EOL. No active GP object')
return []
def get_gp_layer(gp_data_block,name) :
def get_gp_layer(gp_data_block, name) :
gp_layer = gp_data_block.layers.get(name)
if not gp_layer :
gp_layer = gp_data_block.layers.new(name)
return gp_layer
def get_gp_frame(layer,frame_nb = None) :
def get_gp_frame(layer, frame_nb=None) :
scene = bpy.context.scene
if not frame_nb :
frame_nb = scene.frame_current
@@ -488,7 +440,7 @@ def get_active_frame(layer_name=None):
if layer_name:
lay = bpy.context.scene.grease_pencil.layers.get(layer_name)
if lay:
frame = lay.active_frame
frame = lay.current_frame()
if frame:
return frame
else:
@@ -497,7 +449,7 @@ def get_active_frame(layer_name=None):
print('no layers named', layer_name, 'in scene layers')
else:#active layer
frame = bpy.context.scene.grease_pencil.layers.active.active_frame
frame = bpy.context.scene.grease_pencil.layers.active.current_frame()
if frame:
return frame
else:
@@ -505,7 +457,7 @@ def get_active_frame(layer_name=None):
def get_stroke_2D_coords(stroke):
'''return a list containing points 2D coordinates of passed gp stroke object'''
return [location_to_region(p.co) for p in stroke.points]
return [location_to_region(p.position) for p in stroke.points]
'''#foreach method for retreiving multiple other attribute quickly and stack them
point_nb = len(stroke.points)
@@ -520,21 +472,84 @@ def get_stroke_2D_coords(stroke):
def get_all_stroke_2D_coords(frame):
'''return a list of lists with all strokes's points 2D location'''
## using modification from get_stroke_2D_coords func'
return [get_stroke_2D_coords(s) for s in frame.strokes]
return [get_stroke_2D_coords(s) for s in frame.drawing.strokes]
## direct
#return[[location_to_region(p.co) for p in s.points] for s in frame.strokes]
#return[[location_to_region(p.position) for p in s.points] for s in frame.drawing.strokes]
def selected_strokes(frame):
'''return all stroke having a point selected as a list of strokes objects'''
stlist = []
for i, s in enumerate(frame.strokes):
for i, s in enumerate(frame.drawing.strokes):
if any(pt.select for pt in s.points):
stlist.append(s)
return stlist
from math import sqrt
from mathutils import Vector
## Copy stroke to a frame
def copy_stroke_to_frame(s, frame, select=True):
'''Copy stroke to given frame
return created stroke
'''
ns = frame.drawing.strokes.new()
## Set strokes attr
stroke_attr = [
'line_width',
'material_index',
'draw_cyclic',
'use_cyclic',
'uv_scale',
'uv_rotation',
'hardness',
'uv_translation',
'vertex_color_fill',
]
for attr in stroke_attr:
if not hasattr(s, attr):
continue
# print(f'transfer stroke {attr}') # Dbg
setattr(ns, attr, getattr(s, attr))
## create points
point_count = len(s.points)
ns.points.add(len(s.points))
## Set points attr
# for p, np in zip(s.points, ns.points):
flat_list = [0.0] * point_count
flat_uv_fill_list = [0.0, 0.0] * point_count
flat_vector_list = [0.0, 0.0, 0.0] * point_count
flat_color_list = [0.0, 0.0, 0.0, 0.0] * point_count
single_attr = [
'pressure',
'strength',
'uv_factor',
'uv_rotation',
]
for attr in single_attr:
# print(f'transfer point {attr}') # Dbg
s.points.foreach_get(attr, flat_list)
ns.points.foreach_set(attr, flat_list)
# print(f'transfer point co') # Dbg
s.points.foreach_get('co', flat_vector_list)
ns.points.foreach_set('co', flat_vector_list)
# print(f'transfer point uv_fill') # Dbg
s.points.foreach_get('uv_fill', flat_uv_fill_list)
ns.points.foreach_set('uv_fill', flat_uv_fill_list)
# print(f'transfer point vertex_color') # Dbg
s.points.foreach_get('vertex_color', flat_color_list)
ns.points.foreach_set('vertex_color', flat_color_list)
ns.select = select
ns.points.update()
return ns
# -----------------
### Vector utils 3d
@@ -838,6 +853,13 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
### UI utils
# -----------------
def refresh_areas():
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
area.tag_redraw()
# for area in bpy.context.screen.areas:
# area.tag_redraw()
## kmi draw for addon without delete button
def draw_kmi(km, kmi, layout):
map_type = kmi.map_type
@@ -1184,7 +1206,7 @@ def all_anim_enabled(objects) -> bool:
if fcu.mute:
return False
if o.type in ('GPENCIL', 'CAMERA'):
if o.type in ('GREASEPENCIL', 'CAMERA'):
if o.data.animation_data and o.data.animation_data.action:
## Check if object data attributes fcurves are muted
for fcu in o.animation_data.action.fcurves:
@@ -1197,9 +1219,9 @@ def all_anim_enabled(objects) -> bool:
def all_object_modifier_enabled(objects) -> bool:
'''Return False if one modifier of one object has GP modifier disabled in viewport but enabled in render'''
for o in objects:
if o.type != 'GPENCIL':
if o.type != 'GREASEPENCIL':
continue
for m in o.grease_pencil_modifiers:
for m in o.modifier:
if m.show_render and not m.show_viewport:
return False
@@ -1225,7 +1247,7 @@ def has_fully_enabled_anim(o):
if fcu.mute:
return False
if o.type in ('GPENCIL', 'CAMERA'):
if o.type in ('GREASEPENCIL', 'CAMERA'):
if o.data.animation_data and o.data.animation_data.action:
## Check if object data attributes fcurves are muted
for fcu in o.animation_data.action.fcurves:
@@ -1270,7 +1292,7 @@ def anim_status(objects) -> tuple((str, str)):
on_count += 1
count += 1
if o.type in ('GPENCIL', 'CAMERA'):
if o.type in ('GREASEPENCIL', 'CAMERA'):
datablock = o.data
if datablock.animation_data is None:
continue
@@ -1298,12 +1320,12 @@ def gp_modifier_status(objects) -> tuple((str, str)):
'''return icons on/off tuple'''
on_count = off_count = count = 0
for o in objects:
if o.type != 'GPENCIL':
if o.type != 'GREASEPENCIL':
continue
## Skip hided object
if o.hide_get() and o.hide_render:
continue
for m in o.grease_pencil_modifiers:
for m in o.modifier:
if m.show_render and not m.show_viewport:
off_count += 1
else: