Compare commits
11
Commits
abb61ca6d4
...
v3.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7665dd4f4f | ||
|
|
2013c55ba8 | ||
|
|
3695470354 | ||
|
|
186229cdba | ||
|
|
e441f485a6 | ||
|
|
0cee6163aa | ||
|
|
58e6816e39 | ||
|
|
4d6dc06e4e | ||
|
|
c8763f5ca4 | ||
|
|
f9e7c9cc3b | ||
|
|
86fb848e4a |
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -373,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')
|
||||||
|
|
||||||
|
|||||||
+19
-13
@@ -114,13 +114,17 @@ prev_matrix = None
|
|||||||
## used in properties file to register in boolprop update
|
## used in properties file to register in boolprop update
|
||||||
def cursor_follow_update(self, context):
|
def cursor_follow_update(self, context):
|
||||||
'''append or remove cursor_follow handler according a boolean'''
|
'''append or remove cursor_follow handler according a boolean'''
|
||||||
|
ob = bpy.context.object
|
||||||
|
if bpy.context.scene.gptoolprops.cursor_follow_target:
|
||||||
|
## override with target object is specified
|
||||||
|
ob = bpy.context.scene.gptoolprops.cursor_follow_target
|
||||||
global prev_matrix
|
global prev_matrix
|
||||||
# imported in properties to register in boolprop update
|
# imported in properties to register in boolprop update
|
||||||
if self.cursor_follow:#True
|
if self.cursor_follow:#True
|
||||||
|
if ob:
|
||||||
|
# out of below condition to be called when setting target as well
|
||||||
|
prev_matrix = ob.matrix_world.copy()
|
||||||
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
||||||
if context.object:
|
|
||||||
prev_matrix = context.object.matrix_world
|
|
||||||
|
|
||||||
bpy.app.handlers.frame_change_post.append(cursor_follow)
|
bpy.app.handlers.frame_change_post.append(cursor_follow)
|
||||||
|
|
||||||
else:#False
|
else:#False
|
||||||
@@ -131,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()
|
||||||
@@ -149,8 +155,6 @@ 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)
|
||||||
|
|
||||||
@@ -164,11 +168,13 @@ def selection_changed():
|
|||||||
"""Callback function for selection changes"""
|
"""Callback function for selection changes"""
|
||||||
if not bpy.context.scene.gptoolprops.cursor_follow:
|
if not bpy.context.scene.gptoolprops.cursor_follow:
|
||||||
return
|
return
|
||||||
print('select_changed')
|
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
|
global prev_matrix, prev_active_obj
|
||||||
if prev_active_obj != bpy.context.object:
|
if prev_active_obj != bpy.context.object:
|
||||||
## Set stored matrix to active object
|
## Set stored matrix to active object
|
||||||
prev_matrix = bpy.context.object.matrix_world
|
prev_matrix = bpy.context.object.matrix_world.copy()
|
||||||
prev_active_obj = bpy.context.object
|
prev_active_obj = bpy.context.object
|
||||||
|
|
||||||
## Note: Same owner as layer manager (will be removed as well)
|
## Note: Same owner as layer manager (will be removed as well)
|
||||||
@@ -197,15 +203,15 @@ def register():
|
|||||||
# 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
|
||||||
|
|
||||||
## Follow cursor matrix update on object change
|
## Follow cursor matrix update on object change
|
||||||
# bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
|
bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
|
||||||
# ## Directly set msgbus to work at first addon activation # 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.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'
|
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
# bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
|
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
|
||||||
|
|
||||||
|
|||||||
+161
-60
@@ -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,
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
+8
-78
@@ -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
|
||||||
@@ -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
|
||||||
@@ -817,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,
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -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 = 'name')
|
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 = 'name')
|
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 = 'name')
|
_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 = 'name')
|
_val = utils.iterate_active_layer(context.grease_pencil, 1)
|
||||||
# utils.iterate_selector(bpy.context.scene.grease_pencil.layers, 'active_index', 1, info_attr = 'name')#layers
|
|
||||||
trigger = True
|
trigger = True
|
||||||
|
|
||||||
if trigger:
|
if trigger:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -10,14 +10,80 @@ 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
|
||||||
@@ -162,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
|
||||||
@@ -530,64 +623,30 @@ 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)
|
"""## Works, but do not copy all attributes type (probably ok for GP though)
|
||||||
@@ -630,52 +689,6 @@ def bulk_frame_copy_attributes(source_attr, target_attr):
|
|||||||
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
|
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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 bulk_copy_attributes(source_attr, target_attr):
|
def bulk_copy_attributes(source_attr, target_attr):
|
||||||
'''Get and apply data as flat numpy array based on attribute type'''
|
'''Get and apply data as flat numpy array based on attribute type'''
|
||||||
value_string = attribute_value_string[source_attr.data_type]
|
value_string = attribute_value_string[source_attr.data_type]
|
||||||
@@ -998,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]
|
||||||
@@ -1200,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
|
||||||
# -----------------
|
# -----------------
|
||||||
|
|||||||
Reference in New Issue
Block a user