16 Commits
Author SHA1 Message Date
pullusb 7665dd4f4f Add optional object target for constant cursor follow 2024-12-03 16:37:42 +01:00
pullusb 2013c55ba8 Fix broken cursor_follow feature 2024-12-03 16:10:30 +01:00
pullusb 3695470354 Fix problem in viz conflict check 2024-12-03 15:36:55 +01:00
pullusb 186229cdba typo 2024-12-03 14:36:48 +01:00
pullusb e441f485a6 version bump 2024-12-03 14:27:35 +01:00
pullusb 0cee6163aa Improve file checker
4.0.2

changed: File checker doest not fix directly when clicked:
  - 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
2024-12-03 14:26:43 +01:00
pullusb 58e6816e39 Fix material move (fill opacity problem comes from blender 4.3 itself) 2024-12-02 14:51:43 +01:00
pullusb 4d6dc06e4e Remove useless pseudo groups now that there is a native group system 2024-12-02 11:39:17 +01:00
pullusb c8763f5ca4 add create empty frames to layer dropdowm menu 2024-11-28 12:29:27 +01:00
pullusb f9e7c9cc3b Fix layer nav operator
4.0.1

- fixed: layer nav operator on page up/down
2024-11-28 12:26:33 +01:00
pullusb 86fb848e4a fix batch reproject frame issue 2024-11-27 18:01:18 +01:00
pullusb abb61ca6d4 Initial code for better cursor_follow behavior (wip) 2024-11-27 16:43:41 +01:00
pullusb 7430bca02f Fix modifiers access 2024-11-27 16:40:31 +01:00
pullusb f5c20a3499 fix bad initialized value in clipboard paste.
4.3 do not  initialize points opacity and radius as expected
2024-11-26 16:30:21 +01:00
pullusb edfefa874a robust attribute copy for duplicate frames 2024-11-26 14:16:29 +01:00
pullusb 6dbf666ee3 Fix duplicate-send to to layer 2024-11-21 15:11:06 +01:00
16 changed files with 642 additions and 344 deletions
+17
View File
@@ -1,5 +1,22 @@
# Changelog # Changelog
4.0.3
changed: File checker doest not fix directly when clicked (also removed choice in preference):
- list potential change and display an `Apply Fix`
changed: Enhanced visibility conflict list:
- also include viewlayer hide value
- allow to set all hide value from the state of one of the three
- fixed: material move operator
4.0.1
- fixed: layer nav operator on page up/down
4.0.0
- changed: version for Blender 4.3 - Breaking retrocompatibility with previous.
3.3.0 3.3.0
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu. - added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
+4 -8
View File
@@ -5,6 +5,7 @@ from bpy.props import (FloatProperty,
EnumProperty, EnumProperty,
StringProperty, StringProperty,
IntProperty) IntProperty)
from .. import utils
## copied from OP_key_duplicate_send ## copied from OP_key_duplicate_send
def get_layer_list(self, context): def get_layer_list(self, context):
@@ -14,12 +15,6 @@ def get_layer_list(self, context):
def get_group_list(self, context): def get_group_list(self, context):
return [(g.name, g.name, '') for g in context.object.data.layer_groups] return [(g.name, g.name, '') for g in context.object.data.layer_groups]
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
class GP_OT_create_empty_frames(bpy.types.Operator): class GP_OT_create_empty_frames(bpy.types.Operator):
bl_idname = "gp.create_empty_frames" bl_idname = "gp.create_empty_frames"
@@ -84,7 +79,8 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
gp = context.grease_pencil gp = context.grease_pencil
layer_from_group = None layer_from_group = None
if gp.layer_groups.active: if gp.layer_groups.active:
layer_from_group = get_top_layer_from_group(gp, 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: if not gp.layers.active and not layer_from_group:
self.report({'ERROR'}, 'No active layer or active group containing layer on GP object') self.report({'ERROR'}, 'No active layer or active group containing layer on GP object')
return {'CANCELLED'} return {'CANCELLED'}
@@ -126,7 +122,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
gpl = gp.layers gpl = gp.layers
if gp.layer_groups.active: if gp.layer_groups.active:
reference_layer = get_top_layer_from_group(gp, gp.layer_groups.active) reference_layer = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
else: else:
reference_layer = gpl.active reference_layer = gpl.active
+30 -42
View File
@@ -24,19 +24,13 @@ def convertAttr(Attr):
def getMatrix(layer) : def getMatrix(layer) :
matrix = mathutils.Matrix.Identity(4) matrix = mathutils.Matrix.Identity(4)
## FIXME: not "is_parent" or parent_type anymore if parent := layer.parent:
if layer.parent: if parent.type == 'ARMATURE' and layer.parent_bone:
## temp solution bone = parent.pose.bones[layer.parent_bone]
matrix = layer.parent.matrix_world @ layer.matrix_inverse matrix = bone.matrix @ parent.matrix_world
matrix = matrix.copy() @ layer.matrix_parent_inverse
### if layer.parent_type == 'BONE': else:
# if layer.parent.type == 'ARMATURE': matrix = parent.matrix_world @ layer.matrix_parent_inverse
# object = layer.parent
# bone = object.pose.bones[layer.parent_bone]
# matrix = bone.matrix @ object.matrix_world
# matrix = matrix.copy() @ layer.matrix_inverse
# else :
# matrix = layer.parent.matrix_world @ layer.matrix_inverse
return matrix.copy() return matrix.copy()
@@ -312,39 +306,37 @@ def add_stroke(s, frame, layer, obj, select=False):
ns = frame.drawing.strokes[-1] ns = frame.drawing.strokes[-1]
## set strokes atrributes
for att, val in s.items(): for att, val in s.items():
if att not in ('points'): if att not in ('points'):
setattr(ns, att, val) setattr(ns, att, val)
# ns.points.add(pts_to_add)
ob_mat_inv = obj.matrix_world.inverted() ob_mat_inv = obj.matrix_world.inverted()
## patch radius 1
# radius_flat_list = [di['radius'] for di in s['points']] #get all radius flatened
if layer.parent: if layer.parent:
mat = getMatrix(layer).inverted() layer_matrix = getMatrix(layer).inverted()
for i, pt in enumerate(s['points']): transform_matrix = ob_mat_inv @ layer_matrix
for k, v in pt.items():
if k == 'position':
setattr(ns.points[i], k, v)
ns.points[i].position = ob_mat_inv @ mat @ ns.points[i].position # invert of object * invert of layer * coordinate
else:
setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
else: else:
for i, pt in enumerate(s['points']): transform_matrix = ob_mat_inv
for k, v in pt.items():
if k == 'position': ## Set points attributes
setattr(ns.points[i], k, v) for i, pt in enumerate(s['points']):
ns.points[i].position = ob_mat_inv @ ns.points[i].position# invert of object * coordinate for k, v in pt.items():
else: if k == 'position':
setattr(ns.points[i], k, v) setattr(ns.points[i], k, v)
if select: ns.points[i].position = transform_matrix @ ns.points[i].position # invert of object * invert of layer * coordinate
ns.points[i].select = True 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
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False): def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False):
@@ -381,10 +373,6 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
for s in stroke_list: for s in stroke_list:
add_stroke(s, target_frame, layer, obj, select=select) add_stroke(s, target_frame, layer, obj, select=select)
'''
for s in stroke_data:
add_stroke(s, target_frame)
'''
# print(len(stroke_list), 'strokes pasted') # print(len(stroke_list), 'strokes pasted')
+56 -13
View File
@@ -2,6 +2,8 @@
import bpy import bpy
import mathutils import mathutils
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from bpy.app.handlers import persistent
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
## override all sursor snap shortcut with this in keymap ## override all sursor snap shortcut with this in keymap
@@ -105,20 +107,24 @@ def swap_keymap_by_id(org_idname, new_idname):
k.idname = new_idname k.idname = new_idname
# prev_matrix = mathutils.Matrix()
prev_matrix = None prev_matrix = None
# @call_once(bpy.app.handlers.frame_change_post) # @call_once(bpy.app.handlers.frame_change_post)
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''' '''append or remove cursor_follow handler according a boolean'''
ob = bpy.context.object
if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
global prev_matrix global prev_matrix
# imported in properties to register in boolprop update # imported in properties to register in boolprop update
if self.cursor_follow:#True if self.cursor_follow:#True
if ob:
# out of below condition to be called when setting target as well
prev_matrix = ob.matrix_world.copy()
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]: if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
if context.object:
prev_matrix = context.object.matrix_world
bpy.app.handlers.frame_change_post.append(cursor_follow) bpy.app.handlers.frame_change_post.append(cursor_follow)
else:#False else:#False
@@ -129,11 +135,13 @@ def cursor_follow_update(self,context):
def cursor_follow(scene): def cursor_follow(scene):
'''Handler to make the cursor follow active object matrix changes on frame change''' '''Handler to make the cursor follow active object matrix changes on frame change'''
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)... ob = bpy.context.object
if not 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 return
global prev_matrix global prev_matrix
ob = bpy.context.object
current_matrix = ob.matrix_world current_matrix = ob.matrix_world
if not prev_matrix: if not prev_matrix:
prev_matrix = current_matrix.copy() prev_matrix = current_matrix.copy()
@@ -147,14 +155,43 @@ def cursor_follow(scene):
## translation only ## translation only
# scene.cursor.location += (current_matrix - prev_matrix).to_translation() # scene.cursor.location += (current_matrix - prev_matrix).to_translation()
# print('offset:', (current_matrix - prev_matrix).to_translation())
## full ## full
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location) scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
# store for next use # store for next use
prev_matrix = current_matrix.copy() prev_matrix = current_matrix.copy()
prev_active_obj = None
## Add check for object selection change
def selection_changed():
"""Callback function for selection changes"""
if not bpy.context.scene.gptoolprops.cursor_follow:
return
if bpy.context.scene.gptoolprops.cursor_follow_target:
# we are following a target, nothing to update on selection change
return
global prev_matrix, prev_active_obj
if prev_active_obj != bpy.context.object:
## Set stored matrix to active object
prev_matrix = bpy.context.object.matrix_world.copy()
prev_active_obj = bpy.context.object
## Note: Same owner as layer manager (will be removed as well)
def subscribe_object_change():
subscribe_to = (bpy.types.LayerObjects, 'active')
bpy.msgbus.subscribe_rna(
key=subscribe_to,
# owner of msgbus subcribe (for clearing later)
owner=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime.
args=(),
notify=selection_changed,
options={'PERSISTENT'},
)
@persistent
def subscribe_object_change_handler(dummy):
subscribe_object_change()
classes = ( classes = (
GPTB_OT_cusor_snap, GPTB_OT_cusor_snap,
@@ -163,14 +200,18 @@ GPTB_OT_cusor_snap,
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap # swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
# bpy.app.handlers.frame_change_post.append(cursor_follow) ## 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
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
def unregister(): 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 # swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
@@ -180,3 +221,5 @@ def unregister():
# force remove handler if it's there at unregister # force remove handler if it's there at unregister
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]: if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
bpy.app.handlers.frame_change_post.remove(cursor_follow) bpy.app.handlers.frame_change_post.remove(cursor_follow)
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
+161 -60
View File
@@ -4,6 +4,11 @@ from pathlib import Path
import numpy as np import numpy as np
from . import utils from . import utils
from bpy.props import (BoolProperty,
PointerProperty,
CollectionProperty,
StringProperty)
def remove_stroke_exact_duplications(apply=True): def remove_stroke_exact_duplications(apply=True):
'''Remove accidental stroke duplication (points exactly in the same place) '''Remove accidental stroke duplication (points exactly in the same place)
:apply: Remove the duplication instead of just listing dupes :apply: Remove the duplication instead of just listing dupes
@@ -53,6 +58,10 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# Disable use light on all object # Disable use light on all object
# Remove redundant strokes in frames # Remove redundant strokes in frames
apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False,
description="Apply possible fixes instead of just listing (pop the list again in fix mode)",
options={'SKIP_SAVE'})
def invoke(self, context, event): def invoke(self, context, event):
# need some self-control (I had to...) # need some self-control (I had to...)
self.ctrl = event.ctrl self.ctrl = event.ctrl
@@ -63,10 +72,13 @@ class GPTB_OT_file_checker(bpy.types.Operator):
fix = prefs.fixprops fix = prefs.fixprops
problems = [] 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.apply_fixes
apply ^= self.ctrl if self.ctrl:
apply = True
## Lock main cam: ## Lock main cam:
if fix.lock_main_cam: if fix.lock_main_cam:
@@ -169,13 +181,14 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if fix.list_obj_vis_conflict: if fix.list_obj_vis_conflict:
viz_ct = 0 viz_ct = 0
for o in context.scene.objects: for o in context.scene.objects:
if 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' vp = 'No' if o.hide_viewport else 'Yes'
rd = 'No' if o.hide_render else 'Yes' rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1 viz_ct += 1
print(f'{o.name} : viewport {vp} != render {rd}') print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
if viz_ct: 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 ## GP modifiers visibility conflict
if fix.list_gp_mod_vis_conflict: if fix.list_gp_mod_vis_conflict:
@@ -281,8 +294,12 @@ class GPTB_OT_file_checker(bpy.types.Operator):
else: else:
print(p[0]) print(p[0])
if not self.apply_fixes:
## button to call the operator again with apply_fixes set to True
problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}])
# Show in viewport # Show in viewport
title = "Changed Settings" if apply else "Checked Settings (dry run, nothing changed)" title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
utils.show_message_box(problems, _title = title, _icon = 'INFO') utils.show_message_box(problems, _title = title, _icon = 'INFO')
else: else:
self.report({'INFO'}, 'All good') self.report({'INFO'}, 'All good')
@@ -482,45 +499,10 @@ class GPTB_OT_links_checker(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self, width=popup_width) return context.window_manager.invoke_props_dialog(self, width=popup_width)
""" OLD links checker with show_message_box
class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
bl_options = {"REGISTER"}
def execute(self, context): class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
all_lnks = [] bl_idname = "gp.list_viewport_render_visibility"
has_broken_link = False bl_label = "List Viewport And Render Visibility Conflicts"
## 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"
bl_description = "List objects visibility conflicts, when viewport and render have different values" bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@@ -539,20 +521,133 @@ class GPTB_OT_list_object_visibility(bpy.types.Operator):
def execute(self, context): def execute(self, context):
return {'FINISHED'} return {'FINISHED'}
## basic listing as message box # all in invoke now
# li = [] ### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
# viz_ct = 0 class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
# for o in context.scene.objects: bl_idname = "gp.sync_visibility_from_viewlayer"
# if o.hide_viewport != o.hide_render: bl_label = "Sync Visibility From Viewlayer"
# vp = 'No' if o.hide_viewport else 'Yes' bl_description = "Set viewport and render visibility to match viewlayer visibility"
# rd = 'No' if o.hide_render else 'Yes' bl_options = {"REGISTER", "UNDO"}
# viz_ct += 1
# li.append(f'{o.name} : viewport {vp} != render {rd}') def execute(self, context):
# if li: for obj in context.scene.objects:
# utils.show_message_box(_message=li, _title=f'{viz_ct} visibility conflicts found') is_hidden = obj.hide_get() # Get viewlayer visibility
# else: obj.hide_viewport = is_hidden
# self.report({'INFO'}, f"No Object visibility conflict on current scene") obj.hide_render = is_hidden
# return {'FINISHED'} 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):
# 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
# 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) ## not exposed in UI, Check is performed in Check file (can be called in popped menu)
class GPTB_OT_list_modifier_visibility(bpy.types.Operator): class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
@@ -594,7 +689,13 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
classes = ( 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_list_modifier_visibility,
GPTB_OT_copy_string_to_clipboard, GPTB_OT_copy_string_to_clipboard,
GPTB_OT_copy_multipath_clipboard, GPTB_OT_copy_multipath_clipboard,
+1 -1
View File
@@ -7,7 +7,7 @@ import numpy as np
from time import time from time import time
from .utils import (location_to_region, region_to_location) from .utils import (location_to_region, region_to_location)
## DISABLED (in init, also in menu append, see register below)
""" """
## Do not work on multiple object ## Do not work on multiple object
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False): def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
+1 -1
View File
@@ -475,7 +475,7 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
for o in pool: for o in pool:
if o.type != 'GREASEPENCIL': if o.type != 'GREASEPENCIL':
continue continue
for m in o.modifier: for m in o.modifiers:
# skip modifier that are not visible in render # skip modifier that are not visible in render
if not m.show_render: if not m.show_render:
continue continue
+8 -9
View File
@@ -1,5 +1,6 @@
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator
from . import utils
def get_layer_list(self, context): def get_layer_list(self, context):
@@ -65,27 +66,25 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
## Remove overlapping frames ## Remove overlapping frames
for f in reversed(to_replace): for f in reversed(to_replace):
target_layer.frames.remove(f) target_layer.frames.remove(f.frame_number)
# FIXME : need to createa a dedicated function to copy a frame
## Copy original frames ## Copy original frames
for f in selected_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) sent = len(selected_frames)
## Delete original frames as an option ## Delete original frames as an option
if self.delete_source: if self.delete_source:
for f in reversed(selected_frames): for f in reversed(selected_frames):
act_layer.frames.remove(f) 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: if replaced:
mess += f' ({replaced} replaced)' mess += f' ({replaced} replaced)'
mod = context.mode
bpy.ops.gpencil.editmode_toggle()
bpy.ops.object.mode_set(mode=mod)
self.report({'INFO'}, mess) self.report({'INFO'}, mess)
return {'FINISHED'} return {'FINISHED'}
+30 -84
View File
@@ -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<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
# TODO: allow a more flexible prefix pattern
def layer_name_build(layer, prefix='', desc='', suffix=''): def layer_name_build(layer, prefix='', desc='', suffix=''):
'''GET a layer and argument to build and assign name '''GET a layer and argument to build and assign name
@@ -78,7 +79,7 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
# maybe a more elegant way exists to find all objects users ? # maybe a more elegant way exists to find all objects users ?
# update Gpencil modifier targets # update Gpencil modifier targets
for mod in ob_user.modifier: for mod in ob_user.modifiers:
if not hasattr(mod, 'layer_filter'): if not hasattr(mod, 'layer_filter'):
continue continue
if mod.layer_filter == old: if mod.layer_filter == old:
@@ -155,13 +156,16 @@ class GPTB_OT_layer_name_build(Operator):
gpl = ob.data.layers gpl = ob.data.layers
act = gpl.active act = gpl.active
if not act: if not act:
self.report({'ERROR'}, 'no layer active') act = ob.data.layer_groups.active
if not act:
self.report({'ERROR'}, 'No layer active')
return {"CANCELLED"} return {"CANCELLED"}
layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix) layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
## Deactivate multi-selection on layer ! ## /!\ Deactivate multi-selection on layer !
## somethimes it affect a random layer that is still considered selected ## Somethimes it affect a random layer that is still considered selected
# for l in gpl: # for l in gpl:
# if l.select or l == act: # if l.select or l == act:
# layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix) # layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
@@ -169,79 +173,6 @@ class GPTB_OT_layer_name_build(Operator):
return {"FINISHED"} return {"FINISHED"}
def grp_toggle(l, mode='TOGGLE'):
'''take mode in (TOGGLE, GROUP, UNGROUP) '''
grp_item_id = ' - '
res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.name)
if not res and mode in ('TOGGLE', 'GROUP'):
# No gpr : add group prefix after stripping all space and dash
l.name = grp_item_id + l.name.lstrip(' -')
elif res and mode in ('TOGGLE', 'UNGROUP'):
# found : delete group prefix
l.name = 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.name)
if not res:
self.report({'ERROR'}, 'Could not create a group name, create a layer manually')
return {"CANCELLED"}
name = res.group('name').strip(' -')
if not name:
self.report({'ERROR'}, f'No name found in {act.name}')
return {"CANCELLED"}
if name in [l.name.strip(' -') for l in gpl]:
self.report({'WARNING'}, f'Name already exists: {act.name}')
return {"FINISHED"}
grp_toggle(act, mode='GROUP')
n = gpl.new(name, set_active=False)
n.use_onion_skinning = n.use_lights = False
n.hide = True
n.opacity = 0
return {"FINISHED"}
#-## SELECTION MANAGEMENT ##-# #-## SELECTION MANAGEMENT ##-#
def activate_channel_group_color(context): def activate_channel_group_color(context):
@@ -646,6 +577,7 @@ def gpencil_dopesheet_header(self, context):
def gpencil_layer_dropdown_menu(self, context): def gpencil_layer_dropdown_menu(self, context):
'''to append in GPENCIL_MT_layer_context_menu''' '''to append in GPENCIL_MT_layer_context_menu'''
self.layout.operator('gp.create_empty_frames', icon='KEYFRAME')
self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE') self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE')
## handler and msgbus ## handler and msgbus
@@ -675,14 +607,28 @@ def obj_layer_name_callback():
# print('inc:', res.group('inc')) # print('inc:', res.group('inc'))
bpy.context.scene.gptoolprops['layer_name'] = res.group('name') bpy.context.scene.gptoolprops['layer_name'] = res.group('name')
## old gpv2
# def subscribe_layer_change():
# subscribe_to = (bpy.types.GreasePencilLayers, "active_index")
# bpy.msgbus.subscribe_rna(
# key=subscribe_to,
# # owner of msgbus subcribe (for clearing later)
# # owner=handle,
# owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime...
# # Args passed to callback function (tuple)
# args=(),
# # Callback function for property update
# notify=obj_layer_name_callback,
# options={'PERSISTENT'},
# )
def subscribe_layer_change(): def subscribe_layer_change():
subscribe_to = (bpy.types.GreasePencilLayers, "active_index") subscribe_to = (bpy.types.GreasePencilv3Layers, "active")
bpy.msgbus.subscribe_rna( bpy.msgbus.subscribe_rna(
key=subscribe_to, key=subscribe_to,
# owner of msgbus subcribe (for clearing later) # owner of msgbus subcribe (for clearing later)
# owner=handle, # owner=handle,
owner=bpy.types.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 passed to callback function (tuple)
args=(), args=(),
# Callback function for property update # Callback function for property update
@@ -690,6 +636,7 @@ def subscribe_layer_change():
options={'PERSISTENT'}, options={'PERSISTENT'},
) )
@persistent @persistent
def subscribe_layer_change_handler(dummy): def subscribe_layer_change_handler(dummy):
subscribe_layer_change() subscribe_layer_change()
@@ -723,7 +670,7 @@ class GPTB_PT_layer_name_ui(bpy.types.Panel):
row = layout.row() row = layout.row()
row.activate_init = True row.activate_init = True
row.label(icon='OUTLINER_DATA_GP_LAYER') row.label(icon='OUTLINER_DATA_GP_LAYER')
row.prop(context.object.data.layers.active, 'info', text='') row.prop(context.object.data.layers.active, 'name', text='')
def add_layer(context): def add_layer(context):
bpy.ops.gpencil.layer_add() bpy.ops.gpencil.layer_add()
@@ -802,8 +749,6 @@ def unregister_keymaps():
classes=( classes=(
GPTB_OT_rename_gp_layer, GPTB_OT_rename_gp_layer,
GPTB_OT_layer_name_build, GPTB_OT_layer_name_build,
GPTB_OT_layer_group_toggle,
GPTB_OT_layer_new_group,
GPTB_OT_select_set_same_prefix, GPTB_OT_select_set_same_prefix,
GPTB_OT_select_set_same_color, GPTB_OT_select_set_same_color,
@@ -836,5 +781,6 @@ def unregister():
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
# delete layer index trigger # Delete layer index trigger
bpy.msgbus.clear_by_owner(bpy.types.GreasePencil) # /!\ can remove msgbus made for other functions or other addons using same owner
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
+5 -5
View File
@@ -26,10 +26,11 @@ class GPT_OT_layer_nav(bpy.types.Operator):
prefs = utils.get_addon_prefs() prefs = utils.get_addon_prefs()
if not prefs.nav_use_fade: if not prefs.nav_use_fade:
if self.direction == 'DOWN': if self.direction == 'DOWN':
utils.iterate_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': 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'} return {'FINISHED'}
## get up and down keys for use in modal ## 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 context.space_data.overlay.gpencil_fade_layer = fade
if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'): if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'):
_val = utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info') _val = utils.iterate_active_layer(context.grease_pencil, -1)
trigger = True trigger = True
if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'): if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'):
_val = utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info') _val = utils.iterate_active_layer(context.grease_pencil, 1)
# utils.iterate_selector(bpy.context.scene.grease_pencil.layers, 'active_index', 1, info_attr = 'info')#layers
trigger = True trigger = True
if trigger: if trigger:
+15 -15
View File
@@ -93,6 +93,7 @@ class GPTB_OT_move_material_to_layer(Operator) :
# t = time.time() # Dbg # t = time.time() # Dbg
total = 0 total = 0
oct = 0 oct = 0
for ob in pool: for ob in pool:
mat_index = next((i for i, ms in enumerate(ob.material_slots) if ms.material and ms.material == mat), None) 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: 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) ### Move Strokes to a new key (or existing key if comming for yet another layer)
fct = 0 fct = 0
sct = 0 sct = 0
for l in gpl: for layer in gpl:
if l == target_layer: if layer == target_layer:
## ! infinite loop if target layer is included ## ! infinite loop if target layer is included
continue continue
for f in l.frames: for fr in layer.frames:
## skip if no stroke has active material ## skip if no stroke has active material
if not next((s for s in f.drawing.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 continue
## Get/Create a destination frame and keep a reference to it ## Get/Create a destination frame and keep a reference to it
if not (dest_key := key_dict.get(f.frame_number)): if not (dest_key := key_dict.get(fr.frame_number)):
dest_key = target_layer.frames.new(f.frame_number) dest_key = target_layer.frames.new(fr.frame_number)
key_dict[dest_key.frame_number] = dest_key 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 ## Replicate strokes in dest_keys
stroke_to_delete = [] stroke_to_delete = []
for s in f.drawing.strokes: for s_idx, s in enumerate(fr.drawing.strokes):
if s.material_index == mat_index: if s.material_index == mat_index:
utils.copy_stroke_to_frame(s, dest_key) utils.copy_stroke_to_frame(s, dest_key)
stroke_to_delete.append(s) stroke_to_delete.append(s_idx)
## Debug ## Debug
# if time.time() - t > 10: # if time.time() - t > 10:
@@ -138,16 +139,15 @@ class GPTB_OT_move_material_to_layer(Operator) :
sct += len(stroke_to_delete) sct += len(stroke_to_delete)
# print('Removing frames') # Dbg ## Remove from source frame (fr)
## Remove from source frame (f)
if not self.copy: if not self.copy:
for s in reversed(stroke_to_delete): # print('Removing frames') # Dbg
f.drawing.strokes.remove(s) 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 fct += 1
l.frames.update()
if fct: if fct:
+14 -14
View File
@@ -69,19 +69,22 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
# matrix = np.array(obj.matrix_world, dtype='float64') # matrix = np.array(obj.matrix_world, dtype='float64')
# matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64') # matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64')
#mat = src.matrix_world #mat = src.matrix_world
for l in obj.data.layers: for layer in obj.data.layers:
if not all_strokes: if not all_strokes:
if not l.select: if not layer.select:
continue continue
if l.hide or l.lock: if layer.hide or layer.lock:
continue 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)
# FIXME: some strokes are ingored if frame is None:
# print(f'skip {l.name}, no frame at {i}') print(layer.name, 'Not found')
# FIXME: some strokes are ignored
# print(frame'skip {layer.name}, no frame at {i}')
continue continue
for s in f.drawing.strokes:
# print(l.name, s.material_index) for s in frame.drawing.strokes:
# print(layer.name, s.material_index)
## Batch matrix apply (Here is slower than list comprehension). ## Batch matrix apply (Here is slower than list comprehension).
# nb_points = len(s.points) # nb_points = len(s.points)
@@ -96,8 +99,8 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
# Basic method (Slower than foreach_set and compatible with GPv3) # Basic method (Slower than foreach_set and compatible with GPv3)
## TODO: use low level api with curve offsets... ## TODO: use low level api with curve offsets...
for i, p in enumerate(s.points): for pt_index, point in enumerate(s.points):
p.position = matrix_inv @ new_world_co_3d[i] point.position = matrix_inv @ new_world_co_3d[pt_index]
## GPv2: ravel and use foreach_set ## GPv2: ravel and use foreach_set
## Ravel new coordinate on the fly ## Ravel new coordinate on the fly
@@ -417,9 +420,6 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
box.label(text=orient[axis][0], icon=orient[axis][1]) box.label(text=orient[axis][0], icon=orient[axis][1])
def execute(self, context): def execute(self, context):
t0 = time() t0 = time()
orient = self.type orient = self.type
+2
View File
@@ -281,6 +281,8 @@ class GPTB_PT_anim_manager(Panel):
col.use_property_split = False col.use_property_split = False
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR') text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon) col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
if context.scene.gptoolprops.cursor_follow:
col.prop(context.scene.gptoolprops, 'cursor_follow_target', text='Target', icon='OBJECT_DATA')
class GPTB_PT_toolbox_playblast(Panel): class GPTB_PT_toolbox_playblast(Panel):
+7 -7
View File
@@ -4,7 +4,7 @@ bl_info = {
"name": "GP toolbox", "name": "GP toolbox",
"description": "Tool set for Grease Pencil in animation production", "description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux", "author": "Samuel Bernou, Christophe Seux",
"version": (4, 0, 0), "version": (4, 0, 3),
"blender": (4, 3, 0), "blender": (4, 3, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "", "warning": "",
@@ -35,7 +35,7 @@ from . import OP_brushes
from . import OP_file_checker from . import OP_file_checker
from . import OP_copy_paste from . import OP_copy_paste
from . import OP_realign from . import OP_realign
from . import OP_flat_reproject # from . import OP_flat_reproject # Disabled
from . import OP_depth_move from . import OP_depth_move
from . import OP_key_duplicate_send from . import OP_key_duplicate_send
from . import OP_layer_manager from . import OP_layer_manager
@@ -336,7 +336,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
nav_fade_val : FloatProperty( nav_fade_val : FloatProperty(
name='Fade Value', name='Fade Value',
description='Fade value for other layers when navigating (0=invisible)', description='Fade value for other layers when navigating (0=invisible)',
default=0.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( nav_limit : FloatProperty(
name='Fade Duration', 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:') layout.label(text='Following checks will be made when clicking "Check File" button:')
col = layout.column() col = layout.column()
col.use_property_split = True col.use_property_split = True
col.prop(self.fixprops, 'check_only') # col.prop(self.fixprops, 'check_only')
col.label(text='If dry run is checked, no modification is done', icon='INFO') 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" button to invert the behavior', icon='BLANK1') # col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1')
col.separator() col.separator()
col.prop(self.fixprops, 'lock_main_cam') col.prop(self.fixprops, 'lock_main_cam')
col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})') col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})')
@@ -793,7 +793,7 @@ addon_modules = (
OP_brushes, OP_brushes,
OP_cursor_snap_canvas, OP_cursor_snap_canvas,
OP_copy_paste, OP_copy_paste,
OP_flat_reproject, # OP_flat_reproject # Disabled,
OP_realign, OP_realign,
OP_depth_move, OP_depth_move,
OP_key_duplicate_send, OP_key_duplicate_send,
+5 -4
View File
@@ -31,10 +31,6 @@ def update_layer_name(self, context):
class GP_PG_FixSettings(PropertyGroup): class GP_PG_FixSettings(PropertyGroup):
check_only : BoolProperty(
name="Dry run mode (Check only)",
description="Do not change anything, just print the messages",
default=False, options={'HIDDEN'})
lock_main_cam : BoolProperty( lock_main_cam : BoolProperty(
name="Lock Main Cam", name="Lock Main Cam",
@@ -182,6 +178,11 @@ class GP_PG_ToolsSettings(PropertyGroup):
name='Cursor Follow', description="3D cursor follow active object animation when activated", name='Cursor Follow', description="3D cursor follow active object animation when activated",
default=False, update=cursor_follow_update) default=False, update=cursor_follow_update)
cursor_follow_target : bpy.props.PointerProperty(
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 ## gpv3 : no edit line color anymore
# edit_lines_opacity : FloatProperty( # edit_lines_opacity : FloatProperty(
# name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils", # name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils",
+277 -72
View File
@@ -5,18 +5,85 @@ import mathutils
import math import math
import subprocess import subprocess
from time import time
from math import sqrt from math import sqrt
from mathutils import Vector from mathutils import Vector
from sys import platform from sys import platform
## constants values
""" def get_gp_parent(layer) : ## Default stroke and points attributes
if layer.parent_type == "BONE" and layer.parent_bone : stroke_attr = [
return layer.parent.pose.bones.get(layer.parent_bone) 'start_cap',
else : 'end_cap',
return layer.parent '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): def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
@@ -161,6 +228,33 @@ def layer_active_index(gpl):
''' '''
return next((i for i, l in enumerate(gpl) if l == gpl.active), None) 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 ## Check for nested lock
def is_locked(stack_item): def is_locked(stack_item):
'''Check if passed stack item (layer or group) is locked '''Check if passed stack item (layer or group) is locked
@@ -529,66 +623,135 @@ def copy_stroke_to_frame(s, frame, select=True):
return created stroke return created stroke
''' '''
ns = frame.drawing.strokes.new() frame.drawing.add_strokes([len(s.points)])
ns = frame.drawing.strokes[-1]
## Set strokes attr # print(len(s.points), 'new:', len(ns.points))
stroke_attr = [ #ns.material_index
'line_width',
'material_index',
'draw_cyclic',
'use_cyclic',
'uv_scale',
'uv_rotation',
'hardness',
'uv_translation',
'vertex_color_fill',
]
## replicate attributes (simple loop)
## TODO : might need to create atribute domain if does not exists in destination
for attr in stroke_attr: for attr in stroke_attr:
if not hasattr(s, attr):
continue
# print(f'transfer stroke {attr}') # Dbg
setattr(ns, attr, getattr(s, attr)) setattr(ns, attr, getattr(s, attr))
## create points for src_p, dest_p in zip(s.points, ns.points):
point_count = len(s.points) for attr in point_attr:
ns.points.add(len(s.points)) setattr(dest_p, attr, getattr(src_p, attr))
## Define selection
dest_p.select=select
## Set points attr ## Direcly iterate over attribute ?
# for p, np in zip(s.points, ns.points): # src_start = src_dr.curve_offsets[0].value
flat_list = [0.0] * point_count # src_end = src_start + data_size
flat_uv_fill_list = [0.0, 0.0] * point_count # dst_start = dst_dr.curve_offsets[0].value
flat_vector_list = [0.0, 0.0, 0.0] * point_count # dst_end = dst_start + data_size
flat_color_list = [0.0, 0.0, 0.0, 0.0] * point_count # 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))
single_attr = [
'pressure',
'strength',
'uv_factor',
'uv_rotation',
]
for attr in single_attr:
# print(f'transfer point {attr}') # Dbg
s.points.foreach_get(attr, flat_list)
ns.points.foreach_set(attr, flat_list)
# print(f'transfer point co') # Dbg
s.points.foreach_get('co', flat_vector_list)
ns.points.foreach_set('co', flat_vector_list)
# print(f'transfer point uv_fill') # Dbg
s.points.foreach_get('uv_fill', flat_uv_fill_list)
ns.points.foreach_set('uv_fill', flat_uv_fill_list)
# print(f'transfer point vertex_color') # Dbg
s.points.foreach_get('vertex_color', flat_color_list)
ns.points.foreach_set('vertex_color', flat_color_list)
ns.select = select
ns.points.update()
return ns return ns
"""## Works, but do not copy all attributes type (probably ok for GP though)
def bulk_frame_copy_attributes(source_attr, target_attr):
'''Get and apply data as flat numpy array based on attribute type'''
if source_attr.data_type == 'INT':
data = np.empty(len(source_attr.data), dtype=np.int32)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'INT8':
data = np.empty(len(source_attr.data), dtype=np.int8)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'FLOAT':
data = np.empty(len(source_attr.data), dtype=np.float32)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'FLOAT_VECTOR':
data = np.empty(len(source_attr.data) * 3, dtype=np.float32)
source_attr.data.foreach_get('vector', data)
target_attr.data.foreach_set('vector', data)
elif source_attr.data_type == 'FLOAT_COLOR':
data = np.empty(len(source_attr.data) * 4, dtype=np.float32)
source_attr.data.foreach_get('color', data)
target_attr.data.foreach_set('color', data)
elif source_attr.data_type == 'BOOLEAN':
data = np.empty(len(source_attr.data), dtype=bool)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
## works in slowmotion (keep as reference for testing)
# def copy_attribute_values(src_dr, dst_dr, source_attr, dest_attr, data_size):
# ## Zip method to copy one by one
# val_type = {'FLOAT_COLOR': 'color','FLOAT_VECTOR': 'vector'}.get(source_attr.data_type, 'value')
# src_start = src_dr.curve_offsets[0].value
# src_end = src_start + data_size
# dst_start = dst_dr.curve_offsets[0].value
# dst_end = dst_start + data_size
# for src_idx, dest_idx in zip(range(src_start, src_end),range(dst_start, dst_end)):
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
"""
def bulk_copy_attributes(source_attr, target_attr):
'''Get and apply data as flat numpy array based on attribute type'''
value_string = attribute_value_string[source_attr.data_type]
dtype = attribute_value_dtype[source_attr.data_type]
shape = attribute_value_shape[source_attr.data_type]
domain_size = len(source_attr.data)
## Need to pass attributes to get domain size
# domain_size = attributes.domain_size(source_attr.domain)
# start = time()
data = np.empty((domain_size, *shape), dtype=dtype).ravel()
source_attr.data.foreach_get(value_string, data)
target_attr.data.foreach_set(value_string, data)
# end = time()
# np_empty = end - start
## np.prod (works, supposedly faster but tested slower)
# data = np.empty(int(domain_size * np.prod(shape)), dtype=dtype)
# source_attr.data.foreach_get(value_string, data)
# target_attr.data.foreach_set(value_string, data)
## np.zeros (works, sometimes faster on big set of attributes)
# start = time()
# data = np.zeros((domain_size, *shape), dtype=dtype)
# source_attr.data.foreach_get(value_string, np.ravel(data))
# target_attr.data.foreach_set(value_string, np.ravel(data))
# end = time()
# np_zero = end - start
# print('np EMPTY faster' if np_empty < np_zero else 'np ZERO faster', source_attr.domain, source_attr.data_type, domain_size)
# print('np_zero', np_zero)
# print('np_empty', np_empty)
# print()
def copy_frame_at(source_frame, layer, frame_number):
'''Copy a frame (source_frame) to a layer at given frame_number'''
source_drawing = source_frame.drawing
# frame_copy_start = time() # time_dbg
frame = layer.frames.new(frame_number)
dr = frame.drawing
dr.add_strokes([len(s.points) for s in source_drawing.strokes])
for attr_name in source_drawing.attributes.keys():
source_attr = source_drawing.attributes[attr_name]
if attr_name not in dr.attributes:
dr.attributes.new(
name=attr_name, type=source_attr.data_type, domain=source_attr.domain)
target_attr = dr.attributes[attr_name]
# start_time = time() # time_dbg-per-attrib
# bulk_frame_copy_attributes(source_attr, target_attr) # only some attributes
bulk_copy_attributes(source_attr, target_attr)
# copy_attribute_values(source_drawing, dr, source_attr, target_attr, source_drawing.attributes.domain_size(source_attr.domain)) # super slow
# end_time = time() # time_dbg-per-attrib
# print(f"copy_attribute '{attr_name}' execution time: {end_time - start_time} seconds") # time_dbg-per-attrib
# frame_copy_end = time() # time_dbg
# print(f"frame copy execution time: {frame_copy_end - frame_copy_start} seconds") # time_dbg
# ----------------- # -----------------
### Vector utils 3d ### Vector utils 3d
# ----------------- # -----------------
@@ -848,27 +1011,37 @@ def convert_attr(Attr):
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
'''Show message box with element passed as string or list '''Show message box with element passed as string or list
if _message if a list of lists: if _message if a list of lists:
if first element is "OPERATOR":
List format: ["OPERATOR", operator_id, text, icon, {prop_name: value, ...}]
if sublist have 2 element: if sublist have 2 element:
considered a label [text,icon] considered a label [text, icon]
if sublist have 3 element: if sublist have 3 element:
considered as an operator [ops_id_name, text, icon] considered as an operator [ops_id_name, text, icon]
if sublist have 4 element:
considered as a property [object, propname, text, icon]
''' '''
def draw(self, context): def draw(self, context):
layout = self.layout
for l in _message: for l in _message:
if isinstance(l, str): if isinstance(l, str):
self.layout.label(text=l) layout.label(text=l)
else: elif l[0] == "OPERATOR": # Special operator case with properties
if len(l) == 2: # label with icon layout.operator_context = "INVOKE_DEFAULT"
self.layout.label(text=l[0], icon=l[1]) op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False)
elif len(l) == 3: # ops if len(l) > 4 and isinstance(l[4], dict):
self.layout.operator_context = "INVOKE_DEFAULT" for prop_name, value in l[4].items():
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry setattr(op, prop_name, value)
## offset pnale when using row... elif len(l) == 2: # label with icon
# row = self.layout.row() layout.label(text=l[0], icon=l[1])
# row.label(text=l[1]) elif len(l) == 3: # ops
# row.operator(l[0], icon=l[2]) 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): if isinstance(_message, str):
_message = [_message] _message = [_message]
@@ -1050,6 +1223,38 @@ def iterate_selector(zone, attr, state, info_attr = None, active_access='active'
return info, bottom return info, bottom
def iterate_active_layer(gpd, state):
'''Iterate active GP layer in stack
gpd: Grease Pencil Data
'''
layers = gpd.layers
l_count = len(layers)
if state: # swap
# info = None
# bottom = None
## Get active layer index
active_index = closest_layer_active_index(gpd, fallback_index=None)
if active_index == None:
## fallback to first layer if nothing found
gpd.layers.active = layers[0]
return
target_index = active_index + state
new_index = target_index % l_count
## set active layer
gpd.layers.active = layers[new_index]
if target_index == l_count:
bottom = 1 # bottom reached, cycle to first
elif target_index < 0:
bottom = -1 # up reached, cycle to last
# info = gpd.layers.active.name
# return info, bottom
# ----------------- # -----------------
### Curve handle ### Curve handle
# ----------------- # -----------------
@@ -1246,7 +1451,7 @@ def all_object_modifier_enabled(objects) -> bool:
for o in objects: for o in objects:
if o.type != 'GREASEPENCIL': if o.type != 'GREASEPENCIL':
continue continue
for m in o.modifier: for m in o.modifiers:
if m.show_render and not m.show_viewport: if m.show_render and not m.show_viewport:
return False return False
@@ -1350,7 +1555,7 @@ def gp_modifier_status(objects) -> tuple((str, str)):
## Skip hided object ## Skip hided object
if o.hide_get() and o.hide_render: if o.hide_get() and o.hide_render:
continue continue
for m in o.modifier: for m in o.modifiers:
if m.show_render and not m.show_viewport: if m.show_render and not m.show_viewport:
off_count += 1 off_count += 1
else: else: