gp_toolbox/OP_palettes_linker.py

468 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import bpy
import re
import json
import os
from bpy_extras.io_utils import ImportHelper, ExportHelper
from pathlib import Path
from . import utils
# from . import blendfile
from bpy.types import (
Panel,
Operator,
PropertyGroup,
UIList,
)
from bpy.props import (
IntProperty,
BoolProperty,
StringProperty,
FloatProperty,
EnumProperty,
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)
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
layout.label(text=item.blend_name)
def draw_filter(self, context, layout):
row = layout.row()
subrow = row.row(align=True)
subrow.prop(self, "filter_name", text="") # Only show items matching this name (use * as wildcard)
# reverse order
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
def filter_items(self, context, data, propname):
# example : https://docs.blender.org/api/blender_python_api_current/bpy.types.UIList.html
# This function gets the collection property (as the usual tuple (data, propname)), and must return two lists:
# * The first one is for filtering, it must contain 32bit integers were self.bitflag_filter_item marks the
# matching item as filtered (i.e. to be shown), and 31 other bits are free for custom needs. Here we use the
# * The second one is for reordering, it must return a list containing the new indices of the items (which
# gives us a mapping org_idx -> new_idx).
# Please note that the default UI_UL_list defines helper functions for common tasks (see its doc for more info).
# If you do not make filtering and/or ordering, return empty list(s) (this will be more efficient than
# returning full lists doing nothing!).
collec = getattr(data, propname)
helper_funcs = bpy.types.UI_UL_list
# Default return values.
flt_flags = []
flt_neworder = []
# Filtering by name #not working damn !
if self.filter_name:
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, collec, "name",
reverse=self.use_filter_sort_reverse)#self.use_filter_name_reverse)
return flt_flags, flt_neworder
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):
row = layout.row()
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
def filter_items(self, context, data, propname):
collec = getattr(data, propname)
helper_funcs = bpy.types.UI_UL_list
# Default return values.
flt_flags = []
flt_neworder = []
if self.filter_name:
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, collec, "name",
reverse=self.use_filter_sort_reverse)
return flt_flags, flt_neworder
def reload_blends(self, context):
scn = context.scene
pl_prop = scn.bl_palettes_props
uilist = scn.bl_palettes_props.blends
uilist.clear()
pl_prop['bl_idx'] = 0
prefs = utils.get_addon_prefs()
if pl_prop.use_project_path:
palette_fp = prefs.palette_path
else:
palette_fp = pl_prop.custom_dir
if not palette_fp: # singular
item = uilist.add()
item.blend_name = 'No Palette Path Specified'
reload_objects(self, context)
return
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
# 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, ""))
## 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
item = uilist.add()
scn.bl_palettes_props['bl_idx'] = len(uilist) - 1 # don't trigger updates
item.blend_path = bl[0]
item.blend_name = bl[1]
scn.bl_palettes_props.bl_idx = len(uilist) - 1 # trigger update ()
# reload_objects(self, context) # triggered by above assignation
# return len(blends) # return value must be None
class GPTB_OT_palettes_reload_blends(Operator):
bl_idname = "gp.palettes_reload_blends"
bl_label = "Reload Palette Blends"
bl_description = "Reload the blends in UI list of palettes linker"
bl_options = {"REGISTER"} # , "INTERNAL"
def execute(self, context):
reload_blends(self, context)
# ret = reload_blends(self, context)
# if ret is None:
# self.report({'ERROR'}, 'No blend scanned, check palette path')
# else:
# self.report({'INFO'}, f'{ret} blends found')
return {"FINISHED"}
def reload_objects(self, context):
scn = context.scene
prefs = utils.get_addon_prefs()
pal_prop = scn.bl_palettes_props
blend_uil = pal_prop.blends
obj_uil = pal_prop.objects
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'
return
if not blend_uil[pal_prop.bl_idx].blend_path:
item = obj_uil.add()
item.name = 'Selected blend has no path'
return
path_to_blend = Path(blend_uil[pal_prop.bl_idx].blend_path)
## 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
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
#--- PROPERTIES
class GPTB_PG_blend_prop(PropertyGroup):
blend_name : StringProperty() # stem of the path
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
blends : bpy.props.CollectionProperty(type=GPTB_PG_blend_prop)
ob_idx : IntProperty()
objects : bpy.props.CollectionProperty(type=GPTB_PG_object_prop)
use_project_path : BoolProperty(name='Use Project Palettes',
default=True, description='Use palettes directory specified in gp toolbox addon preferences',
update=reload_blends)
show_path : BoolProperty(name='Show path',
default=True, description='Show Palette directoty filepath')
custom_dir : StringProperty(name='Custom Palettes Directory', subtype='DIR_PATH',
description='Use choosen directory to load blend palettes',
update=reload_blends)
import_type : EnumProperty(
name="Import Type", description="Choose inmport type: link, append, append reuse (keep existing materials)",
default='LINK', options={'ANIMATABLE'}, update=None, get=None, set=None,
items=(
('LINK', 'Link', 'Link materials to selected object', 0),
('APPEND', 'Append', 'Append materials to selected objects', 1),
('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...)
classes = (
# blend list
GPTB_PG_blend_prop,
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,
# prop containing two above
GPTB_PG_palette_settings,
GPTB_OT_import_obj_palette,
# TEST_OT_import_obj_palette_test,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.bl_palettes_props = bpy.props.PointerProperty(type=GPTB_PG_palette_settings)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.bl_palettes_props