Palette linker

1.8.0

- feat: palette linker (beta), with pop-up from material stack dropdown
- feat: palette name fuzzy match
- code: add an open addon preference ops
gpv2
Pullusb 2021-12-04 13:57:32 +01:00
parent 78b70c8fca
commit 6c19fa54af
9 changed files with 546 additions and 193 deletions

View File

@ -1,5 +1,12 @@
# Changelog
1.8.0
- feat: palette linker (beta), with pop-up from material stack dropdown
- feat: palette name fuzzy match
- code: add an open addon preference ops
1.7.8
- fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation)

View File

@ -352,7 +352,7 @@ class GPTB_OT_links_checker(bpy.types.Operator):
split.label(text=l[0], icon=l[1])
# layout.label(text=l[0], icon=l[1])
else:
split=layout.split(factor=0.75, align=True)
split=layout.split(factor=0.70, align=True)
split.label(text=l[0], icon=l[1])
## resolve() return somethin different than os.path.abspath.
# split.operator('wm.path_open', text='Open folder', icon='FILE_FOLDER').filepath = Path(bpy.path.abspath(l[0])).resolve().parent.as_posix()

View File

@ -323,8 +323,6 @@ class GPTB_OT_reset_cam_rot(bpy.types.Operator):
def execute(self, context):
cam = context.scene.camera
# self.cam_init_euler = self.cam.rotation_euler.copy()
if not cam.parent or cam.parent.type != 'CAMERA':
self.report({'ERROR'}, "No parents to refer to for rotation reset")
return {"CANCELLED"}
@ -362,40 +360,8 @@ class GPTB_OT_reset_cam_rot(bpy.types.Operator):
new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio_inv)) # restore screen ratio
context.space_data.region_3d.view_camera_offset = new_cam_offset
return {"FINISHED"}
""" # old simple cam draw fit to cam
class GPTB_OT_reset_cam_rot(bpy.types.Operator):
bl_idname = "gp.reset_cam_rot"
bl_label = "Reset rotation"
bl_description = "Reset rotation of the draw manipulation camera"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.scene.camera and not context.scene.camera.name.startswith('Cam')
# return context.region_data.view_perspective == 'CAMERA'# check if in camera
def execute(self, context):
# dcam_name = 'draw_cam'
# camcol_name = 'manip_cams'
drawcam = context.scene.camera
if drawcam.parent.type == 'CAMERA':
## align to parent camera
drawcam.matrix_world = drawcam.parent.matrix_world#wrong, get the parent rotation offset
# drawcam.rotation_euler = drawcam.parent.rotation_euler#wrong, get the parent rotation offset
elif drawcam.parent:
## there is a parent, so align the Y of the camera to object's Z
# drawcam.rotation_euler.rotate(drawcam.parent.matrix_world)# wrong
pass
else:
self.report({'ERROR'}, "No parents to refer to for rotation reset")
return {"CANCELLED"}
return {"FINISHED"}
"""
class GPTB_OT_toggle_mute_animation(bpy.types.Operator):
bl_idname = "gp.toggle_mute_animation"
bl_label = "Toggle Animation Mute"
@ -630,6 +596,16 @@ class GPTB_OT_check_canvas_alignement(bpy.types.Operator):
# self.report({ret}, message)
return {'FINISHED'}
class GPTB_OT_open_addon_prefs(bpy.types.Operator):
bl_idname = "gptb.open_addon_prefs"
bl_label = "Open Addon Prefs"
bl_description = "Open user preferences window in addon tab and prefill the search with addon name"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
utils.open_addon_prefs()
return {'FINISHED'}
classes = (
GPTB_OT_copy_text,
GPTB_OT_flipx_view,
@ -642,6 +618,7 @@ GPTB_OT_toggle_hide_gp_modifier,
GPTB_OT_list_disabled_anims,
GPTB_OT_clear_active_frame,
GPTB_OT_check_canvas_alignement,
GPTB_OT_open_addon_prefs,
)
def register():

View File

