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

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

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

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

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

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

- added: option in copy stroke world space to not bake move
2025-05-06 10:49:22 +02:00
pullusb aecef47d2f Add operator to list collection visibility conflicts
4.1.0
2025-03-04 12:35:45 +01:00
pullusb cf3f39b730 Respect group lock and hide state 2024-12-16 17:29:27 +01:00
pullusb 1c39d81ef1 update links for latest gpv2 release on readmes 2024-12-03 18:29:20 +01:00
pullusb 7665dd4f4f Add optional object target for constant cursor follow 2024-12-03 16:37:42 +01:00
pullusb 2013c55ba8 Fix broken cursor_follow feature 2024-12-03 16:10:30 +01:00
pullusb 3695470354 Fix problem in viz conflict check 2024-12-03 15:36:55 +01:00
pullusb 186229cdba typo 2024-12-03 14:36:48 +01:00
pullusb e441f485a6 version bump 2024-12-03 14:27:35 +01:00
pullusb 0cee6163aa Improve file checker
4.0.2

changed: File checker doest not fix directly when clicked:
  - List potential change and display an Apply fix
changed: Enhanced visibility conflict list:
  - Also include viewlayer hide value
  - Allow to set all hide value from the state of one of the three
2024-12-03 14:26:43 +01:00
pullusb 58e6816e39 Fix material move (fill opacity problem comes from blender 4.3 itself) 2024-12-02 14:51:43 +01:00
pullusb 4d6dc06e4e Remove useless pseudo groups now that there is a native group system 2024-12-02 11:39:17 +01:00
pullusb c8763f5ca4 add create empty frames to layer dropdowm menu 2024-11-28 12:29:27 +01:00
pullusb f9e7c9cc3b Fix layer nav operator
4.0.1

