Compare commits

..

No commits in common. "master" and "v3.3.2" have entirely different histories.

24 changed files with 203 additions and 601 deletions

View File

@ -1,38 +1,5 @@
# Changelog # Changelog
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 4.0.3
changed: File checker doest not fix directly when clicked (also removed choice in preference): changed: File checker doest not fix directly when clicked (also removed choice in preference):

View File

@ -6,7 +6,6 @@ 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):
@ -143,7 +142,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 is_hidden(l) and l != gpl.active] tgt_layers = [l for l in gpl if not l.hide and l != gpl.active]
elif self.targeted_layers == 'CHOSEN': elif self.targeted_layers == 'CHOSEN':
if not self.layers_enum: if not self.layers_enum:

View File

@ -5,14 +5,11 @@ from ..utils import (location_to_region,
vector_length, vector_length,
draw_gp_stroke, draw_gp_stroke,
extrapolate_points_by_length, extrapolate_points_by_length,
simple_draw_gp_stroke, simple_draw_gp_stroke)
is_hidden,
is_locked)
import bpy import bpy
from math import degrees from math import degrees
from mathutils import Vector from mathutils import Vector
# from os.path import join, basename, exists, dirname, abspath, splitext # from os.path import join, basename, exists, dirname, abspath, splitext
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level # iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
@ -279,9 +276,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 is_hidden(l)] lays = [l for l in ob.data.layers if l.select and not l.hide]
elif self.layer_tgt == 'ALL_VISIBLE': elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not is_hidden(l)] lays = [l for l in ob.data.layers if not l.hide]
else: else:
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))] lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
@ -340,9 +337,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 is_hidden(l)] lays = [l for l in ob.data.layers if l.select and not l.hide]
elif self.layer_tgt == 'ALL_VISIBLE': elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not is_hidden(l)] lays = [l for l in ob.data.layers if not l.hide]
else: else:
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))] lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
@ -378,7 +375,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 is_hidden(l) and not is_locked(l)] lays = [l for l in ob.data.layers if not l.hide and not l.lock]
for l in lays: for l in lays:
if not l.current_frame():continue if not l.current_frame():continue
for s in l.current_frame().drawing.strokes: for s in l.current_frame().drawing.strokes:

View File

@ -3,7 +3,6 @@ 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/
@ -22,7 +21,7 @@ def get_surrounding_points(fc, frame):
return p_pt, n_pt return p_pt, n_pt
## unused direct breakdown func ## unused direct breackdown 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')
@ -43,7 +42,7 @@ def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale
skipping = [] skipping = []
for fc in utils.get_fcurves(obj): for fc in action.fcurves:
# 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]
@ -253,7 +252,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 utils.get_fcurves(obj): for fc in action.fcurves:
# 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

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,7 +9,6 @@ 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'''
@ -140,7 +139,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
# color = gp.palettes.active.colors.active.name # color = gp.palettes.active.colors.active.name
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # [] layers = [l for l in gpl if not l.hide and not l.lock] # []
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@ -236,7 +235,7 @@ def copy_all_strokes(layers=None):
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ? layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@ -276,7 +275,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # include locked ? layers = [l for l in gpl if not l.hide and not l.lock] # include locked ?
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@ -478,9 +477,6 @@ 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,
@ -505,7 +501,6 @@ 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')
@ -516,6 +511,7 @@ 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
@ -525,12 +521,12 @@ 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 is_hidden(l) and l.select] # and not is_locked(l) layerpool = [l for l in gpl if not l.hide and l.select] # and not l.lock
if not layerpool: if not layerpool:
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only') self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
return {"CANCELLED"} return {"CANCELLED"}
if not self.bake_moves: # copy only drawed frames as is. if not 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

View File

@ -180,14 +180,10 @@ def selection_changed():
## 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=owner, # <-- attach to ID during it's lifetime. owner=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime.
args=(), args=(),
notify=selection_changed, notify=selection_changed,
options={'PERSISTENT'}, options={'PERSISTENT'},
@ -226,4 +222,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.GreasePencil) bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)

View File

@ -1,173 +0,0 @@
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)

View File

@ -11,7 +11,6 @@ from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vecto
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
from time import time from time import time
from math import pi, cos, sin from math import pi, cos, sin
from .utils import is_locked, is_hidden
def get_gp_mat(gp, name, set_active=False): def get_gp_mat(gp, name, set_active=False):
@ -441,7 +440,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 is_locked(l) or is_hidden(l)] gp_layers = [l for l in gp.data.layers if not l.lock or l.hide]
self.gp_frames = [l.current_frame() for l in gp_layers] self.gp_frames = [l.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]

View File

@ -2,7 +2,6 @@ import bpy
import os import os
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from . import utils from . import utils
from bpy.props import (BoolProperty, from bpy.props import (BoolProperty,
@ -10,86 +9,30 @@ from bpy.props import (BoolProperty,
CollectionProperty, CollectionProperty,
StringProperty) StringProperty)
def remove_stroke_exact_duplications(apply=True, verbose=True, select=False): 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
: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, better to avoid false positive) # TODO: add additional check of material (even if unlikely to happen)
ct = 0 ct = 0
if verbose: gp_datas = [gp for gp in bpy.data.grease_pencils]
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 = []
idx_to_delete = [] for s in reversed(f.drawing.strokes):
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
idx_to_delete.append(idx) if apply:
if not apply and select: # Remove redundancy
s.select = True f.drawing.strokes.remove(s)
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"
@ -222,8 +165,6 @@ class GPTB_OT_file_checker(bpy.types.Operator):
o.use_grease_pencil_lights = False o.use_grease_pencil_lights = False
## Disabled animation ## Disabled animation
# TODO : fix for Blender 5.0
if bpy.app.version < (5,0,0):
if fix.list_disabled_anim: if fix.list_disabled_anim:
fcu_ct = 0 fcu_ct = 0
for act in bpy.data.actions: for act in bpy.data.actions:
@ -325,7 +266,6 @@ 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'
@ -709,98 +649,6 @@ class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator):
def execute(self, context): def execute(self, context):
return {'FINISHED'} 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):
bl_idname = "gp.list_modifier_visibility" bl_idname = "gp.list_modifier_visibility"
@ -848,13 +696,11 @@ GPTB_OT_sync_visibility_from_render,
GPTB_OT_sync_visibible_to_render, GPTB_OT_sync_visibible_to_render,
GPTB_PG_object_visibility, GPTB_PG_object_visibility,
GPTB_OT_list_object_visibility_conflicts, 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():

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 hidden, locked layers :all_stroke: affect hided, locked layers
''' '''
if restore_frame: if restore_frame:

View File

@ -2,6 +2,7 @@ import bpy
from mathutils import Vector from mathutils import Vector
from . import utils from . import utils
class GPTB_OT_create_follow_path_curve(bpy.types.Operator): class GPTB_OT_create_follow_path_curve(bpy.types.Operator):
bl_idname = "object.create_follow_path_curve" bl_idname = "object.create_follow_path_curve"
bl_label = "Create Follow Path Curve" bl_label = "Create Follow Path Curve"

View File

@ -212,14 +212,9 @@ 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'):
@ -272,12 +267,8 @@ 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 = False maincam.hide_viewport = True
## 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'
@ -421,12 +412,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, bag): def set_action_mute(self, act):
for i, fcu in enumerate(bag.fcurves): for i, fcu in enumerate(act.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 bag.groups: for g in act.groups:
g.mute = self.mute g.mute = self.mute
def execute(self, context): def execute(self, context):
@ -443,15 +434,22 @@ 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:
if data_channelbag := utils.get_active_channelbag(o.data): gp_act = o.data.animation_data.action
if gp_act:
print(f'\n---{o.name} data:') print(f'\n---{o.name} data:')
self.set_action_mute(data_channelbag) self.set_action_mute(gp_act)
if not o.animation_data:
continue
act = o.animation_data.action
if not act:
continue
if object_channelbag := utils.get_active_channelbag(o):
print(f'\n---{o.name}:') print(f'\n---{o.name}:')
self.set_action_mute(object_channelbag) self.set_action_mute(act)
return {'FINISHED'} return {'FINISHED'}
@ -488,7 +486,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 selection)" bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=False) mute : bpy.props.BoolProperty(default=False)
@ -514,22 +512,27 @@ class GPTB_OT_list_disabled_anims(Operator):
# continue # continue
if o.type == 'GREASEPENCIL': if o.type == 'GREASEPENCIL':
if fcurves := utils.get_fcurves(o.data): if o.data.animation_data:
for i, fcu in enumerate(fcurves): gp_act = o.data.animation_data.action
if gp_act:
for i, fcu in enumerate(gp_act.fcurves):
if fcu.mute: if fcu.mute:
if o not in oblist: if o not in oblist:
oblist.append(o) oblist.append(o)
li.append(f'{o.name}:') li.append(f'{o.name}:')
li.append(f' - {fcu.data_path} {fcu.array_index}') li.append(f' - {fcu.data_path} {fcu.array_index}')
bag = utils.get_active_channelbag(o) if not o.animation_data:
if not bag:
continue continue
for g in bag.groups: act = o.animation_data.action
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(bag.fcurves): for i, fcu in enumerate(act.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:

View File

@ -1,5 +1,5 @@
import bpy import bpy
from .utils import get_addon_prefs, is_locked, is_hidden from .utils import get_addon_prefs
from bpy.props import BoolProperty ,EnumProperty ,StringProperty from bpy.props import BoolProperty ,EnumProperty ,StringProperty
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator): class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
@ -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 is_hidden(l)] gpl = [l for l in context.object.data.layers if l.select and not l.hide]
if not context.object.data.layers.active in gpl: if not context.object.data.layers.active in gpl:
gpl.append(context.object.data.layers.active) gpl.append(context.object.data.layers.active)
elif self.target == 'VISIBLE': elif self.target == 'VISIBLE':
gpl = [l for l in context.object.data.layers if not is_hidden(l)] gpl = [l for l in context.object.data.layers if not l.hide]
elif self.target == 'ACCESSIBLE': elif self.target == 'ACCESSIBLE':
gpl = [l for l in context.object.data.layers if not is_hidden(l) and not is_locked(l)] gpl = [l for l in context.object.data.layers if not l.hide and not l.lock]
if self.keyframe_type != 'NONE': if self.keyframe_type != 'NONE':
# use shortcut choice override # use shortcut choice override

View File

@ -624,15 +624,11 @@ 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=owner, # <-- can attach to an ID during all it's lifetime... owner=bpy.types.GreasePencilv3, # <-- can attach to an ID during all it's lifetime...
# Args passed to callback function (tuple) # Args passed to callback function (tuple)
args=(), args=(),
# Callback function for property update # Callback function for property update
@ -787,4 +783,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.GreasePencil) bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)

View File

@ -4,7 +4,7 @@ import mathutils
from mathutils import Vector, Matrix, geometry from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from time import time from time import time
from .utils import get_gp_draw_plane, location_to_region, region_to_location, is_locked, is_hidden from .utils import get_gp_draw_plane, location_to_region, region_to_location
class GP_OT_pick_closest_layer(Operator): class GP_OT_pick_closest_layer(Operator):
bl_idname = "gp.pick_closest_layer" bl_idname = "gp.pick_closest_layer"
@ -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 is_hidden(layer): if layer.hide:
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 is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes] # [s for l in gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes]
for layer in gp.layers: for layer in gp.layers:
if is_hidden(layer) or not layer.current_frame(): if layer.hide 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:

View File

@ -4,12 +4,7 @@ import mathutils
from mathutils import Vector, Matrix, geometry from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from time import time from time import time
from .utils import (get_gp_draw_plane, from .utils import get_gp_draw_plane, location_to_region, region_to_location
location_to_region,
region_to_location,
is_locked,
is_hidden)
### passing by 2D projection ### passing by 2D projection
def get_3d_coord_on_drawing_plane_from_2d(context, co): def get_3d_coord_on_drawing_plane_from_2d(context, co):
@ -84,7 +79,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 is_hidden(l):# is_locked(l) or if l.hide:# l.lock or
continue continue
for f in l.frames: for f in l.frames:
if not f.select: if not f.select:
@ -93,9 +88,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 is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes] # [s for l in self.gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes]
for l in self.gp.layers: for l in self.gp.layers:
if is_hidden(l) or not l.current_frame():# is_locked(l) or if l.hide or not l.current_frame():# l.lock 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)
@ -240,7 +235,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 is_hidden(l):# is_locked(l) or if l.hide:# l.lock or
continue continue
for f in l.frames: for f in l.frames:
if not f.select: if not f.select:
@ -249,9 +244,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 is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes] # [s for l in gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes]
for l in gp.layers: for l in gp.layers:
if is_hidden(l) or not l.current_frame():# is_locked(l) or if l.hide or not l.current_frame():# l.lock 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)

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:
data_source.remove(src_ob.data) bpy.data.grease_pencils.remove(src_ob.data)
return {"FINISHED"} return {"FINISHED"}

View File

@ -1,13 +1,11 @@
import bpy import bpy
import mathutils import mathutils
import numpy as np
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
from math import pi from math import pi
import numpy as np
from time import time from time import time
from mathutils.geometry import intersect_line_plane
from . import utils from . import utils
from .utils import is_hidden, is_locked from mathutils.geometry import intersect_line_plane
def get_scale_matrix(scale): def get_scale_matrix(scale):
# recreate a neutral mat scale # recreate a neutral mat scale
@ -17,6 +15,35 @@ 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
@ -46,7 +73,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 is_hidden(layer) or is_locked(layer): if layer.hide or layer.lock:
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)
@ -158,10 +185,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 utils.get_fcurves(o) for k in fcu.keyframe_points if fcu.data_path == chanel] 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]
## normal iter ## normal iter
# for fcu in utils.get_fcurves(o): # for fcu in o.animation_data.action.fcurves:
# if fcu.data_path != chanel : # if fcu.data_path != chanel :
# continue # continue
# for k in fcu.keyframe_points(): # for k in fcu.keyframe_points():
@ -268,12 +295,12 @@ class GPTB_OT_realign(bpy.types.Operator):
self.alert = '' self.alert = ''
o = context.object o = context.object
fcurves = utils.get_fcurves(o) if o.animation_data and o.animation_data.action:
if fcurves: act = o.animation_data.action
for chan in ('rotation_euler', 'rotation_quaternion'): for chan in ('rotation_euler', 'rotation_quaternion'):
if fcurves.find(chan): if act.fcurves.find(chan):
self.alert = 'Animated Rotation (CONSTANT interpolation)' self.alert = 'Animated Rotation (CONSTANT interpolation)'
interpos = [p for fcu in fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT'] interpos = [p for fcu in act.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
@ -323,8 +350,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 fcurves := utils.get_fcurves(o): if o.animation_data and o.animation_data.action:
if fcurves.find('rotation_euler') or fcurves.find('rotation_quaternion'): if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.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)

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 from release page](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)** **[Download for Blender 4.2 and below](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/v3.3.0.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**

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échargement pour Blender 4.2 ou inférieur depuis la page des releases](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)** **[Télécharger pour Blender 4.2 ou inférieure](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/v3.3.0.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**

View File

@ -771,15 +771,9 @@ 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)
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.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
@ -792,14 +786,9 @@ 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)
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.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):

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": (5, 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": "",
@ -47,7 +47,6 @@ 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
@ -160,6 +159,23 @@ 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",
@ -189,40 +205,11 @@ class GPTB_prefs(bpy.types.AddonPreferences):
default=True, default=True,
) )
if bpy.app.version >= (5, 0, 0):
# DIR_PATH type do not accept relative ("//") since blender 5
## using standard string
brush_path : StringProperty(
name="Brushes directory",
description="Path to brushes containing the blends holding the brushes",
default="//", maxlen=0)
output_path : StringProperty(
name="Output path",
description="Path relative to blend to place render",
default="//render", maxlen=0)
playblast_path : StringProperty(
name="Playblast Path",
description="Path to folder for playblasts output",
default="//playblast", maxlen=0)
else:
brush_path : StringProperty( brush_path : StringProperty(
name="Brushes directory", name="Brushes directory",
description="Path to brushes containing the blends holding the brushes", description="Path to brushes containing the blends holding the brushes",
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path 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(
name="Separator", name="Separator",
@ -716,6 +703,8 @@ 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')
@ -816,7 +805,6 @@ 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,

View File

@ -11,12 +11,10 @@ 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):
## Obsolete: Gpv3 has no edit line color anymore for gp in bpy.data.grease_pencils:
# def change_edit_lines_opacity(self, context): if not gp.is_annotation:
# for gp in bpy.data.grease_pencils: gp.edit_line_color[3]=self.edit_lines_opacity
# 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):

View File

@ -144,7 +144,7 @@ def object_derived_get(ob, scene):
# ----------------- # -----------------
# region Bmesh ### Bmesh
# ----------------- # -----------------
def link_vert(v,ordered_vert) : def link_vert(v,ordered_vert) :
@ -219,7 +219,7 @@ def gp_stroke_to_bmesh(strokes):
# ----------------- # -----------------
# region GP Drawing ### 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)
# ----------------- # -----------------
# region GP funcs ### 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
# ----------------- # -----------------
# region Vector utils 3d ### 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)
# ----------------- # -----------------
# region Vector utils 2d ### Vector utils 2d
# ----------------- # -----------------
@ -867,7 +867,7 @@ def midpoint_2d(p1, p2):
# ----------------- # -----------------
# region Collection management ### 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) :
# ----------------- # -----------------
# region Path utils ### Path utils
# ----------------- # -----------------
def get_addon_prefs(): def get_addon_prefs():
@ -1048,7 +1048,7 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
# ----------------- # -----------------
# region UI utils ### UI utils
# ----------------- # -----------------
def refresh_areas(): def refresh_areas():
@ -1164,7 +1164,7 @@ def draw_kmi(km, kmi, layout):
# layout.context_pointer_set("keymap", km) # layout.context_pointer_set("keymap", km)
# ----------------- # -----------------
# region linking utility ### linking utility
# ----------------- # -----------------
def link_objects_in_blend(filepath, obj_name_list, link=True): def link_objects_in_blend(filepath, obj_name_list, link=True):
@ -1192,7 +1192,7 @@ def check_objects_in_blend(filepath, avoid_camera=True):
# ----------------- # -----------------
# region props handling ### 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'):
@ -1256,7 +1256,7 @@ def iterate_active_layer(gpd, state):
# return info, bottom # return info, bottom
# ----------------- # -----------------
# region Curve handle ### 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):
@ -1404,7 +1404,7 @@ def create_follow_path_constraint(ob, curve, follow_curve=False, use_fixed_locat
# ----------------- # -----------------
# region Object ### Object
# ----------------- # -----------------
def go_edit_mode(ob, context=None): def go_edit_mode(ob, context=None):
@ -1485,30 +1485,6 @@ 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')'''
@ -1528,9 +1504,8 @@ 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:
if channelbag := get_active_channelbag(o): for grp in o.animation_data.action.groups:
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
@ -1539,7 +1514,7 @@ def anim_status(objects) -> tuple((str, str)):
count += 1 count += 1
for fcu in channelbag.fcurves: for fcu in o.animation_data.action.fcurves:
## Check if fcurves are muted ## Check if fcurves are muted
if fcu.mute: if fcu.mute:
off_count += 1 off_count += 1
@ -1549,8 +1524,12 @@ 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 get_fcurves(datablock): for fcu in datablock.animation_data.action.fcurves:
if fcu.mute: if fcu.mute:
off_count += 1 off_count += 1
else: else: