2021-11-25 17:00:09 +01:00
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 ,
)
2021-12-04 13:57:32 +01:00
#--- 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 ' }
2022-01-18 22:53:08 +01:00
if not Path ( blend_path ) . exists ( ) :
utils . show_message_box ( [ [ ' gp.palettes_reload_blends ' , ' Invalid blend path! Click here to refresh source blends ' , ' FILE_REFRESH ' ] ] , ' Invalid Palette ' , ' ERROR ' )
return { ' CANCELLED ' }
2021-12-04 13:57:32 +01:00
# 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 " }
2022-01-18 22:53:08 +01:00
## Unused for now, all libs are linked to one library data. need to replace material links one by one.
class GPTB_OT_palette_version_update ( Operator ) :
bl_idname = " gptb.palette_version_update "
bl_label = " Update Palette Version "
bl_description = " Update linked material to selected palette version if curent link has same basename "
bl_options = { " REGISTER " }
mat_scope : EnumProperty (
name = ' Targeted Materials ' ,
items = ( ( ' ALL ' , " All Materials " , " Update all linked material in file to next version " ) ,
( ' SELECTED ' , " Selected Objects " , " Update all linked material on selected gp objects " ) ,
) ,
default = ' ALL ' ,
description = ' Choose material targeted for library update '
)
mat_type : EnumProperty (
name = ' Materials Type ' ,
items = ( ( ' ALL ' , " All Materials " , " Update both gp and obj materials " ) ,
( ' GP ' , " Gpencil Materials " , " update only grease pencil materials " ) ,
( ' OBJ ' , " Non-Gpencil Materials " , " update only non-gpencil objects materials " ) ,
) ,
default = ' GP ' ,
description = ' Filter material type for library update '
)
def invoke ( self , context , event ) :
self . bl_props = context . scene . bl_palettes_props
if not self . bl_props . blends or not self . bl_props . blends [ 0 ] . blend_path :
self . report ( { ' ERROR ' } , ' No blend selected ' )
return { " CANCELLED " }
return context . window_manager . invoke_props_dialog ( self , width = 450 )
def draw ( self , context ) :
layout = self . layout
layout . label ( text = f ' Update links path to palette: { self . bl_props . blends [ self . bl_props . bl_idx ] . blend_name } ' , icon = ' LINK_BLEND ' )
self . bl_props
layout . prop ( self , ' mat_scope ' )
layout . prop ( self , ' mat_type ' )
col = layout . column ( align = True )
col . label ( text = ' Does not check if material exists in target blend ' , icon = ' INFO ' )
col . label ( text = ' Just change source filepath if different version of same source name is found ' )
# col.label(text='version of same source name is found')
def execute ( self , context ) :
if self . mat_scope == ' SELECTED ' and not context . selected_objects :
self . report ( { ' ERROR ' } , ' No selected objects ' )
return { " CANCELLED " }
bl_props = context . scene . bl_palettes_props
bl = bl_props . blends [ bl_props . bl_idx ]
bl_name , bl_path = bl . blend_name , bl . blend_path
if not Path ( bl_path ) . exists ( ) :
self . report ( { ' ERROR ' } , f ' Current selected blend source seem unreachable, try to refresh \n invalid path: { bl_path } ' )
return { " CANCELLED " }
reversion = re . compile ( r ' \ d { 2,4}$ ' ) # version padding from 2 to 4
bl_relpath = bpy . path . relpath ( bl_path )
if self . mat_scope == ' SELECTED ' :
pool = [ ]
for o in context . selected_objects :
for m in o . data . materials :
pool . append ( m )
elif self . mat_scope == ' ALL ' :
pool = [ m for m in bpy . data . materials ]
ct = 0
for m in pool :
if not m . library :
continue
if self . mat_type == ' GP ' and not m . is_grease_pencil :
continue
if self . mat_type == ' OBJ ' and m . is_grease_pencil :
continue
cur_fp = m . library . filepath
if not cur_fp :
print ( f ' ! { m . name } has an empty library filepath ! ' )
continue
p_cur_fp = Path ( cur_fp )
if p_cur_fp . stem == bl_name :
continue # already good
if reversion . sub ( ' ' , p_cur_fp . stem ) != reversion . sub ( ' ' , bl_name ) :
continue # not same stem base
# Same stem without version, can update to this one
print ( f ' { m . name } : { p_cur_fp } >> { bl_relpath } ' )
ct + = 1
m . library . filepath = bl_relpath
if ct :
self . report ( { ' INFO ' } , f ' { ct } material link path updated ' )
else :
self . report ( { ' WARNING ' } , ' No material path updated ' )
return { " FINISHED " }
2021-12-04 13:57:32 +01:00
#--- UI LIST
2021-11-25 17:00:09 +01:00
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 ) :
2021-12-04 13:57:32 +01:00
self . use_filter_show = True # force open the search feature
2021-11-25 17:00:09 +01:00
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
2021-12-04 13:57:32 +01:00
subrow . operator ( ' gptb.palette_fuzzy_search_obj ' , text = ' ' , icon = ' ZOOM_SELECTED ' ) # built-in reverse
2021-11-25 17:00:09 +01:00
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
2021-12-04 13:57:32 +01:00
palettes_dir = Path ( os . path . abspath ( bpy . path . abspath ( palette_fp ) ) )
2021-11-25 17:00:09 +01:00
if not palettes_dir . exists ( ) :
item = uilist . add ( )
item . blend_name = ' Palette Path not found '
reload_objects ( self , context )
return
2021-12-04 13:57:32 +01:00
# 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)]
2021-11-25 17:00:09 +01:00
2021-12-04 13:57:32 +01:00
# 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
2021-11-25 17:00:09 +01:00
# 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
2021-12-04 13:57:32 +01:00
file_libs = [ l . filepath for l in bpy . data . libraries if l . filepath ]
2021-11-25 17:00:09 +01:00
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 )
2021-12-04 13:57:32 +01:00
## get list of string of all object except camera
ob_list = utils . check_objects_in_blend ( str ( path_to_blend ) , avoid_camera = True )
2021-11-25 17:00:09 +01:00
2021-12-04 13:57:32 +01:00
ob_list . sort ( reverse = False ) # filter object by name
2021-11-25 17:00:09 +01:00
for ob_name in ob_list : # populate list
item = obj_uil . add ( )
item . name = ob_name
2021-12-04 13:57:32 +01:00
# print('path_to_blend: ', path_to_blend)
2021-11-25 17:00:09 +01:00
item . path = str ( path_to_blend / ' Object ' / ob_name )
pal_prop . ob_idx = len ( obj_uil ) - 1
2021-12-04 13:57:32 +01:00
## 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 )
2021-11-25 17:00:09 +01:00
# return len(ob_list) # must return None if used in update
2021-12-04 13:57:32 +01:00
del ob_list
2021-11-25 17:00:09 +01:00
2021-12-04 13:57:32 +01:00
#--- PROPERTIES
2021-11-25 17:00:09 +01:00
class GPTB_PG_blend_prop ( PropertyGroup ) :
blend_name : StringProperty ( ) # stem of the path
2021-12-04 13:57:32 +01:00
blend_path : StringProperty ( ) # full path
2021-11-25 17:00:09 +01:00
class GPTB_PG_object_prop ( PropertyGroup ) :
name : StringProperty ( ) # stem of the path
path : StringProperty ( ) # Object / Material ?
2021-12-04 13:57:32 +01:00
## select feature to get multiple at once
# select : BoolProperty(default=False) # Object / Material ?
2021-11-25 17:00:09 +01:00
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 \n keep 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
2021-12-04 13:57:32 +01:00
GPTB_OT_palette_fuzzy_search_obj ,
2021-11-25 17:00:09 +01:00
GPTB_PG_object_prop ,
GPTB_UL_object_list ,
# prop containing two above
GPTB_PG_palette_settings ,
GPTB_OT_import_obj_palette ,
2022-01-18 22:53:08 +01:00
# GPTB_OT_palette_version_update,
2021-12-04 13:57:32 +01:00
# TEST_OT_import_obj_palette_test,
2021-11-25 17:00:09 +01:00
)
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 )
2021-12-04 13:57:32 +01:00
del bpy . types . Scene . bl_palettes_props