@ -114,6 +114,9 @@ def register_keymaps():
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)
addon_keymaps.append((km,kmi))
# km = addon.keymaps.new(name = "Dopesheet", space_type = "DOPESHEET_EDITOR") # try duplicating km (seem to be error at unregsiter)
kmi = km.keymap_items.new('gp.duplicate_send_to_layer', type='X', value="PRESS", ctrl=True, shift=True)
kmi.properties.delete_source = True
addon_keymaps.append((km,kmi))
@ -130,13 +133,17 @@ GPTB_OT_duplicate_send_to_layer,
)
def register():
if not bpy.app.background:
for cls in classes:
bpy.utils.register_class(cls)
register_keymaps()
if bpy.app.background:
return
for cls in classes:
bpy.utils.register_class(cls)
register_keymaps()
def unregister():
if not bpy.app.background:
unregister_keymaps()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if bpy.app.background:
return
unregister_keymaps()
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -57,7 +57,7 @@ class GPTB_OT_load_default_palette(bpy.types.Operator):
line.name = 'line'
# load json
pfp = Path(bpy.path.abspath(get_addon_prefs().palettes_path))
pfp = Path(bpy.path.abspath(get_addon_prefs().palette_path))
if not pfp.exists():
self.report({'ERROR'}, f'Palette path not found')
return {"CANCELLED"}
@ -164,9 +164,7 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper):
def load_blend_palette(context, filepath):
'''Load materials on current active object from current chosen blend'''
#from pathlib import Path
#palette_fp = C.preferences.addons['GP_toolbox'].preferences['palettes_path']
#fp = Path(palette_fp) / 'christina.blend'
print(f'-- import palette from : {filepath} --')
for ob in context.selected_objects:
if ob.type != 'GPENCIL':

View File

