Compare commits
38 Commits
Author | SHA1 | Date |
---|---|---|
pullusb | cf3f39b730 | |
pullusb | 1c39d81ef1 | |
pullusb | 7665dd4f4f | |
pullusb | 2013c55ba8 | |
pullusb | 3695470354 | |
pullusb | 186229cdba | |
pullusb | e441f485a6 | |
pullusb | 0cee6163aa | |
pullusb | 58e6816e39 | |
pullusb | 4d6dc06e4e | |
pullusb | c8763f5ca4 | |
pullusb | f9e7c9cc3b | |
pullusb | 86fb848e4a | |
pullusb | abb61ca6d4 | |
pullusb | 7430bca02f | |
pullusb | f5c20a3499 | |
pullusb | edfefa874a | |
pullusb | 6dbf666ee3 | |
pullusb | d3e4072564 | |
pullusb | 63f377d7d1 | |
pullusb | 49c70860a6 | |
pullusb | b525cda28e | |
pullusb | 05053cff68 | |
pullusb | 4937aa32c0 | |
pullusb | 5d930df06b | |
pullusb | 5d35074d3d | |
pullusb | 94fc926f7a | |
pullusb | 0160b4eae4 | |
pullusb | 1dfb8cff9c | |
pullusb | 4732110b93 | |
pullusb | 7bc7d5d9ff | |
pullusb | 998bd4b0cb | |
pullusb | 25adb5beb6 | |
pullusb | 6e94ee270d | |
pullusb | 3ece64e517 | |
pullusb | eae69b6f75 | |
pullusb | f5a78601b6 | |
pullusb | 98ed92afe2 |
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,5 +1,22 @@
|
|||
# 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.
|
||||
|
|
|
@ -5,23 +5,36 @@ from bpy.props import (FloatProperty,
|
|||
EnumProperty,
|
||||
StringProperty,
|
||||
IntProperty)
|
||||
from .. import utils
|
||||
from ..utils import is_hidden
|
||||
|
||||
## copied from OP_key_duplicate_send
|
||||
def get_layer_list(self, context):
|
||||
'''return (identifier, name, description) of enum content'''
|
||||
return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||
|
||||
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):
|
||||
bl_idname = "gp.create_empty_frames"
|
||||
bl_label = "Create Empty Frames"
|
||||
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)"
|
||||
bl_description = "Create new empty frames on active layer where there is a frame in targeted layers\
|
||||
\n(usefull in color layers to match line frames)"
|
||||
bl_options = {'REGISTER','UNDO'}
|
||||
|
||||
layers_enum : EnumProperty(
|
||||
name="Duplicate to layers",
|
||||
description="Duplicate selected keys in active layer and send them to choosen layer",
|
||||
name="Empty Keys from Layer",
|
||||
description="Reference keys from layer",
|
||||
items=get_layer_list
|
||||
)
|
||||
|
||||
groups_enum : EnumProperty(
|
||||
name="Empty Keys from Group",
|
||||
description="Duplicate keys from group",
|
||||
items=get_group_list
|
||||
)
|
||||
|
||||
targeted_layers : EnumProperty(
|
||||
name="Sources", # Empty keys from targets
|
||||
|
@ -34,7 +47,8 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
|
|||
('ABOVE', 'Layer Directly Above', 'Empty frames from layer directly above'),
|
||||
('BELOW', 'Layer Directly Below', 'Empty frames from layer directly below'),
|
||||
('ALL_VISIBLE', 'Visible', 'Empty frames from all visible layers'),
|
||||
('CHOSEN', 'Chosen layer', ''),
|
||||
('CHOSEN', 'Chosen layer', 'Empty frames from a specific layer'),
|
||||
('CHOSEN_GROUP', 'Chosen group', 'Empty frames from a specific layer group'),
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -57,12 +71,20 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Possible preset with shortcut
|
||||
# if event.alt:
|
||||
# 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)
|
||||
|
||||
def draw(self, context):
|
||||
|
@ -74,7 +96,13 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
|
|||
if self.layers_enum:
|
||||
layout.prop(self, 'layers_enum')
|
||||
else:
|
||||
layout.label(text='No other layers to match keyframe')
|
||||
layout.label(text='No other layers to match keyframe!', icon='ERROR')
|
||||
|
||||
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':
|
||||
row = layout.row()
|
||||
|
@ -91,41 +119,56 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
|
|||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
gpl = obj.data.layers
|
||||
|
||||
gp = obj.data
|
||||
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)
|
||||
if self.targeted_layers == 'ALL_ABOVE':
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i > gpl.active_index]
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i > active_index]
|
||||
|
||||
elif self.targeted_layers == 'ALL_BELOW':
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i < gpl.active_index]
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i < active_index]
|
||||
|
||||
elif self.targeted_layers == 'ABOVE':
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i == gpl.active_index + 1]
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index + 1]
|
||||
|
||||
elif self.targeted_layers == 'BELOW':
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i == gpl.active_index - 1]
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index - 1]
|
||||
|
||||
elif self.targeted_layers == 'ALL_VISIBLE':
|
||||
tgt_layers = [l for l in gpl if not l.hide and l != gpl.active]
|
||||
tgt_layers = [l for l in gpl if not is_hidden(l) and l != gpl.active]
|
||||
|
||||
elif self.targeted_layers == 'CHOSEN':
|
||||
if not self.layers_enum:
|
||||
self.report({'ERROR'}, f"No chosen layers, aborted")
|
||||
return {'CANCELLED'}
|
||||
tgt_layers = [l for l in gpl if l.info == self.layers_enum]
|
||||
tgt_layers = [l for l in gpl if l.name == self.layers_enum]
|
||||
|
||||
elif self.targeted_layers == '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':
|
||||
if self.number == 0:
|
||||
self.report({'ERROR'}, f"Can't have 0 as value")
|
||||
return {'CANCELLED'}
|
||||
|
||||
l_range = gpl.active_index + self.number
|
||||
l_range = active_index + self.number
|
||||
print('l_range: ', l_range)
|
||||
if self.number > 0: # positive
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if gpl.active_index < i <= l_range]
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if active_index < i <= l_range]
|
||||
else:
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if gpl.active_index > i >= l_range]
|
||||
tgt_layers = [l for i, l in enumerate(gpl) if active_index > i >= l_range]
|
||||
|
||||
if not tgt_layers:
|
||||
self.report({'ERROR'}, f"No layers found with chosen Targets")
|
||||
|
@ -163,12 +206,12 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
|
|||
if num in current_frames:
|
||||
continue
|
||||
#Create empty frame
|
||||
gpl.active.frames.new(num, active=False)
|
||||
gpl.active.frames.new(num)
|
||||
fct += 1
|
||||
|
||||
gpl.update()
|
||||
if fct:
|
||||
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
|
||||
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.name}")
|
||||
else:
|
||||
self.report({'WARNING'}, f"No frames to create !")
|
||||
|
||||
|
@ -178,4 +221,4 @@ def register():
|
|||
bpy.utils.register_class(GP_OT_create_empty_frames)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(GP_OT_create_empty_frames)
|
||||
bpy.utils.unregister_class(GP_OT_create_empty_frames)
|
||||
|
|
|
@ -5,11 +5,14 @@ from ..utils import (location_to_region,
|
|||
vector_length,
|
||||
draw_gp_stroke,
|
||||
extrapolate_points_by_length,
|
||||
simple_draw_gp_stroke)
|
||||
simple_draw_gp_stroke,
|
||||
is_hidden,
|
||||
is_locked)
|
||||
|
||||
import bpy
|
||||
from math import degrees
|
||||
from mathutils import Vector
|
||||
|
||||
# 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
|
||||
|
@ -53,11 +56,14 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
|||
encounter = defaultdict(list)
|
||||
plist = []
|
||||
matrix = ob.matrix_world
|
||||
for s in f.strokes:#add first and last
|
||||
for s in f.drawing.strokes: #add first and last
|
||||
smat = ob.material_slots[s.material_index].material
|
||||
if not smat:continue#no material on line
|
||||
if smat.grease_pencil.show_fill:continue# skip fill lines -> #smat.grease_pencil.show_stroke
|
||||
if len(s.points) < 2:continue#avoid 0 or 1 points
|
||||
if not smat:
|
||||
continue #no material on line
|
||||
if smat.grease_pencil.show_fill:
|
||||
continue # skip fill lines -> #smat.grease_pencil.show_stroke
|
||||
if len(s.points) < 2:
|
||||
continue #avoid 0 or 1 points
|
||||
plist.append(s.points[0])
|
||||
plist.append(s.points[-1])
|
||||
# plist.extend([s.points[0], s.points[-1])# is extend faster ?
|
||||
|
@ -70,7 +76,7 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
|||
for op in plist:#other points
|
||||
if p == op:# print('same point')
|
||||
continue
|
||||
gap2d = vector_length_2d(location_to_region(matrix @ p.co), location_to_region(matrix @ op.co))
|
||||
gap2d = vector_length_2d(location_to_region(matrix @ p.position), location_to_region(matrix @ op.position))
|
||||
# print('gap2d: ', gap2d)
|
||||
if gap2d > tol:
|
||||
continue
|
||||
|
@ -102,16 +108,16 @@ def create_gap_stroke(f, ob, tol=10, mat_id=None):
|
|||
encounter[p].append(op)
|
||||
|
||||
|
||||
simple_draw_gp_stroke([p.co, op.co], f, width = 2, mat_id = mat_id)
|
||||
simple_draw_gp_stroke([p.position, op.position], f, width = 2, mat_id = mat_id)
|
||||
ctl += 1
|
||||
|
||||
print(f'{ctl} line created')
|
||||
|
||||
##test_call: #create_gap_stroke(C.object.data.layers.active.active_frame, C.object, mat_id=C.object.active_material_index)
|
||||
##test_call: #create_gap_stroke(C.object.data.layers.active.current_frame(), C.object, mat_id=C.object.active_material_index)
|
||||
|
||||
def create_closing_line(tolerance=0.2):
|
||||
for ob in bpy.context.selected_objects:
|
||||
if ob.type != 'GPENCIL':
|
||||
if ob.type != 'GREASEPENCIL':
|
||||
continue
|
||||
|
||||
mat_id = get_closeline_mat(ob)# get a the closing material
|
||||
|
@ -128,7 +134,7 @@ def create_closing_line(tolerance=0.2):
|
|||
## filter on selected
|
||||
if not l.select:continue# comment this line for all
|
||||
# for f in l.frames:#not all for now
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
## create gap stroke
|
||||
create_gap_stroke(f, ob, tol=tolerance, mat_id=mat_id)
|
||||
|
||||
|
@ -143,9 +149,9 @@ def is_deviating_by(s, deviation=0.75):
|
|||
pb = s.points[-2]
|
||||
pc = s.points[-3]
|
||||
|
||||
a = location_to_region(pa.co)
|
||||
b = location_to_region(pb.co)
|
||||
c = location_to_region(pc.co)
|
||||
a = location_to_region(pa.position)
|
||||
b = location_to_region(pb.position)
|
||||
c = location_to_region(pc.position)
|
||||
|
||||
#cb-> compare angle with ba->
|
||||
angle = (b-c).angle(a-b)
|
||||
|
@ -158,16 +164,16 @@ def extend_stroke_tips(s,f,ob,length, mat_id):
|
|||
'''extend line boundary by given length'''
|
||||
for id_pair in [ [1,0], [-2,-1] ]:# start and end pair
|
||||
## 2D mode
|
||||
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].co)
|
||||
# b_loc = ob.matrix_world @ s.points[id_pair[1]].co
|
||||
# a = location_to_region(ob.matrix_world @ s.points[id_pair[0]].position)
|
||||
# b_loc = ob.matrix_world @ s.points[id_pair[1]].position
|
||||
# b = location_to_region(b_loc)
|
||||
# c = extrapolate_points_by_length(a,b,length)#print(vector_length_2d(b,c))
|
||||
# c_loc = region_to_location(c, b_loc)
|
||||
# simple_draw_gp_stroke([ob.matrix_world.inverted() @ b_loc, ob.matrix_world.inverted() @ c_loc], f, width=2, mat_id=mat_id)
|
||||
|
||||
## 3D
|
||||
a = s.points[id_pair[0]].co# ob.matrix_world @
|
||||
b = s.points[id_pair[1]].co# ob.matrix_world @
|
||||
a = s.points[id_pair[0]].position# ob.matrix_world @
|
||||
b = s.points[id_pair[1]].position# ob.matrix_world @
|
||||
c = extrapolate_points_by_length(a,b,length)#print(vector_length(b,c))
|
||||
simple_draw_gp_stroke([b,c], f, width=2, mat_id=mat_id)
|
||||
|
||||
|
@ -188,15 +194,15 @@ def change_extension_length(ob, strokelist, length, selected=False):
|
|||
|
||||
## Change length of current length to designated
|
||||
# Vector point A to point B (direction), push point B in this direction
|
||||
a = s.points[-2].co
|
||||
a = s.points[-2].position
|
||||
bp = s.points[-1]#end-point
|
||||
b = bp.co
|
||||
b = bp.position
|
||||
ab = b - a
|
||||
if not ab:
|
||||
continue
|
||||
# new pos of B is A + new length in the AB direction
|
||||
newb = a + (ab.normalized() * length)
|
||||
bp.co = newb
|
||||
bp.position = newb
|
||||
ct += 1
|
||||
|
||||
return ct
|
||||
|
@ -210,14 +216,14 @@ def extend_all_strokes_tips(ob, frame, length=10, selected=False):
|
|||
return
|
||||
|
||||
# TODO need custom filters or go in GP refine strokes...
|
||||
# frame = ob.data.layers.active.active_frame
|
||||
# frame = ob.data.layers.active.current_frame()
|
||||
|
||||
if not frame: return
|
||||
ct = 0
|
||||
#TODO need to delete previous closing lines on frame before launching
|
||||
|
||||
# iterate in a copy of stroke list to avoid growing frame.strokes as we loop in !
|
||||
for s in list(frame.strokes):
|
||||
# iterate in a copy of stroke list to avoid growing frame.drawing.strokes as we loop in !
|
||||
for s in list(frame.drawing.strokes):
|
||||
if s.material_index == mat_id:#is a closeline
|
||||
continue
|
||||
if len(s.points) < 2:#not enough point to evaluate
|
||||
|
@ -241,7 +247,7 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
|
||||
|
||||
# mode : bpy.props.StringProperty(
|
||||
# name="mode", description="Set mode for operator", default="render", maxlen=0, subtype='NONE', options={'ANIMATABLE'})
|
||||
|
@ -273,18 +279,18 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
|
|||
if self.layer_tgt == 'ACTIVE':
|
||||
lays = [ob.data.layers.active]
|
||||
elif self.layer_tgt == 'SELECTED':
|
||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
||||
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
|
||||
elif self.layer_tgt == 'ALL_VISIBLE':
|
||||
lays = [l for l in ob.data.layers if not l.hide]
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l)]
|
||||
else:
|
||||
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
|
||||
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
|
||||
|
||||
fct = 0
|
||||
for l in lays:
|
||||
if not l.active_frame:
|
||||
print(f'{l.info} has no active frame')
|
||||
if not l.current_frame():
|
||||
print(f'{l.name} has no active frame')
|
||||
continue
|
||||
fct += extend_all_strokes_tips(ob, l.active_frame, length = self.length, selected = self.selected)
|
||||
fct += extend_all_strokes_tips(ob, l.current_frame(), length = self.length, selected = self.selected)
|
||||
|
||||
if not fct:
|
||||
mess = "No strokes extended... see console"
|
||||
|
@ -306,7 +312,7 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
|
||||
|
||||
layer_tgt : bpy.props.EnumProperty(
|
||||
name="Extend layers", description="Choose which layer to target",
|
||||
|
@ -334,18 +340,18 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
|
|||
if self.layer_tgt == 'ACTIVE':
|
||||
lays = [ob.data.layers.active]
|
||||
elif self.layer_tgt == 'SELECTED':
|
||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
||||
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
|
||||
elif self.layer_tgt == 'ALL_VISIBLE':
|
||||
lays = [l for l in ob.data.layers if not l.hide]
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l)]
|
||||
else:
|
||||
lays = [l for l in ob.data.layers if not any(x in l.info for x in ('spot', 'colo'))]
|
||||
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
|
||||
|
||||
fct = 0
|
||||
for l in lays:
|
||||
if not l.active_frame:
|
||||
print(f'{l.info} has no active frame')
|
||||
if not l.current_frame():
|
||||
print(f'{l.name} has no active frame')
|
||||
continue
|
||||
fct += change_extension_length(ob, [s for s in l.active_frame.strokes], length = self.length, selected = self.selected)
|
||||
fct += change_extension_length(ob, [s for s in l.current_frame().drawing.strokes], length = self.length, selected = self.selected)
|
||||
|
||||
if not fct:
|
||||
mess = "No extension modified... see console"
|
||||
|
@ -367,15 +373,15 @@ class GPSTK_OT_comma_finder(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None and context.active_object.type == 'GPENCIL'
|
||||
return context.active_object is not None and context.active_object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
ct = 0
|
||||
ob = context.object
|
||||
lays = [l for l in ob.data.layers if not l.hide and not l.lock]
|
||||
lays = [l for l in ob.data.layers if not is_hidden(l) and not is_locked(l)]
|
||||
for l in lays:
|
||||
if not l.active_frame:continue
|
||||
for s in l.active_frame.strokes:
|
||||
if not l.current_frame():continue
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
if is_deviating_by(s, context.scene.gpcolor_props.deviation_tolerance):
|
||||
ct+=1
|
||||
|
||||
|
@ -397,7 +403,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.object is not None)# and context.object.type == 'GPENCIL'
|
||||
return (context.object is not None)# and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
## draw stuff inside the header (place before main label)
|
||||
# def draw_header(self, context):
|
||||
|
@ -414,7 +420,7 @@ class GPSTK_PT_line_closer_panel(bpy.types.Panel):
|
|||
layout.operator("gp.extend_close_lines", icon = 'SNAP_MIDPOINT')
|
||||
|
||||
#diplay closeline visibility
|
||||
if context.object.type == 'GPENCIL' and context.object.data.materials.get('closeline'):
|
||||
if context.object.type == 'GREASEPENCIL' and context.object.data.materials.get('closeline'):
|
||||
row=layout.row()
|
||||
row.prop(context.object.data.materials['closeline'].grease_pencil, 'hide', text='Stop lines')
|
||||
row.operator("gp.change_close_lines_extension", text='Length', icon = 'DRIVER_DISTANCE')
|
||||
|
|
|
@ -27,7 +27,7 @@ class GPTB_OT_load_brushes(bpy.types.Operator, ImportHelper):
|
|||
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return context.object and context.object.type == 'GPENCIL'
|
||||
# return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filename_ext = '.blend'
|
||||
|
||||
|
@ -58,7 +58,7 @@ class GPTB_OT_brush_set(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.mode == 'PAINT_GPENCIL'
|
||||
return context.object and context.mode == 'PAINT_GREASE_PENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
brush = bpy.data.brushes.get(self.brush_name)
|
||||
|
@ -72,12 +72,12 @@ class GPTB_OT_brush_set(bpy.types.Operator):
|
|||
|
||||
def load_brush_ui(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
if context.mode == 'PAINT_GPENCIL':
|
||||
if context.mode == 'PAINT_GREASE_PENCIL':
|
||||
self.layout.operator('gp.load_brushes', icon='KEYTYPE_JITTER_VEC').filepath = prefs.brush_path
|
||||
|
||||
def load_brush_top_bar_ui(self, context):
|
||||
prefs = get_addon_prefs()
|
||||
if context.mode == 'PAINT_GPENCIL':
|
||||
if context.mode == 'PAINT_GREASE_PENCIL':
|
||||
self.layout.operator('gp.load_brushes').filepath = prefs.brush_path
|
||||
|
||||
classes = (
|
||||
|
@ -89,12 +89,12 @@ def register():
|
|||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
bpy.types.VIEW3D_MT_brush_gpencil_context_menu.append(load_brush_ui)
|
||||
bpy.types.VIEW3D_MT_brush_context_menu.append(load_brush_ui)
|
||||
bpy.types.VIEW3D_HT_tool_header.append(load_brush_top_bar_ui)
|
||||
|
||||
def unregister():
|
||||
bpy.types.VIEW3D_HT_tool_header.remove(load_brush_top_bar_ui)
|
||||
bpy.types.VIEW3D_MT_brush_gpencil_context_menu.remove(load_brush_ui)
|
||||
bpy.types.VIEW3D_MT_brush_context_menu.remove(load_brush_ui)
|
||||
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
||||
|
|
374
OP_copy_paste.py
374
OP_copy_paste.py
|
@ -1,38 +1,15 @@
|
|||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
## based on GPclipboard 1.3.2 (just stripped addon prefs)
|
||||
|
||||
bl_info = {
|
||||
"name": "GP clipboard",
|
||||
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
|
||||
"author": "Samuel Bernou",
|
||||
"version": (1, 3, 3),
|
||||
"blender": (2, 83, 0),
|
||||
"location": "View3D > Toolbar > Gpencil > GP clipboard",
|
||||
"warning": "",
|
||||
"doc_url": "https://github.com/Pullusb/GP_clipboard",
|
||||
"category": "Object" }
|
||||
## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends
|
||||
## View3D > Toolbar > Gpencil > GP clipboard
|
||||
## in 4.2- existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
|
||||
|
||||
import bpy
|
||||
import os
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
import json
|
||||
from time import time
|
||||
from operator import itemgetter
|
||||
from itertools import groupby
|
||||
# from pprint import pprint
|
||||
from .utils import is_locked, is_hidden
|
||||
|
||||
def convertAttr(Attr):
|
||||
'''Convert given value to a Json serializable format'''
|
||||
|
@ -45,104 +22,106 @@ def convertAttr(Attr):
|
|||
else:
|
||||
return(Attr)
|
||||
|
||||
def getMatrix (layer) :
|
||||
def getMatrix(layer) :
|
||||
matrix = mathutils.Matrix.Identity(4)
|
||||
|
||||
if layer.is_parented:
|
||||
if layer.parent_type == 'BONE':
|
||||
object = layer.parent
|
||||
bone = object.pose.bones[layer.parent_bone]
|
||||
matrix = bone.matrix @ object.matrix_world
|
||||
matrix = matrix.copy() @ layer.matrix_inverse
|
||||
else :
|
||||
matrix = layer.parent.matrix_world @ layer.matrix_inverse
|
||||
if parent := layer.parent:
|
||||
if parent.type == 'ARMATURE' and layer.parent_bone:
|
||||
bone = parent.pose.bones[layer.parent_bone]
|
||||
matrix = bone.matrix @ parent.matrix_world
|
||||
matrix = matrix.copy() @ layer.matrix_parent_inverse
|
||||
else:
|
||||
matrix = parent.matrix_world @ layer.matrix_parent_inverse
|
||||
|
||||
return matrix.copy()
|
||||
|
||||
default_pt_uv_fill = Vector((0.5, 0.5))
|
||||
# default_pt_uv_fill = Vector((0.5, 0.5))
|
||||
|
||||
def dump_gp_point(p, l, obj,
|
||||
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
|
||||
radius=True, opacity=True, vertex_color=True, fill_color=True, uv_factor=True, rotation=True):
|
||||
'''add properties of a given points to a dic and return it'''
|
||||
pdic = {}
|
||||
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
|
||||
point_dict = {}
|
||||
#point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type'
|
||||
#for att in point_attr_list:
|
||||
# pdic[att] = convertAttr(getattr(p, att))
|
||||
# point_dict[att] = convertAttr(getattr(p, att))
|
||||
if l.parent:
|
||||
mat = getMatrix(l)
|
||||
pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
|
||||
point_dict['position'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'position'))
|
||||
else:
|
||||
pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co'))
|
||||
point_dict['position'] = convertAttr(obj.matrix_world @ getattr(p,'position'))
|
||||
|
||||
# pdic['select'] = convertAttr(getattr(p,'select')) # need selection ?
|
||||
if pressure and p.pressure != 1.0:
|
||||
pdic['pressure'] = convertAttr(getattr(p,'pressure'))
|
||||
if strength and p.strength != 1.0:
|
||||
pdic['strength'] = convertAttr(getattr(p,'strength'))
|
||||
# point_dict['select'] = convertAttr(getattr(p,'select')) # need selection ?
|
||||
if radius and p.radius != 1.0:
|
||||
point_dict['radius'] = convertAttr(getattr(p,'radius'))
|
||||
|
||||
if opacity and p.opacity != 1.0:
|
||||
point_dict['opacity'] = convertAttr(getattr(p,'opacity'))
|
||||
|
||||
## get vertex color (long...)
|
||||
if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
|
||||
pdic['vertex_color'] = convertAttr(p.vertex_color)
|
||||
point_dict['vertex_color'] = convertAttr(p.vertex_color)
|
||||
|
||||
## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...)
|
||||
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)
|
||||
if rotation and p.rotation != 0.0:
|
||||
point_dict['rotation'] = convertAttr(p.rotation)
|
||||
|
||||
return pdic
|
||||
## No time infos
|
||||
# if delta_time and p.delta_time != 0.0:
|
||||
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
|
||||
|
||||
return point_dict
|
||||
|
||||
def dump_gp_stroke_range(s, sid, l, obj,
|
||||
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
|
||||
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
||||
'''Get a grease pencil stroke and return a dic with attribute
|
||||
(points attribute being a dic of dics to store points and their attributes)
|
||||
'''
|
||||
|
||||
sdic = {}
|
||||
stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles'
|
||||
for att in stroke_attr_list:
|
||||
sdic[att] = getattr(s, att)
|
||||
|
||||
stroke_dict = {}
|
||||
# stroke_attr_list = ('line_width',)
|
||||
# for att in stroke_attr_list:
|
||||
# stroke_dict[att] = getattr(s, att)
|
||||
|
||||
## Dump following these value only if they are non default
|
||||
if s.material_index != 0:
|
||||
sdic['material_index'] = s.material_index
|
||||
|
||||
if getattr(s, 'draw_cyclic', None): # pre-2.92
|
||||
sdic['draw_cyclic'] = s.draw_cyclic
|
||||
|
||||
if getattr(s, 'use_cyclic', None): # from 2.92
|
||||
sdic['use_cyclic'] = s.use_cyclic
|
||||
|
||||
if s.uv_scale != 1.0:
|
||||
sdic['uv_scale'] = s.uv_scale
|
||||
|
||||
if s.uv_rotation != 0.0:
|
||||
sdic['uv_rotation'] = s.uv_rotation
|
||||
|
||||
if s.hardness != 1.0:
|
||||
sdic['hardness'] = s.hardness
|
||||
|
||||
if s.uv_translation != Vector((0.0, 0.0)):
|
||||
sdic['uv_translation'] = convertAttr(s.uv_translation)
|
||||
stroke_dict['material_index'] = s.material_index
|
||||
|
||||
if s.vertex_color_fill[:] != (0,0,0,0):
|
||||
sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill)
|
||||
if s.cyclic:
|
||||
stroke_dict['cyclic'] = s.cyclic
|
||||
|
||||
if s.softness != 0.0:
|
||||
stroke_dict['softness'] = s.softness
|
||||
|
||||
if s.aspect_ratio != 1.0:
|
||||
stroke_dict['aspect_ratio'] = s.aspect_ratio
|
||||
|
||||
if s.start_cap != 0:
|
||||
stroke_dict['start_cap'] = s.start_cap
|
||||
|
||||
if s.end_cap != 0:
|
||||
stroke_dict['end_cap'] = s.end_cap
|
||||
|
||||
if fill_color and s.fill_color[:] != (0,0,0,0):
|
||||
stroke_dict['fill_color'] = convertAttr(s.fill_color)
|
||||
|
||||
if fill_opacity and s.fill_opacity != 0.0:
|
||||
stroke_dict['fill_opacity'] = s.fill_opacity
|
||||
|
||||
## No time infos
|
||||
# if s.time_start != 0.0:
|
||||
# stroke_dict['time_start'] = s.time_start
|
||||
|
||||
points = []
|
||||
if sid is None: # no ids, just full points...
|
||||
for p in s.points:
|
||||
points.append(dump_gp_point(p,l,obj,
|
||||
pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation))
|
||||
points.append(dump_gp_point(p, l, obj,
|
||||
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
|
||||
else:
|
||||
for pid in sid:
|
||||
points.append(dump_gp_point(s.points[pid],l,obj,
|
||||
pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation))
|
||||
sdic['points'] = points
|
||||
return sdic
|
||||
|
||||
points.append(dump_gp_point(s.points[pid], l, obj,
|
||||
radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
|
||||
|
||||
stroke_dict['points'] = points
|
||||
return stroke_dict
|
||||
|
||||
|
||||
def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
||||
|
@ -161,7 +140,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
|||
# color = gp.palettes.active.colors.active.name
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock] # []
|
||||
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # []
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
@ -169,32 +148,36 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
|||
stroke_list = [] # one stroke list for all layers.
|
||||
|
||||
for l in layers:
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
|
||||
if f: # active frame can be None
|
||||
if not copy:
|
||||
staylist = [] # init part of strokes that must survive on this layer
|
||||
|
||||
for s in f.strokes:
|
||||
if s.select:
|
||||
rm_list = [] # init strokes that must be removed from this layer
|
||||
for s_index, stroke in enumerate(f.drawing.strokes):
|
||||
if stroke.select:
|
||||
# separate in multiple stroke if parts of the strokes a selected.
|
||||
sel = [i for i, p in enumerate(s.points) if p.select]
|
||||
sel = [i for i, p in enumerate(stroke.points) if p.select]
|
||||
substrokes = [] # list of list containing isolated selection
|
||||
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
|
||||
|
||||
# continuity stroke have same substract result between point index and enumerator
|
||||
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):
|
||||
group = list(map(itemgetter(1), g))
|
||||
substrokes.append(group)
|
||||
|
||||
for ss in substrokes:
|
||||
if len(ss) > 1: # avoid copy isolated points
|
||||
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))
|
||||
stroke_list.append(dump_gp_stroke_range(stroke, ss, l, obj))
|
||||
|
||||
# Cutting operation
|
||||
if not copy:
|
||||
maxindex = len(s.points)-1
|
||||
maxindex = len(stroke.points)-1
|
||||
if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
|
||||
f.strokes.remove(s)
|
||||
# f.drawing.strokes.remove(stroke) # gpv2
|
||||
rm_list.append(s_index)
|
||||
else:
|
||||
neg = [i for i, p in enumerate(s.points) if not p.select]
|
||||
neg = [i for i, p in enumerate(stroke.points) if not p.select]
|
||||
|
||||
staying = []
|
||||
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
|
||||
|
@ -208,37 +191,30 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
|
|||
|
||||
for ns in staying:
|
||||
if len(ns) > 1:
|
||||
staylist.append(dump_gp_stroke_range(s,ns,l,obj))
|
||||
staylist.append(dump_gp_stroke_range(stroke, ns, l, obj))
|
||||
# make a negative list containing all last index
|
||||
|
||||
|
||||
'''#full stroke version
|
||||
# if s.colorname == color: #line for future filters
|
||||
stroke_list.append(dump_gp_stroke(s,l))
|
||||
#delete stroke on the fly
|
||||
if not copy:
|
||||
f.strokes.remove(s)
|
||||
'''
|
||||
if rm_list:
|
||||
f.drawing.remove_strokes(indices=rm_list)
|
||||
|
||||
if not copy:
|
||||
selected_ids = [i for i, s in enumerate(f.drawing.strokes) if s.select]
|
||||
|
||||
# delete all selected strokes...
|
||||
for s in f.strokes:
|
||||
if s.select:
|
||||
f.strokes.remove(s)
|
||||
if selected_ids:
|
||||
f.drawing.remove_strokes(indices=selected_ids)
|
||||
|
||||
# ...recreate these uncutted ones
|
||||
#pprint(staylist)
|
||||
if staylist:
|
||||
add_multiple_strokes(staylist, l)
|
||||
#for ns in staylist:#weirdly recreate the stroke twice !
|
||||
# add_stroke(ns, f, l)
|
||||
|
||||
#if nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
|
||||
if not copy and not keep_empty:#
|
||||
if not len(f.strokes):
|
||||
# If nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
|
||||
if not copy and not keep_empty:
|
||||
if not len(f.drawing.strokes):
|
||||
l.frames.remove(f)
|
||||
|
||||
|
||||
|
||||
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||
#print(stroke_list)
|
||||
return stroke_list
|
||||
|
@ -260,7 +236,7 @@ def copy_all_strokes(layers=None):
|
|||
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
|
||||
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ?
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
@ -268,12 +244,12 @@ def copy_all_strokes(layers=None):
|
|||
stroke_list = []# one stroke list for all layers.
|
||||
|
||||
for l in layers:
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
|
||||
if not f:
|
||||
continue# active frame can be None
|
||||
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
## full stroke version
|
||||
# if s.select:
|
||||
stroke_list.append(dump_gp_stroke_range(s, None, l, obj))
|
||||
|
@ -284,7 +260,7 @@ def copy_all_strokes(layers=None):
|
|||
"""
|
||||
|
||||
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
|
||||
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
|
||||
radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
|
||||
'''
|
||||
copy all stroke, not affected by selection on active frame
|
||||
layers can be None, a single layer object or list of layer object as filter
|
||||
|
@ -300,7 +276,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
|
|||
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not l.hide and not l.lock] # include locked ?
|
||||
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # include locked ?
|
||||
if not isinstance(layers, list):
|
||||
# if a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
|
@ -308,17 +284,17 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
|
|||
stroke_list = []
|
||||
|
||||
for l in layers:
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
|
||||
if not f:
|
||||
continue# active frame can be None
|
||||
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
## full stroke version
|
||||
# if s.select:
|
||||
# send index of all points to get the whole stroke with "range"
|
||||
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj,
|
||||
pressure=pressure,strength=strength,vertex_color=vertex_color,uv_fill=uv_fill,uv_factor=uv_factor,uv_rotation=uv_rotation))
|
||||
radius=radius, opacity=opacity, vertex_color=vertex_color, fill_color=fill_color, fill_opacity=fill_opacity, rotation=rotation))
|
||||
|
||||
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
|
||||
return stroke_list
|
||||
|
@ -326,49 +302,43 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
|
|||
def add_stroke(s, frame, layer, obj, select=False):
|
||||
'''add stroke on a given frame, (layer is for parentage setting)'''
|
||||
# print(3*'-',s)
|
||||
ns = frame.strokes.new()
|
||||
pts_to_add = len(s['points'])
|
||||
frame.drawing.add_strokes([pts_to_add])
|
||||
|
||||
ns = frame.drawing.strokes[-1]
|
||||
|
||||
## set strokes atrributes
|
||||
for att, val in s.items():
|
||||
if att not in ('points'):
|
||||
setattr(ns, att, val)
|
||||
pts_to_add = len(s['points'])
|
||||
# print(pts_to_add, 'points')#dbg
|
||||
|
||||
ns.points.add(pts_to_add)
|
||||
|
||||
ob_mat_inv = obj.matrix_world.inverted()
|
||||
|
||||
## patch pressure 1
|
||||
# pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened
|
||||
|
||||
if layer.is_parented:
|
||||
mat = getMatrix(layer).inverted()
|
||||
for i, pt in enumerate(s['points']):
|
||||
for k, v in pt.items():
|
||||
if k == 'co':
|
||||
setattr(ns.points[i], k, v)
|
||||
ns.points[i].co = ob_mat_inv @ mat @ ns.points[i].co # invert of object * invert of layer * coordinate
|
||||
else:
|
||||
setattr(ns.points[i], k, v)
|
||||
if select:
|
||||
ns.points[i].select = True
|
||||
|
||||
if layer.parent:
|
||||
layer_matrix = getMatrix(layer).inverted()
|
||||
transform_matrix = ob_mat_inv @ layer_matrix
|
||||
else:
|
||||
for i, pt in enumerate(s['points']):
|
||||
for k, v in pt.items():
|
||||
if k == 'co':
|
||||
setattr(ns.points[i], k, v)
|
||||
ns.points[i].co = ob_mat_inv @ ns.points[i].co# invert of object * coordinate
|
||||
else:
|
||||
setattr(ns.points[i], k, v)
|
||||
if select:
|
||||
ns.points[i].select = True
|
||||
transform_matrix = ob_mat_inv
|
||||
|
||||
## Set points attributes
|
||||
for i, pt in enumerate(s['points']):
|
||||
for k, v in pt.items():
|
||||
if k == 'position':
|
||||
setattr(ns.points[i], k, v)
|
||||
ns.points[i].position = transform_matrix @ ns.points[i].position # invert of object * invert of layer * coordinate
|
||||
else:
|
||||
setattr(ns.points[i], k, v)
|
||||
if select:
|
||||
ns.points[i].select = True
|
||||
|
||||
## Opacity initialized at 0.0 (should be 1.0)
|
||||
if not 'opacity' in pt:
|
||||
ns.points[i].opacity = 1.0
|
||||
|
||||
## Radius initialized at 0.0 (should probably be 0.01)
|
||||
if not 'radius' in pt:
|
||||
ns.points[i].radius = 0.01
|
||||
|
||||
## 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):
|
||||
'''
|
||||
|
@ -389,7 +359,7 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
|
|||
|
||||
fnum = scene.frame_current
|
||||
target_frame = False
|
||||
act = layer.active_frame
|
||||
act = layer.current_frame()
|
||||
## set frame if needed
|
||||
if act:
|
||||
if use_current_frame or act.frame_number == fnum:
|
||||
|
@ -404,11 +374,7 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
|
|||
|
||||
for s in stroke_list:
|
||||
add_stroke(s, target_frame, layer, obj, select=select)
|
||||
'''
|
||||
for s in stroke_data:
|
||||
add_stroke(s, target_frame)
|
||||
'''
|
||||
|
||||
|
||||
# print(len(stroke_list), 'strokes pasted')
|
||||
|
||||
|
||||
|
@ -423,15 +389,15 @@ class GPCLIP_OT_copy_strokes(bpy.types.Operator):
|
|||
#copy = bpy.props.BoolProperty(default=True)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
t0 = time()
|
||||
#ct = check_pressure()
|
||||
#ct = check_radius()
|
||||
strokelist = copycut_strokes(copy=True, keep_empty=True)
|
||||
if not strokelist:
|
||||
self.report({'ERROR'}, 'Nothing to copy')
|
||||
|
@ -448,19 +414,19 @@ class GPCLIP_OT_cut_strokes(bpy.types.Operator):
|
|||
bl_idname = "gp.cut_strokes"
|
||||
bl_label = "GP Cut strokes"
|
||||
bl_description = "Cut strokes to text in paperclip"
|
||||
bl_options = {"REGISTER"}
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
t0 = time()
|
||||
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_pressure()
|
||||
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_radius()
|
||||
if not strokelist:
|
||||
self.report({'ERROR'},'Nothing to cut')
|
||||
return {"CANCELLED"}
|
||||
|
@ -477,10 +443,10 @@ class GPCLIP_OT_paste_strokes(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# if not context.object or not context.object.type == 'GPENCIL':
|
||||
# if not context.object or not context.object.type == 'GREASEPENCIL':
|
||||
# self.report({'ERROR'},'No GP object selected to paste on')
|
||||
# return {"CANCELLED"}
|
||||
|
||||
|
@ -510,20 +476,22 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
#copy = bpy.props.BoolProperty(default=True)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
pressure : bpy.props.BoolProperty(name='pressure', default=True,
|
||||
description='Dump point pressure attribute (already skipped if at default value)')
|
||||
strength : bpy.props.BoolProperty(name='strength', default=True,
|
||||
description='Dump point strength attribute (already skipped if at default value)')
|
||||
radius : bpy.props.BoolProperty(name='radius', default=True,
|
||||
description='Dump point radius attribute (already skipped if at default value)')
|
||||
opacity : bpy.props.BoolProperty(name='opacity', default=True,
|
||||
description='Dump point opacity attribute (already skipped if at default value)')
|
||||
vertex_color : bpy.props.BoolProperty(name='vertex color', default=True,
|
||||
description='Dump point vertex_color attribute (already skipped if at default value)')
|
||||
uv_fill : bpy.props.BoolProperty(name='uv fill', default=True,
|
||||
description='Dump point uv_fill attribute (already skipped if at default value)')
|
||||
fill_color : bpy.props.BoolProperty(name='fill color', default=True,
|
||||
description='Dump point fill_color attribute (already skipped if at default value)')
|
||||
fill_opacity : bpy.props.BoolProperty(name='fill opacity', default=True,
|
||||
description='Dump point fill_opacity attribute (already skipped if at default value)')
|
||||
uv_factor : bpy.props.BoolProperty(name='uv factor', default=True,
|
||||
description='Dump point uv_factor attribute (already skipped if at default value)')
|
||||
uv_rotation : bpy.props.BoolProperty(name='uv rotation', default=True,
|
||||
description='Dump point uv_rotation attribute (already skipped if at default value)')
|
||||
rotation : bpy.props.BoolProperty(name='rotation', default=True,
|
||||
description='Dump point rotation attribute (already skipped if at default value)')
|
||||
|
||||
def invoke(self, context, event):
|
||||
# self.file_dump = event.ctrl
|
||||
|
@ -535,12 +503,12 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
layout.use_property_split = True
|
||||
col = layout.column()
|
||||
col.label(text='Keep following point attributes:')
|
||||
col.prop(self, 'pressure')
|
||||
col.prop(self, 'strength')
|
||||
col.prop(self, 'radius')
|
||||
col.prop(self, 'opacity')
|
||||
col.prop(self, 'vertex_color')
|
||||
col.prop(self, 'uv_fill')
|
||||
col.prop(self, 'uv_factor')
|
||||
col.prop(self, 'uv_rotation')
|
||||
col.prop(self, 'fill_color')
|
||||
col.prop(self, 'fill_opacity')
|
||||
col.prop(self, 'rotation')
|
||||
return
|
||||
|
||||
def execute(self, context):
|
||||
|
@ -551,10 +519,10 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
obj = context.object
|
||||
gpl = obj.data.layers
|
||||
t0 = time()
|
||||
#ct = check_pressure()
|
||||
#ct = check_radius()
|
||||
layerdic = {}
|
||||
|
||||
layerpool = [l for l in gpl if not l.hide and l.select] # and not l.lock
|
||||
layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l)
|
||||
if not layerpool:
|
||||
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
|
||||
return {"CANCELLED"}
|
||||
|
@ -566,20 +534,20 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
|
||||
frame_dic = {}
|
||||
for f in l.frames:
|
||||
if skip_empty_frame and not len(f.strokes):
|
||||
if skip_empty_frame and not len(f.drawing.strokes):
|
||||
continue
|
||||
context.scene.frame_set(f.frame_number) # use matrix of this frame
|
||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
|
||||
pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color,
|
||||
uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation)
|
||||
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
||||
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
||||
|
||||
frame_dic[f.frame_number] = strokelist
|
||||
|
||||
layerdic[l.info] = frame_dic
|
||||
layerdic[l.name] = frame_dic
|
||||
|
||||
else: # bake position: copy frame where object as moved even if frame is unchanged
|
||||
for l in layerpool:
|
||||
print('dump layer:', l.info)
|
||||
print('dump layer:', l.name)
|
||||
if not l.frames:
|
||||
continue# skip empty layers
|
||||
|
||||
|
@ -603,17 +571,17 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
|||
break
|
||||
|
||||
## skip empty frame if specified
|
||||
if skip_empty_frame and not len(f.strokes):
|
||||
if skip_empty_frame and not len(f.drawing.strokes):
|
||||
continue
|
||||
|
||||
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
|
||||
pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color,
|
||||
uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation)
|
||||
radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
|
||||
fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
|
||||
|
||||
frame_dic[i] = strokelist
|
||||
|
||||
prevmat = curmat
|
||||
layerdic[l.info] = frame_dic
|
||||
layerdic[l.name] = frame_dic
|
||||
|
||||
## All to clipboard manager
|
||||
bpy.context.window_manager.clipboard = json.dumps(layerdic)
|
||||
|
@ -633,14 +601,14 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
|
|||
#copy = bpy.props.BoolProperty(default=True)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
org_frame = context.scene.frame_current
|
||||
obj = context.object
|
||||
gpl = obj.data.layers
|
||||
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:
|
||||
data = json.loads(bpy.context.window_manager.clipboard)
|
||||
except:
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
import bpy
|
||||
import mathutils
|
||||
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
|
||||
|
||||
## override all sursor snap shortcut with this in keymap
|
||||
|
@ -13,7 +15,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
|||
|
||||
# @classmethod
|
||||
# def poll(cls, context):
|
||||
# return context.object and context.object.type == 'GPENCIL'
|
||||
# return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
#print('-!SNAP!-')
|
||||
|
@ -23,7 +25,7 @@ class GPTB_OT_cusor_snap(bpy.types.Operator):
|
|||
return {"FINISHED"}
|
||||
|
||||
def execute(self, context):
|
||||
if not context.object or context.object.type != 'GPENCIL':
|
||||
if not context.object or context.object.type != 'GREASEPENCIL':
|
||||
self.report({'INFO'}, 'Not GP, Cursor surface project')
|
||||
bpy.ops.view3d.cursor3d('INVOKE_DEFAULT', use_depth=True, orientation='NONE')#'NONE', 'VIEW', 'XFORM', 'GEOM'
|
||||
return {"FINISHED"}
|
||||
|
@ -105,20 +107,24 @@ def swap_keymap_by_id(org_idname, new_idname):
|
|||
k.idname = new_idname
|
||||
|
||||
|
||||
# prev_matrix = mathutils.Matrix()
|
||||
prev_matrix = None
|
||||
|
||||
# @call_once(bpy.app.handlers.frame_change_post)
|
||||
|
||||
def cursor_follow_update(self,context):
|
||||
## used in properties file to register in boolprop update
|
||||
def cursor_follow_update(self, context):
|
||||
'''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
|
||||
# imported in properties to register in boolprop update
|
||||
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 context.object:
|
||||
prev_matrix = context.object.matrix_world
|
||||
|
||||
bpy.app.handlers.frame_change_post.append(cursor_follow)
|
||||
|
||||
else:#False
|
||||
|
@ -129,11 +135,13 @@ def cursor_follow_update(self,context):
|
|||
|
||||
def cursor_follow(scene):
|
||||
'''Handler to make the cursor follow active object matrix changes on frame change'''
|
||||
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)...
|
||||
if not bpy.context.object:
|
||||
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
|
||||
if not ob:
|
||||
return
|
||||
global prev_matrix
|
||||
ob = bpy.context.object
|
||||
current_matrix = ob.matrix_world
|
||||
if not prev_matrix:
|
||||
prev_matrix = current_matrix.copy()
|
||||
|
@ -146,15 +154,44 @@ def cursor_follow(scene):
|
|||
|
||||
## translation only
|
||||
# scene.cursor.location += (current_matrix - prev_matrix).to_translation()
|
||||
|
||||
# print('offset:', (current_matrix - prev_matrix).to_translation())
|
||||
|
||||
|
||||
## full
|
||||
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
|
||||
|
||||
# store for next use
|
||||
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 = (
|
||||
GPTB_OT_cusor_snap,
|
||||
|
@ -163,14 +200,18 @@ GPTB_OT_cusor_snap,
|
|||
def register():
|
||||
for cls in classes:
|
||||
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
|
||||
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
|
||||
|
||||
# bpy.app.handlers.frame_change_post.append(cursor_follow)
|
||||
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
|
||||
|
||||
|
||||
def unregister():
|
||||
# bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
||||
bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
|
||||
|
||||
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
|
||||
|
||||
|
@ -179,4 +220,6 @@ def 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]:
|
||||
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)
|
|
@ -11,6 +11,7 @@ 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
|
||||
from time import time
|
||||
from math import pi, cos, sin
|
||||
from .utils import is_locked, is_hidden
|
||||
|
||||
|
||||
def get_gp_mat(gp, name, set_active=False):
|
||||
|
@ -231,7 +232,7 @@ class GPTB_OT_eraser(Operator):
|
|||
|
||||
hld_stroke.points.add(count=1)
|
||||
p = hld_stroke.points[-1]
|
||||
p.co = mat_inv @ mouse_3d
|
||||
p.position = mat_inv @ mouse_3d
|
||||
p.pressure = search_radius * 2000
|
||||
|
||||
#context.scene.cursor.location = mouse_3d
|
||||
|
@ -252,14 +253,14 @@ class GPTB_OT_eraser(Operator):
|
|||
#print(self.cuts_data)
|
||||
|
||||
# for f in self.gp_frames:
|
||||
# for s in [s for s in f.strokes if s.material_index==self.hld_index]:
|
||||
# f.strokes.remove(s)
|
||||
# for s in [s for s in f.drawing.strokes if s.material_index==self.hld_index]:
|
||||
# f.drawing.strokes.remove(s)
|
||||
|
||||
#gp.data.materials.pop(index=self.hld_index)
|
||||
#bpy.data.materials.remove(self.hld_mat)
|
||||
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||
#context.scene.tool_settings.gpencil_selectmode_edit = 'POINT'
|
||||
|
||||
|
@ -281,7 +282,7 @@ class GPTB_OT_eraser(Operator):
|
|||
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||
|
||||
strokes = [s for f in self.gp_frames for s in f.strokes]
|
||||
strokes = [s for f in self.gp_frames for s in f.drawing.strokes]
|
||||
#print('select_circle', time()-t1)
|
||||
|
||||
t2 = time()
|
||||
|
@ -309,18 +310,18 @@ class GPTB_OT_eraser(Operator):
|
|||
bpy.ops.gpencil.stroke_subdivide(number_cuts=number_cuts, only_selected=True)
|
||||
|
||||
new_p1 = stroke.points[p1_index+1]
|
||||
new_p1.co = mat_inv@intersects[0]
|
||||
new_p1.position = mat_inv@intersects[0]
|
||||
new_points += [(stroke, p1_index+1)]
|
||||
|
||||
#print('number_cuts', number_cuts)
|
||||
|
||||
if number_cuts == 2:
|
||||
new_p2 = stroke.points[p1_index+2]
|
||||
new_p2.co = mat_inv@( (intersects[0] + intersects[1])/2 )
|
||||
new_p2.position = mat_inv@( (intersects[0] + intersects[1])/2 )
|
||||
#new_points += [new_p2]
|
||||
|
||||
new_p3 = stroke.points[p1_index+3]
|
||||
new_p3.co = mat_inv@intersects[1]
|
||||
new_p3.position = mat_inv@intersects[1]
|
||||
new_points += [(stroke, p1_index+3)]
|
||||
|
||||
#print('subdivide', time() - t3)
|
||||
|
@ -329,7 +330,7 @@ class GPTB_OT_eraser(Operator):
|
|||
bpy.ops.gpencil.select_circle(x=x, y=y, radius=radius, wait_for_input=False)
|
||||
|
||||
'''
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
|
||||
tip_points = [p for s in selected_strokes for i, p in enumerate(s.points) if p.select and (i==0 or i == len(s.points)-1)]
|
||||
|
||||
bpy.ops.gpencil.select_less()
|
||||
|
@ -341,7 +342,7 @@ class GPTB_OT_eraser(Operator):
|
|||
'''
|
||||
|
||||
t4 = time()
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.strokes if s.select]
|
||||
selected_strokes = [s for f in self.gp_frames for s in f.drawing.strokes if s.select]
|
||||
|
||||
if selected_strokes:
|
||||
bpy.ops.gpencil.delete(type='POINTS')
|
||||
|
@ -358,9 +359,9 @@ class GPTB_OT_eraser(Operator):
|
|||
|
||||
#bpy.ops.object.mode_set(mode='OBJECT')
|
||||
context.scene.tool_settings.gpencil_selectmode_edit = self.gpencil_selectmode_edit
|
||||
bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
|
||||
#selected_strokes = [s for s in self.gp_frame.strokes if s.select]
|
||||
#bpy.ops.object.mode_set(mode='PAINT_GPENCIL')
|
||||
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||
#selected_strokes = [s for s in self.gp_frame.drawing.strokes if s.select]
|
||||
#bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
|
||||
|
||||
def modal(self, context, event):
|
||||
self.mouse = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
|
@ -440,23 +441,23 @@ class GPTB_OT_eraser(Operator):
|
|||
|
||||
t0 = time()
|
||||
gp_mats = gp.data.materials
|
||||
gp_layers = [l for l in gp.data.layers if not l.lock or l.hide]
|
||||
self.gp_frames = [l.active_frame for l in gp_layers]
|
||||
gp_layers = [l for l in gp.data.layers if not is_locked(l) or is_hidden(l)]
|
||||
self.gp_frames = [l.current_frame() for l in gp_layers]
|
||||
'''
|
||||
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.strokes]
|
||||
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes]
|
||||
points_data = [(s, f, m) for s, f, m in points_data if not m.grease_pencil.hide or m.grease_pencil.lock]
|
||||
print('get_gp_points', time()-t0)
|
||||
|
||||
t0 = time()
|
||||
#points_data = [(s, f, m, p, get_screen_co(p.co, matrix)) for s, f, m in points_data for p in reversed(s.points)]
|
||||
points_data = [(s, f, m, p, org + ((matrix @ p.co)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
|
||||
#points_data = [(s, f, m, p, get_screen_co(p.position, matrix)) for s, f, m in points_data for p in reversed(s.points)]
|
||||
points_data = [(s, f, m, p, org + ((matrix @ p.position)-org).normalized()*1) for s, f, m in points_data for p in reversed(s.points)]
|
||||
print('points_to_2d', time()-t0)
|
||||
|
||||
#print(points_data)
|
||||
self.points_data = [(s, f, m, p, co) for s, f, m, p, co in points_data if co is not None]
|
||||
|
||||
#for s, f, m, p, co in self.points_data:
|
||||
# p.co = co
|
||||
# p.position = co
|
||||
|
||||
|
||||
t0 = time()
|
||||
|
@ -481,7 +482,7 @@ class GPTB_OT_eraser(Operator):
|
|||
|
||||
self.hld_strokes = []
|
||||
for f in self.gp_frames:
|
||||
hld_stroke = f.strokes.new()
|
||||
hld_stroke = f.drawing.strokes.new()
|
||||
hld_stroke.start_cap_mode = 'ROUND'
|
||||
hld_stroke.end_cap_mode = 'ROUND'
|
||||
hld_stroke.material_index = self.hld_index
|
||||
|
|
|
@ -2,8 +2,14 @@ import bpy
|
|||
import os
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
from . import utils
|
||||
|
||||
from bpy.props import (BoolProperty,
|
||||
PointerProperty,
|
||||
CollectionProperty,
|
||||
StringProperty)
|
||||
|
||||
def remove_stroke_exact_duplications(apply=True):
|
||||
'''Remove accidental stroke duplication (points exactly in the same place)
|
||||
:apply: Remove the duplication instead of just listing dupes
|
||||
|
@ -16,15 +22,15 @@ def remove_stroke_exact_duplications(apply=True):
|
|||
for l in gp.layers:
|
||||
for f in l.frames:
|
||||
stroke_list = []
|
||||
for s in reversed(f.strokes):
|
||||
for s in reversed(f.drawing.strokes):
|
||||
|
||||
point_list = [p.co for p in s.points]
|
||||
point_list = [p.position for p in s.points]
|
||||
|
||||
if point_list in stroke_list:
|
||||
ct += 1
|
||||
if apply:
|
||||
# Remove redundancy
|
||||
f.strokes.remove(s)
|
||||
f.drawing.strokes.remove(s)
|
||||
else:
|
||||
stroke_list.append(point_list)
|
||||
return ct
|
||||
|
@ -53,6 +59,10 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
# Disable use light on all object
|
||||
# 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):
|
||||
# need some self-control (I had to...)
|
||||
self.ctrl = event.ctrl
|
||||
|
@ -63,10 +73,13 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
fix = prefs.fixprops
|
||||
problems = []
|
||||
|
||||
apply = not fix.check_only
|
||||
## Old method : Apply fixes based on pref (inverted by ctrl key)
|
||||
# # If Ctrl is pressed, invert behavior (invert boolean)
|
||||
# apply ^= self.ctrl
|
||||
|
||||
# If Ctrl is pressed, invert behavior (invert boolean)
|
||||
apply ^= self.ctrl
|
||||
apply = self.apply_fixes
|
||||
if self.ctrl:
|
||||
apply = True
|
||||
|
||||
## Lock main cam:
|
||||
if fix.lock_main_cam:
|
||||
|
@ -120,7 +133,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
setattr(area.spaces[0], 'show_locked_time', True)
|
||||
|
||||
## set cursor type
|
||||
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
|
||||
if context.mode in ("EDIT_GREASE_PENCIL", "SCULPT_GREASE_PENCIL"):
|
||||
tool = fix.select_active_tool
|
||||
if tool != 'none':
|
||||
if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool:
|
||||
|
@ -145,7 +158,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
|
||||
## GP Use light disable
|
||||
if fix.set_gp_use_lights_off:
|
||||
gp_with_lights = [o for o in context.scene.objects if o.type == 'GPENCIL' and o.use_grease_pencil_lights]
|
||||
gp_with_lights = [o for o in context.scene.objects if o.type == 'GREASEPENCIL' and o.use_grease_pencil_lights]
|
||||
if gp_with_lights:
|
||||
problems.append(f'Disable "Use Lights" on {len(gp_with_lights)} Gpencil objects')
|
||||
if apply:
|
||||
|
@ -169,45 +182,38 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
if fix.list_obj_vis_conflict:
|
||||
viz_ct = 0
|
||||
for o in context.scene.objects:
|
||||
if o.hide_viewport != o.hide_render:
|
||||
if not (o.hide_get() == o.hide_viewport == o.hide_render):
|
||||
hv = 'No' if o.hide_get() else 'Yes'
|
||||
vp = 'No' if o.hide_viewport else 'Yes'
|
||||
rd = 'No' if o.hide_render else 'Yes'
|
||||
viz_ct += 1
|
||||
print(f'{o.name} : viewport {vp} != render {rd}')
|
||||
print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
|
||||
if viz_ct:
|
||||
problems.append(['gp.list_object_visibility', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
|
||||
problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
|
||||
|
||||
## GP modifiers visibility conflict
|
||||
if fix.list_gp_mod_vis_conflict:
|
||||
mod_viz_ct = 0
|
||||
for o in context.scene.objects:
|
||||
if o.type == 'GPENCIL':
|
||||
for m in o.grease_pencil_modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
vp = 'Yes' if m.show_viewport else 'No'
|
||||
rd = 'Yes' if m.show_render else 'No'
|
||||
mod_viz_ct += 1
|
||||
print(f'{o.name} - GP modifier {m.name}: viewport {vp} != render {rd}')
|
||||
else:
|
||||
for m in o.modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
vp = 'Yes' if m.show_viewport else 'No'
|
||||
rd = 'Yes' if m.show_render else 'No'
|
||||
mod_viz_ct += 1
|
||||
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
|
||||
for m in o.modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
vp = 'Yes' if m.show_viewport else 'No'
|
||||
rd = 'Yes' if m.show_render else 'No'
|
||||
mod_viz_ct += 1
|
||||
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
|
||||
|
||||
if mod_viz_ct:
|
||||
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA'])
|
||||
|
||||
## check if GP modifier have broken layer targets
|
||||
if fix.list_broken_mod_targets:
|
||||
for o in [o for o in bpy.context.scene.objects if o.type == 'GPENCIL']:
|
||||
lay_name_list = [l.info for l in o.data.layers]
|
||||
for m in o.grease_pencil_modifiers:
|
||||
if not hasattr(m, 'layer'):
|
||||
for o in [o for o in bpy.context.scene.objects if o.type == 'GREASEPENCIL']:
|
||||
lay_name_list = [l.name for l in o.data.layers]
|
||||
for m in o.modifiers:
|
||||
if not hasattr(m, 'layer_filter'):
|
||||
continue
|
||||
if m.layer != '' and not m.layer in lay_name_list:
|
||||
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer}'
|
||||
if m.layer_filter != '' and not m.layer_filter in lay_name_list:
|
||||
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer_filter}'
|
||||
print(mess)
|
||||
problems.append(mess)
|
||||
|
||||
|
@ -277,7 +283,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
|
||||
|
||||
# for ob in context.scene.objects:#from object
|
||||
# if ob.type == 'GPENCIL':
|
||||
# if ob.type == 'GREASEPENCIL':
|
||||
# ob.data.onion_keyframe_type = 'ALL'
|
||||
|
||||
#### --- print fix/problems report
|
||||
|
@ -288,9 +294,13 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
|||
print(p)
|
||||
else:
|
||||
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
|
||||
title = "Changed Settings" if apply else "Checked Settings (dry run, nothing changed)"
|
||||
title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
|
||||
utils.show_message_box(problems, _title = title, _icon = 'INFO')
|
||||
else:
|
||||
self.report({'INFO'}, 'All good')
|
||||
|
@ -490,48 +500,13 @@ class GPTB_OT_links_checker(bpy.types.Operator):
|
|||
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()
|
||||
|
||||
all_lnks.sort(key=lambda x: x[1], reverse=True)
|
||||
if all_lnks:
|
||||
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"
|
||||
class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
|
||||
bl_idname = "gp.list_viewport_render_visibility"
|
||||
bl_label = "List Viewport And Render Visibility Conflicts"
|
||||
bl_description = "List objects visibility conflicts, when viewport and render have different values"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
|
||||
def invoke(self, context, event):
|
||||
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)
|
||||
|
@ -547,60 +522,133 @@ class GPTB_OT_list_object_visibility(bpy.types.Operator):
|
|||
|
||||
def execute(self, context):
|
||||
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"}
|
||||
|
||||
## Only GP modifier
|
||||
'''
|
||||
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
|
||||
bl_idname = "gp.list_modifier_visibility"
|
||||
bl_label = "List GP Modifiers Visibility Conflicts"
|
||||
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
|
||||
def execute(self, context):
|
||||
for obj in context.scene.objects:
|
||||
is_hidden = obj.hide_get() # Get viewlayer visibility
|
||||
obj.hide_viewport = is_hidden
|
||||
obj.hide_render = is_hidden
|
||||
return {'FINISHED'}
|
||||
|
||||
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"}
|
||||
|
||||
visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type]
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.ob_list = []
|
||||
for o in context.scene.objects:
|
||||
if o.type != 'GPENCIL':
|
||||
continue
|
||||
if not len(o.grease_pencil_modifiers):
|
||||
continue
|
||||
|
||||
mods = []
|
||||
for m in o.grease_pencil_modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
if not mods:
|
||||
self.ob_list.append([o, mods])
|
||||
mods.append(m)
|
||||
# Clear and rebuild both collections
|
||||
self.visibility_items.clear()
|
||||
|
||||
# Store objects with conflicts
|
||||
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ?
|
||||
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
|
||||
|
||||
# Create visibility items in same order
|
||||
for obj in objects_with_conflicts:
|
||||
item = self.visibility_items.add()
|
||||
item.object_name = obj.name
|
||||
item["is_hidden"] = obj.hide_get()
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for o in self.ob_list:
|
||||
layout.label(text=o[0].name, icon='OUTLINER_OB_GREASEPENCIL')
|
||||
for m in o[1]:
|
||||
row = layout.row()
|
||||
row.label(text='')
|
||||
row.label(text=m.name, icon='MODIFIER_ON')
|
||||
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
|
||||
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
|
||||
|
||||
# Add sync buttons at the top
|
||||
row = layout.row(align=False)
|
||||
row.label(text="Sync All Visibility From:")
|
||||
row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF')
|
||||
row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF')
|
||||
row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF')
|
||||
layout.separator()
|
||||
|
||||
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):
|
||||
return {'FINISHED'}
|
||||
'''
|
||||
|
||||
## not exposed in UI, Check is performed in Check file (can be called in popped menu)
|
||||
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
|
||||
|
@ -612,24 +660,14 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
|
|||
def invoke(self, context, event):
|
||||
self.ob_list = []
|
||||
for o in context.scene.objects:
|
||||
if o.type == 'GPENCIL':
|
||||
if not len(o.grease_pencil_modifiers):
|
||||
continue
|
||||
mods = []
|
||||
for m in o.grease_pencil_modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
if not mods:
|
||||
self.ob_list.append([o, mods, 'OUTLINER_OB_GREASEPENCIL'])
|
||||
mods.append(m)
|
||||
else:
|
||||
if not len(o.modifiers):
|
||||
continue
|
||||
mods = []
|
||||
for m in o.modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
if not mods:
|
||||
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
|
||||
mods.append(m)
|
||||
if not len(o.modifiers):
|
||||
continue
|
||||
mods = []
|
||||
for m in o.modifiers:
|
||||
if m.show_viewport != m.show_render:
|
||||
if not mods:
|
||||
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
|
||||
mods.append(m)
|
||||
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
|
||||
return context.window_manager.invoke_props_dialog(self, width=250)
|
||||
|
||||
|
@ -652,7 +690,13 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
|
|||
return {'FINISHED'}
|
||||
|
||||
classes = (
|
||||
GPTB_OT_list_object_visibility,
|
||||
GPTB_OT_list_viewport_render_visibility, # Only viewport and render
|
||||
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_copy_string_to_clipboard,
|
||||
GPTB_OT_copy_multipath_clipboard,
|
||||
|
|
|
@ -7,19 +7,19 @@ import numpy as np
|
|||
from time import time
|
||||
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
|
||||
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
|
||||
'''Reproject
|
||||
:all_stroke: affect hided, locked layers
|
||||
:all_stroke: affect hidden, locked layers
|
||||
'''
|
||||
|
||||
if restore_frame:
|
||||
oframe = bpy.context.scene.frame_current
|
||||
omode = bpy.context.mode
|
||||
|
||||
# frame_list = [ f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)]
|
||||
# frame_list = [ f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
|
||||
# frame_list = list(set(frame_list))
|
||||
# frame_list.sort()
|
||||
# for fnum in frame_list:
|
||||
|
@ -32,15 +32,15 @@ def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=
|
|||
fnum = len(l.frames)
|
||||
zf = len(str(fnum))
|
||||
for j, f in enumerate(reversed(l.frames)): # whynot...
|
||||
print(f'{obj.name} : {i+1}/{laynum} : {l.info} : {str(j+1).zfill(zf)}/{fnum}{" "*30}', end='\r')
|
||||
print(f'{obj.name} : {i+1}/{laynum} : {l.name} : {str(j+1).zfill(zf)}/{fnum}{" "*30}', end='\r')
|
||||
scn.frame_set(f.frame_number) # more chance to update the matrix
|
||||
bpy.context.view_layer.update() # update the matrix ?
|
||||
bpy.context.scene.camera.location = bpy.context.scene.camera.location
|
||||
scn.frame_current = f.frame_number
|
||||
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
for p in s.points:
|
||||
p.co = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.co), scn.cursor.location)
|
||||
p.position = obj.matrix_world.inverted() @ region_to_location(location_to_region(obj.matrix_world @ p.position), scn.cursor.location)
|
||||
|
||||
if restore_frame:
|
||||
bpy.context.scene.frame_current = oframe
|
||||
|
@ -67,8 +67,8 @@ def batch_flat_reproject(obj):
|
|||
plane_no.rotate(cam_mat)
|
||||
plane_co = scn.cursor.location
|
||||
|
||||
for s in f.strokes:
|
||||
points_co = [obj.matrix_world @ p.co for p in s.points]
|
||||
for s in f.drawing.strokes:
|
||||
points_co = [obj.matrix_world @ p.position for p in s.points]
|
||||
points_co = [mat_inv @ intersect_line_plane(origin, p, plane_co, plane_no) for p in points_co]
|
||||
points_co = [co for vector in points_co for co in vector]
|
||||
|
||||
|
@ -76,8 +76,8 @@ def batch_flat_reproject(obj):
|
|||
s.points.add(1) # update
|
||||
s.points.pop() # update
|
||||
#for p in s.points:
|
||||
# loc_2d = location_to_region(obj.matrix_world @ p.co)
|
||||
# p.co = obj.matrix_world.inverted() @ region_to_location(loc_2d, scn.cursor.location)
|
||||
# loc_2d = location_to_region(obj.matrix_world @ p.position)
|
||||
# p.position = obj.matrix_world.inverted() @ region_to_location(loc_2d, scn.cursor.location)
|
||||
"""
|
||||
|
||||
def batch_flat_reproject(obj):
|
||||
|
@ -96,14 +96,14 @@ def batch_flat_reproject(obj):
|
|||
plane_co = scn.cursor.location
|
||||
|
||||
for l in obj.data.layers:
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
if not f: # No active frame
|
||||
continue
|
||||
|
||||
if f.frame_number != scn.frame_current:
|
||||
f = l.frames.copy(f) # duplicate content of the previous frame
|
||||
for s in f.strokes:
|
||||
points_co = [obj.matrix_world @ p.co for p in s.points]
|
||||
for s in f.drawing.strokes:
|
||||
points_co = [obj.matrix_world @ p.position for p in s.points]
|
||||
points_co = [mat_inv @ intersect_line_plane(origin, p, plane_co, plane_no) for p in points_co]
|
||||
points_co = [co for vector in points_co for co in vector]
|
||||
|
||||
|
@ -119,11 +119,11 @@ class GPTB_OT_batch_flat_reproject(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
for o in context.selected_objects:
|
||||
if o.type != 'GPENCIL' or not o.select_get():
|
||||
if o.type != 'GREASEPENCIL' or not o.select_get():
|
||||
continue
|
||||
batch_flat_reproject(o)
|
||||
|
||||
|
@ -132,12 +132,12 @@ class GPTB_OT_batch_flat_reproject(bpy.types.Operator):
|
|||
### -- MENU ENTRY --
|
||||
|
||||
def flat_reproject_clean_menu(self, context):
|
||||
if context.mode == 'EDIT_GPENCIL':
|
||||
if context.mode == 'EDIT_GREASE_PENCIL':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
|
||||
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
|
||||
|
||||
def flat_reproject_context_menu(self, context):
|
||||
if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
|
||||
self.layout.operator('gp.batch_flat_reproject', icon='KEYTYPE_JITTER_VEC')
|
||||
|
||||
|
@ -149,12 +149,12 @@ def register():
|
|||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
# bpy.types.VIEW3D_MT_gpencil_edit_context_menu.append(flat_reproject_context_menu)
|
||||
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.append(flat_reproject_context_menu)
|
||||
# bpy.types.GPENCIL_MT_cleanup.append(flat_reproject_clean_menu)
|
||||
|
||||
def unregister():
|
||||
# bpy.types.GPENCIL_MT_cleanup.remove(flat_reproject_clean_menu)
|
||||
# bpy.types.VIEW3D_MT_gpencil_edit_context_menu.remove(flat_reproject_context_menu)
|
||||
# bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.remove(flat_reproject_context_menu)
|
||||
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
|
@ -2,7 +2,6 @@ import bpy
|
|||
from mathutils import Vector
|
||||
from . import utils
|
||||
|
||||
|
||||
class GPTB_OT_create_follow_path_curve(bpy.types.Operator):
|
||||
bl_idname = "object.create_follow_path_curve"
|
||||
bl_label = "Create Follow Path Curve"
|
||||
|
@ -135,8 +134,8 @@ class GPTB_OT_go_to_object(bpy.types.Operator):
|
|||
bpy.ops.object.mode_set(mode='POSE', toggle=False)
|
||||
self.report({'INFO'}, f'Back to pose mode, {obj.name}')
|
||||
|
||||
elif obj.type == 'GPENCIL':
|
||||
bpy.ops.object.mode_set(mode='PAINT_GPENCIL', toggle=False)
|
||||
elif obj.type == 'GREASEPENCIL':
|
||||
bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL', toggle=False)
|
||||
|
||||
else:
|
||||
self.report({'INFO'}, f'Back to object mode, {obj.name}')
|
||||
|
|
|
@ -79,7 +79,7 @@ class GPTB_OT_rename_data_from_obj(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
if not self.rename_all:
|
||||
|
@ -93,7 +93,7 @@ class GPTB_OT_rename_data_from_obj(Operator):
|
|||
else:
|
||||
oblist = []
|
||||
for o in context.scene.objects:
|
||||
if o.type == 'GPENCIL':
|
||||
if o.type == 'GREASEPENCIL':
|
||||
if o.name == o.data.name:
|
||||
continue
|
||||
oblist.append(f'{o.data.name} -> {o.name}')
|
||||
|
@ -250,7 +250,7 @@ class GPTB_OT_draw_cam(Operator):
|
|||
drawcam.parent = act
|
||||
vec = Vector((0,1,0))
|
||||
|
||||
if act.type == 'GPENCIL':
|
||||
if act.type == 'GREASEPENCIL':
|
||||
#change vector according to alignement
|
||||
vec = get_gp_alignement_vector(context)
|
||||
|
||||
|
@ -427,15 +427,15 @@ class GPTB_OT_toggle_mute_animation(Operator):
|
|||
pool = context.scene.objects
|
||||
|
||||
for o in pool:
|
||||
if self.mode == 'GPENCIL' and o.type != 'GPENCIL':
|
||||
if self.mode == 'GREASEPENCIL' and o.type != 'GREASEPENCIL':
|
||||
continue
|
||||
if self.mode == 'OBJECT' and o.type in ('GPENCIL', 'CAMERA'):
|
||||
if self.mode == 'OBJECT' and o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||
continue
|
||||
if self.mode == 'CAMERA' and o.type != 'CAMERA':
|
||||
continue
|
||||
|
||||
# mute attribute animation for GP and cameras
|
||||
if o.type in ('GPENCIL', 'CAMERA') and o.data.animation_data:
|
||||
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
|
||||
gp_act = o.data.animation_data.action
|
||||
if gp_act:
|
||||
print(f'\n---{o.name} data:')
|
||||
|
@ -473,9 +473,9 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
|
|||
else:
|
||||
pool = context.scene.objects
|
||||
for o in pool:
|
||||
if o.type != 'GPENCIL':
|
||||
if o.type != 'GREASEPENCIL':
|
||||
continue
|
||||
for m in o.grease_pencil_modifiers:
|
||||
for m in o.modifiers:
|
||||
# skip modifier that are not visible in render
|
||||
if not m.show_render:
|
||||
continue
|
||||
|
@ -506,12 +506,12 @@ class GPTB_OT_list_disabled_anims(Operator):
|
|||
pool = context.scene.objects
|
||||
|
||||
for o in pool:
|
||||
# if self.skip_gp and o.type == 'GPENCIL':
|
||||
# if self.skip_gp and o.type == 'GREASEPENCIL':
|
||||
# continue
|
||||
# if self.skip_obj and o.type != 'GPENCIL':
|
||||
# if self.skip_obj and o.type != 'GREASEPENCIL':
|
||||
# continue
|
||||
|
||||
if o.type == 'GPENCIL':
|
||||
if o.type == 'GREASEPENCIL':
|
||||
if o.data.animation_data:
|
||||
gp_act = o.data.animation_data.action
|
||||
if gp_act:
|
||||
|
@ -611,7 +611,7 @@ class GPTB_OT_clear_active_frame(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
@ -619,18 +619,18 @@ class GPTB_OT_clear_active_frame(Operator):
|
|||
if not l:
|
||||
self.report({'ERROR'}, 'No layers')
|
||||
return {'CANCELLED'}
|
||||
f = l.active_frame
|
||||
f = l.current_frame()
|
||||
if not f:
|
||||
self.report({'ERROR'}, 'No active frame')
|
||||
return {'CANCELLED'}
|
||||
|
||||
ct = len(f.strokes)
|
||||
ct = len(f.drawing.strokes)
|
||||
if not ct:
|
||||
self.report({'ERROR'}, 'Active frame already empty')
|
||||
return {'CANCELLED'}
|
||||
|
||||
for s in reversed(f.strokes):
|
||||
f.strokes.remove(s)
|
||||
for s in reversed(f.drawing.strokes):
|
||||
f.drawing.strokes.remove(s)
|
||||
self.report({'INFO'}, f'Cleared active frame ({ct} strokes removed)')
|
||||
|
||||
return {'FINISHED'}
|
||||
|
@ -644,7 +644,7 @@ class GPTB_OT_check_canvas_alignement(Operator):
|
|||
@classmethod
|
||||
def poll(cls, context):
|
||||
# if lock_axis is 'VIEW' then the draw axis is always aligned
|
||||
return context.object and context.object.type == 'GPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'# and context.scene.tool_settings.gpencil_sculpt.lock_axis != 'VIEW'
|
||||
|
||||
def execute(self, context):
|
||||
if context.scene.tool_settings.gpencil_sculpt.lock_axis == 'VIEW':
|
||||
|
@ -673,7 +673,7 @@ class GPTB_OT_step_select_frames(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
start : bpy.props.IntProperty(name='Start Frame',
|
||||
description='Start frame of the step animation',
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import bpy
|
||||
from bpy.types import Operator
|
||||
from . import utils
|
||||
|
||||
|
||||
def get_layer_list(self, context):
|
||||
|
@ -8,7 +9,7 @@ def get_layer_list(self, context):
|
|||
return [('None', 'None','None')]
|
||||
if not context.object:
|
||||
return [('None', 'None','None')]
|
||||
return [(l.info, l.info, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
|
||||
# try:
|
||||
# except:
|
||||
# return [("", "", "")]
|
||||
|
@ -40,7 +41,7 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'\
|
||||
return context.object and context.object.type == 'GREASEPENCIL'\
|
||||
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)
|
||||
|
@ -65,25 +66,24 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
|
|||
|
||||
## Remove overlapping frames
|
||||
for f in reversed(to_replace):
|
||||
target_layer.frames.remove(f)
|
||||
|
||||
target_layer.frames.remove(f.frame_number)
|
||||
|
||||
## Copy original frames
|
||||
for f in selected_frames:
|
||||
target_layer.frames.copy(f)
|
||||
utils.copy_frame_at(f, target_layer, f.frame_number)
|
||||
# target_layer.frames.copy(f) # GPv2
|
||||
sent = len(selected_frames)
|
||||
|
||||
## Delete original frames as an option
|
||||
if self.delete_source:
|
||||
for f in reversed(selected_frames):
|
||||
act_layer.frames.remove(f)
|
||||
act_layer.frames.remove(f.frame_number)
|
||||
mess = f'{sent} keys moved'
|
||||
else:
|
||||
mess = f'{sent} keys copied'
|
||||
|
||||
mess = f'{sent} keys copied'
|
||||
if replaced:
|
||||
mess += f' ({replaced} replaced)'
|
||||
|
||||
mod = context.mode
|
||||
bpy.ops.gpencil.editmode_toggle()
|
||||
bpy.ops.object.mode_set(mode=mod)
|
||||
|
||||
self.report({'INFO'}, mess)
|
||||
return {'FINISHED'}
|
||||
|
@ -148,7 +148,7 @@ def register():
|
|||
bpy.utils.register_class(cls)
|
||||
|
||||
register_keymaps()
|
||||
bpy.types.DOPESHEET_MT_gpencil_key.append(menu_duplicate_and_send_to_layer)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -157,7 +157,7 @@ def unregister():
|
|||
return
|
||||
|
||||
bpy.types.DOPESHEET_MT_context_menu.remove(menu_duplicate_and_send_to_layer)
|
||||
bpy.types.DOPESHEET_MT_gpencil_key.remove(menu_duplicate_and_send_to_layer)
|
||||
bpy.types.DOPESHEET_MT_key.remove(menu_duplicate_and_send_to_layer)
|
||||
unregister_keymaps()
|
||||
|
||||
for cls in reversed(classes):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import bpy
|
||||
from .utils import get_addon_prefs
|
||||
from .utils import get_addon_prefs, is_locked, is_hidden
|
||||
from bpy.props import BoolProperty ,EnumProperty ,StringProperty
|
||||
|
||||
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||
|
@ -10,7 +10,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
next : BoolProperty(
|
||||
name="Next GP keyframe", description="Go to next active GP keyframe",
|
||||
|
@ -36,6 +36,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4),
|
||||
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5),
|
||||
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6),
|
||||
('GENERATED', 'Generated', '', 'KEYTYPE_GENERATED_VEC', 7),
|
||||
))
|
||||
|
||||
def execute(self, context):
|
||||
|
@ -44,15 +45,15 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
|||
return {"CANCELLED"}
|
||||
|
||||
if self.target == 'ACTIVE':
|
||||
gpl = [l for l in context.object.data.layers if l.select and not l.hide]
|
||||
gpl = [l for l in context.object.data.layers if l.select and not is_hidden(l)]
|
||||
if not context.object.data.layers.active in gpl:
|
||||
gpl.append(context.object.data.layers.active)
|
||||
|
||||
elif self.target == 'VISIBLE':
|
||||
gpl = [l for l in context.object.data.layers if not l.hide]
|
||||
gpl = [l for l in context.object.data.layers if not is_hidden(l)]
|
||||
|
||||
elif self.target == 'ACCESSIBLE':
|
||||
gpl = [l for l in context.object.data.layers if not l.hide and not l.lock]
|
||||
gpl = [l for l in context.object.data.layers if not is_hidden(l) and not is_locked(l)]
|
||||
|
||||
if self.keyframe_type != 'NONE':
|
||||
# use shortcut choice override
|
||||
|
|
|
@ -21,6 +21,7 @@ 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<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=''):
|
||||
'''GET a layer and argument to build and assign name
|
||||
|
@ -30,7 +31,7 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
|
|||
|
||||
prefs = get_addon_prefs()
|
||||
sep = prefs.separator
|
||||
name = old = layer.info
|
||||
name = old = layer.name
|
||||
|
||||
pattern = PATTERN.replace('_', sep) # set separator
|
||||
|
||||
|
@ -69,7 +70,7 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
|
|||
# check if name is available without the increment ending
|
||||
new = f'{grp}{tag}{name}{sfix}'
|
||||
|
||||
layer.info = new
|
||||
layer.name = new
|
||||
|
||||
## update name in modifier targets
|
||||
if old != new:
|
||||
|
@ -78,11 +79,11 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
|
|||
# maybe a more elegant way exists to find all objects users ?
|
||||
|
||||
# update Gpencil modifier targets
|
||||
for mod in ob_user.grease_pencil_modifiers:
|
||||
if not hasattr(mod, 'layer'):
|
||||
for mod in ob_user.modifiers:
|
||||
if not hasattr(mod, 'layer_filter'):
|
||||
continue
|
||||
if mod.layer == old:
|
||||
mod.layer = new
|
||||
if mod.layer_filter == old:
|
||||
mod.layer_filter = new
|
||||
|
||||
"""
|
||||
def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
|
||||
|
@ -93,7 +94,7 @@ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
|
|||
|
||||
prefs = get_addon_prefs()
|
||||
sep = prefs.separator
|
||||
name = layer.info
|
||||
name = layer.name
|
||||
|
||||
pattern = pattern.replace('_', sep) # set separator
|
||||
|
||||
|
@ -122,7 +123,7 @@ def layer_name_build(layer, prefix='', prefix2='', desc='', suffix=''):
|
|||
p4 = sep + suffix.upper().strip()
|
||||
|
||||
new = f'{p1}{p2}{p3}{p4}'
|
||||
layer.info = new
|
||||
layer.name = new
|
||||
"""
|
||||
|
||||
## multi-prefix solution (Caps letters)
|
||||
|
@ -155,13 +156,16 @@ class GPTB_OT_layer_name_build(Operator):
|
|||
gpl = ob.data.layers
|
||||
act = gpl.active
|
||||
if not act:
|
||||
self.report({'ERROR'}, 'no layer active')
|
||||
act = ob.data.layer_groups.active
|
||||
|
||||
if not act:
|
||||
self.report({'ERROR'}, 'No layer active')
|
||||
return {"CANCELLED"}
|
||||
|
||||
layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
|
||||
|
||||
## Deactivate multi-selection on layer !
|
||||
## somethimes it affect a random layer that is still considered selected
|
||||
## /!\ Deactivate multi-selection on layer !
|
||||
## Somethimes it affect a random layer that is still considered selected
|
||||
# for l in gpl:
|
||||
# if l.select or l == act:
|
||||
# layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
|
||||
|
@ -169,79 +173,6 @@ class GPTB_OT_layer_name_build(Operator):
|
|||
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 ##-#
|
||||
|
||||
def activate_channel_group_color(context):
|
||||
|
@ -261,9 +192,9 @@ def build_layers_targets_from_dopesheet(context):
|
|||
|
||||
|
||||
if dopeset.show_only_selected:
|
||||
pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
|
||||
pool = [o for o in context.selected_objects if o.type == 'GREASEPENCIL']
|
||||
else:
|
||||
pool = [o for o in context.scene.objects if o.type == 'GPENCIL']
|
||||
pool = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
|
||||
if not dopeset.show_hidden:
|
||||
pool = [o for o in pool if o.visible_get()]
|
||||
|
||||
|
@ -272,7 +203,7 @@ def build_layers_targets_from_dopesheet(context):
|
|||
|
||||
# apply search filter
|
||||
if dopeset.filter_text:
|
||||
layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.info.lower()) ^ dopeset.use_filter_invert]
|
||||
layer_pool = [l for l in layer_pool if (dopeset.filter_text.lower() in l.name.lower()) ^ dopeset.use_filter_invert]
|
||||
|
||||
return layer_pool
|
||||
|
||||
|
@ -292,7 +223,7 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
|
||||
items=(
|
||||
|
@ -330,35 +261,35 @@ class GPTB_OT_select_set_same_prefix(Operator):
|
|||
self.report({'ERROR'}, 'No active layer to base action')
|
||||
return {"CANCELLED"}
|
||||
|
||||
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
|
||||
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}')
|
||||
|
||||
res = re.search(PATTERN, act.info)
|
||||
res = re.search(PATTERN, act.name)
|
||||
if not res:
|
||||
self.report({'ERROR'}, f'Error scanning {act.info}')
|
||||
self.report({'ERROR'}, f'Error scanning {act.name}')
|
||||
return {"CANCELLED"}
|
||||
|
||||
namespace = res.group('tag')
|
||||
if not namespace:
|
||||
self.report({'WARNING'}, f'No prefix detected in {act.info} with separator {sep}')
|
||||
self.report({'WARNING'}, f'No prefix detected in {act.name} with separator {sep}')
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.mode == 'SELECT':
|
||||
## with split
|
||||
# namespace = act.info.split(sep,1)[0]
|
||||
# namespace_bool_list = [l.info.split(sep,1)[0] == namespace for l in gpl]
|
||||
# namespace = act.name.split(sep,1)[0]
|
||||
# namespace_bool_list = [l.name.split(sep,1)[0] == namespace for l in gpl]
|
||||
|
||||
## with reg # only active
|
||||
# namespace_bool_list = [l.info.split(sep,1)[0] + sep == namespace for l in gpl]
|
||||
# namespace_bool_list = [l.name.split(sep,1)[0] + sep == namespace for l in gpl]
|
||||
# gpl.foreach_set('select', namespace_bool_list)
|
||||
|
||||
## don't work Need Foreach set per gp
|
||||
# for l in pool:
|
||||
# l.select = l.info.split(sep,1)[0] + sep == namespace
|
||||
# l.select = l.name.split(sep,1)[0] + sep == namespace
|
||||
|
||||
for gp, layers in gp_dic.items():
|
||||
# check namespace + restrict selection to visible layers according to filters
|
||||
# TODO : Should use the regex pattern to detect and compare r.group('tag')
|
||||
namespace_bool_list = [(l in layers) and (l.info.split(sep,1)[0] + sep == namespace) for l in gp.layers]
|
||||
namespace_bool_list = [(l in layers) and (l.name.split(sep,1)[0] + sep == namespace) for l in gp.layers]
|
||||
gp.layers.foreach_set('select', namespace_bool_list)
|
||||
|
||||
elif self.mode == 'SET':
|
||||
|
@ -380,7 +311,7 @@ class GPTB_OT_select_set_same_color(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
mode : EnumProperty(default='SELECT', options={'SKIP_SAVE'},
|
||||
items=(
|
||||
|
@ -415,7 +346,7 @@ class GPTB_OT_select_set_same_color(Operator):
|
|||
self.report({'ERROR'}, 'No active layer to base action')
|
||||
return {"CANCELLED"}
|
||||
|
||||
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.info}')
|
||||
print(f'Select/Set ref layer: {gp.name} > {gp.layers.active.name}')
|
||||
color = act.channel_color
|
||||
if self.mode == 'SELECT':
|
||||
## NEED FOREACH TO APPLY SELECT
|
||||
|
@ -426,7 +357,7 @@ class GPTB_OT_select_set_same_color(Operator):
|
|||
|
||||
# On multiple objects -- don't work, need foreach
|
||||
# for l in pool:
|
||||
# print(l.id_data.name, l.info, l.channel_color == act.channel_color)
|
||||
# print(l.id_data.name, l.name, l.channel_color == act.channel_color)
|
||||
# l.select = l.channel_color == act.channel_color
|
||||
|
||||
"""
|
||||
|
@ -463,38 +394,38 @@ def replace_layer_name(target, replacement, selected_only=True, prefix_only=True
|
|||
gpl = bpy.context.object.data.layers
|
||||
|
||||
if selected_only:
|
||||
lays = [l for l in gpl if l.select] # exclude : l.info != 'background'
|
||||
lays = [l for l in gpl if l.select] # exclude : l.name != 'background'
|
||||
else:
|
||||
lays = [l for l in gpl] # exclude : if l.info != 'background'
|
||||
lays = [l for l in gpl] # exclude : if l.name != 'background'
|
||||
|
||||
ct = 0
|
||||
for l in lays:
|
||||
old = l.info
|
||||
old = l.name
|
||||
if regex:
|
||||
new = re.sub(target, replacement, l.info)
|
||||
new = re.sub(target, replacement, l.name)
|
||||
if old != new:
|
||||
l.info = new
|
||||
l.name = new
|
||||
print('rename:', old, '-->', new)
|
||||
ct += 1
|
||||
continue
|
||||
|
||||
if prefix_only:
|
||||
if not sep in l.info:
|
||||
if not sep in l.name:
|
||||
# only if separator exists
|
||||
continue
|
||||
splited = l.info.split(sep)
|
||||
splited = l.name.split(sep)
|
||||
prefix = splited[0]
|
||||
new_prefix = prefix.replace(target, replacement)
|
||||
if prefix != new_prefix:
|
||||
splited[0] = new_prefix
|
||||
l.info = sep.join(splited)
|
||||
print('rename:', old, '-->', l.info)
|
||||
l.name = sep.join(splited)
|
||||
print('rename:', old, '-->', l.name)
|
||||
ct += 1
|
||||
|
||||
else:
|
||||
new = l.info.replace(target, replacement)
|
||||
new = l.name.replace(target, replacement)
|
||||
if old != new:
|
||||
l.info = new
|
||||
l.name = new
|
||||
print('rename:', old, '-->', new)
|
||||
ct += 1
|
||||
return ct
|
||||
|
@ -507,7 +438,7 @@ class GPTB_OT_rename_gp_layer(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
|
||||
replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'ANIMATABLE'}, subtype='NONE')
|
||||
|
@ -548,7 +479,7 @@ class GPTB_OT_rename_gp_layer(Operator):
|
|||
## --- UI layer panel---
|
||||
|
||||
def layer_name_builder_ui(self, context):
|
||||
'''appended to DATA_PT_gpencil_layers'''
|
||||
'''appended to DATA_PT_grease_pencil_layers'''
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
if not prefs.show_prefix_buttons:
|
||||
|
@ -557,7 +488,7 @@ def layer_name_builder_ui(self, context):
|
|||
return
|
||||
|
||||
layout = self.layout
|
||||
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
|
||||
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
|
||||
# layout.separator()
|
||||
col = layout.column()
|
||||
|
||||
|
@ -633,7 +564,7 @@ def gpencil_dopesheet_header(self, context):
|
|||
'''to append in DOPESHEET_HT_header'''
|
||||
layout = self.layout
|
||||
st = context.space_data
|
||||
if st.mode != 'GPENCIL':
|
||||
if st.mode != 'GREASEPENCIL':
|
||||
return
|
||||
|
||||
row = layout.row(align=True)
|
||||
|
@ -646,6 +577,7 @@ def gpencil_dopesheet_header(self, context):
|
|||
|
||||
def gpencil_layer_dropdown_menu(self, context):
|
||||
'''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')
|
||||
|
||||
## handler and msgbus
|
||||
|
@ -653,7 +585,7 @@ def gpencil_layer_dropdown_menu(self, context):
|
|||
def obj_layer_name_callback():
|
||||
'''assign layer name properties so user an tweak it'''
|
||||
ob = bpy.context.object
|
||||
if not ob or ob.type != 'GPENCIL':
|
||||
if not ob or ob.type != 'GREASEPENCIL':
|
||||
return
|
||||
if not ob.data.layers.active:
|
||||
return
|
||||
|
@ -663,7 +595,7 @@ def obj_layer_name_callback():
|
|||
for l in ob.data.layers:
|
||||
l.select = l == ob.data.layers.active
|
||||
|
||||
res = re.search(PATTERN, ob.data.layers.active.info.strip())
|
||||
res = re.search(PATTERN, ob.data.layers.active.name.strip())
|
||||
if not res:
|
||||
return
|
||||
if not res.group('name'):
|
||||
|
@ -675,14 +607,28 @@ def obj_layer_name_callback():
|
|||
# print('inc:', res.group('inc'))
|
||||
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():
|
||||
subscribe_to = (bpy.types.GreasePencilLayers, "active_index")
|
||||
subscribe_to = (bpy.types.GreasePencilv3Layers, "active")
|
||||
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...
|
||||
owner=bpy.types.GreasePencilv3, # <-- can attach to an ID during all it's lifetime...
|
||||
# Args passed to callback function (tuple)
|
||||
args=(),
|
||||
# Callback function for property update
|
||||
|
@ -690,6 +636,7 @@ def subscribe_layer_change():
|
|||
options={'PERSISTENT'},
|
||||
)
|
||||
|
||||
|
||||
@persistent
|
||||
def subscribe_layer_change_handler(dummy):
|
||||
subscribe_layer_change()
|
||||
|
@ -723,7 +670,7 @@ class GPTB_PT_layer_name_ui(bpy.types.Panel):
|
|||
row = layout.row()
|
||||
row.activate_init = True
|
||||
row.label(icon='OUTLINER_DATA_GP_LAYER')
|
||||
row.prop(context.object.data.layers.active, 'info', text='')
|
||||
row.prop(context.object.data.layers.active, 'name', text='')
|
||||
|
||||
def add_layer(context):
|
||||
bpy.ops.gpencil.layer_add()
|
||||
|
@ -736,7 +683,7 @@ class GPTB_OT_add_gp_layer_with_rename(Operator):
|
|||
bl_options = {"REGISTER", "UNDO"}
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
add_layer(context)
|
||||
|
@ -750,7 +697,7 @@ class GPTB_OT_add_gp_layer(Operator):
|
|||
bl_options = {"REGISTER", "UNDO"}
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
add_layer(context)
|
||||
|
@ -776,7 +723,7 @@ def register_keymaps():
|
|||
|
||||
##---# F2 rename calls
|
||||
## Direct rename active layer in Paint mode
|
||||
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
|
||||
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY")
|
||||
kmi = km.keymap_items.new('wm.call_panel', type='F2', value='PRESS')
|
||||
kmi.properties.name = 'GPTB_PT_layer_name_ui'
|
||||
kmi.properties.keep_open = False
|
||||
|
@ -802,8 +749,6 @@ def unregister_keymaps():
|
|||
classes=(
|
||||
GPTB_OT_rename_gp_layer,
|
||||
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_color,
|
||||
|
||||
|
@ -817,9 +762,9 @@ def register():
|
|||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.DATA_PT_gpencil_layers.prepend(layer_name_builder_ui)
|
||||
bpy.types.DATA_PT_grease_pencil_layers.prepend(layer_name_builder_ui)
|
||||
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
|
||||
bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu)
|
||||
bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.append(gpencil_layer_dropdown_menu)
|
||||
bpy.app.handlers.load_post.append(subscribe_layer_change_handler)
|
||||
register_keymaps()
|
||||
|
||||
|
@ -829,12 +774,13 @@ def register():
|
|||
def unregister():
|
||||
unregister_keymaps()
|
||||
bpy.app.handlers.load_post.remove(subscribe_layer_change_handler)
|
||||
bpy.types.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu)
|
||||
bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.remove(gpencil_layer_dropdown_menu)
|
||||
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
|
||||
bpy.types.DATA_PT_gpencil_layers.remove(layer_name_builder_ui)
|
||||
bpy.types.DATA_PT_grease_pencil_layers.remove(layer_name_builder_ui)
|
||||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# delete layer index trigger
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
|
||||
# Delete layer index trigger
|
||||
# /!\ can remove msgbus made for other functions or other addons using same owner
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
|
|
@ -26,10 +26,11 @@ class GPT_OT_layer_nav(bpy.types.Operator):
|
|||
prefs = utils.get_addon_prefs()
|
||||
if not prefs.nav_use_fade:
|
||||
if self.direction == 'DOWN':
|
||||
utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info')
|
||||
utils.iterate_active_layer(context.grease_pencil, -1)
|
||||
# utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'name') # gpv2
|
||||
|
||||
if self.direction == 'UP':
|
||||
utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info')
|
||||
utils.iterate_active_layer(context.grease_pencil, 1)
|
||||
return {'FINISHED'}
|
||||
|
||||
## get up and down keys for use in modal
|
||||
|
@ -91,12 +92,11 @@ class GPT_OT_layer_nav(bpy.types.Operator):
|
|||
context.space_data.overlay.gpencil_fade_layer = fade
|
||||
|
||||
if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'):
|
||||
_val = utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info')
|
||||
_val = utils.iterate_active_layer(context.grease_pencil, -1)
|
||||
trigger = True
|
||||
|
||||
if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'):
|
||||
_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
|
||||
_val = utils.iterate_active_layer(context.grease_pencil, 1)
|
||||
trigger = True
|
||||
|
||||
if trigger:
|
||||
|
@ -127,7 +127,7 @@ addon_keymaps = []
|
|||
def register_keymap():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
|
||||
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY")
|
||||
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY")
|
||||
|
||||
kmi = km.keymap_items.new('gp.layer_nav', type='PAGE_UP', value='PRESS')
|
||||
kmi.properties.direction = 'UP'
|
||||
|
|
|
@ -4,7 +4,7 @@ import mathutils
|
|||
from mathutils import Vector, Matrix, geometry
|
||||
from bpy_extras import view3d_utils
|
||||
from time import time
|
||||
from .utils import get_gp_draw_plane, location_to_region, region_to_location
|
||||
from .utils import get_gp_draw_plane, location_to_region, region_to_location, is_locked, is_hidden
|
||||
|
||||
class GP_OT_pick_closest_layer(Operator):
|
||||
bl_idname = "gp.pick_closest_layer"
|
||||
|
@ -14,7 +14,7 @@ class GP_OT_pick_closest_layer(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
|
||||
|
||||
stroke_filter : bpy.props.EnumProperty(name='Target',
|
||||
default='STROKE',
|
||||
|
@ -33,8 +33,8 @@ class GP_OT_pick_closest_layer(Operator):
|
|||
|
||||
mouse_vec3 = Vector((*self.init_mouse, 0))
|
||||
co, index, _dist = kd.find(mouse_vec3)
|
||||
lid = self.point_pair[index][1]
|
||||
return lid
|
||||
layer = self.point_pair[index][1]
|
||||
return layer
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.t0 = time()
|
||||
|
@ -47,7 +47,7 @@ class GP_OT_pick_closest_layer(Operator):
|
|||
def draw(self, context):
|
||||
layout = self.layout
|
||||
if context.object.data.layers.active:
|
||||
layout.label(text=f'Layer: {context.object.data.layers.active.info}')
|
||||
layout.label(text=f'Layer: {context.object.data.layers.active.name}')
|
||||
layout.prop(self, 'stroke_filter')
|
||||
|
||||
def modal(self, context, event):
|
||||
|
@ -74,50 +74,50 @@ class GP_OT_pick_closest_layer(Operator):
|
|||
|
||||
self.inv_mat = self.ob.matrix_world.inverted()
|
||||
self.point_pair = []
|
||||
if gp.use_multiedit:
|
||||
for layer_id, l in enumerate(gp.layers):
|
||||
if l.hide:# l.lock or
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
for layer in gp.layers:
|
||||
if is_hidden(layer):
|
||||
continue
|
||||
for f in l.frames:
|
||||
for f in layer.frames:
|
||||
if not f.select:
|
||||
continue
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
|
||||
continue
|
||||
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
|
||||
continue
|
||||
self.point_pair += [(Vector((*location_to_region(mat @ p.co), 0)), layer_id) for p in s.points]
|
||||
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
|
||||
|
||||
else:
|
||||
# [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
|
||||
for layer_id, l in enumerate(gp.layers):
|
||||
if l.hide or not l.active_frame:# l.lock or
|
||||
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
|
||||
for layer in gp.layers:
|
||||
if is_hidden(layer) or not layer.current_frame():
|
||||
continue
|
||||
for s in l.active_frame.strokes:
|
||||
for s in layer.current_frame().drawing.strokes:
|
||||
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
|
||||
continue
|
||||
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
|
||||
continue
|
||||
self.point_pair += [(Vector((*location_to_region(mat @ p.co), 0)), layer_id) for p in s.points]
|
||||
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
|
||||
|
||||
if not self.point_pair:
|
||||
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
lid = self.filter_stroke(context)
|
||||
if isinstance(lid, str):
|
||||
self.report({'ERROR'}, lid)
|
||||
layer_target = self.filter_stroke(context)
|
||||
if isinstance(layer_target, str):
|
||||
self.report({'ERROR'}, layer_target)
|
||||
return {'CANCELLED'}
|
||||
|
||||
del self.point_pair # auto garbage collected ?
|
||||
|
||||
self.ob.data.layers.active_index = lid
|
||||
self.ob.data.layers.active = layer_target
|
||||
|
||||
## debug show trigger time
|
||||
# print(f'Trigger time {time() - self.t0:.3f}')
|
||||
# print(f'Search time {time() - t1:.3f}')
|
||||
self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.info}')
|
||||
self.report({'INFO'}, f'Layer: {self.ob.data.layers.active.name}')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
|
@ -125,7 +125,7 @@ class GP_OT_pick_closest_layer(Operator):
|
|||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||
km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||
|
||||
kmi = km.keymap_items.new(
|
||||
# name="",
|
||||
|
|
|
@ -11,7 +11,7 @@ from . import utils
|
|||
# return [('None', 'None','None')]
|
||||
# if not context.object:
|
||||
# return [('None', 'None','None')]
|
||||
# return [(l.info, l.info, '') for l in context.object.data.layers] # if l != context.object.data.layers.active
|
||||
# return [(l.name, l.name, '') for l in context.object.data.layers] # if l != context.object.data.layers.active
|
||||
|
||||
## in Class
|
||||
# bl_property = "layers_enum"
|
||||
|
@ -39,7 +39,7 @@ class GPTB_OT_move_material_to_layer(Operator) :
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.layer_name:
|
||||
|
@ -72,8 +72,8 @@ class GPTB_OT_move_material_to_layer(Operator) :
|
|||
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.info, icon=icon, emboss=False)
|
||||
op.layer_name = l.info
|
||||
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):
|
||||
|
@ -82,7 +82,7 @@ class GPTB_OT_move_material_to_layer(Operator) :
|
|||
return {'CANCELLED'}
|
||||
|
||||
## Active + selection
|
||||
pool = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
|
||||
pool = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
|
||||
if not context.object in pool:
|
||||
pool.append(context.object)
|
||||
|
||||
|
@ -93,6 +93,7 @@ class GPTB_OT_move_material_to_layer(Operator) :
|
|||
# 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:
|
||||
|
@ -110,26 +111,26 @@ class GPTB_OT_move_material_to_layer(Operator) :
|
|||
### Move Strokes to a new key (or existing key if comming for yet another layer)
|
||||
fct = 0
|
||||
sct = 0
|
||||
for l in gpl:
|
||||
if l == target_layer:
|
||||
for layer in gpl:
|
||||
if layer == target_layer:
|
||||
## ! infinite loop if target layer is included
|
||||
continue
|
||||
for f in l.frames:
|
||||
for fr in layer.frames:
|
||||
## skip if no stroke has active material
|
||||
if not next((s for s in f.strokes if s.material_index == mat_index), None):
|
||||
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(f.frame_number)):
|
||||
dest_key = target_layer.frames.new(f.frame_number)
|
||||
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 {f.frame_number}')
|
||||
print(f'{ob.name} : frame {fr.frame_number}')
|
||||
## Replicate strokes in dest_keys
|
||||
stroke_to_delete = []
|
||||
for s in f.strokes:
|
||||
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)
|
||||
stroke_to_delete.append(s_idx)
|
||||
|
||||
## Debug
|
||||
# if time.time() - t > 10:
|
||||
|
@ -138,17 +139,16 @@ class GPTB_OT_move_material_to_layer(Operator) :
|
|||
|
||||
sct += len(stroke_to_delete)
|
||||
|
||||
# print('Removing frames') # Dbg
|
||||
## Remove from source frame (f)
|
||||
## Remove from source frame (fr)
|
||||
if not self.copy:
|
||||
for s in reversed(stroke_to_delete):
|
||||
f.strokes.remove(s)
|
||||
# print('Removing frames') # Dbg
|
||||
if stroke_to_delete:
|
||||
fr.drawing.remove_strokes(indices=stroke_to_delete)
|
||||
|
||||
## ? Remove frame if layer is empty ? -> probably not, will show previous frame
|
||||
## ? Remove frame if layer is empty ? -> probably not, otherwise will show previous frame
|
||||
|
||||
fct += 1
|
||||
l.frames.update()
|
||||
|
||||
|
||||
|
||||
if fct:
|
||||
oct += 1
|
||||
|
|
|
@ -4,7 +4,12 @@ import mathutils
|
|||
from mathutils import Vector, Matrix, geometry
|
||||
from bpy_extras import view3d_utils
|
||||
from time import time
|
||||
from .utils import get_gp_draw_plane, location_to_region, region_to_location
|
||||
from .utils import (get_gp_draw_plane,
|
||||
location_to_region,
|
||||
region_to_location,
|
||||
is_locked,
|
||||
is_hidden)
|
||||
|
||||
|
||||
### passing by 2D projection
|
||||
def get_3d_coord_on_drawing_plane_from_2d(context, co):
|
||||
|
@ -33,20 +38,20 @@ class GP_OT_pick_closest_material(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
|
||||
|
||||
fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||
|
||||
def filter_stroke(self, context):
|
||||
# get stroke under mouse using kdtree
|
||||
point_pair = [(p.co, s) for s in self.stroke_list for p in s.points] # local space
|
||||
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
|
||||
|
||||
kd = mathutils.kdtree.KDTree(len(point_pair))
|
||||
for i, pair in enumerate(point_pair):
|
||||
kd.insert(pair[0], i)
|
||||
kd.balance()
|
||||
|
||||
## Get 3D coordinate on drawing plane according to mouse 2d.co on flat 2d drawing
|
||||
## Get 3D coordinate on drawing plane according to mouse 2d.position on flat 2d drawing
|
||||
_ob, hit, _plane_no = get_3d_coord_on_drawing_plane_from_2d(context, self.init_mouse)
|
||||
|
||||
if not hit:
|
||||
|
@ -62,7 +67,7 @@ class GP_OT_pick_closest_material(Operator):
|
|||
## find point index in stroke
|
||||
self.idx = None
|
||||
for i, p in enumerate(s.points):
|
||||
if p.co == co:
|
||||
if p.position == co:
|
||||
self.idx = i
|
||||
break
|
||||
|
||||
|
@ -77,22 +82,22 @@ class GP_OT_pick_closest_material(Operator):
|
|||
self.stroke_list = []
|
||||
self.inv_mat = self.ob.matrix_world.inverted()
|
||||
|
||||
if self.gp.use_multiedit:
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
for l in self.gp.layers:
|
||||
if l.hide:# l.lock or
|
||||
if is_hidden(l):# is_locked(l) or
|
||||
continue
|
||||
for f in l.frames:
|
||||
if not f.select:
|
||||
continue
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
else:
|
||||
# [s for l in self.gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
|
||||
# [s for l in self.gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
|
||||
for l in self.gp.layers:
|
||||
if l.hide or not l.active_frame:# l.lock or
|
||||
if is_hidden(l) or not l.current_frame():# is_locked(l) or
|
||||
continue
|
||||
for s in l.active_frame.strokes:
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
if self.fill_only:
|
||||
|
@ -116,8 +121,8 @@ class GP_OT_pick_closest_material(Operator):
|
|||
self.report({'WARNING'}, 'No coord found')
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].co
|
||||
self.init_pos = [p.co.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
|
||||
self.depth = self.ob.matrix_world @ self.stroke.points[self.idx].position
|
||||
self.init_pos = [p.position.copy() for p in self.stroke.points] # need a copy otherwise vector is updated
|
||||
## directly use world position ?
|
||||
# self.pos_world = [self.ob.matrix_world @ co for co in self.init_pos]
|
||||
self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
|
||||
|
@ -144,7 +149,7 @@ class GP_OT_pick_closest_material(Operator):
|
|||
|
||||
# if event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
# # for i, p in enumerate(self.stroke.points): # reset position
|
||||
# # self.stroke.points[i].co = self.init_pos[i]
|
||||
# # self.stroke.points[i].position = self.init_pos[i]
|
||||
# context.area.tag_redraw()
|
||||
# return {'CANCELLED'}
|
||||
|
||||
|
@ -159,7 +164,7 @@ class GP_OT_pick_closest_material(Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL' and context.mode == 'PAINT_GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL' and context.mode == 'PAINT_GREASE_PENCIL'
|
||||
|
||||
# fill_only : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
|
||||
stroke_filter : bpy.props.EnumProperty(default='FILL',
|
||||
|
@ -172,7 +177,7 @@ class GP_OT_pick_closest_material(Operator):
|
|||
|
||||
def filter_stroke(self, context):
|
||||
# get stroke under mouse using kdtree
|
||||
point_pair = [(p.co, s) for s in self.stroke_list for p in s.points] # local space
|
||||
point_pair = [(p.position, s) for s in self.stroke_list for p in s.points] # local space
|
||||
|
||||
kd = mathutils.kdtree.KDTree(len(point_pair))
|
||||
for i, pair in enumerate(point_pair):
|
||||
|
@ -195,7 +200,7 @@ class GP_OT_pick_closest_material(Operator):
|
|||
## find point index in stroke
|
||||
self.idx = None
|
||||
for i, p in enumerate(s.points):
|
||||
if p.co == co:
|
||||
if p.position == co:
|
||||
self.idx = i
|
||||
break
|
||||
|
||||
|
@ -233,22 +238,22 @@ class GP_OT_pick_closest_material(Operator):
|
|||
self.stroke_list = []
|
||||
self.inv_mat = self.ob.matrix_world.inverted()
|
||||
|
||||
if gp.use_multiedit:
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
for l in gp.layers:
|
||||
if l.hide:# l.lock or
|
||||
if is_hidden(l):# is_locked(l) or
|
||||
continue
|
||||
for f in l.frames:
|
||||
if not f.select:
|
||||
continue
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
else:
|
||||
# [s for l in gp.layers if not l.lock and not l.hide for s in l.active_frame.stokes]
|
||||
# [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
|
||||
for l in gp.layers:
|
||||
if l.hide or not l.active_frame:# l.lock or
|
||||
if is_hidden(l) or not l.current_frame():# is_locked(l) or
|
||||
continue
|
||||
for s in l.active_frame.strokes:
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
if self.stroke_filter == 'FILL':
|
||||
|
@ -274,8 +279,8 @@ class GP_OT_pick_closest_material(Operator):
|
|||
self.report({'WARNING'}, 'No coord found')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# self.depth = self.ob.matrix_world @ stroke.points[self.idx].co
|
||||
# self.init_pos = [p.co.copy() for p in stroke.points] # need a copy otherwise vector is updated
|
||||
# self.depth = self.ob.matrix_world @ stroke.points[self.idx].position
|
||||
# self.init_pos = [p.position.copy() for p in stroke.points] # need a copy otherwise vector is updated
|
||||
# self.pos_2d = [location_to_region(self.ob.matrix_world @ co) for co in self.init_pos]
|
||||
# self.plen = len(stroke.points)
|
||||
|
||||
|
@ -290,9 +295,8 @@ class GP_OT_pick_closest_material(Operator):
|
|||
addon_keymaps = []
|
||||
def register_keymaps():
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Draw brush)", space_type = "EMPTY", region_type='WINDOW')
|
||||
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Fill)", space_type = "EMPTY", region_type='WINDOW')
|
||||
# km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
|
||||
km = addon.keymaps.new(name = "Grease Pencil Fill Tool", space_type = "EMPTY", region_type='WINDOW')
|
||||
kmi = km.keymap_items.new(
|
||||
# name="",
|
||||
idname="gp.pick_closest_material",
|
||||
|
|
|
@ -42,7 +42,7 @@ class GPTB_OT_load_default_palette(bpy.types.Operator):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
# Start Clean (delete unuesed sh*t)
|
||||
|
@ -82,7 +82,7 @@ class GPTB_OT_load_palette(bpy.types.Operator, ImportHelper):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filename_ext = '.json'
|
||||
|
||||
|
@ -110,7 +110,7 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default='*.json', options={'HIDDEN'})#*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp
|
||||
|
||||
|
@ -169,7 +169,7 @@ def load_blend_palette(context, filepath):
|
|||
|
||||
print(f'-- import palette from : {filepath} --')
|
||||
for ob in context.selected_objects:
|
||||
if ob.type != 'GPENCIL':
|
||||
if ob.type != 'GREASEPENCIL':
|
||||
print(f'{ob.name} not a GP object')
|
||||
continue
|
||||
|
||||
|
@ -224,7 +224,7 @@ class GPTB_OT_load_blend_palette(bpy.types.Operator, ImportHelper):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
filename_ext = '.blend'
|
||||
|
||||
|
@ -252,7 +252,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
|
|||
# path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="")
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def execute(self, context):
|
||||
ob = context.object
|
||||
|
@ -260,7 +260,7 @@ class GPTB_OT_copy_active_to_selected_palette(bpy.types.Operator):
|
|||
self.report({'ERROR'}, 'No materials to transfer')
|
||||
return {"CANCELLED"}
|
||||
|
||||
selection = [o for o in context.selected_objects if o.type == 'GPENCIL' and o != ob]
|
||||
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL' and o != ob]
|
||||
|
||||
if not selection:
|
||||
self.report({'ERROR'}, 'Need to have other Grease pencil objects selected to receive active object materials')
|
||||
|
@ -313,7 +313,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.ob = context.object
|
||||
|
@ -354,7 +354,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
import re
|
||||
diff_ct = 0
|
||||
todel = []
|
||||
if ob.type != 'GPENCIL':
|
||||
if ob.type != 'GREASEPENCIL':
|
||||
return
|
||||
if not hasattr(ob, 'material_slots'):
|
||||
return
|
||||
|
@ -410,7 +410,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
# iterate in all strokes and replace with new_mat_id
|
||||
for l in ob.data.layers:
|
||||
for f in l.frames:
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
if s.material_index == i:
|
||||
s.material_index = new_mat_id
|
||||
|
||||
|
@ -427,7 +427,7 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator):
|
|||
# if self.skip_binded_empty_slots:
|
||||
# for l in ob.data.layers:
|
||||
# for f in l.frames:
|
||||
# for s in f.strokes:
|
||||
# for s in f.drawing.strokes:
|
||||
# if s.material_index == i:
|
||||
# is_binded = True
|
||||
# break
|
||||
|
|
|
@ -45,7 +45,7 @@ class GPTB_OT_import_obj_palette(Operator):
|
|||
|
||||
def execute(self, context):
|
||||
## get targets
|
||||
selection = [o for o in context.selected_objects if o.type == 'GPENCIL']
|
||||
selection = [o for o in context.selected_objects if o.type == 'GREASEPENCIL']
|
||||
if not selection:
|
||||
self.report({'ERROR'}, 'Need to have at least one GP object selected in scene')
|
||||
return {"CANCELLED"}
|
||||
|
@ -98,7 +98,7 @@ class GPTB_OT_import_obj_palette(Operator):
|
|||
return {"CANCELLED"}
|
||||
|
||||
for i in range(len(linked_objs))[::-1]: # reversed(range(len(l))) / range(len(l))[::-1]
|
||||
if linked_objs[i].type != 'GPENCIL':
|
||||
if linked_objs[i].type != 'GREASEPENCIL':
|
||||
print(f'{linked_objs[i].name} type is "{linked_objs[i].type}"')
|
||||
bpy.data.objects.remove(linked_objs.pop(i))
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
|||
# namespace_order
|
||||
namespaces=[]
|
||||
for l in gpl:
|
||||
ns= l.info.lower().split(separator, 1)[0]
|
||||
ns= l.name.lower().split(separator, 1)[0]
|
||||
if ns not in namespaces:
|
||||
namespaces.append(ns)
|
||||
|
||||
|
@ -88,14 +88,14 @@ class GPT_OT_auto_tint_gp_layers(bpy.types.Operator):
|
|||
### step from 0.1 to 0.9
|
||||
|
||||
for i, l in enumerate(gpl):
|
||||
if l.info.lower() not in ('background',):
|
||||
if l.name.lower() not in ('background',):
|
||||
print()
|
||||
print('>', l.info)
|
||||
ns= l.info.lower().split(separator, 1)[0]#get namespace from separator
|
||||
print('>', l.name)
|
||||
ns= l.name.lower().split(separator, 1)[0]#get namespace from separator
|
||||
print("namespace", ns)#Dbg
|
||||
|
||||
if context.scene.gptoolprops.autotint_namespace:
|
||||
h = get_hue_by_name(ns, hue_offset)#l.info == individuels
|
||||
h = get_hue_by_name(ns, hue_offset)#l.name == individuels
|
||||
|
||||
else:
|
||||
h = translate_range((i + hue_offset/100)%layer_ct, 0, layer_ct, 0.1, 0.9)
|
||||
|
|
130
OP_realign.py
130
OP_realign.py
|
@ -1,11 +1,13 @@
|
|||
import bpy
|
||||
import mathutils
|
||||
import numpy as np
|
||||
|
||||
from mathutils import Matrix, Vector
|
||||
from math import pi
|
||||
import numpy as np
|
||||
from time import time
|
||||
from . import utils
|
||||
from mathutils.geometry import intersect_line_plane
|
||||
from . import utils
|
||||
from .utils import is_hidden, is_locked
|
||||
|
||||
def get_scale_matrix(scale):
|
||||
# recreate a neutral mat scale
|
||||
|
@ -15,38 +17,9 @@ def get_scale_matrix(scale):
|
|||
matscale = matscale_x @ matscale_y @ matscale_z
|
||||
return matscale
|
||||
|
||||
'''
|
||||
## Old reproject method using Operators:
|
||||
omode = bpy.context.mode
|
||||
|
||||
if all_strokes:
|
||||
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
|
||||
for l in obj.data.layers:
|
||||
l.hide = False
|
||||
l.lock = False
|
||||
l.lock_frame = False
|
||||
bpy.ops.object.mode_set(mode='EDIT_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)
|
||||
'''
|
||||
|
||||
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
|
||||
'''Reproject - ops method
|
||||
:all_stroke: affect hided, locked layers
|
||||
:all_stroke: affect hidden, locked layers
|
||||
'''
|
||||
|
||||
if restore_frame:
|
||||
|
@ -54,7 +27,7 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
|
|||
|
||||
plan_co, plane_no = utils.get_gp_draw_plane(obj, orient=proj_type)
|
||||
|
||||
frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)]
|
||||
frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.drawing.strokes)]
|
||||
frame_list = list(set(frame_list))
|
||||
frame_list.sort()
|
||||
|
||||
|
@ -69,16 +42,23 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
|
|||
# matrix = np.array(obj.matrix_world, dtype='float64')
|
||||
# matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64')
|
||||
#mat = src.matrix_world
|
||||
for l in obj.data.layers:
|
||||
for layer in obj.data.layers:
|
||||
if not all_strokes:
|
||||
if not l.select:
|
||||
if not layer.select:
|
||||
continue
|
||||
if l.hide or l.lock:
|
||||
if is_hidden(layer) or is_locked(layer):
|
||||
continue
|
||||
f = next((f for f in l.frames if f.frame_number == i), None)
|
||||
if f is None:
|
||||
|
||||
frame = next((f for f in layer.frames if f.frame_number == i), 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
|
||||
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).
|
||||
# nb_points = len(s.points)
|
||||
# coords = np.empty(nb_points * 3, dtype='float64')
|
||||
|
@ -86,21 +66,20 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
|
|||
# world_co_3d = utils.matrix_transform(coords.reshape((nb_points, 3)), matrix)
|
||||
|
||||
## list comprehension method
|
||||
world_co_3d = [obj.matrix_world @ p.co for p in s.points]
|
||||
world_co_3d = [obj.matrix_world @ p.position for p in s.points]
|
||||
|
||||
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d]
|
||||
|
||||
## Basic method (Slower than foreach_set)
|
||||
# for i, p in enumerate(s.points):
|
||||
# p.co = obj.matrix_world.inverted() @ new_world_co_3d[i]
|
||||
# Basic method (Slower than foreach_set and compatible with GPv3)
|
||||
## TODO: use low level api with curve offsets...
|
||||
for pt_index, point in enumerate(s.points):
|
||||
point.position = matrix_inv @ new_world_co_3d[pt_index]
|
||||
|
||||
## GPv2: ravel and use foreach_set
|
||||
## Ravel new coordinate on the fly
|
||||
new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
|
||||
|
||||
## 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)
|
||||
|
||||
## 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]
|
||||
# s.points.foreach_set('co', new_local_coords)
|
||||
|
||||
if restore_frame:
|
||||
bpy.context.scene.frame_current = oframe
|
||||
|
@ -139,24 +118,24 @@ def align_global(reproject=True, ref=None, all_strokes=True):
|
|||
# world_coords = []
|
||||
for l in o.data.layers:
|
||||
for f in l.frames:
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
## foreach
|
||||
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
|
||||
# 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()
|
||||
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
|
||||
|
||||
# for p in s.points:
|
||||
## GPv2
|
||||
# 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 :
|
||||
# world_co = mat @ p.co
|
||||
# p.co = new_mat.inverted() @ world_co
|
||||
# world_co = mat @ p.position
|
||||
# p.position = new_mat.inverted() @ world_co
|
||||
|
||||
## GOOD :
|
||||
# p.co = p.co @ mat.inverted() @ new_mat
|
||||
p.position = p.position @ mat.inverted() @ new_mat
|
||||
|
||||
if o.parent:
|
||||
o.matrix_world = new_mat
|
||||
|
@ -216,9 +195,9 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
|
|||
scale_mat = get_scale_matrix(o_scale)
|
||||
new_mat = loc_mat @ rot_mat @ scale_mat
|
||||
|
||||
for s in f.strokes:
|
||||
for s in f.drawing.strokes:
|
||||
## foreach
|
||||
coords = [p.co @ mat.inverted() @ new_mat for p in s.points]
|
||||
coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
|
||||
# print('coords: ', coords)
|
||||
# print([co for v in coords for co in v])
|
||||
s.points.foreach_set('co', [co for v in coords for co in v])
|
||||
|
@ -267,7 +246,7 @@ class GPTB_OT_realign(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
reproject : bpy.props.BoolProperty(
|
||||
name='Reproject', default=True,
|
||||
|
@ -283,7 +262,7 @@ class GPTB_OT_realign(bpy.types.Operator):
|
|||
## add option to bake strokes if rotation anim is not constant ? might generate too many Keyframes
|
||||
|
||||
def invoke(self, context, event):
|
||||
if context.object.data.use_multiedit:
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
self.report({'ERROR'}, 'Does not work in Multiframe mode')
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
@ -365,7 +344,7 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
return context.object and context.object.type == 'GREASEPENCIL'
|
||||
|
||||
all_strokes : bpy.props.BoolProperty(
|
||||
name='All Strokes', default=True,
|
||||
|
@ -383,7 +362,7 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
|||
default='CURRENT')
|
||||
|
||||
def invoke(self, context, event):
|
||||
if context.object.data.use_multiedit:
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
self.report({'ERROR'}, 'Does not work in Multi-edit')
|
||||
return {"CANCELLED"}
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
@ -413,9 +392,6 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
|||
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):
|
||||
t0 = time()
|
||||
|
@ -432,12 +408,12 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
|
|||
### -- MENU ENTRY --
|
||||
|
||||
def reproject_clean_menu(self, context):
|
||||
if context.mode == 'EDIT_GPENCIL':
|
||||
if context.mode == 'EDIT_GREASE_PENCIL':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup (also works with 'INVOKE_DEFAULT')
|
||||
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
|
||||
|
||||
def reproject_context_menu(self, context):
|
||||
if context.mode == 'EDIT_GPENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||
if context.mode == 'EDIT_GREASE_PENCIL' and context.scene.tool_settings.gpencil_selectmode_edit == 'STROKE':
|
||||
self.layout.operator_context = 'INVOKE_REGION_WIN' # needed for popup
|
||||
self.layout.operator('gp.batch_reproject_all_frames', icon='KEYTYPE_JITTER_VEC')
|
||||
|
||||
|
@ -450,12 +426,12 @@ def register():
|
|||
for cl in classes:
|
||||
bpy.utils.register_class(cl)
|
||||
|
||||
bpy.types.VIEW3D_MT_gpencil_edit_context_menu.append(reproject_context_menu)
|
||||
bpy.types.GPENCIL_MT_cleanup.append(reproject_clean_menu)
|
||||
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.append(reproject_context_menu)
|
||||
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.append(reproject_clean_menu)
|
||||
|
||||
def unregister():
|
||||
bpy.types.GPENCIL_MT_cleanup.remove(reproject_clean_menu)
|
||||
bpy.types.VIEW3D_MT_gpencil_edit_context_menu.remove(reproject_context_menu)
|
||||
bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.remove(reproject_clean_menu)
|
||||
bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.remove(reproject_context_menu)
|
||||
|
||||
for cl in reversed(classes):
|
||||
bpy.utils.unregister_class(cl)
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
Blender addon - Various tool to help with grease pencil in animation productions.
|
||||
|
||||
### /!\ Main branch is currently broken, in migration to gpv3
|
||||
|
||||
**[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)**
|
||||
|
||||
**[Readme Doc in French](README_FR.md)**
|
||||
|
|
|
@ -4,6 +4,8 @@ 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é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)**
|
||||
|
||||
**[English Readme Doc](README.md)**
|
||||
|
|
|
@ -7,7 +7,7 @@ from .utils import get_addon_prefs
|
|||
|
||||
class GPTB_WT_eraser(WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'PAINT_GPENCIL'
|
||||
bl_context_mode = 'PAINT_GREASE_PENCIL'
|
||||
|
||||
# The prefix of the idname should be your add-on name.
|
||||
bl_idname = "gp.eraser_tool"
|
||||
|
|
42
UI_tools.py
42
UI_tools.py
|
@ -20,7 +20,7 @@ class GPTB_PT_dataprop_panel(Panel):
|
|||
# bl_category = "Tool"
|
||||
# bl_idname = "ADDONID_PT_panel_name"# identifier, if ommited, takes the name of the class.
|
||||
bl_label = "Pseudo color"# title
|
||||
bl_parent_id = "DATA_PT_gpencil_layers"#subpanel of this ID
|
||||
bl_parent_id = "DATA_PT_grease_pencil_layers"#subpanel of this ID
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
|
@ -144,7 +144,7 @@ class GPTB_PT_sidebar_panel(Panel):
|
|||
col.prop(context.scene.gptoolprops, 'keyframe_type', text='Jump On') # Keyframe Jump
|
||||
# col.prop(context.space_data.overlay, 'use_gpencil_onion_skin') # not often used
|
||||
|
||||
if context.object and context.object.type == 'GPENCIL':
|
||||
if context.object and context.object.type == 'GREASEPENCIL':
|
||||
# col.prop(context.object.data, 'use_autolock_layers') # not often used
|
||||
col.prop(context.object, 'show_in_front') # text='In Front'
|
||||
|
||||
|
@ -166,7 +166,9 @@ class GPTB_PT_sidebar_panel(Panel):
|
|||
else:
|
||||
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
|
||||
# addon_updater_ops.update_notice_box_ui(self, context)
|
||||
|
@ -191,23 +193,23 @@ class GPTB_PT_anim_manager(Panel):
|
|||
# import time
|
||||
# t0 = time.perf_counter()
|
||||
|
||||
# objs = [o for o in context.scene.objects if o.type not in ('GPENCIL', 'CAMERA')]
|
||||
# gps = [o for o in context.scene.objects if o.type == 'GPENCIL']
|
||||
# objs = [o for o in context.scene.objects if o.type not in ('GREASEPENCIL', 'CAMERA')]
|
||||
# gps = [o for o in context.scene.objects if o.type == 'GREASEPENCIL']
|
||||
# cams = [o for o in context.scene.objects if o.type == 'CAMERA']
|
||||
objs = []
|
||||
gps = []
|
||||
cams = []
|
||||
for o in context.scene.objects:
|
||||
if o.type not in ('GPENCIL', 'CAMERA'):
|
||||
if o.type not in ('GREASEPENCIL', 'CAMERA'):
|
||||
objs.append(o)
|
||||
elif o.type == 'GPENCIL':
|
||||
elif o.type == 'GREASEPENCIL':
|
||||
gps.append(o)
|
||||
elif o.type == 'CAMERA':
|
||||
cams.append(o)
|
||||
|
||||
# print(f'{time.perf_counter() - t0:.8f}s')
|
||||
|
||||
return {'OBJECT': objs, 'GPENCIL': gps, 'CAMERA': cams}
|
||||
return {'OBJECT': objs, 'GREASEPENCIL': gps, 'CAMERA': cams}
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
|
@ -221,7 +223,7 @@ class GPTB_PT_anim_manager(Panel):
|
|||
col.operator('gp.list_disabled_anims')
|
||||
|
||||
## Show Enable / Disable anims
|
||||
for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GPENCIL')]:
|
||||
for cat, cat_type in [('Obj anims:', 'OBJECT'), ('Cam anims:', 'CAMERA'), ('Gp anims:', 'GREASEPENCIL')]:
|
||||
on_icon, off_icon = anim_status(obj_types[cat_type])
|
||||
|
||||
subcol = col.column()
|
||||
|
@ -242,7 +244,7 @@ class GPTB_PT_anim_manager(Panel):
|
|||
|
||||
row = subcol.row(align=True)
|
||||
row.label(text='Gp modifiers:')
|
||||
on_icon, off_icon = gp_modifier_status(obj_types['GPENCIL'])
|
||||
on_icon, off_icon = gp_modifier_status(obj_types['GREASEPENCIL'])
|
||||
# subcol.alert = off_icon == 'LAYER_ACTIVE' # Turn red
|
||||
row.operator('gp.toggle_hide_gp_modifier', text='ON', icon=on_icon).show = True
|
||||
row.operator('gp.toggle_hide_gp_modifier', text='OFF', icon=off_icon).show = False
|
||||
|
@ -279,6 +281,8 @@ class GPTB_PT_anim_manager(Panel):
|
|||
col.use_property_split = False
|
||||
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)
|
||||
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):
|
||||
|
@ -425,7 +429,7 @@ def palette_manager_menu(self, context):
|
|||
"""Palette menu to append in existing menu"""
|
||||
# GPENCIL_MT_material_context_menu
|
||||
layout = self.layout
|
||||
# {'EDIT_GPENCIL', 'PAINT_GPENCIL','SCULPT_GPENCIL','WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
|
||||
# {'EDIT_GREASE_PENCIL', 'PAINT_GREASE_PENCIL','SCULPT_GREASE_PENCIL','WEIGHT_GREASE_PENCIL', 'VERTEX_GPENCIL'}
|
||||
layout.separator()
|
||||
prefs = get_addon_prefs()
|
||||
|
||||
|
@ -658,7 +662,7 @@ class GPTB_PT_tools_grease_pencil_interpolate(Panel):
|
|||
# settings = context.tool_settings.gpencil_interpolate # old 2.92 global settings
|
||||
## access active tool settings
|
||||
# settings = context.workspace.tools[0].operator_properties('gpencil.interpolate')
|
||||
settings = context.workspace.tools.from_space_view3d_mode('PAINT_GPENCIL').operator_properties('gpencil.interpolate')
|
||||
settings = context.workspace.tools.from_space_view3d_mode('PAINT_GREASE_PENCIL').operator_properties('gpencil.interpolate')
|
||||
|
||||
## custom curve access (still in gp interpolate tools)
|
||||
interpolate_settings = context.tool_settings.gpencil_interpolate
|
||||
|
@ -734,7 +738,7 @@ def interpolate_header_ui(self, context):
|
|||
layout = self.layout
|
||||
obj = context.active_object
|
||||
|
||||
if obj and obj.type == 'GPENCIL' and context.gpencil_data:
|
||||
if obj and obj.type == 'GREASEPENCIL' and context.gpencil_data:
|
||||
gpd = context.gpencil_data
|
||||
else:
|
||||
return
|
||||
|
@ -768,7 +772,10 @@ def register():
|
|||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
|
||||
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref)
|
||||
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
|
||||
|
||||
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
|
||||
|
||||
# if bpy.app.version >= (3,0,0):
|
||||
|
@ -777,8 +784,13 @@ def register():
|
|||
|
||||
def unregister():
|
||||
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
|
||||
bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
|
||||
|
||||
bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref)
|
||||
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
|
||||
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
|
||||
|
||||
# if bpy.app.version >= (3,0,0):
|
||||
# bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui)
|
||||
|
||||
|
|
16
__init__.py
16
__init__.py
|
@ -4,8 +4,8 @@ bl_info = {
|
|||
"name": "GP toolbox",
|
||||
"description": "Tool set for Grease Pencil in animation production",
|
||||
"author": "Samuel Bernou, Christophe Seux",
|
||||
"version": (3, 3, 0),
|
||||
"blender": (4, 0, 0),
|
||||
"version": (4, 0, 4),
|
||||
"blender": (4, 3, 0),
|
||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||
"warning": "",
|
||||
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
|
||||
|
@ -35,7 +35,7 @@ from . import OP_brushes
|
|||
from . import OP_file_checker
|
||||
from . import OP_copy_paste
|
||||
from . import OP_realign
|
||||
from . import OP_flat_reproject
|
||||
# from . import OP_flat_reproject # Disabled
|
||||
from . import OP_depth_move
|
||||
from . import OP_key_duplicate_send
|
||||
from . import OP_layer_manager
|
||||
|
@ -336,7 +336,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
nav_fade_val : FloatProperty(
|
||||
name='Fade Value',
|
||||
description='Fade value for other layers when navigating (0=invisible)',
|
||||
default=0.35, min=0.0, max=0.95, step=1, precision=2)
|
||||
default=0.1, min=0.0, max=0.95, step=1, precision=2)
|
||||
|
||||
nav_limit : FloatProperty(
|
||||
name='Fade Duration',
|
||||
|
@ -625,9 +625,9 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
|||
layout.label(text='Following checks will be made when clicking "Check File" button:')
|
||||
col = layout.column()
|
||||
col.use_property_split = True
|
||||
col.prop(self.fixprops, 'check_only')
|
||||
col.label(text='If dry run is checked, no modification is done', icon='INFO')
|
||||
col.label(text='Use Ctrl + Click on "Check File" button to invert the behavior', icon='BLANK1')
|
||||
# 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='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1')
|
||||
col.separator()
|
||||
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})')
|
||||
|
@ -793,7 +793,7 @@ addon_modules = (
|
|||
OP_brushes,
|
||||
OP_cursor_snap_canvas,
|
||||
OP_copy_paste,
|
||||
OP_flat_reproject,
|
||||
# OP_flat_reproject # Disabled,
|
||||
OP_realign,
|
||||
OP_depth_move,
|
||||
OP_key_duplicate_send,
|
||||
|
|
|
@ -99,7 +99,8 @@ def gp_stroke_angle_split (frame, strokes, angle):
|
|||
|
||||
splitted_loops = bm_angle_split(bm,angle)
|
||||
|
||||
frame.strokes.remove(stroke_info['stroke'])
|
||||
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
|
||||
frame.drawing.strokes.remove(stroke_info['stroke'])
|
||||
for loop in splitted_loops :
|
||||
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
|
||||
new_stroke = draw_gp_stroke(loop_info,frame,palette,width = line_width)
|
||||
|
@ -123,6 +124,7 @@ def gp_stroke_uniform_density(cam, frame, strokes, max_spacing):
|
|||
|
||||
bm_uniform_density(bm,cam,max_spacing)
|
||||
|
||||
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
|
||||
frame.strokes.remove(stroke_info['stroke'])
|
||||
bm.verts.ensure_lookup_table()
|
||||
|
||||
|
|
|
@ -21,20 +21,16 @@ def update_layer_name(self, context):
|
|||
if not self.layer_name:
|
||||
# never replace by nothing (since there should be prefix/suffix)
|
||||
return
|
||||
if not context.object or context.object.type != 'GPENCIL':
|
||||
if not context.object or context.object.type != 'GREASEPENCIL':
|
||||
return
|
||||
if not context.object.data.layers.active:
|
||||
return
|
||||
layer_name_build(context.object.data.layers.active, desc=self.layer_name)
|
||||
# context.object.data.layers.active.info = self.layer_name
|
||||
# context.object.data.layers.active.name = self.layer_name
|
||||
|
||||
|
||||
class GP_PG_FixSettings(PropertyGroup):
|
||||
|
||||
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(
|
||||
name="Lock Main Cam",
|
||||
|
@ -182,9 +178,15 @@ class GP_PG_ToolsSettings(PropertyGroup):
|
|||
name='Cursor Follow', description="3D cursor follow active object animation when activated",
|
||||
default=False, update=cursor_follow_update)
|
||||
|
||||
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)
|
||||
cursor_follow_target : bpy.props.PointerProperty(
|
||||
name='Cursor Follow Target',
|
||||
description="Optional target object to follow for cursor instead of active object",
|
||||
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
|
||||
name_for_current_render : StringProperty(
|
||||
|
|
450
utils.py
450
utils.py
|
@ -5,18 +5,85 @@ import mathutils
|
|||
import math
|
||||
import subprocess
|
||||
|
||||
from time import time
|
||||
from math import sqrt
|
||||
from mathutils import Vector
|
||||
from sys import platform
|
||||
|
||||
## constants values
|
||||
|
||||
|
||||
""" def get_gp_parent(layer) :
|
||||
if layer.parent_type == "BONE" and layer.parent_bone :
|
||||
return layer.parent.pose.bones.get(layer.parent_bone)
|
||||
else :
|
||||
return layer.parent
|
||||
"""
|
||||
## Default stroke and points attributes
|
||||
stroke_attr = [
|
||||
'start_cap',
|
||||
'end_cap',
|
||||
'softness',
|
||||
'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):
|
||||
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
|
||||
|
@ -29,9 +96,9 @@ def get_matrix(ob) :
|
|||
return ob.matrix_world.copy()
|
||||
|
||||
def set_matrix(gp_frame,mat):
|
||||
for stroke in gp_frame.strokes :
|
||||
for stroke in gp_frame.drawing.strokes :
|
||||
for point in stroke.points :
|
||||
point.co = mat @ point.co
|
||||
point.position = mat @ point.position
|
||||
|
||||
# get view vector location (the 2 methods work fine)
|
||||
def get_view_origin_position():
|
||||
|
@ -155,12 +222,77 @@ def gp_stroke_to_bmesh(strokes):
|
|||
### 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):
|
||||
'''
|
||||
draw basic stroke by passing list of point 3D coordinate
|
||||
the frame to draw on and optional width parameter (default = 2)
|
||||
'''
|
||||
stroke = frame.strokes.new()
|
||||
stroke = frame.drawing.strokes.new()
|
||||
stroke.line_width = width
|
||||
stroke.display_mode = '3DSPACE'
|
||||
stroke.material_index = mat_id
|
||||
|
@ -173,12 +305,12 @@ def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0):
|
|||
# for i, pt in enumerate(pts):
|
||||
# stroke.points.add()
|
||||
# dest_point = stroke.points[i]
|
||||
# dest_point.co = pt
|
||||
# dest_point.position = pt
|
||||
return stroke
|
||||
|
||||
## OLD - need update
|
||||
def draw_gp_stroke(loop_info, frame, palette, width = 2) :
|
||||
stroke = frame.strokes.new(palette)
|
||||
stroke = frame.drawing.strokes.new(palette)
|
||||
|
||||
stroke.line_width = width
|
||||
stroke.display_mode = '3DSPACE'# old -> draw_mode
|
||||
|
@ -372,24 +504,24 @@ def create_gp_palette(gp_data_block,info) :
|
|||
|
||||
def get_gp_objects(selection=True):
|
||||
'''return selected objects or only the active one'''
|
||||
if not bpy.context.active_object or bpy.context.active_object.type != 'GPENCIL':
|
||||
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL':
|
||||
print('No active GP object')
|
||||
return []
|
||||
|
||||
active = bpy.context.active_object
|
||||
if selection:
|
||||
selection = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
|
||||
selection = [o for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
|
||||
if not active in selection:
|
||||
selection += [active]
|
||||
return selection
|
||||
|
||||
if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL':
|
||||
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL':
|
||||
return [active]
|
||||
return []
|
||||
|
||||
def get_gp_datas(selection=True):
|
||||
'''return selected objects or only the active one'''
|
||||
if not bpy.context.active_object or bpy.context.active_object.type != 'GPENCIL':
|
||||
if not bpy.context.active_object or bpy.context.active_object.type != 'GREASEPENCIL':
|
||||
print('No active GP object')
|
||||
return []
|
||||
|
||||
|
@ -397,15 +529,15 @@ def get_gp_datas(selection=True):
|
|||
if selection:
|
||||
selected = []
|
||||
for o in bpy.context.selected_objects:
|
||||
if o.type == 'GPENCIL':
|
||||
if o.type == 'GREASEPENCIL':
|
||||
if o.data not in selected:
|
||||
selected.append(o.data)
|
||||
# selected = [o.data for o in bpy.context.selected_objects if o.type == 'GPENCIL']
|
||||
# selected = [o.data for o in bpy.context.selected_objects if o.type == 'GREASEPENCIL']
|
||||
if not active_data in selected:
|
||||
selected += [active_data]
|
||||
return selected
|
||||
|
||||
if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL':
|
||||
if bpy.context.active_object and bpy.context.active_object.type == 'GREASEPENCIL':
|
||||
return [active_data]
|
||||
|
||||
print('EOL. No active GP object')
|
||||
|
@ -440,7 +572,7 @@ def get_active_frame(layer_name=None):
|
|||
if layer_name:
|
||||
lay = bpy.context.scene.grease_pencil.layers.get(layer_name)
|
||||
if lay:
|
||||
frame = lay.active_frame
|
||||
frame = lay.current_frame()
|
||||
if frame:
|
||||
return frame
|
||||
else:
|
||||
|
@ -449,7 +581,7 @@ def get_active_frame(layer_name=None):
|
|||
print('no layers named', layer_name, 'in scene layers')
|
||||
|
||||
else:#active layer
|
||||
frame = bpy.context.scene.grease_pencil.layers.active.active_frame
|
||||
frame = bpy.context.scene.grease_pencil.layers.active.current_frame()
|
||||
if frame:
|
||||
return frame
|
||||
else:
|
||||
|
@ -457,7 +589,7 @@ def get_active_frame(layer_name=None):
|
|||
|
||||
def get_stroke_2D_coords(stroke):
|
||||
'''return a list containing points 2D coordinates of passed gp stroke object'''
|
||||
return [location_to_region(p.co) for p in stroke.points]
|
||||
return [location_to_region(p.position) for p in stroke.points]
|
||||
|
||||
'''#foreach method for retreiving multiple other attribute quickly and stack them
|
||||
point_nb = len(stroke.points)
|
||||
|
@ -472,14 +604,14 @@ def get_stroke_2D_coords(stroke):
|
|||
def get_all_stroke_2D_coords(frame):
|
||||
'''return a list of lists with all strokes's points 2D location'''
|
||||
## using modification from get_stroke_2D_coords func'
|
||||
return [get_stroke_2D_coords(s) for s in frame.strokes]
|
||||
return [get_stroke_2D_coords(s) for s in frame.drawing.strokes]
|
||||
## direct
|
||||
#return[[location_to_region(p.co) for p in s.points] for s in frame.strokes]
|
||||
#return[[location_to_region(p.position) for p in s.points] for s in frame.drawing.strokes]
|
||||
|
||||
def selected_strokes(frame):
|
||||
'''return all stroke having a point selected as a list of strokes objects'''
|
||||
stlist = []
|
||||
for i, s in enumerate(frame.strokes):
|
||||
for i, s in enumerate(frame.drawing.strokes):
|
||||
if any(pt.select for pt in s.points):
|
||||
stlist.append(s)
|
||||
return stlist
|
||||
|
@ -491,66 +623,135 @@ def copy_stroke_to_frame(s, frame, select=True):
|
|||
return created stroke
|
||||
'''
|
||||
|
||||
ns = frame.strokes.new()
|
||||
|
||||
## Set strokes attr
|
||||
stroke_attr = [
|
||||
'line_width',
|
||||
'material_index',
|
||||
'draw_cyclic',
|
||||
'use_cyclic',
|
||||
'uv_scale',
|
||||
'uv_rotation',
|
||||
'hardness',
|
||||
'uv_translation',
|
||||
'vertex_color_fill',
|
||||
]
|
||||
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:
|
||||
if not hasattr(s, attr):
|
||||
continue
|
||||
# print(f'transfer stroke {attr}') # Dbg
|
||||
setattr(ns, attr, getattr(s, attr))
|
||||
|
||||
## create points
|
||||
point_count = len(s.points)
|
||||
ns.points.add(len(s.points))
|
||||
|
||||
## Set points attr
|
||||
# for p, np in zip(s.points, ns.points):
|
||||
flat_list = [0.0] * point_count
|
||||
flat_uv_fill_list = [0.0, 0.0] * point_count
|
||||
flat_vector_list = [0.0, 0.0, 0.0] * point_count
|
||||
flat_color_list = [0.0, 0.0, 0.0, 0.0] * point_count
|
||||
|
||||
single_attr = [
|
||||
'pressure',
|
||||
'strength',
|
||||
'uv_factor',
|
||||
'uv_rotation',
|
||||
]
|
||||
for 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
|
||||
|
||||
for attr in single_attr:
|
||||
# print(f'transfer point {attr}') # Dbg
|
||||
s.points.foreach_get(attr, flat_list)
|
||||
ns.points.foreach_set(attr, flat_list)
|
||||
## 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))
|
||||
|
||||
# print(f'transfer point co') # Dbg
|
||||
s.points.foreach_get('co', flat_vector_list)
|
||||
ns.points.foreach_set('co', flat_vector_list)
|
||||
|
||||
# print(f'transfer point uv_fill') # Dbg
|
||||
s.points.foreach_get('uv_fill', flat_uv_fill_list)
|
||||
ns.points.foreach_set('uv_fill', flat_uv_fill_list)
|
||||
|
||||
# print(f'transfer point vertex_color') # Dbg
|
||||
s.points.foreach_get('vertex_color', flat_color_list)
|
||||
ns.points.foreach_set('vertex_color', flat_color_list)
|
||||
|
||||
ns.select = select
|
||||
ns.points.update()
|
||||
return ns
|
||||
|
||||
"""## 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
|
||||
# -----------------
|
||||
|
@ -706,20 +907,7 @@ def set_collection(ob, collection, unlink=True) :
|
|||
# -----------------
|
||||
|
||||
def get_addon_prefs():
|
||||
'''
|
||||
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)
|
||||
return bpy.context.preferences.addons[__package__].preferences
|
||||
|
||||
def open_addon_prefs():
|
||||
'''Open addon prefs windows with focus on current addon'''
|
||||
|
@ -823,27 +1011,37 @@ def convert_attr(Attr):
|
|||
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
|
||||
'''Show message box with element passed as string or list
|
||||
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:
|
||||
considered a label [text,icon]
|
||||
considered a label [text, icon]
|
||||
if sublist have 3 element:
|
||||
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):
|
||||
layout = self.layout
|
||||
for l in _message:
|
||||
if isinstance(l, str):
|
||||
self.layout.label(text=l)
|
||||
else:
|
||||
if len(l) == 2: # label with icon
|
||||
self.layout.label(text=l[0], icon=l[1])
|
||||
elif len(l) == 3: # ops
|
||||
self.layout.operator_context = "INVOKE_DEFAULT"
|
||||
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
|
||||
|
||||
## offset pnale when using row...
|
||||
# row = self.layout.row()
|
||||
# row.label(text=l[1])
|
||||
# row.operator(l[0], icon=l[2])
|
||||
layout.label(text=l)
|
||||
elif l[0] == "OPERATOR": # Special operator case with properties
|
||||
layout.operator_context = "INVOKE_DEFAULT"
|
||||
op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False)
|
||||
if len(l) > 4 and isinstance(l[4], dict):
|
||||
for prop_name, value in l[4].items():
|
||||
setattr(op, prop_name, value)
|
||||
|
||||
elif len(l) == 2: # label with icon
|
||||
layout.label(text=l[0], icon=l[1])
|
||||
elif len(l) == 3: # ops
|
||||
layout.operator_context = "INVOKE_DEFAULT"
|
||||
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):
|
||||
_message = [_message]
|
||||
|
@ -1025,6 +1223,38 @@ def iterate_selector(zone, attr, state, info_attr = None, active_access='active'
|
|||
|
||||
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
|
||||
# -----------------
|
||||
|
@ -1206,7 +1436,7 @@ def all_anim_enabled(objects) -> bool:
|
|||
if fcu.mute:
|
||||
return False
|
||||
|
||||
if o.type in ('GPENCIL', 'CAMERA'):
|
||||
if o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||
if o.data.animation_data and o.data.animation_data.action:
|
||||
## Check if object data attributes fcurves are muted
|
||||
for fcu in o.animation_data.action.fcurves:
|
||||
|
@ -1219,9 +1449,9 @@ def all_anim_enabled(objects) -> bool:
|
|||
def all_object_modifier_enabled(objects) -> bool:
|
||||
'''Return False if one modifier of one object has GP modifier disabled in viewport but enabled in render'''
|
||||
for o in objects:
|
||||
if o.type != 'GPENCIL':
|
||||
if o.type != 'GREASEPENCIL':
|
||||
continue
|
||||
for m in o.grease_pencil_modifiers:
|
||||
for m in o.modifiers:
|
||||
if m.show_render and not m.show_viewport:
|
||||
return False
|
||||
|
||||
|
@ -1247,7 +1477,7 @@ def has_fully_enabled_anim(o):
|
|||
if fcu.mute:
|
||||
return False
|
||||
|
||||
if o.type in ('GPENCIL', 'CAMERA'):
|
||||
if o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||
if o.data.animation_data and o.data.animation_data.action:
|
||||
## Check if object data attributes fcurves are muted
|
||||
for fcu in o.animation_data.action.fcurves:
|
||||
|
@ -1292,7 +1522,7 @@ def anim_status(objects) -> tuple((str, str)):
|
|||
on_count += 1
|
||||
count += 1
|
||||
|
||||
if o.type in ('GPENCIL', 'CAMERA'):
|
||||
if o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||
datablock = o.data
|
||||
if datablock.animation_data is None:
|
||||
continue
|
||||
|
@ -1320,12 +1550,12 @@ def gp_modifier_status(objects) -> tuple((str, str)):
|
|||
'''return icons on/off tuple'''
|
||||
on_count = off_count = count = 0
|
||||
for o in objects:
|
||||
if o.type != 'GPENCIL':
|
||||
if o.type != 'GREASEPENCIL':
|
||||
continue
|
||||
## Skip hided object
|
||||
if o.hide_get() and o.hide_render:
|
||||
continue
|
||||
for m in o.grease_pencil_modifiers:
|
||||
for m in o.modifiers:
|
||||
if m.show_render and not m.show_viewport:
|
||||
off_count += 1
|
||||
else:
|
||||
|
|
Loading…
Reference in New Issue