Compare commits
No commits in common. "master" and "v3.3.2" have entirely different histories.
33
CHANGELOG.md
33
CHANGELOG.md
@ -1,38 +1,5 @@
|
||||
# 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
|
||||
|
||||
changed: File checker doest not fix directly when clicked (also removed choice in preference):
|
||||
|
||||
@ -6,7 +6,6 @@ from bpy.props import (FloatProperty,
|
||||
StringProperty,
|
||||
IntProperty)
|
||||
from .. import utils
|
||||
from ..utils import is_hidden
|
||||
|
||||
## copied from OP_key_duplicate_send
|
||||
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]
|
||||
|
||||
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':
|
||||
if not self.layers_enum:
|
||||
@ -221,4 +220,4 @@ def register():
|
||||
bpy.utils.register_class(GP_OT_create_empty_frames)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(GP_OT_create_empty_frames)
|
||||
bpy.utils.unregister_class(GP_OT_create_empty_frames)
|
||||
@ -5,14 +5,11 @@ from ..utils import (location_to_region,
|
||||
vector_length,
|
||||
draw_gp_stroke,
|
||||
extrapolate_points_by_length,
|
||||
simple_draw_gp_stroke,
|
||||
is_hidden,
|
||||
is_locked)
|
||||
simple_draw_gp_stroke)
|
||||
|
||||
import bpy
|
||||
from math import degrees
|
||||
from mathutils import Vector
|
||||
|
||||
# from os.path import join, basename, exists, dirname, abspath, splitext
|
||||
|
||||
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
|
||||
@ -279,9 +276,9 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
|
||||
if self.layer_tgt == 'ACTIVE':
|
||||
lays = [ob.data.layers.active]
|
||||
elif self.layer_tgt == 'SELECTED':
|
||||
lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
|
||||
lays = [l for l in ob.data.layers if l.select and not l.hide]
|
||||
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:
|
||||
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':
|
||||
lays = [ob.data.layers.active]
|
||||
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':
|
||||
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:
|
||||
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):
|
||||
ct = 0
|
||||
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:
|
||||
if not l.current_frame():continue
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
|
||||
@ -3,7 +3,6 @@ import bpy
|
||||
import re
|
||||
from mathutils import Vector, Matrix
|
||||
from math import radians, degrees
|
||||
from . import utils
|
||||
|
||||
# exemple for future improve: https://justinsbarrett.com/tweenmachine/
|
||||
|
||||
@ -22,7 +21,7 @@ def get_surrounding_points(fc, frame):
|
||||
|
||||
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)):
|
||||
cf = bpy.context.scene.frame_current# use operator context (may be unsynced timeline)
|
||||
axes_name = ('x', 'y', 'z')
|
||||
@ -43,7 +42,7 @@ def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale
|
||||
|
||||
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 channels and fc.array_index in axe:# bones
|
||||
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...
|
||||
|
||||
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 channels and fc.array_index in axe:# bones
|
||||
if fc.data_path in self.channels:# .split('.')[-1]# and fc.array_index in axe
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends
|
||||
## View3D > Toolbar > Gpencil > GP clipboard
|
||||
## in 4.2, existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
|
||||
## in 4.2- existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
@ -9,7 +9,6 @@ import json
|
||||
from time import time
|
||||
from operator import itemgetter
|
||||
from itertools import groupby
|
||||
from .utils import is_locked, is_hidden
|
||||
|
||||
def convertAttr(Attr):
|
||||
'''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
|
||||
if not 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 a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
@ -236,7 +235,7 @@ def copy_all_strokes(layers=None):
|
||||
|
||||
if not 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 a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
@ -276,7 +275,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
|
||||
|
||||
if not layers:
|
||||
# by default all visible layers
|
||||
layers = [l for l in gpl if not 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 a single layer object is send put in a list
|
||||
layers = [layers]
|
||||
@ -478,9 +477,6 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
||||
def poll(cls, context):
|
||||
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,
|
||||
description='Dump point radius attribute (already skipped if at default value)')
|
||||
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.use_property_split = True
|
||||
col = layout.column()
|
||||
col.prop(self, 'bake_moves')
|
||||
col.label(text='Keep following point attributes:')
|
||||
col.prop(self, 'radius')
|
||||
col.prop(self, 'opacity')
|
||||
@ -516,6 +511,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
||||
return
|
||||
|
||||
def execute(self, context):
|
||||
bake_moves = True
|
||||
skip_empty_frame = False
|
||||
|
||||
org_frame = context.scene.frame_current
|
||||
@ -525,15 +521,15 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
|
||||
#ct = check_radius()
|
||||
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:
|
||||
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"}
|
||||
|
||||
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:
|
||||
if not l.frames:
|
||||
continue # skip empty layers
|
||||
continue# skip empty layers
|
||||
|
||||
frame_dic = {}
|
||||
for f in l.frames:
|
||||
|
||||
@ -180,14 +180,10 @@ def selection_changed():
|
||||
## Note: Same owner as layer manager (will be removed as well)
|
||||
def subscribe_object_change():
|
||||
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(
|
||||
key=subscribe_to,
|
||||
# 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=(),
|
||||
notify=selection_changed,
|
||||
options={'PERSISTENT'},
|
||||
@ -226,4 +222,4 @@ def unregister():
|
||||
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
||||
bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
||||
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
|
||||
@ -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)
|
||||
@ -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
|
||||
from time import time
|
||||
from math import pi, cos, sin
|
||||
from .utils import is_locked, is_hidden
|
||||
|
||||
|
||||
def get_gp_mat(gp, name, set_active=False):
|
||||
@ -441,7 +440,7 @@ class GPTB_OT_eraser(Operator):
|
||||
|
||||
t0 = time()
|
||||
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]
|
||||
'''
|
||||
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes]
|
||||
|
||||
@ -2,7 +2,6 @@ import bpy
|
||||
import os
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
from . import utils
|
||||
|
||||
from bpy.props import (BoolProperty,
|
||||
@ -10,86 +9,30 @@ from bpy.props import (BoolProperty,
|
||||
CollectionProperty,
|
||||
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)
|
||||
: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
|
||||
'''
|
||||
# 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
|
||||
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]
|
||||
gp_datas = [gp for gp in bpy.data.grease_pencils]
|
||||
for gp in gp_datas:
|
||||
for l in gp.layers:
|
||||
for f in l.frames:
|
||||
stroke_list = []
|
||||
idx_to_delete = []
|
||||
|
||||
for idx, s in enumerate(f.drawing.strokes):
|
||||
point_list = [p.position.copy() for p in s.points]
|
||||
for s in reversed(f.drawing.strokes):
|
||||
|
||||
point_list = [p.position for p in s.points]
|
||||
|
||||
if point_list in stroke_list:
|
||||
ct += 1
|
||||
idx_to_delete.append(idx)
|
||||
if not apply and select:
|
||||
s.select = True
|
||||
if apply:
|
||||
# Remove redundancy
|
||||
f.drawing.strokes.remove(s)
|
||||
else:
|
||||
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
|
||||
|
||||
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):
|
||||
bl_idname = "gp.file_checker"
|
||||
bl_label = "Check File"
|
||||
@ -222,19 +165,17 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
||||
o.use_grease_pencil_lights = False
|
||||
|
||||
## Disabled animation
|
||||
# TODO : fix for Blender 5.0
|
||||
if bpy.app.version < (5,0,0):
|
||||
if fix.list_disabled_anim:
|
||||
fcu_ct = 0
|
||||
for act in bpy.data.actions:
|
||||
if not act.users:
|
||||
continue
|
||||
for fcu in act.fcurves:
|
||||
if fcu.mute:
|
||||
fcu_ct += 1
|
||||
print(f"muted: {act.name} > {fcu.data_path}")
|
||||
if fcu_ct:
|
||||
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
|
||||
if fix.list_disabled_anim:
|
||||
fcu_ct = 0
|
||||
for act in bpy.data.actions:
|
||||
if not act.users:
|
||||
continue
|
||||
for fcu in act.fcurves:
|
||||
if fcu.mute:
|
||||
fcu_ct += 1
|
||||
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
|
||||
if fix.list_obj_vis_conflict:
|
||||
@ -325,7 +266,6 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
||||
bpy.context.scene.tool_settings.lock_object_mode = False
|
||||
|
||||
if fix.remove_redundant_strokes:
|
||||
print('removing redundant strokes')
|
||||
ct = remove_stroke_exact_duplications(apply=apply)
|
||||
if ct > 0:
|
||||
mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications'
|
||||
@ -333,7 +273,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
||||
|
||||
# ## Set onion skin filter to 'All type'
|
||||
# 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 gp.onion_keyframe_type != 'ALL':
|
||||
# gp.onion_keyframe_type = 'ALL'
|
||||
@ -709,98 +649,6 @@ class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator):
|
||||
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)
|
||||
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
|
||||
bl_idname = "gp.list_modifier_visibility"
|
||||
@ -848,13 +696,11 @@ 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_copy_string_to_clipboard,
|
||||
GPTB_OT_copy_multipath_clipboard,
|
||||
GPTB_OT_file_checker,
|
||||
GPTB_OT_links_checker,
|
||||
GPTB_OT_remove_stroke_duplication,
|
||||
)
|
||||
|
||||
def register():
|
||||
|
||||
@ -12,7 +12,7 @@ from .utils import (location_to_region, region_to_location)
|
||||
## Do not work on multiple object
|
||||
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
|
||||
'''Reproject
|
||||
:all_stroke: affect hidden, locked layers
|
||||
:all_stroke: affect hided, locked layers
|
||||
'''
|
||||
|
||||
if restore_frame:
|
||||
|
||||
@ -2,6 +2,7 @@ import bpy
|
||||
from mathutils import Vector
|
||||
from . import utils
|
||||
|
||||
|
||||
class GPTB_OT_create_follow_path_curve(bpy.types.Operator):
|
||||
bl_idname = "object.create_follow_path_curve"
|
||||
bl_label = "Create Follow Path Curve"
|
||||
|
||||
@ -212,14 +212,9 @@ class GPTB_OT_draw_cam(Operator):
|
||||
# Swap to it, unhide if necessary and hide previous
|
||||
context.scene.camera = maincam
|
||||
|
||||
## Hide cam object
|
||||
|
||||
# Ensure at lviewport viz is active to ensure visibility and refresh state
|
||||
## hide cam object
|
||||
drawcam.hide_viewport = True
|
||||
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
|
||||
elif context.scene.camera.name not in ('draw_cam', 'obj_cam'):
|
||||
@ -272,12 +267,8 @@ class GPTB_OT_draw_cam(Operator):
|
||||
|
||||
## hide cam object
|
||||
context.scene.camera = drawcam
|
||||
# Ensure viewport viz is active to ensure visibility and refresh state
|
||||
drawcam.hide_viewport = False
|
||||
maincam.hide_viewport = False
|
||||
## Set viewlayer visibility
|
||||
drawcam.hide_set(False)
|
||||
maincam.hide_set(True)
|
||||
maincam.hide_viewport = True
|
||||
|
||||
if created and drawcam.name == 'obj_cam': # Go in camera view
|
||||
context.region_data.view_perspective = 'CAMERA'
|
||||
@ -421,12 +412,12 @@ class GPTB_OT_toggle_mute_animation(Operator):
|
||||
self.selection = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def set_action_mute(self, bag):
|
||||
for i, fcu in enumerate(bag.fcurves):
|
||||
def set_action_mute(self, act):
|
||||
for i, fcu in enumerate(act.fcurves):
|
||||
print(i, fcu.data_path, fcu.array_index)
|
||||
# fcu.group don't have mute attribute in api.
|
||||
fcu.mute = self.mute
|
||||
for g in bag.groups:
|
||||
for g in act.groups:
|
||||
g.mute = self.mute
|
||||
|
||||
def execute(self, context):
|
||||
@ -443,15 +434,22 @@ class GPTB_OT_toggle_mute_animation(Operator):
|
||||
if self.mode == 'CAMERA' and o.type != 'CAMERA':
|
||||
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 data_channelbag := utils.get_active_channelbag(o.data):
|
||||
gp_act = o.data.animation_data.action
|
||||
if gp_act:
|
||||
print(f'\n---{o.name} data:')
|
||||
self.set_action_mute(data_channelbag)
|
||||
self.set_action_mute(gp_act)
|
||||
|
||||
if object_channelbag := utils.get_active_channelbag(o):
|
||||
print(f'\n---{o.name}:')
|
||||
self.set_action_mute(object_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)
|
||||
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@ -488,7 +486,7 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
|
||||
class GPTB_OT_list_disabled_anims(Operator):
|
||||
bl_idname = "gp.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"}
|
||||
|
||||
mute : bpy.props.BoolProperty(default=False)
|
||||
@ -514,22 +512,27 @@ class GPTB_OT_list_disabled_anims(Operator):
|
||||
# continue
|
||||
|
||||
if o.type == 'GREASEPENCIL':
|
||||
if fcurves := utils.get_fcurves(o.data):
|
||||
for i, fcu in enumerate(fcurves):
|
||||
if fcu.mute:
|
||||
if o not in oblist:
|
||||
oblist.append(o)
|
||||
li.append(f'{o.name}:')
|
||||
li.append(f' - {fcu.data_path} {fcu.array_index}')
|
||||
|
||||
bag = utils.get_active_channelbag(o)
|
||||
if not bag:
|
||||
if o.data.animation_data:
|
||||
gp_act = o.data.animation_data.action
|
||||
if gp_act:
|
||||
for i, fcu in enumerate(gp_act.fcurves):
|
||||
if fcu.mute:
|
||||
if o not in oblist:
|
||||
oblist.append(o)
|
||||
li.append(f'{o.name}:')
|
||||
li.append(f' - {fcu.data_path} {fcu.array_index}')
|
||||
|
||||
if not o.animation_data:
|
||||
continue
|
||||
for g in bag.groups:
|
||||
act = o.animation_data.action
|
||||
if not act:
|
||||
continue
|
||||
|
||||
for g in act.groups:
|
||||
if g.mute:
|
||||
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)
|
||||
if fcu.mute:
|
||||
if o not in oblist:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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
|
||||
|
||||
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||
@ -45,15 +45,15 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.target == 'ACTIVE':
|
||||
gpl = [l for l in context.object.data.layers if l.select and not 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:
|
||||
gpl.append(context.object.data.layers.active)
|
||||
|
||||
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':
|
||||
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':
|
||||
# use shortcut choice override
|
||||
|
||||
@ -624,15 +624,11 @@ def obj_layer_name_callback():
|
||||
|
||||
def subscribe_layer_change():
|
||||
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(
|
||||
key=subscribe_to,
|
||||
# owner of msgbus subcribe (for clearing later)
|
||||
# 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=(),
|
||||
# Callback function for property update
|
||||
@ -787,4 +783,4 @@ def unregister():
|
||||
|
||||
# Delete layer index trigger
|
||||
# /!\ 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)
|
||||
@ -4,7 +4,7 @@ import mathutils
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
from bpy_extras import view3d_utils
|
||||
from time import time
|
||||
from .utils import get_gp_draw_plane, location_to_region, region_to_location, is_locked, is_hidden
|
||||
from .utils import get_gp_draw_plane, location_to_region, region_to_location
|
||||
|
||||
class GP_OT_pick_closest_layer(Operator):
|
||||
bl_idname = "gp.pick_closest_layer"
|
||||
@ -76,7 +76,7 @@ class GP_OT_pick_closest_layer(Operator):
|
||||
self.point_pair = []
|
||||
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
|
||||
for layer in gp.layers:
|
||||
if is_hidden(layer):
|
||||
if layer.hide:
|
||||
continue
|
||||
for f in layer.frames:
|
||||
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]
|
||||
|
||||
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:
|
||||
if is_hidden(layer) or not layer.current_frame():
|
||||
if layer.hide or not layer.current_frame():
|
||||
continue
|
||||
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:
|
||||
|
||||
@ -4,12 +4,7 @@ import mathutils
|
||||
from mathutils import Vector, Matrix, geometry
|
||||
from bpy_extras import view3d_utils
|
||||
from time import time
|
||||
from .utils import (get_gp_draw_plane,
|
||||
location_to_region,
|
||||
region_to_location,
|
||||
is_locked,
|
||||
is_hidden)
|
||||
|
||||
from .utils import get_gp_draw_plane, location_to_region, region_to_location
|
||||
|
||||
### passing by 2D projection
|
||||
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:
|
||||
for l in self.gp.layers:
|
||||
if is_hidden(l):# is_locked(l) or
|
||||
if l.hide:# l.lock or
|
||||
continue
|
||||
for f in l.frames:
|
||||
if not f.select:
|
||||
@ -93,9 +88,9 @@ class GP_OT_pick_closest_material(Operator):
|
||||
self.stroke_list.append(s)
|
||||
|
||||
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:
|
||||
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
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
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:
|
||||
for l in gp.layers:
|
||||
if is_hidden(l):# is_locked(l) or
|
||||
if l.hide:# l.lock or
|
||||
continue
|
||||
for f in l.frames:
|
||||
if not f.select:
|
||||
@ -249,9 +244,9 @@ class GP_OT_pick_closest_material(Operator):
|
||||
self.stroke_list.append(s)
|
||||
|
||||
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:
|
||||
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
|
||||
for s in l.current_frame().drawing.strokes:
|
||||
self.stroke_list.append(s)
|
||||
|
||||
@ -165,9 +165,9 @@ class GPTB_OT_import_obj_palette(Operator):
|
||||
# self.report({'WARNING'}, 'All materials are already in other selected object')
|
||||
|
||||
# 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:
|
||||
data_source.remove(src_ob.data)
|
||||
bpy.data.grease_pencils.remove(src_ob.data)
|
||||
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
import numpy as np
|
||||
|
||||
from mathutils import Matrix, Vector
|
||||
from math import pi
|
||||
import numpy as np
|
||||
from time import time
|
||||
from mathutils.geometry import intersect_line_plane
|
||||
from . import utils
|
||||
from .utils import is_hidden, is_locked
|
||||
from mathutils.geometry import intersect_line_plane
|
||||
|
||||
def get_scale_matrix(scale):
|
||||
# recreate a neutral mat scale
|
||||
@ -17,6 +15,35 @@ def get_scale_matrix(scale):
|
||||
matscale = matscale_x @ matscale_y @ matscale_z
|
||||
return matscale
|
||||
|
||||
'''
|
||||
## Old reproject method using Operators:
|
||||
omode = bpy.context.mode
|
||||
|
||||
if all_strokes:
|
||||
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
|
||||
for l in obj.data.layers:
|
||||
l.hide = False
|
||||
l.lock = False
|
||||
l.lock_frame = False
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
|
||||
for fnum in frame_list:
|
||||
bpy.context.scene.frame_current = fnum
|
||||
bpy.ops.gpencil.select_all(action='SELECT')
|
||||
bpy.ops.gpencil.reproject(type=proj_type) # 'INVOKE_DEFAULT'
|
||||
bpy.ops.gpencil.select_all(action='DESELECT')
|
||||
|
||||
# restore
|
||||
if all_strokes:
|
||||
for layer, hide, lock, lock_frame in layers_state:
|
||||
layer.hide = hide
|
||||
layer.lock = lock
|
||||
layer.lock_frame = lock_frame
|
||||
|
||||
bpy.ops.object.mode_set(mode=omode)
|
||||
'''
|
||||
|
||||
def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
|
||||
'''Reproject - ops method
|
||||
:all_stroke: affect 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 layer.select:
|
||||
continue
|
||||
if is_hidden(layer) or is_locked(layer):
|
||||
if layer.hide or layer.lock:
|
||||
continue
|
||||
|
||||
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'
|
||||
|
||||
## 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
|
||||
# for fcu in utils.get_fcurves(o):
|
||||
# for fcu in o.animation_data.action.fcurves:
|
||||
# if fcu.data_path != chanel :
|
||||
# continue
|
||||
# for k in fcu.keyframe_points():
|
||||
@ -268,12 +295,12 @@ class GPTB_OT_realign(bpy.types.Operator):
|
||||
|
||||
self.alert = ''
|
||||
o = context.object
|
||||
fcurves = utils.get_fcurves(o)
|
||||
if fcurves:
|
||||
if o.animation_data and o.animation_data.action:
|
||||
act = o.animation_data.action
|
||||
for chan in ('rotation_euler', 'rotation_quaternion'):
|
||||
if fcurves.find(chan):
|
||||
if act.fcurves.find(chan):
|
||||
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:
|
||||
self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)'
|
||||
break
|
||||
@ -323,8 +350,8 @@ class GPTB_OT_realign(bpy.types.Operator):
|
||||
oframe = context.scene.frame_current
|
||||
|
||||
o = bpy.context.object
|
||||
if fcurves := utils.get_fcurves(o):
|
||||
if fcurves.find('rotation_euler') or fcurves.find('rotation_quaternion'):
|
||||
if o.animation_data and o.animation_data.action:
|
||||
if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'):
|
||||
align_all_frames(reproject=self.reproject)
|
||||
print(f'\nAnim realign ({time()-t0:.2f}s)')
|
||||
self.exit(context, oframe)
|
||||
|
||||
@ -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 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)**
|
||||
|
||||
|
||||
@ -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é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)**
|
||||
|
||||
|
||||
19
UI_tools.py
19
UI_tools.py
@ -771,15 +771,9 @@ GPTB_PT_palettes_list_ui, # subpanels
|
||||
def register():
|
||||
for cls in classes:
|
||||
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.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
|
||||
|
||||
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
|
||||
@ -792,14 +786,9 @@ def unregister():
|
||||
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
|
||||
|
||||
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.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
|
||||
|
||||
# if bpy.app.version >= (3,0,0):
|
||||
|
||||
74
__init__.py
74
__init__.py
@ -4,7 +4,7 @@ bl_info = {
|
||||
"name": "GP toolbox",
|
||||
"description": "Tool set for Grease Pencil in animation production",
|
||||
"author": "Samuel Bernou, Christophe Seux",
|
||||
"version": (5, 0, 0),
|
||||
"version": (4, 0, 3),
|
||||
"blender": (4, 3, 0),
|
||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||
"warning": "",
|
||||
@ -47,7 +47,6 @@ from . import OP_layer_namespace
|
||||
from . import OP_pseudo_tint
|
||||
from . import OP_follow_curve
|
||||
from . import OP_material_move_to_layer
|
||||
from . import OP_delete_viewbound
|
||||
# from . import OP_eraser_brush
|
||||
# from . import TOOL_eraser_brush
|
||||
from . import handler_draw_cam
|
||||
@ -160,6 +159,23 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
)
|
||||
|
||||
## 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(
|
||||
name="Use Project Palettes",
|
||||
@ -189,39 +205,10 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
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(
|
||||
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')
|
||||
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
|
||||
|
||||
## namespace
|
||||
separator : StringProperty(
|
||||
@ -291,7 +278,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
)
|
||||
|
||||
## KF jumper
|
||||
kfj_use_shortcut : BoolProperty(
|
||||
kfj_use_shortcut: BoolProperty(
|
||||
name = "Use Keyframe Jump Shortcut",
|
||||
description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)",
|
||||
default = True)
|
||||
@ -301,17 +288,17 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
description="Shortcut to trigger previous keyframe jump",
|
||||
default="F5")
|
||||
|
||||
kfj_prev_shift : BoolProperty(
|
||||
kfj_prev_shift: BoolProperty(
|
||||
name = "Shift",
|
||||
description = "add shift",
|
||||
default = False)
|
||||
|
||||
kfj_prev_alt : BoolProperty(
|
||||
kfj_prev_alt: BoolProperty(
|
||||
name = "Alt",
|
||||
description = "add alt",
|
||||
default = False)
|
||||
|
||||
kfj_prev_ctrl : BoolProperty(
|
||||
kfj_prev_ctrl: BoolProperty(
|
||||
name = "combine with ctrl",
|
||||
description = "add ctrl",
|
||||
default = False)
|
||||
@ -321,17 +308,17 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
description="Shortcut to trigger keyframe jump",
|
||||
default="F6")
|
||||
|
||||
kfj_next_shift : BoolProperty(
|
||||
kfj_next_shift: BoolProperty(
|
||||
name = "Shift",
|
||||
description = "add shift",
|
||||
default = False)
|
||||
|
||||
kfj_next_alt : BoolProperty(
|
||||
kfj_next_alt: BoolProperty(
|
||||
name = "Alt",
|
||||
description = "add alt",
|
||||
default = False)
|
||||
|
||||
kfj_next_ctrl : BoolProperty(
|
||||
kfj_next_ctrl: BoolProperty(
|
||||
name = "combine with ctrl",
|
||||
description = "add ctrl",
|
||||
default = False)
|
||||
@ -716,6 +703,8 @@ def set_namespace_env(name_env, prop_group):
|
||||
n.is_project = n.tag in project_pfix
|
||||
|
||||
def set_env_properties():
|
||||
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
|
||||
fps = os.getenv('FPS')
|
||||
@ -816,7 +805,6 @@ addon_modules = (
|
||||
OP_layer_nav,
|
||||
OP_follow_curve,
|
||||
OP_material_move_to_layer,
|
||||
OP_delete_viewbound,
|
||||
# OP_eraser_brush,
|
||||
# TOOL_eraser_brush, # experimental eraser brush
|
||||
handler_draw_cam,
|
||||
|
||||
@ -11,12 +11,10 @@ from bpy.props import (
|
||||
from .OP_cursor_snap_canvas import cursor_follow_update
|
||||
from .OP_layer_manager import layer_name_build
|
||||
|
||||
|
||||
## Obsolete: Gpv3 has no edit line color anymore
|
||||
# def change_edit_lines_opacity(self, context):
|
||||
# for gp in bpy.data.grease_pencils:
|
||||
# if not gp.is_annotation:
|
||||
# gp.edit_line_color[3]=self.edit_lines_opacity
|
||||
def change_edit_lines_opacity(self, context):
|
||||
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):
|
||||
|
||||
61
utils.py
61
utils.py
@ -144,7 +144,7 @@ def object_derived_get(ob, scene):
|
||||
|
||||
|
||||
# -----------------
|
||||
# region Bmesh
|
||||
### Bmesh
|
||||
# -----------------
|
||||
|
||||
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):
|
||||
@ -393,7 +393,7 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
|
||||
return rightMin + (valueScaled * rightSpan)
|
||||
|
||||
# -----------------
|
||||
# region GP funcs
|
||||
### GP funcs
|
||||
# -----------------
|
||||
|
||||
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
|
||||
|
||||
# -----------------
|
||||
# region Vector utils 3d
|
||||
### Vector utils 3d
|
||||
# -----------------
|
||||
|
||||
def matrix_transform(coords, matrix):
|
||||
@ -814,7 +814,7 @@ def extrapolate_points_by_length(a,b, 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) :
|
||||
@ -903,7 +903,7 @@ def set_collection(ob, collection, unlink=True) :
|
||||
|
||||
|
||||
# -----------------
|
||||
# region Path utils
|
||||
### Path utils
|
||||
# -----------------
|
||||
|
||||
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)
|
||||
|
||||
# -----------------
|
||||
# region UI utils
|
||||
### UI utils
|
||||
# -----------------
|
||||
|
||||
def refresh_areas():
|
||||
@ -1164,7 +1164,7 @@ def draw_kmi(km, kmi, layout):
|
||||
# layout.context_pointer_set("keymap", km)
|
||||
|
||||
# -----------------
|
||||
# region linking utility
|
||||
### linking utility
|
||||
# -----------------
|
||||
|
||||
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'):
|
||||
@ -1256,7 +1256,7 @@ def iterate_active_layer(gpd, state):
|
||||
# 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):
|
||||
@ -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):
|
||||
@ -1485,30 +1485,6 @@ def has_fully_enabled_anim(o):
|
||||
return False
|
||||
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)):
|
||||
'''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
|
||||
|
||||
### Consider All channels individually
|
||||
|
||||
if channelbag := get_active_channelbag(o):
|
||||
for grp in channelbag.groups:
|
||||
if o.animation_data and o.animation_data.action:
|
||||
for grp in o.animation_data.action.groups:
|
||||
## Check if groups are muted
|
||||
if grp.mute:
|
||||
off_count += 1
|
||||
@ -1539,7 +1514,7 @@ def anim_status(objects) -> tuple((str, str)):
|
||||
count += 1
|
||||
|
||||
|
||||
for fcu in channelbag.fcurves:
|
||||
for fcu in o.animation_data.action.fcurves:
|
||||
## Check if fcurves are muted
|
||||
if fcu.mute:
|
||||
off_count += 1
|
||||
@ -1549,8 +1524,12 @@ def anim_status(objects) -> tuple((str, str)):
|
||||
|
||||
if o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||
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
|
||||
for fcu in get_fcurves(datablock):
|
||||
for fcu in datablock.animation_data.action.fcurves:
|
||||
if fcu.mute:
|
||||
off_count += 1
|
||||
else:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user