- fixed: layer nav operator on page up/down
2024-11-28 12:26:33 +01:00
pullusb 86fb848e4a fix batch reproject frame issue 2024-11-27 18:01:18 +01:00
pullusb abb61ca6d4 Initial code for better cursor_follow behavior (wip) 2024-11-27 16:43:41 +01:00
pullusb 7430bca02f Fix modifiers access 2024-11-27 16:40:31 +01:00
pullusb f5c20a3499 fix bad initialized value in clipboard paste.
4.3 do not  initialize points opacity and radius as expected
2024-11-26 16:30:21 +01:00
pullusb edfefa874a robust attribute copy for duplicate frames 2024-11-26 14:16:29 +01:00
pullusb 6dbf666ee3 Fix duplicate-send to to layer 2024-11-21 15:11:06 +01:00
pullusb d3e4072564 wip copy-move keys 2024-11-14 19:09:52 +01:00
pullusb 63f377d7d1 Fix material picker keymap 2024-11-14 18:02:24 +01:00
pullusb 49c70860a6 gpv3 port: create empty frames - add support for group as source
add utility function to check hide-lock state of nested items
2024-11-14 17:27:57 +01:00
pullusb b525cda28e gpv3 : pick closest layer from stroke 2024-11-14 16:18:56 +01:00
pullusb 05053cff68 fix copy paste layers 2024-11-14 16:11:31 +01:00
pullusb 4937aa32c0 gpv3 world copy cut paste 2024-11-13 18:58:28 +01:00
pullusb 5d930df06b add waning message in readme 2024-11-13 11:10:51 +01:00
pullusb 5d35074d3d fix get addons pref
remove edit line opacity
fix menus
2024-11-12 19:06:57 +01:00
28 changed files with 1495 additions and 735 deletions
+54
View File
@@ -1,5 +1,59 @@
# Changelog # Changelog
5.0.1
- fixed: Restored preference paths as dir path (now need `PATH_SUPPORTS_BLEND_RELATIVE` option)
5.0.0
- added: Update for blender 5+. With retro-compatibility
4.2.0
- added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched)
- No preset shortcut
<!-- - `grease_pencil.delete_points_view_bound` for points -->
<!-- - `grease_pencil.delete_strokes_view_bound` for strokes -->
- operator id_name: `grease_pencil.delete_view_bound`
- Added to Delete menu
- support multiframe
- Only do a dissolve (delete for full stroke)
4.1.3
- fixed: error in "remove stroke duplication" using file checker
- added: `Remove Redundant GP Stroke` Operator (Not exposed, avaialbe in search with developer extras enabled). Allow to use directly without going using file checker
4.1.2
- fixed: Error in Draw cam overlay breaking after random undo step `Ctrl + Z`
4.1.1
- added: option in copy stroke world space to not bake move
4.1.0
- added: operator `list collection visibility conflicts` (not exposed, called from search menu with dev extras enabled in prefs)
4.0.3
changed: File checker doest not fix directly when clicked (also removed choice in preference):
- list potential change and display an `Apply Fix`
changed: Enhanced visibility conflict list:
- also include viewlayer hide value
- allow to set all hide value from the state of one of the three
- fixed: material move operator
4.0.1
- fixed: layer nav operator on page up/down
4.0.0
- changed: version for Blender 4.3 - Breaking retrocompatibility with previous.
3.3.0 3.3.0
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu. - added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
+60 -17
View File
@@ -5,23 +5,36 @@ from bpy.props import (FloatProperty,
EnumProperty, EnumProperty,
StringProperty, StringProperty,
IntProperty) IntProperty)
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):
'''return (identifier, name, description) of enum content''' '''return (identifier, name, description) of enum content'''
return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active] return [(l.name, l.name, '') for l in context.object.data.layers if l != context.object.data.layers.active]
def get_group_list(self, context):
return [(g.name, g.name, '') for g in context.object.data.layer_groups]
class GP_OT_create_empty_frames(bpy.types.Operator): class GP_OT_create_empty_frames(bpy.types.Operator):
bl_idname = "gp.create_empty_frames" bl_idname = "gp.create_empty_frames"
bl_label = "Create Empty Frames" bl_label = "Create Empty Frames"
bl_description = "Create new empty frames on active layer where there is a frame in layer above\n(usefull in color layers to match line frames)" bl_description = "Create new empty frames on active layer where there is a frame in targeted layers\
\n(usefull in color layers to match line frames)"
bl_options = {'REGISTER','UNDO'} bl_options = {'REGISTER','UNDO'}
layers_enum : EnumProperty( layers_enum : EnumProperty(
name="Duplicate to layers", name="Empty Keys from Layer",
description="Duplicate selected keys in active layer and send them to choosen layer", description="Reference keys from layer",
items=get_layer_list items=get_layer_list
) )
groups_enum : EnumProperty(
name="Empty Keys from Group",
description="Duplicate keys from group",
items=get_group_list
)
targeted_layers : EnumProperty( targeted_layers : EnumProperty(
name="Sources", # Empty keys from targets name="Sources", # Empty keys from targets
@@ -34,7 +47,8 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
('ABOVE', 'Layer Directly Above', 'Empty frames from layer directly above'), ('ABOVE', 'Layer Directly Above', 'Empty frames from layer directly above'),
('BELOW', 'Layer Directly Below', 'Empty frames from layer directly below'), ('BELOW', 'Layer Directly Below', 'Empty frames from layer directly below'),
('ALL_VISIBLE', 'Visible', 'Empty frames from all visible layers'), ('ALL_VISIBLE', 'Visible', 'Empty frames from all visible layers'),
('CHOSEN', 'Chosen layer', ''), ('CHOSEN', 'Chosen layer', 'Empty frames from a specific layer'),
('CHOSEN_GROUP', 'Chosen group', 'Empty frames from a specific layer group'),
) )
) )
@@ -63,6 +77,14 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
# Possible preset with shortcut # Possible preset with shortcut
# if event.alt: # if event.alt:
# self.targeted_layers = 'ALL_VISIBLE' # self.targeted_layers = 'ALL_VISIBLE'
gp = context.grease_pencil
layer_from_group = None
if gp.layer_groups.active:
layer_from_group = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
## Can just do if not utils.get_closest_active_layer(context.grease_pencil):
if not gp.layers.active and not layer_from_group:
self.report({'ERROR'}, 'No active layer or active group containing layer on GP object')
return {'CANCELLED'}
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def draw(self, context): def draw(self, context):
@@ -74,7 +96,13 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
if self.layers_enum: if self.layers_enum:
layout.prop(self, 'layers_enum') layout.prop(self, 'layers_enum')
else: else:
layout.label(text='No other layers to match keyframe') layout.label(text='No other layers to match keyframe!', icon='ERROR')
if self.targeted_layers == 'CHOSEN_GROUP':
if self.groups_enum:
layout.prop(self, 'groups_enum')
else:
layout.label(text='No other groups to match keyframe!', icon='ERROR')
elif self.targeted_layers == 'NUMBER': elif self.targeted_layers == 'NUMBER':
row = layout.row() row = layout.row()
@@ -91,23 +119,31 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
def execute(self, context): def execute(self, context):
obj = context.object obj = context.object
gpl = obj.data.layers gp = obj.data
gpl = gp.layers
if gp.layer_groups.active:
reference_layer = utils.get_top_layer_from_group(gp, gp.layer_groups.active)
else:
reference_layer = gpl.active
active_index = next((i for i, l in enumerate(gpl) if l == reference_layer), None)
print(self.targeted_layers) print(self.targeted_layers)
if self.targeted_layers == 'ALL_ABOVE': if self.targeted_layers == 'ALL_ABOVE':
tgt_layers = [l for i, l in enumerate(gpl) if i > gpl.active_index] tgt_layers = [l for i, l in enumerate(gpl) if i > active_index]
elif self.targeted_layers == 'ALL_BELOW': elif self.targeted_layers == 'ALL_BELOW':
tgt_layers = [l for i, l in enumerate(gpl) if i < gpl.active_index] tgt_layers = [l for i, l in enumerate(gpl) if i < active_index]
elif self.targeted_layers == 'ABOVE': elif self.targeted_layers == 'ABOVE':
tgt_layers = [l for i, l in enumerate(gpl) if i == gpl.active_index + 1] tgt_layers = [l for i, l in enumerate(gpl) if i == active_index + 1]
elif self.targeted_layers == 'BELOW': elif self.targeted_layers == 'BELOW':
tgt_layers = [l for i, l in enumerate(gpl) if i == gpl.active_index - 1] tgt_layers = [l for i, l in enumerate(gpl) if i == active_index - 1]
elif self.targeted_layers == 'ALL_VISIBLE': elif self.targeted_layers == 'ALL_VISIBLE':
tgt_layers = [l for l in gpl if not l.hide and l != gpl.active] tgt_layers = [l for l in gpl if not is_hidden(l) and l != gpl.active]
elif self.targeted_layers == 'CHOSEN': elif self.targeted_layers == 'CHOSEN':
if not self.layers_enum: if not self.layers_enum:
@@ -115,17 +151,24 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
return {'CANCELLED'} return {'CANCELLED'}
tgt_layers = [l for l in gpl if l.name == self.layers_enum] tgt_layers = [l for l in gpl if l.name == self.layers_enum]
elif self.targeted_layers == 'CHOSEN_GROUP':
if not self.groups_enum:
self.report({'ERROR'}, f"No chosen groups, aborted")
return {'CANCELLED'}
group = gp.layer_groups.get(self.groups_enum)
tgt_layers = [l for l in gpl if l.parent_group == group]
elif self.targeted_layers == 'NUMBER': elif self.targeted_layers == 'NUMBER':
if self.number == 0: if self.number == 0:
self.report({'ERROR'}, f"Can't have 0 as value") self.report({'ERROR'}, f"Can't have 0 as value")
return {'CANCELLED'} return {'CANCELLED'}
l_range = gpl.active_index + self.number l_range = active_index + self.number
print('l_range: ', l_range) print('l_range: ', l_range)
if self.number > 0: # positive if self.number > 0: # positive
tgt_layers = [l for i, l in enumerate(gpl) if gpl.active_index < i <= l_range] tgt_layers = [l for i, l in enumerate(gpl) if active_index < i <= l_range]
else: else:
tgt_layers = [l for i, l in enumerate(gpl) if gpl.active_index > i >= l_range] tgt_layers = [l for i, l in enumerate(gpl) if active_index > i >= l_range]
if not tgt_layers: if not tgt_layers:
self.report({'ERROR'}, f"No layers found with chosen Targets") self.report({'ERROR'}, f"No layers found with chosen Targets")
@@ -163,7 +206,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
if num in current_frames: if num in current_frames:
continue continue
#Create empty frame #Create empty frame
gpl.active.frames.new(num, active=False) gpl.active.frames.new(num)
fct += 1 fct += 1
gpl.update() gpl.update()
@@ -178,4 +221,4 @@ def register():
bpy.utils.register_class(GP_OT_create_empty_frames) bpy.utils.register_class(GP_OT_create_empty_frames)
def unregister(): def unregister():
bpy.utils.unregister_class(GP_OT_create_empty_frames) bpy.utils.unregister_class(GP_OT_create_empty_frames)
+9 -6
View File
@@ -5,11 +5,14 @@ from ..utils import (location_to_region,
vector_length, vector_length,
draw_gp_stroke, draw_gp_stroke,
extrapolate_points_by_length, extrapolate_points_by_length,
simple_draw_gp_stroke) simple_draw_gp_stroke,
is_hidden,
is_locked)
import bpy import bpy
from math import degrees from math import degrees
from mathutils import Vector from mathutils import Vector
# from os.path import join, basename, exists, dirname, abspath, splitext # from os.path import join, basename, exists, dirname, abspath, splitext
# iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level # iterate over selected layer and all/selected frame and close gaps between line extermities with a tolerance level
@@ -276,9 +279,9 @@ class GPSTK_OT_extend_lines(bpy.types.Operator):
if self.layer_tgt == 'ACTIVE': if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active] lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED': elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not l.hide] lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
elif self.layer_tgt == 'ALL_VISIBLE': elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide] lays = [l for l in ob.data.layers if not is_hidden(l)]
else: else:
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))] lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
@@ -337,9 +340,9 @@ class GPSTK_OT_change_closeline_length(bpy.types.Operator):
if self.layer_tgt == 'ACTIVE': if self.layer_tgt == 'ACTIVE':
lays = [ob.data.layers.active] lays = [ob.data.layers.active]
elif self.layer_tgt == 'SELECTED': elif self.layer_tgt == 'SELECTED':
lays = [l for l in ob.data.layers if l.select and not l.hide] lays = [l for l in ob.data.layers if l.select and not is_hidden(l)]
elif self.layer_tgt == 'ALL_VISIBLE': elif self.layer_tgt == 'ALL_VISIBLE':
lays = [l for l in ob.data.layers if not l.hide] lays = [l for l in ob.data.layers if not is_hidden(l)]
else: else:
lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))] lays = [l for l in ob.data.layers if not any(x in l.name for x in ('spot', 'colo'))]
@@ -375,7 +378,7 @@ class GPSTK_OT_comma_finder(bpy.types.Operator):
def execute(self, context): def execute(self, context):
ct = 0 ct = 0
ob = context.object ob = context.object
lays = [l for l in ob.data.layers if not l.hide and not l.lock] lays = [l for l in ob.data.layers if not is_hidden(l) and not is_locked(l)]
for l in lays: for l in lays:
if not l.current_frame():continue if not l.current_frame():continue
for s in l.current_frame().drawing.strokes: for s in l.current_frame().drawing.strokes:
+4 -3
View File
@@ -3,6 +3,7 @@ import bpy
import re import re
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from math import radians, degrees from math import radians, degrees
from . import utils
# exemple for future improve: https://justinsbarrett.com/tweenmachine/ # exemple for future improve: https://justinsbarrett.com/tweenmachine/
@@ -21,7 +22,7 @@ def get_surrounding_points(fc, frame):
return p_pt, n_pt return p_pt, n_pt
## unused direct breackdown func ## unused direct breakdown func
def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale'), axe=(0,1,2)): def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale'), axe=(0,1,2)):
cf = bpy.context.scene.frame_current# use operator context (may be unsynced timeline) cf = bpy.context.scene.frame_current# use operator context (may be unsynced timeline)
axes_name = ('x', 'y', 'z') axes_name = ('x', 'y', 'z')
@@ -42,7 +43,7 @@ def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale
skipping = [] skipping = []
for fc in action.fcurves: for fc in utils.get_fcurves(obj):
# if fc.data_path.split('"')[1] in bone_names_filter:# bones # if fc.data_path.split('"')[1] in bone_names_filter:# bones
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones # if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
if fc.data_path in channels and fc.array_index in axe:# .split('.')[-1] if fc.data_path in channels and fc.array_index in axe:# .split('.')[-1]
@@ -252,7 +253,7 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
## TODO for ob in context.selected objects, need to reduce list with upper filters... ## TODO for ob in context.selected objects, need to reduce list with upper filters...
for fc in action.fcurves: for fc in utils.get_fcurves(obj):
# if fc.data_path.split('"')[1] in bone_names_filter:# bones # if fc.data_path.split('"')[1] in bone_names_filter:# bones
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones # if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
if fc.data_path in self.channels:# .split('.')[-1]# and fc.array_index in axe if fc.data_path in self.channels:# .split('.')[-1]# and fc.array_index in axe
+157 -186
View File
@@ -1,38 +1,15 @@
# This program is free software; you can redistribute it and/or modify ## GP clipboard : Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends
# it under the terms of the GNU General Public License as published by ## View3D > Toolbar > Gpencil > GP clipboard
# the Free Software Foundation; either version 3 of the License, or ## in 4.2, existed in standalone scripts: https://github.com/Pullusb/GP_clipboard
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
## based on GPclipboard 1.3.2 (just stripped addon prefs)
bl_info = {
"name": "GP clipboard",
"description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
"author": "Samuel Bernou",
"version": (1, 3, 3),
"blender": (2, 83, 0),
"location": "View3D > Toolbar > Gpencil > GP clipboard",
"warning": "",
"doc_url": "https://github.com/Pullusb/GP_clipboard",
"category": "Object" }
import bpy import bpy
import os
import mathutils import mathutils
from mathutils import Vector from mathutils import Vector
import json 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 pprint import pprint 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'''
@@ -45,104 +22,106 @@ def convertAttr(Attr):
else: else:
return(Attr) return(Attr)
def getMatrix (layer) : def getMatrix(layer) :
matrix = mathutils.Matrix.Identity(4) matrix = mathutils.Matrix.Identity(4)
if layer.is_parented: if parent := layer.parent:
if layer.parent_type == 'BONE': if parent.type == 'ARMATURE' and layer.parent_bone:
object = layer.parent bone = parent.pose.bones[layer.parent_bone]
bone = object.pose.bones[layer.parent_bone] matrix = bone.matrix @ parent.matrix_world
matrix = bone.matrix @ object.matrix_world matrix = matrix.copy() @ layer.matrix_parent_inverse
matrix = matrix.copy() @ layer.matrix_inverse else:
else : matrix = parent.matrix_world @ layer.matrix_parent_inverse
matrix = layer.parent.matrix_world @ layer.matrix_inverse
return matrix.copy() return matrix.copy()
default_pt_uv_fill = Vector((0.5, 0.5)) # default_pt_uv_fill = Vector((0.5, 0.5))
def dump_gp_point(p, l, obj, def dump_gp_point(p, l, obj,
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True): radius=True, opacity=True, vertex_color=True, fill_color=True, uv_factor=True, rotation=True):
'''add properties of a given points to a dic and return it''' '''add properties of a given points to a dic and return it'''
pdic = {} point_dict = {}
#point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type' #point_attr_list = ('co', 'radius', 'select', 'opacity') #select#'rna_type'
#for att in point_attr_list: #for att in point_attr_list:
# pdic[att] = convertAttr(getattr(p, att)) # point_dict[att] = convertAttr(getattr(p, att))
if l.parent: if l.parent:
mat = getMatrix(l) mat = getMatrix(l)
pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co')) point_dict['position'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'position'))
else: else:
pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co')) point_dict['position'] = convertAttr(obj.matrix_world @ getattr(p,'position'))
# pdic['select'] = convertAttr(getattr(p,'select')) # need selection ? # point_dict['select'] = convertAttr(getattr(p,'select')) # need selection ?
if pressure and p.pressure != 1.0: if radius and p.radius != 1.0:
pdic['pressure'] = convertAttr(getattr(p,'pressure')) point_dict['radius'] = convertAttr(getattr(p,'radius'))
if strength and p.strength != 1.0:
pdic['strength'] = convertAttr(getattr(p,'strength')) if opacity and p.opacity != 1.0:
point_dict['opacity'] = convertAttr(getattr(p,'opacity'))
## get vertex color (long...) ## get vertex color (long...)
if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0): if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
pdic['vertex_color'] = convertAttr(p.vertex_color) point_dict['vertex_color'] = convertAttr(p.vertex_color)
## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...) if rotation and p.rotation != 0.0:
if uv_fill and p.uv_fill != default_pt_uv_fill: point_dict['rotation'] = convertAttr(p.rotation)
pdic['uv_fill'] = convertAttr(p.uv_fill)
if uv_factor and p.uv_factor != 0.0:
pdic['uv_factor'] = convertAttr(p.uv_factor)
if uv_rotation and p.uv_rotation != 0.0:
pdic['uv_rotation'] = convertAttr(p.uv_rotation)
return pdic ## No time infos
# if delta_time and p.delta_time != 0.0:
# point_dict['delta_time'] = convertAttr(getattr(p,'delta_time'))
return point_dict
def dump_gp_stroke_range(s, sid, l, obj, def dump_gp_stroke_range(s, sid, l, obj,
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True): radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
'''Get a grease pencil stroke and return a dic with attribute '''Get a grease pencil stroke and return a dic with attribute
(points attribute being a dic of dics to store points and their attributes) (points attribute being a dic of dics to store points and their attributes)
''' '''
sdic = {} stroke_dict = {}
stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles' # stroke_attr_list = ('line_width',)
for att in stroke_attr_list: # for att in stroke_attr_list:
sdic[att] = getattr(s, att) # stroke_dict[att] = getattr(s, att)
## Dump following these value only if they are non default ## Dump following these value only if they are non default
if s.material_index != 0: if s.material_index != 0:
sdic['material_index'] = s.material_index stroke_dict['material_index'] = s.material_index
if getattr(s, 'draw_cyclic', None): # pre-2.92
sdic['draw_cyclic'] = s.draw_cyclic
if getattr(s, 'use_cyclic', None): # from 2.92
sdic['use_cyclic'] = s.use_cyclic
if s.uv_scale != 1.0:
sdic['uv_scale'] = s.uv_scale
if s.uv_rotation != 0.0:
sdic['uv_rotation'] = s.uv_rotation
if s.hardness != 1.0:
sdic['hardness'] = s.hardness
if s.uv_translation != Vector((0.0, 0.0)):
sdic['uv_translation'] = convertAttr(s.uv_translation)
if s.vertex_color_fill[:] != (0,0,0,0): if s.cyclic:
sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill) stroke_dict['cyclic'] = s.cyclic
if s.softness != 0.0:
stroke_dict['softness'] = s.softness
if s.aspect_ratio != 1.0:
stroke_dict['aspect_ratio'] = s.aspect_ratio
if s.start_cap != 0:
stroke_dict['start_cap'] = s.start_cap
if s.end_cap != 0:
stroke_dict['end_cap'] = s.end_cap
if fill_color and s.fill_color[:] != (0,0,0,0):
stroke_dict['fill_color'] = convertAttr(s.fill_color)
if fill_opacity and s.fill_opacity != 0.0:
stroke_dict['fill_opacity'] = s.fill_opacity
## No time infos
# if s.time_start != 0.0:
# stroke_dict['time_start'] = s.time_start
points = [] points = []
if sid is None: # no ids, just full points... if sid is None: # no ids, just full points...
for p in s.points: for p in s.points:
points.append(dump_gp_point(p,l,obj, points.append(dump_gp_point(p, l, obj,
pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation)) radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
else: else:
for pid in sid: for pid in sid:
points.append(dump_gp_point(s.points[pid],l,obj, points.append(dump_gp_point(s.points[pid], l, obj,
pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation)) radius=radius, opacity=opacity, vertex_color=vertex_color, rotation=rotation))
sdic['points'] = points
return sdic stroke_dict['points'] = points
return stroke_dict
def copycut_strokes(layers=None, copy=True, keep_empty=True): def copycut_strokes(layers=None, copy=True, keep_empty=True):
@@ -161,7 +140,7 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
# color = gp.palettes.active.colors.active.name # color = gp.palettes.active.colors.active.name
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock] # [] layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # []
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@@ -175,26 +154,30 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
if not copy: if not copy:
staylist = [] # init part of strokes that must survive on this layer staylist = [] # init part of strokes that must survive on this layer
for s in f.drawing.strokes: rm_list = [] # init strokes that must be removed from this layer
if s.select: for s_index, stroke in enumerate(f.drawing.strokes):
if stroke.select:
# separate in multiple stroke if parts of the strokes a selected. # separate in multiple stroke if parts of the strokes a selected.
sel = [i for i, p in enumerate(s.points) if p.select] sel = [i for i, p in enumerate(stroke.points) if p.select]
substrokes = [] # list of list containing isolated selection substrokes = [] # list of list containing isolated selection
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
# continuity stroke have same substract result between point index and enumerator
for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):
group = list(map(itemgetter(1), g)) group = list(map(itemgetter(1), g))
substrokes.append(group) substrokes.append(group)
for ss in substrokes: for ss in substrokes:
if len(ss) > 1: # avoid copy isolated points if len(ss) > 1: # avoid copy isolated points
stroke_list.append(dump_gp_stroke_range(s,ss,l,obj)) stroke_list.append(dump_gp_stroke_range(stroke, ss, l, obj))
# Cutting operation # Cutting operation
if not copy: if not copy:
maxindex = len(s.points)-1 maxindex = len(stroke.points)-1
if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
f.drawing.strokes.remove(s) # f.drawing.strokes.remove(stroke) # gpv2
rm_list.append(s_index)
else: else:
neg = [i for i, p in enumerate(s.points) if not p.select] neg = [i for i, p in enumerate(stroke.points) if not p.select]
staying = [] staying = []
for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]): for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
@@ -208,37 +191,30 @@ def copycut_strokes(layers=None, copy=True, keep_empty=True):
for ns in staying: for ns in staying:
if len(ns) > 1: if len(ns) > 1:
staylist.append(dump_gp_stroke_range(s,ns,l,obj)) staylist.append(dump_gp_stroke_range(stroke, ns, l, obj))
# make a negative list containing all last index # make a negative list containing all last index
if rm_list:
'''#full stroke version f.drawing.remove_strokes(indices=rm_list)
# if s.colorname == color: #line for future filters
stroke_list.append(dump_gp_stroke(s,l))
#delete stroke on the fly
if not copy:
f.drawing.strokes.remove(s)
'''
if not copy: if not copy:
selected_ids = [i for i, s in enumerate(f.drawing.strokes) if s.select]
# delete all selected strokes... # delete all selected strokes...
for s in f.drawing.strokes: if selected_ids:
if s.select: f.drawing.remove_strokes(indices=selected_ids)
f.drawing.strokes.remove(s)
# ...recreate these uncutted ones # ...recreate these uncutted ones
#pprint(staylist) #pprint(staylist)
if staylist: if staylist:
add_multiple_strokes(staylist, l) add_multiple_strokes(staylist, l)
#for ns in staylist:#weirdly recreate the stroke twice !
# add_stroke(ns, f, l)
#if nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear) # If nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
if not copy and not keep_empty:# if not copy and not keep_empty:
if not len(f.drawing.strokes): if not len(f.drawing.strokes):
l.frames.remove(f) l.frames.remove(f)
print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds') print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
#print(stroke_list) #print(stroke_list)
return stroke_list return stroke_list
@@ -260,7 +236,7 @@ def copy_all_strokes(layers=None):
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock]# include locked ? layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)]# include locked ?
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@@ -284,7 +260,7 @@ def copy_all_strokes(layers=None):
""" """
def copy_all_strokes_in_frame(frame=None, layers=None, obj=None, def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True): radius=True, opacity=True, vertex_color=True, fill_color=True, fill_opacity=True, rotation=True):
''' '''
copy all stroke, not affected by selection on active frame copy all stroke, not affected by selection on active frame
layers can be None, a single layer object or list of layer object as filter layers can be None, a single layer object or list of layer object as filter
@@ -300,7 +276,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
if not layers: if not layers:
# by default all visible layers # by default all visible layers
layers = [l for l in gpl if not l.hide and not l.lock] # include locked ? layers = [l for l in gpl if not is_hidden(l) and not is_locked(l)] # include locked ?
if not isinstance(layers, list): if not isinstance(layers, list):
# if a single layer object is send put in a list # if a single layer object is send put in a list
layers = [layers] layers = [layers]
@@ -318,7 +294,7 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
# if s.select: # if s.select:
# send index of all points to get the whole stroke with "range" # send index of all points to get the whole stroke with "range"
stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj, stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj,
pressure=pressure,strength=strength,vertex_color=vertex_color,uv_fill=uv_fill,uv_factor=uv_factor,uv_rotation=uv_rotation)) radius=radius, opacity=opacity, vertex_color=vertex_color, fill_color=fill_color, fill_opacity=fill_opacity, rotation=rotation))
# print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds') # print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
return stroke_list return stroke_list
@@ -326,49 +302,43 @@ def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
def add_stroke(s, frame, layer, obj, select=False): def add_stroke(s, frame, layer, obj, select=False):
'''add stroke on a given frame, (layer is for parentage setting)''' '''add stroke on a given frame, (layer is for parentage setting)'''
# print(3*'-',s) # print(3*'-',s)
ns = frame.drawing.strokes.new() pts_to_add = len(s['points'])
frame.drawing.add_strokes([pts_to_add])
ns = frame.drawing.strokes[-1]
## set strokes atrributes
for att, val in s.items(): for att, val in s.items():
if att not in ('points'): if att not in ('points'):
setattr(ns, att, val) setattr(ns, att, val)
pts_to_add = len(s['points'])
# print(pts_to_add, 'points')#dbg
ns.points.add(pts_to_add)
ob_mat_inv = obj.matrix_world.inverted() ob_mat_inv = obj.matrix_world.inverted()
## patch pressure 1 if layer.parent:
# pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened layer_matrix = getMatrix(layer).inverted()
transform_matrix = ob_mat_inv @ layer_matrix
if layer.is_parented:
mat = getMatrix(layer).inverted()
for i, pt in enumerate(s['points']):
for k, v in pt.items():
if k == 'co':
setattr(ns.points[i], k, v)
ns.points[i].position = ob_mat_inv @ mat @ ns.points[i].position # invert of object * invert of layer * coordinate
else:
setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
else: else:
for i, pt in enumerate(s['points']): transform_matrix = ob_mat_inv
for k, v in pt.items():
if k == 'co': ## Set points attributes
setattr(ns.points[i], k, v) for i, pt in enumerate(s['points']):
ns.points[i].position = ob_mat_inv @ ns.points[i].position# invert of object * coordinate for k, v in pt.items():
else: if k == 'position':
setattr(ns.points[i], k, v) setattr(ns.points[i], k, v)
if select: ns.points[i].position = transform_matrix @ ns.points[i].position # invert of object * invert of layer * coordinate
ns.points[i].select = True else:
setattr(ns.points[i], k, v)
if select:
ns.points[i].select = True
## Opacity initialized at 0.0 (should be 1.0)
if not 'opacity' in pt:
ns.points[i].opacity = 1.0
## Radius initialized at 0.0 (should probably be 0.01)
if not 'radius' in pt:
ns.points[i].radius = 0.01
## trigger updapte (in 2.93 fix some drawing problem with fills and UVs)
ns.points.update()
## patch pressure 2
# ns.points.foreach_set('pressure', pressure_flat_list)
def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False): def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False):
''' '''
@@ -404,11 +374,7 @@ def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select
for s in stroke_list: for s in stroke_list:
add_stroke(s, target_frame, layer, obj, select=select) add_stroke(s, target_frame, layer, obj, select=select)
'''
for s in stroke_data:
add_stroke(s, target_frame)
'''
# print(len(stroke_list), 'strokes pasted') # print(len(stroke_list), 'strokes pasted')
@@ -431,7 +397,7 @@ class GPCLIP_OT_copy_strokes(bpy.types.Operator):
# return {"CANCELLED"} # return {"CANCELLED"}
t0 = time() t0 = time()
#ct = check_pressure() #ct = check_radius()
strokelist = copycut_strokes(copy=True, keep_empty=True) strokelist = copycut_strokes(copy=True, keep_empty=True)
if not strokelist: if not strokelist:
self.report({'ERROR'}, 'Nothing to copy') self.report({'ERROR'}, 'Nothing to copy')
@@ -448,7 +414,7 @@ class GPCLIP_OT_cut_strokes(bpy.types.Operator):
bl_idname = "gp.cut_strokes" bl_idname = "gp.cut_strokes"
bl_label = "GP Cut strokes" bl_label = "GP Cut strokes"
bl_description = "Cut strokes to text in paperclip" bl_description = "Cut strokes to text in paperclip"
bl_options = {"REGISTER"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@@ -460,7 +426,7 @@ class GPCLIP_OT_cut_strokes(bpy.types.Operator):
# return {"CANCELLED"} # return {"CANCELLED"}
t0 = time() t0 = time()
strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_pressure() strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_radius()
if not strokelist: if not strokelist:
self.report({'ERROR'},'Nothing to cut') self.report({'ERROR'},'Nothing to cut')
return {"CANCELLED"} return {"CANCELLED"}
@@ -512,18 +478,23 @@ 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'
pressure : bpy.props.BoolProperty(name='pressure', default=True, bake_moves : bpy.props.BoolProperty(name='Bake Move', default=True,
description='Dump point pressure attribute (already skipped if at default value)') description='Copy every frame where object has moved, else copy only existing frames')
strength : bpy.props.BoolProperty(name='strength', default=True,
description='Dump point strength attribute (already skipped if at default value)') 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,
description='Dump point opacity attribute (already skipped if at default value)')
vertex_color : bpy.props.BoolProperty(name='vertex color', default=True, vertex_color : bpy.props.BoolProperty(name='vertex color', default=True,
description='Dump point vertex_color attribute (already skipped if at default value)') description='Dump point vertex_color attribute (already skipped if at default value)')
uv_fill : bpy.props.BoolProperty(name='uv fill', default=True, fill_color : bpy.props.BoolProperty(name='fill color', default=True,
description='Dump point uv_fill attribute (already skipped if at default value)') description='Dump point fill_color attribute (already skipped if at default value)')
fill_opacity : bpy.props.BoolProperty(name='fill opacity', default=True,
description='Dump point fill_opacity attribute (already skipped if at default value)')
uv_factor : bpy.props.BoolProperty(name='uv factor', default=True, uv_factor : bpy.props.BoolProperty(name='uv factor', default=True,
description='Dump point uv_factor attribute (already skipped if at default value)') description='Dump point uv_factor attribute (already skipped if at default value)')
uv_rotation : bpy.props.BoolProperty(name='uv rotation', default=True, rotation : bpy.props.BoolProperty(name='rotation', default=True,
description='Dump point uv_rotation attribute (already skipped if at default value)') description='Dump point rotation attribute (already skipped if at default value)')
def invoke(self, context, event): def invoke(self, context, event):
# self.file_dump = event.ctrl # self.file_dump = event.ctrl
@@ -534,35 +505,35 @@ 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, 'pressure') col.prop(self, 'radius')
col.prop(self, 'strength') col.prop(self, 'opacity')
col.prop(self, 'vertex_color') col.prop(self, 'vertex_color')
col.prop(self, 'uv_fill') col.prop(self, 'fill_color')
col.prop(self, 'uv_factor') col.prop(self, 'fill_opacity')
col.prop(self, 'uv_rotation') col.prop(self, 'rotation')
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
obj = context.object obj = context.object
gpl = obj.data.layers gpl = obj.data.layers
t0 = time() t0 = time()
#ct = check_pressure() #ct = check_radius()
layerdic = {} layerdic = {}
layerpool = [l for l in gpl if not l.hide and l.select] # and not l.lock layerpool = [l for l in gpl if not is_hidden(l) and l.select] # and not is_locked(l)
if not layerpool: if not layerpool:
self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only') self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
return {"CANCELLED"} return {"CANCELLED"}
if not bake_moves: # copy only drawed frames as is. if not self.bake_moves: # copy only drawed frames as is.
for l in layerpool: for l in layerpool:
if not l.frames: if not l.frames:
continue# skip empty layers continue # skip empty layers
frame_dic = {} frame_dic = {}
for f in l.frames: for f in l.frames:
@@ -570,8 +541,8 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
continue continue
context.scene.frame_set(f.frame_number) # use matrix of this frame context.scene.frame_set(f.frame_number) # use matrix of this frame
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj, strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color, radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation) fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
frame_dic[f.frame_number] = strokelist frame_dic[f.frame_number] = strokelist
@@ -607,8 +578,8 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
continue continue
strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj, strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color, radius=self.radius, opacity=self.opacity, vertex_color=self.vertex_color,
uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation) fill_color=self.fill_color, fill_opacity=self.fill_opacity, rotation=self.rotation)
frame_dic[i] = strokelist frame_dic[i] = strokelist
@@ -640,7 +611,7 @@ class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
obj = context.object obj = context.object
gpl = obj.data.layers gpl = obj.data.layers
t0 = time() t0 = time()
#add a validity check por the content of the paperclip (check if not data.startswith('[{') ? ) # add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
try: try:
data = json.loads(bpy.context.window_manager.clipboard) data = json.loads(bpy.context.window_manager.clipboard)
except: except:
+62 -15
View File
@@ -2,6 +2,8 @@
import bpy import bpy
import mathutils import mathutils
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from bpy.app.handlers import persistent
from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position from .utils import get_gp_draw_plane, region_to_location, get_view_origin_position
## override all sursor snap shortcut with this in keymap ## override all sursor snap shortcut with this in keymap
@@ -105,20 +107,24 @@ def swap_keymap_by_id(org_idname, new_idname):
k.idname = new_idname k.idname = new_idname
# prev_matrix = mathutils.Matrix()
prev_matrix = None prev_matrix = None
# @call_once(bpy.app.handlers.frame_change_post) # @call_once(bpy.app.handlers.frame_change_post)
def cursor_follow_update(self,context): ## used in properties file to register in boolprop update
def cursor_follow_update(self, context):
'''append or remove cursor_follow handler according a boolean''' '''append or remove cursor_follow handler according a boolean'''
ob = bpy.context.object
if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
global prev_matrix global prev_matrix
# imported in properties to register in boolprop update # imported in properties to register in boolprop update
if self.cursor_follow:#True if self.cursor_follow:#True
if ob:
# out of below condition to be called when setting target as well
prev_matrix = ob.matrix_world.copy()
if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]: if not cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
if context.object:
prev_matrix = context.object.matrix_world
bpy.app.handlers.frame_change_post.append(cursor_follow) bpy.app.handlers.frame_change_post.append(cursor_follow)
else:#False else:#False
@@ -129,11 +135,13 @@ def cursor_follow_update(self,context):
def cursor_follow(scene): def cursor_follow(scene):
'''Handler to make the cursor follow active object matrix changes on frame change''' '''Handler to make the cursor follow active object matrix changes on frame change'''
## TODO update global prev_matrix to equal current_matrix on selection change (need another handler)... ob = bpy.context.object
if not bpy.context.object: if bpy.context.scene.gptoolprops.cursor_follow_target:
## override with target object is specified
ob = bpy.context.scene.gptoolprops.cursor_follow_target
if not ob:
return return
global prev_matrix global prev_matrix
ob = bpy.context.object
current_matrix = ob.matrix_world current_matrix = ob.matrix_world
if not prev_matrix: if not prev_matrix:
prev_matrix = current_matrix.copy() prev_matrix = current_matrix.copy()
@@ -146,15 +154,48 @@ def cursor_follow(scene):
## translation only ## translation only
# scene.cursor.location += (current_matrix - prev_matrix).to_translation() # scene.cursor.location += (current_matrix - prev_matrix).to_translation()
# print('offset:', (current_matrix - prev_matrix).to_translation())
## full ## full
scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location) scene.cursor.location = current_matrix @ (prev_matrix.inverted() @ scene.cursor.location)
# store for next use # store for next use
prev_matrix = current_matrix.copy() prev_matrix = current_matrix.copy()
prev_active_obj = None
## Add check for object selection change
def selection_changed():
"""Callback function for selection changes"""
if not bpy.context.scene.gptoolprops.cursor_follow:
return
if bpy.context.scene.gptoolprops.cursor_follow_target:
# we are following a target, nothing to update on selection change
return
global prev_matrix, prev_active_obj
if prev_active_obj != bpy.context.object:
## Set stored matrix to active object
prev_matrix = bpy.context.object.matrix_world.copy()
prev_active_obj = bpy.context.object
## Note: Same owner as layer manager (will be removed as well)
def subscribe_object_change():
subscribe_to = (bpy.types.LayerObjects, 'active')
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.
args=(),
notify=selection_changed,
options={'PERSISTENT'},
)
@persistent
def subscribe_object_change_handler(dummy):
subscribe_object_change()
classes = ( classes = (
GPTB_OT_cusor_snap, GPTB_OT_cusor_snap,
@@ -163,14 +204,18 @@ GPTB_OT_cusor_snap,
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap # swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
## Follow cursor matrix update on object change
bpy.app.handlers.load_post.append(subscribe_object_change_handler) # select_change
# ## Directly set msgbus to work at first addon activation # select_change
bpy.app.timers.register(subscribe_object_change, first_interval=1) # select_change
# bpy.app.handlers.frame_change_post.append(cursor_follow) ## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
def unregister(): def unregister():
# bpy.app.handlers.frame_change_post.remove(cursor_follow) bpy.app.handlers.load_post.remove(subscribe_object_change_handler) # select_change
# swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap # swap_keymap_by_id('view3d.cursor_snap','view3d.cursor3d')#Restore normal snap
@@ -179,4 +224,6 @@ def unregister():
# force remove handler if it's there at unregister # force remove handler if it's there at unregister
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]: if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
bpy.app.handlers.frame_change_post.remove(cursor_follow) bpy.app.handlers.frame_change_post.remove(cursor_follow)
bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
+173
View File
@@ -0,0 +1,173 @@
import bpy
from . import utils
from bpy_extras.view3d_utils import location_3d_to_region_2d
def is_stroke_in_view(stroke, matrix, region, region_3d):
## for optimization, use first and last
region_width = region.width
region_height = region.height
point_list = [stroke.points[0], stroke.points[-1]]
# point_list = stroke.points # whole points
coord_list = [matrix @ point.position for point in point_list]
for coord in coord_list:
# Convert 3D coordinates to 2D screen coordinates
screen_coord = location_3d_to_region_2d(region, region_3d, coord)
if screen_coord is None:
continue
# Check if the point is within the viewport bounds
if 0 <= screen_coord.x <= region_width and 0 <= screen_coord.y <= region_height:
## one point in view, in view
return True
return False
class GP_OT_delete_view_bound(bpy.types.Operator):
bl_idname = "grease_pencil.delete_view_bound"
bl_label = "Delete View Bound"
bl_description = "Delete all selected strokes / points only if there are in view (current viewport region)\
\nnote: does not work in multiframe yet)"
bl_options = {'REGISTER', 'UNDO'}
# def invoke(self, context, event):
## TODO: add scope (default to automatic)
## TODO: add option for normal delete (currenly only dissolve), probably another operator.
@classmethod
def poll(cls, context):
return context.mode == 'EDIT_GREASE_PENCIL'
def execute(self, context):
gp = context.grease_pencil
if not gp:
self.report({'ERROR'}, "No Grease Pencil object found")
return {'CANCELLED'}
ob = context.object
if not ob or ob.type != 'GREASEPENCIL':
self.report({'ERROR'}, "Active object is not a Grease Pencil object")
return {'CANCELLED'}
select_mode = context.scene.tool_settings.gpencil_selectmode_edit
matrix_world = ob.matrix_world.copy()
## only visibile and unlocked layer in active GP object
layers = [l for l in gp.layers if not l.hide and not l.lock]
region = context.region
region_3d = context.space_data.region_3d
removed_ct = 0
avoid_ct = 0
for layer in layers:
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
## multiframe, affect all selected frames and active frame
target_frames = [f for f in layer.frames if f.select or f == layer.current_frame()]
else:
## Only active frame
current = layer.current_frame()
if not current:
continue
target_frames = [current]
for frame in target_frames:
drawing = frame.drawing
if not drawing or not drawing.strokes:
continue
stroke_indices_to_delete = []
## List comprehention version (For stroke mode only)
# stroke_indices_to_delete = [sidx for sidx, stroke in enumerate(drawing.strokes) if stroke.select and is_stroke_in_view(stroke, matrix_world, region, region_3d)]
for sid, stroke in enumerate(drawing.strokes):
if not stroke.select:
continue
# Check if the stroke is within the view bounds
if not is_stroke_in_view(stroke, matrix_world, region, region_3d):
avoid_ct += 1
continue
## No need for further check if we are in stroke mod (direct delete)
if select_mode == 'STROKE':
stroke_indices_to_delete.append(sid)
removed_ct += 1
continue
## --- Handle points ---
## remove points, only Dissolve for now.
select_count = len([p for p in stroke.points if p.select])
if select_count == len(stroke.points):
## Mark as full delete and go next
stroke_indices_to_delete.append(sid)
continue
## Make a copy of deselected points attributes
point_attrs = []
for p in stroke.points:
if p.select:
continue
point_attrs.append([p.position.copy(), p.rotation, p.radius, p.opacity])
## remove as many points as select_count
stroke.remove_points(select_count)
## re-assign attributes in order
for i, p in enumerate(stroke.points):
if i < len(point_attrs):
p.position = point_attrs[i][0]
p.rotation = point_attrs[i][1]
p.radius = point_attrs[i][2]
p.opacity = point_attrs[i][3]
p.select = False # Deselect all points after processing
removed_ct += 1
## End of stroke loop (here either part if the stroke is deleted or marked for deletion )
if stroke_indices_to_delete:
# print(f'layer {layer.name} : delete {len(stroke_indices_to_delete)} strokes')
drawing.remove_strokes(indices=stroke_indices_to_delete)
if removed_ct and avoid_ct:
self.report({'INFO'}, f"Skipped {avoid_ct} out of view")
elif not removed_ct and not avoid_ct:
self.report({'WARNING'}, "Nothing to Delete")
elif not removed_ct and avoid_ct:
self.report({'WARNING'}, "All selected strokes are out of view")
return {'FINISHED'}
def draw_delete_view_bound_ui(self, context):
layout = self.layout
layout.operator('grease_pencil.delete_view_bound', text='Delete View bound', icon='LOCKVIEW_ON') # RESTRICT_VIEW_ON
classes = (
# GP_OT_delete_strokes_view_bound,
# GP_OT_delete_points_view_bound,
GP_OT_delete_view_bound,
)
def register():
if bpy.app.background:
return
for cl in classes:
bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_edit_greasepencil_delete.append(draw_delete_view_bound_ui)
## make scene property for empty key preservation and bake movement for layers...
# register_keymaps()
def unregister():
if bpy.app.background:
return
bpy.types.VIEW3D_MT_edit_greasepencil_delete.remove(draw_delete_view_bound_ui)
# unregister_keymaps()
for cl in reversed(classes):
bpy.utils.unregister_class(cl)
+2 -1
View File
@@ -11,6 +11,7 @@ from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vecto
location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d location_3d_to_region_2d, region_2d_to_origin_3d, region_2d_to_location_3d
from time import time from time import time
from math import pi, cos, sin from math import pi, cos, sin
from .utils import is_locked, is_hidden
def get_gp_mat(gp, name, set_active=False): def get_gp_mat(gp, name, set_active=False):
@@ -440,7 +441,7 @@ class GPTB_OT_eraser(Operator):
t0 = time() t0 = time()
gp_mats = gp.data.materials gp_mats = gp.data.materials
gp_layers = [l for l in gp.data.layers if not l.lock or l.hide] gp_layers = [l for l in gp.data.layers if not is_locked(l) or is_hidden(l)]
self.gp_frames = [l.current_frame() for l in gp_layers] self.gp_frames = [l.current_frame() for l in gp_layers]
''' '''
points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes] points_data = [(s, f, gp_mats[s.material_index]) for f in gp_frames for s in f.drawing.strokes]
+339 -84
View File
@@ -2,32 +2,94 @@ import bpy
import os import os
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from . import utils from . import utils
def remove_stroke_exact_duplications(apply=True): from bpy.props import (BoolProperty,
PointerProperty,
CollectionProperty,
StringProperty)
def remove_stroke_exact_duplications(apply=True, verbose=True, select=False):
'''Remove accidental stroke duplication (points exactly in the same place) '''Remove accidental stroke duplication (points exactly in the same place)
:apply: Remove the duplication instead of just listing dupes :apply: Remove the duplication instead of just listing dupes
:select: Select the duplicated strokes instead of removing them (disabled by apply)
return number of duplication found/deleted return number of duplication found/deleted
''' '''
# TODO: add additional check of material (even if unlikely to happen) # TODO: Add additional check of material (even if unlikely to happen, better to avoid false positive)
ct = 0 ct = 0
gp_datas = [gp for gp in bpy.data.grease_pencils] if verbose:
print('\nRemove redundant strokes in GP frames...')
gp_source = bpy.data.grease_pencils if bpy.app.version >= (5,0,0) else bpy.data.grease_pencils_v3
gp_datas = [gp for gp in gp_source]
for gp in gp_datas: for gp in gp_datas:
for l in gp.layers: for l in gp.layers:
for f in l.frames: for f in l.frames:
stroke_list = [] stroke_list = []
for s in reversed(f.drawing.strokes): idx_to_delete = []
point_list = [p.position for p in s.points] for idx, s in enumerate(f.drawing.strokes):
point_list = [p.position.copy() for p in s.points]
if point_list in stroke_list: if point_list in stroke_list:
ct += 1 ct += 1
if apply: idx_to_delete.append(idx)
# Remove redundancy if not apply and select:
f.drawing.strokes.remove(s) s.select = True
else: else:
stroke_list.append(point_list) stroke_list.append(point_list)
if not apply and select:
s.select = False
if idx_to_delete:
if verbose:
print(f"{gp.name} > {l.name} > frame {f.frame_number}: {len(idx_to_delete)} strokes")
if apply:
# Remove redundancy (carefull, passing an empty list delete all strokes)
f.drawing.remove_strokes(indices=idx_to_delete)
return ct return ct
class GPTB_OT_remove_stroke_duplication(bpy.types.Operator):
bl_idname = "gp.remove_stroke_duplication"
bl_label = "Remove Redundant GP Stroke"
bl_description = "Within every frame, remove every strokes that are exactly superposed with a previous one"
bl_options = {"REGISTER", "UNDO"}
apply : bpy.props.BoolProperty(name="Remove Stroke", default=True,
description="Remove the duplication, else list only",
options={'SKIP_SAVE'})
select : bpy.props.BoolProperty(name="Select Duplicated Strokes", default=False)
def invoke(self, context, event):
# self.file_dump = event.ctrl
return context.window_manager.invoke_props_dialog(self) # , width=400
# return self.execute(context)
def draw(self, context):
layout=self.layout
layout.use_property_split = True
col = layout.column()
col.prop(self, 'apply')
row=col.row(align=True)
row.prop(self, 'select')
row.enabled = not self.apply
if not self.apply and not self.select:
col.label(text="Only list in console and report number of duplications", icon='INFO')
def execute(self, context):
ct = remove_stroke_exact_duplications(apply=self.apply, select=self.select, verbose=True)
if ct > 0:
if self.apply:
self.report({'INFO'}, f'Removed {ct} strokes duplications')
else:
self.report({'INFO'}, f'Found {ct} strokes duplications')
else:
self.report({'INFO'}, 'No stroke duplication found')
return {'FINISHED'}
class GPTB_OT_file_checker(bpy.types.Operator): class GPTB_OT_file_checker(bpy.types.Operator):
bl_idname = "gp.file_checker" bl_idname = "gp.file_checker"
bl_label = "Check File" bl_label = "Check File"
@@ -53,6 +115,10 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# Disable use light on all object # Disable use light on all object
# Remove redundant strokes in frames # Remove redundant strokes in frames
apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False,
description="Apply possible fixes instead of just listing (pop the list again in fix mode)",
options={'SKIP_SAVE'})
def invoke(self, context, event): def invoke(self, context, event):
# need some self-control (I had to...) # need some self-control (I had to...)
self.ctrl = event.ctrl self.ctrl = event.ctrl
@@ -63,10 +129,13 @@ class GPTB_OT_file_checker(bpy.types.Operator):
fix = prefs.fixprops fix = prefs.fixprops
problems = [] problems = []
apply = not fix.check_only ## Old method : Apply fixes based on pref (inverted by ctrl key)
# # If Ctrl is pressed, invert behavior (invert boolean)
# apply ^= self.ctrl
# If Ctrl is pressed, invert behavior (invert boolean) apply = self.apply_fixes
apply ^= self.ctrl if self.ctrl:
apply = True
## Lock main cam: ## Lock main cam:
if fix.lock_main_cam: if fix.lock_main_cam:
@@ -153,29 +222,32 @@ class GPTB_OT_file_checker(bpy.types.Operator):
o.use_grease_pencil_lights = False o.use_grease_pencil_lights = False
## Disabled animation ## Disabled animation
if fix.list_disabled_anim: # TODO : fix for Blender 5.0
fcu_ct = 0 if bpy.app.version < (5,0,0):
for act in bpy.data.actions: if fix.list_disabled_anim:
if not act.users: fcu_ct = 0
continue for act in bpy.data.actions:
for fcu in act.fcurves: if not act.users:
if fcu.mute: continue
fcu_ct += 1 for fcu in act.fcurves:
print(f"muted: {act.name} > {fcu.data_path}") if fcu.mute:
if fcu_ct: fcu_ct += 1
problems.append(f'{fcu_ct} anim channel disabled (details in console)') print(f"muted: {act.name} > {fcu.data_path}")
if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
## Object visibility conflict ## Object visibility conflict
if fix.list_obj_vis_conflict: if fix.list_obj_vis_conflict:
viz_ct = 0 viz_ct = 0
for o in context.scene.objects: for o in context.scene.objects:
if o.hide_viewport != o.hide_render: if not (o.hide_get() == o.hide_viewport == o.hide_render):
hv = 'No' if o.hide_get() else 'Yes'
vp = 'No' if o.hide_viewport else 'Yes' vp = 'No' if o.hide_viewport else 'Yes'
rd = 'No' if o.hide_render else 'Yes' rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1 viz_ct += 1
print(f'{o.name} : viewport {vp} != render {rd}') print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
if viz_ct: if viz_ct:
problems.append(['gp.list_object_visibility', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE']) problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
## GP modifiers visibility conflict ## GP modifiers visibility conflict
if fix.list_gp_mod_vis_conflict: if fix.list_gp_mod_vis_conflict:
@@ -253,6 +325,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
bpy.context.scene.tool_settings.lock_object_mode = False bpy.context.scene.tool_settings.lock_object_mode = False
if fix.remove_redundant_strokes: if fix.remove_redundant_strokes:
print('removing redundant strokes')
ct = remove_stroke_exact_duplications(apply=apply) ct = remove_stroke_exact_duplications(apply=apply)
if ct > 0: if ct > 0:
mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications' mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications'
@@ -260,7 +333,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# ## Set onion skin filter to 'All type' # ## Set onion skin filter to 'All type'
# fix_kf_type = 0 # fix_kf_type = 0
# for gp in bpy.data.grease_pencils:#from data # for gp in bpy.data.grease_pencils: #from data
# if not gp.is_annotation: # if not gp.is_annotation:
# if gp.onion_keyframe_type != 'ALL': # if gp.onion_keyframe_type != 'ALL':
# gp.onion_keyframe_type = 'ALL' # gp.onion_keyframe_type = 'ALL'
@@ -280,9 +353,13 @@ class GPTB_OT_file_checker(bpy.types.Operator):
print(p) print(p)
else: else:
print(p[0]) print(p[0])
if not self.apply_fixes:
## button to call the operator again with apply_fixes set to True
problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}])
# Show in viewport # Show in viewport
title = "Changed Settings" if apply else "Checked Settings (dry run, nothing changed)" title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
utils.show_message_box(problems, _title = title, _icon = 'INFO') utils.show_message_box(problems, _title = title, _icon = 'INFO')
else: else:
self.report({'INFO'}, 'All good') self.report({'INFO'}, 'All good')
@@ -482,48 +559,13 @@ class GPTB_OT_links_checker(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self, width=popup_width) return context.window_manager.invoke_props_dialog(self, width=popup_width)
""" OLD links checker with show_message_box
class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
bl_options = {"REGISTER"}
def execute(self, context):
all_lnks = []
has_broken_link = False
## check for broken links
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
lfp = Path(lib)
realib = Path(current)
if not lfp.exists():
has_broken_link = True
all_lnks.append( (f"Broken link: {realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
else:
if realib.as_posix().startswith('//'):
all_lnks.append( (f"Link: {realib.as_posix()}", 'LINKED') )#lfp.as_posix()
else:
all_lnks.append( (f"Link: {realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
all_lnks.sort(key=lambda x: x[1], reverse=True) class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
if all_lnks: bl_idname = "gp.list_viewport_render_visibility"
print('===File check===') bl_label = "List Viewport And Render Visibility Conflicts"
for p in all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
utils.show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
return {"FINISHED"} """
class GPTB_OT_list_object_visibility(bpy.types.Operator):
bl_idname = "gp.list_object_visibility"
bl_label = "List Object Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values" bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
def invoke(self, context, event): def invoke(self, context, event):
self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render] self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render]
return context.window_manager.invoke_props_dialog(self, width=250) return context.window_manager.invoke_props_dialog(self, width=250)
@@ -539,20 +581,225 @@ class GPTB_OT_list_object_visibility(bpy.types.Operator):
def execute(self, context): def execute(self, context):
return {'FINISHED'} return {'FINISHED'}
## basic listing as message box # all in invoke now
# li = [] ### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
# viz_ct = 0 class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
# for o in context.scene.objects: bl_idname = "gp.sync_visibility_from_viewlayer"
# if o.hide_viewport != o.hide_render: bl_label = "Sync Visibility From Viewlayer"
# vp = 'No' if o.hide_viewport else 'Yes' bl_description = "Set viewport and render visibility to match viewlayer visibility"
# rd = 'No' if o.hide_render else 'Yes' bl_options = {"REGISTER", "UNDO"}
# viz_ct += 1
# li.append(f'{o.name} : viewport {vp} != render {rd}') def execute(self, context):
# if li: for obj in context.scene.objects:
# utils.show_message_box(_message=li, _title=f'{viz_ct} visibility conflicts found') is_hidden = obj.hide_get() # Get viewlayer visibility
# else: obj.hide_viewport = is_hidden
# self.report({'INFO'}, f"No Object visibility conflict on current scene") obj.hide_render = is_hidden
# return {'FINISHED'} return {'FINISHED'}
class GPTB_OT_sync_visibility_from_viewport(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_viewport"
bl_label = "Sync Visibility From Viewport"
bl_description = "Set viewlayer and render visibility to match viewport visibility"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.scene.objects:
is_hidden = obj.hide_viewport
obj.hide_set(is_hidden)
obj.hide_render = is_hidden
return {'FINISHED'}
class GPTB_OT_sync_visibility_from_render(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_render"
bl_label = "Sync Visibility From Render"
bl_description = "Set viewlayer and viewport visibility to match render visibility"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.scene.objects:
is_hidden = obj.hide_render
obj.hide_set(is_hidden)
obj.hide_viewport = is_hidden
return {'FINISHED'}
class GPTB_OT_sync_visibible_to_render(bpy.types.Operator):
bl_idname = "gp.sync_visibible_to_render"
bl_label = "Sync Overall Viewport Visibility To Render"
bl_description = "Set render visibility from"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
for obj in context.scene.objects:
## visible_get is the current visibility status combination of hide_viewport and viewlayer hide (eye)
obj.hide_render = not obj.visible_get()
return {'FINISHED'}
class GPTB_PG_object_visibility(bpy.types.PropertyGroup):
"""Property group to handle object visibility"""
is_hidden: BoolProperty(
name="Hide in Viewport",
description="Toggle object visibility in viewport",
get=lambda self: self.get("is_hidden", False),
set=lambda self, value: self.set_visibility(value)
)
object_name: StringProperty(name="Object Name")
def set_visibility(self, value):
"""Set the visibility using hide_set()"""
obj = bpy.context.view_layer.objects.get(self.object_name)
if obj:
obj.hide_set(value)
self["is_hidden"] = value
class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator):
bl_idname = "gp.list_object_visibility_conflicts"
bl_label = "List Object Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type]
def invoke(self, context, event):
# Clear and rebuild both collections
self.visibility_items.clear()
# Store objects with conflicts
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ?
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
# Create visibility items in same order
for obj in objects_with_conflicts:
item = self.visibility_items.add()
item.object_name = obj.name
item["is_hidden"] = obj.hide_get()
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
# Add sync buttons at the top
row = layout.row(align=False)
row.label(text="Sync All Visibility From:")
row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF')
row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF')
row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF')
layout.separator()
col = layout.column()
# We can safely iterate over visibility_items since objects are stored in same order
for vis_item in self.visibility_items:
obj = context.view_layer.objects.get(vis_item.object_name)
if not obj:
continue
row = col.row(align=False)
row.label(text=obj.name)
## Viewlayer visibility "as prop" to allow slide toggle
# hide_icon='HIDE_ON' if vis_item.is_hidden else 'HIDE_OFF'
hide_icon='HIDE_ON' if obj.hide_get() else 'HIDE_OFF' # based on object state
row.prop(vis_item, "is_hidden", text="", icon=hide_icon, emboss=False)
# Direct object properties
row.prop(obj, 'hide_viewport', text='', emboss=False)
row.prop(obj, 'hide_render', text='', emboss=False)
def execute(self, context):
return {'FINISHED'}
## -- List collection visibility conflicts
# class GPTB_PG_collection_visibility(bpy.types.PropertyGroup):
# """Property group to handle collection visibility"""
# is_hidden: BoolProperty(
# name="Hide in Viewport",
# description="Toggle collection visibility in viewport",
# get=lambda self: self.get("is_hidden", False),
# set=lambda self, value: self.set_visibility(value)
# )
# collection_name: StringProperty(name="Collection Name")
def get_collection_children_recursive(col, cols=None) -> list:
'''return a list of all the child collections
and their subcollections in the passed collection'''
cols = cols or []
for sub in col.children:
if sub not in cols:
cols.append(sub)
if len(sub.children):
cols = get_collection_children_recursive(sub, cols)
return cols
class GPTB_OT_list_collection_visibility_conflicts(bpy.types.Operator):
bl_idname = "gp.list_collection_visibility_conflicts"
bl_label = "List Collection Visibility Conflicts"
bl_description = "List collection visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
# visibility_items: CollectionProperty(type=GPTB_PG_collection_visibility)
show_filter : bpy.props.EnumProperty(
name="View Filter",
description="Filter collections based on their exclusion status",
items=(
('ALL', "All", "Show all collections", 0),
('NOT_EXCLUDED', "Not Excluded", "Show collections that are not excluded", 1),
('EXCLUDED', "Excluded", "Show collections that are excluded", 2)
),
default='NOT_EXCLUDED')
def invoke(self, context, event):
## get all viewlayer collections
vcols = get_collection_children_recursive(context.view_layer.layer_collection)
vcols = list(set(vcols)) # ensure no duplicates
## Store collection with conflicts
# layer_collection.is_visible against render visibility ?
## Do not list currently excluded collections
self.conflict_collections = [vc for vc in vcols if not (vc.hide_viewport == vc.collection.hide_viewport == vc.collection.hide_render)]
self.included_collection = [vc for vc in self.conflict_collections if not vc.exclude]
self.excluded_collection = [vc for vc in self.conflict_collections if vc.exclude]
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
layout.prop(self, 'show_filter', expand=True)
# Add sync buttons at the top
row = layout.row(align=False)
# TODO: Add "set all from" ops on collection (optionnal)
# row.label(text="Sync All Visibility From:")
# row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF')
# row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF')
# row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF')
layout.separator()
if self.show_filter == 'ALL':
vl_collections = self.conflict_collections
elif self.show_filter == 'EXCLUDED':
vl_collections = self.excluded_collection
elif self.show_filter == 'NOT_EXCLUDED':
vl_collections = self.included_collection
col = layout.column()
for vlcol in vl_collections:
row = col.row(align=False)
row.label(text=vlcol.name)
# Viewlayer collection settings
row.prop(vlcol, "exclude", text="", emboss=False)
row.prop(vlcol, "hide_viewport", text="", emboss=False)
# Direct collection properties
row.prop(vlcol.collection, 'hide_viewport', text='', emboss=False)
row.prop(vlcol.collection, 'hide_render', text='', emboss=False)
def execute(self, context):
return {'FINISHED'}
## not exposed in UI, Check is performed in Check file (can be called in popped menu) ## not exposed in UI, Check is performed in Check file (can be called in popped menu)
class GPTB_OT_list_modifier_visibility(bpy.types.Operator): class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
@@ -594,12 +841,20 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
classes = ( classes = (
GPTB_OT_list_object_visibility, GPTB_OT_list_viewport_render_visibility, # Only viewport and render
GPTB_OT_sync_visibility_from_viewlayer,
GPTB_OT_sync_visibility_from_viewport,
GPTB_OT_sync_visibility_from_render,
GPTB_OT_sync_visibible_to_render,
GPTB_PG_object_visibility,
GPTB_OT_list_object_visibility_conflicts,
GPTB_OT_list_collection_visibility_conflicts,
GPTB_OT_list_modifier_visibility, GPTB_OT_list_modifier_visibility,
GPTB_OT_copy_string_to_clipboard, GPTB_OT_copy_string_to_clipboard,
GPTB_OT_copy_multipath_clipboard, GPTB_OT_copy_multipath_clipboard,
GPTB_OT_file_checker, GPTB_OT_file_checker,
GPTB_OT_links_checker, GPTB_OT_links_checker,
GPTB_OT_remove_stroke_duplication,
) )
def register(): def register():
+2 -2
View File
@@ -7,12 +7,12 @@ import numpy as np
from time import time from time import time
from .utils import (location_to_region, region_to_location) from .utils import (location_to_region, region_to_location)
## DISABLED (in init, also in menu append, see register below)
""" """
## Do not work on multiple object ## Do not work on multiple object
def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False): def batch_flat_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False):
'''Reproject '''Reproject
:all_stroke: affect hided, locked layers :all_stroke: affect hidden, locked layers
''' '''
if restore_frame: if restore_frame:
-1
View File
@@ -2,7 +2,6 @@ import bpy
from mathutils import Vector from mathutils import Vector
from . import utils from . import utils
class GPTB_OT_create_follow_path_curve(bpy.types.Operator): class GPTB_OT_create_follow_path_curve(bpy.types.Operator):
bl_idname = "object.create_follow_path_curve" bl_idname = "object.create_follow_path_curve"
bl_label = "Create Follow Path Curve" bl_label = "Create Follow Path Curve"
+35 -38
View File
@@ -212,9 +212,14 @@ class GPTB_OT_draw_cam(Operator):
# Swap to it, unhide if necessary and hide previous # Swap to it, unhide if necessary and hide previous
context.scene.camera = maincam context.scene.camera = maincam
## hide cam object ## Hide cam object
drawcam.hide_viewport = True
# Ensure at lviewport viz is active to ensure visibility and refresh state
maincam.hide_viewport = False maincam.hide_viewport = False
# drawcam.hide_viewport = False # Not absolutely needed
drawcam.hide_set(True)
maincam.hide_set(False)
## if in main camera GO to drawcam ## if in main camera GO to drawcam
elif context.scene.camera.name not in ('draw_cam', 'obj_cam'): elif context.scene.camera.name not in ('draw_cam', 'obj_cam'):
@@ -267,8 +272,12 @@ class GPTB_OT_draw_cam(Operator):
## hide cam object ## hide cam object
context.scene.camera = drawcam context.scene.camera = drawcam
# Ensure viewport viz is active to ensure visibility and refresh state
drawcam.hide_viewport = False drawcam.hide_viewport = False
maincam.hide_viewport = True maincam.hide_viewport = False
## Set viewlayer visibility
drawcam.hide_set(False)
maincam.hide_set(True)
if created and drawcam.name == 'obj_cam': # Go in camera view if created and drawcam.name == 'obj_cam': # Go in camera view
context.region_data.view_perspective = 'CAMERA' context.region_data.view_perspective = 'CAMERA'
@@ -412,12 +421,12 @@ class GPTB_OT_toggle_mute_animation(Operator):
self.selection = event.shift self.selection = event.shift
return self.execute(context) return self.execute(context)
def set_action_mute(self, act): def set_action_mute(self, bag):
for i, fcu in enumerate(act.fcurves): for i, fcu in enumerate(bag.fcurves):
print(i, fcu.data_path, fcu.array_index) print(i, fcu.data_path, fcu.array_index)
# fcu.group don't have mute attribute in api. # fcu.group don't have mute attribute in api.
fcu.mute = self.mute fcu.mute = self.mute
for g in act.groups: for g in bag.groups:
g.mute = self.mute g.mute = self.mute
def execute(self, context): def execute(self, context):
@@ -434,22 +443,15 @@ class GPTB_OT_toggle_mute_animation(Operator):
if self.mode == 'CAMERA' and o.type != 'CAMERA': if self.mode == 'CAMERA' and o.type != 'CAMERA':
continue continue
# mute attribute animation for GP and cameras ## Mute attribute animation for GP and cameras
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data: if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
gp_act = o.data.animation_data.action if data_channelbag := utils.get_active_channelbag(o.data):
if gp_act:
print(f'\n---{o.name} data:') print(f'\n---{o.name} data:')
self.set_action_mute(gp_act) self.set_action_mute(data_channelbag)
if not o.animation_data: if object_channelbag := utils.get_active_channelbag(o):
continue print(f'\n---{o.name}:')
act = o.animation_data.action self.set_action_mute(object_channelbag)
if not act:
continue
print(f'\n---{o.name}:')
self.set_action_mute(act)
return {'FINISHED'} return {'FINISHED'}
@@ -475,7 +477,7 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
for o in pool: for o in pool:
if o.type != 'GREASEPENCIL': if o.type != 'GREASEPENCIL':
continue continue
for m in o.modifier: for m in o.modifiers:
# skip modifier that are not visible in render # skip modifier that are not visible in render
if not m.show_render: if not m.show_render:
continue continue
@@ -486,7 +488,7 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
class GPTB_OT_list_disabled_anims(Operator): class GPTB_OT_list_disabled_anims(Operator):
bl_idname = "gp.list_disabled_anims" bl_idname = "gp.list_disabled_anims"
bl_label = "List Disabled Anims" bl_label = "List Disabled Anims"
bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)" bl_description = "List disabled animations channels in scene. (shit+clic to list only on selection)"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=False) mute : bpy.props.BoolProperty(default=False)
@@ -512,27 +514,22 @@ class GPTB_OT_list_disabled_anims(Operator):
# continue # continue
if o.type == 'GREASEPENCIL': if o.type == 'GREASEPENCIL':
if o.data.animation_data: if fcurves := utils.get_fcurves(o.data):
gp_act = o.data.animation_data.action for i, fcu in enumerate(fcurves):
if gp_act: if fcu.mute:
for i, fcu in enumerate(gp_act.fcurves): if o not in oblist:
if fcu.mute: oblist.append(o)
if o not in oblist: li.append(f'{o.name}:')
oblist.append(o) li.append(f' - {fcu.data_path} {fcu.array_index}')
li.append(f'{o.name}:')
li.append(f' - {fcu.data_path} {fcu.array_index}')
if not o.animation_data:
continue
act = o.animation_data.action
if not act:
continue
for g in act.groups: bag = utils.get_active_channelbag(o)
if not bag:
continue
for g in bag.groups:
if g.mute: if g.mute:
li.append(f'{o.name} - group: {g.name}') li.append(f'{o.name} - group: {g.name}')
for i, fcu in enumerate(act.fcurves): for i, fcu in enumerate(bag.fcurves):
# print(i, fcu.data_path, fcu.array_index) # print(i, fcu.data_path, fcu.array_index)
if fcu.mute: if fcu.mute:
if o not in oblist: if o not in oblist:
+11 -11
View File
@@ -1,5 +1,6 @@
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator
from . import utils
def get_layer_list(self, context): def get_layer_list(self, context):
@@ -41,7 +42,7 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GREASEPENCIL'\ return context.object and context.object.type == 'GREASEPENCIL'\
and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GREASEPENCIL' and context.space_data.bl_rna.identifier == 'SpaceDopeSheetEditor' and context.space_data.ui_mode == 'GPENCIL'
# history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self) # history : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) # need to have a variable to store (to get it in self)
@@ -65,25 +66,24 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
## Remove overlapping frames ## Remove overlapping frames
for f in reversed(to_replace): for f in reversed(to_replace):
target_layer.frames.remove(f) target_layer.frames.remove(f.frame_number)
## Copy original frames ## Copy original frames
for f in selected_frames: for f in selected_frames:
target_layer.frames.copy(f) utils.copy_frame_at(f, target_layer, f.frame_number)
# target_layer.frames.copy(f) # GPv2
sent = len(selected_frames) sent = len(selected_frames)
## Delete original frames as an option ## Delete original frames as an option
if self.delete_source: if self.delete_source:
for f in reversed(selected_frames): for f in reversed(selected_frames):
act_layer.frames.remove(f) act_layer.frames.remove(f.frame_number)
mess = f'{sent} keys moved'
else:
mess = f'{sent} keys copied'
mess = f'{sent} keys copied'
if replaced: if replaced:
mess += f' ({replaced} replaced)' mess += f' ({replaced} replaced)'
mod = context.mode
bpy.ops.gpencil.editmode_toggle()
bpy.ops.object.mode_set(mode=mod)
self.report({'INFO'}, mess) self.report({'INFO'}, mess)
return {'FINISHED'} return {'FINISHED'}
@@ -131,7 +131,7 @@ def unregister_keymaps():
def menu_duplicate_and_send_to_layer(self, context): def menu_duplicate_and_send_to_layer(self, context):
if context.space_data.ui_mode == 'GREASEPENCIL': if context.space_data.ui_mode == 'GPENCIL':
self.layout.operator_context = 'INVOKE_REGION_WIN' self.layout.operator_context = 'INVOKE_REGION_WIN'
self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer') self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
+5 -4
View File
@@ -1,5 +1,5 @@
import bpy import bpy
from .utils import get_addon_prefs from .utils import get_addon_prefs, is_locked, is_hidden
from bpy.props import BoolProperty ,EnumProperty ,StringProperty from bpy.props import BoolProperty ,EnumProperty ,StringProperty
class GPTB_OT_jump_gp_keyframe(bpy.types.Operator): class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
@@ -36,6 +36,7 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4), ('MOVING_HOLD', 'Moving Hold', '', 'KEYTYPE_MOVING_HOLD_VEC', 4),
('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5), ('EXTREME', 'Extreme', '', 'KEYTYPE_EXTREME_VEC', 5),
('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6), ('JITTER', 'Jitter', '', 'KEYTYPE_JITTER_VEC', 6),
('GENERATED', 'Generated', '', 'KEYTYPE_GENERATED_VEC', 7),
)) ))
def execute(self, context): def execute(self, context):
@@ -44,15 +45,15 @@ class GPTB_OT_jump_gp_keyframe(bpy.types.Operator):
return {"CANCELLED"} return {"CANCELLED"}
if self.target == 'ACTIVE': if self.target == 'ACTIVE':
gpl = [l for l in context.object.data.layers if l.select and not l.hide] gpl = [l for l in context.object.data.layers if l.select and not is_hidden(l)]
if not context.object.data.layers.active in gpl: if not context.object.data.layers.active in gpl:
gpl.append(context.object.data.layers.active) gpl.append(context.object.data.layers.active)
elif self.target == 'VISIBLE': elif self.target == 'VISIBLE':
gpl = [l for l in context.object.data.layers if not l.hide] gpl = [l for l in context.object.data.layers if not is_hidden(l)]
elif self.target == 'ACCESSIBLE': elif self.target == 'ACCESSIBLE':
gpl = [l for l in context.object.data.layers if not l.hide and not l.lock] gpl = [l for l in context.object.data.layers if not is_hidden(l) and not is_locked(l)]
if self.keyframe_type != 'NONE': if self.keyframe_type != 'NONE':
# use shortcut choice override # use shortcut choice override
+35 -85
View File
@@ -21,6 +21,7 @@ from .utils import get_addon_prefs, is_vector_close
# PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering # PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<tag2>[A-Z]{1,6}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering PATTERN = r'^(?P<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\d{3})?$' # numering
# TODO: allow a more flexible prefix pattern
def layer_name_build(layer, prefix='', desc='', suffix=''): def layer_name_build(layer, prefix='', desc='', suffix=''):
'''GET a layer and argument to build and assign name '''GET a layer and argument to build and assign name
@@ -78,7 +79,7 @@ def layer_name_build(layer, prefix='', desc='', suffix=''):
# maybe a more elegant way exists to find all objects users ? # maybe a more elegant way exists to find all objects users ?
# update Gpencil modifier targets # update Gpencil modifier targets
for mod in ob_user.modifier: for mod in ob_user.modifiers:
if not hasattr(mod, 'layer_filter'): if not hasattr(mod, 'layer_filter'):
continue continue
if mod.layer_filter == old: if mod.layer_filter == old:
@@ -155,13 +156,16 @@ class GPTB_OT_layer_name_build(Operator):
gpl = ob.data.layers gpl = ob.data.layers
act = gpl.active act = gpl.active
if not act: if not act:
self.report({'ERROR'}, 'no layer active') act = ob.data.layer_groups.active
if not act:
self.report({'ERROR'}, 'No layer active')
return {"CANCELLED"} return {"CANCELLED"}
layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix) layer_name_build(act, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
## Deactivate multi-selection on layer ! ## /!\ Deactivate multi-selection on layer !
## somethimes it affect a random layer that is still considered selected ## Somethimes it affect a random layer that is still considered selected
# for l in gpl: # for l in gpl:
# if l.select or l == act: # if l.select or l == act:
# layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix) # layer_name_build(l, prefix=self.prefix, desc=self.desc, suffix=self.suffix)
@@ -169,79 +173,6 @@ class GPTB_OT_layer_name_build(Operator):
return {"FINISHED"} return {"FINISHED"}
def grp_toggle(l, mode='TOGGLE'):
'''take mode in (TOGGLE, GROUP, UNGROUP) '''
grp_item_id = ' - '
res = re.search(r'^(\s{1,3}-\s{0,3})(.*)', l.name)
if not res and mode in ('TOGGLE', 'GROUP'):
# No gpr : add group prefix after stripping all space and dash
l.name = grp_item_id + l.name.lstrip(' -')
elif res and mode in ('TOGGLE', 'UNGROUP'):
# found : delete group prefix
l.name = res.group(2)
class GPTB_OT_layer_group_toggle(Operator):
bl_idname = "gp.layer_group_toggle"
bl_label = "Group Toggle"
bl_description = "Group or ungroup a layer"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return True
# group : StringProperty(default='', options={'SKIP_SAVE'})
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
for l in gpl:
if l.select or l == act:
grp_toggle(l)
return {"FINISHED"}
class GPTB_OT_layer_new_group(Operator):
bl_idname = "gp.layer_new_group"
bl_label = "New Group"
bl_description = "Create a group from active layer"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
ob = context.object
gpl = ob.data.layers
act = gpl.active
if not act:
self.report({'ERROR'}, 'no layer active')
return {"CANCELLED"}
res = re.search(PATTERN, act.name)
if not res:
self.report({'ERROR'}, 'Could not create a group name, create a layer manually')
return {"CANCELLED"}
name = res.group('name').strip(' -')
if not name:
self.report({'ERROR'}, f'No name found in {act.name}')
return {"CANCELLED"}
if name in [l.name.strip(' -') for l in gpl]:
self.report({'WARNING'}, f'Name already exists: {act.name}')
return {"FINISHED"}
grp_toggle(act, mode='GROUP')
n = gpl.new(name, set_active=False)
n.use_onion_skinning = n.use_lights = False
n.hide = True
n.opacity = 0
return {"FINISHED"}
#-## SELECTION MANAGEMENT ##-# #-## SELECTION MANAGEMENT ##-#
def activate_channel_group_color(context): def activate_channel_group_color(context):
@@ -646,6 +577,7 @@ def gpencil_dopesheet_header(self, context):
def gpencil_layer_dropdown_menu(self, context): def gpencil_layer_dropdown_menu(self, context):
'''to append in GPENCIL_MT_layer_context_menu''' '''to append in GPENCIL_MT_layer_context_menu'''
self.layout.operator('gp.create_empty_frames', icon='KEYFRAME')
self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE') self.layout.operator('gp.rename_gp_layers', icon='BORDERMOVE')
## handler and msgbus ## handler and msgbus
@@ -675,14 +607,32 @@ def obj_layer_name_callback():
# print('inc:', res.group('inc')) # print('inc:', res.group('inc'))
bpy.context.scene.gptoolprops['layer_name'] = res.group('name') bpy.context.scene.gptoolprops['layer_name'] = res.group('name')
## old gpv2
# def subscribe_layer_change():
# subscribe_to = (bpy.types.GreasePencilLayers, "active_index")
# bpy.msgbus.subscribe_rna(
# key=subscribe_to,
# # owner of msgbus subcribe (for clearing later)
# # owner=handle,
# owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime...
# # Args passed to callback function (tuple)
# args=(),
# # Callback function for property update
# notify=obj_layer_name_callback,
# options={'PERSISTENT'},
# )
def subscribe_layer_change(): def subscribe_layer_change():
subscribe_to = (bpy.types.GreasePencilLayers, "active_index") subscribe_to = (bpy.types.GreasePencilv3Layers, "active")
if bpy.app.version >= (5, 0, 0):
owner = bpy.types.GreasePencil
else:
owner = bpy.types.GreasePencilv3
bpy.msgbus.subscribe_rna( bpy.msgbus.subscribe_rna(
key=subscribe_to, key=subscribe_to,
# owner of msgbus subcribe (for clearing later) # owner of msgbus subcribe (for clearing later)
# owner=handle, # owner=handle,
owner=bpy.types.GreasePencil, # <-- can attach to an ID during all it's lifetime... owner=owner, # <-- can attach to an ID during all it's lifetime...
# Args passed to callback function (tuple) # Args passed to callback function (tuple)
args=(), args=(),
# Callback function for property update # Callback function for property update
@@ -690,6 +640,7 @@ def subscribe_layer_change():
options={'PERSISTENT'}, options={'PERSISTENT'},
) )
@persistent @persistent
def subscribe_layer_change_handler(dummy): def subscribe_layer_change_handler(dummy):
subscribe_layer_change() subscribe_layer_change()
@@ -723,7 +674,7 @@ class GPTB_PT_layer_name_ui(bpy.types.Panel):
row = layout.row() row = layout.row()
row.activate_init = True row.activate_init = True
row.label(icon='OUTLINER_DATA_GP_LAYER') row.label(icon='OUTLINER_DATA_GP_LAYER')
row.prop(context.object.data.layers.active, 'info', text='') row.prop(context.object.data.layers.active, 'name', text='')
def add_layer(context): def add_layer(context):
bpy.ops.gpencil.layer_add() bpy.ops.gpencil.layer_add()
@@ -802,8 +753,6 @@ def unregister_keymaps():
classes=( classes=(
GPTB_OT_rename_gp_layer, GPTB_OT_rename_gp_layer,
GPTB_OT_layer_name_build, GPTB_OT_layer_name_build,
GPTB_OT_layer_group_toggle,
GPTB_OT_layer_new_group,
GPTB_OT_select_set_same_prefix, GPTB_OT_select_set_same_prefix,
GPTB_OT_select_set_same_color, GPTB_OT_select_set_same_color,
@@ -819,7 +768,7 @@ def register():
bpy.types.DATA_PT_grease_pencil_layers.prepend(layer_name_builder_ui) bpy.types.DATA_PT_grease_pencil_layers.prepend(layer_name_builder_ui)
bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header) bpy.types.DOPESHEET_HT_header.append(gpencil_dopesheet_header)
bpy.types.GPENCIL_MT_layer_context_menu.append(gpencil_layer_dropdown_menu) bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.append(gpencil_layer_dropdown_menu)
bpy.app.handlers.load_post.append(subscribe_layer_change_handler) bpy.app.handlers.load_post.append(subscribe_layer_change_handler)
register_keymaps() register_keymaps()
@@ -829,12 +778,13 @@ def register():
def unregister(): def unregister():
unregister_keymaps() unregister_keymaps()
bpy.app.handlers.load_post.remove(subscribe_layer_change_handler) bpy.app.handlers.load_post.remove(subscribe_layer_change_handler)
bpy.types.GPENCIL_MT_layer_context_menu.remove(gpencil_layer_dropdown_menu) bpy.types.GREASE_PENCIL_MT_grease_pencil_add_layer_extra.remove(gpencil_layer_dropdown_menu)
bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header) bpy.types.DOPESHEET_HT_header.remove(gpencil_dopesheet_header)
bpy.types.DATA_PT_grease_pencil_layers.remove(layer_name_builder_ui) bpy.types.DATA_PT_grease_pencil_layers.remove(layer_name_builder_ui)
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
# delete layer index trigger # Delete layer index trigger
# /!\ 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.GreasePencil)
+5 -5
View File
@@ -26,10 +26,11 @@ class GPT_OT_layer_nav(bpy.types.Operator):
prefs = utils.get_addon_prefs() prefs = utils.get_addon_prefs()
if not prefs.nav_use_fade: if not prefs.nav_use_fade:
if self.direction == 'DOWN': if self.direction == 'DOWN':
utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info') utils.iterate_active_layer(context.grease_pencil, -1)
# utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'name') # gpv2
if self.direction == 'UP': if self.direction == 'UP':
utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info') utils.iterate_active_layer(context.grease_pencil, 1)
return {'FINISHED'} return {'FINISHED'}
## get up and down keys for use in modal ## get up and down keys for use in modal
@@ -91,12 +92,11 @@ class GPT_OT_layer_nav(bpy.types.Operator):
context.space_data.overlay.gpencil_fade_layer = fade context.space_data.overlay.gpencil_fade_layer = fade
if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'): if self.direction == 'DOWN' or ((event.type in self.down_keys) and event.value == 'PRESS'):
_val = utils.iterate_selector(context.object.data.layers, 'active_index', -1, info_attr = 'info') _val = utils.iterate_active_layer(context.grease_pencil, -1)
trigger = True trigger = True
if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'): if self.direction == 'UP' or ((event.type in self.up_keys) and event.value == 'PRESS'):
_val = utils.iterate_selector(context.object.data.layers, 'active_index', 1, info_attr = 'info') _val = utils.iterate_active_layer(context.grease_pencil, 1)
# utils.iterate_selector(bpy.context.scene.grease_pencil.layers, 'active_index', 1, info_attr = 'info')#layers
trigger = True trigger = True
if trigger: if trigger:
+16 -16
View File
@@ -4,7 +4,7 @@ import mathutils
from mathutils import Vector, Matrix, geometry from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from time import time from time import time
from .utils import get_gp_draw_plane, location_to_region, region_to_location from .utils import get_gp_draw_plane, location_to_region, region_to_location, is_locked, is_hidden
class GP_OT_pick_closest_layer(Operator): class GP_OT_pick_closest_layer(Operator):
bl_idname = "gp.pick_closest_layer" bl_idname = "gp.pick_closest_layer"
@@ -33,8 +33,8 @@ class GP_OT_pick_closest_layer(Operator):
mouse_vec3 = Vector((*self.init_mouse, 0)) mouse_vec3 = Vector((*self.init_mouse, 0))
co, index, _dist = kd.find(mouse_vec3) co, index, _dist = kd.find(mouse_vec3)
lid = self.point_pair[index][1] layer = self.point_pair[index][1]
return lid return layer
def invoke(self, context, event): def invoke(self, context, event):
self.t0 = time() self.t0 = time()
@@ -75,10 +75,10 @@ class GP_OT_pick_closest_layer(Operator):
self.inv_mat = self.ob.matrix_world.inverted() self.inv_mat = self.ob.matrix_world.inverted()
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_id, l in enumerate(gp.layers): for layer in gp.layers:
if l.hide: # l.lock or if is_hidden(layer):
continue continue
for f in l.frames: for f in layer.frames:
if not f.select: if not f.select:
continue continue
for s in f.drawing.strokes: for s in f.drawing.strokes:
@@ -86,33 +86,33 @@ class GP_OT_pick_closest_layer(Operator):
continue continue
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill: elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
continue continue
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer_id) for p in s.points] self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
else: else:
# [s for l in gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes] # [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for layer_id, l in enumerate(gp.layers): for layer in gp.layers:
if l.hide or not l.current_frame(): # l.lock or if is_hidden(layer) or not layer.current_frame():
continue continue
for s in l.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:
continue continue
elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill: elif self.stroke_filter == 'FILL' and not self.ob.data.materials[s.material_index].grease_pencil.show_fill:
continue continue
self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer_id) for p in s.points] self.point_pair += [(Vector((*location_to_region(mat @ p.position), 0)), layer) for p in s.points]
if not self.point_pair: if not self.point_pair:
self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden') self.report({'ERROR'}, 'No stroke found, maybe layers are locked or hidden')
return {'CANCELLED'} return {'CANCELLED'}
lid = self.filter_stroke(context) layer_target = self.filter_stroke(context)
if isinstance(lid, str): if isinstance(layer_target, str):
self.report({'ERROR'}, lid) self.report({'ERROR'}, layer_target)
return {'CANCELLED'} return {'CANCELLED'}
del self.point_pair # auto garbage collected ? del self.point_pair # auto garbage collected ?
self.ob.data.layers.active_index = lid self.ob.data.layers.active = layer_target
## debug show trigger time ## debug show trigger time
# print(f'Trigger time {time() - self.t0:.3f}') # print(f'Trigger time {time() - self.t0:.3f}')
+17 -17
View File
@@ -93,6 +93,7 @@ class GPTB_OT_move_material_to_layer(Operator) :
# t = time.time() # Dbg # t = time.time() # Dbg
total = 0 total = 0
oct = 0 oct = 0
for ob in pool: for ob in pool:
mat_index = next((i for i, ms in enumerate(ob.material_slots) if ms.material and ms.material == mat), None) mat_index = next((i for i, ms in enumerate(ob.material_slots) if ms.material and ms.material == mat), None)
if mat_index is None: if mat_index is None:
@@ -110,26 +111,26 @@ class GPTB_OT_move_material_to_layer(Operator) :
### Move Strokes to a new key (or existing key if comming for yet another layer) ### Move Strokes to a new key (or existing key if comming for yet another layer)
fct = 0 fct = 0
sct = 0 sct = 0
for l in gpl: for layer in gpl:
if l == target_layer: if layer == target_layer:
## ! infinite loop if target layer is included ## ! infinite loop if target layer is included
continue continue
for f in l.frames: for fr in layer.frames:
## skip if no stroke has active material ## skip if no stroke has active material
if not next((s for s in f.drawing.strokes if s.material_index == mat_index), None): if not next((s for s in fr.drawing.strokes if s.material_index == mat_index), None):
continue continue
## Get/Create a destination frame and keep a reference to it ## Get/Create a destination frame and keep a reference to it
if not (dest_key := key_dict.get(f.frame_number)): if not (dest_key := key_dict.get(fr.frame_number)):
dest_key = target_layer.frames.new(f.frame_number) dest_key = target_layer.frames.new(fr.frame_number)
key_dict[dest_key.frame_number] = dest_key key_dict[dest_key.frame_number] = dest_key
print(f'{ob.name} : frame {f.frame_number}') print(f'{ob.name} : frame {fr.frame_number}')
## Replicate strokes in dest_keys ## Replicate strokes in dest_keys
stroke_to_delete = [] stroke_to_delete = []
for s in f.drawing.strokes: for s_idx, s in enumerate(fr.drawing.strokes):
if s.material_index == mat_index: if s.material_index == mat_index:
utils.copy_stroke_to_frame(s, dest_key) utils.copy_stroke_to_frame(s, dest_key)
stroke_to_delete.append(s) stroke_to_delete.append(s_idx)
## Debug ## Debug
# if time.time() - t > 10: # if time.time() - t > 10:
@@ -138,17 +139,16 @@ class GPTB_OT_move_material_to_layer(Operator) :
sct += len(stroke_to_delete) sct += len(stroke_to_delete)
# print('Removing frames') # Dbg ## Remove from source frame (fr)
## Remove from source frame (f)
if not self.copy: if not self.copy:
for s in reversed(stroke_to_delete): # print('Removing frames') # Dbg
f.drawing.strokes.remove(s) if stroke_to_delete:
fr.drawing.remove_strokes(indices=stroke_to_delete)
## ? Remove frame if layer is empty ? -> probably not, will show previous frame ## ? Remove frame if layer is empty ? -> probably not, otherwise will show previous frame
fct += 1 fct += 1
l.frames.update()
if fct: if fct:
oct += 1 oct += 1
@@ -165,7 +165,7 @@ class GPTB_OT_move_material_to_layer(Operator) :
# def menu_duplicate_and_send_to_layer(self, context): # def menu_duplicate_and_send_to_layer(self, context):
# if context.space_data.ui_mode == 'GREASEPENCIL': # if context.space_data.ui_mode == 'GPENCIL':
# self.layout.operator_context = 'INVOKE_REGION_WIN' # self.layout.operator_context = 'INVOKE_REGION_WIN'
# self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True # self.layout.operator('gp.duplicate_send_to_layer', text='Move Keys To Layer').delete_source = True
# self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer') # self.layout.operator('gp.duplicate_send_to_layer', text='Copy Keys To Layer')
+13 -9
View File
@@ -4,7 +4,12 @@ import mathutils
from mathutils import Vector, Matrix, geometry from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from time import time from time import time
from .utils import get_gp_draw_plane, location_to_region, region_to_location from .utils import (get_gp_draw_plane,
location_to_region,
region_to_location,
is_locked,
is_hidden)
### passing by 2D projection ### passing by 2D projection
def get_3d_coord_on_drawing_plane_from_2d(context, co): def get_3d_coord_on_drawing_plane_from_2d(context, co):
@@ -79,7 +84,7 @@ class GP_OT_pick_closest_material(Operator):
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for l in self.gp.layers: for l in self.gp.layers:
if l.hide:# l.lock or if is_hidden(l):# is_locked(l) or
continue continue
for f in l.frames: for f in l.frames:
if not f.select: if not f.select:
@@ -88,9 +93,9 @@ class GP_OT_pick_closest_material(Operator):
self.stroke_list.append(s) self.stroke_list.append(s)
else: else:
# [s for l in self.gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes] # [s for l in self.gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for l in self.gp.layers: for l in self.gp.layers:
if l.hide or not l.current_frame():# l.lock or if is_hidden(l) or not l.current_frame():# is_locked(l) or
continue continue
for s in l.current_frame().drawing.strokes: for s in l.current_frame().drawing.strokes:
self.stroke_list.append(s) self.stroke_list.append(s)
@@ -235,7 +240,7 @@ class GP_OT_pick_closest_material(Operator):
if context.scene.tool_settings.use_grease_pencil_multi_frame_editing: if context.scene.tool_settings.use_grease_pencil_multi_frame_editing:
for l in gp.layers: for l in gp.layers:
if l.hide:# l.lock or if is_hidden(l):# is_locked(l) or
continue continue
for f in l.frames: for f in l.frames:
if not f.select: if not f.select:
@@ -244,9 +249,9 @@ class GP_OT_pick_closest_material(Operator):
self.stroke_list.append(s) self.stroke_list.append(s)
else: else:
# [s for l in gp.layers if not l.lock and not l.hide for s in l.current_frame().stokes] # [s for l in gp.layers if not is_locked(l) and not is_hidden(l) for s in l.current_frame().stokes]
for l in gp.layers: for l in gp.layers:
if l.hide or not l.current_frame():# l.lock or if is_hidden(l) or not l.current_frame():# is_locked(l) or
continue continue
for s in l.current_frame().drawing.strokes: for s in l.current_frame().drawing.strokes:
self.stroke_list.append(s) self.stroke_list.append(s)
@@ -290,9 +295,8 @@ class GP_OT_pick_closest_material(Operator):
addon_keymaps = [] addon_keymaps = []
def register_keymaps(): def register_keymaps():
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
# km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Draw brush)", space_type = "EMPTY", region_type='WINDOW')
# km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW') # km = addon.keymaps.new(name = "Grease Pencil Paint Mode", space_type = "EMPTY", region_type='WINDOW')
km = addon.keymaps.new(name = "Grease Pencil Stroke Paint (Fill)", space_type = "EMPTY", region_type='WINDOW') km = addon.keymaps.new(name = "Grease Pencil Fill Tool", space_type = "EMPTY", region_type='WINDOW')
kmi = km.keymap_items.new( kmi = km.keymap_items.new(
# name="", # name="",
idname="gp.pick_closest_material", idname="gp.pick_closest_material",
+2 -2
View File
@@ -165,9 +165,9 @@ class GPTB_OT_import_obj_palette(Operator):
# self.report({'WARNING'}, 'All materials are already in other selected object') # self.report({'WARNING'}, 'All materials are already in other selected object')
# unlink objects and their gp data # unlink objects and their gp data
data_source = bpy.data.grease_pencils if bpy.app.version >= (5, 0, 0) else bpy.data.grease_pencils_v3
for src_ob in linked_objs: for src_ob in linked_objs:
bpy.data.grease_pencils.remove(src_ob.data) data_source.remove(src_ob.data)
return {"FINISHED"} return {"FINISHED"}
+47 -71
View File
@@ -1,11 +1,13 @@
import bpy import bpy
import mathutils import mathutils
import numpy as np
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
from math import pi from math import pi
import numpy as np
from time import time from time import time
from . import utils
from mathutils.geometry import intersect_line_plane from mathutils.geometry import intersect_line_plane
from . import utils
from .utils import is_hidden, is_locked
def get_scale_matrix(scale): def get_scale_matrix(scale):
# recreate a neutral mat scale # recreate a neutral mat scale
@@ -15,38 +17,9 @@ 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 hided, locked layers :all_stroke: affect hidden, locked layers
''' '''
if restore_frame: if restore_frame:
@@ -69,16 +42,23 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
# matrix = np.array(obj.matrix_world, dtype='float64') # matrix = np.array(obj.matrix_world, dtype='float64')
# matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64') # matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64')
#mat = src.matrix_world #mat = src.matrix_world
for l in obj.data.layers: for layer in obj.data.layers:
if not all_strokes: if not all_strokes:
if not l.select: if not layer.select:
continue continue
if l.hide or l.lock: if is_hidden(layer) or is_locked(layer):
continue continue
f = next((f for f in l.frames if f.frame_number == i), None)
if f is None: frame = next((f for f in layer.frames if f.frame_number == i), None)
if frame is None:
print(layer.name, 'Not found')
# FIXME: some strokes are ignored
# print(frame'skip {layer.name}, no frame at {i}')
continue continue
for s in f.drawing.strokes:
for s in frame.drawing.strokes:
# print(layer.name, s.material_index)
## Batch matrix apply (Here is slower than list comprehension). ## Batch matrix apply (Here is slower than list comprehension).
# nb_points = len(s.points) # nb_points = len(s.points)
# coords = np.empty(nb_points * 3, dtype='float64') # coords = np.empty(nb_points * 3, dtype='float64')
@@ -90,17 +70,16 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d]
## Basic method (Slower than foreach_set) # Basic method (Slower than foreach_set and compatible with GPv3)
# for i, p in enumerate(s.points): ## TODO: use low level api with curve offsets...
# p.position = obj.matrix_world.inverted() @ new_world_co_3d[i] for pt_index, point in enumerate(s.points):
point.position = matrix_inv @ new_world_co_3d[pt_index]
## GPv2: ravel and use foreach_set
## Ravel new coordinate on the fly ## Ravel new coordinate on the fly
new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p] ## NOTE: Set points in obj local space (apply matrix is slower): new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
# new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p]
## Set points in obj local space (apply matrix slower) # s.points.foreach_set('co', new_local_coords)
# new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
s.points.foreach_set('co', new_local_coords)
if restore_frame: if restore_frame:
bpy.context.scene.frame_current = oframe bpy.context.scene.frame_current = oframe
@@ -142,21 +121,21 @@ def align_global(reproject=True, ref=None, all_strokes=True):
for s in f.drawing.strokes: for s in f.drawing.strokes:
## foreach ## foreach
coords = [p.position @ mat.inverted() @ new_mat for p in s.points] coords = [p.position @ mat.inverted() @ new_mat for p in s.points]
# print('coords: ', coords)
# print([co for v in coords for co in v])
s.points.foreach_set('co', [co for v in coords for co in v])
# s.points.update() # seem to works # but adding/deleting a point is "safer"
## force update
s.points.add(1)
s.points.pop()
# for p in s.points: ## GPv2
# s.points.foreach_set('co', [co for v in coords for co in v])
# # s.points.update() # seem to works # but adding/deleting a point is "safer"
# ## force update
# s.points.add(1)
# s.points.pop()
for p in s.points:
## GOOD : ## GOOD :
# world_co = mat @ p.position # world_co = mat @ p.position
# p.position = new_mat.inverted() @ world_co # p.position = new_mat.inverted() @ world_co
## GOOD : ## GOOD :
# p.position = p.position @ mat.inverted() @ new_mat p.position = p.position @ mat.inverted() @ new_mat
if o.parent: if o.parent:
o.matrix_world = new_mat o.matrix_world = new_mat
@@ -179,10 +158,10 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler' chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler'
## double list keys ## double list keys
rot_keys = [int(k.co.x) for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel] rot_keys = [int(k.co.x) for fcu in utils.get_fcurves(o) for k in fcu.keyframe_points if fcu.data_path == chanel]
## normal iter ## normal iter
# for fcu in o.animation_data.action.fcurves: # for fcu in utils.get_fcurves(o):
# if fcu.data_path != chanel : # if fcu.data_path != chanel :
# continue # continue
# for k in fcu.keyframe_points(): # for k in fcu.keyframe_points():
@@ -289,12 +268,12 @@ class GPTB_OT_realign(bpy.types.Operator):
self.alert = '' self.alert = ''
o = context.object o = context.object
if o.animation_data and o.animation_data.action: fcurves = utils.get_fcurves(o)
act = o.animation_data.action if fcurves:
for chan in ('rotation_euler', 'rotation_quaternion'): for chan in ('rotation_euler', 'rotation_quaternion'):
if act.fcurves.find(chan): if fcurves.find(chan):
self.alert = 'Animated Rotation (CONSTANT interpolation)' self.alert = 'Animated Rotation (CONSTANT interpolation)'
interpos = [p for fcu in act.fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT'] interpos = [p for fcu in fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT']
if interpos: if interpos:
self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)' self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)'
break break
@@ -344,8 +323,8 @@ class GPTB_OT_realign(bpy.types.Operator):
oframe = context.scene.frame_current oframe = context.scene.frame_current
o = bpy.context.object o = bpy.context.object
if o.animation_data and o.animation_data.action: if fcurves := utils.get_fcurves(o):
if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'): if fcurves.find('rotation_euler') or fcurves.find('rotation_quaternion'):
align_all_frames(reproject=self.reproject) align_all_frames(reproject=self.reproject)
print(f'\nAnim realign ({time()-t0:.2f}s)') print(f'\nAnim realign ({time()-t0:.2f}s)')
self.exit(context, oframe) self.exit(context, oframe)
@@ -413,9 +392,6 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
box = layout.box() box = layout.box()
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
box.label(text=orient[axis][0], icon=orient[axis][1]) box.label(text=orient[axis][0], icon=orient[axis][1])
def execute(self, context): def execute(self, context):
t0 = time() t0 = time()
@@ -450,12 +426,12 @@ def register():
for cl in classes: for cl in classes:
bpy.utils.register_class(cl) bpy.utils.register_class(cl)
bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.append(reproject_context_menu) bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.append(reproject_context_menu)
bpy.types.GPENCIL_MT_cleanup.append(reproject_clean_menu) bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.append(reproject_clean_menu)
def unregister(): def unregister():
bpy.types.GPENCIL_MT_cleanup.remove(reproject_clean_menu) bpy.types.VIEW3D_MT_edit_greasepencil_cleanup.remove(reproject_clean_menu)
bpy.types.VIEW3D_MT_grease_pencil_edit_context_menu.remove(reproject_context_menu) bpy.types.VIEW3D_MT_greasepencil_edit_context_menu.remove(reproject_context_menu)
for cl in reversed(classes): for cl in reversed(classes):
bpy.utils.unregister_class(cl) bpy.utils.unregister_class(cl)
+3 -1
View File
@@ -2,9 +2,11 @@
Blender addon - Various tool to help with grease pencil in animation productions. Blender addon - Various tool to help with grease pencil in animation productions.
### /!\ Main branch is currently broken, in migration to gpv3
**[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)** **[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Download for Blender 4.2 and below](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/v3.3.0.zip)** **[Download for Blender 4.2 and below from release page](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+1 -1
View File
@@ -4,7 +4,7 @@ Blender addon - Boîte à outils de grease pencil pour la production d'animation
**[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)** **[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Télécharger pour Blender 4.2 ou inférieure](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/v3.3.0.zip)** **[Téléchargement pour Blender 4.2 ou inférieur depuis la page des releases](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/releases)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+20 -5
View File
@@ -166,7 +166,9 @@ class GPTB_PT_sidebar_panel(Panel):
else: else:
col.label(text='No GP object selected') col.label(text='No GP object selected')
col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
## Gpv3: not more edit line (use Curve lines)
# col.prop(context.scene.gptoolprops, 'edit_lines_opacity')
# Mention update as notice # Mention update as notice
# addon_updater_ops.update_notice_box_ui(self, context) # addon_updater_ops.update_notice_box_ui(self, context)
@@ -279,6 +281,8 @@ class GPTB_PT_anim_manager(Panel):
col.use_property_split = False col.use_property_split = False
text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR') text, icon = ('Cursor Follow On', 'PIVOT_CURSOR') if context.scene.gptoolprops.cursor_follow else ('Cursor Follow Off', 'CURSOR')
col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon) col.prop(context.scene.gptoolprops, 'cursor_follow', text=text, icon=icon)
if context.scene.gptoolprops.cursor_follow:
col.prop(context.scene.gptoolprops, 'cursor_follow_target', text='Target', icon='OBJECT_DATA')
class GPTB_PT_toolbox_playblast(Panel): class GPTB_PT_toolbox_playblast(Panel):
@@ -767,9 +771,15 @@ GPTB_PT_palettes_list_ui, # subpanels
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
if bpy.app.version >= (5,0,0):
bpy.types.GREASE_PENCIL_MT_material_context_menu.append(palette_manager_menu)
# bpy.types.GREASE_PENCIL_MT_material_context_menu.append(palette_manager_menu)
else:
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref) bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref)
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref) # bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP # bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
@@ -782,9 +792,14 @@ def unregister():
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP # bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref) bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref)
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu) if bpy.app.version >= (5,0,0):
bpy.types.GREASE_PENCIL_MT_material_context_menu.remove(palette_manager_menu)
# bpy.types.GREASE_PENCIL_MT_material_context_menu.remove(palette_manager_menu)
else:
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref) # bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
# if bpy.app.version >= (3,0,0): # if bpy.app.version >= (3,0,0):
+48 -37
View File
@@ -4,7 +4,7 @@ bl_info = {
"name": "GP toolbox", "name": "GP toolbox",
"description": "Tool set for Grease Pencil in animation production", "description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux", "author": "Samuel Bernou, Christophe Seux",
"version": (4, 0, 0), "version": (5, 0, 1),
"blender": (4, 3, 0), "blender": (4, 3, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "", "warning": "",
@@ -35,7 +35,7 @@ from . import OP_brushes
from . import OP_file_checker from . import OP_file_checker
from . import OP_copy_paste from . import OP_copy_paste
from . import OP_realign from . import OP_realign
from . import OP_flat_reproject # from . import OP_flat_reproject # Disabled
from . import OP_depth_move from . import OP_depth_move
from . import OP_key_duplicate_send from . import OP_key_duplicate_send
from . import OP_layer_manager from . import OP_layer_manager
@@ -47,6 +47,7 @@ from . import OP_layer_namespace
from . import OP_pseudo_tint from . import OP_pseudo_tint
from . import OP_follow_curve from . import OP_follow_curve
from . import OP_material_move_to_layer from . import OP_material_move_to_layer
from . import OP_delete_viewbound
# from . import OP_eraser_brush # from . import OP_eraser_brush
# from . import TOOL_eraser_brush # from . import TOOL_eraser_brush
from . import handler_draw_cam from . import handler_draw_cam
@@ -159,23 +160,6 @@ class GPTB_prefs(bpy.types.AddonPreferences):
) )
## output settings for automated renders ## output settings for automated renders
output_parent_level = IntProperty(
name='Parent level',
description="Go up in folder to define a render path relative to the file in upper directotys",
default=0,
min=0,
max=20
)
output_path : StringProperty(
name="Output path",
description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH')
playblast_path : StringProperty(
name="Playblast Path",
description="Path to folder for playblasts output",
default="//playblast", maxlen=0, subtype='DIR_PATH')
use_env_palettes : BoolProperty( use_env_palettes : BoolProperty(
name="Use Project Palettes", name="Use Project Palettes",
@@ -205,10 +189,38 @@ class GPTB_prefs(bpy.types.AddonPreferences):
default=True, default=True,
) )
brush_path : StringProperty( if bpy.app.version >= (5, 0, 0):
name="Brushes directory", # DIR_PATH type do not accept relative ("//") without PATH_SUPPORTS_BLEND_RELATIVE since blender 5
description="Path to brushes containing the blends holding the brushes", brush_path : StringProperty(
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path name="Brushes directory",
description="Path to brushes containing the blends holding the brushes",
default="//", maxlen=0, subtype='DIR_PATH', options={'PATH_SUPPORTS_BLEND_RELATIVE'})
output_path : StringProperty(
name="Output path",
description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH', options={'PATH_SUPPORTS_BLEND_RELATIVE'})
playblast_path : StringProperty(
name="Playblast Path",
description="Path to folder for playblasts output",
default="//playblast", maxlen=0, subtype='DIR_PATH', options={'PATH_SUPPORTS_BLEND_RELATIVE'})
else:
brush_path : StringProperty(
name="Brushes directory",
description="Path to brushes containing the blends holding the brushes",
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
output_path : StringProperty(
name="Output path",
description="Path relative to blend to place render",
default="//render", maxlen=0, subtype='DIR_PATH')
playblast_path : StringProperty(
name="Playblast Path",
description="Path to folder for playblasts output",
default="//playblast", maxlen=0, subtype='DIR_PATH')
## namespace ## namespace
separator : StringProperty( separator : StringProperty(
@@ -278,7 +290,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
) )
## KF jumper ## KF jumper
kfj_use_shortcut: BoolProperty( kfj_use_shortcut : BoolProperty(
name = "Use Keyframe Jump Shortcut", name = "Use Keyframe Jump Shortcut",
description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)", description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)",
default = True) default = True)
@@ -288,17 +300,17 @@ class GPTB_prefs(bpy.types.AddonPreferences):
description="Shortcut to trigger previous keyframe jump", description="Shortcut to trigger previous keyframe jump",
default="F5") default="F5")
kfj_prev_shift: BoolProperty( kfj_prev_shift : BoolProperty(
name = "Shift", name = "Shift",
description = "add shift", description = "add shift",
default = False) default = False)
kfj_prev_alt: BoolProperty( kfj_prev_alt : BoolProperty(
name = "Alt", name = "Alt",
description = "add alt", description = "add alt",
default = False) default = False)
kfj_prev_ctrl: BoolProperty( kfj_prev_ctrl : BoolProperty(
name = "combine with ctrl", name = "combine with ctrl",
description = "add ctrl", description = "add ctrl",
default = False) default = False)
@@ -308,17 +320,17 @@ class GPTB_prefs(bpy.types.AddonPreferences):
description="Shortcut to trigger keyframe jump", description="Shortcut to trigger keyframe jump",
default="F6") default="F6")
kfj_next_shift: BoolProperty( kfj_next_shift : BoolProperty(
name = "Shift", name = "Shift",
description = "add shift", description = "add shift",
default = False) default = False)
kfj_next_alt: BoolProperty( kfj_next_alt : BoolProperty(
name = "Alt", name = "Alt",
description = "add alt", description = "add alt",
default = False) default = False)
kfj_next_ctrl: BoolProperty( kfj_next_ctrl : BoolProperty(
name = "combine with ctrl", name = "combine with ctrl",
description = "add ctrl", description = "add ctrl",
default = False) default = False)
@@ -336,7 +348,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
nav_fade_val : FloatProperty( nav_fade_val : FloatProperty(
name='Fade Value', name='Fade Value',
description='Fade value for other layers when navigating (0=invisible)', description='Fade value for other layers when navigating (0=invisible)',
default=0.35, min=0.0, max=0.95, step=1, precision=2) default=0.1, min=0.0, max=0.95, step=1, precision=2)
nav_limit : FloatProperty( nav_limit : FloatProperty(
name='Fade Duration', name='Fade Duration',
@@ -625,9 +637,9 @@ class GPTB_prefs(bpy.types.AddonPreferences):
layout.label(text='Following checks will be made when clicking "Check File" button:') layout.label(text='Following checks will be made when clicking "Check File" button:')
col = layout.column() col = layout.column()
col.use_property_split = True col.use_property_split = True
col.prop(self.fixprops, 'check_only') # col.prop(self.fixprops, 'check_only')
col.label(text='If dry run is checked, no modification is done', icon='INFO') col.label(text='The Popup list possible fixes, you can then use the "Apply Fixes"', icon='INFO')
col.label(text='Use Ctrl + Click on "Check File" button to invert the behavior', icon='BLANK1') # col.label(text='Use Ctrl + Click on "Check File" to abply directly', icon='BLANK1')
col.separator() col.separator()
col.prop(self.fixprops, 'lock_main_cam') col.prop(self.fixprops, 'lock_main_cam')
col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})') col.prop(self.fixprops, 'set_scene_res', text=f'Reset Scene Resolution (to {self.render_res_x}x{self.render_res_y})')
@@ -703,8 +715,6 @@ def set_namespace_env(name_env, prop_group):
n.is_project = n.tag in project_pfix n.is_project = n.tag in project_pfix
def set_env_properties(): def set_env_properties():
prefs = get_addon_prefs() prefs = get_addon_prefs()
fps = os.getenv('FPS') fps = os.getenv('FPS')
@@ -793,7 +803,7 @@ addon_modules = (
OP_brushes, OP_brushes,
OP_cursor_snap_canvas, OP_cursor_snap_canvas,
OP_copy_paste, OP_copy_paste,
OP_flat_reproject, # OP_flat_reproject # Disabled,
OP_realign, OP_realign,
OP_depth_move, OP_depth_move,
OP_key_duplicate_send, OP_key_duplicate_send,
@@ -805,6 +815,7 @@ addon_modules = (
OP_layer_nav, OP_layer_nav,
OP_follow_curve, OP_follow_curve,
OP_material_move_to_layer, OP_material_move_to_layer,
OP_delete_viewbound,
# OP_eraser_brush, # OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush # TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam, handler_draw_cam,
+2
View File
@@ -99,6 +99,7 @@ def gp_stroke_angle_split (frame, strokes, angle):
splitted_loops = bm_angle_split(bm,angle) splitted_loops = bm_angle_split(bm,angle)
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
frame.drawing.strokes.remove(stroke_info['stroke']) frame.drawing.strokes.remove(stroke_info['stroke'])
for loop in splitted_loops : for loop in splitted_loops :
loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop] loop_info = [{'co':v.co,'strength': v[strength], 'pressure' :v[pressure],'select':v[select]} for v in loop]
@@ -123,6 +124,7 @@ def gp_stroke_uniform_density(cam, frame, strokes, max_spacing):
bm_uniform_density(bm,cam,max_spacing) bm_uniform_density(bm,cam,max_spacing)
## FIXME: Should use -> drawing.remove_strokes(indices=(0,))
frame.strokes.remove(stroke_info['stroke']) frame.strokes.remove(stroke_info['stroke'])
bm.verts.ensure_lookup_table() bm.verts.ensure_lookup_table()
+15 -11
View File
@@ -11,10 +11,12 @@ from bpy.props import (
from .OP_cursor_snap_canvas import cursor_follow_update from .OP_cursor_snap_canvas import cursor_follow_update
from .OP_layer_manager import layer_name_build from .OP_layer_manager import layer_name_build
def change_edit_lines_opacity(self, context):
for gp in bpy.data.grease_pencils: ## Obsolete: Gpv3 has no edit line color anymore
if not gp.is_annotation: # def change_edit_lines_opacity(self, context):
gp.edit_line_color[3]=self.edit_lines_opacity # for gp in bpy.data.grease_pencils:
# if not gp.is_annotation:
# gp.edit_line_color[3]=self.edit_lines_opacity
def update_layer_name(self, context): def update_layer_name(self, context):
@@ -31,10 +33,6 @@ def update_layer_name(self, context):
class GP_PG_FixSettings(PropertyGroup): class GP_PG_FixSettings(PropertyGroup):
check_only : BoolProperty(
name="Dry run mode (Check only)",
description="Do not change anything, just print the messages",
default=False, options={'HIDDEN'})
lock_main_cam : BoolProperty( lock_main_cam : BoolProperty(
name="Lock Main Cam", name="Lock Main Cam",
@@ -182,9 +180,15 @@ class GP_PG_ToolsSettings(PropertyGroup):
name='Cursor Follow', description="3D cursor follow active object animation when activated", name='Cursor Follow', description="3D cursor follow active object animation when activated",
default=False, update=cursor_follow_update) default=False, update=cursor_follow_update)
edit_lines_opacity : FloatProperty( cursor_follow_target : bpy.props.PointerProperty(
name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils", name='Cursor Follow Target',
default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity) description="Optional target object to follow for cursor instead of active object",
type=bpy.types.Object, update=cursor_follow_update)
## gpv3 : no edit line color anymore
# edit_lines_opacity : FloatProperty(
# name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils",
# default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
## render ## render
name_for_current_render : StringProperty( name_for_current_render : StringProperty(
+358 -107
View File
@@ -5,18 +5,85 @@ import mathutils
import math import math
import subprocess import subprocess
from time import time
from math import sqrt from math import sqrt
from mathutils import Vector from mathutils import Vector
from sys import platform from sys import platform
## constants values
""" def get_gp_parent(layer) : ## Default stroke and points attributes
if layer.parent_type == "BONE" and layer.parent_bone : stroke_attr = [
return layer.parent.pose.bones.get(layer.parent_bone) 'start_cap',
else : 'end_cap',
return layer.parent 'softness',
""" 'material_index',
'fill_opacity',
'fill_color',
'cyclic',
'aspect_ratio',
'time_start',
# 'curve_type', # read-only
]
point_attr = [
'position',
'radius',
'rotation',
'opacity',
'vertex_color',
'delta_time',
# 'select',
]
### Attribute value, types and shape
attribute_value_string = {
'FLOAT': "value",
'INT': "value",
'FLOAT_VECTOR': "vector",
'FLOAT_COLOR': "color",
'BYTE_COLOR': "color",
'STRING': "value",
'BOOLEAN': "value",
'FLOAT2': "value",
'INT8': "value",
'INT32_2D': "value",
'QUATERNION': "value",
'FLOAT4X4': "value",
}
attribute_value_dtype = {
'FLOAT': np.float32,
'INT': np.dtype('int'),
'FLOAT_VECTOR': np.float32,
'FLOAT_COLOR': np.float32,
'BYTE_COLOR': np.int8,
'STRING': np.dtype('str'),
'BOOLEAN': np.dtype('bool'),
'FLOAT2': np.float32,
'INT8': np.int8,
'INT32_2D': np.dtype('int'),
'QUATERNION': np.float32,
'FLOAT4X4': np.float32,
}
attribute_value_shape = {
'FLOAT': (),
'INT': (),
'FLOAT_VECTOR': (3,),
'FLOAT_COLOR': (4,),
'BYTE_COLOR': (4,),
'STRING': (),
'BOOLEAN': (),
'FLOAT2':(2,),
'INT8': (),
'INT32_2D': (2,),
'QUATERNION': (4,),
'FLOAT4X4': (4,4),
}
def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin): def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin):
return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
@@ -77,7 +144,7 @@ def object_derived_get(ob, scene):
# ----------------- # -----------------
### Bmesh # region Bmesh
# ----------------- # -----------------
def link_vert(v,ordered_vert) : def link_vert(v,ordered_vert) :
@@ -152,9 +219,74 @@ def gp_stroke_to_bmesh(strokes):
# ----------------- # -----------------
### GP Drawing # region GP Drawing
# ----------------- # -----------------
def layer_active_index(gpl):
'''Get layer list and return index of active layer
Can return None if no active layer found (active item can be a group)
'''
return next((i for i, l in enumerate(gpl) if l == gpl.active), None)
def get_top_layer_from_group(gp, group):
upper_layer = None
for layer in gp.layers:
if layer.parent_group == group:
upper_layer = layer
return upper_layer
def get_closest_active_layer(gp):
'''Get active layer from GP object, getting upper layer if in group
if a group is active, return the top layer of this group
if group is active but no layer in it, return None
'''
if gp.layers.active:
return gp.layers.active
## No active layer, return active from group (can be None !)
return get_top_layer_from_group(gp, gp.layer_groups.active)
def closest_layer_active_index(gp, fallback_index=0):
'''Get active layer index from GP object, getting upper layer if in group
if a group is active, return index at the top layer of this group
if group is active but no layer in it, return fallback_index (0 by default, stack bottom)'''
closest_active_layer = get_closest_active_layer(gp)
if closest_active_layer:
return next((i for i, l in enumerate(gp.layers) if l == closest_active_layer), fallback_index)
return fallback_index
## Check for nested lock
def is_locked(stack_item):
'''Check if passed stack item (layer or group) is locked
either itself or by parent groups'''
if stack_item.lock:
return True
if stack_item.parent_group:
return is_locked(stack_item.parent_group)
return False
def is_parent_locked(stack_item):
'''Check if passed stack item (layer or group) is locked by parent groups'''
if stack_item.parent_group:
return is_locked(stack_item.parent_group)
return False
## Check for nested hide
def is_hidden(stack_item):
'''Check if passed stack item (layer or group) is hidden
either itself or by parent groups'''
if stack_item.hide:
return True
if stack_item.parent_group:
return is_hidden(stack_item.parent_group)
return False
def is_parent_hidden(stack_item):
'''Check if passed stack item (layer or group) is hidden by parent groups'''
if stack_item.parent_group:
return is_hidden(stack_item.parent_group)
return False
def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0): def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0):
''' '''
draw basic stroke by passing list of point 3D coordinate draw basic stroke by passing list of point 3D coordinate
@@ -261,7 +393,7 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
return rightMin + (valueScaled * rightSpan) return rightMin + (valueScaled * rightSpan)
# ----------------- # -----------------
### GP funcs # region GP funcs
# ----------------- # -----------------
def get_gp_draw_plane(obj=None, orient=None): def get_gp_draw_plane(obj=None, orient=None):
@@ -491,68 +623,137 @@ def copy_stroke_to_frame(s, frame, select=True):
return created stroke return created stroke
''' '''
ns = frame.drawing.strokes.new() frame.drawing.add_strokes([len(s.points)])
ns = frame.drawing.strokes[-1]
## Set strokes attr # print(len(s.points), 'new:', len(ns.points))
stroke_attr = [ #ns.material_index
'line_width',
'material_index',
'draw_cyclic',
'use_cyclic',
'uv_scale',
'uv_rotation',
'hardness',
'uv_translation',
'vertex_color_fill',
]
## replicate attributes (simple loop)
## TODO : might need to create atribute domain if does not exists in destination
for attr in stroke_attr: for attr in stroke_attr:
if not hasattr(s, attr):
continue
# print(f'transfer stroke {attr}') # Dbg
setattr(ns, attr, getattr(s, attr)) setattr(ns, attr, getattr(s, attr))
## create points for src_p, dest_p in zip(s.points, ns.points):
point_count = len(s.points) for attr in point_attr:
ns.points.add(len(s.points)) setattr(dest_p, attr, getattr(src_p, attr))
## Define selection
## Set points attr dest_p.select=select
# for p, np in zip(s.points, ns.points):
flat_list = [0.0] * point_count
flat_uv_fill_list = [0.0, 0.0] * point_count
flat_vector_list = [0.0, 0.0, 0.0] * point_count
flat_color_list = [0.0, 0.0, 0.0, 0.0] * point_count
single_attr = [
'pressure',
'strength',
'uv_factor',
'uv_rotation',
]
for attr in single_attr: ## Direcly iterate over attribute ?
# print(f'transfer point {attr}') # Dbg # src_start = src_dr.curve_offsets[0].value
s.points.foreach_get(attr, flat_list) # src_end = src_start + data_size
ns.points.foreach_set(attr, flat_list) # dst_start = dst_dr.curve_offsets[0].value
# dst_end = dst_start + data_size
# for src_idx, dest_idx in zip(range(src_start, src_end),range(dst_start, dst_end)):
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
# print(f'transfer point co') # Dbg
s.points.foreach_get('co', flat_vector_list)
ns.points.foreach_set('co', flat_vector_list)
# print(f'transfer point uv_fill') # Dbg
s.points.foreach_get('uv_fill', flat_uv_fill_list)
ns.points.foreach_set('uv_fill', flat_uv_fill_list)
# print(f'transfer point vertex_color') # Dbg
s.points.foreach_get('vertex_color', flat_color_list)
ns.points.foreach_set('vertex_color', flat_color_list)
ns.select = select
ns.points.update()
return ns return ns
"""## Works, but do not copy all attributes type (probably ok for GP though)
def bulk_frame_copy_attributes(source_attr, target_attr):
'''Get and apply data as flat numpy array based on attribute type'''
if source_attr.data_type == 'INT':
data = np.empty(len(source_attr.data), dtype=np.int32)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'INT8':
data = np.empty(len(source_attr.data), dtype=np.int8)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'FLOAT':
data = np.empty(len(source_attr.data), dtype=np.float32)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
elif source_attr.data_type == 'FLOAT_VECTOR':
data = np.empty(len(source_attr.data) * 3, dtype=np.float32)
source_attr.data.foreach_get('vector', data)
target_attr.data.foreach_set('vector', data)
elif source_attr.data_type == 'FLOAT_COLOR':
data = np.empty(len(source_attr.data) * 4, dtype=np.float32)
source_attr.data.foreach_get('color', data)
target_attr.data.foreach_set('color', data)
elif source_attr.data_type == 'BOOLEAN':
data = np.empty(len(source_attr.data), dtype=bool)
source_attr.data.foreach_get('value', data)
target_attr.data.foreach_set('value', data)
## works in slowmotion (keep as reference for testing)
# def copy_attribute_values(src_dr, dst_dr, source_attr, dest_attr, data_size):
# ## Zip method to copy one by one
# val_type = {'FLOAT_COLOR': 'color','FLOAT_VECTOR': 'vector'}.get(source_attr.data_type, 'value')
# src_start = src_dr.curve_offsets[0].value
# src_end = src_start + data_size
# dst_start = dst_dr.curve_offsets[0].value
# dst_end = dst_start + data_size
# for src_idx, dest_idx in zip(range(src_start, src_end),range(dst_start, dst_end)):
# setattr(dest_attr.data[dest_idx], val_type, getattr(source_attr.data[src_idx], val_type))
"""
def bulk_copy_attributes(source_attr, target_attr):
'''Get and apply data as flat numpy array based on attribute type'''
value_string = attribute_value_string[source_attr.data_type]
dtype = attribute_value_dtype[source_attr.data_type]
shape = attribute_value_shape[source_attr.data_type]
domain_size = len(source_attr.data)
## Need to pass attributes to get domain size
# domain_size = attributes.domain_size(source_attr.domain)
# start = time()
data = np.empty((domain_size, *shape), dtype=dtype).ravel()
source_attr.data.foreach_get(value_string, data)
target_attr.data.foreach_set(value_string, data)
# end = time()
# np_empty = end - start
## np.prod (works, supposedly faster but tested slower)
# data = np.empty(int(domain_size * np.prod(shape)), dtype=dtype)
# source_attr.data.foreach_get(value_string, data)
# target_attr.data.foreach_set(value_string, data)
## np.zeros (works, sometimes faster on big set of attributes)
# start = time()
# data = np.zeros((domain_size, *shape), dtype=dtype)
# source_attr.data.foreach_get(value_string, np.ravel(data))
# target_attr.data.foreach_set(value_string, np.ravel(data))
# end = time()
# np_zero = end - start
# print('np EMPTY faster' if np_empty < np_zero else 'np ZERO faster', source_attr.domain, source_attr.data_type, domain_size)
# print('np_zero', np_zero)
# print('np_empty', np_empty)
# print()
def copy_frame_at(source_frame, layer, frame_number):
'''Copy a frame (source_frame) to a layer at given frame_number'''
source_drawing = source_frame.drawing
# frame_copy_start = time() # time_dbg
frame = layer.frames.new(frame_number)
dr = frame.drawing
dr.add_strokes([len(s.points) for s in source_drawing.strokes])
for attr_name in source_drawing.attributes.keys():
source_attr = source_drawing.attributes[attr_name]
if attr_name not in dr.attributes:
dr.attributes.new(
name=attr_name, type=source_attr.data_type, domain=source_attr.domain)
target_attr = dr.attributes[attr_name]
# start_time = time() # time_dbg-per-attrib
# bulk_frame_copy_attributes(source_attr, target_attr) # only some attributes
bulk_copy_attributes(source_attr, target_attr)
# copy_attribute_values(source_drawing, dr, source_attr, target_attr, source_drawing.attributes.domain_size(source_attr.domain)) # super slow
# end_time = time() # time_dbg-per-attrib
# print(f"copy_attribute '{attr_name}' execution time: {end_time - start_time} seconds") # time_dbg-per-attrib
# frame_copy_end = time() # time_dbg
# print(f"frame copy execution time: {frame_copy_end - frame_copy_start} seconds") # time_dbg
# ----------------- # -----------------
### Vector utils 3d # region Vector utils 3d
# ----------------- # -----------------
def matrix_transform(coords, matrix): def matrix_transform(coords, matrix):
@@ -613,7 +814,7 @@ def extrapolate_points_by_length(a,b, length):
return b + (ab.normalized() * length) return b + (ab.normalized() * length)
# ----------------- # -----------------
### Vector utils 2d # region Vector utils 2d
# ----------------- # -----------------
@@ -666,7 +867,7 @@ def midpoint_2d(p1, p2):
# ----------------- # -----------------
### Collection management # region Collection management
# ----------------- # -----------------
def set_collection(ob, collection, unlink=True) : def set_collection(ob, collection, unlink=True) :
@@ -702,24 +903,11 @@ def set_collection(ob, collection, unlink=True) :
# ----------------- # -----------------
### Path utils # region Path utils
# ----------------- # -----------------
def get_addon_prefs(): def get_addon_prefs():
''' return bpy.context.preferences.addons[__package__].preferences
function to read current addon preferences properties
access a prop like this :
prefs = get_addon_prefs()
option_state = prefs.super_special_option
oneliner : get_addon_prefs().super_special_option
'''
import os
addon_name = os.path.splitext(__name__)[0]
preferences = bpy.context.preferences
addon_prefs = preferences.addons[addon_name].preferences
return (addon_prefs)
def open_addon_prefs(): def open_addon_prefs():
'''Open addon prefs windows with focus on current addon''' '''Open addon prefs windows with focus on current addon'''
@@ -823,34 +1011,44 @@ def convert_attr(Attr):
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
'''Show message box with element passed as string or list '''Show message box with element passed as string or list
if _message if a list of lists: if _message if a list of lists:
if first element is "OPERATOR":
List format: ["OPERATOR", operator_id, text, icon, {prop_name: value, ...}]
if sublist have 2 element: if sublist have 2 element:
considered a label [text,icon] considered a label [text, icon]
if sublist have 3 element: if sublist have 3 element:
considered as an operator [ops_id_name, text, icon] considered as an operator [ops_id_name, text, icon]
if sublist have 4 element:
considered as a property [object, propname, text, icon]
''' '''
def draw(self, context): def draw(self, context):
layout = self.layout
for l in _message: for l in _message:
if isinstance(l, str): if isinstance(l, str):
self.layout.label(text=l) layout.label(text=l)
else: elif l[0] == "OPERATOR": # Special operator case with properties
if len(l) == 2: # label with icon layout.operator_context = "INVOKE_DEFAULT"
self.layout.label(text=l[0], icon=l[1]) op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False)
elif len(l) == 3: # ops if len(l) > 4 and isinstance(l[4], dict):
self.layout.operator_context = "INVOKE_DEFAULT" for prop_name, value in l[4].items():
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry setattr(op, prop_name, value)
## offset pnale when using row... elif len(l) == 2: # label with icon
# row = self.layout.row() layout.label(text=l[0], icon=l[1])
# row.label(text=l[1]) elif len(l) == 3: # ops
# row.operator(l[0], icon=l[2]) layout.operator_context = "INVOKE_DEFAULT"
layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
elif len(l) == 4: # prop
row = layout.row(align=True)
row.label(text=l[2], icon=l[3])
row.prop(l[0], l[1], text='')
if isinstance(_message, str): if isinstance(_message, str):
_message = [_message] _message = [_message]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
# ----------------- # -----------------
### UI utils # region UI utils
# ----------------- # -----------------
def refresh_areas(): def refresh_areas():
@@ -966,7 +1164,7 @@ def draw_kmi(km, kmi, layout):
# layout.context_pointer_set("keymap", km) # layout.context_pointer_set("keymap", km)
# ----------------- # -----------------
### linking utility # region linking utility
# ----------------- # -----------------
def link_objects_in_blend(filepath, obj_name_list, link=True): def link_objects_in_blend(filepath, obj_name_list, link=True):
@@ -994,7 +1192,7 @@ def check_objects_in_blend(filepath, avoid_camera=True):
# ----------------- # -----------------
### props handling # region props handling
# ----------------- # -----------------
def iterate_selector(zone, attr, state, info_attr = None, active_access='active'): def iterate_selector(zone, attr, state, info_attr = None, active_access='active'):
@@ -1025,8 +1223,40 @@ def iterate_selector(zone, attr, state, info_attr = None, active_access='active'
return info, bottom return info, bottom
def iterate_active_layer(gpd, state):
'''Iterate active GP layer in stack
gpd: Grease Pencil Data
'''
layers = gpd.layers
l_count = len(layers)
if state: # swap
# info = None
# bottom = None
## Get active layer index
active_index = closest_layer_active_index(gpd, fallback_index=None)
if active_index == None:
## fallback to first layer if nothing found
gpd.layers.active = layers[0]
return
target_index = active_index + state
new_index = target_index % l_count
## set active layer
gpd.layers.active = layers[new_index]
if target_index == l_count:
bottom = 1 # bottom reached, cycle to first
elif target_index < 0:
bottom = -1 # up reached, cycle to last
# info = gpd.layers.active.name
# return info, bottom
# ----------------- # -----------------
### Curve handle # region Curve handle
# ----------------- # -----------------
def create_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None): def create_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None):
@@ -1174,7 +1404,7 @@ def create_follow_path_constraint(ob, curve, follow_curve=False, use_fixed_locat
# ----------------- # -----------------
### Object # region Object
# ----------------- # -----------------
def go_edit_mode(ob, context=None): def go_edit_mode(ob, context=None):
@@ -1221,7 +1451,7 @@ def all_object_modifier_enabled(objects) -> bool:
for o in objects: for o in objects:
if o.type != 'GREASEPENCIL': if o.type != 'GREASEPENCIL':
continue continue
for m in o.modifier: for m in o.modifiers:
if m.show_render and not m.show_viewport: if m.show_render and not m.show_viewport:
return False return False
@@ -1255,6 +1485,30 @@ def has_fully_enabled_anim(o):
return False return False
return True return True
def get_fcurves(obj):
'''return f-curves of object's active action'''
if bpy.app.version >= (5, 0, 0):
# action slot
if obj.animation_data and obj.animation_data.action_slot:
active_slot = obj.animation_data.action_slot
return active_slot.id_data.layers[0].strips[0].channelbag(active_slot).fcurves
else:
if obj.animation_data and obj.animation_data.action:
return obj.animation_data.action.fcurves
return []
def get_active_channelbag(obj):
'''return active channelbag in blender 5, else action'''
if bpy.app.version >= (5, 0, 0):
# action slot
if obj.animation_data and obj.animation_data.action_slot:
active_slot = obj.animation_data.action_slot
return active_slot.id_data.layers[0].strips[0].channelbag(active_slot)
else:
if obj.animation_data and obj.animation_data.action:
return obj.animation_data.action
def anim_status(objects) -> tuple((str, str)): def anim_status(objects) -> tuple((str, str)):
'''Return a tutple of icon string status in ('ALL_ON', 'MIXED', 'ALL_OFF', 'NONE')''' '''Return a tutple of icon string status in ('ALL_ON', 'MIXED', 'ALL_OFF', 'NONE')'''
@@ -1274,8 +1528,9 @@ def anim_status(objects) -> tuple((str, str)):
# off_count += 1 # off_count += 1
### Consider All channels individually ### Consider All channels individually
if o.animation_data and o.animation_data.action:
for grp in o.animation_data.action.groups: if channelbag := get_active_channelbag(o):
for grp in channelbag.groups:
## Check if groups are muted ## Check if groups are muted
if grp.mute: if grp.mute:
off_count += 1 off_count += 1
@@ -1284,7 +1539,7 @@ def anim_status(objects) -> tuple((str, str)):
count += 1 count += 1
for fcu in o.animation_data.action.fcurves: for fcu in channelbag.fcurves:
## Check if fcurves are muted ## Check if fcurves are muted
if fcu.mute: if fcu.mute:
off_count += 1 off_count += 1
@@ -1294,12 +1549,8 @@ def anim_status(objects) -> tuple((str, str)):
if o.type in ('GREASEPENCIL', 'CAMERA'): if o.type in ('GREASEPENCIL', 'CAMERA'):
datablock = o.data datablock = o.data
if datablock.animation_data is None:
continue
if not datablock.animation_data.action:
continue
## Check if object data attributes fcurves are muted ## Check if object data attributes fcurves are muted
for fcu in datablock.animation_data.action.fcurves: for fcu in get_fcurves(datablock):
if fcu.mute: if fcu.mute:
off_count += 1 off_count += 1
else: else:
@@ -1325,7 +1576,7 @@ def gp_modifier_status(objects) -> tuple((str, str)):
## Skip hided object ## Skip hided object
if o.hide_get() and o.hide_render: if o.hide_get() and o.hide_render:
continue continue
for m in o.modifier: for m in o.modifiers:
if m.show_render and not m.show_viewport: if m.show_render and not m.show_viewport:
off_count += 1 off_count += 1
else: else: