Compare commits

..

No commits in common. "master" and "v2.5.0" have entirely different histories.

31 changed files with 985 additions and 1555 deletions

View File

@ -1,49 +1,5 @@
# Changelog # Changelog
4.0.3
changed: File checker doest not fix directly when clicked (also removed choice in preference):
- list potential change and display an `Apply Fix`
changed: Enhanced visibility conflict list:
- also include viewlayer hide value
- allow to set all hide value from the state of one of the three
- fixed: material move operator
4.0.1
- fixed: layer nav operator on page up/down
4.0.0
- changed: version for Blender 4.3 - Breaking retrocompatibility with previous.
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 2.5.0
- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet) - added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)

View File

@ -5,36 +5,23 @@ from bpy.props import (FloatProperty,
EnumProperty, EnumProperty,
StringProperty, StringProperty,
IntProperty) IntProperty)
from .. import utils
from ..utils import is_hidden
## copied from OP_key_duplicate_send ## copied from OP_key_duplicate_send
def get_layer_list(self, context): def get_layer_list(self, context):
'''return (identifier, name, description) of enum content''' '''return (identifier, name, description) of enum content'''
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active] return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
def get_group_list(self, context):
return [(g.name, g.name, '') for g in context.object.data.layer_groups]
class GP_OT_create_empty_frames(bpy.types.Operator): class GP_OT_create_empty_frames(bpy.types.Operator):
bl_idname = "gp.create_empty_frames" bl_idname = "gp.create_empty_frames"
bl_label = "Create Empty Frames" bl_label = "Create Empty Frames"
bl_description = "Create new empty frames on active layer where there is a frame in targeted layers\ bl_description = "Create new empty frames on active layer where there is a frame in layer above\n(usefull in color layers to match line frames)"
\n(usefull in color layers to match line frames)"
bl_options = {'REGISTER','UNDO'} bl_options = {'REGISTER','UNDO'}
layers_enum : EnumProperty( layers_enum : EnumProperty(
name="Empty Keys from Layer", name="Duplicate to layers",
description="Reference keys from layer", description="Duplicate selected keys in active layer and send them to choosen layer",
items=get_layer_list items=get_layer_list
) )
groups_enum : EnumProperty(
name="Empty Keys from Group",
description="Duplicate keys from group",
items=get_group_list
)
targeted_layers : EnumProperty( targeted_layers : EnumProperty(
name="Sources", # Empty keys from targets name="Sources", # Empty keys from targets
@ -47,8 +34,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
('ABOVE', 'Layer Directly Above', 'Empty frames from layer directly above'), ('ABOVE', 'Layer Directly Above', 'Empty frames from layer directly above'),
('BELOW', 'Layer Directly Below', 'Empty frames from layer directly below'), ('BELOW', 'Layer Directly Below', 'Empty frames from layer directly below'),
('ALL_VISIBLE', 'Visible', 'Empty frames from all visible layers'), ('ALL_VISIBLE', 'Visible', 'Empty frames from all visible layers'),
('CHOSEN', 'Chosen layer', 'Empty frames from a specific layer'), ('CHOSEN', 'Chosen layer', ''),
('CHOSEN_GROUP', 'Chosen group', 'Empty frames from a specific layer group'),
) )
) )
@ -71,20 +57,12 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL' return context.active_object is not None and context.active_object.type == 'GPENCIL'
def invoke(self, context, event): def invoke(self, context, event):
# Possible preset with shortcut # Possible preset with shortcut
# if event.alt: # if event.alt:
# self.targeted_layers = 'ALL_VISIBLE' # self.targeted_layers = 'ALL_VISIBLE'
gp = context.grease_pencil
layer_from_group = None
if gp.layer_groups.active:
layer_from_group = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
## Can just do if not utils.get_closest_active_layer(context.grease_pencil):
if not gp.layers.active and not layer_from_group:
self.report({'ERROR'}, 'No active layer or active group containing layer on GP object')
return {'CANCELLED'}
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def draw(self, context): def draw(self, context):
@ -96,13 +74,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
if self.layers_enum: if self.layers_enum:
layout.prop(self, 'layers_enum') layout.prop(self, 'layers_enum')
else: else:
layout.label(text='No other layers to match keyframe!', icon='ERROR') layout.label(text='No other layers to match keyframe')
if self.targeted_layers == 'CHOSEN_GROUP':
if self.groups_enum:
layout.prop(self, 'groups_enum')
else:
layout.label(text='No other groups to match keyframe!', icon='ERROR')
elif self.targeted_layers == 'NUMBER': elif self.targeted_layers == 'NUMBER':
row = layout.row() row = layout.row()
@ -119,56 +91,41 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
def execute(self, context): def execute(self, context):
obj = context.object obj = context.object
gp = obj.data gpl = obj.data.layers
gpl = gp.layers
if gp.layer_groups.active:
reference_layer = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
else:
reference_layer = gpl.active
active_index = next((i for i, l in enumerate(gpl) if l == reference_layer), None)
print(self.targeted_layers) print(self.targeted_layers)
if self.targeted_layers == 'ALL_ABOVE': if self.targeted_layers == 'ALL_ABOVE':
tgt_layers = [l for i, l in enumerate(gpl) if i > active_index] tgt_layers = [l for i, l in enumerate(gpl) if i > gpl.active_index]
elif self.targeted_layers == 'ALL_BELOW': elif self.targeted_layers == 'ALL_BELOW':
tgt_layers = [l for i, l in enumerate(gpl) if i < active_index] tgt_layers = [l for i, l in enumerate(gpl) if i < gpl.active_index]
elif self.targeted_layers == 'ABOVE': elif self.targeted_layers == 'ABOVE':
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index + 1] tgt_layers = [l for i, l in enumerate(gpl) if i == gpl.active_index + 1]
elif self.targeted_layers == 'BELOW': elif self.targeted_layers == 'BELOW':
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index - 1] tgt_layers = [l for i, l in enumerate(gpl) if i == gpl.active_index - 1]
elif self.targeted_layers == 'ALL_VISIBLE': elif self.targeted_layers == 'ALL_VISIBLE':
tgt_layers = [l for l in gpl if not is_hidden(l) and l != gpl.active] tgt_layers = [l for l in gpl if not l.hide and l != gpl.active]
elif self.targeted_layers == 'CHOSEN': elif self.targeted_layers == 'CHOSEN':
if not self.layers_enum: if not self.layers_enum:
self.report({'ERROR'}, f"No chosen layers, aborted") self.report({'ERROR'}, f"No chosen layers, aborted")
return {'CANCELLED'} return {'CANCELLED'}
tgt_layers = [l for l in gpl if l.name == self.layers_enum] tgt_layers = [l for l in gpl if l.info == self.layers_enum]
elif self.targeted_layers == 'CHOSEN_GROUP':
if not self.groups_enum:
self.report({'ERROR'}, f"No chosen groups, aborted")
return {'CANCELLED'}
group = gp.layer_groups.get(self.groups_enum)
tgt_layers = [l for l in gpl if l.parent_group == group]
elif self.targeted_layers == 'NUMBER': elif self.targeted_layers == 'NUMBER':
if self.number == 0: if self.number == 0:
self.report({'ERROR'}, f"Can't have 0 as value") self.report({'ERROR'}, f"Can't have 0 as value")
return {'CANCELLED'} return {'CANCELLED'}
l_range = active_index + self.number l_range = gpl.active_index + self.number
print('l_range: ', l_range) print('l_range: ', l_range)
if self.number > 0: # positive if self.number > 0: # positive
tgt_layers = [l for i, l in enumerate(gpl) if active_index < i <= l_range] tgt_layers = [l for i, l in enumerate(gpl) if gpl.active_index < i <= l_range]
else: else:
tgt_layers = [l for i, l in enumerate(gpl) if active_index > i >= l_range] tgt_layers = [l for i, l in enumerate(gpl) if gpl.active_index > i >= l_range]
if not tgt_layers: if not tgt_layers:
self.report({'ERROR'}, f"No layers found with chosen Targets") self.report({'ERROR'}, f"No layers found with chosen Targets")
@ -206,12 +163,11 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
if num in current_frames: if num in current_frames:
continue continue
#Create empty frame #Create empty frame
gpl.active.frames.new(num) gpl.active.frames.new(num, active=False)
fct += 1 fct += 1
gpl.update()
if fct: if fct:
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.name}") self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
else: else:
self.report({'WARNING'}, f"No frames to create !") self.report({'WARNING'}, f"No frames to create !")
@ -221,4 +177,4 @@ def register():
bpy.utils.register_class(GP_OT_create_empty_frames) bpy.utils.register_class(GP_OT_create_empty_frames)
def unregister(): def unregister():
bpy.utils.unregister_class(GP_OT_create_empty_frames) bpy.utils.unregister_class(GP_OT_create_empty_frames)

View File

@ -5,14 +5,11 @@ from ..utils import (location_to_region,
vector_length, vector_length,
draw_gp_stroke, draw_gp_stroke,
extrapolate_points_by_length, extrapolate_points_by_length,
simple_draw_gp_stroke, simple_draw_gp_stroke)
is_hidden,
is_locked)
import bpy import bpy
from math import degrees from math import degrees
from mathutils import Vector from mathutils import Vector
# from os.path import join, basename, exists, dirname, abspath, splitext # from os.path import join, basename, exists, dirname, abspath, splitext
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level # iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
@ -56,14 +53,11 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
encounter = defaultdict(list) encounter = defaultdict(list)
plist = [] plist = []
matrix = ob.matrix_world matrix = ob.matrix_world
for s in f.drawing.strokes: #add first and last for s in f.strokes:#add first and last
smat = ob.material_slots[s.material_index].material smat = ob.material_slots[s.material_index].material
if not smat: if not smat:continue#no material on line
continue #no material on line if smat.grease_pencil.show_fill:continue# skip fill lines -> #smat.grease_pencil.show_stroke
if smat.grease_pencil.show_fill: if len(s.points) < 2:continue#avoid 0 or 1 points
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[0])
plist.append(s.points[-1]) plist.append(s.points[-1])
# plist.extend([s.points[0], s.points[-1])# is extend faster ? # plist.extend([s.points[0], s.points[-1])# is extend faster ?
@ -76,7 +70,7 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
for op in plist:#other points for op in plist:#other points
if p == op:# print('same point') if p == op:# print('same point')
continue continue
gap2d = vector_length_2d(location_to_region(matrix @ p.position), location_to_region(matrix @ op.position)) gap2d = vector_length_2d(location_to_region(matrix @ p.co), location_to_region(matrix @ op.co))
# print('gap2d: ', gap2d) # print('gap2d: ', gap2d)
if gap2d > tol: if gap2d > tol:
continue continue
@ -108,16 +102,16 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
encounter[p].append(op) encounter[p].append(op)
simple_draw_gp_stroke([p.position, op.position], f, width = 2, mat_id = mat_id) simple_draw_gp_stroke([p.co, op.co], f, width = 2, mat_id = mat_id)
ctl += 1 ctl += 1
print(f'{ctl} line created') print(f'{ctl} line created')
##test_call: #create_gap_stroke(C.object.data.layers.active.current_frame(), C.object, mat_id=C.object.active_material_index) ##test_call: #create_gap_stroke(C.object.data.layers.active.active_frame, C.object, mat_id=C.object.active_material_index)
def create_closing_line(tolerance=0.2): def create_closing_line(tolerance=0.2):
for ob in bpy.context.selected_objects: for ob in bpy.context.selected_objects:
if ob.type != 'GREASEPENCIL': if ob.type != 'GPENCIL':
continue continue
mat_id = get_closeline_mat(ob)# get a the closing material mat_id = get_closeline_mat(ob)# get a the closing material
@ -134,7 +128,7 @@ def create_closing_line(tolerance=0.2):
## filter on selected ## filter on selected
if not l.select:continue# comment this line for all if not l.select:continue# comment this line for all
# for f in l.frames:#not all for now # for f in l.frames:#not all for now
f = l.current_frame() f = l.active_frame
## create gap stroke ## create gap stroke
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id) create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
@ -149,9 +143,9 @@ def is_deviating_by(s, deviation=0.75):
pb = s.points[-2] pb = s.points[-2]
pc = s.points[-3] pc = s.points[-3]
a = location_to_region(pa.position) a = location_to_region(pa.co)
b = location_to_region(pb.position) b = location_to_region(pb.co)
c = location_to_region(pc.position) c = location_to_region(pc.co)
#cb-> compare angle with ba-> #cb-> compare angle with ba->
angle = (b-c).angle(a-b) angle = (b-c).angle(a-b)
@ -164,16 +158,16 @@ def extend_stroke_tips(s,f,ob,length, mat_id):
'''extend line boundary by given length''' '''extend line boundary by given length'''
for id_pair in [ [1,0], [-2,-1] ]:# start and end pair for id_pair in [ [1,0], [-2,-1] ]:# start and end pair
## 2D mode ## 2D mode
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].position) # a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].co)
# b_loc = ob.matrix_world @ s.points[id_pair[1]].position # b_loc = ob.matrix_world @ s.points[id_pair[1]].co
# b = location_to_region(b_loc) # b = location_to_region(b_loc)
# c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c)) # c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c))
# c_loc = region_to_location(c, b_loc) # 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) # simple_draw_gp_stroke([ob.matrix_world.inverted() @ b_loc, ob.matrix_world.inverted() @ c_loc], f, width=2, mat_id=mat_id)
## 3D ## 3D
a = s.points[id_pair[0]].position# ob.matrix_world @ a = s.points[id_pair[0]].co# ob.matrix_world @
b = s.points[id_pair[1]].position# ob.matrix_world @ b = s.points[id_pair[1]].co# ob.matrix_world @
c = extrapolate_points_by_length(a,b,length)#print(vector_length(b,c)) 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) simple_draw_gp_stroke([b,c], f, width=2, mat_id=mat_id)
@ -194,15 +188,15 @@ def change_extension_length(ob, strokelist, length, selected=False):
## Change length of current length to designated ## Change length of current length to designated
# Vector point A to point B (direction), push point B in this direction # Vector point A to point B (direction), push point B in this direction
a = s.points[-2].position a = s.points[-2].co
bp = s.points[-1]#end-point bp = s.points[-1]#end-point
b = bp.position b = bp.co
ab = b - a ab = b - a
if not ab: if not ab:
continue continue
# new pos of B is A + new length in the AB direction # new pos of B is A + new length in the AB direction
newb = a + (ab.normalized() * length) newb = a + (ab.normalized() * length)
bp.position = newb bp.co = newb
ct += 1 ct += 1
return ct return ct
@ -216,14 +210,14 @@ def extend_all_strokes_tips(ob, frame, length=10, selected=False):
return return
# TODO need custom filters or go in GP refine strokes... # TODO need custom filters or go in GP refine strokes...
# frame = ob.data.layers.active.current_frame() # frame = ob.data.layers.active.active_frame
if not frame: return if not frame: return
ct = 0 ct = 0
#TODO need to delete previous closing lines on frame before launching #TODO need to delete previous closing lines on frame before launching
# iterate in a copy of stroke list to avoid growing frame.drawing.strokes as we loop in ! # iterate in a copy of stroke list to avoid growing frame.strokes as we loop in !
for s in list(frame.drawing.strokes): for s in list(frame.strokes):
if s.material_index == mat_id:#is a closeline if s.material_index == mat_id:#is a closeline
continue continue
if len(s.points) < 2:#not enough point to evaluate if len(s.points) < 2:#not enough point to evaluate
@ -247,7 +241,7 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL' return context.active_object is not None and context.active_object.type == 'GPENCIL'
# mode : bpy.props.StringProperty( # mode : bpy.props.StringProperty(
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'}) # name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
@ -279,18 +273,18 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
if self.layer_tgt == 'ACTIVE': if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active] lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED': elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)] lays = [l for l in ob.data.layers if l.select and not l.hide]
elif self.layer_tgt == 'ALL_VISIBLE': elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not is_hidden(l)] lays = [l for l in ob.data.layers if not l.hide]
else: else:
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))] lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
fct = 0 fct = 0
for l in lays: for l in lays:
if not l.current_frame(): if not l.active_frame:
print(f'{l.name} has no active frame') print(f'{l.info} has no active frame')
continue continue
fct += extend_all_strokes_tips(ob, l.current_frame(), length = self.length, selected = self.selected) fct += extend_all_strokes_tips(ob, l.active_frame, length = self.length, selected = self.selected)
if not fct: if not fct:
mess = "No strokes extended... see console" mess = "No strokes extended... see console"
@ -312,7 +306,7 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL' return context.active_object is not None and context.active_object.type == 'GPENCIL'
layer_tgt : bpy.props.EnumProperty( layer_tgt : bpy.props.EnumProperty(
name="Extend layers", description="Choose which layer to target", name="Extend layers", description="Choose which layer to target",
@ -340,18 +334,18 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
if self.layer_tgt == 'ACTIVE': if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active] lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED': elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)] lays = [l for l in ob.data.layers if l.select and not l.hide]
elif self.layer_tgt == 'ALL_VISIBLE': elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not is_hidden(l)] lays = [l for l in ob.data.layers if not l.hide]
else: else:
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))] lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
fct = 0 fct = 0
for l in lays: for l in lays:
if not l.current_frame(): if not l.active_frame:
print(f'{l.name} has no active frame') print(f'{l.info} has no active frame')
continue continue
fct += change_extension_length(ob, [s for s in l.current_frame().drawing.strokes], length = self.length, selected = self.selected) fct += change_extension_length(ob, [s for s in l.active_frame.strokes], length = self.length, selected = self.selected)
if not fct: if not fct:
mess = "No extension modified... see console" mess = "No extension modified... see console"
@ -373,15 +367,15 @@ class GPSTK_OT_comma_finder(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL' return context.active_object is not None and context.active_object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
ct = 0 ct = 0
ob = context.object ob = context.object
lays = [l for l in ob.data.layers if not is_hidden(l) and not is_locked(l)] lays = [l for l in ob.data.layers if not l.hide and not l.lock]
for l in lays: for l in lays:
if not l.current_frame():continue if not l.active_frame:continue
for s in l.current_frame().drawing.strokes: for s in l.active_frame.strokes:
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance): if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
ct+=1 ct+=1
@ -403,7 +397,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return (context.object is not None)# and context.object.type == 'GREASEPENCIL' return (context.object is not None)# and context.object.type == 'GPENCIL'
## draw stuff inside the header (place before main label) ## draw stuff inside the header (place before main label)
# def draw_header(self, context): # def draw_header(self, context):
@ -420,7 +414,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT') layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT')
#diplay closeline visibility #diplay closeline visibility
if context.object.type == 'GREASEPENCIL' and context.object.data.materials.get('closeline'): if context.object.type == 'GPENCIL' and context.object.data.materials.get('closeline'):
row=layout.row() row=layout.row()
row.prop(context.object.data.materials['closeline'].grease_pencil, 'hide', text='Stop lines') 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') row.operator("gp.change_close_lines_extension", text='Length', icon = 'DRIVER_DISTANCE')

View File