@ -23,6 +23,182 @@ from bpy.props import (
PointerProperty,
)
#--- OPERATORS
def print_materials_sources(ob):
for m in ob.data.materials:
if m.library:
print(f'{m.name} - {Path(m.library.filepath).name}')
else:
print(m.name)
def replace_mat_slots(src_mat, obj):
for ms in obj.material_slots:
if ms.material.name == src_mat.name:
# Only on different linked, else mat.name differ (.001))
ms.material = src_mat
class GPTB_OT_import_obj_palette(Operator):
bl_idname = "gp.import_obj_palette"
bl_label = "Import Object Palette"
bl_description = "Import object palette from blend"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
## get targets
selection = [o for o in context.selected_objects if o.type == 'GPENCIL']
if not selection:
self.report({'ERROR'}, 'Need to have at least one GP object selected in scene')
return {"CANCELLED"}
# Avoid looping on linked duplicate
objs = []
datas = []
for o in selection:
if o.data in datas:
continue
objs.append(o)
datas.append(o.data)
del datas # datas.clear()
pl_prop = context.scene.bl_palettes_props
blend_path = pl_prop.blends[pl_prop.bl_idx].blend_path
target_objs = [pl_prop.objects[pl_prop.ob_idx].name]
# Future improvement
# target_objs = [o.name for o in pl_prop.objects if o.select]
if not target_objs:
self.report({'ERROR'}, 'Need at least one palette source selected')
return {"CANCELLED"}
mode = pl_prop.import_type
if mode == 'LINK' and not bpy.data.is_saved: # autorise for absolute path
self.report({'ERROR'}, 'Blend file must be saved to use link mode')
return {"CANCELLED"}
if mode != 'LINK':
self.report({'ERROR'}, 'Not supported yet, use link')
return {'CANCELLED'}
# get relative path
blend_path = bpy.path.relpath(blend_path)
# TODO append object to list all material that belongs to it...
linked_objs = utils.link_objects_in_blend(blend_path, target_objs, link=True)
if not linked_objs:
self.report({'ERROR'}, f'Could not link/append obj from {blend_path}')
return {"CANCELLED"}
for i in range(len(linked_objs))[::-1]: # reversed(range(len(l))) / range(len(l))[::-1]
if linked_objs[i].type != 'GPENCIL':
print(f'{linked_objs[i].name} type is "{linked_objs[i].type}"')
bpy.data.objects.remove(linked_objs.pop(i))
if not linked_objs:
self.report({'ERROR'}, f'Linked object was not a Grease Pencil')
return {"CANCELLED"}
print('blend_path: ', blend_path)
# if materials have been renamed, there must be already be appended / linked
# to_clear = []
ct = 0
for src_ob in linked_objs:
ct += len(src_ob.data.materials)
if mode == 'LINK': # link new mats and update already linked ones
## link mats
for ob in objs:
for src_ob in linked_objs:
for src_mat in src_ob.data.materials:
print(f'- {src_mat.name}')
mat = ob.data.materials.get(src_mat.name)
if mat and mat.library == src_mat.library:
# print('already exists')
continue # same material, skip
elif mat:
# print('already but not same lib')
## same material but not from same lib
## remap_user will replace this mat in all objects blend...
mat.user_remap(src_mat)
## (we might want to keep links in other objects untouched ?)
## else use a basic material slot swap (loop, can be added on multiple slots)
# replace_mat_slots(ob, src_mat)
else:
# print('Not in dest')
## material not in dest, append
ob.data.materials.append(src_mat)
elif mode == 'APPEND':
## append, overwrite all already existing materials with new ones
pass
# ct = 0
# for o in selection:
# for mat in ob.data.materials:
# if mat in o.data.materials[:]:
# continue
# o.data.materials.append(mat)
# ct += 1
elif mode == 'APPEND_REUSE':
## append, Skip existing material
pass
if ct:
self.report({'INFO'}, f'{ct} Materials appended')
# else:
# self.report({'WARNING'}, 'All materials are already in other selected object')
# unlink objects and their gp data
for src_ob in linked_objs:
bpy.data.grease_pencils.remove(src_ob.data)
return {"FINISHED"}
class GPTB_OT_palette_fuzzy_search_obj(Operator):
bl_idname = "gptb.palette_fuzzy_search_obj"
bl_label = "Palette Fuzzy Match"
bl_description = "Try to find a palette with name closest to active object"
bl_options = {"REGISTER"}
def execute(self, context):
if not context.object:
self.report({'ERROR'}, 'No active object to search name from')
return {"CANCELLED"}
bl_props = context.scene.bl_palettes_props
final_ratio = 0
new_idx = None
for i, o in enumerate(bl_props.objects):
ratio = utils.fuzzy_match_ratio(context.object.name, o.name, case_sensitive=False)
if ratio > final_ratio:
new_idx = i
final_ratio = ratio
limit = 0.3
if final_ratio < limit:
self.report({'ERROR'}, f'Could not find a name matching at least {limit*100:.0f}% "{context.object.name}"')
return {"CANCELLED"}
if new_idx is None:
self.report({'ERROR'}, f'Could not find match')
return {"CANCELLED"}
bl_props.ob_idx = new_idx
self.report({'INFO'}, f'Select {bl_props.objects[bl_props.ob_idx].name} (match at {final_ratio*100:.1f}% with "{context.object.name}")')
return {"FINISHED"}
#--- UI LIST
class GPTB_UL_blend_list(UIList):
# order_by_distance : BoolProperty(default=True)
@ -68,6 +244,7 @@ class GPTB_UL_object_list(UIList):
# order_by_distance : BoolProperty(default=True)
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
self.use_filter_show = True # force open the search feature
layout.label(text=item.name)
def draw_filter(self, context, layout):
@ -75,6 +252,7 @@ class GPTB_UL_object_list(UIList):
subrow = row.row(align=True)
subrow.prop(self, "filter_name", text="") # Only show items matching this name (use * as wildcard)
# reverse order
subrow.operator('gptb.palette_fuzzy_search_obj', text='', icon='ZOOM_SELECTED') # built-in reverse
icon = 'SORT_DESC' if self.use_filter_sort_reverse else 'SORT_ASC'
subrow.prop(self, "use_filter_sort_reverse", text="", icon=icon) # built-in reverse
@ -110,19 +288,36 @@ def reload_blends(self, context):
reload_objects(self, context)
return
palettes_dir = Path(palette_fp)
palettes_dir = Path(os.path.abspath(bpy.path.abspath(palette_fp)))
if not palettes_dir.exists():
item = uilist.add()
item.blend_name = 'Palette Path not found'
reload_objects(self, context)
return
blends = [(o.path, Path(o).stem, "") for o in os.scandir(palettes_dir)
if o.is_file()
and o.name.endswith('.blend')
and re.search(r'v\d{3}', o.name, re.I)]
# list blends
pattern = r'[vV](\d{2,3})' # rightest = r'[vV](\d+)(?!.*[vV]\d)'
blends = [] # recursive
for root, _dirs, files in os.walk(palettes_dir):
for f in files:
fp = Path(root) / f
if not f.endswith('.blend'):
continue
if not re.search(pattern, f):
continue
if not fp.is_file():
continue
blends.append((str(fp), fp.stem, ""))
blends.sort(key=lambda x: x[1], reverse=False)
## only in palette folder.
# blends = [(o.path, Path(o).stem, "") for o in os.scandir(palettes_dir)
# if o.is_file()
# and o.name.endswith('.blend')
# and re.search(pattern, o.name)]
# blends.sort(key=lambda x: x[1], reverse=False) # sort alphabetically
blends.sort(key=lambda x: int(re.search(pattern, x[1]).group(1)), reverse=False) # sort by version
# print('blends found', len(blends))
for bl in blends: # populate list
@ -161,6 +356,8 @@ def reload_objects(self, context):
obj_uil.clear()
pal_prop['ob_idx'] = 0
file_libs = [l.filepath for l in bpy.data.libraries if l.filepath]
if not len(blend_uil) or (len(blend_uil) == 1 and not bool(blend_uil[0].blend_path)):
item = obj_uil.add()
item.name = 'No blend to list object'
@ -173,31 +370,41 @@ def reload_objects(self, context):
path_to_blend = Path(blend_uil[pal_prop.bl_idx].blend_path)
ob_list = utils.check_objects_in_blend(str(path_to_blend)) # get list of string of all object except camera
## get list of string of all object except camera
ob_list = utils.check_objects_in_blend(str(path_to_blend), avoid_camera=True)
ob_list.sort(reverse=False) # filter object by name ?
# remove camera
# print('blends found', len(blends))
ob_list.sort(reverse=False) # filter object by name
for ob_name in ob_list: # populate list
item = obj_uil.add()
item.name = ob_name
# print('path_to_blend: ', path_to_blend)
item.path = str(path_to_blend / 'Object' / ob_name)
pal_prop.ob_idx = len(obj_uil) - 1
## those temp libraries are not saved (auto-cleared)
## But best to keep library list tidy while file is opened
for lib in reversed(bpy.data.libraries):
if lib.filepath and not lib.users_id:
if lib.filepath not in file_libs:
bpy.data.libraries.remove(lib)
# return len(ob_list) # must return None if used in update
del ob_list
## PROPS
#--- PROPERTIES
class GPTB_PG_blend_prop(PropertyGroup):
blend_name : StringProperty() # stem of the path
blend_path : StringProperty() # ful path
# select: BoolProperty(update=update_select) # use and update to set the plane selection
blend_path : StringProperty() # full path
class GPTB_PG_object_prop(PropertyGroup):
name : StringProperty() # stem of the path
path : StringProperty() # Object / Material ?
## select feature to get multiple at once
# select : BoolProperty(default=False) # Object / Material ?
class GPTB_PG_palette_settings(PropertyGroup):
bl_idx : IntProperty(update=reload_objects) # update_on_index_change to reload object
@ -227,87 +434,8 @@ class GPTB_PG_palette_settings(PropertyGroup):
)
)
# fav_blend: StringProperty() ## mark a blend as prefered ? (need to be stored in prefereneces to restore in other blend...)
class GPTB_OT_import_obj_palette(Operator):
bl_idname = "gp.import_obj_palette"
bl_label = "Import Object Palette"
bl_description = "Import object palette from blend"
bl_options = {"REGISTER", "INTERNAL"}
def execute(self, context):
pl_prop = context.scene.bl_palettes_props
path = pl_prop.objects[pl_prop.ob_idx].path
self.report({'INFO'}, f'Path to object: {path}')
return {"FINISHED"}
## PANEL
class GPTB_PT_palettes_linker_ui(Panel):
bl_idname = 'GPTB_PT_palettes_linker_ui'
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"#Dev
bl_label = "Palettes Mat Linker"
def draw(self, context):
layout = self.layout
scn = bpy.context.scene
pl_prop = scn.bl_palettes_props
col= layout.column()
prefs = utils.get_addon_prefs()
## Here put the path thing (only to use a non-library)
# maybe in submenu...
row = col.row()
expand_icon = 'TRIA_DOWN' if pl_prop.show_path else 'TRIA_RIGHT'
row.prop(pl_prop, 'show_path', text='', icon=expand_icon, emboss=False)
row.prop(pl_prop, 'use_project_path', text='Use Project Palettes')
row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
if pl_prop.use_project_path:
## gp toolbox addon prefs path
if not prefs.palette_path:
col.label(text='Gp Toolbox Palette Directory Needed')
col.label(text='(saved with preferences)')
if pl_prop.show_path or not prefs.palette_path:
col.prop(prefs, 'palette_path', text='Project Dir')
else:
## local path
if not pl_prop.custom_dir:
col.label(text='Need to specify directory')
if pl_prop.show_path or not pl_prop.custom_dir:
col.prop(pl_prop, 'custom_dir', text='Custom Dir')
row = col.row()
row.template_list("GPTB_UL_blend_list", "", pl_prop, "blends", pl_prop, "bl_idx",
rows=2)
# side panel
# subcol = row.column(align=True)
# subcol.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
## Show object UI list only once blend Uilist is filled ?
if not len(pl_prop.blends) or (len(pl_prop.blends) == 1 and not bool(pl_prop.blends[0].blend_path)):
col.label(text='Select a blend to list available object')
row = col.row()
row.template_list("GPTB_UL_object_list", "", pl_prop, "objects", pl_prop, "ob_idx",
rows=4)
## Show link button in the border of the UI list ?
# col.prop(pl_prop, 'import_type')
split = col.split(align=True, factor=0.4 )
split.prop(pl_prop, 'import_type', text='')
split.enabled = len(pl_prop.objects) and bool(pl_prop.objects[pl_prop.ob_idx].path)
split.operator('gp.import_obj_palette', text='Palette')
# button to launch link with combined props (active only if the two items are valids)
# str(Path(self.blends) / 'Object' / self.objects
classes = (
# blend list
@ -316,6 +444,7 @@ GPTB_UL_blend_list,
GPTB_OT_palettes_reload_blends,
# object in blend list
GPTB_OT_palette_fuzzy_search_obj,
GPTB_PG_object_prop,
GPTB_UL_object_list,
@ -323,7 +452,7 @@ GPTB_UL_object_list,
GPTB_PG_palette_settings,
GPTB_OT_import_obj_palette,
GPTB_PT_palettes_linker_ui,
# TEST_OT_import_obj_palette_test,
)
def register():

