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
This commit is contained in:
+224
-95
@@ -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
|
||||
|
||||
@@ -109,20 +287,37 @@ def reload_blends(self, context):
|
||||
item.blend_name = 'No Palette Path Specified'
|
||||
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
|
||||
@@ -226,88 +433,9 @@ class GPTB_PG_palette_settings(PropertyGroup):
|
||||
('APPEND_REUSE', 'Append (Reuse)', 'Append materials to selected objects\nkeep those already there', 2),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
# 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():
|
||||
@@ -335,4 +464,4 @@ def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
del bpy.types.Scene.bl_palettes_props
|
||||
del bpy.types.Scene.bl_palettes_props
|
||||
|
||||
Reference in New Issue
Block a user