@ -27,7 +27,7 @@ class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
# @classmethod # @classmethod
# def poll(cls, context): # def poll(cls, context):
# return context.object and context.object.type == 'GREASEPENCIL' # return context.object and context.object.type == 'GPENCIL'
filename_ext = '.blend' filename_ext = '.blend'
@ -58,7 +58,7 @@ class GPTB_OT_brush_set(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.mode == 'PAINT_GREASE_PENCIL' return context.object and context.mode == 'PAINT_GPENCIL'
def execute(self, context): def execute(self, context):
brush = bpy.data.brushes.get(self.brush_name) 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): def load_brush_ui(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
if context.mode == 'PAINT_GREASE_PENCIL': if context.mode == 'PAINT_GPENCIL':
self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path
def load_brush_top_bar_ui(self, context): def load_brush_top_bar_ui(self, context):
prefs = get_addon_prefs() prefs = get_addon_prefs()
if context.mode == 'PAINT_GREASE_PENCIL': if context.mode == 'PAINT_GPENCIL':
self.layout.operator('gp.load_brushes').filepath = prefs.brush_path self.layout.operator('gp.load_brushes').filepath = prefs.brush_path
classes = ( classes = (
@ -89,12 +89,12 @@ def register():
for cl in classes: for cl in classes:
bpy.utils.register_class(cl) bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_brush_context_menu.append(load_brush_ui) bpy.types.VIEW3D_MT_brush_gpencil_context_menu.append(load_brush_ui)
bpy.types.VIEW3D_HT_tool_header.append(load_brush_top_bar_ui) bpy.types.VIEW3D_HT_tool_header.append(load_brush_top_bar_ui)
def unregister(): def unregister():
bpy.types.VIEW3D_HT_tool_header.remove(load_brush_top_bar_ui) bpy.types.VIEW3D_HT_tool_header.remove(load_brush_top_bar_ui)
bpy.types.VIEW3D_MT_brush_context_menu.remove(load_brush_ui) bpy.types.VIEW3D_MT_brush_gpencil_context_menu.remove(load_brush_ui)
for cl in reversed(classes): for cl in reversed(classes):
bpy.utils.unregister_class(cl) bpy.utils.unregister_class(cl)

View File

@ -1,15 +1,38 @@
## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends # This program is free software; you can redistribute it and/or modify
## View3D > Toolbar > Gpencil > GP clipboard # it under the terms of the GNU General Public License as published by
## in 4.2- existed in standalone scripts: https://github.com/Pullusb/GP_clipboard # the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
## based on GPclipboard 1.3.2 (just stripped addon prefs)
bl_info = {
"name": "GP clipboard",
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
"author": "Samuel Bernou",
"version": (1, 3, 3),
"blender": (2, 83, 0),
"location": "View3D > Toolbar > Gpencil > GP clipboard",
"warning": "",
"doc_url": "https://github.com/Pullusb/GP_clipboard",
"category": "Object" }
import bpy import bpy
import os
import mathutils import mathutils
from mathutils import Vector from mathutils import Vector
import json import json
from time import time from time import time
from operator import itemgetter from operator import itemgetter
from itertools import groupby from itertools import groupby
from .utils import is_locked, is_hidden # from pprint import pprint
def convertAttr(Attr): def convertAttr(Attr):
'''Convert given value to a Json serializable format''' '''Convert given value to a Json serializable format'''
@ -22,106 +45,104 @@ def convertAttr(Attr):
else: else:
return(Attr) return(Attr)
def getMatrix(layer) : def getMatrix (layer) :
matrix = mathutils.Matrix.Identity(4) matrix = mathutils.Matrix.Identity(4)
if parent := layer.parent: if layer.is_parented:
if parent.type == 'ARMATURE' and layer.parent_bone: if layer.parent_type == 'BONE':
bone = parent.pose.bones[layer.parent_bone] object = layer.parent
matrix = bone.matrix @ parent.matrix_world bone = object.pose.bones[layer.parent_bone]
matrix = matrix.copy() @ layer.matrix_parent_inverse matrix = bone.matrix @ object.matrix_world
else: matrix = matrix.copy() @ layer.matrix_inverse
matrix = parent.matrix_world @ layer.matrix_parent_inverse else :
matrix = layer.parent.matrix_world @ layer.matrix_inverse
return matrix.copy() return matrix.copy()
# default_pt_uv_fill = Vector((0.5, 0.5)) default_pt_uv_fill = Vector((0.5, 0.5))
def dump_gp_point(p, l, obj, def dump_gp_point(p, l, obj,
radius=True, opacity=True, vertex_color=True, fill_color=True, uv_factor=True, rotation=True): pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
'''add properties of a given points to a dic and return it''' '''add properties of a given points to a dic and return it'''
point_dict = {} pdic = {}
#point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type' #point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
#for att in point_attr_list: #for att in point_attr_list:
# point_dict[att] = convertAttr(getattr(p, att)) # pdic[att] = convertAttr(getattr(p, att))
if l.parent: if l.parent:
mat = getMatrix(l) mat = getMatrix(l)
point_dict['position'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'position')) pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
else: else:
point_dict['position'] = convertAttr(obj.matrix_world @ getattr(p,'position')) pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co'))
# point_dict['select'] = convertAttr(getattr(p,'select')) # need selection ? # pdic['select'] = convertAttr(getattr(p,'select')) # need selection ?
if radius and p.radius != 1.0: if pressure and p.pressure != 1.0:
point_dict['radius'] = convertAttr(getattr(p,'radius')) pdic['pressure'] = convertAttr(getattr(p,'pressure'))
if strength and p.strength != 1.0:
if opacity and p.opacity != 1.0: pdic['strength'] = convertAttr(getattr(p,'strength'))
point_dict['opacity'] = convertAttr(getattr(p,'opacity'))
## get vertex color (long...) ## get vertex color (long...)
if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0): if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
point_dict['vertex_color'] = convertAttr(p.vertex_color) pdic['vertex_color'] = convertAttr(p.vertex_color)
if rotation and p.rotation != 0.0: ## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...)
point_dict['rotation'] = convertAttr(p.rotation) if uv_fill and p.uv_fill != default_pt_uv_fill:
pdic['uv_fill'] = convertAttr(p.uv_fill)
if uv_factor and p.uv_factor != 0.0:
pdic['uv_factor'] = convertAttr(p.uv_factor)
if uv_rotation and p.uv_rotation != 0.0:
pdic['uv_rotation'] = convertAttr(p.uv_rotation)
## No time infos return pdic
# if delta_time and p.delta_time != 0.0:
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
return point_dict
def dump_gp_stroke_range(s, sid, l, obj, def dump_gp_stroke_range(s, sid, l, obj,
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True): pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
'''Get a grease pencil stroke and return a dic with attribute '''Get a grease pencil stroke and return a dic with attribute
(points attribute being a dic of dics to store points and their attributes) (points attribute being a dic of dics to store points and their attributes)
''' '''
stroke_dict = {} sdic = {}
# stroke_attr_list = ('line_width',) stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles'
# for att in stroke_attr_list: for att in stroke_attr_list:
# stroke_dict[att] = getattr(s, att) sdic[att] = getattr(s, att)
## Dump following these value only if they are non default ## Dump following these value only if they are non default
if s.material_index != 0: if s.material_index != 0:
stroke_dict['material_index'] = s.material_index sdic['material_index'] = s.material_index
if s.cyclic:
stroke_dict['cyclic'] = s.cyclic
if s.softness != 0.0:
stroke_dict['softness'] = s.softness
if s.aspect_ratio != 1.0: if getattr(s, 'draw_cyclic', None): # pre-2.92
stroke_dict['aspect_ratio'] = s.aspect_ratio sdic['draw_cyclic'] = s.draw_cyclic
if getattr(s, 'use_cyclic', None): # from 2.92
sdic['use_cyclic'] = s.use_cyclic
if s.uv_scale != 1.0:
sdic['uv_scale'] = s.uv_scale
if s.uv_rotation != 0.0:
sdic['uv_rotation'] = s.uv_rotation
if s.hardness != 1.0:
sdic['hardness'] = s.hardness
if s.uv_translation != Vector((0.0, 0.0)):
sdic['uv_translation'] = convertAttr(s.uv_translation)
if s.start_cap != 0: if s.vertex_color_fill[:] != (0,0,0,0):
stroke_dict['start_cap'] = s.start_cap sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill)
if s.end_cap != 0:
stroke_dict['end_cap'] = s.end_cap
if fill_color and s.fill_color[:] != (0,0,0,0):
stroke_dict['fill_color'] = convertAttr(s.fill_color)
if fill_opacity and s.fill_opacity != 0.0:
stroke_dict['fill_opacity'] = s.fill_opacity
## No time infos
# if s.time_start != 0.0:
# stroke_dict['time_start'] = s.time_start
points = [] points = []
if sid is None: # no ids, just full points... if sid is None: # no ids, just full points...
for p in s.points: for p in s.points:
points.append(dump_gp_point(p, l, obj, points.append(dump_gp_point(p,l,obj,
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation)) pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation))
else: else:
for pid in sid: for pid in sid:
points.append(dump_gp_point(s.points[pid], l, obj, points.append(dump_gp_point(s.points[pid],l,obj,
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation)) pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation))
sdic['points'] = points
stroke_dict['points'] = points return sdic
return stroke_dict
def copycut_strokes(layers=None, copy=True, keep_empty=True): def copycut_strokes(layers=None, copy=True, keep_empty=True):
@ -140,7 +161,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
# color = gp.palettes.active.colors.active.name # color = gp.palettes.active.colors.active.name
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # [] layers = [l for l in gpl if not l.hide and not l.lock] # []
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@ -148,36 +169,32 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
stroke_list = [] # one stroke list for all layers. stroke_list = [] # one stroke list for all layers.
for l in layers: for l in layers:
f = l.current_frame() f = l.active_frame
if f: # active frame can be None if f: # active frame can be None
if not copy: if not copy:
staylist = [] # init part of strokes that must survive on this layer staylist = [] # init part of strokes that must survive on this layer
rm_list = [] # init strokes that must be removed from this layer for s in f.strokes:
for s_index, stroke in enumerate(f.drawing.strokes): if s.select:
if stroke.select:
# separate in multiple stroke if parts of the strokes a selected. # separate in multiple stroke if parts of the strokes a selected.
sel = [i for i, p in enumerate(stroke.points) if p.select] sel = [i for i, p in enumerate(s.points) if p.select]
substrokes = [] # list of list containing isolated selection substrokes = [] # list of list containing isolated selection
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
# continuity stroke have same substract result between point index and enumerator
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):
group = list(map(itemgetter(1), g)) group = list(map(itemgetter(1), g))
substrokes.append(group) substrokes.append(group)
for ss in substrokes: for ss in substrokes:
if len(ss) > 1: # avoid copy isolated points if len(ss) > 1: # avoid copy isolated points
stroke_list.append(dump_gp_stroke_range(stroke, ss, l, obj)) stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
# Cutting operation # Cutting operation
if not copy: if not copy:
maxindex = len(stroke.points)-1 maxindex = len(s.points)-1
if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
# f.drawing.strokes.remove(stroke) # gpv2 f.strokes.remove(s)
rm_list.append(s_index)
else: else:
neg = [i for i, p in enumerate(stroke.points) if not p.select] neg = [i for i, p in enumerate(s.points) if not p.select]
staying = [] staying = []
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]): for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
@ -191,30 +208,37 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
for ns in staying: for ns in staying:
if len(ns) > 1: if len(ns) > 1:
staylist.append(dump_gp_stroke_range(stroke, ns, l, obj)) staylist.append(dump_gp_stroke_range(s,ns,l,obj))
# make a negative list containing all last index # make a negative list containing all last index
if rm_list:
f.drawing.remove_strokes(indices=rm_list) '''#full stroke version
# if s.colorname == color: #line for future filters
stroke_list.append(dump_gp_stroke(s,l))
#delete stroke on the fly
if not copy:
f.strokes.remove(s)
'''
if not copy: if not copy:
selected_ids = [i for i, s in enumerate(f.drawing.strokes) if s.select]
# delete all selected strokes... # delete all selected strokes...
if selected_ids: for s in f.strokes:
f.drawing.remove_strokes(indices=selected_ids) if s.select:
f.strokes.remove(s)
# ...recreate these uncutted ones # ...recreate these uncutted ones
#pprint(staylist) #pprint(staylist)
if staylist: if staylist:
add_multiple_strokes(staylist, l) add_multiple_strokes(staylist, l)
#for ns in staylist:#weirdly recreate the stroke twice !
# add_stroke(ns, f, l)
# If nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear) #if 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 copy and not keep_empty:#
if not len(f.drawing.strokes): if not len(f.strokes):
l.frames.remove(f) l.frames.remove(f)
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds') print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
#print(stroke_list) #print(stroke_list)
return stroke_list return stroke_list
@ -236,7 +260,7 @@ def copy_all_strokes(layers=None):
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ? layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@ -244,12 +268,12 @@ def copy_all_strokes(layers=None):
stroke_list = []# one stroke list for all layers. stroke_list = []# one stroke list for all layers.
for l in layers: for l in layers:
f = l.current_frame() f = l.active_frame
if not f: if not f:
continue# active frame can be None continue# active frame can be None
for s in f.drawing.strokes: for s in f.strokes:
## full stroke version ## full stroke version
# if s.select: # if s.select:
stroke_list.append(dump_gp_stroke_range(s, None, l, obj)) stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
@ -260,7 +284,7 @@ def copy_all_strokes(layers=None):
""" """
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None, def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True): pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
''' '''
copy all stroke, not affected by selection on active frame copy all stroke, not affected by selection on active frame
layers can be None, a single layer object or list of layer object as filter layers can be None, a single layer object or list of layer object as filter
@ -276,7 +300,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # include locked ? layers = [l for l in gpl if not l.hide and not l.lock] # include locked ?
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@ -284,17 +308,17 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
stroke_list = [] stroke_list = []
for l in layers: for l in layers:
f = l.current_frame() f = l.active_frame
if not f: if not f:
continue# active frame can be None continue# active frame can be None
for s in f.drawing.strokes: for s in f.strokes:
## full stroke version ## full stroke version
# if s.select: # if s.select:
# send index of all points to get the whole stroke with "range" # send index of all points to get the whole stroke with "range"
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj, stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj,
radius=radius, opacity=opacity, vertex_color=vertex_color, fill_color=fill_color, fill_opacity=fill_opacity, rotation=rotation)) pressure=pressure,strength=strength,vertex_color=vertex_color,uv_fill=uv_fill,uv_factor=uv_factor,uv_rotation=uv_rotation))
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds') # print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
return stroke_list return stroke_list
@ -302,43 +326,49 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
def add_stroke(s, frame, layer, obj, select=False): def add_stroke(s, frame, layer, obj, select=False):
'''add stroke on a given frame, (layer is for parentage setting)''' '''add stroke on a given frame, (layer is for parentage setting)'''
# print(3*'-',s) # print(3*'-',s)
pts_to_add = len(s['points']) ns = frame.strokes.new()
frame.drawing.add_strokes([pts_to_add])
ns = frame.drawing.strokes[-1]
## set strokes atrributes
for att, val in s.items(): for att, val in s.items():
if att not in ('points'): if att not in ('points'):
setattr(ns, att, val) setattr(ns, att, val)
pts_to_add = len(s['points'])
# print(pts_to_add, 'points')#dbg
ns.points.add(pts_to_add)
ob_mat_inv = obj.matrix_world.inverted() ob_mat_inv = obj.matrix_world.inverted()
if layer.parent: ## patch pressure 1
layer_matrix = getMatrix(layer).inverted() # pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened
transform_matrix = ob_mat_inv @ layer_matrix
if layer.is_parented:
mat = getMatrix(layer).inverted()
for i, pt in enumerate(s['points']):
for k, v in pt.items():
if k == 'co':
setattr(ns.points[i], k, v)
ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co # invert of object * invert of layer * coordinate
else:
setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
else: else:
transform_matrix = ob_mat_inv for i, pt in enumerate(s['points']):
for k, v in pt.items():
## Set points attributes if k == 'co':
for i, pt in enumerate(s['points']): setattr(ns.points[i], k, v)
for k, v in pt.items(): ns.points[i].co = ob_mat_inv @ ns.points[i].co# invert of object * coordinate
if k == 'position': else:
setattr(ns.points[i], k, v) setattr(ns.points[i], k, v)
ns.points[i].position = transform_matrix @ ns.points[i].position # invert of object * invert of layer * coordinate if select:
else: ns.points[i].select = True
setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
## Opacity initialized at 0.0 (should be 1.0)
if not 'opacity' in pt:
ns.points[i].opacity = 1.0
## Radius initialized at 0.0 (should probably be 0.01)
if not 'radius' in pt:
ns.points[i].radius = 0.01
## trigger updapte (in 2.93 fix some drawing problem with fills and UVs)
ns.points.update()
## patch pressure 2
# ns.points.foreach_set('pressure', pressure_flat_list)
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False): def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False):
''' '''
@ -359,7 +389,7 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
fnum = scene.frame_current fnum = scene.frame_current
target_frame = False target_frame = False
act = layer.current_frame() act = layer.active_frame
## set frame if needed ## set frame if needed
if act: if act:
if use_current_frame or act.frame_number == fnum: if use_current_frame or act.frame_number == fnum:
@ -374,7 +404,11 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
for s in stroke_list: for s in stroke_list:
add_stroke(s, target_frame, layer, obj, select=select) add_stroke(s, target_frame, layer, obj, select=select)
'''
for s in stroke_data:
add_stroke(s, target_frame)
'''
# print(len(stroke_list), 'strokes pasted') # print(len(stroke_list), 'strokes pasted')
@ -389,15 +423,15 @@ class GPCLIP_OT_copy_strokes(bpy.types.Operator):
#copy = bpy.props.BoolProperty(default=True) #copy = bpy.props.BoolProperty(default=True)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
# if not context.object or not context.object.type == 'GREASEPENCIL': # if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected') # self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"} # return {"CANCELLED"}
t0 = time() t0 = time()
#ct = check_radius() #ct = check_pressure()
strokelist = copycut_strokes(copy=True, keep_empty=True) strokelist = copycut_strokes(copy=True, keep_empty=True)
if not strokelist: if not strokelist:
self.report({'ERROR'}, 'Nothing to copy') self.report({'ERROR'}, 'Nothing to copy')
@ -414,19 +448,19 @@ class GPCLIP_OT_cut_strokes(bpy.types.Operator):
bl_idname = "gp.cut_strokes" bl_idname = "gp.cut_strokes"
bl_label = "GP Cut strokes" bl_label = "GP Cut strokes"
bl_description = "Cut strokes to text in paperclip" bl_description = "Cut strokes to text in paperclip"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
# if not context.object or not context.object.type == 'GREASEPENCIL': # if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected') # self.report({'ERROR'},'No GP object selected')
# return {"CANCELLED"} # return {"CANCELLED"}
t0 = time() t0 = time()
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_radius() strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_pressure()
if not strokelist: if not strokelist:
self.report({'ERROR'},'Nothing to cut') self.report({'ERROR'},'Nothing to cut')
return {"CANCELLED"} return {"CANCELLED"}
@ -443,10 +477,10 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
# if not context.object or not context.object.type == 'GREASEPENCIL': # if not context.object or not context.object.type == 'GPENCIL':
# self.report({'ERROR'},'No GP object selected to paste on') # self.report({'ERROR'},'No GP object selected to paste on')
# return {"CANCELLED"} # return {"CANCELLED"}
@ -476,22 +510,20 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
#copy = bpy.props.BoolProperty(default=True) #copy = bpy.props.BoolProperty(default=True)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
radius : bpy.props.BoolProperty(name='radius', default=True, pressure : bpy.props.BoolProperty(name='pressure', default=True,
description='Dump point radius attribute (already skipped if at default value)') description='Dump point pressure attribute (already skipped if at default value)')
opacity : bpy.props.BoolProperty(name='opacity', default=True, strength : bpy.props.BoolProperty(name='strength', default=True,
description='Dump point opacity attribute (already skipped if at default value)') description='Dump point strength attribute (already skipped if at default value)')
vertex_color : bpy.props.BoolProperty(name='vertex color', default=True, vertex_color : bpy.props.BoolProperty(name='vertex color', default=True,
description='Dump point vertex_color attribute (already skipped if at default value)') description='Dump point vertex_color attribute (already skipped if at default value)')
fill_color : bpy.props.BoolProperty(name='fill color', default=True, uv_fill : bpy.props.BoolProperty(name='uv fill', default=True,
description='Dump point fill_color attribute (already skipped if at default value)') description='Dump point uv_fill attribute (already skipped if at default value)')
fill_opacity : bpy.props.BoolProperty(name='fill opacity', default=True,
description='Dump point fill_opacity attribute (already skipped if at default value)')
uv_factor : bpy.props.BoolProperty(name='uv factor', default=True, uv_factor : bpy.props.BoolProperty(name='uv factor', default=True,
description='Dump point uv_factor attribute (already skipped if at default value)') description='Dump point uv_factor attribute (already skipped if at default value)')
rotation : bpy.props.BoolProperty(name='rotation', default=True, uv_rotation : bpy.props.BoolProperty(name='uv rotation', default=True,
description='Dump point rotation attribute (already skipped if at default value)') description='Dump point uv_rotation attribute (already skipped if at default value)')
def invoke(self, context, event): def invoke(self, context, event):
# self.file_dump = event.ctrl # self.file_dump = event.ctrl
@ -503,12 +535,12 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
layout.use_property_split = True layout.use_property_split = True
col = layout.column() col = layout.column()
col.label(text='Keep following point attributes:') col.label(text='Keep following point attributes:')
col.prop(self, 'radius') col.prop(self, 'pressure')
col.prop(self, 'opacity') col.prop(self, 'strength')
col.prop(self, 'vertex_color') col.prop(self, 'vertex_color')
col.prop(self, 'fill_color') col.prop(self, 'uv_fill')
col.prop(self, 'fill_opacity') col.prop(self, 'uv_factor')
col.prop(self, 'rotation') col.prop(self, 'uv_rotation')
return return
def execute(self, context): def execute(self, context):
@ -519,10 +551,10 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
obj = context.object obj = context.object
gpl = obj.data.layers gpl = obj.data.layers
t0 = time() t0 = time()
#ct = check_radius() #ct = check_pressure()
layerdic = {} layerdic = {}
layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l) layerpool = [l for l in gpl if not l.hide and l.select] # and not l.lock
if not layerpool: if not layerpool:
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only') self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
return {"CANCELLED"} return {"CANCELLED"}
@ -534,20 +566,20 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
frame_dic = {} frame_dic = {}
for f in l.frames: for f in l.frames:
if skip_empty_frame and not len(f.drawing.strokes): if skip_empty_frame and not len(f.strokes):
continue continue
context.scene.frame_set(f.frame_number) # use matrix of this frame context.scene.frame_set(f.frame_number) # use matrix of this frame
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj, strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color, pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color,
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation) uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation)
frame_dic[f.frame_number] = strokelist frame_dic[f.frame_number] = strokelist
layerdic[l.name] = frame_dic layerdic[l.info] = frame_dic
else: # bake position: copy frame where object as moved even if frame is unchanged else: # bake position: copy frame where object as moved even if frame is unchanged
for l in layerpool: for l in layerpool:
print('dump layer:', l.name) print('dump layer:', l.info)
if not l.frames: if not l.frames:
continue# skip empty layers continue# skip empty layers
@ -571,17 +603,17 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
break break
## skip empty frame if specified ## skip empty frame if specified
if skip_empty_frame and not len(f.drawing.strokes): if skip_empty_frame and not len(f.strokes):
continue continue
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj, strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color, pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color,
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation) uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation)
frame_dic[i] = strokelist frame_dic[i] = strokelist
prevmat = curmat prevmat = curmat
layerdic[l.name] = frame_dic layerdic[l.info] = frame_dic
## All to clipboard manager ## All to clipboard manager
bpy.context.window_manager.clipboard = json.dumps(layerdic) bpy.context.window_manager.clipboard = json.dumps(layerdic)
@ -601,14 +633,14 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
#copy = bpy.props.BoolProperty(default=True) #copy = bpy.props.BoolProperty(default=True)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
org_frame = context.scene.frame_current org_frame = context.scene.frame_current
obj = context.object obj = context.object
gpl = obj.data.layers gpl = obj.data.layers
t0 = time() t0 = time()
# add a validity check por the content of the paperclip (check if not data.startswith('[{') ? ) #add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
try: try:
data = json.loads(bpy.context.window_manager.clipboard) data = json.loads(bpy.context.window_manager.clipboard)
except: except:

View File

@ -2,8 +2,6 @@
import bpy import bpy
import mathutils import mathutils
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from bpy.app.handlers import persistent
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
## override all sursor snap shortcut with this in keymap ## override all sursor snap shortcut with this in keymap
@ -15,7 +13,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
# @classmethod # @classmethod
# def poll(cls, context): # def poll(cls, context):
# return context.object and context.object.type == 'GREASEPENCIL' # return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event): def invoke(self, context, event):
#print('-!SNAP!-') #print('-!SNAP!-')
@ -25,7 +23,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
def execute(self, context): def execute(self, context):
if not context.object or context.object.type != 'GREASEPENCIL': if not context.object or context.object.type != 'GPENCIL':
self.report({'INFO'}, 'Not GP, Cursor surface project') self.report({'INFO'}, 'Not GP, Cursor surface project')
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM' bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
return {"FINISHED"} return {"FINISHED"}
@ -107,24 +105,20 @@ def swap_keymap_by_id(org_idname, new_idname):
k.idname = new_idname k.idname = new_idname
# prev_matrix = mathutils.Matrix()
prev_matrix = None prev_matrix = None
# @call_once(bpy.app.handlers.frame_change_post) # @call_once(bpy.app.handlers.frame_change_post)
## used in properties file to register in boolprop update def cursor_follow_update(self,context):
def cursor_follow_update(self, context):
'''append or remove cursor_follow handler according a boolean''' '''append or remove cursor_follow handler according a boolean'''
ob = bpy.context.object
if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
global prev_matrix global prev_matrix
# imported in properties to register in boolprop update # imported in properties to register in boolprop update
if self.cursor_follow:#True if self.cursor_follow:#True
if ob:
# out of below condition to be called when setting target as well
prev_matrix = ob.matrix_world.copy()
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]: if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
if context.object:
prev_matrix = context.object.matrix_world
bpy.app.handlers.frame_change_post.append(cursor_follow) bpy.app.handlers.frame_change_post.append(cursor_follow)
else:#False else:#False
@ -135,13 +129,11 @@ def cursor_follow_update(self, context):
def cursor_follow(scene): def cursor_follow(scene):
'''Handler to make the cursor follow active object matrix changes on frame change''' '''Handler to make the cursor follow active object matrix changes on frame change'''
ob = bpy.context.object ## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)...
if bpy.context.scene.gptoolprops.cursor_follow_target: if not bpy.context.object:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
if not ob:
return return
global prev_matrix global prev_matrix
ob = bpy.context.object
current_matrix = ob.matrix_world current_matrix = ob.matrix_world
if not prev_matrix: if not prev_matrix:
prev_matrix = current_matrix.copy() prev_matrix = current_matrix.copy()
@ -154,44 +146,15 @@ def cursor_follow(scene):
## translation only ## translation only
# scene.cursor.location += (current_matrix - prev_matrix).to_translation() # scene.cursor.location += (current_matrix - prev_matrix).to_translation()
# print('offset:', (current_matrix - prev_matrix).to_translation())
## full ## full
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location) scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
# store for next use # store for next use
prev_matrix = current_matrix.copy() prev_matrix = current_matrix.copy()
prev_active_obj = None
## Add check for object selection change
def selection_changed():
"""Callback function for selection changes"""
if not bpy.context.scene.gptoolprops.cursor_follow:
return
if bpy.context.scene.gptoolprops.cursor_follow_target:
# we are following a target, nothing to update on selection change
return
global prev_matrix, prev_active_obj
if prev_active_obj != bpy.context.object:
## Set stored matrix to active object
prev_matrix = bpy.context.object.matrix_world.copy()
prev_active_obj = bpy.context.object
## Note: Same owner as layer manager (will be removed as well)
def subscribe_object_change():
subscribe_to = (bpy.types.LayerObjects, 'active')
bpy.msgbus.subscribe_rna(
key=subscribe_to,
# owner of msgbus subcribe (for clearing later)
owner=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime.
args=(),
notify=selection_changed,
options={'PERSISTENT'},
)
@persistent
def subscribe_object_change_handler(dummy):
subscribe_object_change()
classes = ( classes = (
GPTB_OT_cusor_snap, GPTB_OT_cusor_snap,
@ -200,18 +163,14 @@ GPTB_OT_cusor_snap,
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
## Follow cursor matrix update on object change # swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
# ## Directly set msgbus to work at first addon activation # select_change
bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow' # bpy.app.handlers.frame_change_post.append(cursor_follow)
def unregister(): def unregister():
bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change # bpy.app.handlers.frame_change_post.remove(cursor_follow)
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap # swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
@ -220,6 +179,4 @@ def unregister():
# force remove handler if it's there at unregister # force remove handler if it's there at unregister
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]: if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
bpy.app.handlers.frame_change_post.remove(cursor_follow) bpy.app.handlers.frame_change_post.remove(cursor_follow)
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)

View File

@ -1,5 +1,6 @@
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator
import bgl
from gpu_extras.presets import draw_circle_2d from gpu_extras.presets import draw_circle_2d
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
import gpu import gpu
@ -11,7 +12,6 @@ from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vecto
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
from time import time from time import time
from math import pi, cos, sin from math import pi, cos, sin
from .utils import is_locked, is_hidden
def get_gp_mat(gp, name, set_active=False): def get_gp_mat(gp, name, set_active=False):
@ -190,7 +190,7 @@ class GPTB_OT_eraser(Operator):
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
def draw_callback_px(self): def draw_callback_px(self):
gpu.state.blend_set('ALPHA') bgl.glEnable(bgl.GL_BLEND)
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA) #bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1) #bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
@ -201,7 +201,7 @@ class GPTB_OT_eraser(Operator):
bg_color = area.spaces.active.shading.background_color bg_color = area.spaces.active.shading.background_color
#print(bg_color) #print(bg_color)
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR') shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
shader.bind() shader.bind()
shader.uniform_float("color", (1, 1, 1, 1)) shader.uniform_float("color", (1, 1, 1, 1))
for mouse, radius in self.mouse_path: for mouse, radius in self.mouse_path:
@ -210,7 +210,7 @@ class GPTB_OT_eraser(Operator):
batch.draw(shader) batch.draw(shader)
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24) draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
gpu.state.blend_set('NONE') bgl.glDisable(bgl.GL_BLEND)
@ -232,7 +232,7 @@ class GPTB_OT_eraser(Operator):
hld_stroke.points.add(count=1) hld_stroke.points.add(count=1)
p = hld_stroke.points[-1] p = hld_stroke.points[-1]
p.position = mat_inv @ mouse_3d p.co = mat_inv @ mouse_3d
p.pressure = search_radius * 2000 p.pressure = search_radius * 2000
#context.scene.cursor.location = mouse_3d #context.scene.cursor.location = mouse_3d
@ -253,14 +253,14 @@ class GPTB_OT_eraser(Operator):
#print(self.cuts_data) #print(self.cuts_data)
# for f in self.gp_frames: # for f in self.gp_frames:
# for s in [s for s in f.drawing.strokes if s.material_index==self.hld_index]: # for s in [s for s in f.strokes if s.material_index==self.hld_index]:
# f.drawing.strokes.remove(s) # f.strokes.remove(s)
#gp.data.materials.pop(index=self.hld_index) #gp.data.materials.pop(index=self.hld_index)
#bpy.data.materials.remove(self.hld_mat) #bpy.data.materials.remove(self.hld_mat)
bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT' context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT' #context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
@ -282,7 +282,7 @@ class GPTB_OT_eraser(Operator):
bpy.ops.gpencil.select_all(action='DESELECT') bpy.ops.gpencil.select_all(action='DESELECT')
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False) 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.drawing.strokes] strokes = [s for f in self.gp_frames for s in f.strokes]
#print('select_circle', time()-t1) #print('select_circle', time()-t1)
t2 = time() t2 = time()
@ -310,18 +310,18 @@ class GPTB_OT_eraser(Operator):
bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True) bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True)
new_p1 = stroke.points[p1_index+1] new_p1 = stroke.points[p1_index+1]
new_p1.position = mat_inv@intersects[0] new_p1.co = mat_inv@intersects[0]
new_points += [(stroke, p1_index+1)] new_points += [(stroke, p1_index+1)]
#print('number_cuts', number_cuts) #print('number_cuts', number_cuts)
if number_cuts == 2: if number_cuts == 2:
new_p2 = stroke.points[p1_index+2] new_p2 = stroke.points[p1_index+2]
new_p2.position = mat_inv@( (intersects[0] + intersects[1])/2 ) new_p2.co = mat_inv@( (intersects[0] + intersects[1])/2 )
#new_points += [new_p2] #new_points += [new_p2]
new_p3 = stroke.points[p1_index+3] new_p3 = stroke.points[p1_index+3]
new_p3.position = mat_inv@intersects[1] new_p3.co = mat_inv@intersects[1]
new_points += [(stroke, p1_index+3)] new_points += [(stroke, p1_index+3)]
#print('subdivide', time() - t3) #print('subdivide', time() - t3)
@ -330,7 +330,7 @@ class GPTB_OT_eraser(Operator):
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False) 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.drawing.strokes if s.select] selected_strokes = [s for f in self.gp_frames for s in f.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)] 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() bpy.ops.gpencil.select_less()
@ -342,7 +342,7 @@ class GPTB_OT_eraser(Operator):
''' '''
t4 = time() t4 = time()
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select] selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
if selected_strokes: if selected_strokes:
bpy.ops.gpencil.delete(type='POINTS') bpy.ops.gpencil.delete(type='POINTS')
@ -359,9 +359,9 @@ class GPTB_OT_eraser(Operator):
#bpy.ops.object.mode_set(mode='OBJECT') #bpy.ops.object.mode_set(mode='OBJECT')
context.scene.tool_settings.gpencil_selectmode_edit = self.gpencil_selectmode_edit context.scene.tool_settings.gpencil_selectmode_edit = self.gpencil_selectmode_edit
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL') bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
#selected_strokes = [s for s in self.gp_frame.drawing.strokes if s.select] #selected_strokes = [s for s in self.gp_frame.strokes if s.select]
#bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL') #bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
def modal(self, context, event): def modal(self, context, event):
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y)) self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
@ -441,23 +441,23 @@ class GPTB_OT_eraser(Operator):
t0 = time() t0 = time()
gp_mats = gp.data.materials gp_mats = gp.data.materials
gp_layers = [l for l in gp.data.layers if not is_locked(l) or is_hidden(l)] gp_layers = [l for l in gp.data.layers if not l.lock or l.hide]
self.gp_frames = [l.current_frame() for l in gp_layers] self.gp_frames = [l.active_frame for l in gp_layers]
''' '''
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes] points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.strokes]
points_data = [(s, f, m) for s, f, m in points_data if not m.grease_pencil.hide or m.grease_pencil.lock] 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) print('get_gp_points', time()-t0)
t0 = time() t0 = time()
#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, 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.position)-org).normalized()*1) 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)]
print('points_to_2d', time()-t0) print('points_to_2d', time()-t0)
#print(points_data) #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] 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: #for s, f, m, p, co in self.points_data:
# p.position = co # p.co = co
t0 = time() t0 = time()
@ -482,7 +482,7 @@ class GPTB_OT_eraser(Operator):
self.hld_strokes = [] self.hld_strokes = []
for f in self.gp_frames: for f in self.gp_frames:
hld_stroke = f.drawing.strokes.new() hld_stroke = f.strokes.new()
hld_stroke.start_cap_mode = 'ROUND' hld_stroke.start_cap_mode = 'ROUND'
hld_stroke.end_cap_mode = 'ROUND' hld_stroke.end_cap_mode = 'ROUND'
hld_stroke.material_index = self.hld_index hld_stroke.material_index = self.hld_index

View File

