14 Commits
Author SHA1 Message Date
pullusb 462a013d98 copy paste : make baking optional, still default
3.3.5

- added: copy paste option to copy only existing frame without baking the whole move
2025-05-06 10:01:13 +02:00
pullusb 5aeb21342a Prevent loading prevent when trying to install in 4.3+ without raising error
3.3.4

- fixed: Error when trying to load addon in 4.3.0+ (prevent loading without error, print about incompatibility in console)
2025-04-17 16:42:25 +02:00
pullusb abe526d046 update_changelog 2025-03-04 14:20:22 +01:00
pullusb f40d4d7a58 backport collection visibility conflict check from gpv3 version
3.3.3
2025-03-04 14:11:51 +01:00
pullusb cca9494ae4 backport cursor follow fix from gpv3 - add msg_bus on selection change owned by GreasePencil type
3.3.2

- added: fix cursor follow and add optional target object (backported from gpv3)
2024-12-03 17:23:10 +01:00
pullusb e51cc474d6 backport check file improve from gpv3 version 2024-12-03 15:28:32 +01:00
pullusb c02b890915 Add Copy material to layer
3.3.0

- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
2024-07-16 17:57:27 +02:00
pullusb 19e26f8cee Fix batch project bug and expose placemnt options
3.2.0

- added: UI settings to show GP tool settings placement and orientation
- fixed: Bug with reproject orientation settings
- added: show current orientation in batch reproject popup UI (if current is selected)
2024-06-04 14:33:31 +02:00
pullusb 01ce06201e move material to layer feature
3.1.0

- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
2024-05-30 18:33:05 +02:00
pullusb 92e53f8368 update urls to gitea repository 2024-03-28 17:10:32 +01:00
pullusb 386be46251 update changelog 3.0.2 2024-03-27 12:10:15 +01:00
pullusb 47b9b68e9e Expose ops copy-move keys to layer in menus
3.0.2

- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
2024-03-27 12:09:57 +01:00
pullusb 810256f5cb fix crash after empty frame creation
3.0.1

- fixed: Crash when drawing directly after generating empty frames
2024-02-22 11:09:26 +01:00
pullusb cf2ba8448a replace bgl with gpu calls update for 4.0
3.0.0

- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
- fixed: openGL draw camera frame and passepartout
2024-02-20 16:07:20 +01:00
17 changed files with 780 additions and 275 deletions
+49
View File
@@ -1,5 +1,54 @@
# Changelog # Changelog
3.3.5
- added: copy paste option to copy only existing frame without baking the whole move
3.3.4
- fixed: Error when trying to load addon in 4.3.0+ (prevent loading without error, print about incompatibility in console)
3.3.3
- added: list collections visibility conflict from search menu with dev extras (backported from gpv3)
3.3.2
- added: fix cursor follow and add optional target object (backported from gpv3)
3.3.1
- added: improve file checker and visibility conflict feature (backported from gpv3)
-- GPv2 code - Above this line, version is separated from 4.3+ version (using GPv3) --
3.3.0
- added: `Move Material To Layer` has now option to copy instead of moving in pop-up menu.
3.2.0
- added: UI settings to show GP tool settings placement and orientation
- fixed: Bug with reproject orientation settings
- added: show current orientation in batch reproject popup UI (if current is selected)
3.1.0
- added: Feature to move all strokes using active material to an existing or new layer (material dropdown menu > `Move Material To Layer`)
3.0.2
- changed: Exposed `Copy/Move Keys To Layer` in Dopesheet(Gpencil), in right clic context menu and `Keys` menu.
3.0.1
- fixed: Crash after generating empty frames
3.0.0
- Update for Blender 4.0 (Breaking release, removed bgl to use gpu)
- fixed: openGL draw camera frame and passepartout
2.5.0 2.5.0
- added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet) - added: Animation manager new button `Frame Select Step` (sort of a checker deselect, but in GP dopesheet)
@@ -166,6 +166,7 @@ class GP_OT_create_empty_frames(bpy.types.Operator):
gpl.active.frames.new(num, active=False) gpl.active.frames.new(num, active=False)
fct += 1 fct += 1
gpl.update()
if fct: if fct:
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}") self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
else: else:
+5 -2
View File
@@ -512,6 +512,9 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' return context.object and context.object.type == 'GPENCIL'
bake_moves : bpy.props.BoolProperty(name='Bake Move', default=True,
description='Copy every frame where object has moved, else copy only existing frames)')
pressure : bpy.props.BoolProperty(name='pressure', default=True, pressure : bpy.props.BoolProperty(name='pressure', default=True,
description='Dump point pressure attribute (already skipped if at default value)') description='Dump point pressure attribute (already skipped if at default value)')
strength : bpy.props.BoolProperty(name='strength', default=True, strength : bpy.props.BoolProperty(name='strength', default=True,
@@ -534,6 +537,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
layout=self.layout layout=self.layout
layout.use_property_split = True layout.use_property_split = True
col = layout.column() col = layout.column()
col.prop(self, 'bake_moves')
col.label(text='Keep following point attributes:') col.label(text='Keep following point attributes:')
col.prop(self, 'pressure') col.prop(self, 'pressure')
col.prop(self, 'strength') col.prop(self, 'strength')
@@ -544,7 +548,6 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
return return
def execute(self, context): def execute(self, context):
bake_moves = True
skip_empty_frame = False skip_empty_frame = False
org_frame = context.scene.frame_current org_frame = context.scene.frame_current
@@ -559,7 +562,7 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
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
+58 -14
View File
@@ -3,6 +3,7 @@ import bpy
import mathutils import mathutils
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
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
from bpy.app.handlers import persistent
## override all sursor snap shortcut with this in keymap ## override all sursor snap shortcut with this in keymap
class GPTB_OT_cusor_snap(bpy.types.Operator): class GPTB_OT_cusor_snap(bpy.types.Operator):
@@ -110,15 +111,20 @@ 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,9 +154,7 @@ 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)
@@ -156,6 +162,38 @@ def cursor_follow(scene):
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')
bpy.msgbus.subscribe_rna(
key=subscribe_to,
# owner of msgbus subcribe (for clearing later)
owner=bpy.types.GreasePencil, # <-- 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 +201,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 +221,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)
+3 -4
View File
@@ -1,6 +1,5 @@
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator
import bgl
from gpu_extras.presets import draw_circle_2d from gpu_extras.presets import draw_circle_2d
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
import gpu import gpu
@@ -190,7 +189,7 @@ class GPTB_OT_eraser(Operator):
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
def draw_callback_px(self): def draw_callback_px(self):
bgl.glEnable(bgl.GL_BLEND) gpu.state.blend_set('ALPHA')
#bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA) #bgl.glBlendFunc(bgl.GL_CONSTANT_ALPHA, bgl.GL_ONE_MINUS_CONSTANT_ALPHA)
#bgl.glBlendColor(1.0, 1.0, 1.0, 0.1) #bgl.glBlendColor(1.0, 1.0, 1.0, 0.1)
@@ -201,7 +200,7 @@ class GPTB_OT_eraser(Operator):
bg_color = area.spaces.active.shading.background_color bg_color = area.spaces.active.shading.background_color
#print(bg_color) #print(bg_color)
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
shader.bind() shader.bind()
shader.uniform_float("color", (1, 1, 1, 1)) shader.uniform_float("color", (1, 1, 1, 1))
for mouse, radius in self.mouse_path: for mouse, radius in self.mouse_path:
@@ -210,7 +209,7 @@ class GPTB_OT_eraser(Operator):
batch.draw(shader) batch.draw(shader)
draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24) draw_circle_2d(self.mouse, (0.75, 0.25, 0.35, 1.0), self.radius, 24)
bgl.glDisable(bgl.GL_BLEND) gpu.state.blend_set('NONE')
+240 -91
View File
@@ -4,6 +4,11 @@ from pathlib import Path
import numpy as np import numpy as np
from . import utils from . import utils
from bpy.props import (BoolProperty,
PointerProperty,
CollectionProperty,
StringProperty)
def remove_stroke_exact_duplications(apply=True): def remove_stroke_exact_duplications(apply=True):
'''Remove accidental stroke duplication (points exactly in the same place) '''Remove accidental stroke duplication (points exactly in the same place)
:apply: Remove the duplication instead of just listing dupes :apply: Remove the duplication instead of just listing dupes
@@ -53,6 +58,10 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# Disable use light on all object # Disable use light on all object
# Remove redundant strokes in frames # Remove redundant strokes in frames
apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False,
description="Apply possible fixes instead of just listing (pop the list again in fix mode)",
options={'SKIP_SAVE'})
def invoke(self, context, event): def invoke(self, context, event):
# need some self-control (I had to...) # need some self-control (I had to...)
self.ctrl = event.ctrl self.ctrl = event.ctrl
@@ -63,10 +72,14 @@ 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)
# apply = not fix.check_only
# # If Ctrl is pressed, invert behavior (invert boolean)
# apply ^= self.ctrl
# If Ctrl is pressed, invert behavior (invert boolean) apply = self.apply_fixes
apply ^= self.ctrl if self.ctrl:
apply = True
## Lock main cam: ## Lock main cam:
if fix.lock_main_cam: if fix.lock_main_cam:
@@ -169,13 +182,14 @@ class GPTB_OT_file_checker(bpy.types.Operator):
if fix.list_obj_vis_conflict: if fix.list_obj_vis_conflict:
viz_ct = 0 viz_ct = 0
for o in context.scene.objects: for o in context.scene.objects:
if o.hide_viewport != o.hide_render: if not (o.hide_get() == o.hide_viewport == o.hide_render):
hv = 'No' if o.hide_get() else 'Yes'
vp = 'No' if o.hide_viewport else 'Yes' vp = 'No' if o.hide_viewport else 'Yes'
rd = 'No' if o.hide_render else 'Yes' rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1 viz_ct += 1
print(f'{o.name} : viewport {vp} != render {rd}') print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
if viz_ct: if viz_ct:
problems.append(['gp.list_object_visibility', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE']) problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
## GP modifiers visibility conflict ## GP modifiers visibility conflict
if fix.list_gp_mod_vis_conflict: if fix.list_gp_mod_vis_conflict:
@@ -289,8 +303,12 @@ class GPTB_OT_file_checker(bpy.types.Operator):
else: else:
print(p[0]) print(p[0])
if not self.apply_fixes:
## button to call the operator again with apply_fixes set to True
problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}])
# Show in viewport # Show in viewport
title = "Changed Settings" if apply else "Checked Settings (dry run, nothing changed)" title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
utils.show_message_box(problems, _title = title, _icon = 'INFO') utils.show_message_box(problems, _title = title, _icon = 'INFO')
else: else:
self.report({'INFO'}, 'All good') self.report({'INFO'}, 'All good')
@@ -489,49 +507,12 @@ class GPTB_OT_links_checker(bpy.types.Operator):
self.proj = os.environ.get('PROJECT_ROOT') self.proj = os.environ.get('PROJECT_ROOT')
return context.window_manager.invoke_props_dialog(self, width=popup_width) return context.window_manager.invoke_props_dialog(self, width=popup_width)
class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
""" OLD links checker with show_message_box bl_idname = "gp.list_viewport_render_visibility"
class GPTB_OT_links_checker(bpy.types.Operator): bl_label = "List Viewport And Render Visibility Conflicts"
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)
if all_lnks:
print('===File check===')
for p in all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
utils.show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
return {"FINISHED"} """
class GPTB_OT_list_object_visibility(bpy.types.Operator):
bl_idname = "gp.list_object_visibility"
bl_label = "List Object Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values" bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
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)
@@ -547,60 +528,221 @@ 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 = []
# viz_ct = 0
# for o in context.scene.objects:
# if o.hide_viewport != o.hide_render:
# vp = 'No' if o.hide_viewport else 'Yes'
# rd = 'No' if o.hide_render else 'Yes'
# viz_ct += 1
# li.append(f'{o.name} : viewport {vp} != render {rd}')
# if li:
# utils.show_message_box(_message=li, _title=f'{viz_ct} visibility conflicts found')
# else:
# self.report({'INFO'}, f"No Object visibility conflict on current scene")
# return {'FINISHED'}
### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
bl_idname = "gp.sync_visibility_from_viewlayer"
bl_label = "Sync Visibility From Viewlayer"
bl_description = "Set viewport and render visibility to match viewlayer visibility"
bl_options = {"REGISTER", "UNDO"}
## Only GP modifier def execute(self, context):
''' for obj in context.scene.objects:
class GPTB_OT_list_modifier_visibility(bpy.types.Operator): is_hidden = obj.hide_get() # Get viewlayer visibility
bl_idname = "gp.list_modifier_visibility" obj.hide_viewport = is_hidden
bl_label = "List GP Modifiers Visibility Conflicts" obj.hide_render = is_hidden
bl_description = "List Modifier visibility conflicts, when viewport and render have different values" 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"} bl_options = {"REGISTER"}
def invoke(self, context, event): visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type]
self.ob_list = []
for o in context.scene.objects:
if o.type != 'GPENCIL':
continue
if not len(o.grease_pencil_modifiers):
continue
mods = [] ## options:
for m in o.grease_pencil_modifiers: # check_viewlayer : BoolProperty(name="Check Viewlayer", default=False, description="Compare viewlayer (eye) visibility")
if m.show_viewport != m.show_render: # check_viewport : BoolProperty(name="Check Viewport", default=False, description="Compare Viewport (screen icon) visibility")
if not mods: # check_render : BoolProperty(name="Check Viewport", default=False, description="Compare Render visibility")
self.ob_list.append([o, mods])
mods.append(m) def invoke(self, context, event):
# Clear and rebuild both collections
self.visibility_items.clear()
# Store objects with conflicts
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
# Create visibility items
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) return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
for o in self.ob_list: # row.prop(self, "check_viewlayer")
layout.label(text=o[0].name, icon='OUTLINER_OB_GREASEPENCIL') # row.prop(self, "check_viewport")
for m in o[1]: # row.prop(self, "check_render")
row = layout.row() ## If filtered by prop, displayed list will resize while applying changes ! (not good)
row.label(text='')
row.label(text=m.name, icon='MODIFIER_ON') # Add sync buttons at the top
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True row = layout.row(align=False)
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True 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'
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'}
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): def execute(self, context):
return {'FINISHED'} 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):
@@ -652,7 +794,14 @@ 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,
+27 -13
View File
@@ -17,20 +17,27 @@ def get_layer_list(self, context):
class GPTB_OT_duplicate_send_to_layer(Operator) : class GPTB_OT_duplicate_send_to_layer(Operator) :
bl_idname = "gp.duplicate_send_to_layer" bl_idname = "gp.duplicate_send_to_layer"
bl_label = 'Duplicate and send to layer' bl_label = 'Duplicate Send To Layer'
bl_description = 'Duplicate selected keys in active layer and send to chosen layer'
# important to have the updated enum here as bl_property # important to have the updated enum here as bl_property
bl_property = "layers_enum" bl_property = "layers_enum"
layers_enum : bpy.props.EnumProperty( layers_enum : bpy.props.EnumProperty(
name="Duplicate to layers", name="Duplicate to layers",
description="Duplicate selected keys in active layer and send them to choosen layer", description="Duplicate selected keys in active layer and send them to chosen layer",
items=get_layer_list, items=get_layer_list,
options={'HIDDEN'}, options={'HIDDEN'},
) )
delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'}) delete_source : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
@classmethod
def description(cls, context, properties):
if properties.delete_source:
return f"Move selected keys in active layer to chosen layer"
else:
return f"Copy selected keys in active layer and send to chosen layer"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'\ return context.object and context.object.type == 'GPENCIL'\
@@ -56,16 +63,16 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
replaced = len(to_replace) replaced = len(to_replace)
## 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)
## copy original frames ## Copy original frames
for f in selected_frames: for f in selected_frames:
target_layer.frames.copy(f) target_layer.frames.copy(f)
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)
@@ -73,9 +80,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
mess = f'{sent} keys copied' mess = f'{sent} keys copied'
if replaced: if replaced:
mess += f' ({replaced} replaced)' mess += f' ({replaced} replaced)'
# context.view_layer.update()
# bpy.ops.gpencil.editmode_toggle()
mod = context.mode mod = context.mode
bpy.ops.gpencil.editmode_toggle() bpy.ops.gpencil.editmode_toggle()
@@ -109,9 +113,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
addon_keymaps = [] addon_keymaps = []
def register_keymaps(): def register_keymaps():
# pref = get_addon_prefs()
# if not pref.kfj_use_shortcut:
# return
addon = bpy.context.window_manager.keyconfigs.addon addon = bpy.context.window_manager.keyconfigs.addon
km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR")
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True) kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='D', value="PRESS", ctrl=True, shift=True)
@@ -129,6 +130,12 @@ def unregister_keymaps():
addon_keymaps.clear() addon_keymaps.clear()
def menu_duplicate_and_send_to_layer(self, context):
if context.space_data.ui_mode == 'GPENCIL':
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='Copy Keys To Layer')
classes = ( classes = (
GPTB_OT_duplicate_send_to_layer, GPTB_OT_duplicate_send_to_layer,
) )
@@ -139,12 +146,19 @@ def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
register_keymaps() register_keymaps()
bpy.types.DOPESHEET_MT_gpencil_key.append(menu_duplicate_and_send_to_layer)
bpy.types.DOPESHEET_MT_context_menu.append(menu_duplicate_and_send_to_layer)
def unregister(): def unregister():
if bpy.app.background: if bpy.app.background:
return return
bpy.types.DOPESHEET_MT_context_menu.remove(menu_duplicate_and_send_to_layer)
bpy.types.DOPESHEET_MT_gpencil_key.remove(menu_duplicate_and_send_to_layer)
unregister_keymaps() unregister_keymaps()
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)
+183
View File
@@ -0,0 +1,183 @@
import bpy
from bpy.types import Operator
import mathutils
from mathutils import Vector, Matrix, geometry
from bpy_extras import view3d_utils
from . import utils
# def get_layer_list(self, context):
# '''return (identifier, name, description) of enum content'''
# if not context:
# return [('None', 'None','None')]
# if not context.object:
# return [('None', 'None','None')]
# return [(l.info, l.info, '') for l in context.object.data.layers] # if l != context.object.data.layers.active
## in Class
# bl_property = "layers_enum"
# layers_enum : bpy.props.EnumProperty(
# name="Send Material To Layer",
# description="Send active material to layer",
# items=get_layer_list,
# options={'HIDDEN'},
# )
class GPTB_OT_move_material_to_layer(Operator) :
bl_idname = "gp.move_material_to_layer"
bl_label = 'Move Material To Layer'
bl_description = 'Move active material to an existing or new layer'
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
layer_name : bpy.props.StringProperty(
name='Layer Name', default='', options={'SKIP_SAVE'})
copy : bpy.props.BoolProperty(
name='Copy to layer', default=False,
description='Copy strokes to layer instead of moving',
options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event):
if self.layer_name:
return self.execute(context)
if not len(context.object.data.layers):
self.report({'WARNING'}, 'No layers on current GP object')
return {'CANCELLED'}
mat = context.object.data.materials[context.object.active_material_index]
self.mat_name = mat.name
# wm.invoke_search_popup(self)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
# layout.operator_context = "INVOKE_DEFAULT"
layout.prop(self, 'copy', text='Copy Strokes')
action_label = 'Copy' if self.copy else 'Move'
layout.label(text=f'{action_label} material "{self.mat_name}" to layer:', icon='MATERIAL')
col = layout.column()
col.prop(self, 'layer_name', text='', icon='ADD')
# if self.layer_name:
# col.label(text='Ok/Enter to create new layer', icon='INFO')
col.separator()
for l in reversed(context.object.data.layers):
icon = 'GREASEPENCIL' if l == context.object.data.layers.active else 'BLANK1'
row = col.row()
row.alignment = 'LEFT'
op = col.operator('gp.move_material_to_layer', text=l.info, icon=icon, emboss=False)
op.layer_name = l.info
op.copy = self.copy
def execute(self, context):
if not self.layer_name:
print('Out')
return {'CANCELLED'}
## Active + selection
pool = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL']
if not context.object in pool:
pool.append(context.object)
mat = context.object.data.materials[context.object.active_material_index]
print(f'Moving strokes using material "{mat.name}" on {len(pool)} object(s)')
# import time
# t = time.time() # Dbg
total = 0
oct = 0
for ob in pool:
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:
print(f'/!\ {ob.name} has no Material {mat.name} in stack')
continue
gpl = ob.data.layers
if not (target_layer := gpl.get(self.layer_name)):
target_layer = gpl.new(self.layer_name)
## List existing frames
key_dict = {f.frame_number : f for f in target_layer.frames}
### Move Strokes to a new key (or existing key if comming for yet another layer)
fct = 0
sct = 0
for l in gpl:
if l == target_layer:
## ! infinite loop if target layer is included
continue
for f in l.frames:
## skip if no stroke has active material
if not next((s for s in f.strokes if s.material_index == mat_index), None):
continue
## Get/Create a destination frame and keep a reference to it
if not (dest_key := key_dict.get(f.frame_number)):
dest_key = target_layer.frames.new(f.frame_number)
key_dict[dest_key.frame_number] = dest_key
print(f'{ob.name} : frame {f.frame_number}')
## Replicate strokes in dest_keys
stroke_to_delete = []
for s in f.strokes:
if s.material_index == mat_index:
utils.copy_stroke_to_frame(s, dest_key)
stroke_to_delete.append(s)
## Debug
# if time.time() - t > 10:
# print('TIMEOUT')
# return {'CANCELLED'}
sct += len(stroke_to_delete)
# print('Removing frames') # Dbg
## Remove from source frame (f)
if not self.copy:
for s in reversed(stroke_to_delete):
f.strokes.remove(s)
## ? Remove frame if layer is empty ? -> probably not, will show previous frame
fct += 1
l.frames.update()
if fct:
oct += 1
print(f'{ob.name}: Moved {fct} frames -> {sct} Strokes') # Dbg
total += fct
report_type = 'INFO' if total else 'WARNING'
if self.copy:
self.report({report_type}, f'Copied {total} frames accross {oct} object(s)')
else:
self.report({report_type}, f'Moved {total} frames accross {oct} object(s)')
return {'FINISHED'}
# def menu_duplicate_and_send_to_layer(self, context):
# if context.space_data.ui_mode == 'GPENCIL':
# 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='Copy Keys To Layer')
classes = (
GPTB_OT_move_material_to_layer,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+56 -34
View File
@@ -15,6 +15,35 @@ def get_scale_matrix(scale):
matscale = matscale_x @ matscale_y @ matscale_z matscale = matscale_x @ matscale_y @ matscale_z
return matscale return matscale
'''
## Old reproject method using Operators:
omode = bpy.context.mode
if all_strokes:
layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers]
for l in obj.data.layers:
l.hide = False
l.lock = False
l.lock_frame = False
bpy.ops.object.mode_set(mode='EDIT_GPENCIL')
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 hided, locked layers
@@ -72,40 +101,13 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False
# new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel() # new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel()
s.points.foreach_set('co', new_local_coords) s.points.foreach_set('co', new_local_coords)
bpy.context.area.tag_redraw()
'''
## Old 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_GPENCIL')
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)
'''
if restore_frame: if restore_frame:
bpy.context.scene.frame_current = oframe bpy.context.scene.frame_current = oframe
## Update the layer and redraw all viewports
obj.data.layers.update()
utils.refresh_areas()
def align_global(reproject=True, ref=None, all_strokes=True): def align_global(reproject=True, ref=None, all_strokes=True):
@@ -369,14 +371,14 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
name='All Strokes', default=True, name='All Strokes', default=True,
description='Hided and locked layer will also be reprojected') description='Hided and locked layer will also be reprojected')
type: bpy.props.EnumProperty(name='Type', type : bpy.props.EnumProperty(name='Type',
items=(('CURRENT', "Current", ""), items=(('CURRENT', "Current", ""),
('FRONT', "Front", ""), ('FRONT', "Front", ""),
('SIDE', "Side", ""), ('SIDE', "Side", ""),
('TOP', "Top", ""), ('TOP', "Top", ""),
('VIEW', "View", ""), ('VIEW', "View", ""),
('SURFACE', "Surface", ""),
('CURSOR', "Cursor", ""), ('CURSOR', "Cursor", ""),
# ('SURFACE', "Surface", ""),
), ),
default='CURRENT') default='CURRENT')
@@ -390,9 +392,29 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
layout = self.layout layout = self.layout
if not context.region_data.view_perspective == 'CAMERA': if not context.region_data.view_perspective == 'CAMERA':
# layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR') # layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR')
layout.label(text='Reprojection is made from camera, not current view', icon='ERROR') layout.label(text='Reprojection is made from camera', icon='ERROR')
layout.prop(self, "all_strokes") layout.prop(self, "all_strokes")
layout.prop(self, "type") layout.prop(self, "type", text='Project Axis')
## Hint show axis
if self.type == 'CURRENT':
## Show as prop
# row = layout.row()
# row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='Current', icon='INFO')
# row.enabled = False
orient = {
'VIEW' : ['View', 'RESTRICT_VIEW_ON'],
'AXIS_Y': ['front (X-Z)', 'AXIS_FRONT'], # AXIS_Y
'AXIS_X': ['side (Y-Z)', 'AXIS_SIDE'], # AXIS_X
'AXIS_Z': ['top (X-Y)', 'AXIS_TOP'], # AXIS_Z
'CURSOR': ['Cursor', 'PIVOT_CURSOR'],
}
box = layout.box()
axis = context.scene.tool_settings.gpencil_sculpt.lock_axis
box.label(text=orient[axis][0], icon=orient[axis][1])
def execute(self, context): def execute(self, context):
+1 -1
View File
@@ -2,7 +2,7 @@
Blender addon - Various tool to help with grease pencil in animation productions. Blender addon - Various tool to help with grease pencil in animation productions.
**[Download latest](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)** **[Download latest](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+1 -1
View File
@@ -2,7 +2,7 @@
Blender addon - Boîte à outils de grease pencil pour la production d'animation. Blender addon - Boîte à outils de grease pencil pour la production d'animation.
**[Télécharger la dernière version](https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/archive/master/gp_toolbox-master.zip)** **[Télécharger la dernière version](https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/archive/master.zip)**
**[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)** **[Demo video](https://www.youtube.com/watch?v=Htgao_uPWNs)**
+9 -1
View File
@@ -58,6 +58,11 @@ class GPTB_PT_sidebar_panel(Panel):
## flip X cam ## flip X cam
# layout.label(text='! Flipped !') # layout.label(text='! Flipped !')
row = col.row(align=True)
row.prop(context.scene.tool_settings, 'gpencil_stroke_placement_view3d', text='')
row.prop(context.scene.tool_settings.gpencil_sculpt, 'lock_axis', text='')
row = col.row(align=True) row = col.row(align=True)
row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT row.operator('view3d.camera_mirror_flipx', text = 'Mirror Flip', icon = 'MOD_MIRROR')# ARROW_LEFTRIGHT
@@ -274,7 +279,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):
bl_label = "Playblast" bl_label = "Playblast"
@@ -432,6 +438,8 @@ def palette_manager_menu(self, context):
layout.separator() layout.separator()
layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path layout.operator("gp.load_palette", text='Load json Palette', icon='IMPORT').filepath = prefs.palette_path
layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path layout.operator("gp.save_palette", text='Save json Palette', icon='EXPORT').filepath = prefs.palette_path
layout.separator()
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
def expose_use_channel_color_pref(self, context): def expose_use_channel_color_pref(self, context):
+16 -7
View File
@@ -4,12 +4,12 @@ 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": (2, 5, 0), "version": (3, 3, 5),
"blender": (3, 0, 0), "blender": (4, 0, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "", "warning": "",
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox", "doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues", "tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues",
"category": "3D View", "category": "3D View",
} }
@@ -46,6 +46,7 @@ from . import OP_git_update
from . import OP_layer_namespace 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_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
@@ -624,9 +625,8 @@ 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.label(text='The popup list possible fixes, you can then use "Apply Fixes"', icon='INFO')
col.label(text='If dry run is checked, no modification is done', icon='INFO') # col.label(text='(preferences for tool changes are directly applied)', icon='BLANK1')
col.label(text='Use Ctrl + Click on "Check File" button to invert the behavior', 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})')
@@ -803,6 +803,7 @@ addon_modules = (
OP_layer_picker, OP_layer_picker,
OP_layer_nav, OP_layer_nav,
OP_follow_curve, OP_follow_curve,
OP_material_move_to_layer,
# OP_eraser_brush, # OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush # TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam, handler_draw_cam,
@@ -811,6 +812,10 @@ addon_modules = (
) )
def register(): def register():
if bpy.app.version >= (4, 3, 0):
print(f"Skip GP toolbox load, version {'.'.join([str(x) for x in bl_info['version']])} incompatible with Blender 4.3+")
return
# Register property group first # Register property group first
properties.register() properties.register()
for cls in classes: for cls in classes:
@@ -836,6 +841,10 @@ def register():
def unregister(): def unregister():
if bpy.app.version >= (4, 3, 0):
# print(f'GP Toolbox version is not compatible with Blender 4.3+')
return
if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]: if 'remap_relative' in [hand.__name__ for hand in bpy.app.handlers.save_pre]:
bpy.app.handlers.save_pre.remove(remap_relative) bpy.app.handlers.save_pre.remove(remap_relative)
-20
View File
@@ -165,7 +165,6 @@ def randomise_points(mat, points, attr, strength) :
setattr(point,attr,value+random*strength) setattr(point,attr,value+random*strength)
def zoom_to_object(cam, resolution, box, margin=0.01) : def zoom_to_object(cam, resolution, box, margin=0.01) :
min_x= box[0] min_x= box[0]
max_x= box[1] max_x= box[1]
@@ -216,25 +215,6 @@ def zoom_to_object(cam, resolution, box, margin=0.01) :
#print(matrix,resolution) #print(matrix,resolution)
return modelview_matrix,projection_matrix,frame,resolution return modelview_matrix,projection_matrix,frame,resolution
def set_viewport_matrix(width, height, mat):
from bgl import glViewport,glMatrixMode,GL_PROJECTION,glLoadMatrixf,Buffer,GL_FLOAT,glMatrixMode,GL_MODELVIEW,glLoadIdentity
glViewport(0,0,width,height)
#glLoadIdentity()
glMatrixMode(GL_PROJECTION)
projection = [mat[j][i] for i in range(4) for j in range(4)]
glLoadMatrixf(Buffer(GL_FLOAT, 16, projection))
#glMatrixMode( GL_MODELVIEW )
#glLoadIdentity()
# get object info # get object info
def get_object_info(mesh_groups, order_list = []) : def get_object_info(mesh_groups, order_list = []) :
scene = bpy.context.scene scene = bpy.context.scene
+22 -9
View File
@@ -1,6 +1,5 @@
import bpy import bpy
import gpu import gpu
import bgl
# import blf # import blf
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -30,6 +29,20 @@ def view3d_camera_border_2d(context, cam):
frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame] frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame]
return frame_px return frame_px
def vertices_to_line_loop(v_list, closed=True) -> list:
'''Take a sequence of vertices
return a position lists of segments to create a line loop passing in all points
the result is usable with gpu_shader 'LINES'
ex: vlist = [a,b,c] -> closed=True return [a,b,b,c,c,a], closed=False return [a,b,b,c]
'''
loop = []
for i in range(len(v_list) - 1):
loop += [v_list[i], v_list[i + 1]]
if closed:
# Add segment between last and first to close loop
loop += [v_list[-1], v_list[0]]
return loop
def draw_cam_frame_callback_2d(): def draw_cam_frame_callback_2d():
context = bpy.context context = bpy.context
if context.region_data.view_perspective != 'CAMERA': if context.region_data.view_perspective != 'CAMERA':
@@ -41,11 +54,12 @@ def draw_cam_frame_callback_2d():
if not main_cam: if not main_cam:
return return
bgl.glEnable(bgl.GL_BLEND) gpu.state.blend_set('ALPHA')
frame_point = view3d_camera_border_2d( frame_point = view3d_camera_border_2d(
context, context.scene.camera.parent) context, context.scene.camera.parent)
shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR') shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') # POLYLINE_FLAT_COLOR
# gpu.shader.from_builtin('2D_UNIFORM_COLOR')
if context.scene.gptoolprops.drawcam_passepartout: if context.scene.gptoolprops.drawcam_passepartout:
### PASSEPARTOUT ### PASSEPARTOUT
@@ -109,8 +123,8 @@ def draw_cam_frame_callback_2d():
### Camera framing trace over ### Camera framing trace over
bgl.glLineWidth(1) gpu.state.line_width_set(1.0)
bgl.glEnable(bgl.GL_LINE_SMOOTH) # bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth
""" """
## need to accurately detect viewport background color (difficult) ## need to accurately detect viewport background color (difficult)
@@ -135,15 +149,14 @@ def draw_cam_frame_callback_2d():
frame_color = (0.0, 0.0, 0.25, 1.0) frame_color = (0.0, 0.0, 0.25, 1.0)
screen_framing = batch_for_shader( screen_framing = batch_for_shader(
shader_2d, 'LINE_LOOP', {"pos": frame_point}) shader_2d, 'LINES', {"pos": vertices_to_line_loop(frame_point)})
shader_2d.bind() shader_2d.bind()
shader_2d.uniform_float("color", frame_color) shader_2d.uniform_float("color", frame_color)
screen_framing.draw(shader_2d) screen_framing.draw(shader_2d)
# bgl.glLineWidth(1) # bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth
bgl.glDisable(bgl.GL_LINE_SMOOTH) gpu.state.blend_set('NONE')
bgl.glDisable(bgl.GL_BLEND)
draw_handle = None draw_handle = None
+5 -5
View File
@@ -31,11 +31,6 @@ def update_layer_name(self, context):
class GP_PG_FixSettings(PropertyGroup): class GP_PG_FixSettings(PropertyGroup):
check_only : BoolProperty(
name="Dry run mode (Check only)",
description="Do not change anything, just print the messages",
default=False, options={'HIDDEN'})
lock_main_cam : BoolProperty( lock_main_cam : BoolProperty(
name="Lock Main Cam", name="Lock Main Cam",
description="Lock the main camera (works only if 'layout' is not in name)", description="Lock the main camera (works only if 'layout' is not in name)",
@@ -182,6 +177,11 @@ class GP_PG_ToolsSettings(PropertyGroup):
name='Cursor Follow', description="3D cursor follow active object animation when activated", name='Cursor Follow', description="3D cursor follow active object animation when activated",
default=False, update=cursor_follow_update) default=False, update=cursor_follow_update)
cursor_follow_target : bpy.props.PointerProperty(
name='Cursor Follow Target',
description="Optional target object to follow for cursor instead of active object",
type=bpy.types.Object, update=cursor_follow_update)
edit_lines_opacity : FloatProperty( edit_lines_opacity : FloatProperty(
name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils", name="Edit Lines Opacity", description="Change edit lines opacity for all grease pencils",
default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity) default=0.5, min=0.0, max=1.0, step=3, precision=2, update=change_edit_lines_opacity)
+104 -73
View File
@@ -2,12 +2,13 @@ import bpy, os
import numpy as np import numpy as np
import bmesh import bmesh
import mathutils import mathutils
from mathutils import Vector
import math import math
from math import sqrt
from sys import platform
import subprocess import subprocess
from math import sqrt
from mathutils import Vector
from sys import platform
""" def get_gp_parent(layer) : """ def get_gp_parent(layer) :
@@ -263,55 +264,6 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
### GP funcs ### GP funcs
# ----------------- # -----------------
""" V1
def get_gp_draw_plane(obj=None):
''' return tuple with plane coordinate and normal
of the curent drawing accordign to geometry'''
context = bpy.context
settings = context.scene.tool_settings
orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR'
loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE'
if obj:
mat = obj.matrix_world
else:
mat = context.object.matrix_world if context.object else None
# -> placement
if loc == "CURSOR":
plane_co = context.scene.cursor.location
else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE')
if not context.object:
plane_co = None
else:
plane_co = context.object.matrix_world.to_translation()# context.object.location
# -> orientation
if orient == 'VIEW':
#only depth is important, no need to get view vector
plane_no = None
elif orient == 'AXIS_Y':#front (X-Z)
plane_no = Vector((0,1,0))
plane_no.rotate(mat)
elif orient == 'AXIS_X':#side (Y-Z)
plane_no = Vector((1,0,0))
plane_no.rotate(mat)
elif orient == 'AXIS_Z':#top (X-Y)
plane_no = Vector((0,0,1))
plane_no.rotate(mat)
elif orient == 'CURSOR':
plane_no = Vector((0,0,1))
plane_no.rotate(context.scene.cursor.matrix)
return plane_co, plane_no
"""
## V2
def get_gp_draw_plane(obj=None, orient=None): def get_gp_draw_plane(obj=None, orient=None):
''' return tuple with plane coordinate and normal ''' return tuple with plane coordinate and normal
of the curent drawing according to geometry''' of the curent drawing according to geometry'''
@@ -336,13 +288,13 @@ def get_gp_draw_plane(obj=None, orient=None):
plane_co = bpy.context.scene.cursor.location plane_co = bpy.context.scene.cursor.location
mat = bpy.context.scene.cursor.matrix mat = bpy.context.scene.cursor.matrix
elif orient == 'AXIS_Y':#front (X-Z) elif orient in ('AXIS_Y', 'FRONT'): # front (X-Z)
plane_no = Vector((0,1,0)) plane_no = Vector((0,1,0))
elif orient == 'AXIS_X':#side (Y-Z) elif orient in ('AXIS_X', 'SIDE'): # side (Y-Z)
plane_no = Vector((1,0,0)) plane_no = Vector((1,0,0))
elif orient == 'AXIS_Z':#top (X-Y) elif orient in ('AXIS_Z', 'TOP'): # top (X-Y)
plane_no = Vector((0,0,1)) plane_no = Vector((0,0,1))
plane_no.rotate(mat) plane_no.rotate(mat)
@@ -459,14 +411,14 @@ def get_gp_datas(selection=True):
print('EOL. No active GP object') print('EOL. No active GP object')
return [] return []
def get_gp_layer(gp_data_block,name) : def get_gp_layer(gp_data_block, name) :
gp_layer = gp_data_block.layers.get(name) gp_layer = gp_data_block.layers.get(name)
if not gp_layer : if not gp_layer :
gp_layer = gp_data_block.layers.new(name) gp_layer = gp_data_block.layers.new(name)
return gp_layer return gp_layer
def get_gp_frame(layer,frame_nb = None) : def get_gp_frame(layer, frame_nb=None) :
scene = bpy.context.scene scene = bpy.context.scene
if not frame_nb : if not frame_nb :
frame_nb = scene.frame_current frame_nb = scene.frame_current
@@ -532,9 +484,72 @@ def selected_strokes(frame):
stlist.append(s) stlist.append(s)
return stlist return stlist
from math import sqrt ## Copy stroke to a frame
from mathutils import Vector
def copy_stroke_to_frame(s, frame, select=True):
'''Copy stroke to given frame
return created stroke
'''
ns = frame.strokes.new()
## Set strokes attr
stroke_attr = [
'line_width',
'material_index',
'draw_cyclic',
'use_cyclic',
'uv_scale',
'uv_rotation',
'hardness',
'uv_translation',
'vertex_color_fill',
]
for attr in stroke_attr:
if not hasattr(s, attr):
continue
# print(f'transfer stroke {attr}') # Dbg
setattr(ns, attr, getattr(s, attr))
## create points
point_count = len(s.points)
ns.points.add(len(s.points))
## Set points attr
# 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:
# print(f'transfer point {attr}') # Dbg
s.points.foreach_get(attr, flat_list)
ns.points.foreach_set(attr, flat_list)
# print(f'transfer point co') # Dbg
s.points.foreach_get('co', flat_vector_list)
ns.points.foreach_set('co', flat_vector_list)
# print(f'transfer point uv_fill') # Dbg
s.points.foreach_get('uv_fill', flat_uv_fill_list)
ns.points.foreach_set('uv_fill', flat_uv_fill_list)
# print(f'transfer point vertex_color') # Dbg
s.points.foreach_get('vertex_color', flat_color_list)
ns.points.foreach_set('vertex_color', flat_color_list)
ns.select = select
ns.points.update()
return ns
# ----------------- # -----------------
### Vector utils 3d ### Vector utils 3d
@@ -808,28 +823,37 @@ def convert_attr(Attr):
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
'''Show message box with element passed as string or list '''Show message box with element passed as string or list
if _message if a list of lists: if _message if a list of lists:
if first element is "OPERATOR":
List format: ["OPERATOR", operator_id, text, icon, {prop_name: value, ...}]
if sublist have 2 element: if sublist have 2 element:
considered a label [text,icon] considered a label [text, icon]
if sublist have 3 element: if sublist have 3 element:
considered as an operator [ops_id_name, text, icon] considered as an operator [ops_id_name, text, icon]
if sublist have 4 element:
considered as a property [object, propname, text, icon]
''' '''
def draw(self, context): def draw(self, context):
layout = self.layout
for l in _message: for l in _message:
if isinstance(l, str): if isinstance(l, str):
self.layout.label(text=l) layout.label(text=l)
else: elif l[0] == "OPERATOR": # Special operator case with properties
if len(l) == 2: # label with icon layout.operator_context = "INVOKE_DEFAULT"
self.layout.label(text=l[0], icon=l[1]) op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False)
elif len(l) == 3: # ops if len(l) > 4 and isinstance(l[4], dict):
self.layout.operator_context = "INVOKE_DEFAULT" for prop_name, value in l[4].items():
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry setattr(op, prop_name, value)
## offset pnale when using row... elif len(l) == 2: # label with icon
# row = self.layout.row() layout.label(text=l[0], icon=l[1])
# row.label(text=l[1]) elif len(l) == 3: # ops
# row.operator(l[0], icon=l[2]) layout.operator_context = "INVOKE_DEFAULT"
layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
elif len(l) == 4: # prop
row = layout.row(align=True)
row.label(text=l[2], icon=l[3])
row.prop(l[0], l[1], text='')
if isinstance(_message, str): if isinstance(_message, str):
_message = [_message] _message = [_message]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
@@ -838,6 +862,13 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
### UI utils ### UI utils
# ----------------- # -----------------
def refresh_areas():
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
area.tag_redraw()
# for area in bpy.context.screen.areas:
# area.tag_redraw()
## kmi draw for addon without delete button ## kmi draw for addon without delete button
def draw_kmi(km, kmi, layout): def draw_kmi(km, kmi, layout):
map_type = kmi.map_type map_type = kmi.map_type