View File

@ -2,13 +2,13 @@
from .utils import get_addon_prefs
import bpy
from pathlib import Path
from bpy.types import Panel
## UI in properties
### dataprop_panel not used --> transferred to sidebar
"""
class GPTB_PT_dataprop_panel(bpy.types.Panel):
class GPTB_PT_dataprop_panel(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
# bl_space_type = 'VIEW_3D'
@ -35,7 +35,7 @@ class GPTB_PT_dataprop_panel(bpy.types.Panel):
## UI in Gpencil sidebar menu
class GPTB_PT_sidebar_panel(bpy.types.Panel):
class GPTB_PT_sidebar_panel(Panel):
bl_label = "GP Toolbox"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -165,7 +165,7 @@ class GPTB_PT_sidebar_panel(bpy.types.Panel):
# row.operator("my_operator.multi_op", text='', icon='TRIA_LEFT').left = 1
# row.operator("my_operator.multi_op", text='', icon='TRIA_RIGHT').left = 0
class GPTB_PT_anim_manager(bpy.types.Panel):
class GPTB_PT_anim_manager(Panel):
bl_label = "Animation Manager"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -221,7 +221,7 @@ class GPTB_PT_anim_manager(bpy.types.Panel):
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)
class GPTB_PT_toolbox_playblast(bpy.types.Panel):
class GPTB_PT_toolbox_playblast(Panel):
bl_label = "Playblast"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -243,7 +243,7 @@ class GPTB_PT_toolbox_playblast(bpy.types.Panel):
# row.operator('render.playblast_anim', text = 'Playblast', icon = 'RENDER_ANIMATION').use_view = False # old (but robust) blocking playblast
row.operator('render.playblast_anim', text = 'Viewport').use_view = True
class GPTB_PT_tint_layers(bpy.types.Panel):
class GPTB_PT_tint_layers(Panel):
bl_label = "Tint Layers"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -268,7 +268,7 @@ class GPTB_PT_tint_layers(bpy.types.Panel):
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
class GPTB_PT_checker(bpy.types.Panel):
class GPTB_PT_checker(Panel):
bl_label = "Checker"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -298,7 +298,7 @@ class GPTB_PT_checker(bpy.types.Panel):
row.operator('gp.links_checker', text = 'Check links', icon = 'UNLINKED')
class GPTB_PT_color(bpy.types.Panel):
class GPTB_PT_color(Panel):
bl_label = "Color"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -310,11 +310,14 @@ class GPTB_PT_color(bpy.types.Panel):
layout = self.layout
col = layout.column()
## Create empty frame on layer
col.operator('gp.palette_linker', text=f'Link Materials Palette TO Object', icon='COLOR') ## ops
# Material panel as a pop-up (would work if palette dir is separated)
# col.operator("wm.call_panel", text="Link Materials Palette", icon='COLOR').name = "GPTB_PT_palettes_linker_ui"
col.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME')
# col.operator("wm.call_panel", text="Link Material Palette", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
""" # unused : added in Animation Manager
class GPTB_PT_extra(bpy.types.Panel):
class GPTB_PT_extra(Panel):
bl_label = "Extra"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -331,7 +334,7 @@ class GPTB_PT_extra(bpy.types.Panel):
"""
## unused -- (integrated in sidebar_panel)
class GPTB_PT_cam_ref_panel(bpy.types.Panel):
class GPTB_PT_cam_ref_panel(Panel):
bl_label = "Background imgs"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
@ -366,10 +369,13 @@ def palette_manager_menu(self, context):
prefs = get_addon_prefs()
layout.operator("gp.copy_active_to_selected_palette", text='Append Materials To Selected', icon='MATERIAL')
layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_MATERIAL')
layout.separator()
layout.operator("wm.call_panel", text="Pop Palette Linker", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
layout.operator("gp.load_blend_palette", text='Load Mats From Single Blend', icon='RESTRICT_COLOR_ON').filepath = prefs.palette_path
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.operator("gp.load_blend_palette", text='Load color Palette', icon='COLOR').filepath = prefs.palette_path
layout.operator("gp.clean_material_stack", text='Clean material Stack', icon='NODE_MATERIAL')
def expose_use_channel_color_pref(self, context):
@ -380,6 +386,150 @@ def expose_use_channel_color_pref(self, context):
layout.label(text='Use Channel Colors (User preferences):')
layout.prop(context.preferences.edit, 'use_anim_channel_group_colors')
#--- Palette Linker Panels
def palettes_path_ui(self, context):
layout = self.layout
scn = bpy.context.scene
pl_prop = scn.bl_palettes_props
col= layout.column()
prefs = get_addon_prefs()
## Here put the path thing (only to use a non-library)
# maybe in submenu...
row = col.row()
# expand_icon = 'TRIA_DOWN' if pl_prop.show_path else 'TRIA_RIGHT'
# row.prop(pl_prop, 'show_path', text='', icon=expand_icon, emboss=False)
row.prop(pl_prop, 'use_project_path', text='Use Project Palettes')
# row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
if pl_prop.use_project_path:
## gp toolbox addon prefs path
if not prefs.palette_path:
col.label(text='GP toolbox Palette Directory Needed', icon='INFO')
col.operator('gptb.open_addon_prefs', icon='PREFERENCES')
# if not prefs.palette_path: # or pl_prop.show_path
# col.prop(prefs, 'palette_path', text='Project Dir')
#col.label(text='(saved with preferences)')
else:
## local path
if not pl_prop.custom_dir:
col.label(text='Need to specify directory')
col.prop(pl_prop, 'custom_dir', text='Custom Dir')
# if not pl_prop.custom_dir or pl_prop.show_path:
# col.prop(pl_prop, 'custom_dir', text='Custom Dir')
def palettes_lists_ui(self, context, popup=False):
layout = self.layout
scn = bpy.context.scene
pl_prop = scn.bl_palettes_props
col= layout.column()
row=col.row()
# refresh button
txt = 'Project Palettes' if pl_prop.use_project_path else 'Custom Palettes'
row.label(text=txt)
row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
col= layout.column()
row = col.row()
if popup:
blends_minimum_row = 5
objects_minimum_row = 25
else:
blends_minimum_row = 2
objects_minimum_row = 4
row.template_list("GPTB_UL_blend_list", "", pl_prop, "blends", pl_prop, "bl_idx",
rows=blends_minimum_row)
# side panel
# subcol = row.column(align=True)
# subcol.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="")
## Show object UI list only once blend Uilist is filled ?
if not len(pl_prop.blends) or (len(pl_prop.blends) == 1 and not bool(pl_prop.blends[0].blend_path)):
col.label(text='Select blend refresh available objects')
row = col.row()
row.template_list("GPTB_UL_object_list", "", pl_prop, "objects", pl_prop, "ob_idx",
rows=objects_minimum_row)
## Show link button in the border of the UI list ?
# col.prop(pl_prop, 'import_type')
split = col.split(align=True, factor=0.4)
split.prop(pl_prop, 'import_type', text='')
split.enabled = len(pl_prop.objects) and bool(pl_prop.objects[pl_prop.ob_idx].path)
split.operator('gp.import_obj_palette', text='Palette')
# button to launch link with combined props (active only if the two items are valids)
# str(Path(self.blends) / 'Object' / self.objects
class GPTB_PT_palettes_linker_main_ui(Panel):
bl_space_type = 'TOPBAR' # dummy
bl_region_type = 'HEADER'
# bl_space_type = "VIEW_3D"
# bl_region_type = "UI"
# bl_category = "Gpencil"
bl_label = "Palettes Mat Linker"
def draw(self, context):
layout = self.layout
## link button for tests
# layout.operator('gp.import_obj_palette', text='Palette')
# Subpanel are appended to this main UI
## Or just as One fat panel
# palettes_path_ui(self, context)
# palettes_lists_ui(self, context)
class GPTB_PT_palettes_path_ui(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_label = "Palettes Source" # Source Path
# bl_parent_id = "GPTB_PT_palettes_linker_main_ui"
bl_parent_id = "GPTB_PT_color"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
palettes_path_ui(self, context)
# layout.label()
# pop-up version of object lists
class GPTB_PT_palettes_list_popup(Panel):
bl_space_type = 'TOPBAR' # dummy
bl_region_type = 'HEADER'
bl_category = "Gpencil"
bl_label = "Palettes Lists"
bl_ui_units_x = 18
def draw(self, context):
palettes_lists_ui(self, context, popup=True)
class GPTB_PT_palettes_list_ui(Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Gpencil"
bl_label = "Palettes Lists"
bl_parent_id = "GPTB_PT_color"
bl_options = {'DEFAULT_CLOSED'}
def draw_header(self, context):
layout = self.layout
# layout.label(text="My Select Panel")
layout.operator("wm.call_panel", text="", icon='COLOR').name = "GPTB_PT_palettes_list_popup"
def draw(self, context):
palettes_lists_ui(self, context, popup=False)
## bl 3+ UI
def asset_browser_ui(self, context):
'''Only shows in blender >= 3.0.0'''
@ -429,6 +579,12 @@ GPTB_PT_anim_manager,
GPTB_PT_color,
GPTB_PT_tint_layers,
GPTB_PT_toolbox_playblast,
# palettes linker
GPTB_PT_palettes_linker_main_ui, # main panel
GPTB_PT_palettes_list_popup, # popup (dummy region)
GPTB_PT_palettes_path_ui, # subpanels
GPTB_PT_palettes_list_ui, # subpanels
# GPTB_PT_extra,
)
@ -469,32 +625,59 @@ def GPdata_toolbox_panel(self, context):
col.operator("gp.auto_tint_gp_layers", text = "Reset tint", icon = "COLOR").reset = True
"""
"""
Put back UI for interpolate
### old
# Grease Pencil stroke interpolation tools native pop hover panel from 2.92
class VIEW3D_PT_tools_grease_pencil_interpolate(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_label = "Interpolate"
@classmethod
def poll(cls, context):
if context.gpencil_data is None:
return False
gpd = context.gpencil_data
valid_mode = bool(gpd.use_stroke_edit_mode or gpd.is_stroke_paint_mode)
return bool(context.editable_gpencil_strokes) and valid_mode
def draw(self, context):
layout = self.layout
settings = context.tool_settings.gpencil_interpolate
col = layout.column(align=True)
col.label(text="Interpolate Strokes")
col.operator("gpencil.interpolate", text="Interpolate")
col.operator("gpencil.interpolate_sequence", text="Sequence")
col.operator("gpencil.interpolate_reverse", text="Remove Breakdowns")
col = layout.column(align=True)
col.label(text="Options:")
col.prop(settings, "interpolate_all_layers")
gpd = context.gpencil_data
if gpd.use_stroke_edit_mode:
col.prop(settings, "interpolate_selected_only")
col = layout.column(align=True)
col.label(text="Sequence Options:")
col.prop(settings, "step")
col.prop(settings, "type")
if settings.type == 'CUSTOM':
# TODO: Options for loading/saving curve presets?
col.template_curve_mapping(settings, "interpolation_curve", brush=True,
use_negative_slope=True)
elif settings.type != 'LINEAR':
col.prop(settings, "easing")
if settings.type == 'BACK':
layout.prop(settings, "back")
elif settings.type == 'ELASTIC':
sub = layout.column(align=True)
sub.prop(settings, "amplitude")
sub.prop(settings, "period")
"""
col = layout.column(align = True)
col.operator("gpencil.stroke_change_color", text="Move to Color",icon = "COLOR")
col.operator("transform.shear", text="Shear")
col.operator("gpencil.stroke_cyclical_set", text="Toggle Cyclic").type = 'TOGGLE'
col.operator("gpencil.stroke_subdivide", text="Subdivide",icon = "OUTLINER_DATA_MESH")
row = layout.row(align = True)
row.operator("gpencil.stroke_join", text="Join").type = 'JOIN'
row.operator("grease_pencil.stroke_separate", text = "Separate")
col.operator("gpencil.stroke_flip", text="Flip Direction",icon = "ARROW_LEFTRIGHT")
col = layout.column(align = True)
col.operator("gptools.randomise",icon = 'RNDCURVE')
col.operator("gptools.thickness",icon = 'LINE_DATA')
col.operator("gptools.angle_split",icon = 'MOD_BEVEL',text='Angle Splitting')
col.operator("gptools.stroke_uniform_density",icon = 'MESH_DATA',text = 'Density')
row = layout.row(align = True)
row.prop(settings,"extra_tools",text='',icon = "DOWNARROW_HLT" if settings.extra_tools else "RIGHTARROW",emboss = False)
row.label("Extra tools")
if settings.extra_tools :
layout.operator_menu_enum("gpencil.stroke_arrange", text="Arrange Strokes...", property="direction")
"""

View File

@ -13,9 +13,9 @@
bl_info = {
"name": "GP toolbox",
"description": "Set of tools for Grease Pencil in animation production",
"description": "Tool set for Grease Pencil in animation production",
"author": "Samuel Bernou, Christophe Seux",
"version": (1, 7, 8),
"version": (1, 8, 0),
"blender": (2, 91, 0),
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
"warning": "",
@ -633,7 +633,7 @@ def register():
OP_playblast_bg.register()
OP_playblast.register()
OP_palettes.register()
# OP_palettes_linker.register()
OP_palettes_linker.register()
OP_brushes.register()
OP_cursor_snap_canvas.register()
OP_copy_paste.register()
@ -680,7 +680,7 @@ def unregister():
OP_copy_paste.unregister()
OP_cursor_snap_canvas.unregister()
OP_brushes.unregister()
# OP_palettes_linker.unregister()
OP_palettes_linker.unregister()
OP_palettes.unregister()
OP_file_checker.unregister()
OP_helpers.unregister()

View File

@ -252,7 +252,9 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax):
# Convert the 0-1 range into a value in the right range.
return rightMin + (valueScaled * rightSpan)
#### GP funcs
# -----------------
### GP funcs
# -----------------
def get_gp_draw_plane(context, obj=None):
''' return tuple with plane coordinate and normal
@ -546,15 +548,20 @@ def extrapolate_points_by_length(a,b, length):
### Vector utils 2d
# -----------------
def is_vector_close(a, b, rel_tol=1e-03):
'''compare Vector or sequence of value
by default tolerance is set on 1e-03 (0.001)
'''
return all([math.isclose(i, j, rel_tol=rel_tol) for i, j in zip(a,b)])
def single_vector_length_2d(v):
return sqrt((v[0] * v[0]) + (v[1] * v[1]))
def vector_length_2d(A,B):
''''take two Vector and return length'''
return sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2)
def vector_length_coeff_2d(size, A, B):
'''
Calculate the vector lenght
@ -569,7 +576,7 @@ def vector_length_coeff_2d(size, A, B):
def cross_vector_coord_2d(foo, bar, size):
'''Return the coord in space of a cross vector between the two point with specified size'''
###middle location between 2 vector is calculated by adding the two vector and divide by two
##middle location between 2 vector is calculated by adding the two vector and divide by two
##mid = (foo + bar) / 2
between = foo - bar
#create a generic Up vector (on Y or Z)
@ -646,6 +653,17 @@ def get_addon_prefs():
addon_prefs = preferences.addons[addon_name].preferences
return (addon_prefs)
def open_addon_prefs():
'''Open addon prefs windows with focus on current addon'''
from .__init__ import bl_info
wm = bpy.context.window_manager
wm.addon_filter = 'All'
if not 'COMMUNITY' in wm.addon_support: # reactivate community
wm.addon_support = set([i for i in wm.addon_support] + ['COMMUNITY'])
wm.addon_search = bl_info['name']
bpy.context.preferences.active_section = 'ADDONS'
bpy.ops.preferences.addon_expand(module=__package__)
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
def open_file(file_path) :
'''Open filepath with default browser'''
@ -703,10 +721,24 @@ def detect_OS():
print("Cannot detect OS, python 'sys.platform' give :", myOS)
return None
def is_vector_close(a, b, rel_tol=1e-03):
'''compare Vector or sequence of value
by default tolerance is set on 1e-03 (0.001)'''
return all([math.isclose(i, j, rel_tol=rel_tol) for i, j in zip(a,b)])
def fuzzy_match(s1, s2, tol=0.8, case_sensitive=False):
'''Tell if two strings are similar using a similarity ratio (0 to 1) value passed as third arg'''
from difflib import SequenceMatcher
# can also use difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)
if case_sensitive:
similarity = SequenceMatcher(None, s1, s2)
else:
similarity = SequenceMatcher(None, s1.lower(), s2.lower())
return similarity.ratio() > tol
def fuzzy_match_ratio(s1, s2, case_sensitive=False):
'''Tell how much two passed strings are similar 1.0 being exactly similar'''
from difflib import SequenceMatcher
if case_sensitive:
similarity = SequenceMatcher(None, s1, s2)
else:
similarity = SequenceMatcher(None, s1.lower(), s2.lower())
return similarity.ratio()
def convert_attr(Attr):
'''Convert given value to a Json serializable format'''
@ -749,8 +781,9 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
_message = [_message]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
# -----------------
### UI utils
# -----------------
## kmi draw for addon without delete button
def draw_kmi(km, kmi, layout):
@ -857,17 +890,36 @@ def draw_kmi(km, kmi, layout):
# draw_km(display_keymaps, kc, kmm, None, layout + 1)
# layout.context_pointer_set("keymap", km)
# -----------------
### linking utility
# -----------------
"""
def link_objects_in_blend(filepath, obj_name, link=True):
'''Link an object by name from a file, if link is False, append instead of linking'''
with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
data_to.objects = [o for o in data_from.objects if o == obj_name] # c.startswith(obj_name)
return data_to.objects
"""
def link_objects_in_blend(filepath, obj_name_list, link=True):
'''Link an object by name from a file, if link is False, append instead of linking'''
if isinstance(obj_name_list, str):
obj_name_list = [obj_name_list]
with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to):
data_to.objects = [o for o in data_from.objects if o in obj_name_list]
return data_to.objects
def check_materials_in_blend(filepath):
'''Return a list of all material in remote blend file'''
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
l = [m for m in data_from.materials]
return l
def check_objects_in_blend(filepath, avoid_camera=True):
'''return a list of object name in file'''
'''Return a list of object name in remote blend file'''
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
if avoid_camera:
l = [o for o in data_from.objects if not 'camera' in o.lower()]
l = [o for o in data_from.objects if not any(x in o.lower() for x in ('camera', 'draw_cam', 'obj_cam'))]
else:
l = [o for o in data_from.objects]
return l