@ -2,14 +2,8 @@ import bpy
import os import os
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from . import utils from . import utils
from bpy.props import (BoolProperty,
PointerProperty,
CollectionProperty,
StringProperty)
def remove_stroke_exact_duplications(apply=True): def remove_stroke_exact_duplications(apply=True):
'''Remove accidental stroke duplication (points exactly in the same place) '''Remove accidental stroke duplication (points exactly in the same place)
:apply: Remove the duplication instead of just listing dupes :apply: Remove the duplication instead of just listing dupes
@ -22,15 +16,15 @@ def remove_stroke_exact_duplications(apply=True):
for l in gp.layers: for l in gp.layers:
for f in l.frames: for f in l.frames:
stroke_list = [] stroke_list = []
for s in reversed(f.drawing.strokes): for s in reversed(f.strokes):
point_list = [p.position for p in s.points] point_list = [p.co for p in s.points]
if point_list in stroke_list: if point_list in stroke_list:
ct += 1 ct += 1
if apply: if apply:
# Remove redundancy # Remove redundancy
f.drawing.strokes.remove(s) f.strokes.remove(s)
else: else:
stroke_list.append(point_list) stroke_list.append(point_list)
return ct return ct
@ -59,10 +53,6 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# Disable use light on all object # Disable use light on all object
# Remove redundant strokes in frames # Remove redundant strokes in frames
apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False,
description="Apply possible fixes instead of just listing (pop the list again in fix mode)",
options={'SKIP_SAVE'})
def invoke(self, context, event): def invoke(self, context, event):
# need some self-control (I had to...) # need some self-control (I had to...)
self.ctrl = event.ctrl self.ctrl = event.ctrl
@ -73,13 +63,10 @@ class GPTB_OT_file_checker(bpy.types.Operator):
fix = prefs.fixprops fix = prefs.fixprops
problems = [] problems = []
## Old method : Apply fixes based on pref (inverted by ctrl key) apply = not fix.check_only
# # If Ctrl is pressed, invert behavior (invert boolean)
# apply ^= self.ctrl
apply = self.apply_fixes # If Ctrl is pressed, invert behavior (invert boolean)
if self.ctrl: apply ^= self.ctrl
apply = True
## Lock main cam: ## Lock main cam:
if fix.lock_main_cam: if fix.lock_main_cam:
@ -133,7 +120,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
setattr(area.spaces[0], 'show_locked_time', True) setattr(area.spaces[0], 'show_locked_time', True)
## set cursor type ## set cursor type
if context.mode in ("EDIT_GREASE_PENCIL", "SCULPT_GREASE_PENCIL"): if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
tool = fix.select_active_tool tool = fix.select_active_tool
if tool != 'none': if tool != 'none':
if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool: if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool:
@ -158,7 +145,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
## GP Use light disable ## GP Use light disable
if fix.set_gp_use_lights_off: if fix.set_gp_use_lights_off:
gp_with_lights = [o for o in context.scene.objects if o.type == 'GREASEPENCIL' and o.use_grease_pencil_lights] gp_with_lights = [o for o in context.scene.objects if o.type == 'GPENCIL' and o.use_grease_pencil_lights]
if gp_with_lights: if gp_with_lights:
problems.append(f'Disable "Use Lights" on {len(gp_with_lights)} Gpencil objects') problems.append(f'Disable "Use Lights" on {len(gp_with_lights)} Gpencil objects')
if apply: if apply:
@ -182,38 +169,45 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if fix.list_obj_vis_conflict: if fix.list_obj_vis_conflict:
viz_ct = 0 viz_ct = 0
for o in context.scene.objects: for o in context.scene.objects:
if not (o.hide_get() == o.hide_viewport == o.hide_render): if o.hide_viewport != o.hide_render:
hv = 'No' if o.hide_get() else 'Yes'
vp = 'No' if o.hide_viewport else 'Yes' vp = 'No' if o.hide_viewport else 'Yes'
rd = 'No' if o.hide_render else 'Yes' rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1 viz_ct += 1
print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}') print(f'{o.name} : viewport {vp} != render {rd}')
if viz_ct: if viz_ct:
problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE']) problems.append(['gp.list_object_visibility', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
## GP modifiers visibility conflict ## GP modifiers visibility conflict
if fix.list_gp_mod_vis_conflict: if fix.list_gp_mod_vis_conflict:
mod_viz_ct = 0 mod_viz_ct = 0
for o in context.scene.objects: for o in context.scene.objects:
for m in o.modifiers: if o.type == 'GPENCIL':
if m.show_viewport != m.show_render: for m in o.grease_pencil_modifiers:
vp = 'Yes' if m.show_viewport else 'No' if m.show_viewport != m.show_render:
rd = 'Yes' if m.show_render else 'No' vp = 'Yes' if m.show_viewport else 'No'
mod_viz_ct += 1 rd = 'Yes' if m.show_render else 'No'
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}') 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}')
if mod_viz_ct: if mod_viz_ct:
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA']) 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 ## check if GP modifier have broken layer targets
if fix.list_broken_mod_targets: if fix.list_broken_mod_targets:
for o in [o for o in bpy.context.scene.objects if o.type == 'GREASEPENCIL']: for o in [o for o in bpy.context.scene.objects if o.type == 'GPENCIL']:
lay_name_list = [l.name for l in o.data.layers] lay_name_list = [l.info for l in o.data.layers]
for m in o.modifiers: for m in o.grease_pencil_modifiers:
if not hasattr(m, 'layer_filter'): if not hasattr(m, 'layer'):
continue continue
if m.layer_filter != '' and not m.layer_filter in lay_name_list: if m.layer != '' and not m.layer in lay_name_list:
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer_filter}' mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer}'
print(mess) print(mess)
problems.append(mess) problems.append(mess)
@ -283,7 +277,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'") # problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
# for ob in context.scene.objects:#from object # for ob in context.scene.objects:#from object
# if ob.type == 'GREASEPENCIL': # if ob.type == 'GPENCIL':
# ob.data.onion_keyframe_type = 'ALL' # ob.data.onion_keyframe_type = 'ALL'
#### --- print fix/problems report #### --- print fix/problems report
@ -294,13 +288,9 @@ class GPTB_OT_file_checker(bpy.types.Operator):
print(p) print(p)
else: else:
print(p[0]) print(p[0])
if not self.apply_fixes:
## button to call the operator again with apply_fixes set to True
problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}])
# Show in viewport # Show in viewport
title = "Changed Settings" if apply else "Checked Settings (nothing changed)" title = "Changed Settings" if apply else "Checked Settings (dry run, nothing changed)"
utils.show_message_box(problems, _title = title, _icon = 'INFO') utils.show_message_box(problems, _title = title, _icon = 'INFO')
else: else:
self.report({'INFO'}, 'All good') self.report({'INFO'}, 'All good')
@ -500,13 +490,48 @@ class GPTB_OT_links_checker(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self, width=popup_width) return context.window_manager.invoke_props_dialog(self, width=popup_width)
""" OLD links checker with show_message_box
class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
bl_options = {"REGISTER"}
def execute(self, context):
all_lnks = []
has_broken_link = False
## check for broken links
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
lfp = Path(lib)
realib = Path(current)
if not lfp.exists():
has_broken_link = True
all_lnks.append( (f"Broken link: {realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
else:
if realib.as_posix().startswith('//'):
all_lnks.append( (f"Link: {realib.as_posix()}", 'LINKED') )#lfp.as_posix()
else:
all_lnks.append( (f"Link: {realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator): all_lnks.sort(key=lambda x: x[1], reverse=True)
bl_idname = "gp.list_viewport_render_visibility" if all_lnks:
bl_label = "List Viewport And Render Visibility Conflicts" print('===File check===')
for p in all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
utils.show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
return {"FINISHED"} """
class GPTB_OT_list_object_visibility(bpy.types.Operator):
bl_idname = "gp.list_object_visibility"
bl_label = "List Object Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values" bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
def invoke(self, context, event): def invoke(self, context, event):
self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render] self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render]
return context.window_manager.invoke_props_dialog(self, width=250) return context.window_manager.invoke_props_dialog(self, width=250)
@ -522,133 +547,60 @@ class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
def execute(self, context): def execute(self, context):
return {'FINISHED'} return {'FINISHED'}
## basic listing as message box # all in invoke now
# li = []
# viz_ct = 0
# for o in context.scene.objects:
# if o.hide_viewport != o.hide_render:
# vp = 'No' if o.hide_viewport else 'Yes'
# rd = 'No' if o.hide_render else 'Yes'
# viz_ct += 1
# li.append(f'{o.name} : viewport {vp} != render {rd}')
# if li:
# utils.show_message_box(_message=li, _title=f'{viz_ct} visibility conflicts found')
# else:
# self.report({'INFO'}, f"No Object visibility conflict on current scene")
# return {'FINISHED'}
### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_viewlayer"
bl_label = "Sync Visibility From Viewlayer"
bl_description = "Set viewport and render visibility to match viewlayer visibility"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context): ## Only GP modifier
for obj in context.scene.objects: '''
is_hidden = obj.hide_get() # Get viewlayer visibility class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
obj.hide_viewport = is_hidden bl_idname = "gp.list_modifier_visibility"
obj.hide_render = is_hidden bl_label = "List GP Modifiers Visibility Conflicts"
return {'FINISHED'} bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
class GPTB_OT_sync_visibility_from_viewport(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_viewport"
bl_label = "Sync Visibility From Viewport"
bl_description = "Set viewlayer and render visibility to match viewport visibility"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.scene.objects:
is_hidden = obj.hide_viewport
obj.hide_set(is_hidden)
obj.hide_render = is_hidden
return {'FINISHED'}
class GPTB_OT_sync_visibility_from_render(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_render"
bl_label = "Sync Visibility From Render"
bl_description = "Set viewlayer and viewport visibility to match render visibility"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.scene.objects:
is_hidden = obj.hide_render
obj.hide_set(is_hidden)
obj.hide_viewport = is_hidden
return {'FINISHED'}
class GPTB_OT_sync_visibible_to_render(bpy.types.Operator):
bl_idname = "gp.sync_visibible_to_render"
bl_label = "Sync Overall Viewport Visibility To Render"
bl_description = "Set render visibility from"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.scene.objects:
## visible_get is the current visibility status combination of hide_viewport and viewlayer hide (eye)
obj.hide_render = not obj.visible_get()
return {'FINISHED'}
class GPTB_PG_object_visibility(bpy.types.PropertyGroup):
"""Property group to handle object visibility"""
is_hidden: BoolProperty(
name="Hide in Viewport",
description="Toggle object visibility in viewport",
get=lambda self: self.get("is_hidden", False),
set=lambda self, value: self.set_visibility(value)
)
object_name: StringProperty(name="Object Name")
def set_visibility(self, value):
"""Set the visibility using hide_set()"""
obj = bpy.context.view_layer.objects.get(self.object_name)
if obj:
obj.hide_set(value)
self["is_hidden"] = value
class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator):
bl_idname = "gp.list_object_visibility_conflicts"
bl_label = "List Object Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type]
def invoke(self, context, event): def invoke(self, context, event):
# Clear and rebuild both collections self.ob_list = []
self.visibility_items.clear() for o in context.scene.objects:
if o.type != 'GPENCIL':
# Store objects with conflicts continue
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ? if not len(o.grease_pencil_modifiers):
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)] continue
# Create visibility items in same order mods = []
for obj in objects_with_conflicts: for m in o.grease_pencil_modifiers:
item = self.visibility_items.add() if m.show_viewport != m.show_render:
item.object_name = obj.name if not mods:
item["is_hidden"] = obj.hide_get() self.ob_list.append([o, mods])
mods.append(m)
return context.window_manager.invoke_props_dialog(self, width=250) return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
for o in self.ob_list:
# Add sync buttons at the top layout.label(text=o[0].name, icon='OUTLINER_OB_GREASEPENCIL')
row = layout.row(align=False) for m in o[1]:
row.label(text="Sync All Visibility From:") row = layout.row()
row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF') row.label(text='')
row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF') row.label(text=m.name, icon='MODIFIER_ON')
row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF') row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
layout.separator() row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
col = layout.column()
# We can safely iterate over visibility_items since objects are stored in same order
for vis_item in self.visibility_items:
obj = context.view_layer.objects.get(vis_item.object_name)
if not obj:
continue
row = col.row(align=False)
row.label(text=obj.name)
## Viewlayer visibility "as prop" to allow slide toggle
# hide_icon='HIDE_ON' if vis_item.is_hidden else 'HIDE_OFF'
hide_icon='HIDE_ON' if obj.hide_get() else 'HIDE_OFF' # based on object state
row.prop(vis_item, "is_hidden", text="", icon=hide_icon, emboss=False)
# Direct object properties
row.prop(obj, 'hide_viewport', text='', emboss=False)
row.prop(obj, 'hide_render', text='', emboss=False)
def execute(self, context): def execute(self, context):
return {'FINISHED'} return {'FINISHED'}
'''
## not exposed in UI, Check is performed in Check file (can be called in popped menu) ## 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): class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
@ -660,14 +612,24 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
def invoke(self, context, event): def invoke(self, context, event):
self.ob_list = [] self.ob_list = []
for o in context.scene.objects: for o in context.scene.objects:
if not len(o.modifiers): if o.type == 'GPENCIL':
continue if not len(o.grease_pencil_modifiers):
mods = [] continue
for m in o.modifiers: mods = []
if m.show_viewport != m.show_render: for m in o.grease_pencil_modifiers:
if not mods: if m.show_viewport != m.show_render:
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type]) if not mods:
mods.append(m) 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)
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name) 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) return context.window_manager.invoke_props_dialog(self, width=250)
@ -690,13 +652,7 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
classes = ( classes = (
GPTB_OT_list_viewport_render_visibility, # Only viewport and render GPTB_OT_list_object_visibility,
GPTB_OT_sync_visibility_from_viewlayer,
GPTB_OT_sync_visibility_from_viewport,
GPTB_OT_sync_visibility_from_render,
GPTB_OT_sync_visibible_to_render,
GPTB_PG_object_visibility,
GPTB_OT_list_object_visibility_conflicts,
GPTB_OT_list_modifier_visibility, GPTB_OT_list_modifier_visibility,
GPTB_OT_copy_string_to_clipboard, GPTB_OT_copy_string_to_clipboard,
GPTB_OT_copy_multipath_clipboard, GPTB_OT_copy_multipath_clipboard,

View File

@ -7,19 +7,19 @@ import numpy as np
from time import time from time import time
from .utils import (location_to_region, region_to_location) from .utils import (location_to_region, region_to_location)
## DISABLED (in init, also in menu append, see register below)
""" """
## Do not work on multiple object ## Do not work on multiple object
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False): def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject '''Reproject
:all_stroke: affect hidden, locked layers :all_stroke: affect hided, locked layers
''' '''
if restore_frame: if restore_frame:
oframe = bpy.context.scene.frame_current oframe = bpy.context.scene.frame_current
omode = bpy.context.mode omode = bpy.context.mode
# frame_list = [ f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)] # frame_list = [ f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)]
# frame_list = list(set(frame_list)) # frame_list = list(set(frame_list))
# frame_list.sort() # frame_list.sort()
# for fnum in frame_list: # 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) fnum = len(l.frames)
zf = len(str(fnum)) zf = len(str(fnum))
for j, f in enumerate(reversed(l.frames)): # whynot... for j, f in enumerate(reversed(l.frames)): # whynot...
print(f'{obj.name} : {i+1}/{laynum} : {l.name} : {str(j+1).zfill(zf)}/{fnum}{" "*30}', end='\r') print(f'{obj.name} : {i+1}/{laynum} : {l.info} : {str(j+1).zfill(zf)}/{fnum}{" "*30}', end='\r')
scn.frame_set(f.frame_number) # more chance to update the matrix scn.frame_set(f.frame_number) # more chance to update the matrix
bpy.context.view_layer.update() # update the matrix ? bpy.context.view_layer.update() # update the matrix ?
bpy.context.scene.camera.location = bpy.context.scene.camera.location bpy.context.scene.camera.location = bpy.context.scene.camera.location
scn.frame_current = f.frame_number scn.frame_current = f.frame_number
for s in f.drawing.strokes: for s in f.strokes:
for p in s.points: for p in s.points:
p.position = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.position), scn.cursor.location) p.co = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.co), scn.cursor.location)
if restore_frame: if restore_frame:
bpy.context.scene.frame_current = oframe bpy.context.scene.frame_current = oframe
@ -67,8 +67,8 @@ def batch_flat_reproject(obj):
plane_no.rotate(cam_mat) plane_no.rotate(cam_mat)
plane_co = scn.cursor.location plane_co = scn.cursor.location
for s in f.drawing.strokes: for s in f.strokes:
points_co = [obj.matrix_world @ p.position for p in s.points] points_co = [obj.matrix_world @ p.co 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 = [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] 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.add(1) # update
s.points.pop() # update s.points.pop() # update
#for p in s.points: #for p in s.points:
# loc_2d = location_to_region(obj.matrix_world @ p.position) # loc_2d = location_to_region(obj.matrix_world @ p.co)
# p.position = obj.matrix_world.inverted() @ region_to_location(loc_2d, scn.cursor.location) # p.co = obj.matrix_world.inverted() @ region_to_location(loc_2d, scn.cursor.location)
""" """
def batch_flat_reproject(obj): def batch_flat_reproject(obj):
@ -96,14 +96,14 @@ def batch_flat_reproject(obj):
plane_co = scn.cursor.location plane_co = scn.cursor.location
for l in obj.data.layers: for l in obj.data.layers:
f = l.current_frame() f = l.active_frame
if not f: # No active frame if not f: # No active frame
continue continue
if f.frame_number != scn.frame_current: if f.frame_number != scn.frame_current:
f = l.frames.copy(f) # duplicate content of the previous frame f = l.frames.copy(f) # duplicate content of the previous frame
for s in f.drawing.strokes: for s in f.strokes:
points_co = [obj.matrix_world @ p.position for p in s.points] points_co = [obj.matrix_world @ p.co 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 = [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] 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 @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
for o in context.selected_objects: for o in context.selected_objects:
if o.type != 'GREASEPENCIL' or not o.select_get(): if o.type != 'GPENCIL' or not o.select_get():
continue continue
batch_flat_reproject(o) batch_flat_reproject(o)
@ -132,12 +132,12 @@ class GPTB_OT_batch_flat_reproject(bpy.types.Operator):
### -- MENU ENTRY -- ### -- MENU ENTRY --
def flat_reproject_clean_menu(self, context): def flat_reproject_clean_menu(self, context):
if context.mode == 'EDIT_GREASE_PENCIL': if context.mode == 'EDIT_GPENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT') 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') self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
def flat_reproject_context_menu(self, context): def flat_reproject_context_menu(self, context):
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE': if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC') self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
@ -149,12 +149,12 @@ def register():
for cl in classes: for cl in classes:
bpy.utils.register_class(cl) bpy.utils.register_class(cl)
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.append(flat_reproject_context_menu) # bpy.types.VIEW3D_MT_gpencil_edit_context_menu.append(flat_reproject_context_menu)
# bpy.types.GPENCIL_MT_cleanup.append(flat_reproject_clean_menu) # bpy.types.GPENCIL_MT_cleanup.append(flat_reproject_clean_menu)
def unregister(): def unregister():
# bpy.types.GPENCIL_MT_cleanup.remove(flat_reproject_clean_menu) # bpy.types.GPENCIL_MT_cleanup.remove(flat_reproject_clean_menu)
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.remove(flat_reproject_context_menu) # bpy.types.VIEW3D_MT_gpencil_edit_context_menu.remove(flat_reproject_context_menu)
for cl in reversed(classes): for cl in reversed(classes):
bpy.utils.unregister_class(cl) bpy.utils.unregister_class(cl)

View File

@ -2,6 +2,7 @@ import bpy
from mathutils import Vector from mathutils import Vector
from . import utils from . import utils
class GPTB_OT_create_follow_path_curve(bpy.types.Operator): class GPTB_OT_create_follow_path_curve(bpy.types.Operator):
bl_idname = "object.create_follow_path_curve" bl_idname = "object.create_follow_path_curve"
bl_label = "Create Follow Path Curve" bl_label = "Create Follow Path Curve"
@ -134,8 +135,8 @@ class GPTB_OT_go_to_object(bpy.types.Operator):
bpy.ops.object.mode_set(mode='POSE', toggle=False) bpy.ops.object.mode_set(mode='POSE', toggle=False)
self.report({'INFO'}, f'Back to pose mode, {obj.name}') self.report({'INFO'}, f'Back to pose mode, {obj.name}')
elif obj.type == 'GREASEPENCIL': elif obj.type == 'GPENCIL':
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL', toggle=False) bpy.ops.object.mode_set(mode='PAINT_GPENCIL', toggle=False)
else: else:
self.report({'INFO'}, f'Back to object mode, {obj.name}') self.report({'INFO'}, f'Back to object mode, {obj.name}')

View File

@ -79,7 +79,7 @@ class GPTB_OT_rename_data_from_obj(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
if not self.rename_all: if not self.rename_all:
@ -93,7 +93,7 @@ class GPTB_OT_rename_data_from_obj(Operator):
else: else:
oblist = [] oblist = []
for o in context.scene.objects: for o in context.scene.objects:
if o.type == 'GREASEPENCIL': if o.type == 'GPENCIL':
if o.name == o.data.name: if o.name == o.data.name:
continue continue
oblist.append(f'{o.data.name} -> {o.name}') oblist.append(f'{o.data.name} -> {o.name}')
@ -250,7 +250,7 @@ class GPTB_OT_draw_cam(Operator):
drawcam.parent = act drawcam.parent = act
vec = Vector((0,1,0)) vec = Vector((0,1,0))
if act.type == 'GREASEPENCIL': if act.type == 'GPENCIL':
#change vector according to alignement #change vector according to alignement
vec = get_gp_alignement_vector(context) vec = get_gp_alignement_vector(context)
@ -427,15 +427,15 @@ class GPTB_OT_toggle_mute_animation(Operator):
pool = context.scene.objects pool = context.scene.objects
for o in pool: for o in pool:
if self.mode == 'GREASEPENCIL' and o.type != 'GREASEPENCIL': if self.mode == 'GPENCIL' and o.type != 'GPENCIL':
continue continue
if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'): if self.mode == 'OBJECT' and o.type in ('GPENCIL', 'CAMERA'):
continue continue
if self.mode == 'CAMERA' and o.type != 'CAMERA': if self.mode == 'CAMERA' and o.type != 'CAMERA':
continue continue
# mute attribute animation for GP and cameras # mute attribute animation for GP and cameras
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data: if o.type in ('GPENCIL', 'CAMERA') and o.data.animation_data:
gp_act = o.data.animation_data.action gp_act = o.data.animation_data.action
if gp_act: if gp_act:
print(f'\n---{o.name} data:') print(f'\n---{o.name} data:')
@ -473,9 +473,9 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
else: else:
pool = context.scene.objects pool = context.scene.objects
for o in pool: for o in pool:
if o.type != 'GREASEPENCIL': if o.type != 'GPENCIL':
continue continue
for m in o.modifiers: for m in o.grease_pencil_modifiers:
# skip modifier that are not visible in render # skip modifier that are not visible in render
if not m.show_render: if not m.show_render:
continue continue
@ -506,12 +506,12 @@ class GPTB_OT_list_disabled_anims(Operator):
pool = context.scene.objects pool = context.scene.objects
for o in pool: for o in pool:
# if self.skip_gp and o.type == 'GREASEPENCIL': # if self.skip_gp and o.type == 'GPENCIL':
# continue # continue
# if self.skip_obj and o.type != 'GREASEPENCIL': # if self.skip_obj and o.type != 'GPENCIL':
# continue # continue
if o.type == 'GREASEPENCIL': if o.type == 'GPENCIL':
if o.data.animation_data: if o.data.animation_data:
gp_act = o.data.animation_data.action gp_act = o.data.animation_data.action
if gp_act: if gp_act:
@ -611,7 +611,7 @@ class GPTB_OT_clear_active_frame(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
obj = context.object obj = context.object
@ -619,18 +619,18 @@ class GPTB_OT_clear_active_frame(Operator):
if not l: if not l:
self.report({'ERROR'}, 'No layers') self.report({'ERROR'}, 'No layers')
return {'CANCELLED'} return {'CANCELLED'}
f = l.current_frame() f = l.active_frame
if not f: if not f:
self.report({'ERROR'}, 'No active frame') self.report({'ERROR'}, 'No active frame')
return {'CANCELLED'} return {'CANCELLED'}
ct = len(f.drawing.strokes) ct = len(f.strokes)
if not ct: if not ct:
self.report({'ERROR'}, 'Active frame already empty') self.report({'ERROR'}, 'Active frame already empty')
return {'CANCELLED'} return {'CANCELLED'}
for s in reversed(f.drawing.strokes): for s in reversed(f.strokes):
f.drawing.strokes.remove(s) f.strokes.remove(s)
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)') self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
return {'FINISHED'} return {'FINISHED'}
@ -644,7 +644,7 @@ class GPTB_OT_check_canvas_alignement(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
# if lock_axis is 'VIEW' then the draw axis is always aligned # if lock_axis is 'VIEW' then the draw axis is always aligned
return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW' return context.object and context.object.type == 'GPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
def execute(self, context): def execute(self, context):
if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW': if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW':
@ -673,7 +673,7 @@ class GPTB_OT_step_select_frames(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
start : bpy.props.IntProperty(name='Start Frame', start : bpy.props.IntProperty(name='Start Frame',
description='Start frame of the step animation', description='Start frame of the step animation',

View File

@ -1,6 +1,5 @@
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator
from . import utils
def get_layer_list(self, context): def get_layer_list(self, context):
@ -9,7 +8,7 @@ def get_layer_list(self, context):
return [('None', 'None','None')] return [('None', 'None','None')]
if not context.object: if not context.object:
return [('None', 'None','None')] return [('None', 'None','None')]
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active] return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
# try: # try:
# except: # except:
# return [("", "", "")] # return [("", "", "")]
@ -18,30 +17,23 @@ def get_layer_list(self, context):
class GPTB_OT_duplicate_send_to_layer(Operator) : class GPTB_OT_duplicate_send_to_layer(Operator) :
bl_idname = "gp.duplicate_send_to_layer" bl_idname = "gp.duplicate_send_to_layer"
bl_label = 'Duplicate Send To Layer' bl_label = 'Duplicate and 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 # important to have the updated enum here as bl_property
bl_property = "layers_enum" bl_property = "layers_enum"
layers_enum : bpy.props.EnumProperty( layers_enum : bpy.props.EnumProperty(
name="Duplicate to layers", name="Duplicate to layers",
description="Duplicate selected keys in active layer and send them to chosen layer", description="Duplicate selected keys in active layer and send them to choosen layer",
items=get_layer_list, items=get_layer_list,
options={'HIDDEN'}, options={'HIDDEN'},
) )
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) 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 @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'\ return context.object and context.object.type == 'GPENCIL'\
and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GPENCIL' and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GPENCIL'
# history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self) # history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self)
@ -64,27 +56,31 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
replaced = len(to_replace) replaced = len(to_replace)
## Remove overlapping frames ## remove overlapping frames
for f in reversed(to_replace): for f in reversed(to_replace):
target_layer.frames.remove(f.frame_number) target_layer.frames.remove(f)
## Copy original frames ## copy original frames
for f in selected_frames: for f in selected_frames:
utils.copy_frame_at(f, target_layer, f.frame_number) target_layer.frames.copy(f)
# target_layer.frames.copy(f) # GPv2
sent = len(selected_frames) sent = len(selected_frames)
## Delete original frames as an option ## delete original frames as an option
if self.delete_source: if self.delete_source:
for f in reversed(selected_frames): for f in reversed(selected_frames):
act_layer.frames.remove(f.frame_number) act_layer.frames.remove(f)
mess = f'{sent} keys moved'
else:
mess = f'{sent} keys copied'
mess = f'{sent} keys copied'
if replaced: if replaced:
mess += f' ({replaced} 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)
self.report({'INFO'}, mess) self.report({'INFO'}, mess)
return {'FINISHED'} return {'FINISHED'}
@ -113,6 +109,9 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
addon_keymaps = [] addon_keymaps = []
def register_keymaps(): def register_keymaps():
# pref = get_addon_prefs()
# if not pref.kfj_use_shortcut:
# return
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") 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) kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True)
@ -130,12 +129,6 @@ def unregister_keymaps():
addon_keymaps.clear() addon_keymaps.clear()
def menu_duplicate_and_send_to_layer(self, context):
if context.space_data.ui_mode == 'GPENCIL':
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 = ( classes = (
GPTB_OT_duplicate_send_to_layer, GPTB_OT_duplicate_send_to_layer,
) )
@ -146,19 +139,12 @@ def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
register_keymaps() 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(): def unregister():
if bpy.app.background: if bpy.app.background:
return 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() unregister_keymaps()
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)

View File

@ -1,5 +1,5 @@
import bpy import bpy
from .utils import get_addon_prefs, is_locked, is_hidden from .utils import get_addon_prefs
from bpy.props import BoolProperty ,EnumProperty ,StringProperty from bpy.props import BoolProperty ,EnumProperty ,StringProperty
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator): class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
@ -10,7 +10,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
next : BoolProperty( next : BoolProperty(
name="Next GP keyframe", description="Go to next active GP keyframe", name="Next GP keyframe", description="Go to next active GP keyframe",
@ -36,7 +36,6 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4), ('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4),
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5), ('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5),
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6), ('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6),
('GENERATED', 'Generated', '', 'KEYTYPE_GENERATED_VEC', 7),
)) ))
def execute(self, context): def execute(self, context):
@ -45,15 +44,15 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
return {"CANCELLED"} return {"CANCELLED"}
if self.target == 'ACTIVE': if self.target == 'ACTIVE':
gpl = [l for l in context.object.data.layers if l.select and not is_hidden(l)] gpl = [l for l in context.object.data.layers if l.select and not l.hide]
if not context.object.data.layers.active in gpl: if not context.object.data.layers.active in gpl:
gpl.append(context.object.data.layers.active) gpl.append(context.object.data.layers.active)
elif self.target == 'VISIBLE': elif self.target == 'VISIBLE':
gpl = [l for l in context.object.data.layers if not is_hidden(l)] gpl = [l for l in context.object.data.layers if not l.hide]
elif self.target == 'ACCESSIBLE': elif self.target == 'ACCESSIBLE':
gpl = [l for l in context.object.data.layers if not is_hidden(l) and not is_locked(l)] gpl = [l for l in context.object.data.layers if not l.hide and not l.lock]
if self.keyframe_type != 'NONE': if self.keyframe_type != 'NONE':
# use shortcut choice override # use shortcut choice override

View File

@ -21,7 +21,6 @@ from .utils import get_addon_prefs, is_vector_close
# PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering # PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
# TODO: allow a more flexible prefix pattern
def layer_name_build(layer, prefix='', desc='', suffix=''): def layer_name_build(layer, prefix='', desc='', suffix=''):
'''GET a layer and argument to build and assign name '''GET a layer and argument to build and assign name
@ -31,7 +30,7 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
prefs = get_addon_prefs() prefs = get_addon_prefs()
sep = prefs.separator sep = prefs.separator
name = old = layer.name name = old = layer.info
pattern = PATTERN.replace('_', sep) # set separator pattern = PATTERN.replace('_', sep) # set separator
@ -70,7 +69,7 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
# check if name is available without the increment ending # check if name is available without the increment ending
new = f'{grp}{tag}{name}{sfix}' new = f'{grp}{tag}{name}{sfix}'
layer.name = new layer.info = new
## update name in modifier targets ## update name in modifier targets
if old != new: if old != new:
@ -79,11 +78,11 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
# maybe a more elegant way exists to find all objects users ? # maybe a more elegant way exists to find all objects users ?
# update Gpencil modifier targets # update Gpencil modifier targets
for mod in ob_user.modifiers: for mod in ob_user.grease_pencil_modifiers:
if not hasattr(mod, 'layer_filter'): if not hasattr(mod, 'layer'):
continue continue
if mod.layer_filter == old: if mod.layer == old:
mod.layer_filter = new mod.layer = new
""" """
def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''): def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
@ -94,7 +93,7 @@ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
prefs = get_addon_prefs() prefs = get_addon_prefs()
sep = prefs.separator sep = prefs.separator
name = layer.name name = layer.info
pattern = pattern.replace('_', sep) # set separator pattern = pattern.replace('_', sep) # set separator
@ -123,7 +122,7 @@ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
p4 = sep + suffix.upper().strip() p4 = sep + suffix.upper().strip()
new = f'{p1}{p2}{p3}{p4}' new = f'{p1}{p2}{p3}{p4}'
layer.name = new layer.info = new
""" """
## multi-prefix solution (Caps letters) ## multi-prefix solution (Caps letters)
@ -156,16 +155,13 @@ class GPTB_OT_layer_name_build(Operator):
gpl = ob.data.layers gpl = ob.data.layers
act = gpl.active act = gpl.active
if not act: if not act:
act = ob.data.layer_groups.active self.report({'ERROR'}, 'no layer active')
if not act:
self.report({'ERROR'}, 'No layer active')
return {"CANCELLED"} return {"CANCELLED"}
layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix) layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
## /!\ Deactivate multi-selection on layer ! ## Deactivate multi-selection on layer !
## Somethimes it affect a random layer that is still considered selected ## somethimes it affect a random layer that is still considered selected
# for l in gpl: # for l in gpl:
# if l.select or l == act: # if l.select or l == act:
# layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix) # layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
@ -173,6 +169,79 @@ class GPTB_OT_layer_name_build(Operator):
return {"FINISHED"} return {"FINISHED"}
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)
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(' -')
elif res and mode in ('TOGGLE', 'UNGROUP'):
# found : delete group prefix
l.info = res.group(2)
class GPTB_OT_layer_group_toggle(Operator):
bl_idname = "gp.layer_group_toggle"
bl_label = "Group Toggle"
bl_description = "Group or ungroup a layer"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return True
# group : StringProperty(default='', options={'SKIP_SAVE'})
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
for l in gpl:
if l.select or l == act:
grp_toggle(l)
return {"FINISHED"}
class GPTB_OT_layer_new_group(Operator):
bl_idname = "gp.layer_new_group"
bl_label = "New Group"
bl_description = "Create a group from active layer"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
res = re.search(PATTERN, act.info)
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}')
return {"CANCELLED"}
if name in [l.info.strip(' -') for l in gpl]:
self.report({'WARNING'}, f'Name already exists: {act.info}')
return {"FINISHED"}
grp_toggle(act, mode='GROUP')
n = gpl.new(name, set_active=False)
n.use_onion_skinning = n.use_lights = False
n.hide = True
n.opacity = 0
return {"FINISHED"}
#-## SELECTION MANAGEMENT ##-# #-## SELECTION MANAGEMENT ##-#
def activate_channel_group_color(context): def activate_channel_group_color(context):
@ -192,9 +261,9 @@ def build_layers_targets_from_dopesheet(context):
if dopeset.show_only_selected: if dopeset.show_only_selected:
pool = [o for o in context.selected_objects if o.type == 'GREASEPENCIL'] pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
else: else:
pool = [o for o in context.scene.objects if o.type == 'GREASEPENCIL'] pool = [o for o in context.scene.objects if o.type == 'GPENCIL']
if not dopeset.show_hidden: if not dopeset.show_hidden:
pool = [o for o in pool if o.visible_get()] pool = [o for o in pool if o.visible_get()]
@ -203,7 +272,7 @@ def build_layers_targets_from_dopesheet(context):
# apply search filter # apply search filter
if dopeset.filter_text: if dopeset.filter_text:
layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.name.lower()) ^ dopeset.use_filter_invert] layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.info.lower()) ^ dopeset.use_filter_invert]
return layer_pool return layer_pool
@ -223,7 +292,7 @@ class GPTB_OT_select_set_same_prefix(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'}, mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
items=( items=(
@ -261,35 +330,35 @@ class GPTB_OT_select_set_same_prefix(Operator):
self.report({'ERROR'}, 'No active layer to base action') self.report({'ERROR'}, 'No active layer to base action')
return {"CANCELLED"} return {"CANCELLED"}
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}') print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
res = re.search(PATTERN, act.name) res = re.search(PATTERN, act.info)
if not res: if not res:
self.report({'ERROR'}, f'Error scanning {act.name}') self.report({'ERROR'}, f'Error scanning {act.info}')
return {"CANCELLED"} return {"CANCELLED"}
namespace = res.group('tag') namespace = res.group('tag')
if not namespace: if not namespace:
self.report({'WARNING'}, f'No prefix detected in {act.name} with separator {sep}') self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}')
return {"CANCELLED"} return {"CANCELLED"}
if self.mode == 'SELECT': if self.mode == 'SELECT':
## with split ## with split
# namespace = act.name.split(sep,1)[0] # namespace = act.info.split(sep,1)[0]
# namespace_bool_list = [l.name.split(sep,1)[0] == namespace for l in gpl] # namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl]
## with reg # only active ## with reg # only active
# namespace_bool_list = [l.name.split(sep,1)[0] + sep == namespace for l in gpl] # namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl]
# gpl.foreach_set('select', namespace_bool_list) # gpl.foreach_set('select', namespace_bool_list)
## don't work Need Foreach set per gp ## don't work Need Foreach set per gp
# for l in pool: # for l in pool:
# l.select = l.name.split(sep,1)[0] + sep == namespace # l.select = l.info.split(sep,1)[0] + sep == namespace
for gp, layers in gp_dic.items(): for gp, layers in gp_dic.items():
# check namespace + restrict selection to visible layers according to filters # check namespace + restrict selection to visible layers according to filters
# TODO : Should use the regex pattern to detect and compare r.group('tag') # TODO : Should use the regex pattern to detect and compare r.group('tag')
namespace_bool_list = [(l in layers) and (l.name.split(sep,1)[0] + sep == namespace) for l in gp.layers] namespace_bool_list = [(l in layers) and (l.info.split(sep,1)[0] + sep == namespace) for l in gp.layers]
gp.layers.foreach_set('select', namespace_bool_list) gp.layers.foreach_set('select', namespace_bool_list)
elif self.mode == 'SET': elif self.mode == 'SET':
@ -311,7 +380,7 @@ class GPTB_OT_select_set_same_color(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'}, mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
items=( items=(
@ -346,7 +415,7 @@ class GPTB_OT_select_set_same_color(Operator):
self.report({'ERROR'}, 'No active layer to base action') self.report({'ERROR'}, 'No active layer to base action')
return {"CANCELLED"} return {"CANCELLED"}
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}') print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
color = act.channel_color color = act.channel_color
if self.mode == 'SELECT': if self.mode == 'SELECT':
## NEED FOREACH TO APPLY SELECT ## NEED FOREACH TO APPLY SELECT
@ -357,7 +426,7 @@ class GPTB_OT_select_set_same_color(Operator):
# On multiple objects -- don't work, need foreach # On multiple objects -- don't work, need foreach
# for l in pool: # for l in pool:
# print(l.id_data.name, l.name, l.channel_color == act.channel_color) # print(l.id_data.name, l.info, l.channel_color == act.channel_color)
# l.select = l.channel_color == act.channel_color # l.select = l.channel_color == act.channel_color
""" """
@ -394,38 +463,38 @@ def replace_layer_name(target, replacement, selected_only=True, prefix_only=True
gpl = bpy.context.object.data.layers gpl = bpy.context.object.data.layers
if selected_only: if selected_only:
lays = [l for l in gpl if l.select] # exclude : l.name != 'background' lays = [l for l in gpl if l.select] # exclude : l.info != 'background'
else: else:
lays = [l for l in gpl] # exclude : if l.name != 'background' lays = [l for l in gpl] # exclude : if l.info != 'background'
ct = 0 ct = 0
for l in lays: for l in lays:
old = l.name old = l.info
if regex: if regex:
new = re.sub(target, replacement, l.name) new = re.sub(target, replacement, l.info)
if old != new: if old != new:
l.name = new l.info = new
print('rename:', old, '-->', new) print('rename:', old, '-->', new)
ct += 1 ct += 1
continue continue
if prefix_only: if prefix_only:
if not sep in l.name: if not sep in l.info:
# only if separator exists # only if separator exists
continue continue
splited = l.name.split(sep) splited = l.info.split(sep)
prefix = splited[0] prefix = splited[0]
new_prefix = prefix.replace(target, replacement) new_prefix = prefix.replace(target, replacement)
if prefix != new_prefix: if prefix != new_prefix:
splited[0] = new_prefix splited[0] = new_prefix
l.name = sep.join(splited) l.info = sep.join(splited)
print('rename:', old, '-->', l.name) print('rename:', old, '-->', l.info)
ct += 1 ct += 1
else: else:
new = l.name.replace(target, replacement) new = l.info.replace(target, replacement)
if old != new: if old != new:
l.name = new l.info = new
print('rename:', old, '-->', new) print('rename:', old, '-->', new)
ct += 1 ct += 1
return ct return ct
@ -438,7 +507,7 @@ class GPTB_OT_rename_gp_layer(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE') 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') replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
@ -479,7 +548,7 @@ class GPTB_OT_rename_gp_layer(Operator):
## --- UI layer panel--- ## --- UI layer panel---
def layer_name_builder_ui(self, context): def layer_name_builder_ui(self, context):
'''appended to DATA_PT_grease_pencil_layers''' '''appended to DATA_PT_gpencil_layers'''
prefs = get_addon_prefs() prefs = get_addon_prefs()
if not prefs.show_prefix_buttons: if not prefs.show_prefix_buttons:
@ -488,7 +557,7 @@ def layer_name_builder_ui(self, context):
return return
layout = self.layout layout = self.layout
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'} # {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
# layout.separator() # layout.separator()
col = layout.column() col = layout.column()
@ -564,7 +633,7 @@ def gpencil_dopesheet_header(self, context):
'''to append in DOPESHEET_HT_header''' '''to append in DOPESHEET_HT_header'''
layout = self.layout layout = self.layout
st = context.space_data st = context.space_data
if st.mode != 'GREASEPENCIL': if st.mode != 'GPENCIL':
return return
row = layout.row(align=True) row = layout.row(align=True)
@ -577,7 +646,6 @@ def gpencil_dopesheet_header(self, context):
def gpencil_layer_dropdown_menu(self, context): def gpencil_layer_dropdown_menu(self, context):
'''to append in GPENCIL_MT_layer_context_menu''' '''to append in GPENCIL_MT_layer_context_menu'''
self.layout.operator('gp.create_empty_frames', icon='KEYFRAME')
self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE') self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE')
## handler and msgbus ## handler and msgbus
@ -585,7 +653,7 @@ def gpencil_layer_dropdown_menu(self, context):
def obj_layer_name_callback(): def obj_layer_name_callback():
'''assign layer name properties so user an tweak it''' '''assign layer name properties so user an tweak it'''
ob = bpy.context.object ob = bpy.context.object
if not ob or ob.type != 'GREASEPENCIL': if not ob or ob.type != 'GPENCIL':
return return
if not ob.data.layers.active: if not ob.data.layers.active:
return return
@ -595,7 +663,7 @@ def obj_layer_name_callback():
for l in ob.data.layers: for l in ob.data.layers:
l.select = l == ob.data.layers.active l.select = l == ob.data.layers.active
res = re.search(PATTERN, ob.data.layers.active.name.strip()) res = re.search(PATTERN, ob.data.layers.active.info.strip())
if not res: if not res:
return return
if not res.group('name'): if not res.group('name'):
@ -607,28 +675,14 @@ def obj_layer_name_callback():
# print('inc:', res.group('inc')) # print('inc:', res.group('inc'))
bpy.context.scene.gptoolprops['layer_name'] = res.group('name') bpy.context.scene.gptoolprops['layer_name'] = res.group('name')
## old gpv2
# def subscribe_layer_change():
# subscribe_to = (bpy.types.GreasePencilLayers, "active_index")
# bpy.msgbus.subscribe_rna(
# key=subscribe_to,
# # owner of msgbus subcribe (for clearing later)
# # owner=handle,
# owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime...
# # Args passed to callback function (tuple)
# args=(),
# # Callback function for property update
# notify=obj_layer_name_callback,
# options={'PERSISTENT'},
# )
def subscribe_layer_change(): def subscribe_layer_change():
subscribe_to = (bpy.types.GreasePencilv3Layers, "active") subscribe_to = (bpy.types.GreasePencilLayers, "active_index")
bpy.msgbus.subscribe_rna( bpy.msgbus.subscribe_rna(
key=subscribe_to, key=subscribe_to,
# owner of msgbus subcribe (for clearing later) # owner of msgbus subcribe (for clearing later)
# owner=handle, # owner=handle,
owner=bpy.types.GreasePencilv3, # <-- can attach to an ID during all it's lifetime... owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime...
# Args passed to callback function (tuple) # Args passed to callback function (tuple)
args=(), args=(),
# Callback function for property update # Callback function for property update
@ -636,7 +690,6 @@ def subscribe_layer_change():
options={'PERSISTENT'}, options={'PERSISTENT'},
) )
@persistent @persistent
def subscribe_layer_change_handler(dummy): def subscribe_layer_change_handler(dummy):
subscribe_layer_change() subscribe_layer_change()
@ -670,7 +723,7 @@ class GPTB_PT_layer_name_ui(bpy.types.Panel):
row = layout.row() row = layout.row()
row.activate_init = True row.activate_init = True
row.label(icon='OUTLINER_DATA_GP_LAYER') row.label(icon='OUTLINER_DATA_GP_LAYER')
row.prop(context.object.data.layers.active, 'name', text='') row.prop(context.object.data.layers.active, 'info', text='')
def add_layer(context): def add_layer(context):
bpy.ops.gpencil.layer_add() bpy.ops.gpencil.layer_add()
@ -683,7 +736,7 @@ class GPTB_OT_add_gp_layer_with_rename(Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
add_layer(context) add_layer(context)
@ -697,7 +750,7 @@ class GPTB_OT_add_gp_layer(Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
add_layer(context) add_layer(context)
@ -723,7 +776,7 @@ def register_keymaps():
##---# F2 rename calls ##---# F2 rename calls
## Direct rename active layer in Paint mode ## Direct rename active layer in Paint mode
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY") km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS') kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
kmi.properties.name = 'GPTB_PT_layer_name_ui' kmi.properties.name = 'GPTB_PT_layer_name_ui'
kmi.properties.keep_open = False kmi.properties.keep_open = False
@ -749,6 +802,8 @@ def unregister_keymaps():
classes=( classes=(
GPTB_OT_rename_gp_layer, GPTB_OT_rename_gp_layer,
GPTB_OT_layer_name_build, GPTB_OT_layer_name_build,
GPTB_OT_layer_group_toggle,
GPTB_OT_layer_new_group,
GPTB_OT_select_set_same_prefix, GPTB_OT_select_set_same_prefix,
GPTB_OT_select_set_same_color, GPTB_OT_select_set_same_color,
@ -762,9 +817,9 @@ def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.DATA_PT_grease_pencil_layers.prepend(layer_name_builder_ui) bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder_ui)
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header) bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.append(gpencil_layer_dropdown_menu) bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu)
bpy.app.handlers.load_post.append(subscribe_layer_change_handler) bpy.app.handlers.load_post.append(subscribe_layer_change_handler)
register_keymaps() register_keymaps()
@ -774,13 +829,12 @@ def register():
def unregister(): def unregister():
unregister_keymaps() unregister_keymaps()
bpy.app.handlers.load_post.remove(subscribe_layer_change_handler) bpy.app.handlers.load_post.remove(subscribe_layer_change_handler)
bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.remove(gpencil_layer_dropdown_menu) bpy.types.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu)
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header) bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
bpy.types.DATA_PT_grease_pencil_layers.remove(layer_name_builder_ui) bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder_ui)
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
# Delete layer index trigger # delete layer index trigger
# /!\ can remove msgbus made for other functions or other addons using same owner bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)

View File

@ -26,11 +26,10 @@ class GPT_OT_layer_nav(bpy.types.Operator):
prefs = utils.get_addon_prefs() prefs = utils.get_addon_prefs()
if not prefs.nav_use_fade: if not prefs.nav_use_fade:
if self.direction == 'DOWN': if self.direction == 'DOWN':
utils.iterate_active_layer(context.grease_pencil, -1) utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info')
# utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'name') # gpv2
if self.direction == 'UP': if self.direction == 'UP':
utils.iterate_active_layer(context.grease_pencil, 1) utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info')
return {'FINISHED'} return {'FINISHED'}
## get up and down keys for use in modal ## get up and down keys for use in modal
@ -92,11 +91,12 @@ class GPT_OT_layer_nav(bpy.types.Operator):
context.space_data.overlay.gpencil_fade_layer = fade context.space_data.overlay.gpencil_fade_layer = fade
if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'): if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'):
_val = utils.iterate_active_layer(context.grease_pencil, -1) _val = utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info')
trigger = True trigger = True
if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'): if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'):
_val = utils.iterate_active_layer(context.grease_pencil, 1) _val = utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info')
# utils.iterate_selector(bpy.context.scene.grease_pencil.layers, 'active_index', 1, info_attr = 'info')#layers
trigger = True trigger = True
if trigger: if trigger:
@ -127,7 +127,7 @@ addon_keymaps = []
def register_keymap(): def register_keymap():
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY") km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_UP', value='PRESS') kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_UP', value='PRESS')
kmi.properties.direction = 'UP' kmi.properties.direction = 'UP'

View File

@ -4,7 +4,7 @@ import mathutils
from mathutils import Vector, Matrix, geometry from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from time import time from time import time
from .utils import get_gp_draw_plane, location_to_region, region_to_location, is_locked, is_hidden from .utils import get_gp_draw_plane, location_to_region, region_to_location
class GP_OT_pick_closest_layer(Operator): class GP_OT_pick_closest_layer(Operator):
bl_idname = "gp.pick_closest_layer" bl_idname = "gp.pick_closest_layer"
@ -14,7 +14,7 @@ class GP_OT_pick_closest_layer(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL' return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
stroke_filter : bpy.props.EnumProperty(name='Target', stroke_filter : bpy.props.EnumProperty(name='Target',
default='STROKE', default='STROKE',
@ -33,8 +33,8 @@ class GP_OT_pick_closest_layer(Operator):
mouse_vec3 = Vector((*self.init_mouse, 0)) mouse_vec3 = Vector((*self.init_mouse, 0))
co, index, _dist = kd.find(mouse_vec3) co, index, _dist = kd.find(mouse_vec3)
layer = self.point_pair[index][1] lid = self.point_pair[index][1]
return layer return lid
def invoke(self, context, event): def invoke(self, context, event):
self.t0 = time() self.t0 = time()
@ -47,7 +47,7 @@ class GP_OT_pick_closest_layer(Operator):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
if context.object.data.layers.active: if context.object.data.layers.active:
layout.label(text=f'Layer: {context.object.data.layers.active.name}') layout.label(text=f'Layer: {context.object.data.layers.active.info}')
layout.prop(self, 'stroke_filter') layout.prop(self, 'stroke_filter')
def modal(self, context, event): def modal(self, context, event):
@ -74,50 +74,50 @@ class GP_OT_pick_closest_layer(Operator):
self.inv_mat = self.ob.matrix_world.inverted() self.inv_mat = self.ob.matrix_world.inverted()
self.point_pair = [] self.point_pair = []
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if gp.use_multiedit:
for layer in gp.layers: for layer_id, l in enumerate(gp.layers):
if is_hidden(layer): if l.hide:# l.lock or
continue continue
for f in layer.frames: for f in l.frames:
if not f.select: if not f.select:
continue continue
for s in f.drawing.strokes: for s in f.strokes:
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke: if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
continue continue
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill: elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
continue continue
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points] self.point_pair += [(Vector((*location_to_region(mat @ p.co), 0)), layer_id) for p in s.points]
else: else:
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes] # [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
for layer in gp.layers: for layer_id, l in enumerate(gp.layers):
if is_hidden(layer) or not layer.current_frame(): if l.hide or not l.active_frame:# l.lock or
continue continue
for s in layer.current_frame().drawing.strokes: for s in l.active_frame.strokes:
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke: if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
continue continue
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill: elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
continue continue
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points] self.point_pair += [(Vector((*location_to_region(mat @ p.co), 0)), layer_id) for p in s.points]
if not self.point_pair: if not self.point_pair:
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden') self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
return {'CANCELLED'} return {'CANCELLED'}
layer_target = self.filter_stroke(context) lid = self.filter_stroke(context)
if isinstance(layer_target, str): if isinstance(lid, str):
self.report({'ERROR'}, layer_target) self.report({'ERROR'}, lid)
return {'CANCELLED'} return {'CANCELLED'}
del self.point_pair # auto garbage collected ? del self.point_pair # auto garbage collected ?
self.ob.data.layers.active = layer_target self.ob.data.layers.active_index = lid
## debug show trigger time ## debug show trigger time
# print(f'Trigger time {time() - self.t0:.3f}') # print(f'Trigger time {time() - self.t0:.3f}')
# print(f'Search time {time() - t1:.3f}') # print(f'Search time {time() - t1:.3f}')
self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.name}') self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.info}')
return {'FINISHED'} return {'FINISHED'}
@ -125,7 +125,7 @@ class GP_OT_pick_closest_layer(Operator):
addon_keymaps = [] addon_keymaps = []
def register_keymaps(): def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW') km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new( kmi = km.keymap_items.new(
# name="", # name="",

View File

@ -1,183 +0,0 @@
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 layer in gpl:
if layer == target_layer:
## ! infinite loop if target layer is included
continue
for fr in layer.frames:
## skip if no stroke has active material
if not next((s for s in fr.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(fr.frame_number)):
dest_key = target_layer.frames.new(fr.frame_number)
key_dict[dest_key.frame_number] = dest_key
print(f'{ob.name} : frame {fr.frame_number}')
## Replicate strokes in dest_keys
stroke_to_delete = []
for s_idx, s in enumerate(fr.drawing.strokes):
if s.material_index == mat_index:
utils.copy_stroke_to_frame(s, dest_key)
stroke_to_delete.append(s_idx)
## Debug
# if time.time() - t > 10:
# print('TIMEOUT')
# return {'CANCELLED'}
sct += len(stroke_to_delete)
## Remove from source frame (fr)
if not self.copy:
# print('Removing frames') # Dbg
if stroke_to_delete:
fr.drawing.remove_strokes(indices=stroke_to_delete)
## ? Remove frame if layer is empty ? -> probably not, otherwise will show previous frame
fct += 1
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 == 'GPENCIL':
# 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)

View File

@ -4,12 +4,7 @@ import mathutils
from mathutils import Vector, Matrix, geometry from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from time import time from time import time
from .utils import (get_gp_draw_plane, from .utils import get_gp_draw_plane, location_to_region, region_to_location
location_to_region,
region_to_location,
is_locked,
is_hidden)
### passing by 2D projection ### passing by 2D projection
def get_3d_coord_on_drawing_plane_from_2d(context, co): def get_3d_coord_on_drawing_plane_from_2d(context, co):
@ -38,20 +33,20 @@ class GP_OT_pick_closest_material(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL' return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
def filter_stroke(self, context): def filter_stroke(self, context):
# get stroke under mouse using kdtree # get stroke under mouse using kdtree
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space point_pair = [(p.co, s) for s in self.stroke_list for p in s.points] # local space
kd = mathutils.kdtree.KDTree(len(point_pair)) kd = mathutils.kdtree.KDTree(len(point_pair))
for i, pair in enumerate(point_pair): for i, pair in enumerate(point_pair):
kd.insert(pair[0], i) kd.insert(pair[0], i)
kd.balance() kd.balance()
## Get 3D coordinate on drawing plane according to mouse 2d.position on flat 2d drawing ## Get 3D coordinate on drawing plane according to mouse 2d.co on flat 2d drawing
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse) _ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
if not hit: if not hit:
@ -67,7 +62,7 @@ class GP_OT_pick_closest_material(Operator):
## find point index in stroke ## find point index in stroke
self.idx = None self.idx = None
for i, p in enumerate(s.points): for i, p in enumerate(s.points):
if p.position == co: if p.co == co:
self.idx = i self.idx = i
break break
@ -82,22 +77,22 @@ class GP_OT_pick_closest_material(Operator):
self.stroke_list = [] self.stroke_list = []
self.inv_mat = self.ob.matrix_world.inverted() self.inv_mat = self.ob.matrix_world.inverted()
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if self.gp.use_multiedit:
for l in self.gp.layers: for l in self.gp.layers:
if is_hidden(l):# is_locked(l) or if l.hide:# l.lock or
continue continue
for f in l.frames: for f in l.frames:
if not f.select: if not f.select:
continue continue
for s in f.drawing.strokes: for s in f.strokes:
self.stroke_list.append(s) self.stroke_list.append(s)
else: else:
# [s for l in self.gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes] # [s for l in self.gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
for l in self.gp.layers: for l in self.gp.layers:
if is_hidden(l) or not l.current_frame():# is_locked(l) or if l.hide or not l.active_frame:# l.lock or
continue continue
for s in l.current_frame().drawing.strokes: for s in l.active_frame.strokes:
self.stroke_list.append(s) self.stroke_list.append(s)
if self.fill_only: if self.fill_only:
@ -121,8 +116,8 @@ class GP_OT_pick_closest_material(Operator):
self.report({'WARNING'}, 'No coord found') self.report({'WARNING'}, 'No coord found')
return {'CANCELLED'} return {'CANCELLED'}
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].position self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].co
self.init_pos = [p.position.copy() for p in self.stroke.points] # need a copy otherwise vector is updated self.init_pos = [p.co.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
## directly use world position ? ## directly use world position ?
# self.pos_world = [self.ob.matrix_world @ co for co in self.init_pos] # 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] self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
@ -149,7 +144,7 @@ class GP_OT_pick_closest_material(Operator):
# if event.type in {'RIGHTMOUSE', 'ESC'}: # if event.type in {'RIGHTMOUSE', 'ESC'}:
# # for i, p in enumerate(self.stroke.points): # reset position # # for i, p in enumerate(self.stroke.points): # reset position
# # self.stroke.points[i].position = self.init_pos[i] # # self.stroke.points[i].co = self.init_pos[i]
# context.area.tag_redraw() # context.area.tag_redraw()
# return {'CANCELLED'} # return {'CANCELLED'}
@ -164,7 +159,7 @@ class GP_OT_pick_closest_material(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL' return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
# fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) # fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
stroke_filter : bpy.props.EnumProperty(default='FILL', stroke_filter : bpy.props.EnumProperty(default='FILL',
@ -177,7 +172,7 @@ class GP_OT_pick_closest_material(Operator):
def filter_stroke(self, context): def filter_stroke(self, context):
# get stroke under mouse using kdtree # get stroke under mouse using kdtree
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space point_pair = [(p.co, s) for s in self.stroke_list for p in s.points] # local space
kd = mathutils.kdtree.KDTree(len(point_pair)) kd = mathutils.kdtree.KDTree(len(point_pair))
for i, pair in enumerate(point_pair): for i, pair in enumerate(point_pair):
@ -200,7 +195,7 @@ class GP_OT_pick_closest_material(Operator):
## find point index in stroke ## find point index in stroke
self.idx = None self.idx = None
for i, p in enumerate(s.points): for i, p in enumerate(s.points):
if p.position == co: if p.co == co:
self.idx = i self.idx = i
break break
@ -238,22 +233,22 @@ class GP_OT_pick_closest_material(Operator):
self.stroke_list = [] self.stroke_list = []
self.inv_mat = self.ob.matrix_world.inverted() self.inv_mat = self.ob.matrix_world.inverted()
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if gp.use_multiedit:
for l in gp.layers: for l in gp.layers:
if is_hidden(l):# is_locked(l) or if l.hide:# l.lock or
continue continue
for f in l.frames: for f in l.frames:
if not f.select: if not f.select:
continue continue
for s in f.drawing.strokes: for s in f.strokes:
self.stroke_list.append(s) self.stroke_list.append(s)
else: else:
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes] # [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
for l in gp.layers: for l in gp.layers:
if is_hidden(l) or not l.current_frame():# is_locked(l) or if l.hide or not l.active_frame:# l.lock or
continue continue
for s in l.current_frame().drawing.strokes: for s in l.active_frame.strokes:
self.stroke_list.append(s) self.stroke_list.append(s)
if self.stroke_filter == 'FILL': if self.stroke_filter == 'FILL':
@ -279,8 +274,8 @@ class GP_OT_pick_closest_material(Operator):
self.report({'WARNING'}, 'No coord found') self.report({'WARNING'}, 'No coord found')
return {'CANCELLED'} return {'CANCELLED'}
# self.depth = self.ob.matrix_world @ stroke.points[self.idx].position # self.depth = self.ob.matrix_world @ stroke.points[self.idx].co
# self.init_pos = [p.position.copy() for p in stroke.points] # need a copy otherwise vector is updated # self.init_pos = [p.co.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.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
# self.plen = len(stroke.points) # self.plen = len(stroke.points)
@ -295,8 +290,9 @@ class GP_OT_pick_closest_material(Operator):
addon_keymaps = [] addon_keymaps = []
def register_keymaps(): def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
# km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW') # km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Draw brush)", space_type = "EMPTY", region_type='WINDOW')
km = addon.keymaps.new(name = "Grease Pencil Fill Tool", 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 Stroke Paint (Fill)", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new( kmi = km.keymap_items.new(
# name="", # name="",
idname="gp.pick_closest_material", idname="gp.pick_closest_material",

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="") # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
# Start Clean (delete unuesed sh*t) # 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="") # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
filename_ext = '.json' 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="") # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp 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} --') print(f'-- import palette from : {filepath} --')
for ob in context.selected_objects: for ob in context.selected_objects:
if ob.type != 'GREASEPENCIL': if ob.type != 'GPENCIL':
print(f'{ob.name} not a GP object') print(f'{ob.name} not a GP object')
continue 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="") # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
filename_ext = '.blend' 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="") # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def execute(self, context): def execute(self, context):
ob = context.object 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') self.report({'ERROR'}, 'No materials to transfer')
return {"CANCELLED"} return {"CANCELLED"}
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL' and o != ob] selection = [o for o in context.selected_objects if o.type == 'GPENCIL' and o != ob]
if not selection: if not selection:
self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials') 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 @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event): def invoke(self, context, event):
self.ob = context.object self.ob = context.object
@ -354,7 +354,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
import re import re
diff_ct = 0 diff_ct = 0
todel = [] todel = []
if ob.type != 'GREASEPENCIL': if ob.type != 'GPENCIL':
return return
if not hasattr(ob, 'material_slots'): if not hasattr(ob, 'material_slots'):
return return
@ -410,7 +410,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
# iterate in all strokes and replace with new_mat_id # iterate in all strokes and replace with new_mat_id
for l in ob.data.layers: for l in ob.data.layers:
for f in l.frames: for f in l.frames:
for s in f.drawing.strokes: for s in f.strokes:
if s.material_index == i: if s.material_index == i:
s.material_index = new_mat_id 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: # if self.skip_binded_empty_slots:
# for l in ob.data.layers: # for l in ob.data.layers:
# for f in l.frames: # for f in l.frames:
# for s in f.drawing.strokes: # for s in f.strokes:
# if s.material_index == i: # if s.material_index == i:
# is_binded = True # is_binded = True
# break # break

View File

@ -45,7 +45,7 @@ class GPTB_OT_import_obj_palette(Operator):
def execute(self, context): def execute(self, context):
## get targets ## get targets
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL'] selection = [o for o in context.selected_objects if o.type == 'GPENCIL']
if not selection: if not selection:
self.report({'ERROR'}, 'Need to have at least one GP object selected in scene') self.report({'ERROR'}, 'Need to have at least one GP object selected in scene')
return {"CANCELLED"} return {"CANCELLED"}
@ -98,7 +98,7 @@ class GPTB_OT_import_obj_palette(Operator):
return {"CANCELLED"} return {"CANCELLED"}
for i in range(len(linked_objs))[::-1]: # reversed(range(len(l))) / range(len(l))[::-1] for i in range(len(linked_objs))[::-1]: # reversed(range(len(l))) / range(len(l))[::-1]
if linked_objs[i].type != 'GREASEPENCIL': if linked_objs[i].type != 'GPENCIL':
print(f'{linked_objs[i].name} type is "{linked_objs[i].type}"') print(f'{linked_objs[i].name} type is "{linked_objs[i].type}"')
bpy.data.objects.remove(linked_objs.pop(i)) bpy.data.objects.remove(linked_objs.pop(i))

View File

@ -74,7 +74,7 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
# namespace_order # namespace_order
namespaces=[] namespaces=[]
for l in gpl: for l in gpl:
ns= l.name.lower().split(separator, 1)[0] ns= l.info.lower().split(separator, 1)[0]
if ns not in namespaces: if ns not in namespaces:
namespaces.append(ns) namespaces.append(ns)
@ -88,14 +88,14 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
### step from 0.1 to 0.9 ### step from 0.1 to 0.9
for i, l in enumerate(gpl): for i, l in enumerate(gpl):
if l.name.lower() not in ('background',): if l.info.lower() not in ('background',):
print() print()
print('>', l.name) print('>', l.info)
ns= l.name.lower().split(separator, 1)[0]#get namespace from separator ns= l.info.lower().split(separator, 1)[0]#get namespace from separator
print("namespace", ns)#Dbg print("namespace", ns)#Dbg
if context.scene.gptoolprops.autotint_namespace: if context.scene.gptoolprops.autotint_namespace:
h = get_hue_by_name(ns, hue_offset)#l.name == individuels h = get_hue_by_name(ns, hue_offset)#l.info == individuels
else: else:
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9) h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)

View File

@ -1,13 +1,11 @@
import bpy import bpy
import mathutils import mathutils
import numpy as np
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
from math import pi from math import pi
import numpy as np
from time import time from time import time
from mathutils.geometry import intersect_line_plane
from . import utils from . import utils
from .utils import is_hidden, is_locked from mathutils.geometry import intersect_line_plane
def get_scale_matrix(scale): def get_scale_matrix(scale):
# recreate a neutral mat scale # recreate a neutral mat scale
@ -19,7 +17,7 @@ def get_scale_matrix(scale):
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False): def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject - ops method '''Reproject - ops method
:all_stroke: affect hidden, locked layers :all_stroke: affect hided, locked layers
''' '''
if restore_frame: if restore_frame:
@ -27,7 +25,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) 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.drawing.strokes)] frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)]
frame_list = list(set(frame_list)) frame_list = list(set(frame_list))
frame_list.sort() frame_list.sort()
@ -42,23 +40,16 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
# matrix = np.array(obj.matrix_world, dtype='float64') # matrix = np.array(obj.matrix_world, dtype='float64')
# matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64') # matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64')
#mat = src.matrix_world #mat = src.matrix_world
for layer in obj.data.layers: for l in obj.data.layers:
if not all_strokes: if not all_strokes:
if not layer.select: if not l.select:
continue continue
if is_hidden(layer) or is_locked(layer): if l.hide or l.lock:
continue continue
f = next((f for f in l.frames if f.frame_number == i), None)
frame = next((f for f in layer.frames if f.frame_number == i), None) if f is None:
if frame is None:
print(layer.name, 'Not found')
# FIXME: some strokes are ignored
# print(frame'skip {layer.name}, no frame at {i}')
continue continue
for s in f.strokes:
for s in frame.drawing.strokes:
# print(layer.name, s.material_index)
## Batch matrix apply (Here is slower than list comprehension). ## Batch matrix apply (Here is slower than list comprehension).
# nb_points = len(s.points) # nb_points = len(s.points)
# coords = np.empty(nb_points * 3, dtype='float64') # coords = np.empty(nb_points * 3, dtype='float64')
@ -66,27 +57,55 @@ 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) # world_co_3d = utils.matrix_transform(coords.reshape((nb_points, 3)), matrix)
## list comprehension method ## list comprehension method
world_co_3d = [obj.matrix_world @ p.position for p in s.points] world_co_3d = [obj.matrix_world @ p.co for p in s.points]
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] 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 and compatible with GPv3) ## Basic method (Slower than foreach_set)
## TODO: use low level api with curve offsets... # for i, p in enumerate(s.points):
for pt_index, point in enumerate(s.points): # p.co = obj.matrix_world.inverted() @ new_world_co_3d[i]
point.position = matrix_inv @ new_world_co_3d[pt_index]
## GPv2: ravel and use foreach_set
## Ravel new coordinate on the fly ## Ravel new coordinate on the fly
## NOTE: Set points in obj local space (apply matrix is slower): new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel() new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
# new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
# s.points.foreach_set('co', new_local_coords) ## Set points in obj local space (apply matrix slower)
# 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: if restore_frame:
bpy.context.scene.frame_current = oframe 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): def align_global(reproject=True, ref=None, all_strokes=True):
@ -118,24 +137,24 @@ def align_global(reproject=True, ref=None, all_strokes=True):
# world_coords = [] # world_coords = []
for l in o.data.layers: for l in o.data.layers:
for f in l.frames: for f in l.frames:
for s in f.drawing.strokes: for s in f.strokes:
## foreach ## foreach
coords = [p.position @ mat.inverted() @ new_mat for p in s.points] coords = [p.co @ 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])
# s.points.update() # seem to works # but adding/deleting a point is "safer"
## force update
s.points.add(1)
s.points.pop()
## GPv2 # for p in s.points:
# s.points.foreach_set('co', [co for v in coords for co in v])
# # s.points.update() # seem to works # but adding/deleting a point is "safer"
# ## force update
# s.points.add(1)
# s.points.pop()
for p in s.points:
## GOOD : ## GOOD :
# world_co = mat @ p.position # world_co = mat @ p.co
# p.position = new_mat.inverted() @ world_co # p.co = new_mat.inverted() @ world_co
## GOOD : ## GOOD :
p.position = p.position @ mat.inverted() @ new_mat # p.co = p.co @ mat.inverted() @ new_mat
if o.parent: if o.parent:
o.matrix_world = new_mat o.matrix_world = new_mat
@ -195,9 +214,9 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
scale_mat = get_scale_matrix(o_scale) scale_mat = get_scale_matrix(o_scale)
new_mat = loc_mat @ rot_mat @ scale_mat new_mat = loc_mat @ rot_mat @ scale_mat
for s in f.drawing.strokes: for s in f.strokes:
## foreach ## foreach
coords = [p.position @ mat.inverted() @ new_mat for p in s.points] coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
# print('coords: ', coords) # print('coords: ', coords)
# print([co for v in coords for co in v]) # print([co for v in coords for co in v])
s.points.foreach_set('co', [co for v in coords for co in v]) s.points.foreach_set('co', [co for v in coords for co in v])
@ -246,7 +265,7 @@ class GPTB_OT_realign(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
reproject : bpy.props.BoolProperty( reproject : bpy.props.BoolProperty(
name='Reproject', default=True, name='Reproject', default=True,
@ -262,7 +281,7 @@ class GPTB_OT_realign(bpy.types.Operator):
## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes ## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes
def invoke(self, context, event): def invoke(self, context, event):
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if context.object.data.use_multiedit:
self.report({'ERROR'}, 'Does not work in Multiframe mode') self.report({'ERROR'}, 'Does not work in Multiframe mode')
return {"CANCELLED"} return {"CANCELLED"}
@ -344,25 +363,25 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GPENCIL'
all_strokes : bpy.props.BoolProperty( all_strokes : bpy.props.BoolProperty(
name='All Strokes', default=True, name='All Strokes', default=True,
description='Hided and locked layer will also be reprojected') description='Hided and locked layer will also be reprojected')
type : bpy.props.EnumProperty(name='Type', type: bpy.props.EnumProperty(name='Type',
items=(('CURRENT', "Current", ""), items=(('CURRENT', "Current", ""),
('FRONT', "Front", ""), ('FRONT', "Front", ""),
('SIDE', "Side", ""), ('SIDE', "Side", ""),
('TOP', "Top", ""), ('TOP', "Top", ""),
('VIEW', "View", ""), ('VIEW', "View", ""),
('SURFACE', "Surface", ""),
('CURSOR', "Cursor", ""), ('CURSOR', "Cursor", ""),
# ('SURFACE', "Surface", ""),
), ),
default='CURRENT') default='CURRENT')
def invoke(self, context, event): def invoke(self, context, event):
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if context.object.data.use_multiedit:
self.report({'ERROR'}, 'Does not work in Multi-edit') self.report({'ERROR'}, 'Does not work in Multi-edit')
return {"CANCELLED"} return {"CANCELLED"}
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
@ -371,27 +390,10 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
layout = self.layout layout = self.layout
if not context.region_data.view_perspective == 'CAMERA': if not context.region_data.view_perspective == 'CAMERA':
# layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR') # layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
layout.label(text='Reprojection is made from camera', icon='ERROR') layout.label(text='Reprojection is made from camera, not current view', icon='ERROR')
layout.prop(self, "all_strokes") layout.prop(self, "all_strokes")
layout.prop(self, "type", text='Project Axis') layout.prop(self, "type")
## 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): def execute(self, context):
t0 = time() t0 = time()
@ -408,12 +410,12 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
### -- MENU ENTRY -- ### -- MENU ENTRY --
def reproject_clean_menu(self, context): def reproject_clean_menu(self, context):
if context.mode == 'EDIT_GREASE_PENCIL': if context.mode == 'EDIT_GPENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT') 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') self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
def reproject_context_menu(self, context): def reproject_context_menu(self, context):
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE': if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC') self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
@ -426,12 +428,12 @@ def register():
for cl in classes: for cl in classes:
bpy.utils.register_class(cl) bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.append(reproject_context_menu) bpy.types.VIEW3D_MT_gpencil_edit_context_menu.append(reproject_context_menu)
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.append(reproject_clean_menu) bpy.types.GPENCIL_MT_cleanup.append(reproject_clean_menu)
def unregister(): def unregister():
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.remove(reproject_clean_menu) bpy.types.GPENCIL_MT_cleanup.remove(reproject_clean_menu)
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.remove(reproject_context_menu) bpy.types.VIEW3D_MT_gpencil_edit_context_menu.remove(reproject_context_menu)
for cl in reversed(classes): for cl in reversed(classes):
bpy.utils.unregister_class(cl) bpy.utils.unregister_class(cl)

View File

@ -2,11 +2,7 @@
Blender addon - Various tool to help with grease pencil in animation productions. Blender addon - Various tool to help with grease pencil in animation productions.
### /!\ Main branch is currently broken, in migration to gpv3 **[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 from release page](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**

View File

@ -2,9 +2,7 @@
Blender addon - Boîte à outils de grease pencil pour la production d'animation. Blender addon - Boîte à outils de grease pencil pour la production d'animation.
**[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)** **[Télécharger la dernière version](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)**
**[Téléchargement pour Blender 4.2 ou inférieur depuis la page des releases](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**

View File

@ -7,7 +7,7 @@ from .utils import get_addon_prefs
class GPTB_WT_eraser(WorkSpaceTool): class GPTB_WT_eraser(WorkSpaceTool):
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_context_mode = 'PAINT_GREASE_PENCIL' bl_context_mode = 'PAINT_GPENCIL'
# The prefix of the idname should be your add-on name. # The prefix of the idname should be your add-on name.
bl_idname = "gp.eraser_tool" bl_idname = "gp.eraser_tool"

View File

@ -20,7 +20,7 @@ class GPTB_PT_dataprop_panel(Panel):
# bl_category = "Tool" # bl_category = "Tool"
# bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class. # bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class.
bl_label = "Pseudo color"# title bl_label = "Pseudo color"# title
bl_parent_id = "DATA_PT_grease_pencil_layers"#subpanel of this ID bl_parent_id = "DATA_PT_gpencil_layers"#subpanel of this ID
bl_options = {'DEFAULT_CLOSED'} bl_options = {'DEFAULT_CLOSED'}
def draw(self, context): def draw(self, context):
@ -58,11 +58,6 @@ class GPTB_PT_sidebar_panel(Panel):
## flip X cam ## flip X cam
# layout.label(text='! Flipped !') # 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 = col.row(align=True)
row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
@ -144,7 +139,7 @@ class GPTB_PT_sidebar_panel(Panel):
col.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump 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 # col.prop(context.space_data.overlay, 'use_gpencil_onion_skin') # not often used
if context.object and context.object.type == 'GREASEPENCIL': if context.object and context.object.type == 'GPENCIL':
# col.prop(context.object.data, 'use_autolock_layers') # not often used # col.prop(context.object.data, 'use_autolock_layers') # not often used
col.prop(context.object, 'show_in_front') # text='In Front' col.prop(context.object, 'show_in_front') # text='In Front'
@ -166,9 +161,7 @@ class GPTB_PT_sidebar_panel(Panel):
else: else:
col.label(text='No GP object selected') col.label(text='No GP object selected')
col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
## Gpv3: not more edit line (use Curve lines)
# col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
# Mention update as notice # Mention update as notice
# addon_updater_ops.update_notice_box_ui(self, context) # addon_updater_ops.update_notice_box_ui(self, context)
@ -193,23 +186,23 @@ class GPTB_PT_anim_manager(Panel):
# import time # import time
# t0 = time.perf_counter() # t0 = time.perf_counter()
# objs = [o for o in context.scene.objects if o.type not in ('GREASEPENCIL', 'CAMERA')] # 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 == 'GREASEPENCIL'] # gps = [o for o in context.scene.objects if o.type == 'GPENCIL']
# cams = [o for o in context.scene.objects if o.type == 'CAMERA'] # cams = [o for o in context.scene.objects if o.type == 'CAMERA']
objs = [] objs = []
gps = [] gps = []
cams = [] cams = []
for o in context.scene.objects: for o in context.scene.objects:
if o.type not in ('GREASEPENCIL', 'CAMERA'): if o.type not in ('GPENCIL', 'CAMERA'):
objs.append(o) objs.append(o)
elif o.type == 'GREASEPENCIL': elif o.type == 'GPENCIL':
gps.append(o) gps.append(o)
elif o.type == 'CAMERA': elif o.type == 'CAMERA':
cams.append(o) cams.append(o)
# print(f'{time.perf_counter() - t0:.8f}s') # print(f'{time.perf_counter() - t0:.8f}s')
return {'OBJECT': objs, 'GREASEPENCIL': gps, 'CAMERA': cams} return {'OBJECT': objs, 'GPENCIL': gps, 'CAMERA': cams}
def draw(self, context): def draw(self, context):
@ -223,7 +216,7 @@ class GPTB_PT_anim_manager(Panel):
col.operator('gp.list_disabled_anims') col.operator('gp.list_disabled_anims')
## Show Enable / Disable anims ## Show Enable / Disable anims
for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GREASEPENCIL')]: for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GPENCIL')]:
on_icon, off_icon = anim_status(obj_types[cat_type]) on_icon, off_icon = anim_status(obj_types[cat_type])
subcol = col.column() subcol = col.column()
@ -244,7 +237,7 @@ class GPTB_PT_anim_manager(Panel):
row = subcol.row(align=True) row = subcol.row(align=True)
row.label(text='Gp modifiers:') row.label(text='Gp modifiers:')
on_icon, off_icon = gp_modifier_status(obj_types['GREASEPENCIL']) on_icon, off_icon = gp_modifier_status(obj_types['GPENCIL'])
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red # 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='ON', icon=on_icon).show = True
row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False
@ -281,8 +274,6 @@ class GPTB_PT_anim_manager(Panel):
col.use_property_split = False col.use_property_split = False
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR') text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon) col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
if context.scene.gptoolprops.cursor_follow:
col.prop(context.scene.gptoolprops, 'cursor_follow_target', text='Target', icon='OBJECT_DATA')
class GPTB_PT_toolbox_playblast(Panel): class GPTB_PT_toolbox_playblast(Panel):
@ -429,7 +420,7 @@ def palette_manager_menu(self, context):
"""Palette menu to append in existing menu""" """Palette menu to append in existing menu"""
# GPENCIL_MT_material_context_menu # GPENCIL_MT_material_context_menu
layout = self.layout layout = self.layout
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'} # {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
layout.separator() layout.separator()
prefs = get_addon_prefs() prefs = get_addon_prefs()
@ -441,8 +432,6 @@ def palette_manager_menu(self, context):
layout.separator() layout.separator()
layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path 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.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): def expose_use_channel_color_pref(self, context):
@ -662,7 +651,7 @@ class GPTB_PT_tools_grease_pencil_interpolate(Panel):
# settings = context.tool_settings.gpencil_interpolate # old 2.92 global settings # settings = context.tool_settings.gpencil_interpolate # old 2.92 global settings
## access active tool settings ## access active tool settings
# settings = context.workspace.tools[0].operator_properties('gpencil.interpolate') # settings = context.workspace.tools[0].operator_properties('gpencil.interpolate')
settings = context.workspace.tools.from_space_view3d_mode('PAINT_GREASE_PENCIL').operator_properties('gpencil.interpolate') settings = context.workspace.tools.from_space_view3d_mode('PAINT_GPENCIL').operator_properties('gpencil.interpolate')
## custom curve access (still in gp interpolate tools) ## custom curve access (still in gp interpolate tools)
interpolate_settings = context.tool_settings.gpencil_interpolate interpolate_settings = context.tool_settings.gpencil_interpolate
@ -738,7 +727,7 @@ def interpolate_header_ui(self, context):
layout = self.layout layout = self.layout
obj = context.active_object obj = context.active_object
if obj and obj.type == 'GREASEPENCIL' and context.gpencil_data: if obj and obj.type == 'GPENCIL' and context.gpencil_data:
gpd = context.gpencil_data gpd = context.gpencil_data
else: else:
return return
@ -772,10 +761,7 @@ def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu) bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref) bpy.types.DOPESHEET_PT_gpencil_layer_display.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 # bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
# if bpy.app.version >= (3,0,0): # if bpy.app.version >= (3,0,0):
@ -784,13 +770,8 @@ def register():
def unregister(): def unregister():
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP # 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.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): # if bpy.app.version >= (3,0,0):
# bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui) # bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui)

