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
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
- 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)
fct += 1
gpl.update()
if fct:
self.report({'INFO'}, f"{fct} frame created on layer {gpl.active.info}")
else:
+5 -2
View File
@@ -512,6 +512,9 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
def poll(cls, context):
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,
description='Dump point pressure attribute (already skipped if at default value)')
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.use_property_split = True
col = layout.column()
col.prop(self, 'bake_moves')
col.label(text='Keep following point attributes:')
col.prop(self, 'pressure')
col.prop(self, 'strength')
@@ -544,7 +548,6 @@ class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
return
def execute(self, context):
bake_moves = True
skip_empty_frame = False
org_frame = context.scene.frame_current
@@ -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')
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:
if not l.frames:
continue# skip empty layers
+56 -12
View File
@@ -3,6 +3,7 @@ import bpy
import mathutils
from bpy_extras import view3d_utils
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
class GPTB_OT_cusor_snap(bpy.types.Operator):
@@ -110,15 +111,20 @@ prev_matrix = None
# @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'''
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
# imported in properties to register in boolprop update
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 context.object:
prev_matrix = context.object.matrix_world
bpy.app.handlers.frame_change_post.append(cursor_follow)
else:#False
@@ -129,11 +135,13 @@ def cursor_follow_update(self,context):
def cursor_follow(scene):
'''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)...
if not bpy.context.object:
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
if not ob:
return
global prev_matrix
ob = bpy.context.object
current_matrix = ob.matrix_world
if not prev_matrix:
prev_matrix = current_matrix.copy()
@@ -147,8 +155,6 @@ def cursor_follow(scene):
## translation only
# scene.cursor.location += (current_matrix - prev_matrix).to_translation()
# print('offset:', (current_matrix - prev_matrix).to_translation())
## full
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_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 = (
GPTB_OT_cusor_snap,
)
@@ -163,14 +201,18 @@ GPTB_OT_cusor_snap,
def register():
for cls in classes:
bpy.utils.register_class(cls)
# swap_keymap_by_id('view3d.cursor3d','view3d.cursor_snap')#auto swap to custom GP snap wrap
# bpy.app.handlers.frame_change_post.append(cursor_follow)
## 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
## No need to frame_change_post.append(cursor_follow). Added by property update, when activating 'cursor follow'
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
@@ -180,3 +222,5 @@ def 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]:
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
from bpy.types import Operator
import bgl
from gpu_extras.presets import draw_circle_2d
from gpu_extras.batch import batch_for_shader
import gpu
@@ -190,7 +189,7 @@ class GPTB_OT_eraser(Operator):
bl_options = {'REGISTER', 'UNDO'}
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.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
#print(bg_color)
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
shader.bind()
shader.uniform_float("color", (1, 1, 1, 1))
for mouse, radius in self.mouse_path:
@@ -210,7 +209,7 @@ class GPTB_OT_eraser(Operator):
batch.draw(shader)
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')
+238 -89
View File
@@ -4,6 +4,11 @@ from pathlib import Path
import numpy as np
from . import utils
from bpy.props import (BoolProperty,
PointerProperty,
CollectionProperty,
StringProperty)
def remove_stroke_exact_duplications(apply=True):
'''Remove accidental stroke duplication (points exactly in the same place)
:apply: Remove the duplication instead of just listing dupes
@@ -53,6 +58,10 @@ class GPTB_OT_file_checker(bpy.types.Operator):
# Disable use light on all object
# 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):
# need some self-control (I had to...)
self.ctrl = event.ctrl
@@ -63,10 +72,14 @@ class GPTB_OT_file_checker(bpy.types.Operator):
fix = prefs.fixprops
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.ctrl
apply = self.apply_fixes
if self.ctrl:
apply = True
## 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:
viz_ct = 0
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'
rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1
print(f'{o.name} : viewport {vp} != render {rd}')
print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
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
if fix.list_gp_mod_vis_conflict:
@@ -289,8 +303,12 @@ class GPTB_OT_file_checker(bpy.types.Operator):
else:
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
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')
else:
self.report({'INFO'}, 'All good')
@@ -489,46 +507,9 @@ class GPTB_OT_links_checker(bpy.types.Operator):
self.proj = os.environ.get('PROJECT_ROOT')
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)
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"
class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
bl_idname = "gp.list_viewport_render_visibility"
bl_label = "List Viewport And Render Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
@@ -547,60 +528,221 @@ class GPTB_OT_list_object_visibility(bpy.types.Operator):
def execute(self, context):
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
'''
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
bl_idname = "gp.list_modifier_visibility"
bl_label = "List GP Modifiers Visibility Conflicts"
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
def execute(self, context):
for obj in context.scene.objects:
is_hidden = obj.hide_get() # Get viewlayer visibility
obj.hide_viewport = is_hidden
obj.hide_render = is_hidden
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]
## options:
# check_viewlayer : BoolProperty(name="Check Viewlayer", default=False, description="Compare viewlayer (eye) visibility")
# check_viewport : BoolProperty(name="Check Viewport", default=False, description="Compare Viewport (screen icon) visibility")
# check_render : BoolProperty(name="Check Viewport", default=False, description="Compare Render visibility")
def invoke(self, context, event):
self.ob_list = []
for o in context.scene.objects:
if o.type != 'GPENCIL':
continue
if not len(o.grease_pencil_modifiers):
continue
# 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()
mods = []
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods])
mods.append(m)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
for o in self.ob_list:
layout.label(text=o[0].name, icon='OUTLINER_OB_GREASEPENCIL')
for m in o[1]:
row = layout.row()
row.label(text='')
row.label(text=m.name, icon='MODIFIER_ON')
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
# row.prop(self, "check_viewlayer")
# row.prop(self, "check_viewport")
# row.prop(self, "check_render")
## If filtered by prop, displayed list will resize while applying changes ! (not good)
# 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'
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):
return {'FINISHED'}
'''
## not exposed in UI, Check is performed in Check file (can be called in popped menu)
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
@@ -652,7 +794,14 @@ class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
return {'FINISHED'}
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_copy_string_to_clipboard,
GPTB_OT_copy_multipath_clipboard,
+26 -12
View File
@@ -17,20 +17,27 @@ def get_layer_list(self, context):
class GPTB_OT_duplicate_send_to_layer(Operator) :
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
bl_property = "layers_enum"
layers_enum : bpy.props.EnumProperty(
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,
options={'HIDDEN'},
)
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
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'\
@@ -56,16 +63,16 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
replaced = len(to_replace)
## remove overlapping frames
## Remove overlapping frames
for f in reversed(to_replace):
target_layer.frames.remove(f)
## copy original frames
## Copy original frames
for f in selected_frames:
target_layer.frames.copy(f)
sent = len(selected_frames)
## delete original frames as an option
## Delete original frames as an option
if self.delete_source:
for f in reversed(selected_frames):
act_layer.frames.remove(f)
@@ -74,9 +81,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
if replaced:
mess += f' ({replaced} replaced)'
# context.view_layer.update()
# bpy.ops.gpencil.editmode_toggle()
mod = context.mode
bpy.ops.gpencil.editmode_toggle()
bpy.ops.object.mode_set(mode=mod)
@@ -109,9 +113,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
addon_keymaps = []
def register_keymaps():
# pref = get_addon_prefs()
# if not pref.kfj_use_shortcut:
# return
addon = bpy.context.window_manager.keyconfigs.addon
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)
@@ -129,6 +130,12 @@ def unregister_keymaps():
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 = (
GPTB_OT_duplicate_send_to_layer,
)
@@ -139,12 +146,19 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
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():
if bpy.app.background:
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()
for cls in reversed(classes):
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
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):
'''Reproject - ops method
: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()
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:
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):
@@ -369,14 +371,14 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
name='All Strokes', default=True,
description='Hided and locked layer will also be reprojected')
type: bpy.props.EnumProperty(name='Type',
type : bpy.props.EnumProperty(name='Type',
items=(('CURRENT', "Current", ""),
('FRONT', "Front", ""),
('SIDE', "Side", ""),
('TOP', "Top", ""),
('VIEW', "View", ""),
('SURFACE', "Surface", ""),
('CURSOR', "Cursor", ""),
# ('SURFACE', "Surface", ""),
),
default='CURRENT')
@@ -390,9 +392,29 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator):
layout = self.layout
if not context.region_data.view_perspective == 'CAMERA':
# 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, "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):
+1 -1
View File
@@ -2,7 +2,7 @@
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)**
+1 -1
View File
@@ -2,7 +2,7 @@
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)**
+9 -1
View File
@@ -58,6 +58,11 @@ class GPTB_PT_sidebar_panel(Panel):
## flip X cam
# 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.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
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)
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):
bl_label = "Playblast"
@@ -432,6 +438,8 @@ def palette_manager_menu(self, context):
layout.separator()
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.separator()
layout.operator("gp.move_material_to_layer", text='Move Material To Layer', icon='MATERIAL')
def expose_use_channel_color_pref(self, context):
+16 -7
View File
@@ -4,12 +4,12 @@ bl_info = {
"name": "GP toolbox",
"description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux",
"version": (2, 5, 0),
"blender": (3, 0, 0),
"version": (3, 3, 5),
"blender": (4, 0, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "",
"doc_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox",
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/gp_toolbox/-/issues",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox",
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/gp_toolbox/issues",
"category": "3D View",
}
@@ -46,6 +46,7 @@ from . import OP_git_update
from . import OP_layer_namespace
from . import OP_pseudo_tint
from . import OP_follow_curve
from . import OP_material_move_to_layer
# from . import OP_eraser_brush
# from . import TOOL_eraser_brush
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:')
col = layout.column()
col.use_property_split = True
col.prop(self.fixprops, 'check_only')
col.label(text='If dry run is checked, no modification is done', icon='INFO')
col.label(text='Use Ctrl + Click on "Check File" button to invert the behavior', icon='BLANK1')
col.label(text='The popup list possible fixes, you can then use "Apply Fixes"', icon='INFO')
# col.label(text='(preferences for tool changes are directly applied)', icon='BLANK1')
col.separator()
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})')
@@ -803,6 +803,7 @@ addon_modules = (
OP_layer_picker,
OP_layer_nav,
OP_follow_curve,
OP_material_move_to_layer,
# OP_eraser_brush,
# TOOL_eraser_brush, # experimental eraser brush
handler_draw_cam,
@@ -811,6 +812,10 @@ addon_modules = (
)
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
properties.register()
for cls in classes:
@@ -836,6 +841,10 @@ def register():
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]:
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)
def zoom_to_object(cam, resolution, box, margin=0.01) :
min_x= box[0]
max_x= box[1]
@@ -216,25 +215,6 @@ def zoom_to_object(cam, resolution, box, margin=0.01) :
#print(matrix,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
def get_object_info(mesh_groups, order_list = []) :
scene = bpy.context.scene
+22 -9
View File
@@ -1,6 +1,5 @@
import bpy
import gpu
import bgl
# import blf
from gpu_extras.batch import batch_for_shader
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]
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():
context = bpy.context
if context.region_data.view_perspective != 'CAMERA':
@@ -41,11 +54,12 @@ def draw_cam_frame_callback_2d():
if not main_cam:
return
bgl.glEnable(bgl.GL_BLEND)
gpu.state.blend_set('ALPHA')
frame_point = view3d_camera_border_2d(
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:
### PASSEPARTOUT
@@ -109,8 +123,8 @@ def draw_cam_frame_callback_2d():
### Camera framing trace over
bgl.glLineWidth(1)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
gpu.state.line_width_set(1.0)
# bgl.glEnable(bgl.GL_LINE_SMOOTH) # old smooth
"""
## 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)
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.uniform_float("color", frame_color)
screen_framing.draw(shader_2d)
# bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_LINE_SMOOTH)
bgl.glDisable(bgl.GL_BLEND)
# bgl.glDisable(bgl.GL_LINE_SMOOTH) # old smooth
gpu.state.blend_set('NONE')
draw_handle = None
+5 -5
View File
@@ -31,11 +31,6 @@ def update_layer_name(self, context):
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(
name="Lock Main Cam",
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",
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(
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)
+103 -72
View File
@@ -2,12 +2,13 @@ import bpy, os
import numpy as np
import bmesh
import mathutils
from mathutils import Vector
import math
from math import sqrt
from sys import platform
import subprocess
from math import sqrt
from mathutils import Vector
from sys import platform
""" def get_gp_parent(layer) :
@@ -263,55 +264,6 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
### 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):
''' return tuple with plane coordinate and normal
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
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))
elif orient == 'AXIS_X':#side (Y-Z)
elif orient in ('AXIS_X', 'SIDE'): # side (Y-Z)
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.rotate(mat)
@@ -459,14 +411,14 @@ def get_gp_datas(selection=True):
print('EOL. No active GP object')
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)
if not gp_layer :
gp_layer = gp_data_block.layers.new(name)
return gp_layer
def get_gp_frame(layer,frame_nb = None) :
def get_gp_frame(layer, frame_nb=None) :
scene = bpy.context.scene
if not frame_nb :
frame_nb = scene.frame_current
@@ -532,9 +484,72 @@ def selected_strokes(frame):
stlist.append(s)
return stlist
from math import sqrt
from mathutils import Vector
## Copy stroke to a frame
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
@@ -808,28 +823,37 @@ def convert_attr(Attr):
def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
'''Show message box with element passed as string or list
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:
considered a label [text,icon]
considered a label [text, icon]
if sublist have 3 element:
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):
layout = self.layout
for l in _message:
if isinstance(l, str):
self.layout.label(text=l)
else:
if len(l) == 2: # label with icon
self.layout.label(text=l[0], icon=l[1])
layout.label(text=l)
elif l[0] == "OPERATOR": # Special operator case with properties
layout.operator_context = "INVOKE_DEFAULT"
op = layout.operator(l[1], text=l[2], icon=l[3], emboss=False)
if len(l) > 4 and isinstance(l[4], dict):
for prop_name, value in l[4].items():
setattr(op, prop_name, value)
elif len(l) == 2: # label with icon
layout.label(text=l[0], icon=l[1])
elif len(l) == 3: # ops
self.layout.operator_context = "INVOKE_DEFAULT"
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
## offset pnale when using row...
# row = self.layout.row()
# row.label(text=l[1])
# 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):
_message = [_message]
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
# -----------------
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
def draw_kmi(km, kmi, layout):
map_type = kmi.map_type