Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
462a013d98 | ||
|
|
5aeb21342a | ||
|
|
abe526d046 | ||
|
|
f40d4d7a58 | ||
|
|
cca9494ae4 | ||
|
|
e51cc474d6 | ||
|
|
c02b890915 | ||
|
|
19e26f8cee | ||
|
|
01ce06201e | ||
|
|
92e53f8368 | ||
|
|
386be46251 | ||
|
|
47b9b68e9e | ||
|
|
810256f5cb | ||
|
|
cf2ba8448a |
@@ -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
@@ -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
|
||||||
|
|||||||
+56
-12
@@ -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()
|
||||||
@@ -147,8 +155,6 @@ 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
|
||||||
|
|
||||||
# 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():
|
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
|
||||||
|
|
||||||
@@ -180,3 +222,5 @@ 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
@@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+238
-89
@@ -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,46 +507,9 @@ 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"}
|
||||||
|
|
||||||
@@ -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"}
|
||||||
|
|
||||||
|
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):
|
def invoke(self, context, event):
|
||||||
self.ob_list = []
|
# Clear and rebuild both collections
|
||||||
for o in context.scene.objects:
|
self.visibility_items.clear()
|
||||||
if o.type != 'GPENCIL':
|
|
||||||
continue
|
# Store objects with conflicts
|
||||||
if not len(o.grease_pencil_modifiers):
|
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
|
||||||
continue
|
|
||||||
|
|
||||||
|
# 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)
|
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,
|
||||||
|
|||||||
+26
-12
@@ -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)
|
||||||
@@ -74,9 +81,6 @@ class GPTB_OT_duplicate_send_to_layer(Operator) :
|
|||||||
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()
|
||||||
bpy.ops.object.mode_set(mode=mod)
|
bpy.ops.object.mode_set(mode=mod)
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
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
|
elif len(l) == 3: # ops
|
||||||
self.layout.operator_context = "INVOKE_DEFAULT"
|
layout.operator_context = "INVOKE_DEFAULT"
|
||||||
self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
|
layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry
|
||||||
|
elif len(l) == 4: # prop
|
||||||
## offset pnale when using row...
|
row = layout.row(align=True)
|
||||||
# row = self.layout.row()
|
row.label(text=l[2], icon=l[3])
|
||||||
# row.label(text=l[1])
|
row.prop(l[0], l[1], text='')
|
||||||
# row.operator(l[0], icon=l[2])
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user