View File

@ -4,12 +4,12 @@ bl_info = {
"name": "GP toolbox", "name": "GP toolbox",
"description": "Tool set for Grease Pencil in animation production", "description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux", "author": "Samuel Bernou, Christophe Seux",
"version": (4, 0, 4), "version": (2, 5, 0),
"blender": (4, 3, 0), "blender": (3, 0, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "", "warning": "",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox", "doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues", "tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues",
"category": "3D View", "category": "3D View",
} }
@ -35,7 +35,7 @@ from . import OP_brushes
from . import OP_file_checker from . import OP_file_checker
from . import OP_copy_paste from . import OP_copy_paste
from . import OP_realign from . import OP_realign
# from . import OP_flat_reproject # Disabled from . import OP_flat_reproject
from . import OP_depth_move from . import OP_depth_move
from . import OP_key_duplicate_send from . import OP_key_duplicate_send
from . import OP_layer_manager from . import OP_layer_manager
@ -46,7 +46,6 @@ from . import OP_git_update
from . import OP_layer_namespace from . import OP_layer_namespace
from . import OP_pseudo_tint from . import OP_pseudo_tint
from . import OP_follow_curve from . import OP_follow_curve
from . import OP_material_move_to_layer
# from . import OP_eraser_brush # from . import OP_eraser_brush
# from . import TOOL_eraser_brush # from . import TOOL_eraser_brush
from . import handler_draw_cam from . import handler_draw_cam
@ -336,7 +335,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
nav_fade_val : FloatProperty( nav_fade_val : FloatProperty(
name='Fade Value', name='Fade Value',
description='Fade value for other layers when navigating (0=invisible)', description='Fade value for other layers when navigating (0=invisible)',
default=0.1, min=0.0, max=0.95, step=1, precision=2) default=0.35, min=0.0, max=0.95, step=1, precision=2)
nav_limit : FloatProperty( nav_limit : FloatProperty(
name='Fade Duration', name='Fade Duration',
@ -625,9 +624,9 @@ class GPTB_prefs(bpy.types.AddonPreferences):
layout.label(text='Following checks will be made when clicking "Check File" button:') layout.label(text='Following checks will be made when clicking "Check File" button:')
col = layout.column() col = layout.column()
col.use_property_split = True col.use_property_split = True
# col.prop(self.fixprops, 'check_only') col.prop(self.fixprops, 'check_only')
col.label(text='The Popup list possible fixes, you can then use the "Apply Fixes"', icon='INFO') col.label(text='If dry run is checked, no modification is done', icon='INFO')
# col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1') col.label(text='Use Ctrl + Click on "Check File" button to invert the behavior', icon='BLANK1')
col.separator() col.separator()
col.prop(self.fixprops, 'lock_main_cam') col.prop(self.fixprops, 'lock_main_cam')
col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})') col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})')
@ -793,7 +792,7 @@ addon_modules = (
OP_brushes, OP_brushes,
OP_cursor_snap_canvas, OP_cursor_snap_canvas,
OP_copy_paste, OP_copy_paste,
# OP_flat_reproject # Disabled, OP_flat_reproject,
OP_realign, OP_realign,
OP_depth_move, OP_depth_move,
OP_key_duplicate_send, OP_key_duplicate_send,
@ -804,7 +803,6 @@ addon_modules = (
OP_layer_picker, OP_layer_picker,
OP_layer_nav, OP_layer_nav,
OP_follow_curve, OP_follow_curve,
OP_material_move_to_layer,
# OP_eraser_brush, # OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush # TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam, handler_draw_cam,

View File

@ -99,8 +99,7 @@ def gp_stroke_angle_split (frame, strokes, angle):
splitted_loops = bm_angle_split(bm,angle) splitted_loops = bm_angle_split(bm,angle)
## FIXME: Should use -> drawing.remove_strokes(indices=(0,)) frame.strokes.remove(stroke_info['stroke'])
frame.drawing.strokes.remove(stroke_info['stroke'])
for loop in splitted_loops : for loop in splitted_loops :
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop] 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) new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
@ -124,7 +123,6 @@ def gp_stroke_uniform_density(cam, frame, strokes, max_spacing):
bm_uniform_density(bm,cam,max_spacing) bm_uniform_density(bm,cam,max_spacing)
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
frame.strokes.remove(stroke_info['stroke']) frame.strokes.remove(stroke_info['stroke'])
bm.verts.ensure_lookup_table() bm.verts.ensure_lookup_table()
@ -167,6 +165,7 @@ def randomise_points(mat, points, attr, strength) :
setattr(point,attr,value+random*strength) setattr(point,attr,value+random*strength)
def zoom_to_object(cam, resolution, box, margin=0.01) : def zoom_to_object(cam, resolution, box, margin=0.01) :
min_x= box[0] min_x= box[0]
max_x= box[1] max_x= box[1]
@ -217,6 +216,25 @@ def zoom_to_object(cam, resolution, box, margin=0.01) :
#print(matrix,resolution) #print(matrix,resolution)
return modelview_matrix,projection_matrix,frame,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 # get object info
def get_object_info(mesh_groups, order_list = []) : def get_object_info(mesh_groups, order_list = []) :
scene = bpy.context.scene scene = bpy.context.scene

View File

@ -1,5 +1,6 @@
import bpy import bpy
import gpu import gpu
import bgl
# import blf # import blf
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d from bpy_extras.view3d_utils import location_3d_to_region_2d
@ -29,20 +30,6 @@ def view3d_camera_border_2d(context, cam):
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame] frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
return frame_px 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(): def draw_cam_frame_callback_2d():
context = bpy.context context = bpy.context
if context.region_data.view_perspective != 'CAMERA': if context.region_data.view_perspective != 'CAMERA':
@ -54,12 +41,11 @@ def draw_cam_frame_callback_2d():
if not main_cam: if not main_cam:
return return
gpu.state.blend_set('ALPHA') bgl.glEnable(bgl.GL_BLEND)
frame_point = view3d_camera_border_2d( frame_point = view3d_camera_border_2d(
context, context.scene.camera.parent) context, context.scene.camera.parent)
shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') # POLYLINE_FLAT_COLOR shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
# gpu.shader.from_builtin('2D_UNIFORM_COLOR')
if context.scene.gptoolprops.drawcam_passepartout: if context.scene.gptoolprops.drawcam_passepartout:
### PASSEPARTOUT ### PASSEPARTOUT
@ -123,8 +109,8 @@ def draw_cam_frame_callback_2d():
### Camera framing trace over ### Camera framing trace over
gpu.state.line_width_set(1.0) bgl.glLineWidth(1)
# bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth bgl.glEnable(bgl.GL_LINE_SMOOTH)
""" """
## need to accurately detect viewport background color (difficult) ## need to accurately detect viewport background color (difficult)
@ -149,14 +135,15 @@ def draw_cam_frame_callback_2d():
frame_color = (0.0, 0.0, 0.25, 1.0) frame_color = (0.0, 0.0, 0.25, 1.0)
screen_framing = batch_for_shader( screen_framing = batch_for_shader(
shader_2d, 'LINES', {"pos": vertices_to_line_loop(frame_point)}) shader_2d, 'LINE_LOOP', {"pos": frame_point})
shader_2d.bind() shader_2d.bind()
shader_2d.uniform_float("color", frame_color) shader_2d.uniform_float("color", frame_color)
screen_framing.draw(shader_2d) screen_framing.draw(shader_2d)
# bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth # bgl.glLineWidth(1)
gpu.state.blend_set('NONE') bgl.glDisable(bgl.GL_LINE_SMOOTH)
bgl.glDisable(bgl.GL_BLEND)
draw_handle = None draw_handle = None

View File

@ -21,16 +21,20 @@ def update_layer_name(self, context):
if not self.layer_name: if not self.layer_name:
# never replace by nothing (since there should be prefix/suffix) # never replace by nothing (since there should be prefix/suffix)
return return
if not context.object or context.object.type != 'GREASEPENCIL': if not context.object or context.object.type != 'GPENCIL':
return return
if not context.object.data.layers.active: if not context.object.data.layers.active:
return return
layer_name_build(context.object.data.layers.active, desc=self.layer_name) layer_name_build(context.object.data.layers.active, desc=self.layer_name)
# context.object.data.layers.active.name = self.layer_name # context.object.data.layers.active.info = self.layer_name
class GP_PG_FixSettings(PropertyGroup): class GP_PG_FixSettings(PropertyGroup):
check_only : BoolProperty(
name="Dry run mode (Check only)",
description="Do not change anything, just print the messages",
default=False, options={'HIDDEN'})
lock_main_cam : BoolProperty( lock_main_cam : BoolProperty(
name="Lock Main Cam", name="Lock Main Cam",
@ -178,15 +182,9 @@ class GP_PG_ToolsSettings(PropertyGroup):
name='Cursor Follow', description="3D cursor follow active object animation when activated", name='Cursor Follow', description="3D cursor follow active object animation when activated",
default=False, update=cursor_follow_update) default=False, update=cursor_follow_update)
cursor_follow_target : bpy.props.PointerProperty( edit_lines_opacity : FloatProperty(
name='Cursor Follow Target', name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils",
description="Optional target object to follow for cursor instead of active object", default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
type=bpy.types.Object, update=cursor_follow_update)
## gpv3 : no edit line color anymore
# edit_lines_opacity : FloatProperty(
# name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils",
# default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
## render ## render
name_for_current_render : StringProperty( name_for_current_render : StringProperty(

486
utils.py
View File

@ -2,88 +2,20 @@ import bpy, os
import numpy as np import numpy as np
import bmesh import bmesh
import mathutils import mathutils
from mathutils import Vector
import math import math
from math import sqrt
from sys import platform
import subprocess import subprocess
from time import time
from math import sqrt
from mathutils import Vector
from sys import platform
## constants values
## Default stroke and points attributes """ def get_gp_parent(layer) :
stroke_attr = [ if layer.parent_type == "BONE" and layer.parent_bone :
'start_cap', return layer.parent.pose.bones.get(layer.parent_bone)
'end_cap', else :
'softness', return layer.parent
'material_index', """
'fill_opacity',
'fill_color',
'cyclic',
'aspect_ratio',
'time_start',
# 'curve_type', # read-only
]
point_attr = [
'position',
'radius',
'rotation',
'opacity',
'vertex_color',
'delta_time',
# 'select',
]
### Attribute value, types and shape
attribute_value_string = {
'FLOAT': "value",
'INT': "value",
'FLOAT_VECTOR': "vector",
'FLOAT_COLOR': "color",
'BYTE_COLOR': "color",
'STRING': "value",
'BOOLEAN': "value",
'FLOAT2': "value",
'INT8': "value",
'INT32_2D': "value",
'QUATERNION': "value",
'FLOAT4X4': "value",
}
attribute_value_dtype = {
'FLOAT': np.float32,
'INT': np.dtype('int'),
'FLOAT_VECTOR': np.float32,
'FLOAT_COLOR': np.float32,
'BYTE_COLOR': np.int8,
'STRING': np.dtype('str'),
'BOOLEAN': np.dtype('bool'),
'FLOAT2': np.float32,
'INT8': np.int8,
'INT32_2D': np.dtype('int'),
'QUATERNION': np.float32,
'FLOAT4X4': np.float32,
}
attribute_value_shape = {
'FLOAT': (),
'INT': (),
'FLOAT_VECTOR': (3,),
'FLOAT_COLOR': (4,),
'BYTE_COLOR': (4,),
'STRING': (),
'BOOLEAN': (),
'FLOAT2':(2,),
'INT8': (),
'INT32_2D': (2,),
'QUATERNION': (4,),
'FLOAT4X4': (4,4),
}
def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin): def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
@ -96,9 +28,9 @@ def get_matrix(ob) :
return ob.matrix_world.copy() return ob.matrix_world.copy()
def set_matrix(gp_frame,mat): def set_matrix(gp_frame,mat):
for stroke in gp_frame.drawing.strokes : for stroke in gp_frame.strokes :
for point in stroke.points : for point in stroke.points :
point.position = mat @ point.position point.co = mat @ point.co
# get view vector location (the 2 methods work fine) # get view vector location (the 2 methods work fine)
def get_view_origin_position(): def get_view_origin_position():
@ -222,77 +154,12 @@ def gp_stroke_to_bmesh(strokes):
### GP Drawing ### GP Drawing
# ----------------- # -----------------
def layer_active_index(gpl):
'''Get layer list and return index of active layer
Can return None if no active layer found (active item can be a group)
'''
return next((i for i, l in enumerate(gpl) if l == gpl.active), None)
def get_top_layer_from_group(gp, group):
upper_layer = None
for layer in gp.layers:
if layer.parent_group == group:
upper_layer = layer
return upper_layer
def get_closest_active_layer(gp):
'''Get active layer from GP object, getting upper layer if in group
if a group is active, return the top layer of this group
if group is active but no layer in it, return None
'''
if gp.layers.active:
return gp.layers.active
## No active layer, return active from group (can be None !)
return get_top_layer_from_group(gp, gp.layer_groups.active)
def closest_layer_active_index(gp, fallback_index=0):
'''Get active layer index from GP object, getting upper layer if in group
if a group is active, return index at the top layer of this group
if group is active but no layer in it, return fallback_index (0 by default, stack bottom)'''
closest_active_layer = get_closest_active_layer(gp)
if closest_active_layer:
return next((i for i, l in enumerate(gp.layers) if l == closest_active_layer), fallback_index)
return fallback_index
## Check for nested lock
def is_locked(stack_item):
'''Check if passed stack item (layer or group) is locked
either itself or by parent groups'''
if stack_item.lock:
return True
if stack_item.parent_group:
return is_locked(stack_item.parent_group)
return False
def is_parent_locked(stack_item):
'''Check if passed stack item (layer or group) is locked by parent groups'''
if stack_item.parent_group:
return is_locked(stack_item.parent_group)
return False
## Check for nested hide
def is_hidden(stack_item):
'''Check if passed stack item (layer or group) is hidden
either itself or by parent groups'''
if stack_item.hide:
return True
if stack_item.parent_group:
return is_hidden(stack_item.parent_group)
return False
def is_parent_hidden(stack_item):
'''Check if passed stack item (layer or group) is hidden by parent groups'''
if stack_item.parent_group:
return is_hidden(stack_item.parent_group)
return False
def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0): def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0):
''' '''
draw basic stroke by passing list of point 3D coordinate draw basic stroke by passing list of point 3D coordinate
the frame to draw on and optional width parameter (default = 2) the frame to draw on and optional width parameter (default = 2)
''' '''
stroke = frame.drawing.strokes.new() stroke = frame.strokes.new()
stroke.line_width = width stroke.line_width = width
stroke.display_mode = '3DSPACE' stroke.display_mode = '3DSPACE'
stroke.material_index = mat_id stroke.material_index = mat_id
@ -305,12 +172,12 @@ def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0):
# for i, pt in enumerate(pts): # for i, pt in enumerate(pts):
# stroke.points.add() # stroke.points.add()
# dest_point = stroke.points[i] # dest_point = stroke.points[i]
# dest_point.position = pt # dest_point.co = pt
return stroke return stroke
## OLD - need update ## OLD - need update
def draw_gp_stroke(loop_info, frame, palette, width = 2) : def draw_gp_stroke(loop_info, frame, palette, width = 2) :
stroke = frame.drawing.strokes.new(palette) stroke = frame.strokes.new(palette)
stroke.line_width = width stroke.line_width = width
stroke.display_mode = '3DSPACE'# old -> draw_mode stroke.display_mode = '3DSPACE'# old -> draw_mode
@ -396,6 +263,55 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
### GP funcs ### 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): def get_gp_draw_plane(obj=None, orient=None):
''' return tuple with plane coordinate and normal ''' return tuple with plane coordinate and normal
of the curent drawing according to geometry''' of the curent drawing according to geometry'''
@ -420,13 +336,13 @@ def get_gp_draw_plane(obj=None, orient=None):
plane_co = bpy.context.scene.cursor.location plane_co = bpy.context.scene.cursor.location
mat = bpy.context.scene.cursor.matrix mat = bpy.context.scene.cursor.matrix
elif orient in ('AXIS_Y', 'FRONT'): # front (X-Z) elif orient == 'AXIS_Y':#front (X-Z)
plane_no = Vector((0,1,0)) plane_no = Vector((0,1,0))
elif orient in ('AXIS_X', 'SIDE'): # side (Y-Z) elif orient == 'AXIS_X':#side (Y-Z)
plane_no = Vector((1,0,0)) plane_no = Vector((1,0,0))
elif orient in ('AXIS_Z', 'TOP'): # top (X-Y) elif orient == 'AXIS_Z':#top (X-Y)
plane_no = Vector((0,0,1)) plane_no = Vector((0,0,1))
plane_no.rotate(mat) plane_no.rotate(mat)
@ -504,24 +420,24 @@ def create_gp_palette(gp_data_block,info) :
def get_gp_objects(selection=True): def get_gp_objects(selection=True):
'''return selected objects or only the active one''' '''return selected objects or only the active one'''
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL': if not bpy.context.active_object or bpy.context.active_object.type != 'GPENCIL':
print('No active GP object') print('No active GP object')
return [] return []
active = bpy.context.active_object active = bpy.context.active_object
if selection: if selection:
selection = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL'] selection = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
if not active in selection: if not active in selection:
selection += [active] selection += [active]
return selection return selection
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL': if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL':
return [active] return [active]
return [] return []
def get_gp_datas(selection=True): def get_gp_datas(selection=True):
'''return selected objects or only the active one''' '''return selected objects or only the active one'''
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL': if not bpy.context.active_object or bpy.context.active_object.type != 'GPENCIL':
print('No active GP object') print('No active GP object')
return [] return []
@ -529,28 +445,28 @@ def get_gp_datas(selection=True):
if selection: if selection:
selected = [] selected = []
for o in bpy.context.selected_objects: for o in bpy.context.selected_objects:
if o.type == 'GREASEPENCIL': if o.type == 'GPENCIL':
if o.data not in selected: if o.data not in selected:
selected.append(o.data) selected.append(o.data)
# selected = [o.data for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL'] # selected = [o.data for o in bpy.context.selected_objects if o.type == 'GPENCIL']
if not active_data in selected: if not active_data in selected:
selected += [active_data] selected += [active_data]
return selected return selected
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL': if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL':
return [active_data] return [active_data]
print('EOL. No active GP object') print('EOL. No active GP object')
return [] 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) gp_layer = gp_data_block.layers.get(name)
if not gp_layer : if not gp_layer :
gp_layer = gp_data_block.layers.new(name) gp_layer = gp_data_block.layers.new(name)
return gp_layer return gp_layer
def get_gp_frame(layer, frame_nb=None) : def get_gp_frame(layer,frame_nb = None) :
scene = bpy.context.scene scene = bpy.context.scene
if not frame_nb : if not frame_nb :
frame_nb = scene.frame_current frame_nb = scene.frame_current
@ -572,7 +488,7 @@ def get_active_frame(layer_name=None):
if layer_name: if layer_name:
lay = bpy.context.scene.grease_pencil.layers.get(layer_name) lay = bpy.context.scene.grease_pencil.layers.get(layer_name)
if lay: if lay:
frame = lay.current_frame() frame = lay.active_frame
if frame: if frame:
return frame return frame
else: else:
@ -581,7 +497,7 @@ def get_active_frame(layer_name=None):
print('no layers named', layer_name, 'in scene layers') print('no layers named', layer_name, 'in scene layers')
else:#active layer else:#active layer
frame = bpy.context.scene.grease_pencil.layers.active.current_frame() frame = bpy.context.scene.grease_pencil.layers.active.active_frame
if frame: if frame:
return frame return frame
else: else:
@ -589,7 +505,7 @@ def get_active_frame(layer_name=None):
def get_stroke_2D_coords(stroke): def get_stroke_2D_coords(stroke):
'''return a list containing points 2D coordinates of passed gp stroke object''' '''return a list containing points 2D coordinates of passed gp stroke object'''
return [location_to_region(p.position) for p in stroke.points] return [location_to_region(p.co) for p in stroke.points]
'''#foreach method for retreiving multiple other attribute quickly and stack them '''#foreach method for retreiving multiple other attribute quickly and stack them
point_nb = len(stroke.points) point_nb = len(stroke.points)
@ -604,153 +520,21 @@ def get_stroke_2D_coords(stroke):
def get_all_stroke_2D_coords(frame): def get_all_stroke_2D_coords(frame):
'''return a list of lists with all strokes's points 2D location''' '''return a list of lists with all strokes's points 2D location'''
## using modification from get_stroke_2D_coords func' ## using modification from get_stroke_2D_coords func'
return [get_stroke_2D_coords(s) for s in frame.drawing.strokes] return [get_stroke_2D_coords(s) for s in frame.strokes]
## direct ## direct
#return[[location_to_region(p.position) for p in s.points] for s in frame.drawing.strokes] #return[[location_to_region(p.co) for p in s.points] for s in frame.strokes]
def selected_strokes(frame): def selected_strokes(frame):
'''return all stroke having a point selected as a list of strokes objects''' '''return all stroke having a point selected as a list of strokes objects'''
stlist = [] stlist = []
for i, s in enumerate(frame.drawing.strokes): for i, s in enumerate(frame.strokes):
if any(pt.select for pt in s.points): if any(pt.select for pt in s.points):
stlist.append(s) stlist.append(s)
return stlist return stlist
## Copy stroke to a frame from math import sqrt
from mathutils import Vector
def copy_stroke_to_frame(s, frame, select=True):
'''Copy stroke to given frame
return created stroke
'''
frame.drawing.add_strokes([len(s.points)])
ns = frame.drawing.strokes[-1]
# print(len(s.points), 'new:', len(ns.points))
#ns.material_index
## replicate attributes (simple loop)
## TODO : might need to create atribute domain if does not exists in destination
for attr in stroke_attr:
setattr(ns, attr, getattr(s, attr))
for src_p, dest_p in zip(s.points, ns.points):
for attr in point_attr:
setattr(dest_p, attr, getattr(src_p, attr))
## Define selection
dest_p.select=select
## Direcly iterate over attribute ?
# src_start = src_dr.curve_offsets[0].value
# src_end = src_start + data_size
# dst_start = dst_dr.curve_offsets[0].value
# dst_end = dst_start + data_size
# for src_idx, dest_idx in zip(range(src_start, src_end),range(dst_start, dst_end)):
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
return ns
"""## Works, but do not copy all attributes type (probably ok for GP though)
def bulk_frame_copy_attributes(source_attr, target_attr):
'''Get and apply data as flat numpy array based on attribute type'''
if source_attr.data_type == 'INT':
data = np.empty(len(source_attr.data), dtype=np.int32)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'INT8':
data = np.empty(len(source_attr.data), dtype=np.int8)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'FLOAT':
data = np.empty(len(source_attr.data), dtype=np.float32)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'FLOAT_VECTOR':
data = np.empty(len(source_attr.data) * 3, dtype=np.float32)
source_attr.data.foreach_get('vector', data)
target_attr.data.foreach_set('vector', data)
elif source_attr.data_type == 'FLOAT_COLOR':
data = np.empty(len(source_attr.data) * 4, dtype=np.float32)
source_attr.data.foreach_get('color', data)
target_attr.data.foreach_set('color', data)
elif source_attr.data_type == 'BOOLEAN':
data = np.empty(len(source_attr.data), dtype=bool)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
## works in slowmotion (keep as reference for testing)
# def copy_attribute_values(src_dr, dst_dr, source_attr, dest_attr, data_size):
# ## Zip method to copy one by one
# val_type = {'FLOAT_COLOR': 'color','FLOAT_VECTOR': 'vector'}.get(source_attr.data_type, 'value')
# src_start = src_dr.curve_offsets[0].value
# src_end = src_start + data_size
# dst_start = dst_dr.curve_offsets[0].value
# dst_end = dst_start + data_size
# for src_idx, dest_idx in zip(range(src_start, src_end),range(dst_start, dst_end)):
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
"""
def bulk_copy_attributes(source_attr, target_attr):
'''Get and apply data as flat numpy array based on attribute type'''
value_string = attribute_value_string[source_attr.data_type]
dtype = attribute_value_dtype[source_attr.data_type]
shape = attribute_value_shape[source_attr.data_type]
domain_size = len(source_attr.data)
## Need to pass attributes to get domain size
# domain_size = attributes.domain_size(source_attr.domain)
# start = time()
data = np.empty((domain_size, *shape), dtype=dtype).ravel()
source_attr.data.foreach_get(value_string, data)
target_attr.data.foreach_set(value_string, data)
# end = time()
# np_empty = end - start
## np.prod (works, supposedly faster but tested slower)
# data = np.empty(int(domain_size * np.prod(shape)), dtype=dtype)
# source_attr.data.foreach_get(value_string, data)
# target_attr.data.foreach_set(value_string, data)
## np.zeros (works, sometimes faster on big set of attributes)
# start = time()
# data = np.zeros((domain_size, *shape), dtype=dtype)
# source_attr.data.foreach_get(value_string, np.ravel(data))
# target_attr.data.foreach_set(value_string, np.ravel(data))
# end = time()
# np_zero = end - start
# print('np EMPTY faster' if np_empty < np_zero else 'np ZERO faster', source_attr.domain, source_attr.data_type, domain_size)
# print('np_zero', np_zero)
# print('np_empty', np_empty)
# print()
def copy_frame_at(source_frame, layer, frame_number):
'''Copy a frame (source_frame) to a layer at given frame_number'''
source_drawing = source_frame.drawing
# frame_copy_start = time() # time_dbg
frame = layer.frames.new(frame_number)
dr = frame.drawing
dr.add_strokes([len(s.points) for s in source_drawing.strokes])
for attr_name in source_drawing.attributes.keys():
source_attr = source_drawing.attributes[attr_name]
if attr_name not in dr.attributes:
dr.attributes.new(
name=attr_name, type=source_attr.data_type, domain=source_attr.domain)
target_attr = dr.attributes[attr_name]
# start_time = time() # time_dbg-per-attrib
# bulk_frame_copy_attributes(source_attr, target_attr) # only some attributes
bulk_copy_attributes(source_attr, target_attr)
# copy_attribute_values(source_drawing, dr, source_attr, target_attr, source_drawing.attributes.domain_size(source_attr.domain)) # super slow
# end_time = time() # time_dbg-per-attrib
# print(f"copy_attribute '{attr_name}' execution time: {end_time - start_time} seconds") # time_dbg-per-attrib
# frame_copy_end = time() # time_dbg
# print(f"frame copy execution time: {frame_copy_end - frame_copy_start} seconds") # time_dbg
# ----------------- # -----------------
### Vector utils 3d ### Vector utils 3d
@ -907,7 +691,20 @@ def set_collection(ob, collection, unlink=True) :
# ----------------- # -----------------
def get_addon_prefs(): def get_addon_prefs():
return bpy.context.preferences.addons[__package__].preferences '''
function to read current addon preferences properties
access a prop like this :
prefs = get_addon_prefs()
option_state = prefs.super_special_option
oneliner : get_addon_prefs().super_special_option
'''
import os
addon_name = os.path.splitext(__name__)[0]
preferences = bpy.context.preferences
addon_prefs = preferences.addons[addon_name].preferences
return (addon_prefs)
def open_addon_prefs(): def open_addon_prefs():
'''Open addon prefs windows with focus on current addon''' '''Open addon prefs windows with focus on current addon'''
@ -1011,37 +808,27 @@ def convert_attr(Attr):
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
'''Show message box with element passed as string or list '''Show message box with element passed as string or list
if _message if a list of lists: if _message if a list of lists:
if first element is "OPERATOR":
List format: ["OPERATOR", operator_id, text, icon, {prop_name: value, ...}]
if sublist have 2 element: if sublist have 2 element:
considered a label [text, icon] considered a label [text,icon]
if sublist have 3 element: if sublist have 3 element:
considered as an operator [ops_id_name, text, icon] considered as an operator [ops_id_name, text, icon]
if sublist have 4 element:
considered as a property [object, propname, text, icon]
''' '''
def draw(self, context): def draw(self, context):
layout = self.layout
for l in _message: for l in _message:
if isinstance(l, str): if isinstance(l, str):
layout.label(text=l) self.layout.label(text=l)
elif l[0] == "OPERATOR": # Special operator case with properties else:
layout.operator_context = "INVOKE_DEFAULT" if len(l) == 2: # label with icon
op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False) self.layout.label(text=l[0], icon=l[1])
if len(l) > 4 and isinstance(l[4], dict): elif len(l) == 3: # ops
for prop_name, value in l[4].items(): self.layout.operator_context = "INVOKE_DEFAULT"
setattr(op, prop_name, value) self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
elif len(l) == 2: # label with icon ## offset pnale when using row...
layout.label(text=l[0], icon=l[1]) # row = self.layout.row()
elif len(l) == 3: # ops # row.label(text=l[1])
layout.operator_context = "INVOKE_DEFAULT" # row.operator(l[0], icon=l[2])
layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
elif len(l) == 4: # prop
row = layout.row(align=True)
row.label(text=l[2], icon=l[3])
row.prop(l[0], l[1], text='')
if isinstance(_message, str): if isinstance(_message, str):
_message = [_message] _message = [_message]
@ -1051,13 +838,6 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
### UI utils ### 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 ## kmi draw for addon without delete button
def draw_kmi(km, kmi, layout): def draw_kmi(km, kmi, layout):
map_type = kmi.map_type map_type = kmi.map_type
@ -1223,38 +1003,6 @@ def iterate_selector(zone, attr, state, info_attr = None, active_access='active'
return info, bottom return info, bottom
def iterate_active_layer(gpd, state):
'''Iterate active GP layer in stack
gpd: Grease Pencil Data
'''
layers = gpd.layers
l_count = len(layers)
if state: # swap
# info = None
# bottom = None
## Get active layer index
active_index = closest_layer_active_index(gpd, fallback_index=None)
if active_index == None:
## fallback to first layer if nothing found
gpd.layers.active = layers[0]
return
target_index = active_index + state
new_index = target_index % l_count
## set active layer
gpd.layers.active = layers[new_index]
if target_index == l_count:
bottom = 1 # bottom reached, cycle to first
elif target_index < 0:
bottom = -1 # up reached, cycle to last
# info = gpd.layers.active.name
# return info, bottom
# ----------------- # -----------------
### Curve handle ### Curve handle
# ----------------- # -----------------
@ -1436,7 +1184,7 @@ def all_anim_enabled(objects) -> bool:
if fcu.mute: if fcu.mute:
return False return False
if o.type in ('GREASEPENCIL', 'CAMERA'): if o.type in ('GPENCIL', 'CAMERA'):
if o.data.animation_data and o.data.animation_data.action: if o.data.animation_data and o.data.animation_data.action:
## Check if object data attributes fcurves are muted ## Check if object data attributes fcurves are muted
for fcu in o.animation_data.action.fcurves: for fcu in o.animation_data.action.fcurves:
@ -1449,9 +1197,9 @@ def all_anim_enabled(objects) -> bool:
def all_object_modifier_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''' '''Return False if one modifier of one object has GP modifier disabled in viewport but enabled in render'''
for o in objects: for o in objects:
if o.type != 'GREASEPENCIL': if o.type != 'GPENCIL':
continue continue
for m in o.modifiers: for m in o.grease_pencil_modifiers:
if m.show_render and not m.show_viewport: if m.show_render and not m.show_viewport:
return False return False
@ -1477,7 +1225,7 @@ def has_fully_enabled_anim(o):
if fcu.mute: if fcu.mute:
return False return False
if o.type in ('GREASEPENCIL', 'CAMERA'): if o.type in ('GPENCIL', 'CAMERA'):
if o.data.animation_data and o.data.animation_data.action: if o.data.animation_data and o.data.animation_data.action:
## Check if object data attributes fcurves are muted ## Check if object data attributes fcurves are muted
for fcu in o.animation_data.action.fcurves: for fcu in o.animation_data.action.fcurves:
@ -1522,7 +1270,7 @@ def anim_status(objects) -> tuple((str, str)):
on_count += 1 on_count += 1
count += 1 count += 1
if o.type in ('GREASEPENCIL', 'CAMERA'): if o.type in ('GPENCIL', 'CAMERA'):
datablock = o.data datablock = o.data
if datablock.animation_data is None: if datablock.animation_data is None:
continue continue
@ -1550,12 +1298,12 @@ def gp_modifier_status(objects) -> tuple((str, str)):
'''return icons on/off tuple''' '''return icons on/off tuple'''
on_count = off_count = count = 0 on_count = off_count = count = 0
for o in objects: for o in objects:
if o.type != 'GREASEPENCIL': if o.type != 'GPENCIL':
continue continue
## Skip hided object ## Skip hided object
if o.hide_get() and o.hide_render: if o.hide_get() and o.hide_render:
continue continue
for m in o.modifiers: for m in o.grease_pencil_modifiers:
if m.show_render and not m.show_viewport: if m.show_render and not m.show_viewport:
off_count += 1 off_count += 1
else: else: