22 Commits
Author SHA1 Message Date
Pullusb 9a4b965d51 Restore relative dir explorer props in preferences
5.0.1

- fixed: Restored preference paths as dir path (now need `PATH_SUPPORTS_BLEND_RELATIVE` option)
2026-01-13 09:29:05 +01:00
Pullusb 9d2644eb9f Update for blender 5+ with retro-compatibility
- Added version check to keep addon compatible with previous version
- Some feature are still broken (e.g: realign, broken since 4.3's gpv3)
- note: DIR_PATH string property do not accept relative path since Blender 5.0.
preferences brush_path,output_path and playblast_path had "//" by default. those props are turned to stnadard string for now
2025-12-15 23:57:57 +01:00
pullusb 9fd3b4af83 Fix delete view bound and add multiframe support
Also better report
2025-07-30 12:13:04 +02:00
pullusb a86a1fd409 merge stroke-point del into one ops - add in menu 2025-07-29 18:24:04 +02:00
pullusb 93b7ad91d8 Add special GP stroke deleter bound to vierw
4.2.0

- added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched)
  - No preset shortcut:
  - `gp.delete_points_view_bound` for points
  - `gp.delete_strokes_view_bound` for strokes
2025-07-29 17:41:07 +02:00
pullusb 9289b2ed6d add region keyword for better code navigation in utils 2025-07-29 17:40:03 +02:00
pullusb 2b198d03c9 Add popup with option to select duplicated strokes instead of removing 2025-06-17 18:01:01 +02:00
pullusb e93ee458b9 fix removed stroke frame number in prints 2025-06-17 17:38:08 +02:00
pullusb 2868bad3da fix verbose mode 2025-06-17 17:18:47 +02:00
pullusb a5dae7ee19 Fix redundant checker and add operator "Remove Redundant GP Stroke" (search) 2025-06-17 17:17:29 +02:00
pullusb 939b4f534f Fix file checker's redundant strokes delete
4.1.3

- fixed: error in "remove stroke duplication" using file checker
2025-06-17 16:57:20 +02:00
pullusb 3624f5cabd Fix #9 Draw cam overlay breking after undo
4.1.2

- fixed: Error in Draw cam overlay breaking after random undo step `Ctrl + Z`

use viewlayer viewport hide instead of object hide_viewport, otherwise transform seem to not be evaluated correctly
2025-06-04 14:01:26 +02:00
pullusb 34f8193c7b option to disable move baking in gp clipboard copy operator
4.1.1

- added: option in copy stroke world space to not bake move
2025-05-06 10:49:22 +02:00
pullusb aecef47d2f Add operator to list collection visibility conflicts
4.1.0
2025-03-04 12:35:45 +01:00
pullusb cf3f39b730 Respect group lock and hide state 2024-12-16 17:29:27 +01:00
pullusb 1c39d81ef1 update links for latest gpv2 release on readmes 2024-12-03 18:29:20 +01:00
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
24 changed files with 829 additions and 299 deletions
+46
View File
@@ -1,5 +1,51 @@
# Changelog # Changelog
5.0.1
- fixed: Restored preference paths as dir path (now need `PATH_SUPPORTS_BLEND_RELATIVE` option)
5.0.0
- added: Update for blender 5+. With retro-compatibility
4.2.0
- added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched)
- No preset shortcut
<!-- - `grease_pencil.delete_points_view_bound` for points -->
<!-- - `grease_pencil.delete_strokes_view_bound` for strokes -->
- operator id_name: `grease_pencil.delete_view_bound`
- Added to Delete menu
- support multiframe
- Only do a dissolve (delete for full stroke)
4.1.3
- fixed: error in "remove stroke duplication" using file checker
- added: `Remove Redundant GP Stroke` Operator (Not exposed, avaialbe in search with developer extras enabled). Allow to use directly without going using file checker
4.1.2
- fixed: Error in Draw cam overlay breaking after random undo step `Ctrl + Z`
4.1.1
- added: option in copy stroke world space to not bake move
4.1.0
- added: operator `list collection visibility conflicts` (not exposed, called from search menu with dev extras enabled in prefs)
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 4.0.1
- fixed: layer nav operator on page up/down - fixed: layer nav operator on page up/down
+2 -1
View File
@@ -6,6 +6,7 @@ from bpy.props import (FloatProperty,
StringProperty, StringProperty,
IntProperty) IntProperty)
from .. import utils from .. import utils
from ..utils import is_hidden
## copied from OP_key_duplicate_send ## copied from OP_key_duplicate_send
def get_layer_list(self, context): def get_layer_list(self, context):
@@ -142,7 +143,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
tgt_layers = [l for i, l in enumerate(gpl) if i == active_index - 1] tgt_layers = [l for i, l in enumerate(gpl) if i == active_index - 1]
elif self.targeted_layers == 'ALL_VISIBLE': elif self.targeted_layers == 'ALL_VISIBLE':
tgt_layers = [l for l in gpl if not l.hide and l != gpl.active] tgt_layers = [l for l in gpl if not is_hidden(l) and l != gpl.active]
elif self.targeted_layers == 'CHOSEN': elif self.targeted_layers == 'CHOSEN':
if not self.layers_enum: if not self.layers_enum:
+9 -6
View File
@@ -5,11 +5,14 @@ from ..utils import (location_to_region,
vector_length, vector_length,
draw_gp_stroke, draw_gp_stroke,
extrapolate_points_by_length, extrapolate_points_by_length,
simple_draw_gp_stroke) simple_draw_gp_stroke,
is_hidden,
is_locked)
import bpy import bpy
from math import degrees from math import degrees
from mathutils import Vector from mathutils import Vector
# from os.path import join, basename, exists, dirname, abspath, splitext # from os.path import join, basename, exists, dirname, abspath, splitext
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level # iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
@@ -276,9 +279,9 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
if self.layer_tgt == 'ACTIVE': if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active] lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED': elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not l.hide] lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
elif self.layer_tgt == 'ALL_VISIBLE': elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide] lays = [l for l in ob.data.layers if not is_hidden(l)]
else: else:
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))] lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
@@ -337,9 +340,9 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
if self.layer_tgt == 'ACTIVE': if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active] lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED': elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not l.hide] lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
elif self.layer_tgt == 'ALL_VISIBLE': elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide] lays = [l for l in ob.data.layers if not is_hidden(l)]
else: else:
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))] lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
@@ -375,7 +378,7 @@ class GPSTK_OT_comma_finder(bpy.types.Operator):
def execute(self, context): def execute(self, context):
ct = 0 ct = 0
ob = context.object ob = context.object
lays = [l for l in ob.data.layers if not l.hide and not l.lock] lays = [l for l in ob.data.layers if not is_hidden(l) and not is_locked(l)]
for l in lays: for l in lays:
if not l.current_frame():continue if not l.current_frame():continue
for s in l.current_frame().drawing.strokes: for s in l.current_frame().drawing.strokes:
+4 -3
View File
@@ -3,6 +3,7 @@ import bpy
import re import re
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from math import radians, degrees from math import radians, degrees
from . import utils
# exemple for future improve: https://justinsbarrett.com/tweenmachine/ # exemple for future improve: https://justinsbarrett.com/tweenmachine/
@@ -21,7 +22,7 @@ def get_surrounding_points(fc, frame):
return p_pt, n_pt return p_pt, n_pt
## unused direct breackdown func ## unused direct breakdown func
def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale'), axe=(0,1,2)): def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale'), axe=(0,1,2)):
cf = bpy.context.scene.frame_current# use operator context (may be unsynced timeline) cf = bpy.context.scene.frame_current# use operator context (may be unsynced timeline)
axes_name = ('x', 'y', 'z') axes_name = ('x', 'y', 'z')
@@ -42,7 +43,7 @@ def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale
skipping = [] skipping = []
for fc in action.fcurves: for fc in utils.get_fcurves(obj):
# if fc.data_path.split('"')[1] in bone_names_filter:# bones # if fc.data_path.split('"')[1] in bone_names_filter:# bones
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones # if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
if fc.data_path in channels and fc.array_index in axe:# .split('.')[-1] if fc.data_path in channels and fc.array_index in axe:# .split('.')[-1]
@@ -252,7 +253,7 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
## TODO for ob in context.selected objects, need to reduce list with upper filters... ## TODO for ob in context.selected objects, need to reduce list with upper filters...
for fc in action.fcurves: for fc in utils.get_fcurves(obj):
# if fc.data_path.split('"')[1] in bone_names_filter:# bones # if fc.data_path.split('"')[1] in bone_names_filter:# bones
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones # if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
if fc.data_path in self.channels:# .split('.')[-1]# and fc.array_index in axe if fc.data_path in self.channels:# .split('.')[-1]# and fc.array_index in axe
+12 -8
View File
@@ -1,6 +1,6 @@
## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends ## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends
## View3D > Toolbar > Gpencil > GP clipboard ## View3D > Toolbar > Gpencil > GP clipboard
## in 4.2- existed in standalone scripts: https://github.com/Pullusb/GP_clipboard ## in 4.2, existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
import bpy import bpy
import mathutils import mathutils
@@ -9,6 +9,7 @@ import json
from time import time from time import time
from operator import itemgetter from operator import itemgetter
from itertools import groupby from itertools import groupby
from .utils import is_locked, is_hidden
def convertAttr(Attr): def convertAttr(Attr):
'''Convert given value to a Json serializable format''' '''Convert given value to a Json serializable format'''
@@ -139,7 +140,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
# color = gp.palettes.active.colors.active.name # color = gp.palettes.active.colors.active.name
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock] # [] layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # []
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@@ -235,7 +236,7 @@ def copy_all_strokes(layers=None):
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ? layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ?
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@@ -275,7 +276,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock] # include locked ? layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # include locked ?
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@@ -477,6 +478,9 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL' return context.object and context.object.type == 'GREASEPENCIL'
bake_moves : bpy.props.BoolProperty(name='Bake Move', default=True,
description='Copy every frame where object has moved, else copy only existing frames')
radius : bpy.props.BoolProperty(name='radius', default=True, radius : bpy.props.BoolProperty(name='radius', default=True,
description='Dump point radius attribute (already skipped if at default value)') description='Dump point radius attribute (already skipped if at default value)')
opacity : bpy.props.BoolProperty(name='opacity', default=True, opacity : bpy.props.BoolProperty(name='opacity', default=True,
@@ -501,6 +505,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
layout=self.layout layout=self.layout
layout.use_property_split = True layout.use_property_split = True
col = layout.column() col = layout.column()
col.prop(self, 'bake_moves')
col.label(text='Keep following point attributes:') col.label(text='Keep following point attributes:')
col.prop(self, 'radius') col.prop(self, 'radius')
col.prop(self, 'opacity') col.prop(self, 'opacity')
@@ -511,7 +516,6 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
return return
def execute(self, context): def execute(self, context):
bake_moves = True
skip_empty_frame = False skip_empty_frame = False
org_frame = context.scene.frame_current org_frame = context.scene.frame_current
@@ -521,15 +525,15 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
#ct = check_radius() #ct = check_radius()
layerdic = {} layerdic = {}
layerpool = [l for l in gpl if not l.hide and l.select] # and not l.lock layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l)
if not layerpool: if not layerpool:
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only') self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
return {"CANCELLED"} return {"CANCELLED"}
if not bake_moves: # copy only drawed frames as is. if not self.bake_moves: # copy only drawed frames as is.
for l in layerpool: for l in layerpool:
if not l.frames: if not l.frames:
continue# skip empty layers continue # skip empty layers
frame_dic = {} frame_dic = {}
for f in l.frames: for f in l.frames:
+25 -15
View File
@@ -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,20 +168,26 @@ 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)
def subscribe_object_change(): def subscribe_object_change():
subscribe_to = (bpy.types.LayerObjects, 'active') subscribe_to = (bpy.types.LayerObjects, 'active')
if bpy.app.version >= (5, 0, 0):
owner = bpy.types.GreasePencil
else:
owner = bpy.types.GreasePencilv3
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=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime. owner=owner, # <-- attach to ID during it's lifetime.
args=(), args=(),
notify=selection_changed, notify=selection_changed,
options={'PERSISTENT'}, options={'PERSISTENT'},
@@ -197,15 +207,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
@@ -216,4 +226,4 @@ def 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) bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
+173
View File
@@ -0,0 +1,173 @@
import bpy
from . import utils
from bpy_extras.view3d_utils import location_3d_to_region_2d
def is_stroke_in_view(stroke, matrix, region, region_3d):
## for optimization, use first and last
region_width = region.width
region_height = region.height
point_list = [stroke.points[0], stroke.points[-1]]
# point_list = stroke.points # whole points
coord_list = [matrix @ point.position for point in point_list]
for coord in coord_list:
# Convert 3D coordinates to 2D screen coordinates
screen_coord = location_3d_to_region_2d(region, region_3d, coord)
if screen_coord is None:
continue
# Check if the point is within the viewport bounds
if 0 <= screen_coord.x <= region_width and 0 <= screen_coord.y <= region_height:
## one point in view, in view
return True
return False
class GP_OT_delete_view_bound(bpy.types.Operator):
bl_idname = "grease_pencil.delete_view_bound"
bl_label = "Delete View Bound"
bl_description = "Delete all selected strokes / points only if there are in view (current viewport region)\
\nnote: does not work in multiframe yet)"
bl_options = {'REGISTER', 'UNDO'}
# def invoke(self, context, event):
## TODO: add scope (default to automatic)
## TODO: add option for normal delete (currenly only dissolve), probably another operator.
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_GREASE_PENCIL'
def execute(self, context):
gp = context.grease_pencil
if not gp:
self.report({'ERROR'}, "No Grease Pencil object found")
return {'CANCELLED'}
ob = context.object
if not ob or ob.type != 'GREASEPENCIL':
self.report({'ERROR'}, "Active object is not a Grease Pencil object")
return {'CANCELLED'}
select_mode = context.scene.tool_settings.gpencil_selectmode_edit
matrix_world = ob.matrix_world.copy()
## only visibile and unlocked layer in active GP object
layers = [l for l in gp.layers if not l.hide and not l.lock]
region = context.region
region_3d = context.space_data.region_3d
removed_ct = 0
avoid_ct = 0
for layer in layers:
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
## multiframe, affect all selected frames and active frame
target_frames = [f for f in layer.frames if f.select or f == layer.current_frame()]
else:
## Only active frame
current = layer.current_frame()
if not current:
continue
target_frames = [current]
for frame in target_frames:
drawing = frame.drawing
if not drawing or not drawing.strokes:
continue
stroke_indices_to_delete = []
## List comprehention version (For stroke mode only)
# stroke_indices_to_delete = [sidx for sidx, stroke in enumerate(drawing.strokes) if stroke.select and is_stroke_in_view(stroke, matrix_world, region, region_3d)]
for sid, stroke in enumerate(drawing.strokes):
if not stroke.select:
continue
# Check if the stroke is within the view bounds
if not is_stroke_in_view(stroke, matrix_world, region, region_3d):
avoid_ct += 1
continue
## No need for further check if we are in stroke mod (direct delete)
if select_mode == 'STROKE':
stroke_indices_to_delete.append(sid)
removed_ct += 1
continue
## --- Handle points ---
## remove points, only Dissolve for now.
select_count = len([p for p in stroke.points if p.select])
if select_count == len(stroke.points):
## Mark as full delete and go next
stroke_indices_to_delete.append(sid)
continue
## Make a copy of deselected points attributes
point_attrs = []
for p in stroke.points:
if p.select:
continue
point_attrs.append([p.position.copy(), p.rotation, p.radius, p.opacity])
## remove as many points as select_count
stroke.remove_points(select_count)
## re-assign attributes in order
for i, p in enumerate(stroke.points):
if i < len(point_attrs):
p.position = point_attrs[i][0]
p.rotation = point_attrs[i][1]
p.radius = point_attrs[i][2]
p.opacity = point_attrs[i][3]
p.select = False # Deselect all points after processing
removed_ct += 1
## End of stroke loop (here either part if the stroke is deleted or marked for deletion )
if stroke_indices_to_delete:
# print(f'layer {layer.name} : delete {len(stroke_indices_to_delete)} strokes')
drawing.remove_strokes(indices=stroke_indices_to_delete)
if removed_ct and avoid_ct:
self.report({'INFO'}, f"Skipped {avoid_ct} out of view")
elif not removed_ct and not avoid_ct:
self.report({'WARNING'}, "Nothing to Delete")
elif not removed_ct and avoid_ct:
self.report({'WARNING'}, "All selected strokes are out of view")
return {'FINISHED'}
def draw_delete_view_bound_ui(self, context):
layout = self.layout
layout.operator('grease_pencil.delete_view_bound', text='Delete View bound', icon='LOCKVIEW_ON') # RESTRICT_VIEW_ON
classes = (
# GP_OT_delete_strokes_view_bound,
# GP_OT_delete_points_view_bound,
GP_OT_delete_view_bound,
)
def register():
if bpy.app.background:
return
for cl in classes:
bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_edit_greasepencil_delete.append(draw_delete_view_bound_ui)
## make scene property for empty key preservation and bake movement for layers...
# register_keymaps()
def unregister():
if bpy.app.background:
return
bpy.types.VIEW3D_MT_edit_greasepencil_delete.remove(draw_delete_view_bound_ui)
# unregister_keymaps()
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
+2 -1
View File
@@ -11,6 +11,7 @@ from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vecto
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
from time import time from time import time
from math import pi, cos, sin from math import pi, cos, sin
from .utils import is_locked, is_hidden
def get_gp_mat(gp, name, set_active=False): def get_gp_mat(gp, name, set_active=False):
@@ -440,7 +441,7 @@ class GPTB_OT_eraser(Operator):
t0 = time() t0 = time()
gp_mats = gp.data.materials gp_mats = gp.data.materials
gp_layers = [l for l in gp.data.layers if not l.lock or l.hide] gp_layers = [l for l in gp.data.layers if not is_locked(l) or is_hidden(l)]
self.gp_frames = [l.current_frame() for l in gp_layers] self.gp_frames = [l.current_frame() for l in gp_layers]
''' '''
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes] points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes]
+336 -81
View File
@@ -2,32 +2,94 @@ import bpy
import os import os
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from . import utils from . import utils
def remove_stroke_exact_duplications(apply=True): from bpy.props import (BoolProperty,
PointerProperty,
CollectionProperty,
StringProperty)
def remove_stroke_exact_duplications(apply=True, verbose=True, select=False):
'''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
:select: Select the duplicated strokes instead of removing them (disabled by apply)
return number of duplication found/deleted return number of duplication found/deleted
''' '''
# TODO: add additional check of material (even if unlikely to happen) # TODO: Add additional check of material (even if unlikely to happen, better to avoid false positive)
ct = 0 ct = 0
gp_datas = [gp for gp in bpy.data.grease_pencils] if verbose:
print('\nRemove redundant strokes in GP frames...')
gp_source = bpy.data.grease_pencils if bpy.app.version >= (5,0,0) else bpy.data.grease_pencils_v3
gp_datas = [gp for gp in gp_source]
for gp in gp_datas: for gp in gp_datas:
for l in gp.layers: for l in gp.layers:
for f in l.frames: for f in l.frames:
stroke_list = [] stroke_list = []
for s in reversed(f.drawing.strokes): idx_to_delete = []
point_list = [p.position for p in s.points]
for idx, s in enumerate(f.drawing.strokes):
point_list = [p.position.copy() for p in s.points]
if point_list in stroke_list: if point_list in stroke_list:
ct += 1 ct += 1
if apply: idx_to_delete.append(idx)
# Remove redundancy if not apply and select:
f.drawing.strokes.remove(s) s.select = True
else: else:
stroke_list.append(point_list) stroke_list.append(point_list)
if not apply and select:
s.select = False
if idx_to_delete:
if verbose:
print(f"{gp.name} > {l.name} > frame {f.frame_number}: {len(idx_to_delete)} strokes")
if apply:
# Remove redundancy (carefull, passing an empty list delete all strokes)
f.drawing.remove_strokes(indices=idx_to_delete)
return ct return ct
class GPTB_OT_remove_stroke_duplication(bpy.types.Operator):
bl_idname = "gp.remove_stroke_duplication"
bl_label = "Remove Redundant GP Stroke"
bl_description = "Within every frame, remove every strokes that are exactly superposed with a previous one"
bl_options = {"REGISTER", "UNDO"}
apply : bpy.props.BoolProperty(name="Remove Stroke", default=True,
description="Remove the duplication, else list only",
options={'SKIP_SAVE'})
select : bpy.props.BoolProperty(name="Select Duplicated Strokes", default=False)
def invoke(self, context, event):
# self.file_dump = event.ctrl
return context.window_manager.invoke_props_dialog(self) # , width=400
# return self.execute(context)
def draw(self, context):
layout=self.layout
layout.use_property_split = True
col = layout.column()
col.prop(self, 'apply')
row=col.row(align=True)
row.prop(self, 'select')
row.enabled = not self.apply
if not self.apply and not self.select:
col.label(text="Only list in console and report number of duplications", icon='INFO')
def execute(self, context):
ct = remove_stroke_exact_duplications(apply=self.apply, select=self.select, verbose=True)
if ct > 0:
if self.apply:
self.report({'INFO'}, f'Removed {ct} strokes duplications')
else:
self.report({'INFO'}, f'Found {ct} strokes duplications')
else:
self.report({'INFO'}, 'No stroke duplication found')
return {'FINISHED'}
class GPTB_OT_file_checker(bpy.types.Operator): class GPTB_OT_file_checker(bpy.types.Operator):
bl_idname = "gp.file_checker" bl_idname = "gp.file_checker"
bl_label = "Check File" bl_label = "Check File"
@@ -53,6 +115,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 +129,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:
@@ -153,29 +222,32 @@ class GPTB_OT_file_checker(bpy.types.Operator):
o.use_grease_pencil_lights = False o.use_grease_pencil_lights = False
## Disabled animation ## Disabled animation
if fix.list_disabled_anim: # TODO : fix for Blender 5.0
fcu_ct = 0 if bpy.app.version < (5,0,0):
for act in bpy.data.actions: if fix.list_disabled_anim:
if not act.users: fcu_ct = 0
continue for act in bpy.data.actions:
for fcu in act.fcurves: if not act.users:
if fcu.mute: continue
fcu_ct += 1 for fcu in act.fcurves:
print(f"muted: {act.name} > {fcu.data_path}") if fcu.mute:
if fcu_ct: fcu_ct += 1
problems.append(f'{fcu_ct} anim channel disabled (details in console)') print(f"muted: {act.name} > {fcu.data_path}")
if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
## Object visibility conflict ## Object visibility conflict
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:
@@ -253,6 +325,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
bpy.context.scene.tool_settings.lock_object_mode = False bpy.context.scene.tool_settings.lock_object_mode = False
if fix.remove_redundant_strokes: if fix.remove_redundant_strokes:
print('removing redundant strokes')
ct = remove_stroke_exact_duplications(apply=apply) ct = remove_stroke_exact_duplications(apply=apply)
if ct > 0: if ct > 0:
mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications' mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications'
@@ -260,7 +333,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# ## Set onion skin filter to 'All type' # ## Set onion skin filter to 'All type'
# fix_kf_type = 0 # fix_kf_type = 0
# for gp in bpy.data.grease_pencils:#from data # for gp in bpy.data.grease_pencils: #from data
# if not gp.is_annotation: # if not gp.is_annotation:
# if gp.onion_keyframe_type != 'ALL': # if gp.onion_keyframe_type != 'ALL':
# gp.onion_keyframe_type = 'ALL' # gp.onion_keyframe_type = 'ALL'
@@ -281,8 +354,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 +559,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 +581,225 @@ 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'}
## -- List collection visibility conflicts
# class GPTB_PG_collection_visibility(bpy.types.PropertyGroup):
# """Property group to handle collection visibility"""
# is_hidden: BoolProperty(
# name="Hide in Viewport",
# description="Toggle collection visibility in viewport",
# get=lambda self: self.get("is_hidden", False),
# set=lambda self, value: self.set_visibility(value)
# )
# collection_name: StringProperty(name="Collection Name")
def get_collection_children_recursive(col, cols=None) -> list:
'''return a list of all the child collections
and their subcollections in the passed collection'''
cols = cols or []
for sub in col.children:
if sub not in cols:
cols.append(sub)
if len(sub.children):
cols = get_collection_children_recursive(sub, cols)
return cols
class GPTB_OT_list_collection_visibility_conflicts(bpy.types.Operator):
bl_idname = "gp.list_collection_visibility_conflicts"
bl_label = "List Collection Visibility Conflicts"
bl_description = "List collection visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
# visibility_items: CollectionProperty(type=GPTB_PG_collection_visibility)
show_filter : bpy.props.EnumProperty(
name="View Filter",
description="Filter collections based on their exclusion status",
items=(
('ALL', "All", "Show all collections", 0),
('NOT_EXCLUDED', "Not Excluded", "Show collections that are not excluded", 1),
('EXCLUDED', "Excluded", "Show collections that are excluded", 2)
),
default='NOT_EXCLUDED')
def invoke(self, context, event):
## get all viewlayer collections
vcols = get_collection_children_recursive(context.view_layer.layer_collection)
vcols = list(set(vcols)) # ensure no duplicates
## Store collection with conflicts
# layer_collection.is_visible against render visibility ?
## Do not list currently excluded collections
self.conflict_collections = [vc for vc in vcols if not (vc.hide_viewport == vc.collection.hide_viewport == vc.collection.hide_render)]
self.included_collection = [vc for vc in self.conflict_collections if not vc.exclude]
self.excluded_collection = [vc for vc in self.conflict_collections if vc.exclude]
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
layout.prop(self, 'show_filter', expand=True)
# Add sync buttons at the top
row = layout.row(align=False)
# TODO: Add "set all from" ops on collection (optionnal)
# 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()
if self.show_filter == 'ALL':
vl_collections = self.conflict_collections
elif self.show_filter == 'EXCLUDED':
vl_collections = self.excluded_collection
elif self.show_filter == 'NOT_EXCLUDED':
vl_collections = self.included_collection
col = layout.column()
for vlcol in vl_collections:
row = col.row(align=False)
row.label(text=vlcol.name)
# Viewlayer collection settings
row.prop(vlcol, "exclude", text="", emboss=False)
row.prop(vlcol, "hide_viewport", text="", emboss=False)
# Direct collection properties
row.prop(vlcol.collection, 'hide_viewport', text='', emboss=False)
row.prop(vlcol.collection, '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,12 +841,20 @@ 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_collection_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,
GPTB_OT_file_checker, GPTB_OT_file_checker,
GPTB_OT_links_checker, GPTB_OT_links_checker,
GPTB_OT_remove_stroke_duplication,
) )
def register(): def register():
+1 -1
View File
@@ -12,7 +12,7 @@ from .utils import (location_to_region, region_to_location)
## Do not work on multiple object ## Do not work on multiple object
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False): def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject '''Reproject
:all_stroke: affect hided, locked layers :all_stroke: affect hidden, locked layers
''' '''
if restore_frame: if restore_frame:
-1
View File
@@ -2,7 +2,6 @@ import bpy
from mathutils import Vector from mathutils import Vector
from . import utils from . import utils
class GPTB_OT_create_follow_path_curve(bpy.types.Operator): class GPTB_OT_create_follow_path_curve(bpy.types.Operator):
bl_idname = "object.create_follow_path_curve" bl_idname = "object.create_follow_path_curve"
bl_label = "Create Follow Path Curve" bl_label = "Create Follow Path Curve"
+33 -36
View File
@@ -212,9 +212,14 @@ class GPTB_OT_draw_cam(Operator):
# Swap to it, unhide if necessary and hide previous # Swap to it, unhide if necessary and hide previous
context.scene.camera = maincam context.scene.camera = maincam
## hide cam object ## Hide cam object
drawcam.hide_viewport = True
# Ensure at lviewport viz is active to ensure visibility and refresh state
maincam.hide_viewport = False maincam.hide_viewport = False
# drawcam.hide_viewport = False # Not absolutely needed
drawcam.hide_set(True)
maincam.hide_set(False)
## if in main camera GO to drawcam ## if in main camera GO to drawcam
elif context.scene.camera.name not in ('draw_cam', 'obj_cam'): elif context.scene.camera.name not in ('draw_cam', 'obj_cam'):
@@ -267,8 +272,12 @@ class GPTB_OT_draw_cam(Operator):
## hide cam object ## hide cam object
context.scene.camera = drawcam context.scene.camera = drawcam
# Ensure viewport viz is active to ensure visibility and refresh state
drawcam.hide_viewport = False drawcam.hide_viewport = False
maincam.hide_viewport = True maincam.hide_viewport = False
## Set viewlayer visibility
drawcam.hide_set(False)
maincam.hide_set(True)
if created and drawcam.name == 'obj_cam': # Go in camera view if created and drawcam.name == 'obj_cam': # Go in camera view
context.region_data.view_perspective = 'CAMERA' context.region_data.view_perspective = 'CAMERA'
@@ -412,12 +421,12 @@ class GPTB_OT_toggle_mute_animation(Operator):
self.selection = event.shift self.selection = event.shift
return self.execute(context) return self.execute(context)
def set_action_mute(self, act): def set_action_mute(self, bag):
for i, fcu in enumerate(act.fcurves): for i, fcu in enumerate(bag.fcurves):
print(i, fcu.data_path, fcu.array_index) print(i, fcu.data_path, fcu.array_index)
# fcu.group don't have mute attribute in api. # fcu.group don't have mute attribute in api.
fcu.mute = self.mute fcu.mute = self.mute
for g in act.groups: for g in bag.groups:
g.mute = self.mute g.mute = self.mute
def execute(self, context): def execute(self, context):
@@ -434,22 +443,15 @@ class GPTB_OT_toggle_mute_animation(Operator):
if self.mode == 'CAMERA' and o.type != 'CAMERA': if self.mode == 'CAMERA' and o.type != 'CAMERA':
continue continue
# mute attribute animation for GP and cameras ## Mute attribute animation for GP and cameras
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data: if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
gp_act = o.data.animation_data.action if data_channelbag := utils.get_active_channelbag(o.data):
if gp_act:
print(f'\n---{o.name} data:') print(f'\n---{o.name} data:')
self.set_action_mute(gp_act) self.set_action_mute(data_channelbag)
if not o.animation_data:
continue
act = o.animation_data.action
if not act:
continue
print(f'\n---{o.name}:')
self.set_action_mute(act)
if object_channelbag := utils.get_active_channelbag(o):
print(f'\n---{o.name}:')
self.set_action_mute(object_channelbag)
return {'FINISHED'} return {'FINISHED'}
@@ -486,7 +488,7 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
class GPTB_OT_list_disabled_anims(Operator): class GPTB_OT_list_disabled_anims(Operator):
bl_idname = "gp.list_disabled_anims" bl_idname = "gp.list_disabled_anims"
bl_label = "List Disabled Anims" bl_label = "List Disabled Anims"
bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)" bl_description = "List disabled animations channels in scene. (shit+clic to list only on selection)"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=False) mute : bpy.props.BoolProperty(default=False)
@@ -512,27 +514,22 @@ class GPTB_OT_list_disabled_anims(Operator):
# continue # continue
if o.type == 'GREASEPENCIL': if o.type == 'GREASEPENCIL':
if o.data.animation_data: if fcurves := utils.get_fcurves(o.data):
gp_act = o.data.animation_data.action for i, fcu in enumerate(fcurves):
if gp_act: if fcu.mute:
for i, fcu in enumerate(gp_act.fcurves): if o not in oblist:
if fcu.mute: oblist.append(o)
if o not in oblist: li.append(f'{o.name}:')
oblist.append(o) li.append(f' - {fcu.data_path} {fcu.array_index}')
li.append(f'{o.name}:')
li.append(f' - {fcu.data_path} {fcu.array_index}')
if not o.animation_data: bag = utils.get_active_channelbag(o)
if not bag:
continue continue
act = o.animation_data.action for g in bag.groups:
if not act:
continue
for g in act.groups:
if g.mute: if g.mute:
li.append(f'{o.name} - group: {g.name}') li.append(f'{o.name} - group: {g.name}')
for i, fcu in enumerate(act.fcurves): for i, fcu in enumerate(bag.fcurves):
# print(i, fcu.data_path, fcu.array_index) # print(i, fcu.data_path, fcu.array_index)
if fcu.mute: if fcu.mute:
if o not in oblist: if o not in oblist:
+4 -4
View File
@@ -1,5 +1,5 @@
import bpy import bpy
from .utils import get_addon_prefs from .utils import get_addon_prefs, is_locked, is_hidden
from bpy.props import BoolProperty ,EnumProperty ,StringProperty from bpy.props import BoolProperty ,EnumProperty ,StringProperty
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator): class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
@@ -45,15 +45,15 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
return {"CANCELLED"} return {"CANCELLED"}
if self.target == 'ACTIVE': if self.target == 'ACTIVE':
gpl = [l for l in context.object.data.layers if l.select and not l.hide] gpl = [l for l in context.object.data.layers if l.select and not is_hidden(l)]
if not context.object.data.layers.active in gpl: if not context.object.data.layers.active in gpl:
gpl.append(context.object.data.layers.active) gpl.append(context.object.data.layers.active)
elif self.target == 'VISIBLE': elif self.target == 'VISIBLE':
gpl = [l for l in context.object.data.layers if not l.hide] gpl = [l for l in context.object.data.layers if not is_hidden(l)]
elif self.target == 'ACCESSIBLE': elif self.target == 'ACCESSIBLE':
gpl = [l for l in context.object.data.layers if not l.hide and not l.lock] gpl = [l for l in context.object.data.layers if not is_hidden(l) and not is_locked(l)]
if self.keyframe_type != 'NONE': if self.keyframe_type != 'NONE':
# use shortcut choice override # use shortcut choice override
+6 -2
View File
@@ -624,11 +624,15 @@ def obj_layer_name_callback():
def subscribe_layer_change(): def subscribe_layer_change():
subscribe_to = (bpy.types.GreasePencilv3Layers, "active") subscribe_to = (bpy.types.GreasePencilv3Layers, "active")
if bpy.app.version >= (5, 0, 0):
owner = bpy.types.GreasePencil
else:
owner = bpy.types.GreasePencilv3
bpy.msgbus.subscribe_rna( bpy.msgbus.subscribe_rna(
key=subscribe_to, key=subscribe_to,
# owner of msgbus subcribe (for clearing later) # owner of msgbus subcribe (for clearing later)
# owner=handle, # owner=handle,
owner=bpy.types.GreasePencilv3, # <-- can attach to an ID during all it's lifetime... owner=owner, # <-- 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
@@ -783,4 +787,4 @@ def unregister():
# Delete layer index trigger # Delete layer index trigger
# /!\ can remove msgbus made for other functions or other addons using same owner # /!\ can remove msgbus made for other functions or other addons using same owner
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3) bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
+4 -4
View File
@@ -4,7 +4,7 @@ import mathutils
from mathutils import Vector, Matrix, geometry from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from time import time from time import time
from .utils import get_gp_draw_plane, location_to_region, region_to_location from .utils import get_gp_draw_plane, location_to_region, region_to_location, is_locked, is_hidden
class GP_OT_pick_closest_layer(Operator): class GP_OT_pick_closest_layer(Operator):
bl_idname = "gp.pick_closest_layer" bl_idname = "gp.pick_closest_layer"
@@ -76,7 +76,7 @@ class GP_OT_pick_closest_layer(Operator):
self.point_pair = [] self.point_pair = []
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for layer in gp.layers: for layer in gp.layers:
if layer.hide: if is_hidden(layer):
continue continue
for f in layer.frames: for f in layer.frames:
if not f.select: if not f.select:
@@ -89,9 +89,9 @@ class GP_OT_pick_closest_layer(Operator):
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points] self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
else: else:
# [s for l in gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes] # [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for layer in gp.layers: for layer in gp.layers:
if layer.hide or not layer.current_frame(): if is_hidden(layer) or not layer.current_frame():
continue continue
for s in layer.current_frame().drawing.strokes: for s in layer.current_frame().drawing.strokes:
if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke: if self.stroke_filter == 'STROKE' and not self.ob.data.materials[s.material_index].grease_pencil.show_stroke:
+12 -7
View File
@@ -4,7 +4,12 @@ import mathutils
from mathutils import Vector, Matrix, geometry from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from time import time from time import time
from .utils import get_gp_draw_plane, location_to_region, region_to_location from .utils import (get_gp_draw_plane,
location_to_region,
region_to_location,
is_locked,
is_hidden)
### passing by 2D projection ### passing by 2D projection
def get_3d_coord_on_drawing_plane_from_2d(context, co): def get_3d_coord_on_drawing_plane_from_2d(context, co):
@@ -79,7 +84,7 @@ class GP_OT_pick_closest_material(Operator):
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for l in self.gp.layers: for l in self.gp.layers:
if l.hide:# l.lock or if is_hidden(l):# is_locked(l) or
continue continue
for f in l.frames: for f in l.frames:
if not f.select: if not f.select:
@@ -88,9 +93,9 @@ class GP_OT_pick_closest_material(Operator):
self.stroke_list.append(s) self.stroke_list.append(s)
else: else:
# [s for l in self.gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes] # [s for l in self.gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for l in self.gp.layers: for l in self.gp.layers:
if l.hide or not l.current_frame():# l.lock or if is_hidden(l) or not l.current_frame():# is_locked(l) or
continue continue
for s in l.current_frame().drawing.strokes: for s in l.current_frame().drawing.strokes:
self.stroke_list.append(s) self.stroke_list.append(s)
@@ -235,7 +240,7 @@ class GP_OT_pick_closest_material(Operator):
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for l in gp.layers: for l in gp.layers:
if l.hide:# l.lock or if is_hidden(l):# is_locked(l) or
continue continue
for f in l.frames: for f in l.frames:
if not f.select: if not f.select:
@@ -244,9 +249,9 @@ class GP_OT_pick_closest_material(Operator):
self.stroke_list.append(s) self.stroke_list.append(s)
else: else:
# [s for l in gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes] # [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for l in gp.layers: for l in gp.layers:
if l.hide or not l.current_frame():# l.lock or if is_hidden(l) or not l.current_frame():# is_locked(l) or
continue continue
for s in l.current_frame().drawing.strokes: for s in l.current_frame().drawing.strokes:
self.stroke_list.append(s) self.stroke_list.append(s)
+2 -2
View File
@@ -165,9 +165,9 @@ class GPTB_OT_import_obj_palette(Operator):
# self.report({'WARNING'}, 'All materials are already in other selected object') # self.report({'WARNING'}, 'All materials are already in other selected object')
# unlink objects and their gp data # unlink objects and their gp data
data_source = bpy.data.grease_pencils if bpy.app.version >= (5, 0, 0) else bpy.data.grease_pencils_v3
for src_ob in linked_objs: for src_ob in linked_objs:
bpy.data.grease_pencils.remove(src_ob.data) data_source.remove(src_ob.data)
return {"FINISHED"} return {"FINISHED"}
+13 -40
View File
@@ -1,11 +1,13 @@
import bpy import bpy
import mathutils import mathutils
import numpy as np
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
from math import pi from math import pi
import numpy as np
from time import time from time import time
from . import utils
from mathutils.geometry import intersect_line_plane from mathutils.geometry import intersect_line_plane
from . import utils
from .utils import is_hidden, is_locked
def get_scale_matrix(scale): def get_scale_matrix(scale):
# recreate a neutral mat scale # recreate a neutral mat scale
@@ -15,35 +17,6 @@ def get_scale_matrix(scale):
matscale = matscale_x @ matscale_y @ matscale_z matscale = matscale_x @ matscale_y @ matscale_z
return matscale return matscale
'''
## Old reproject method using Operators:
omode = bpy.context.mode
if all_strokes:
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
for l in obj.data.layers:
l.hide = False
l.lock = False
l.lock_frame = False
bpy.ops.object.mode_set(mode='EDIT')
for fnum in frame_list:
bpy.context.scene.frame_current = fnum
bpy.ops.gpencil.select_all(action='SELECT')
bpy.ops.gpencil.reproject(type=proj_type) # 'INVOKE_DEFAULT'
bpy.ops.gpencil.select_all(action='DESELECT')
# restore
if all_strokes:
for layer, hide, lock, lock_frame in layers_state:
layer.hide = hide
layer.lock = lock
layer.lock_frame = lock_frame
bpy.ops.object.mode_set(mode=omode)
'''
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False): def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject - ops method '''Reproject - ops method
:all_stroke: affect hidden, locked layers :all_stroke: affect hidden, locked layers
@@ -73,7 +46,7 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
if not all_strokes: if not all_strokes:
if not layer.select: if not layer.select:
continue continue
if layer.hide or layer.lock: if is_hidden(layer) or is_locked(layer):
continue continue
frame = next((f for f in layer.frames if f.frame_number == i), None) frame = next((f for f in layer.frames if f.frame_number == i), None)
@@ -185,10 +158,10 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler' chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler'
## double list keys ## double list keys
rot_keys = [int(k.co.x) for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel] rot_keys = [int(k.co.x) for fcu in utils.get_fcurves(o) for k in fcu.keyframe_points if fcu.data_path == chanel]
## normal iter ## normal iter
# for fcu in o.animation_data.action.fcurves: # for fcu in utils.get_fcurves(o):
# if fcu.data_path != chanel : # if fcu.data_path != chanel :
# continue # continue
# for k in fcu.keyframe_points(): # for k in fcu.keyframe_points():
@@ -295,12 +268,12 @@ class GPTB_OT_realign(bpy.types.Operator):
self.alert = '' self.alert = ''
o = context.object o = context.object
if o.animation_data and o.animation_data.action: fcurves = utils.get_fcurves(o)
act = o.animation_data.action if fcurves:
for chan in ('rotation_euler', 'rotation_quaternion'): for chan in ('rotation_euler', 'rotation_quaternion'):
if act.fcurves.find(chan): if fcurves.find(chan):
self.alert = 'Animated Rotation (CONSTANT interpolation)' self.alert = 'Animated Rotation (CONSTANT interpolation)'
interpos = [p for fcu in act.fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT'] interpos = [p for fcu in fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT']
if interpos: if interpos:
self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)' self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)'
break break
@@ -350,8 +323,8 @@ class GPTB_OT_realign(bpy.types.Operator):
oframe = context.scene.frame_current oframe = context.scene.frame_current
o = bpy.context.object o = bpy.context.object
if o.animation_data and o.animation_data.action: if fcurves := utils.get_fcurves(o):
if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'): if fcurves.find('rotation_euler') or fcurves.find('rotation_quaternion'):
align_all_frames(reproject=self.reproject) align_all_frames(reproject=self.reproject)
print(f'\nAnim realign ({time()-t0:.2f}s)') print(f'\nAnim realign ({time()-t0:.2f}s)')
self.exit(context, oframe) self.exit(context, oframe)
+1 -1
View File
@@ -6,7 +6,7 @@ Blender addon - Various tool to help with grease pencil in animation productions
**[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)** **[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Download for Blender 4.2 and below](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/v3.3.0.zip)** **[Download for Blender 4.2 and below from release page](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+1 -1
View File
@@ -4,7 +4,7 @@ Blender addon - Boîte à outils de grease pencil pour la production d'animation
**[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)** **[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Télécharger pour Blender 4.2 ou inférieure](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/v3.3.0.zip)** **[Téléchargement pour Blender 4.2 ou inférieur depuis la page des releases](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+17 -4
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):
@@ -769,9 +771,15 @@ GPTB_PT_palettes_list_ui, # subpanels
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
if bpy.app.version >= (5,0,0):
bpy.types.GREASE_PENCIL_MT_material_context_menu.append(palette_manager_menu)
# bpy.types.GREASE_PENCIL_MT_material_context_menu.append(palette_manager_menu)
else:
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref) bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref)
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref) # bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP # bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
@@ -784,9 +792,14 @@ def unregister():
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP # bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref) bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref)
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu) if bpy.app.version >= (5,0,0):
bpy.types.GREASE_PENCIL_MT_material_context_menu.remove(palette_manager_menu)
# bpy.types.GREASE_PENCIL_MT_material_context_menu.remove(palette_manager_menu)
else:
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref) # bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# if bpy.app.version >= (3,0,0): # if bpy.app.version >= (3,0,0):
+45 -34
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, 2), "version": (5, 0, 1),
"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": "",
@@ -47,6 +47,7 @@ from . import OP_layer_namespace
from . import OP_pseudo_tint from . import OP_pseudo_tint
from . import OP_follow_curve from . import OP_follow_curve
from . import OP_material_move_to_layer from . import OP_material_move_to_layer
from . import OP_delete_viewbound
# from . import OP_eraser_brush # from . import OP_eraser_brush
# from . import TOOL_eraser_brush # from . import TOOL_eraser_brush
from . import handler_draw_cam from . import handler_draw_cam
@@ -159,23 +160,6 @@ class GPTB_prefs(bpy.types.AddonPreferences):
) )
## output settings for automated renders ## output settings for automated renders
output_parent_level = IntProperty(
name='Parent level',
description="Go up in folder to define a render path relative to the file in upper directotys",
default=0,
min=0,
max=20
)
output_path : StringProperty(
name="Output path",
description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH')
playblast_path : StringProperty(
name="Playblast Path",
description="Path to folder for playblasts output",
default="//playblast", maxlen=0, subtype='DIR_PATH')
use_env_palettes : BoolProperty( use_env_palettes : BoolProperty(
name="Use Project Palettes", name="Use Project Palettes",
@@ -205,10 +189,38 @@ class GPTB_prefs(bpy.types.AddonPreferences):
default=True, default=True,
) )
brush_path : StringProperty( if bpy.app.version >= (5, 0, 0):
name="Brushes directory", # DIR_PATH type do not accept relative ("//") without PATH_SUPPORTS_BLEND_RELATIVE since blender 5
description="Path to brushes containing the blends holding the brushes", brush_path : StringProperty(
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path name="Brushes directory",
description="Path to brushes containing the blends holding the brushes",
default="//", maxlen=0, subtype='DIR_PATH', options={'PATH_SUPPORTS_BLEND_RELATIVE'})
output_path : StringProperty(
name="Output path",
description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH', options={'PATH_SUPPORTS_BLEND_RELATIVE'})
playblast_path : StringProperty(
name="Playblast Path",
description="Path to folder for playblasts output",
default="//playblast", maxlen=0, subtype='DIR_PATH', options={'PATH_SUPPORTS_BLEND_RELATIVE'})
else:
brush_path : StringProperty(
name="Brushes directory",
description="Path to brushes containing the blends holding the brushes",
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
output_path : StringProperty(
name="Output path",
description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH')
playblast_path : StringProperty(
name="Playblast Path",
description="Path to folder for playblasts output",
default="//playblast", maxlen=0, subtype='DIR_PATH')
## namespace ## namespace
separator : StringProperty( separator : StringProperty(
@@ -278,7 +290,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
) )
## KF jumper ## KF jumper
kfj_use_shortcut: BoolProperty( kfj_use_shortcut : BoolProperty(
name = "Use Keyframe Jump Shortcut", name = "Use Keyframe Jump Shortcut",
description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)", description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)",
default = True) default = True)
@@ -288,17 +300,17 @@ class GPTB_prefs(bpy.types.AddonPreferences):
description="Shortcut to trigger previous keyframe jump", description="Shortcut to trigger previous keyframe jump",
default="F5") default="F5")
kfj_prev_shift: BoolProperty( kfj_prev_shift : BoolProperty(
name = "Shift", name = "Shift",
description = "add shift", description = "add shift",
default = False) default = False)
kfj_prev_alt: BoolProperty( kfj_prev_alt : BoolProperty(
name = "Alt", name = "Alt",
description = "add alt", description = "add alt",
default = False) default = False)
kfj_prev_ctrl: BoolProperty( kfj_prev_ctrl : BoolProperty(
name = "combine with ctrl", name = "combine with ctrl",
description = "add ctrl", description = "add ctrl",
default = False) default = False)
@@ -308,17 +320,17 @@ class GPTB_prefs(bpy.types.AddonPreferences):
description="Shortcut to trigger keyframe jump", description="Shortcut to trigger keyframe jump",
default="F6") default="F6")
kfj_next_shift: BoolProperty( kfj_next_shift : BoolProperty(
name = "Shift", name = "Shift",
description = "add shift", description = "add shift",
default = False) default = False)
kfj_next_alt: BoolProperty( kfj_next_alt : BoolProperty(
name = "Alt", name = "Alt",
description = "add alt", description = "add alt",
default = False) default = False)
kfj_next_ctrl: BoolProperty( kfj_next_ctrl : BoolProperty(
name = "combine with ctrl", name = "combine with ctrl",
description = "add ctrl", description = "add ctrl",
default = False) default = False)
@@ -625,9 +637,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})')
@@ -703,8 +715,6 @@ def set_namespace_env(name_env, prop_group):
n.is_project = n.tag in project_pfix n.is_project = n.tag in project_pfix
def set_env_properties(): def set_env_properties():
prefs = get_addon_prefs() prefs = get_addon_prefs()
fps = os.getenv('FPS') fps = os.getenv('FPS')
@@ -805,6 +815,7 @@ addon_modules = (
OP_layer_nav, OP_layer_nav,
OP_follow_curve, OP_follow_curve,
OP_material_move_to_layer, OP_material_move_to_layer,
OP_delete_viewbound,
# OP_eraser_brush, # OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush # TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam, handler_draw_cam,
+11 -8
View File
@@ -11,10 +11,12 @@ from bpy.props import (
from .OP_cursor_snap_canvas import cursor_follow_update from .OP_cursor_snap_canvas import cursor_follow_update
from .OP_layer_manager import layer_name_build from .OP_layer_manager import layer_name_build
def change_edit_lines_opacity(self, context):
for gp in bpy.data.grease_pencils: ## Obsolete: Gpv3 has no edit line color anymore
if not gp.is_annotation: # def change_edit_lines_opacity(self, context):
gp.edit_line_color[3]=self.edit_lines_opacity # for gp in bpy.data.grease_pencils:
# if not gp.is_annotation:
# gp.edit_line_color[3]=self.edit_lines_opacity
def update_layer_name(self, context): def update_layer_name(self, context):
@@ -31,10 +33,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 +180,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",
+63 -32
View File
@@ -144,7 +144,7 @@ def object_derived_get(ob, scene):
# ----------------- # -----------------
### Bmesh # region Bmesh
# ----------------- # -----------------
def link_vert(v,ordered_vert) : def link_vert(v,ordered_vert) :
@@ -219,7 +219,7 @@ def gp_stroke_to_bmesh(strokes):
# ----------------- # -----------------
### GP Drawing # region GP Drawing
# ----------------- # -----------------
def layer_active_index(gpl): def layer_active_index(gpl):
@@ -393,7 +393,7 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
return rightMin + (valueScaled * rightSpan) return rightMin + (valueScaled * rightSpan)
# ----------------- # -----------------
### GP funcs # region GP funcs
# ----------------- # -----------------
def get_gp_draw_plane(obj=None, orient=None): def get_gp_draw_plane(obj=None, orient=None):
@@ -753,7 +753,7 @@ def copy_frame_at(source_frame, layer, frame_number):
# print(f"frame copy execution time: {frame_copy_end - frame_copy_start} seconds") # time_dbg # print(f"frame copy execution time: {frame_copy_end - frame_copy_start} seconds") # time_dbg
# ----------------- # -----------------
### Vector utils 3d # region Vector utils 3d
# ----------------- # -----------------
def matrix_transform(coords, matrix): def matrix_transform(coords, matrix):
@@ -814,7 +814,7 @@ def extrapolate_points_by_length(a,b, length):
return b + (ab.normalized() * length) return b + (ab.normalized() * length)
# ----------------- # -----------------
### Vector utils 2d # region Vector utils 2d
# ----------------- # -----------------
@@ -867,7 +867,7 @@ def midpoint_2d(p1, p2):
# ----------------- # -----------------
### Collection management # region Collection management
# ----------------- # -----------------
def set_collection(ob, collection, unlink=True) : def set_collection(ob, collection, unlink=True) :
@@ -903,7 +903,7 @@ def set_collection(ob, collection, unlink=True) :
# ----------------- # -----------------
### Path utils # region Path utils
# ----------------- # -----------------
def get_addon_prefs(): def get_addon_prefs():
@@ -1011,34 +1011,44 @@ 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]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
# ----------------- # -----------------
### UI utils # region UI utils
# ----------------- # -----------------
def refresh_areas(): def refresh_areas():
@@ -1154,7 +1164,7 @@ def draw_kmi(km, kmi, layout):
# layout.context_pointer_set("keymap", km) # layout.context_pointer_set("keymap", km)
# ----------------- # -----------------
### linking utility # region linking utility
# ----------------- # -----------------
def link_objects_in_blend(filepath, obj_name_list, link=True): def link_objects_in_blend(filepath, obj_name_list, link=True):
@@ -1182,7 +1192,7 @@ def check_objects_in_blend(filepath, avoid_camera=True):
# ----------------- # -----------------
### props handling # region props handling
# ----------------- # -----------------
def iterate_selector(zone, attr, state, info_attr = None, active_access='active'): def iterate_selector(zone, attr, state, info_attr = None, active_access='active'):
@@ -1246,7 +1256,7 @@ def iterate_active_layer(gpd, state):
# return info, bottom # return info, bottom
# ----------------- # -----------------
### Curve handle # region Curve handle
# ----------------- # -----------------
def create_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None): def create_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None):
@@ -1394,7 +1404,7 @@ def create_follow_path_constraint(ob, curve, follow_curve=False, use_fixed_locat
# ----------------- # -----------------
### Object # region Object
# ----------------- # -----------------
def go_edit_mode(ob, context=None): def go_edit_mode(ob, context=None):
@@ -1475,6 +1485,30 @@ def has_fully_enabled_anim(o):
return False return False
return True return True
def get_fcurves(obj):
'''return f-curves of object's active action'''
if bpy.app.version >= (5, 0, 0):
# action slot
if obj.animation_data and obj.animation_data.action_slot:
active_slot = obj.animation_data.action_slot
return active_slot.id_data.layers[0].strips[0].channelbag(active_slot).fcurves
else:
if obj.animation_data and obj.animation_data.action:
return obj.animation_data.action.fcurves
return []
def get_active_channelbag(obj):
'''return active channelbag in blender 5, else action'''
if bpy.app.version >= (5, 0, 0):
# action slot
if obj.animation_data and obj.animation_data.action_slot:
active_slot = obj.animation_data.action_slot
return active_slot.id_data.layers[0].strips[0].channelbag(active_slot)
else:
if obj.animation_data and obj.animation_data.action:
return obj.animation_data.action
def anim_status(objects) -> tuple((str, str)): def anim_status(objects) -> tuple((str, str)):
'''Return a tutple of icon string status in ('ALL_ON', 'MIXED', 'ALL_OFF', 'NONE')''' '''Return a tutple of icon string status in ('ALL_ON', 'MIXED', 'ALL_OFF', 'NONE')'''
@@ -1494,8 +1528,9 @@ def anim_status(objects) -> tuple((str, str)):
# off_count += 1 # off_count += 1
### Consider All channels individually ### Consider All channels individually
if o.animation_data and o.animation_data.action:
for grp in o.animation_data.action.groups: if channelbag := get_active_channelbag(o):
for grp in channelbag.groups:
## Check if groups are muted ## Check if groups are muted
if grp.mute: if grp.mute:
off_count += 1 off_count += 1
@@ -1504,7 +1539,7 @@ def anim_status(objects) -> tuple((str, str)):
count += 1 count += 1
for fcu in o.animation_data.action.fcurves: for fcu in channelbag.fcurves:
## Check if fcurves are muted ## Check if fcurves are muted
if fcu.mute: if fcu.mute:
off_count += 1 off_count += 1
@@ -1514,12 +1549,8 @@ def anim_status(objects) -> tuple((str, str)):
if o.type in ('GREASEPENCIL', 'CAMERA'): if o.type in ('GREASEPENCIL', 'CAMERA'):
datablock = o.data datablock = o.data
if datablock.animation_data is None:
continue
if not datablock.animation_data.action:
continue
## Check if object data attributes fcurves are muted ## Check if object data attributes fcurves are muted
for fcu in datablock.animation_data.action.fcurves: for fcu in get_fcurves(datablock):
if fcu.mute: if fcu.mute:
off_count += 1 off_count += 1
else: else: