custom_shelf initial commit

master
Cos_Min 2022-03-04 12:28:53 +01:00
parent 705b6f2017
commit 232e5cc7f6
45 changed files with 4221 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
shelves
*.pyc
__pycache__

279
Types.py Normal file
View File

@ -0,0 +1,279 @@
import bpy
from bpy.types import Operator, Panel, PropertyGroup
from .utils import *
from bpy.props import *
from .properties import CustomShelfProps
class Types():
def initialyse(self,label) :
package = __package__.replace('_','').replace('-','').lower()
idname = label.replace(' ','_').replace('-','_').lower()
self.bl_label = title(label)
self.bl_idname = '%s.%s'%(package,idname)
#self.bl_props = bpy.context.scene.CustomShelf
#self.prefs = ""
class CSHELF_OT_shelf_popup(Types,Operator) :
#props = None
@property
def props(self) :
#print(getattr(bpy.context.window_manager.CustomShelf,self.prop))
return getattr(bpy.context.window_manager.CustomShelf,self.prop)
def get_props(self):
properties = self.props.bl_rna.properties
#props =
return {k:v for k,v in self.args.items() if k in properties}
def set_props(self,prop,value):
setattr(bpy.context.window_manager.CustomShelf,prop,value)
def initialyse(self,label,info) :
super().initialyse(CSHELF_OT_shelf_popup,label)
self.args = dic_to_args(info)
Props = type("props",(PropertyGroup,),{'__annotations__' : dict(self.args)})
bpy.utils.register_class(Props)
setattr(CustomShelfProps,arg_name(label),PointerProperty(type=Props))
#self.props = getattr(bpy.context.window_manager,self.bl_idname)
self.info = info
self.prop = arg_name(label)
#print(self.args)
#properties = self.get_props().bl_rna.properties
#props = {k:v for k,v in self.args.items() if k in properties or hasattr(v,"bl_idname")}
return #props
def invoke(self, context, event):
if event.alt :
for t in [t for t in bpy.data.texts if t.filepath == self.script] :
bpy.data.texts.remove(t)
text = bpy.data.texts.load(self.script)
areas = []
text_editor = None
for area in context.screen.areas :
if area.type == 'TEXT_EDITOR' :
text_editor = area
else :
areas.append(area)
if not text_editor :
bpy.ops.screen.area_split(direction = "VERTICAL")
for area in context.screen.areas :
if area not in areas :
text_editor = area
text_editor.type = "TEXT_EDITOR"
text_editor.spaces[0].show_syntax_highlight = True
text_editor.spaces[0].show_word_wrap = True
text_editor.spaces[0].show_line_numbers = True
context_copy = context.copy()
context_copy['area'] = text_editor
#bpy.ops.text.properties(context_copy)
#bpy.ops.view3d.toolshelf(context_copy)
text_editor.spaces[0].text = text
return {"FINISHED"}
else :
if self.args :
#set default value for collection
for k,v in self.info.items() :
if isinstance(v,dict) and v.get('collection') :
collection = getattr(bpy.data, id_type[v["collection"]])
setattr(self.props, k,collection.get(v["value"]))
return context.window_manager.invoke_props_dialog(self)
else :
return self.execute(context)
def bl_report(self,*args) :
if len(args) and args[0] in ('INFO','ERROR', 'WARNING') :
return self.report({args[0]},' '.join(args[1:]))
else :
print(*args)
def execute(self,context):
import tempfile
info,lines = read_info(self.script)
values = dict(info)
for arg in self.args :
value = getattr(self.props,arg)
if hasattr(value,"name") :
values[arg] = {'value': value.name}
elif isinstance(info.get(arg),dict) :
values[arg] = {'value': value}
else :
values[arg] = value
info_str = "info = %s\n\n"%values
tmp_folder = tempfile.gettempdir()
script_path = join(tmp_folder, "shelf.py")
with open(script_path,"w",encoding = 'utf-8') as f :
f.write(info_str+'\n'.join(lines))
exec(compile(open(script_path,encoding = 'utf-8').read(), script_path, 'exec'),{'info' : values,'print':self.bl_report})
return {'FINISHED'}
def draw(self,context) :
layout = self.layout
bl_props = context.scene.CustomShelf
#print(self.get_props())
for k in self.args :
#filter
row = layout.row(align = True)
row.label(text=title(k))
row.prop(self.props,k,text='')
#row.prop_search(self.props,k,self.props,k+"_prop_search",text='')
#op = self.args[k+"_prop_search"]
#row.operator(op.bl_idname,text="",icon ="COLLAPSEMENU" )
def check(self,context) :
return True
"""
class ShelfPropSearch(Types,Operator):
bl_property = "enum"
enum = None
prop = ""
def execute(self,context) :
parent_props = getattr(bpy.context.window_manager.CustomShelf,self.parent.prop)
setattr(parent_props,self.prop,self.enum)
return {"FINISHED"}
def initialyse(self,label,enum_list) :
super().initialyse(ShelfPropSearch,"prop_search_"+label)
self.enum = EnumProperty(items = [(i,i,"") for i in enum_list])
self.prop = label
#print(self.enum)
def invoke(self, context, event):
#print("invoke",self.parent)
context.window_manager.invoke_search_popup(self)
return {"FINISHED"}
"""
class CSHELF_PT_shelf_panel(Types,Panel):
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
#bl_category = "SHELF"
settings = {}
scripts = []
@staticmethod
def search_filter(search,str) :
return search.lower() in str.lower()
@classmethod
def poll(self, context):
bl_props = getattr(context.scene.CustomShelf,self.cat)
prefs = bpy.context.preferences.addons[__package__].preferences
search = bl_props.search
shelf = [self.search_filter(search,s["text"]) for s in self.scripts]
#print("settings",self.settings)
is_taged = True
if self.settings and self.settings.get('tags') :
tag = prefs.tag_filter
if not tag :
tag = os.getenv("CUSTOM_SHELF_TAG")
is_taged = tag in self.settings['tags']
return any(shelf) and is_taged
def draw(self,context) :
layout = self.layout
bl_props = getattr(context.scene.CustomShelf,self.cat)
search = bl_props.search
for script in self.scripts :
args = script.copy()
if not self.search_filter(search,script["text"]) : continue
if bl_props.filter and script["text"].startswith('_') : continue
args["text"] = title(script["text"])
#print(script)
layout.operator(**args)
class CSHELF_PT_shelf_header(Panel):
bl_label = "Custom Shelf"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_options = {"HIDE_HEADER"}
def draw(self, context):
layout = self.layout
bl_props = bpy.context.scene.CustomShelf
prefs = bpy.context.preferences.addons[__package__].preferences
cat_props = getattr(bl_props, self.cat)
row = layout.row(align = True)
row.operator_menu_enum("customshelf.set_tag_filter",'tag_filter',icon= "DOWNARROW_HLT",text='')
row.operator("customshelf.refresh", text='',emboss=True,icon= "FILE_REFRESH")
#row.separator()
row.prop(cat_props, "filter",text='',emboss=True,icon= "FILTER")
#row.separator()
row.prop(cat_props,"search",icon = "VIEWZOOM",text='')
#row.separator()
row.operator("customshelf.open_shelf_folder", text='',emboss=True,icon= "FILE").path = self.path

83
__init__.py Normal file
View File

@ -0,0 +1,83 @@
bl_info = {
"name": "Custom Shelf",
"author": "Christophe Seux",
"description": "Adds buttons to launch custom python scripts in the User panel.",
"version": (0, 3, 2),
"blender": (2, 82, 0),
"location": "View3D > User Panel",
"doc_url": "https://gitlab.com/autour-de-minuit/blender/custom_shelf/README.md"
"tracker_url": "https://gitlab.com/autour-de-minuit/blender/custom_shelf/-/issues",
"support": "COMMUNITY",
"category": "User"}
if "bpy" in locals():
import importlib
importlib.reload(operators)
importlib.reload(panels)
importlib.reload(functions)
importlib.reload(properties)
from .utils import report
from .functions import read_shelves
from .import operators
#from .import shelves
from .import properties
from .import panels
from .properties import CustomShelfSettings,CustomShelfProps
import bpy
import os
class_to_register = [
properties.AdditionnalShelves,
properties.CustomShelfProps,
properties.CustomShelfSettings,
properties.CustomShelfPrefs,
operators.CSHELF_OT_refresh,
operators.CSHELF_OT_add_shelf_folder,
operators.CSHELF_OT_remove_shelf_folder,
operators.CSHELF_OT_open_shelf_folder,
operators.CSHELF_OT_add_script,
operators.CSHELF_OT_set_tag_filter,
panels.CSHELF_PT_text_editor
]
def register():
for bl_classes in class_to_register :
bpy.utils.register_class(bl_classes)
bpy.types.Scene.CustomShelf = bpy.props.PointerProperty(type=CustomShelfSettings)
bpy.types.WindowManager.CustomShelf = bpy.props.PointerProperty(type=CustomShelfProps)
env_shelves = os.getenv("CUSTOM_SHELVES")
if env_shelves:
shelves = env_shelves.split(os.pathsep)
prefs = bpy.context.preferences.addons[__name__].preferences
# prefs.global_shelves = ''
# prefs.additionnal_shelves.clear()
for path in shelves:
s = next((s for s in prefs.additionnal_shelves if s.path == path), None)
if not s:
s = prefs.additionnal_shelves.add()
s.path = path
read_shelves()
def unregister():
# unregister panel :
for p in CustomShelfSettings.panel_list :
try :
bpy.utils.unregister_class(p)
except :
pass
del bpy.types.Scene.CustomShelf
del bpy.types.WindowManager.CustomShelf
for bl_classes in class_to_register :
bpy.utils.unregister_class(bl_classes)

View File

@ -0,0 +1,59 @@
info = {
'icon' : 'LIGHT_SPOT',
'description' : 'Save as lighting file from currently opened anim file',
}
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
import datetime
C = bpy.context
D = bpy.data
scene = C.scene
def create_lighting_file():
'''Create lighting_v001 file from anim'''
# if D.is_dirty:
# print('Current file is not saved')
# return
if 'lighting' in D.filepath:
print('File already detected as lighting')
return
if not 'animation' in D.filepath:
print('Not in animation folder')
return
name = re.sub(r'anim_v\d{3}', 'lighting_v001', basename(D.filepath))
print('name: ', name)
fp = dirname(dirname(D.filepath))
print('shot path: ', fp)
fp = join(fp, 'lighting', name)
print('light filepath: ', fp)
if exists(fp):
print('lighting_v001 already exists in folder')
return
info = D.texts.get('info')
if not info:
info = D.texts.new('info')
info.write(f'\nCreated from {basename(D.filepath)} at {str( datetime.datetime.now() )}')
print('Saving current file as lighting')
bpy.ops.wm.save_as_mainfile(filepath=fp)
print('Done')
create_lighting_file()

View File

@ -0,0 +1,43 @@
info = {
'icon' : 'CHECKMARK',
'description' : 'Check objects modifiers',
}
import bpy
import os, re, shutil
from os.path import dirname, basename, join, abspath, splitext, exists, isfile, isdir
from os import listdir
import datetime
C = bpy.context
D = bpy.data
scn = scene = C.scene
## in scene syubloop ob.instance_collection.all_objects:
print(f'\n== Modifiers check {basename(D.filepath)} ==')
for ob in bpy.data.objects:
if ob.type != 'ARMATURE' and ob.hide_viewport != ob.hide_render:
print(f'/!\ {ob.name} view != render : hide_viewport {ob.hide_viewport}, hide_render {ob.hide_render}')
if ob.type in ('MESH', 'CURVE', 'TEXT') and len(ob.modifiers) > 0:
for mod in ob.modifiers:
#mod.show_viewport = True
#mod.show_render = True
if mod.type == 'SUBSURF':
# print (ob.name)
if mod.render_levels < mod.levels:
print(f'[M] {ob.name} subsurf render levels inferior to viewport : view {mod.levels} > render {mod.render_levels}')
elif mod.render_levels == 0:
print(f'[M] {ob.name} subsurf useless : render levels set to 0 : view {mod.levels} > render {mod.render_levels}')
'''
if mod.type == 'ARMATURE':
if mod.object:
print (mod.object.name)#print armatrue target
#ob.modifiers.remove(mod) #remove modifier
if mod.type == 'SOLIDIFY':
mod.thickness
mod.offset
'''
print('== check done.')

View File

@ -0,0 +1,68 @@
info = {
'icon' : 'DECORATE_LIBRARY_OVERRIDE',
'description' : 'Check available library update',
'update' : False,
'fullreport': False,
}
import bpy
import os
from os import listdir, scandir
from os.path import abspath, relpath, join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = C.scene
auto_update = info['update']#False
fullreport = info['fullreport']
## 1: all execpt num, 2: num, 3:ext
rseq = re.compile('^(.*?)(\d+)(\D*)$')
print('\n--library check--')
for ob in bpy.context.scene.objects:
if not ob.type == 'EMPTY': continue
if not ob.instance_collection: continue
if not ob.instance_collection.library: continue
lib = ob.instance_collection.library
if not lib: continue
fp = lib.filepath
if not fp: continue
rel_dir = dirname(fp)
abs_lib = abspath(bpy.path.abspath(fp))
if not exists(abs_lib):
print(f'Lib not found: obj: {ob.name} > instance: {ob.instance_collection.name}\n-> {abs_lib}')
continue
lib_dir = dirname(abs_lib)
lib_name = basename(abs_lib)
#same length ? #last modified ? #last by name without #must be 90% identical ? # parts without rightmost number must be identical (good enough)
regname = rseq.search(lib_name)
if not regname:
print(f'X - {lib_name} : could not identify version using regex: {rseq.pattern}')
continue
name_base = regname.group(1)
# filelist = [f for f in scandir(lib_dir) if f.is_file() and len(f.name) == len(lib_name)]
# more strict with filename
filelist = [f for f in scandir(lib_dir) if f.is_file() and len(f.name) == len(lib_name) and rseq.search(f.name) and rseq.search(f.name).group(1) == name_base]
filelist.sort(key=lambda x : x.name)#sort in place alphabetically
last = filelist[-1]
if last.name != lib.name:
print(f'/!\ Lib update found : obj: {ob.name} > instance: {ob.instance_collection.name}\n {lib_name} >> {last.name}\n')
if auto_update:
### use relocate or do it automagically...
nfp = join(dirname(fp), last.name)
lib.filepath = nfp
continue
if fullreport: print(f'Lib OK : obj: {ob.name} > instance: {ob.instance_collection.name} > file: {lib_name}')
## make breakdown popup for addon version

View File

@ -0,0 +1,121 @@
info = {
'icon' : 'TOOL_SETTINGS',
'description' : 'Set render path for PNG - RGB out',
'stamp' : False
}
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
from time import time
C = bpy.context
D = bpy.data
scn = scene = C.scene
rd = scn.render
ev = scn.eevee
def render_anim(GL=False):
#openGl render
if GL:
bpy.ops.render.opengl(animation=True, view_context=False)#view_context False > look throughcam
else:
bpy.ops.render.render(animation=True)
def set_stamp(active_note=False):
rd.use_stamp = True
## Enable
rd.use_stamp_filename = True
rd.use_stamp_frame = True
rd.use_stamp_render_time = True
rd.use_stamp_time = True
rd.use_stamp_date = True
## Disable
rd.use_stamp_sequencer_strip = False
rd.use_stamp_marker = False
rd.use_stamp_scene = False
rd.use_stamp_lens = False
rd.use_stamp_camera = False
rd.use_stamp_hostname = False
rd.use_stamp_memory = False
rd.use_stamp_frame_range = False
## Notes
rd.use_stamp_note = active_note
if rd.use_stamp_note:
info = D.texts.get('info')
txt = ''
if info:
for l in info.lines[:2]:
if 'Created from' in l.body:
txt = l.body
if txt:
rd.stamp_note_text = txt
else:#disable notes
rd.use_stamp_note = False
def setup_render_params(use_stamp=False):
'''set dimensions, percentage, fps...'''
if use_stamp:
set_stamp(active_note=True)
else:
rd.use_stamp = False
rd.resolution_x = 1920
rd.resolution_y = 1080
rd.resolution_percentage = 100
rd.use_border = False
rd.fps = 25
rd.use_sequencer = False
#sampling
ev.taa_render_samples = 128#push up sample for shadow mainly
#AO
ev.use_gtao = True
ev.gtao_distance = 0.7
ev.gtao_factor = 0.7
ev.gtao_quality = 0.2
ev.use_gtao_bent_normals = False#bent normal makes it lighter than shadows !
#not sure...
ev.use_gtao_bounce = True#no bounce is darker... (less AO on claer object)
#setup color management
scene.view_settings.look = 'Filmic - Medium High Contrast'
#no disable auto folder opening after auto video making
if hasattr(scene, 'MVopen'):
scene.MVopen = False
def set_render_path():
setup_render_params(use_stamp=info['stamp'])
rd.image_settings.file_format = 'PNG'
rd.image_settings.color_mode = 'RGB'#A
rd.image_settings.color_depth = '8'
rd.use_file_extension = True
rd.use_overwrite = True#erase file
rd.use_placeholder = False
rd.image_settings.compression = 50#default = 15, high = slower and lighter (same quality)
filename = splitext(basename(D.filepath))[0]
# fp = join(dirname(D.filepath), 'render')
rd.filepath = join(dirname(D.filepath), 'images', filename, filename + '.')# '.####' #let a dot to separate padding number
set_render_path()
'''
start = time()
render_anim(GL=False)
print(f'elapsed {time() - start}')
'''

View File

@ -0,0 +1,155 @@
info = {
'icon' : 'TOOL_SETTINGS',
'description' : 'Set filepath and video settings for direct video out',
'stamp' : False
}
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
from time import time
C = bpy.context
D = bpy.data
scn = scene = C.scene
rd = scn.render
ev = scn.eevee
def set_stamp(active_note=False):
rd.use_stamp = True
## Enable
rd.use_stamp_filename = True
rd.use_stamp_frame = True
rd.use_stamp_render_time = True
rd.use_stamp_time = True
rd.use_stamp_date = True
## Disable
rd.use_stamp_sequencer_strip = False
rd.use_stamp_marker = False
rd.use_stamp_scene = False
rd.use_stamp_lens = False
rd.use_stamp_camera = False
rd.use_stamp_hostname = False
rd.use_stamp_memory = False
rd.use_stamp_frame_range = False
## Notes
rd.use_stamp_note = active_note
if rd.use_stamp_note:
info = D.texts.get('info')
txt = ''
if info:
for l in info.lines[:2]:
if 'Created from' in l.body:
txt = l.body
if txt:
rd.stamp_note_text = txt
else:#disable notes
rd.use_stamp_note = False
def setup_render_params(use_stamp=False):
'''set dimensions, percentage, fps...'''
if use_stamp:
set_stamp(active_note=True)
else:
rd.use_stamp = False
rd.resolution_x = 1920
rd.resolution_y = 1080
rd.resolution_percentage = 100
rd.use_border = False
rd.fps = 25
rd.use_sequencer = False
#sampling
ev.taa_render_samples = 128#push up sample for shadow mainly
#AO
ev.use_gtao = True
ev.gtao_distance = 0.7
ev.gtao_factor = 0.7
ev.gtao_quality = 0.2
ev.use_gtao_bent_normals = False#bent normal makes it lighter than shadows !
#not sure...
ev.use_gtao_bounce = True#no bounce is darker... (less AO on claer object)
#setup color management
scene.view_settings.look = 'Filmic - Medium High Contrast'
#no disable auto folder opening after auto video making
if hasattr(scene, 'MVopen'):
scene.MVopen = False
def render_anim(GL=False):
#openGl render
if GL:
bpy.ops.render.opengl(animation=True, view_context=False)#view_context False > look throughcam
else:
bpy.ops.render.render(animation=True)
def check_name(name, fp):
filelist = [splitext(f)[0] for f in os.listdir(fp)]#raw names
if not name in filelist:
return name
ct = 2
new = name + '_' + str(ct).zfill(2)
while new in filelist:
new = name + '_' + str(ct).zfill(2)
ct+=1
if ct > 99:
return None
return new
def set_video_path():
setup_render_params(use_stamp=info['stamp'])
rd.image_settings.file_format = 'FFMPEG'
rd.image_settings.color_mode = 'RGB'
rd.ffmpeg.codec = 'H264'
rd.ffmpeg.constant_rate_factor = 'HIGH'#default 'MEDIUM'
rd.ffmpeg.format = 'MPEG4'#'MKV', 'QUICKTIME'
rd.ffmpeg.ffmpeg_preset = 'GOOD'#default = 'GOOD'(compromise), BEST(light - slow), REALTIME(fat-fast)'
ext = ''
if rd.ffmpeg.format == 'MPEG4':
ext = '.mp4'
elif rd.ffmpeg.format == 'QUICKTIME':
ext = '.mov'
elif rd.ffmpeg.format == 'MKV':
ext = '.mkv'
filename = splitext(basename(D.filepath))[0]
fp = join(dirname(D.filepath), 'images')
if not exists(fp):
print(f'not found : {fp} ')
return
filename = check_name(filename, fp)
if not filename:
print('name not available')
return
rd.filepath = join(dirname(D.filepath), 'images', filename + ext)
set_video_path()
'''
start = time()
render_anim(GL=False)
print(f'elapsed {time() - start}')
'''

View File

@ -0,0 +1,39 @@
info = {
'icon' : 'RENDER_ANIMATION',
'description' : 'render timed',
}
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
from time import time
import datetime
C = bpy.context
D = bpy.data
scn = scene = C.scene
rd = scn.render
def render_anim(GL=False):
#openGl render
if GL:
bpy.ops.render.opengl(animation=True, view_context=True)#view_context False > look throughcam
else:
bpy.ops.render.render(animation=True)
start = time()
render_anim()
secs = time() - start
timesec_str = f'elapsed {secs} seconds'
time_str = str(datetime.timedelta(seconds=secs))
print(timesec_str)
print(time_str)
C.window_manager.clipboard = time_str

View File

@ -0,0 +1,39 @@
info = {
'icon' : 'RENDER_ANIMATION',
'description' : 'render this viewport openGL',
}
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
from time import time
import datetime
C = bpy.context
D = bpy.data
scn = scene = C.scene
rd = scn.render
def render_anim(GL=False):
#openGl render
if GL:
bpy.ops.render.opengl(animation=True, view_context=True)#view_context False > look throughcam
else:
bpy.ops.render.render(animation=True)
start = time()
render_anim(GL=True)
secs = time() - start
timesec_str = f'elapsed {secs} seconds'
time_str = str(datetime.timedelta(seconds=secs))
print(timesec_str)
print(time_str)
C.window_manager.clipboard = time_str

View File

@ -0,0 +1,62 @@
info = {
'icon' : 'RENDER_STILL',
'description' : 'render still image timed',
}
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
from time import time
import datetime
C = bpy.context
D = bpy.data
scn = scene = C.scene
rd = scn.render
def pad_with_current_frame(match):
return str(C.scene.frame_current).zfill(len(match.group(0)))
def add_frame_padding(name):
'''
return string padded with current blender frame
if # found, use this padding to write theframe
'''
import re
if not '#' in name: return name + str(C.scene.frame_current).zfill(4)
# return re.sub(r'\#{1,10}', pad_with_current_frame, name)# all '#...' in string
return re.sub(r'\#{1,10}(?!.*\#)', pad_with_current_frame, name)# only last '#...'
def render(anim=False, GL=False):
if GL:#openGl render
bpy.ops.render.opengl(animation=anim, write_still=True, view_context=True)#view_context False > look throughcam
else:
bpy.ops.render.render(animation=anim, write_still=True)
start = time()
orgfp = C.scene.render.filepath
C.scene.render.filepath = add_frame_padding(C.scene.render.filepath)
try:
render(anim=False)
except Exception as e:
print(e)
finally:
C.scene.render.filepath = orgfp
secs = time() - start
timesec_str = f'elapsed {secs} seconds'
time_str = str(datetime.timedelta(seconds=secs))
print(timesec_str)
print(time_str)
C.window_manager.clipboard = time_str

View File

@ -0,0 +1,109 @@
info = {
'icon' : 'PACKAGE',
'description' : 'pack all videos',
'dryrun' : False
}
import os, re, shutil
from os.path import dirname, basename, join, abspath, splitext, exists, isfile, isdir
from os import listdir
import datetime
dryrun = info['dryrun']
resid = re.compile(r'\d{2}-\d{3}-\d{2}$')
shots = '/z/___SERIE/WIND-UPS/episodes/00/shots/'
packzone = '/z/___SERIE/WIND-UPS/medias/pack'
old_folder = join(packzone, '_old')
new_list = []
warning = []
extension_filter = ('mp4', 'mov',)#, 'mkv'
exclusion_words = ('test', 'old',)#use this for loose word search
rexclude = re.compile(r'[ _-]old|test')#old if after separator (' _-') and word test anywhere (might be too loose)
for f in os.scandir(shots):
if not resid.match(f.name):continue
imgfp = f'/z/___SERIE/WIND-UPS/episodes/00/shots/{f.name}/lighting/images'
if not exists(imgfp):continue
#need to be video file type
all_videos = [v for v in os.scandir(imgfp) if v.name.lower().endswith(extension_filter) and not rexclude.search(v.name)]#any(x in v.name for x in exclusion_words)
if not all_videos:continue
print(f'\n{f.name}')
bytime = sorted(all_videos, key=lambda x: x.stat().st_mtime)
byname = sorted(all_videos, key=lambda x: x.name)
# print('bytime: ', bytime[-1].name)
# print('byname: ', byname[-1].name)
if bytime[-1] != byname[-1]:
warn = f'''/!\\ {f.name}
last by name and by modif time different in {imgfp}
by time : {bytime[-1].name} ({datetime.datetime.fromtimestamp(bytime[-1].stat().st_mtime)})
by name : {byname[-1].name} ({datetime.datetime.fromtimestamp(byname[-1].stat().st_mtime)})
'''
print(warn)
warning.append(warn)
good_file = bytime[-1]
topack = good_file.path
topackname = good_file.name
dest = join(packzone, topackname)
# run checks and copy if needed
if exists(dest):
dup = [basename(dest)]
else:
print(f"compare with {'_'.join(splitext(good_file.name)[0].split('_')[:3])}")
# ex : twu_00-008-01_lighting_v001.mp4 (split on third to)
dup = [i for i in listdir(packzone) if isfile(join(packzone, i)) and '_'.join(splitext(i)[0].split('_')[:3]) == '_'.join(splitext(good_file.name)[0].split('_')[:3])]
# check duplication against this file
if dup:
print(f'same shot already exists:{dup}')
newer = True
for i in dup:
dfp = join(packzone, i)
# print(f'{os.stat(dfp).st_mtime} VS file {good_file.stat().st_mtime}')
if os.stat(dfp).st_mtime >= good_file.stat().st_mtime:
# print(f'{i} is same or most recent')
newer = False
if not newer:
print(f'{good_file.name} is not newer, skip')
continue
#src is most recent, do the copy and erase/move older
for d in dup:
if not exists(old_folder):os.mkdir(old_folder)
old_src = join(packzone, d)
old_dest = join(old_folder, d)
if exists(old_dest):
print(f' - deleting {d} (already in _old)')
if not dryrun: os.remove(old_src)
else:
print(f' - moving {d} to _old')
if not dryrun: shutil.move(old_src, old_dest)
# if made it here, safely do the copy
print(f'- packing new file : {topack}')
if not dryrun: shutil.copy2(topack, dest)
new_list.append(f'{topackname} : {topack}')
print('DONE\n----')
if warning:
print('\nWarnings')
for w in warning: print(w)
if new_list:
print(f'\n{len(new_list)} New:')
for n in new_list: print(n)

View File

@ -0,0 +1,119 @@
info = {
'icon' : 'COPYDOWN',
'description' : 'Go in render node mode waiting for jobs in watchfolder',
'checkfiles' : False,
'stamp' : False,
'cmds' : ''
}
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re
from time import sleep
import time
import datetime
## Blender
import bpy
from mathutils import Vector, Matrix
C = bpy.context
D = bpy.data
scn = scene = C.scene
def submit_to_watchfolder(checkfiles=False):
'''submitter (with or without file_copy)'''
if D.is_dirty or not D.is_saved:
print('!!! Not submitted : File must be saved first')
return
totalframes = scn.frame_end - scn.frame_start
if not overwrite:
#check if output path exists and have file
out = scn.render.filepath
outfolder = dirname(out)
if exists(outfolder):
outname = basename(out)
refile = re.compile(outname.rstrip('#') + r'\d{4}')
outlist = [f for f in listdir(outfolder) if refile.match(f)]
if outlist:
print(f'!!! Abort submission : Overwrite is False and {len(outlist)} files already exists ({totalframes} wanted)')
return
print(f'{totalframes} frames to render')
watchfolder = '/z/___SERIE/WIND-UPS/episodes/00/watchfolder'
if not exists(watchfolder):
print(f'watchfolder not found at {watchfolder}')
watchfolder = '/mnt/admserveur/prod/___SERIE/WIND-UPS/episodes/00/watchfolder'
if not exists(watchfolder):
print(f'ABORT : watchfolder not found at {watchfolder}')
return 1
#save a copy to the file ?
the_file = D.filepath
finalname = basename(D.filepath)
'''
if use_copy:
#save a copy
head, tail = os.path.split(D.filepath)
name, ext = splitext(tail)
uid = datetime.datetime.now().strftime('_rdtemp%Y%m%d%H%M%S')
finalname = name + uid + ext
the_file = join(head, finalname)
bpy.ops.wm.save_as_mainfile(filepath=the_file, copy=True)
link = join(watchfolder, finalname)
print(f'Create symlink:\n src: {finalname}\n dest: {link}')#the_file, complete path to finalname
os.symlink(the_file, link)#impossible to create symlink... so fuck-it.
'''
noext = splitext(finalname)[0]
stby = join(watchfolder, 'stby--' + noext)# + '.txt'
todo = join(watchfolder, 'todo--' + noext)# + '.txt'
running = join(watchfolder, 'runn--' + noext)# + '.txt'
done = join(watchfolder, 'done--' + noext)# + '.txt'
bad = 0
statelist = []
if exists(stby):
statelist.append(basename(stby))
if exists(todo):
statelist.append(basename(todo))
if exists(running):
statelist.append(basename(running))
if exists(done):
statelist.append(basename(done))
for f in os.listdir(watchfolder):
if splitext(f)[0][len('stby--'):] == noext:
print('Something already exists')
if exists(todo):
bad = 1
print(f'{noext}: Job already submitted')
if len(statelist) > 1:
print('problem : job exists in multiple states')
for state in statelist:
print(state)
bad = 1
if bad:
print('skip')
return
with open(todo, 'w') as fd:
fd.write(the_file+'\n')
if info['stamp']:
fd.write('stamp\n')
if info['cmds']:
fd.write(info['cmds'])
print(f'Created job {todo}')
print(f'Submit {basename(D.filepath)}')
submit_to_watchfolder(checkfiles=info['checkfiles'])

View File

@ -0,0 +1,215 @@
info = {
'icon' : 'RENDER_RESULT',
'description' : 'Pass in render node watchmode (freeze and get job from watchfolder when a todo is available)',
}
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from math import radians, degrees
from time import sleep
import datetime
import time
import platform
# Blender
import bpy
from mathutils import Vector, Matrix
C = bpy.context
D = bpy.data
scene = C.scene
## General timer to disable ?
## How to push a key to stop the mega-loop ? (or barbaric ctrl-C in cmd)
def logit(message):
try:
print(message)
with open(log, 'a') as fd:
if not message.endswith('\n'):
message += '\n'
fd.write(message)#add trailing new line
except Exception as e:
print('Log failed', e)
def set_stamp(active_note=False):
rd = bpy.context.scene.render
rd.use_stamp = True
## Enable
rd.use_stamp_filename = True
rd.use_stamp_frame = True
rd.use_stamp_render_time = True
rd.use_stamp_time = True
rd.use_stamp_date = True
## Disable
rd.use_stamp_sequencer_strip = False
rd.use_stamp_marker = False
rd.use_stamp_scene = False
rd.use_stamp_lens = False
rd.use_stamp_camera = False
rd.use_stamp_hostname = False
rd.use_stamp_memory = False
rd.use_stamp_frame_range = False
## Notes
rd.use_stamp_note = active_note
if rd.use_stamp_note:
info = D.texts.get('info')
txt = ''
if info:
for l in info.lines[:2]:
if 'Created from' in l.body:
txt = l.body
if txt:
rd.stamp_note_text = txt
else:#disable notes
rd.use_stamp_note = False
def run(fp):
running = fp.replace('todo--', 'runn--')
stamp = False
warn = []
# Check if a running job already exists
if exists(running):
logit(f'Problem ! job already exists : {running}')
return 1
# Get blend path
with open(fp, 'r') as fd:
# blend = fd.read().strip()
lines = fd.readlines()
blend = lines[0].strip()
stamp = 'stamp' in lines
for l in lines[1:]:#skip filepath
if '=' in l:#skip simple keyword qand focus on assignation
try:
exec(l)
except Exception as e:
print(f'could not exec {l}:\n->{e}')
warn.append(f'could not exec {l}:\n->{e}')
if not blend:
logit(f'Problem reading {fp}')
return 1
# check path
logit(f'Target blend : {blend}')
if not exists(blend):
logit(f'ERROR ! job {basename(running)} give an invalid filepath')
#delete/rename running job
os.rename(running, running.replace('runn--', 'error-'))
return 1
# Pass on job mode
try:
os.rename(fp, running)
except Exception as e:
logit(f'Could not rename job to running mode : {fp} ')
return 1
# write the timestamp and launch the job
with open(running, 'a') as fd:
startmess = f'starting : {str(datetime.datetime.now())}\n'
logit(startmess)
fd.write(startmess)
# Blender time ! open, check output and render the job
start_time = time.time()# get start time
bpy.ops.wm.open_mainfile(filepath=blend)
scn = bpy.context.scene
#here change Ao/filepath/stuff if needed
if stamp:
set_stamp(active_note=True)
else:
bpy.context.scene.render.use_stamp = False
bpy.ops.render.render(animation=True)#anim render
print('Rendering done, launching make_video in 2 sec...')
## log end time and exit
elapsed_time = time.time() - start_time# seconds
full_time = str(datetime.timedelta(seconds=elapsed_time))# hh:mm:ss format
timecode = datetime.datetime.now().strftime('%m%d%H%M')#timecode format '02041139' month+day+hour+minutes
sleep(2)
#no disable auto folder opening after auto video making
if hasattr(scn, 'MVopen'):
scn.MVopen = False
bpy.ops.render.make_video()
with open(running, 'a') as fd:
endmess = f'ended : {str(datetime.datetime.now())}\nrender time: {full_time}\n'
logit(endmess)
fd.write(endmess)
if warn:
fd.write('\n'.join(warn)+'\n')
# rename and move into done folder
donename = basename(running).replace('runn--', 'done--') + f'__{timecode}'#.replace('.txt', f'__{timecode}.txt')
done = join(donefolder, donename)
try:
os.rename(running, done)
except Exception as e:
logit(f'Could not move/rename run job to done : {done} (from {running}) ')
return
print(f'Finished job {basename(running)}, Continue in 5 seconds')
sleep(5)
return 0
###================
def watchmode():
if not bpy.data.is_saved or bpy.data.is_dirty:
print('You need to start from a saved blender file (preferably empty)')
return
if not exists(watchfolder):
print(f'!!!! Abort, watchfolder ({watchfolder}) not found')
return
if not exists(logfolder):
os.mkdir(logfolder)
print('log folder created')
while True:#infinite loop
job_status = 1
todolist = [f for f in os.listdir(watchfolder) if f.startswith('todo--')]
if todolist:
first = todolist[0]
print(f'{len(todolist)} todo, starting: {first}')
fp = join(watchfolder, first)
job_status = run(fp)
print('job_status: ', job_status)
#reopen folder
bpy.ops.wm.open_mainfile(filepath=org_blend)
sleep(300)#5min wait before looping
org_blend = bpy.data.filepath
machine = platform.node().split('.')[0]
if machine:
machine = 'node_' + machine + '.txt'
if not machine:
print('Cannot find machine name')
machine = 'nodes.txt'
print(f'use machine name : {machine}')
watchfolder = '/z/___SERIE/WIND-UPS/episodes/00/watchfolder'
donefolder = join(watchfolder, 'done')
logfolder = join(watchfolder, 'logs')
log = join(logfolder, machine)
watchmode()
print('End of batch.\nEOF')

View File

@ -0,0 +1,30 @@
# coding: utf-8
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = C.scene
print('====')
'''
for i in range(0, 8):
ao = i * 0.25
scene.eevee.gtao_distance = ao
scene.render.filepath = join(dirname(D.filepath), f'{splitext(basename(D.filepath))[0]}-{scene.frame_current}_AO{i}-{ao}')
bpy.ops.render.render(animation=False, write_still=True)
'''
for i in range(0, 8):
fac = 1 - i * 0.1
#scene.eevee.gtao_distance = ao
scene.eevee.gtao_factor = fac
scene.render.filepath = join(dirname(D.filepath), f'{splitext(basename(D.filepath))[0]}-{scene.frame_current}_AOfac{i}-{fac}')
bpy.ops.render.render(animation=False, write_still=True)
print('done')

View File

@ -0,0 +1,37 @@
# coding: utf-8
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = C.scene
print('====')
def more_sss(val=0.04):
for mat in D.materials:
if not mat.use_nodes:continue
nodes = mat.node_tree
if not nodes: continue
nodes = nodes.nodes
for n in nodes:
if n.type == 'GROUP':
sss = n.inputs.get('Subsurface')
if not sss: continue
#print(f'{mat.name} : {sss.default_value}')
current = sss.default_value
new = current + val
print(f'{mat.name} : {current} >> {new}')
sss.default_value = new
value = 0.06
for i in range(0, 8):
scene.render.filepath = join(dirname(D.filepath), f'{splitext(basename(D.filepath))[0]}-{scene.frame_current}_sss_plus_0.{str(i*6).zfill(2)}')
bpy.ops.render.render(animation=False, write_still=True)
more_sss(value)

View File

@ -0,0 +1,32 @@
# coding: utf-8
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = C.scene
print('====')
def print_sss():
for mat in D.materials:
if not mat.use_nodes:continue
nodes = mat.node_tree
if not nodes: continue
nodes = nodes.nodes
for n in nodes:
if n.type == 'GROUP':
sss = n.inputs.get('Subsurface')
if not sss: continue
print(f'{mat.name} : {sss.default_value}')
if n.type == 'BSDF_PRINCIPLED':
sss = n.inputs.get('Subsurface')
if not sss: continue
print(f'PRINCIPLED : {mat.name} : {sss.default_value}')
print_sss()

View File

@ -0,0 +1,58 @@
info = {
'icon' : 'LINENUMBERS_ON',
'description' : 'Note and render',
'render' : False,
}
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = C.scene
print('====')
scene.render.use_stamp_note = True
dist = scene.eevee.gtao_distance
fac = scene.eevee.gtao_factor
print('dist: ', dist)
print('fac: ', fac)
dist = round(dist, 2)
fac = round(fac, 2)
scene.render.stamp_note_text = f'AO dist {dist} fac {fac}'
if scene.view_settings.look != 'Filmic - Medium Contrast':
scene.render.stamp_note_text += f' - {scene.view_settings.look}'
org = scene.render.filepath
print(f'filepath was : {org}')
## full filename
# scene.render.filepath = join(dirname(D.filepath), f'{splitext(basename(D.filepath))[0]}-{scene.frame_current}_AO-dist{dist}-fac{fac}')
## custom filename
ep = int(re.search(r"(\d{3})", splitext(basename(D.filepath))[0]).group(1))
print('ep: ', ep)
scene.render.filepath = join(dirname(D.filepath), f'twu_{ep}-{scene.frame_current}_AO-dist{dist}-fac{fac}')
i = 2#increment if needed
while exists(scene.render.filepath + '.png'):
scene.render.filepath = join(dirname(D.filepath), f'twu_{ep}-{scene.frame_current}_AO-dist{dist}-fac{fac}_{i}')
if info['render']:
bpy.ops.render.render(animation=False, write_still=True)
# restore old sauce ?
scene.render.filepath = org
print('done')

View File

@ -0,0 +1,44 @@
info = {
'icon' : 'FILE_TEXT',
'description' : 'explore API in console at datapath of the popup',
'data_path' : ''
}
import bpy, mathutils
exclude = (
### add lines here to exclude specific attribute
'bl_rna', 'identifier','name_property','rna_type','properties',#basic
## To avoid recursion/crash on direct object call (comment for API check on deeper props)
'data', 'edges', 'faces', 'edge_keys', 'polygons', 'loops', 'face_maps', 'original',
## Avoid some specific properties
#'matrix_local', 'matrix_parent_inverse', 'matrix_basis','location','rotation_euler', 'rotation_quaternion', 'rotation_axis_angle', 'scale', 'translation',
)
def list_attr(path, ct=0):
for attr in dir(eval(path)):
if not attr.startswith('__') and not attr in exclude:
try:
value = getattr(eval(path),attr)
except AttributeError:
value = None
if value != None:
if not callable(value):
if type(value) in ( type(0),type(0.0),type(True),type('str'),type(mathutils.Vector()),type(mathutils.Color()), type(mathutils.Matrix()) ):
print(ct*' ' + attr, value)
else:
print(ct*' ' + attr,value,type(value))
ct+=1
# print (ct*' ' + '>')
list_attr('%s.%s'%(path,attr), ct)#Comment this line to kill recursion
print('---')
# write datapath as string. ex : "bpy.data.objects['Cube'].modifiers['Softbody']"
dt = info['data_path']
if dt:
list_attr(dt)
print ('Done')
else:
print('no data path given')

View File

@ -0,0 +1,56 @@
info = {
'icon' : 'EVENT_X',
'description' : 'Delete all blend backup version (.blend1 and next backups) in passed directory',
'folderpath' : '',
'recursive' : True,
'test' : False,
}
# coding: utf-8
import bpy
import os
import re, fnmatch, glob
from pathlib import Path
import shutil
C = bpy.context
D = bpy.data
scene = C.scene
def kill_blend_backup_version(fp, recursive=True, test=False):
if not fp:
print('ERROR', 'no filepath given')
return
re_blendbackup = re.compile(r'\.blend\d+')
fpo = Path(fp)
if not fpo.exists():
print('ERROR', f'invalid path: {fp}')
return
ct = 0
print(f'Removing blend backup files in : {fpo}')
if recursive:
for root, dirs, files in os.walk(fp):
for name in files:
fo = Path(root) / name
if re_blendbackup.match(fo.suffix):
ct += 1
print(fo)
if not test:
fo.unlink()
else:
for f in os.listdir(fp):
fo = Path(fp) / f
if re_blendbackup.match(fo.suffix):
ct += 1
print(fo)
if not test:
fo.unlink()
if ct:
print('INFO', f'Deleted {ct} file(s)')
kill_blend_backup_version(info['folderpath'], recursive = info['recursive'], test = info['test'])

View File

@ -0,0 +1,71 @@
info = {
'icon' : 'LIBRARY_DATA_BROKEN',
'description' : 'Correct path (print in console, apply as option) when weirdly auto added unwanted parent directory (still break rotate order for armatures...)',
'apply' : False
}
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext, abspath
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = C.scene
""" rel = D.libraries[0].filepath.replace('//','')
parent = rel.count('..')
print('parent: ', parent)
abspath(join(dirname(D.filepath), rel)) """
## relpath
def to_abs(rfp):
return abspath(bpy.path.abspath(rfp))
print('\n--- Cheking libraries filepath ---')
apply = False
apply = info['apply']#get it from info dic
folder = dirname(D.filepath)
for lib in D.libraries:
libfp = lib.filepath
if exists(to_abs(libfp)):
print('clean :', libfp)
else:
print('broken:', libfp)
pct = libfp.count('..')
for i in range(1, pct+1):
newrel = ('').join(libfp.split('../',i))
if exists(to_abs(newrel)):
print(f'found at -{i} parent folder')
print(f'-> {newrel}')
if apply:
lib.filepath = newrel
print()
break
if i == pct:
print(f'tested with {pct} folder up and nothing found')
print('Done.')
'''
## abspath technique
folder = dirname(D.filepath)
for lib in D.libraries:
curfp = abspath(bpy.path.abspath(lib.filepath))
if exists(curfp):
print('clean :', lib.filepath)
else:
print('broken:', lib.filepath)
pct = lib.filepath.count('..')
print('up folder: ', pct)
endlib = lib.filepath.lstrip(os.sep+'.')
for i in range(1, pct+1):
start = '/'.join(folder.split(os.sep)[:-i])
nfp = join(start, endlib)
if exists(nfp):
print(f'{i} parent folder VS {pct} in current')
'''

View File

@ -0,0 +1,60 @@
info = {
'icon' : 'ARMATURE_DATA',
'description' : 'run some check on proxys and armautre and print in console',
}
# coding: utf-8
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = bpy.context.scene
print('\nCollection instance check -- ')
problems = []
for o in bpy.context.scene.objects:
if o.instance_collection:
for instob in o.instance_collection.objects:
badname = withproxy = withcolproxy = alert = ''
if instob.type == 'ARMATURE':
mess = f'{o.name} -> rig: {instob.name}'
arm = instob.data
# print("arm", arm.name)#Dbg
if instob.name != arm.name:
badname = 1
mess += f' != arm data: {arm.name}'
rigname = arm.name.replace('_rig','_proxy') if 'rig' in arm.name else arm.name + '_proxy'
proxy = bpy.context.scene.objects.get(rigname)
if proxy:
withproxy = 1
mess += ' ->> Proxy in scene'
if not proxy:
collec_proxy = instob.name.replace('_rig','_proxy') if 'rig' in instob.name else instob.name + '_proxy'
proxy = bpy.context.scene.objects.get(collec_proxy)
if proxy :
withcolproxy = 1
mess += ' ->> Proxy from colname in scene'
if proxy and proxy.animation_data and proxy.animation_data.action and len(proxy.animation_data.action.fcurves) > 0:
mess += ' (animated)'
if badname and withcolproxy:
alert = '!!!'
elif badname and withproxy:
alert = '!!'
elif badname:
alert = '!'
mess = f'{alert} {mess}'
if badname or withproxy or withcolproxy:
problems.append(mess)
print(mess)
print('Done')

View File

@ -0,0 +1,140 @@
info = {
'icon' : 'DRIVER',
'description' : 'create visibility on driver objects or armature selection from a bone of a rig',
'prop_rig' : 'name_of_the_rig'
'prop_name' : 'hide_something'
'prop_bone' : 'root'
}
import bpy
def add_driver(source, target, prop, dataPath, index = -1, negative = False, func = ''):
''' Add driver to source prop (at index), driven by target dataPath '''
source.driver_remove(prop, index)
if index != -1:
d = source.driver_add( prop, index ).driver
else:
d = source.driver_add( prop ).driver
v = d.variables.new()
v.targets[0].id = target
v.targets[0].data_path = dataPath
d.expression = func + "(" + v.name + ")" if func else v.name
d.expression = d.expression if not negative else "-1 * " + d.expression
def create_hide_custom_prop(src_object, prop_name, prop_bone = ''):
'''
add source propertie with boolean option
place the hide prop on src_object with name prop_name
'''
rig = bpy.data.objects.get(src_object)
if not rig:
print(f"No objects named {src_object}")
return 1
if rig.type != 'ARMATURE':
print(f"Not an armature : {src_object}")
return 1
#add target bone
if prop_bone:
holder = rig.pose.bones.get(prop_bone)
else:
holder = rig.pose.bones.get('root')
if not holder:
print(f'problem finding bone {prop_bone} (or root)')
return 1
# create
if not holder.get('_RNA_UI'):
holder['_RNA_UI'] = {}
if not prop_name in holder.keys() :
holder[prop_name] = 0
holder['_RNA_UI'][prop_name] = {"default": 0,"min":0,"max":1,"soft_min":0,"soft_max":1}
else:
print(f'{prop_name} : already exists on root key')
return
return 0
def drive_selection_visibility(rig, prop_name, prop_bone = ''):
# add driver on selection
prefixs = ('MCH','DEF','ORG', 'WGT')
rig = bpy.data.objects.get(src_object)
if not rig:
print(f"No objects named {src_object}")
return 1
if rig.type != 'ARMATURE':
print(f"Not an armature : {src_object}")
return 1
#add target bone
if not prop_bone:
prop_bone = 'root'
if not rig.pose.bones.get(prop_bone):
print(f'no bones {prop_bone} on rig {rig.name}')
return 1
meshes = [i for i in bpy.context.selected_objects if i.type in ('MESH','CURVE','TEXT') and not i.name.startswith(('WGT', 'WDGT'))]
armatures = [i for i in bpy.context.selected_objects if i.type == 'ARMATURE']
if bpy.context.mode == 'POSE':
obarm = bpy.context.active_object
for bone in bpy.context.selected_pose_bones_from_active_object:
prop = 'bones["%s"].hide'%bone.name
index = -1
layer = bone.bone.layers
protect_layer = rig.data.layers_protected
### dont check for protected, strictly use selection.
# if bone.name.startswith(prefixs) or any([i==j==1 for i,j in zip(layer,protect_layer)]) :
# print(f'Skipped : Prefixed or protected bone : {bone.name}')
# rig.data.driver_remove(prop, index)
# continue
print(f'New : Driver on bone {bone.name}')
add_driver(obarm.data, rig, prop, f'pose.bones["{prop_bone}"]["{prop_name}"]', index)
return
for ob in meshes :
print('Object : ', obarm.name)
add_driver(ob, rig, 'hide_viewport', f'pose.bones["{prop_bone}"]["{prop_name}"]', -1)
add_driver(ob, rig, 'hide_render', f'pose.bones["{prop_bone}"]["{prop_name}"]', -1)
for obarm in armatures:
print('Armature : ', obarm.name)
## mask armature object
## add_driver(obarm, rig, 'hide_viewport', f'pose.bones["{prop_bone}"]["{prop_name}"]', -1)
## bette mask pose bones since its a proxy...
for bone in obarm.pose.bones :
prop = 'bones["%s"].hide'%bone.name
index = -1
layer = bone.bone.layers
protect_layer = rig.data.layers_protected
if bone.name.startswith(prefixs) or any([i==j==1 for i,j in zip(layer,protect_layer)]) :
print(f'Skipped : Prefixed or protected bone : {bone.name}')
rig.data.driver_remove(prop, index)
else :
print(f'New : Driver on bone {bone.name}')
add_driver(obarm.data, rig, prop, f'pose.bones["{prop_bone}"]["{prop_name}"]', index)
### ----
## write the name of the rig source (will put the propertie on the root of this armature)
prop_rig = info['prop_rig']# name_of_the_rig
## write the name of the propertie to attach
prop_name = info["prop_name"]#'hide_headband'# hide_something
## prop_bone (bone holding the propertie), 'root' if left string empty.
prop_bone = info["prop_bone"]
create_hide_custom_prop(prop_rig, prop_name, prop_bone = prop_bone)
drive_selection_visibility(prop_rig, prop_name, prop_bone = prop_bone)

View File

@ -0,0 +1,27 @@
info = {
'icon' : 'ACTION_TWEAK',
'description' : 'Rename selected armature actions (with name of the rig, replacing "_rig" with "_act")',
}
# coding: utf-8
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = C.scene
for o in C.selected_objects:
if o.type == 'ARMATURE':
print(o.animation_data.action.name)
if '_rig' in o.name:
action = o.animation_data.action
if action.name.replace('_act','') != o.name.replace('_rig', ''):
newname = o.name.replace('_rig', '_act')
print(f'action rename {action.name} > {newname}')
action.name = newname

View File

@ -0,0 +1,21 @@
info = {
'icon' : 'HIDE_OFF',
'description' : 'Force viewport visibility ON for objects with hide_viewport prop animated',
}
# coding: utf-8
import bpy
import os
from os import listdir
from os.path import join, dirname, basename, exists, isfile, isdir, splitext
import re, fnmatch, glob
from mathutils import Vector, Matrix
from math import radians, degrees
C = bpy.context
D = bpy.data
scene = C.scene
import bpy
for o in bpy.context.scene.objects:
if o.animation_data and o.animation_data.action and o.animation_data.action.fcurves.find('hide_viewport'):
o.hide_viewport = False

View File

@ -0,0 +1,22 @@
info = {
'icon' : 'OUTLINER_OB_ARMATURE',
'description' : 'Hide/Unhide all rigs',
}
import bpy
C = bpy.context
D = bpy.data
scene = C.scene
#hide armature
hide = True
arms = [o for o in C.scene.objects if o.type == 'ARMATURE']
if arms:
hide = not arms[0].hide_viewport
#invert of the state of the first armature found for all of them
for o in C.scene.objects:
if o.type == 'ARMATURE':
o.hide_viewport = hide

View File

@ -0,0 +1,5 @@
import bpy
from bpy import data as D
ob = D.objects['boom_hairs_group_ref']
ob.hide_viewport = not ob.hide_viewport

192
functions.py Normal file
View File

@ -0,0 +1,192 @@
from .utils import *
from bpy.props import *
from bpy.types import Panel,Operator
from .properties import CustomShelfSettings,CustomShelfPrefs
from .Types import *
SHELF_DIR = join(dirname(__file__),'shelves')
def operator(idname,args) :
op_execute = args['execute']
def execute(self, context):
op_execute(context,self)
return {"FINISHED"}
package = __package__.replace('_','').replace('-','').lower()
args['bl_idname'] = "%s.%s"%(package,idname)
args['bl_label'] = title(idname)
args['execute'] = execute
op = type('operator',(Operator,),args)
bpy.utils.register_class(op)
return op
def register_bl_class(bl_class,args) :
a = type('test',(bl_class,),{})
a.initialyse(a,*args)
bpy.utils.register_class(a)
return a
def filter_dir(dir_list) :
return [d for d in dir_list if d.is_dir() and not d.name.startswith('_')]
def get_shelves_folder():
prefs = bpy.context.preferences.addons[__package__].preferences
shelves_path = prefs.global_shelves
additionnal_shelves_path = [s.path for s in prefs.additionnal_shelves]
return [p for p in additionnal_shelves_path+[shelves_path] if exists(p)]
def get_dirs(paths) :
if isinstance(paths,(tuple,list)) :
dirs = []
for dir in paths:
if not isinstance(dir, str) :
dir = dir.path
dirs += filter_dir(scandir(dir))
return dirs
else :
return filter_dir(scandir(paths))
def get_categories():
return [d.name for d in get_dirs(get_shelves_folder())]
def get_tabs() :
return [d.name for d in get_dirs(get_dirs(get_shelves_folder()))]
def get_category_path(category) :
category_dir = [f for f in get_shelves_folder() if category in [d.name for d in get_dirs(f)]]
if category_dir :
return join(category_dir[0],category)
def get_category_tabs(category):
return [d.name for d in get_dirs(get_category_path(category))]
def read_shelves() :
tag_filter_items = []
# unregister panel :
for p in CustomShelfSettings.panel_list :
if p.__name__ not in get_tabs() :
try :
bpy.utils.unregister_class(p)
except :
pass
CustomShelfSettings.panel_list.remove(p)
for cat in get_dirs(get_shelves_folder()) :
cat_name = cat.name.replace('_',' ').title()
header_info = {
"bl_region_type" : "UI",#petit ajout pour voir si foncitonne...
"bl_category" : cat_name,
"bl_idname" : "%s_PT_%s_header"%(__package__.upper(),cat.name),# -> "%s.%s_header" makes _PT_ warning
"cat" : cat.name,
"path" : cat.path
}
header = type("Header",(CSHELF_PT_shelf_header,),header_info)
bpy.utils.register_class(header)
#enable bool prefs
# panel_props = {t.name : BoolProperty(default = False) for t in get_dirs(cat.path)}# give "should be an annotation" field warning
## need annoattion type : https://blender.stackexchange.com/questions/118118/blender-2-8-field-property-declaration-and-dynamic-class-creation
panel_props = {'__annotations__': {t.name : BoolProperty(default = False) for t in get_dirs(cat.path)}}
PanelProps = type("CustomShelfPrefs",(PropertyGroup,),panel_props)
bpy.utils.register_class(PanelProps)
setattr(CustomShelfPrefs,cat.name,PointerProperty(type = PanelProps))
#search and filter
props = {
"search" : StringProperty(options = {"TEXTEDIT_UPDATE"}),
"filter" : BoolProperty(default = True)
}
# Props = type("props",(PropertyGroup,), props)# annotation error
Props = type("props",(PropertyGroup,),{'__annotations__': props})
bpy.utils.register_class(Props)
setattr(CustomShelfSettings,cat.name,PointerProperty(type = Props))
#enable bool prefs
#Prefs = type("CustomShelfPrefs",(PropertyGroup,),{})
#bpy.utils.register_class(Prefs)
#setattr(CustomShelfPrefs,cat.name,PointerProperty(type = Prefs))
for tab in get_dirs(cat.path) :
#get_settings
settings_json = join(tab.path,'settings.json')
tab_settings = {}
if exists(settings_json) :
tab_settings = read_json(settings_json)
if not tab_settings or not tab_settings.get('tags') : continue
for tag in tab_settings['tags'] :
if tag not in tag_filter_items :
tag_filter_items.append(tag)
panel_info = {
"bl_region_type" : "UI",#Added, maybe need it in 2.8+...
"bl_category" : cat_name,
"bl_idname" : "%s_PT_%s_%s"%(__package__.upper(),cat.name,tab.name),#"%s.%s_%s" -> makes _PT_ warning
"bl_label" : tab.name.replace('_',' ').title(),
"cat" : cat.name,
"settings" :tab_settings
}
space_info = {k:v for k,v in tab_settings.items() if k in ("bl_space_type","bl_region_type")}
panel_info.update(space_info)
panel = type("Panel",(CSHELF_PT_shelf_panel,),panel_info)
bpy.utils.register_class(panel)
CustomShelfSettings.panel_list.append(panel)
scripts = []
for script in scandir(tab.path) :
if not script.name.endswith('.py') : continue
script_name = splitext(script.name)[0]
info,lines = read_info(script.path)
info["description"] = info.get('description',"")
icon = info.get('icon',"WORDWRAP_OFF")
popup = type(script_name,(CSHELF_OT_shelf_popup,),{"script":script.path,'bl_description':info["description"]})
popup.initialyse(popup,script_name,info)
bpy.utils.register_class(popup)
scripts.append({"operator" : popup.bl_idname,"text" : script_name,"icon" : icon})
panel.scripts = sorted(scripts,key = lambda x :x['text'])
#Register tag filter enum
#setattr(Pre, v)
prefs = bpy.context.preferences.addons[__package__].preferences
tag_filter_items.sort()
tag_filter_items.insert(0, '__clear__')
prefs['tag_filter_items'] = tag_filter_items
#bpy.utils.unregister_class(CustomShelfPrefs)
#bpy.utils.register_class(CustomShelfPrefs)
#CustomShelfSettings.folders = list(get_shelves_folder().keys())
#folder_enum = EnumProperty(items = lambda s,c : [(f,f,"") for f in CustomShelfSettings.folders])
#setattr(CustomShelfSettings,'folders_enum',folder_enum)

292
operators.py Normal file
View File

@ -0,0 +1,292 @@
from .utils import *
from .functions import *
from bpy.types import Operator
from bpy.props import *
from .properties import CustomShelfSettings
class CSHELF_OT_refresh(Operator):
bl_idname = "customshelf.refresh"
bl_label = 'Refresh Shelves'
def execute(self,context) :
read_shelves()
return {'FINISHED'}
def get_tag_items(self,context) :
prefs = bpy.context.preferences.addons[__package__].preferences
return [(i,i.title(),"") for i in prefs['tag_filter_items']]
class CSHELF_OT_set_tag_filter(Operator):
bl_idname = "customshelf.set_tag_filter"
bl_label = 'Refresh Shelves'
tag_filter : EnumProperty(items = get_tag_items)
def execute(self, context):
prefs = bpy.context.preferences.addons[__package__].preferences
tag_filter = self.tag_filter
if tag_filter == '__clear__' :
tag_filter = ""
prefs.tag_filter = tag_filter
return {'FINISHED'}
class CSHELF_OT_add_shelf_folder(Operator):
bl_idname = "customshelf.add_shelves_folder"
bl_label = 'Refresh Shelves'
def execute(self, context):
prefs = bpy.context.preferences.addons[__package__].preferences
path = prefs.additionnal_shelves.add()
return {'FINISHED'}
class CSHELF_OT_remove_shelf_folder(Operator):
bl_idname = "customshelf.remove_shelves_folder"
bl_label = 'Refresh Shelves'
index : IntProperty()
def execute(self, context):
prefs = bpy.context.preferences.addons[__package__].preferences
prefs.additionnal_shelves.remove(self.index)
return {'FINISHED'}
class CSHELF_OT_open_shelf_folder(Operator):
bl_idname = "customshelf.open_shelf_folder"
bl_label = 'Run Function'
path : StringProperty()
def execute(self,context) :
open_folder(self.path)
return {'FINISHED'}
class CSHELF_OT_add_script(Operator) :
bl_idname = "customshelf.add_script"
bl_label = 'Add script to a shelf'
add_category : BoolProperty()
add_tab : BoolProperty()
new_category : StringProperty()
new_tab : StringProperty()
name : StringProperty()
description : StringProperty()
icon : StringProperty()
show_icons : BoolProperty()
def add_folder(self,context,op) :
folder = self.folder.add()
folder.name = self.name
#CustomShelfSettings
def remove_folder(self,context,op) :
bl_props = context.scene.CustomShelf
index = self.folders.find(self.folders_enum)
self.folders.remove(index)
def get_all_icons (self) :
ui_layout = bpy.types.UILayout
icons = ui_layout.bl_rna.functions["prop"].parameters["icon"].enum_items.keys()
prefixes = ('BRUSH_','MATCAP_','COLORSET_')
exception = ('BRUSH_DATA')
return [i for i in icons if not i.startswith(prefixes) or i in exception]
def get_icons(self,context,op) :
icons = [
["SCENE_DATA","RENDERLAYERS","MATERIAL_DATA","GROUP_UVS","TEXTURE","WORLD","SPEAKER","TEXT","NODETREE","NODE_INSERT_OFF","PARTICLES","SORTALPHA"],
["MODIFIER","MOD_WAVE","MOD_SUBSURF","MOD_FLUIDSIM","MOD_OCEAN","BLANK1",
"ARMATURE_DATA","BONE_DATA","GROUP_BONE"],
["SEQUENCE","CAMERA_DATA","SCENE","BLANK1",
"FILE_NEW","CONSOLE","BLENDER","APPEND_BLEND"],
["GROUP","MESH_CUBE","MESH_PLANE","MESH_CIRCLE","MESH_UVSPHERE","MESH_GRID","EMPTY_DATA","OUTLINER_DATA_MESH",
"LIGHT_SUN","LIGHT_SPOT","LIGHT"],
["TRIA_RIGHT_BAR","REC","PLAY","PREV_KEYFRAME","NEXT_KEYFRAME","PAUSE","X","ADD","REMOVE","RESTRICT_VIEW_OFF","RESTRICT_VIEW_ON","RESTRICT_SELECT_OFF",],
["BRUSH_DATA","GREASEPENCIL","LINE_DATA","PARTICLEMODE","SCULPTMODE_HLT","WPAINT_HLT","TPAINT_HLT","VIEWZOOM","HAND","KEY_HLT","KEY_DEHLT",],
["PLUGIN","SCRIPT","PREFERENCES",
"ACTION","SOLO_OFF",
"RNDCURVE","SNAP_ON",
"FORCE_WIND","COPY_ID","EYEDROPPER","AUTO","UNLOCKED","LOCKED",
"UNPINNED","PINNED","PLUGIN","HELP","GHOST_ENABLED","GHOST_DISABLED","COLOR",
"LINKED","UNLINKED","LINKED","ZOOM_ALL",
"FREEZE","STYLUS_PRESSURE","FILE_TICK",
"QUIT","RECOVER_LAST","TIME","PREVIEW_RANGE",
"OUTLINER","NLA","EDITMODE_HLT",
"BOIDS","RNA","CAMERA_STEREO"],
]
self.icons = icons
def set_icon(self,context,op) :
#bl_props = context.scene.CustomShelf
self.icon = op.icon
self.icons = []
def draw(self,context) :
bl_props = context.scene.CustomShelf
layout = self.layout
row = layout.row()
row.operator("customshelf.get_icons",text= '',icon = self.icon)
row.prop(self,"name",text ="")
col = layout.column(align = True)
for icon_list in self.icons :
i=0
for icon in icon_list :
if not i%12 :
row = col.row(align= True)
row.operator("customshelf.set_icon",icon=icon,emboss = False,text="").icon = icon
i += 1
row = col.row(align= True)
layout.prop(self,"description",text="")
layout.separator()
#Category Row
folder_row = layout.row(align = True)
folder_row.label(text="", icon='FILE_FOLDER')
folder_row.separator()
if not self.add_category :
folder_row.prop(bl_props,"category_enum",expand = True)
else :
folder_row.prop(self,"new_category",text='')
folder_row.prop(self,"add_category",icon = 'ADD',text='')
# Tabs row
tab_row = layout.row(align = True)
tab_row.label(text='', icon='MENU_PANEL')
tab_row.separator()
if not self.add_tab :
category_tabs = get_category_tabs(bl_props.category_enum)
for t in [t for t in category_tabs if t in self.tabs] :
tab_row.prop_enum(bl_props,"tab_enum",t)
else :
tab_row.prop(self,"new_tab",text='')
tab_row.prop(self,"add_tab",icon = 'ADD',text='')
#folder_row.operator("customshelf.remove_folder",icon = 'REMOVE',text='')
def write_script(self,f) :
keys = ['icon','description']
keys += [k for k in self.info if k not in keys]
f.write("info = "+dic_to_str(self.info,keys))
print(self.lines)
f.write('\n'.join(self.lines))
def execute(self,context) :
preferences = context.preferences
bl_props = context.scene.CustomShelf
addon_prefs = preferences.addons[__package__].preferences
if self.new_category :
category_path = get_category_path(self.new_category)
if not exists(category_path):
mkdir(new_category)
else :
category_path = get_category_path(bl_props.category_enum)
if self.new_tab :
tab_path = join(category_path, self.new_tab)
if not exists(tab_path):
mkdir(tab_path)
else :
tab_path = join(category_path,bl_props.tab_enum)
script_name = self.name.replace(' ','_').replace('-','_')
script_path = join(tab_path,script_name+'.py')
if os.path.exists(script_path):
os.remove(script_path)
self.info['icon'] = self.icon
self.info['description'] = self.description
with open(script_path,"w") as f :
self.write_script(f)
line_index = self.active_text.current_line_index
bpy.data.texts.remove(self.active_text)
text = bpy.data.texts.load(script_path)
text.current_line_index = line_index
context.space_data.text = text
read_shelves()
return {"FINISHED"}
def check(self,context) :
return True
def invoke(self,context,event) :
bl_props = context.scene.CustomShelf
self.active_text = context.space_data.text
self.info,self.lines = read_info([l.body for l in self.active_text.lines])
icon = "LONGDISPLAY"
if self.info.get('icon') :
icon = self.info["icon"]
description = "Some description"
if self.info.get('description') :
description = self.info["description"]
self.icon = icon
self.name = splitext(self.active_text.name)[0]
self.show_icons = False
self.description = description
self.add_shelf = False
self.new_shelf = ""
self.icons = []
operator("get_icons",{'execute':self.get_icons})
operator("set_icon",{'execute':self.set_icon,'icon':StringProperty()})
operator("add_folder",{'execute':self.add_folder})
#operator("remove_folder",{'execute':self.remove_folder})
self.tabs = get_tabs()
self.categories = get_categories()
CustomShelfSettings.category_enum = EnumProperty(items = [(i,i,"") for i in self.categories])
CustomShelfSettings.tab_enum = EnumProperty(items = [(i,i,"") for i in self.tabs])
if self.active_text.filepath :
tab_path = dirname(self.active_text.filepath)
category = basename(dirname(tab_path))
tab = basename(tab_path)
if category in self.categories :
bl_props.category_enum = category
if tab in self.tabs :
bl_props.tab_enum = tab
return context.window_manager.invoke_props_dialog(self,width = 500)

91
panels.py Normal file
View File

@ -0,0 +1,91 @@
from .utils import *
class CSHELF_PT_text_editor(bpy.types.Panel):
bl_space_type = "TEXT_EDITOR"
bl_region_type = "UI"
bl_category = "Dev"
bl_label = "Custom Shelf"
def draw(self, context):
layout = self.layout
shelves = bpy.context.scene.CustomShelf
row = layout.row(align = True)
row.operator("customshelf.add_script", text='Add Script',emboss=True,icon= "LONGDISPLAY")
row.separator()
"""
class CustomShelfPanel(bpy.types.Panel):
bl_label = "Custom Shelf"
bl_category = "SHELF"
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
# bl_context = "posemode"
def draw_header(self, context):
view = context.space_data
layout = self.layout
row=layout.row(align=True)
row.operator("customshelf.refresh", \
text='',emboss=False,icon= "FILE_REFRESH")
def draw(self, context):
layout = self.layout
window = context.window
scene = context.scene
rd = scene.render
shelves = bpy.context.scene.CustomShelf.folders
for key,value in sorted(shelves.items()) :
if key.startswith(('_','-')) :
continue
box = layout.box()
#box.alignment='EXPAND'
box_col = box.column(align =False)
if value['expand'] == True :
expandIcon = 'TRIA_DOWN'
else :
expandIcon = 'TRIA_RIGHT'
row = box_col.row(align = True)
row.operator("customshelf.expand",text=key.upper(), icon = expandIcon,emboss=False).folder = key
#subrow = row.row(align = True)
#subrow.alignment = 'CENTER'
#subrow.label(key.upper())
#col.separator()
if value['expand'] == True :
#text =' '.join([' ' for a in range(0,len(key))])
#row.prop(context.scene.CustomShelf,key,emboss = False,text=' ')
for script,settings in sorted(value['scripts'].items()) :
if script.startswith(('_','-')) :
continue
#print(s[1])
func = box_col.operator("customshelf.run_function", \
text=script,icon = settings['icon'])
func.path = settings['path']
#box_col.separator()
#layout.separator()
#col.separator()
"""

48
properties.py Normal file
View File

@ -0,0 +1,48 @@
from .utils import *
from bpy.props import *
class CustomShelfSettings(bpy.types.PropertyGroup) :
panel_list = []
#category_enum = EnumProperty(items = )
#tab_enum = EnumProperty(items = lambda s,c : [(f,f,"") for f in s.tabs])
#search = StringProperty(options = {"TEXTEDIT_UPDATE"})
#filter = BoolProperty(default = True)
#folders = PointerProperty(type= CustomShelfFolders)
#variables = bpy.props.PointerProperty(type= CustomShelfVariables)
class CustomShelfProps(bpy.types.PropertyGroup) :
pass
class AdditionnalShelves(bpy.types.PropertyGroup) :
path: StringProperty(name="Path",subtype = 'FILE_PATH')
class CustomShelfPrefs(bpy.types.AddonPreferences):
bl_idname = __package__
global_path = join(dirname(__file__),'shelves')
global_shelves : StringProperty(name="Shelves Path",subtype = 'DIR_PATH',default =global_path)
additionnal_shelves : CollectionProperty(type = AdditionnalShelves)
tag_filter : StringProperty()
def draw(self, context):
prefs = bpy.context.preferences.addons[__package__].preferences
layout = self.layout
layout.prop(self, "global_shelves")
row = layout.row(align = True)
row.operator("customshelf.add_shelves_folder",icon="ADD",text="")
row.label(text='Additionnal Shelves folder')
for i,shelf in enumerate(prefs.additionnal_shelves) :
row = layout.row(align = True)
row.prop(shelf, "path")
row.operator("customshelf.remove_shelves_folder",icon="REMOVE",text="").index = i
row = layout.row(align = True)
row.prop(self, "tag_filter")
row.operator_menu_enum("customshelf.set_tag_filter",'tag_filter',icon= "DOWNARROW_HLT",text='')

View File

@ -0,0 +1,103 @@
info = {
'icon': 'RECOVER_LAST',
'description': 'Duplicate current low as mid',
}
import bpy
from pathlib import Path
import re
# make a proxy through python
### / unused, just because it's interesting (but relinking break hide statement)
## https://blender.stackexchange.com/questions/157562/sorting-collections-alphabetically-in-the-outliner
def sort_collection(collection, case = False):
if collection.children is None: return
children = sorted (
collection.children,
key = lambda c: c.name if case else c.name.lower()
)
for child in children:
collection.children.unlink(child)
collection.children.link(child)
sort_collection(child)
def sort_all_collection():
# case_sensitive sort, (default is False)
case_sensitive = True
for scene in bpy.data.scenes:
sort_collection(scene.collection, case_sensitive)
### unused /
def set_collection(ob, collection, unlink=True) :
''' link an object in a collection and create it if necessary, if unlink object is removed from other collections'''
scn = bpy.context.scene
col = None
visible = False
linked = False
# check if collection exist or create it
for c in bpy.data.collections :
if c.name == collection : col = c
if not col : col = bpy.data.collections.new(name=collection)
# link the collection to the scene's collection if necessary
for c in scn.collection.children :
if c.name == col.name : visible = True
if not visible : scn.collection.children.link(col)
# check if the object is already in the collection and link it if necessary
for o in col.objects :
if o == ob : linked = True
if not linked : col.objects.link(ob)
# remove object from scene's collection
for o in scn.collection.objects :
if o == ob : scn.collection.objects.unlink(ob)
# if unlink flag we remove the object from other collections
if unlink :
for c in ob.users_collection :
if c.name != collection : c.objects.unlink(ob)
return col
# mid = None
# low = None
# for c in bpy.data.collections:
# if re.search(r'vetement.*_mid', c.name, re.I):
# mid = c
# if re.search(r'vetement.*_low', c.name, re.I):
# low = c
def backup_low_to_mid():
low = mid = None
for o in bpy.context.scene.objects:
if re.search(r'vetements?_mid', o.name, re.I):
mid = o
if re.search(r'vetements?_low', o.name, re.I):
low = o
if not low:
print('ERROR', 'low not found')
return
if not mid and low:
#create mid by duplicating low
mid = low.copy()
mid.name = low.name.replace('_low', '_mid')
mid.data = low.data.copy()# also copy data !
mid.data.name = low.data.name.replace('_low', '_mid')
col = set_collection(mid, mid.name)
# hide viewport
col.hide_viewport = True
else:
print('ERROR', 'mid already exists')
return
backup_low_to_mid()

View File

@ -0,0 +1,27 @@
info = {
'icon': 'SYNTAX_OFF',
'description': 'rename all lowercase (a bit dangerous, check if any break)',
}
import bpy
for c in bpy.data.collections:
new = c.name.lower()
if bpy.data.collections.get(new):
print(f'{new} exists!')
else:
c.name = new
for o in bpy.data.meshes:
new = o.name.lower()
if bpy.data.meshes.get(new):
print(f'{new} exists!')
else:
o.name = new
for o in bpy.data.objects:
new = o.name.lower()
if bpy.data.objects.get(new):
print(f'{new} exists!')
else:
o.name = new

View File

@ -0,0 +1,90 @@
info = {
'icon': 'MOD_CLOTH',
'description': 'Decimate cloth',
}
import bpy
from pathlib import Path
import re
def set_panel(panel):
'''take a panel name and apply it to properties zone'''
for area in bpy.context.screen.areas:
if area.type == 'PROPERTIES':
for space in area.spaces:
if space.type == 'PROPERTIES':
space.context = panel
return (1)
return (0)
def get_panel():
'''return active panel name of the properties zone'''
for area in bpy.context.screen.areas:
if area.type == 'PROPERTIES':
for space in area.spaces:
if space.type == 'PROPERTIES':
return(space.context)
return (0)
def get_override():
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'PROPERTIES':
#for region in area.regions:
# if region.type == 'WINDOW':
return {'window': window, 'screen': screen, 'area': area}
def simplify_cloth(poly_target):
if not get_override():
print('ERROR', f'Need a properties windows editor open !')
return
obj = bpy.context.object
mods = obj.modifiers
polycount = len(obj.data.polygons)
if polycount < poly_target:
print('ERROR', f'Already lowpoly ({polycount} polygons)')
return
target_ratio = poly_target / polycount
print('target_ratio: ', target_ratio)
sdef = None
for m in mods:
if m.type == 'SURFACE_DEFORM':
sdef = m
break
if not sdef:
print('ERROR', 'no surface deform found')
return
set_panel('MODIFIER')
properties_override = get_override()
if sdef.is_bound:
bpy.ops.object.surfacedeform_bind(properties_override, modifier="SurfaceDeform")
# create decimate and move to top
decim = obj.modifiers.new('Decimate', 'DECIMATE')
bpy.ops.object.modifier_move_to_index(modifier="Decimate", index=0)
# use target ratio
decim.ratio = target_ratio
# apply
bpy.ops.object.modifier_apply(modifier="Decimate")
# rebind
bpy.ops.object.surfacedeform_bind(properties_override, modifier="SurfaceDeform")
simplify_cloth(15000)

View File

@ -0,0 +1,275 @@
info = {
'icon' : 'PHYSICS',
'description' : 'Copy dynamic physics object from selected instance_collection',
}
import bpy
from os.path import abspath, relpath, dirname, basename, join
from time import time
C = bpy.context
D = bpy.data
scene = C.scene
def set_collection(ob, collection, unlink=True) :
''' link an object in a collection and create it if necessary, if unlink object is removed from other collections'''
scn = bpy.context.scene
col = None
visible = False
linked = False
# check if collection exist or create it
for c in bpy.data.collections :
if c.name == collection : col = c
if not col : col = bpy.data.collections.new(name=collection)
# link the collection to the scene's collection if necessary
for c in scn.collection.children :
if c.name == col.name : visible = True
if not visible : scn.collection.children.link(col)
# check if the object is already in the collection and link it if necessary
for o in col.objects :
if o == ob : linked = True
if not linked : col.objects.link(ob)
# remove object from scene's collection
for o in scn.collection.objects :
if o == ob : scn.collection.objects.unlink(ob)
# if unlink flag we remove the object from other collections
if unlink :
for c in ob.users_collection :
if c.name != collection : c.objects.unlink(ob)
def append_stuff(blendfile, section, obj):
filepath = blendfile
directory = join(blendfile, section)#join(blendfile, section)
filename = obj
print('Append')
print('filepath: ', filepath)
print('directory: ', directory)
print('filename: ', filename)
print(f'''using:
bpy.ops.wm.append(
filepath='{filepath}',
filename='{filename}',
directory='{directory}',
active_collection=True,
)
''')
bpy.ops.wm.append(
filepath=filepath,
filename=filename,
directory=directory,
filter_blender=True, filter_backup=False, filter_image=False, filter_movie=False,
filter_python=False, filter_font=False, filter_sound=False, filter_text=False,
filter_archive=False, filter_btx=False, filter_collada=False, filter_alembic=False,
filter_folder=True, filter_blenlib=True, filemode=1, display_type='DEFAULT', sort_method='FILE_SORT_ALPHA',
link=False, autoselect=True, active_collection=True,#default -> active_collection=True
instance_collections=False, set_fake=False,
use_recursive=True
)
#return {'finished'}
## data way (not working as is...)
'''
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
data_to.objects = [o for o in data_from.objects if o == obj]
#link object to current scene
for obj in data_to.objects:
if obj is not None:
C.scene.collection.objects.link(obj)
'''
def set_soft_body(ob, mod, bake=False):
if bake:
print(f'baking {ob.name} > {mod.name}')
ptc = mod.point_cache
name = ptc.name
override = {'scene': bpy.context.scene,
'active_object': ob,#no sure of this line.. .
'point_cache': ptc}
if bake:
if name:#kill point cache related to point cache name
# delete_point_cache(name)
bpy.ops.ptcache.free_bake(override)
## bpy.ops.ptcache.free_bake_all(override)
#ptc.is_outdated = True # read-only
ptc.frame_start = scene.frame_start
ptc.frame_end = scene.frame_end
# ptc.filepath = join(dirname(D.filepath), splitext(basename)[0]+'_cache')
if not ptc.use_disk_cache:
ptc.use_disk_cache = True
print('enabling use disk cache')
if bake:
print('>>> baking')
start = time()
#bpy.ops.ptcache.bake(override, bake=False)# bake to current frame
bpy.ops.ptcache.bake(override, bake=True)
print(f'Done {time()-start:.3f}')
def set_particle_system(ob, mod, bake=False):
if bake:
print(f'baking {ob.name} > {mod.name}')
ptc = mod.particle_system.point_cache
ptc.frame_start = scene.frame_start
ptc.frame_end = scene.frame_end
# ptc.filepath = join(dirname(D.filepath), splitext(basename)[0]+'_cache')
if not ptc.use_disk_cache:
ptc.use_disk_cache = True
print('enabling use disk cache')
if bake:
override = {'scene': bpy.context.scene,
'active_object': ob,#no sure of this line.. .
'point_cache': ptc}
print('>>> baking')
start = time()
#bpy.ops.ptcache.bake(override, bake=False)# bake to current frame
bpy.ops.ptcache.bake(override, bake=True)
print(f'Done {time()-start:.3f}')
def bake_mods(ob, bake=False):
for mod in ob.modifiers:
if mod.type == 'SOFT_BODY':
set_soft_body(ob, mod, bake)
if mod.type == 'PARTICLE_SYSTEM':
set_particle_system(ob, mod, bake)
def clear_obj_driver(ob):
'''delete visibility driver if any'''
print('clearing driver', ob.name)
ob.driver_remove('hide_viewport')
ob.driver_remove('hide_render')
ob.hide_viewport = False
ob.hide_render = False
def append_physics_obj(selected=True, purge=False, bake=False):
'''On selection or all scene'''
current_active_col = C.view_layer.active_layer_collection
if selected:
obpool = C.selected_objects
else:
obpool = bpy.context.scene.objects
pool = [o for o in obpool if o.type == 'EMPTY' and o.instance_collection]#PARTICLE_SYSTEM
if selected:
for proxyarm in [o for o in obpool if o.type == 'ARMATURE' and o.name.endswith('_proxy')]:
inst = proxyarm.proxy_collection
if inst not in pool: pool.append(inst)
linked = []
#[m for m in o.modifiers if m.type == 'SOFT_BODY']
for instance in pool:
print("instance", instance.name)#Dbg
for ob in instance.instance_collection.all_objects:
# print("ob", ob.name)#Dbg
if ob.type in ('MESH', 'CURVE'):
for mod in ob.modifiers:
if mod.type in ('SOFT_BODY', 'PARTICLE_SYSTEM'):
print(f'{ob.name} > {mod.name} ({mod.type})')
## check if object already exists in object base.
dynob_name = ob.name + '_DYN'
phycol = D.collections.get('Physics_sim')#Dynamic_physics
theob = None
if phycol: theob = phycol.objects.get(dynob_name)
if theob:
print(f'{dynob_name} already as object in scene')
continue
'''
if purge:#delete whats in physics collection, crashy as hell..
print(f'--> purge: deleting {dynob_name}')
D.objects.remove(theob)
else:
continue
'''
## get library path
libpath = abspath(bpy.path.abspath(instance.instance_collection.library.filepath))
print('libpath: ', libpath)
#libpath = instance.instance_collection.library.filepath
#objpath = join(libpath, 'Object', ob.name)
if not phycol:
phycol = D.collections.new('Physics_sim')
C.scene.collection.children.link(phycol)
## make it active
C.view_layer.active_layer_collection = C.view_layer.layer_collection.children['Physics_sim']
## Append individual object, (physics object with a different name from the blend file, else it 'links' to the one withi instance_collection)
#append_stuff(libpath, 'Object', ob.name)
## test copy from existing object
newob = ob.copy()
#newob.make_local()
#newob.data.make_local()
set_collection(newob, 'Physics_sim')
linked.append(newob)
# newob.name = newob.name + '_DYN'
#clear_obj_driver(newob)##crash when clearing driver of the copied object immediately. Do it later
'''
## get the newly appended object
newob = phycol.objects.get(ob.name)#not rename currently
if not newob:
print(f'problem, did not found "{dynob_name}" in physics collection')
continue
linked.append(newob)
'''
# setup proxy visibility...
if instance.name == 'jenny':
proxy = C.scene.objects.get('jenny_proxy')
hairdyn = proxy.pose.bones['root']['_RNA_UI'].get('hair_dynamic')
if hairdyn:
if hairdyn['max'] < 2:
#setmax
proxy.pose.bones['root']['_RNA_UI']['hair_dynamic']['max'] = 2
proxy.pose.bones['root']['_RNA_UI']['hair_dynamic']['soft_max'] = 2
#set val
proxy.pose.bones['root']['hair_dynamic'] = 2
if instance.name == 'boom':
proxy = C.scene.objects.get('boom_proxy')
hairdyn = proxy.pose.bones['root']['_RNA_UI'].get('hair_placeholder')
if hairdyn:
if hairdyn['max'] < 3:
#setmax
proxy.pose.bones['root']['_RNA_UI']['hair_placeholder']['max'] = 3
proxy.pose.bones['root']['_RNA_UI']['hair_placeholder']['soft_max'] = 3
#set val
proxy.pose.bones['root']['hair_placeholder'] = 3
for o in linked:#linked
print(f'-- linked: {o.name}')
# o.make_local()#localize then modify, usually no need to make local if appended
o.name = o.name + '_DYN'
clear_obj_driver(o)
print(o.name)
#set_collection(o, 'Physics_sim')
bake_mods(o, bake=False)
#reset active collection
C.view_layer.active_layer_collection = current_active_col
print()
append_physics_obj(selected=True, purge=False, bake=False)#purge bugged

View File

@ -0,0 +1,143 @@
info = {
'icon' : 'ACTION_TWEAK',
'description' : 'Bake action of selected objects',
'selection' : True
}
import bpy
from os.path import abspath, relpath, dirname, basename, join
from time import time
C = bpy.context
D = bpy.data
scene = C.scene
def set_collection(ob, collection, unlink=True) :
''' link an object in a collection and create it if necessary, if unlink object is removed from other collections'''
scn = bpy.context.scene
col = None
visible = False
linked = False
# check if collection exist or create it
for c in bpy.data.collections :
if c.name == collection : col = c
if not col : col = bpy.data.collections.new(name=collection)
# link the collection to the scene's collection if necessary
for c in scn.collection.children :
if c.name == col.name : visible = True
if not visible : scn.collection.children.link(col)
# check if the object is already in the collection and link it if necessary
for o in col.objects :
if o == ob : linked = True
if not linked : col.objects.link(ob)
# remove object from scene's collection
for o in scn.collection.objects :
if o == ob : scn.collection.objects.unlink(ob)
# if unlink flag we remove the object from other collections
if unlink :
for c in ob.users_collection :
if c.name != collection : c.objects.unlink(ob)
def set_soft_body(ob, mod, bake=False):
if bake:
print(f'baking {ob.name} > {mod.name}')
ptc = mod.point_cache
name = ptc.name
override = {'scene': bpy.context.scene,
'active_object': ob,#no sure of this line.. .
'point_cache': ptc}
if bake:
if name:#kill point cache related to point cache name
bpy.ops.ptcache.free_bake(override)
## bpy.ops.ptcache.free_bake_all(override)
#ptc.is_outdated = True # read-only
ptc.frame_start = scene.frame_start
ptc.frame_end = scene.frame_end
if not ptc.use_disk_cache:
ptc.use_disk_cache = True
print('enabling use disk cache')
if bake:
print('>>> baking')
start = time()
#bpy.ops.ptcache.bake(override, bake=False)# bake to current frame
bpy.ops.ptcache.bake(override, bake=True)
print(f'Done {time()-start:.3f}')
def set_particle_system(ob, mod, bake=False):
if bake:
print(f'baking {ob.name} > {mod.name}')
ptc = mod.particle_system.point_cache
ptc.frame_start = scene.frame_start
ptc.frame_end = scene.frame_end
# ptc.filepath = join(dirname(D.filepath), splitext(basename)[0]+'_cache')
if not ptc.use_disk_cache:
ptc.use_disk_cache = True
print('enabling use disk cache')
if bake:
override = {'scene': bpy.context.scene,
'active_object': ob,#no sure of this line.. .
'point_cache': ptc}
print('>>> baking')
start = time()
#bpy.ops.ptcache.bake(override, bake=False)# bake to current frame
bpy.ops.ptcache.bake(override, bake=True)
print(f'Done {time()-start:.3f}')
def bake_mods(ob, bake=False):
for mod in ob.modifiers:
if mod.type == 'SOFT_BODY':
set_soft_body(ob, mod, bake)
if mod.type == 'PARTICLE_SYSTEM':
set_particle_system(ob, mod, bake)
def bake_action(selected=True):
if selected:
pool = []
for o in C.selected_objects:
if o.animation_data.action:
print(f'{o.name} already has an action')
o.select_set(False)
continue
else:
pool.append(o)
if not pool:
print('no object ready for action bake')
return
else:#go in physics ob collection
phycol = D.collections.get('Physics_sim')
if not phycol:
print('No physics collection')
return
pool = [o for o in phycol.all_objects if not o.hide_viewport and not o.animation_data.action]
#redefine selection
bpy.ops.object.select_all(action='DESELECT')
for o in pool:
o.select_set(True)
C.view_layer.objects.active = o
# get viewport override ? (no need if launched from custom shelf)
print('Baking action...')
# launch action baking
bpy.ops.nla.bake(frame_start=scene.frame_start, frame_end=scene.frame_end, only_selected=False, visual_keying=True, clear_constraints=True, clear_parents=True, bake_types={'OBJECT'})
bake_action(selected=info['selection'])
print('EOF')

View File

@ -0,0 +1,100 @@
info = {
'icon' : 'PHYSICS',
'description' : 'Bake modifier cache of selected objects',
'selection' : True
}
import bpy
from os.path import abspath, relpath, dirname, basename, join
from time import time
C = bpy.context
D = bpy.data
scene = C.scene
def set_soft_body(ob, mod, bake=False):
if bake:
print(f'baking {ob.name} > {mod.name}')
ptc = mod.point_cache
name = ptc.name
override = {'scene': bpy.context.scene,
'active_object': ob,#no sure of this line.. .
'point_cache': ptc}
if bake:
if name:#kill point cache related to point cache name
bpy.ops.ptcache.free_bake(override)
## bpy.ops.ptcache.free_bake_all(override)
#ptc.is_outdated = True # read-only
ptc.frame_start = scene.frame_start
ptc.frame_end = scene.frame_end
if not ptc.use_disk_cache:
ptc.use_disk_cache = True
print('enabling use disk cache')
if bake:
print('>>> baking')
start = time()
#bpy.ops.ptcache.bake(override, bake=False)# bake to current frame
bpy.ops.ptcache.bake(override, bake=True)
print(f'Done {time()-start:.3f}')
def set_particle_system(ob, mod, bake=False):
if bake:
print(f'baking {ob.name} > {mod.name}')
ptc = mod.particle_system.point_cache
ptc.frame_start = scene.frame_start
ptc.frame_end = scene.frame_end
# ptc.filepath = join(dirname(D.filepath), splitext(basename)[0]+'_cache')
if not ptc.use_disk_cache:
ptc.use_disk_cache = True
print('enabling use disk cache')
if bake:
override = {'scene': bpy.context.scene,
'active_object': ob,#no sure of this line.. .
'point_cache': ptc}
print('>>> baking')
start = time()
#bpy.ops.ptcache.bake(override, bake=False)# bake to current frame
bpy.ops.ptcache.bake(override, bake=True)
print(f'Done {time()-start:.3f}')
def bake_mods(ob, bake=False):
for mod in ob.modifiers:
if mod.type == 'SOFT_BODY':
set_soft_body(ob, mod, bake)
if mod.type == 'PARTICLE_SYSTEM':
set_particle_system(ob, mod, bake)
def bake_mod_cache(selected=True):
phycol = D.collections.get('Physics_sim')
if not phycol:
print('No physics collection')
return
if selected:
pool = []
for o in C.selected_objects:
if not phycol in o.users_collection:
print(f'{o.name} is not in Physics_sim collection' )
continue
pool.append(o)
if not pool:
print('no object ready for cache baking')
return
else:#go in physics ob collection
pool = [o for o in phycol.all_objects if not o.hide_viewport and not o.animation_data.action]
#automate preroll ?
for o in pool:
bake_mods(o, bake=True)
bake_mod_cache(selected=info['selection'])
print('EOF')

View File

@ -0,0 +1,19 @@
info = {
'icon' : 'X',
'description' : 'Delete everything in collection Physics_sim',
}
import bpy
from os.path import abspath, relpath, dirname, basename, join
from time import time
C = bpy.context
D = bpy.data
scene = C.scene
phycol = D.collections.get('Physics_sim')#Dynamic_physics
if phycol:
for ob in phycol.all_objects:
D.objects.remove(ob)
D.collections.remove(phycol)

View File

@ -0,0 +1,144 @@
import bpy
from mathutils import Vector, Matrix
from math import radians, degrees
from random import uniform
import bmesh
C = bpy.context
# Assumes we have a mesh object selected in OBJECT mode
def transfer_value(Value, OldMin, OldMax, NewMin, NewMax):
'''map a value from a range to another (transfer/translate value)'''
return (((Value - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin
def chunks(lst, n):
"""Yield successive n-sized chunks from lst."""
for i in range(0, len(lst), n):
yield lst[i:i + n]
ob = C.object
M = ob.matrix_world
# Get the active mesh
me = ob.data
## get active group
#C.vertex_groups.active_index
if ob.type != 'MESH':
print('ERROR : not a mesh')
mode = bpy.context.mode
if mode == 'EDIT_MESH':
#me = bpy.context.edit_object.data
bm = bmesh.from_edit_mesh(me) #get Bmesh from edit
elif mode == 'OBJECT':
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(me) # fill it in from a Mesh
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
#deselect everything first
for f in bm.edges:f.select = False
for e in bm.edges:e.select = False
for v in bm.verts:v.select = False
# Modify the BMesh, can do anything here...
chunks = []
sc = 1
ec = 0
for v in bm.verts:
v.select_set(False)
#v.co.x += 1.0
if len(v.link_edges) == 1:#vertices chain tip
#sc+=1
#if sc == 2:
# sc = 0
chunks.append(v.index)
if len(chunks)%2 != 0:
print("list not pair")
for i in chunks:
bm.verts[i].select_set(True)
n = 2
pairs = [chunks[i:i + n] for i in range(0, len(chunks), n)]
#print("pairs", pairs)#Dbg
vlists = []
cursorloc = bpy.context.scene.cursor.location
for p in pairs:
vlen = p[1] - p[0]#number of vertices in chunk
#get_closest to 3d cursor
if (cursorloc - M @ bm.verts[p[0]].co).length < (cursorloc - M @ bm.verts[p[1]].co).length:
print(f'{p[0]} to {p[1]}')
bm.verts[p[1]].select = False
vlists.append( [i for i in range(p[0], p[1]+1)] )
else:# Last point is closer to cursor (so invert direction)
print(f'{p[1]} to {p[0]}')
bm.verts[p[0]].select = False
vlists.append( [i for i in reversed(range(p[0], p[1]+1))] )
if mode == 'EDIT_MESH':
bmesh.update_edit_mesh(me, True)
elif mode == 'OBJECT':
bm.to_mesh(me)
bm.free()
# tweak vertex group (need to be in object mode)
'''
## classic progressive
for vl in vlists:
vnum = len(vl)
for i, vid in enumerate(vl):
weight = i/vnum# progressive
weight = abs(weight-1)# reverse
weight = transfer_value(weight, 0, 1, 0.8, 1)# clamp between two value
C.object.vertex_groups.active.add([vid], weight, "REPLACE")
'''
## with a divider to limit
## ex with 5 points:
## divider 2 : 0, 0, 0 , 0.5, 1.0
## divider 3 : 0, 0, 0.33 , 0.66, 1.0
percentage = 0.4 #(0 for full progressive)
#divider = 3
for vl in vlists:
vnum = len(vl)
# limit = int(vnum/divider)
limit = int(vnum*percentage)
rest = vnum - limit
mini = uniform(0.86, 0.96)#random minimum
for i, vid in enumerate(vl):
#weight = i/vnum
w = weight = (i - limit)/(vnum-limit-1)
if weight < 0: weight=0
weight = abs(weight-1)#reverse
# mini = 0.9
weight = transfer_value(weight, 0, 1, mini, 1)# clamp between two last value
print(i, w, "->", weight)#Dbg
C.object.vertex_groups.active.add([vid], weight, "REPLACE")
print("vnum", vnum)#Dbg
print("random minimum", mini)#Dbg
print("limit", limit)#Dbg
print("rest", rest)#Dbg
print("full", rest + limit)#Dbg
#too smooth with 0.8 on fast movements
## transfer_value(weight, 0, 1, 0.8, 1)

View File

@ -0,0 +1,132 @@
## create visibility on objects or armature selection from a bone of a rig ##
import bpy
def add_driver(source, target, prop, dataPath, index = -1, negative = False, func = ''):
''' Add driver to source prop (at index), driven by target dataPath '''
source.driver_remove(prop, index)
if index != -1:
d = source.driver_add( prop, index ).driver
else:
d = source.driver_add( prop ).driver
v = d.variables.new()
v.targets[0].id = target
v.targets[0].data_path = dataPath
d.expression = func + "(" + v.name + ")" if func else v.name
d.expression = d.expression if not negative else "-1 * " + d.expression
def create_hide_custom_prop(src_object, prop_name, prop_bone = ''):
'''
add source propertie with boolean option
place the hide prop on src_object with name prop_name
'''
rig = bpy.data.objects.get(src_object)
if not rig:
print(f"No objects named {src_object}")
return 1
if rig.type != 'ARMATURE':
print(f"Not an armature : {src_object}")
return 1
#add target bone
if prop_bone:
holder = rig.pose.bones.get(prop_bone)
else:
holder = rig.pose.bones.get('root')
if not holder:
print(f'problem finding bone {prop_bone} (or root)')
return 1
# create
if not holder.get('_RNA_UI'):
holder['_RNA_UI'] = {}
if not prop_name in holder.keys() :
holder[prop_name] = 0
holder['_RNA_UI'][prop_name] = {"default": 0,"min":0,"max":1,"soft_min":0,"soft_max":1}
else:
print(f'{prop_name} : already exists on root key')
return
return 0
def drive_selection_visibility(rig, prop_name, prop_bone = ''):
# add driver on selection
prefixs = ('MCH','DEF','ORG', 'WGT')
rig = bpy.data.objects.get(src_object)
if not rig:
print(f"No objects named {src_object}")
return 1
if rig.type != 'ARMATURE':
print(f"Not an armature : {src_object}")
return 1
#add target bone
if not prop_bone:
prop_bone = 'root'
if not rig.pose.bones.get(prop_bone):
print(f'no bones {prop_bone} on rig {rig.name}')
return 1
meshes = [i for i in bpy.context.selected_objects if i.type in ('MESH','CURVE','TEXT') and not i.name.startswith(('WGT', 'WDGT'))]
armatures = [i for i in bpy.context.selected_objects if i.type == 'ARMATURE']
if bpy.context.mode == 'POSE':
obarm = bpy.context.active_object
for bone in bpy.context.selected_pose_bones_from_active_object:
prop = 'bones["%s"].hide'%bone.name
index = -1
layer = bone.bone.layers
protect_layer = rig.data.layers_protected
### dont check for protected, strictly use selection.
# if bone.name.startswith(prefixs) or any([i==j==1 for i,j in zip(layer,protect_layer)]) :
# print(f'Skipped : Prefixed or protected bone : {bone.name}')
# rig.data.driver_remove(prop, index)
# continue
print(f'New : Driver on bone {bone.name}')
add_driver(obarm.data, rig, prop, f'pose.bones["{prop_bone}"]["{prop_name}"]', index)
return
for ob in meshes :
print('Object : ', obarm.name)
add_driver(ob, rig, 'hide_viewport', f'pose.bones["{prop_bone}"]["{prop_name}"]', -1)
add_driver(ob, rig, 'hide_render', f'pose.bones["{prop_bone}"]["{prop_name}"]', -1)
for obarm in armatures:
print('Armature : ', obarm.name)
## mask armature object
## add_driver(obarm, rig, 'hide_viewport', f'pose.bones["{prop_bone}"]["{prop_name}"]', -1)
## bette mask pose bones since its a proxy...
for bone in obarm.pose.bones :
prop = 'bones["%s"].hide'%bone.name
index = -1
layer = bone.bone.layers
protect_layer = rig.data.layers_protected
if bone.name.startswith(prefixs) or any([i==j==1 for i,j in zip(layer,protect_layer)]) :
print(f'Skipped : Prefixed or protected bone : {bone.name}')
rig.data.driver_remove(prop, index)
else :
print(f'New : Driver on bone {bone.name}')
add_driver(obarm.data, rig, prop, f'pose.bones["{prop_bone}"]["{prop_name}"]', index)
### ----
## write the name of the rig source (will put the propertie on the root of this armature)
prop_rig = 'name_of_the_rig'
## write the name of the propertie to attach
prop_name = "hide_something"#'hide_headband'
## prop_bone (bone holding the propertie), 'root' if left string empty.
prop_bone = ''
create_hide_custom_prop(prop_rig, prop_name, prop_bone = prop_bone)
drive_selection_visibility(prop_rig, prop_name, prop_bone = prop_bone)

View File

@ -0,0 +1,32 @@
# coding: utf-8
import bpy
import bmesh
ob = bpy.context.object
me = ob.data
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(me) # fill it in from a Mesh
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
#deselect everything first
for f in bm.edges:f.select = False
for e in bm.edges:e.select = False
for v in bm.verts:v.select = False
#Checker deselect on each separate mesh portion
ct = 0
for v in bm.verts:
ct+=1
if len(v.link_edges) == 1:#star/end of chain
# v.select_set(False)#already deselected
ct = 0#reset ct for next
else:
print(v.index, 'select', ct%2)
v.select_set(ct%2)
bm.to_mesh(me)
bm.free()

View File

@ -0,0 +1,286 @@
bl_info = {
"name": "FTP sync",
"author": "Christophe Seux",
"version": (1, 0),
"blender": (2, 81, 0),
"warning": "",
"wiki_url": "",
"category": "User",
}
import bpy
from bpy.types import Operator, AddonPreferences
from bpy.props import FloatVectorProperty
import os
from os.path import *
import re
### EXCLUSIONS
EXCLUSIONS = ['.*','*.db','*.blend1','*~','*sync-conflict*','*.DS_Store']
'''
TO DO
Accepts empty folders, option for type of sync (override etc)
'''
class Preferences(AddonPreferences):
bl_idname = __name__
domain : bpy.props.StringProperty(name="FTP Domain")
login : bpy.props.StringProperty(name="FTP Login")
password : bpy.props.StringProperty(name="FTP Password",subtype='PASSWORD')
ftp_folder : bpy.props.StringProperty(name="FTP folder", default = '/')
local_folder : bpy.props.StringProperty(name="Local Folder", subtype='DIR_PATH')
active_connection : bpy.props.BoolProperty(name="Active Connection", default = True)
sync_type : bpy.props.EnumProperty(
name="Sync Type",
items = [(i,i.title(),"") for i in ("ONLY_NEW", "OVERRIDE", "MORE_RECENT")],
default = "ONLY_NEW")
def draw(self, context):
layout = self.layout
layout.prop(self, "domain")
layout.prop(self, "login")
layout.prop(self, "password")
layout.prop(self, "ftp_folder")
layout.prop(self, "active_connection")
layout.prop(self, "local_folder")
def connect_to_ftp(domain, login, passwd, active = True) :
from ftplib import FTP
ftp = FTP(domain,timeout = 10)
ftp.login(user = login, passwd = passwd)
ftp.set_pasv(active)
return ftp
def is_exclude(name, patterns) :
from fnmatch import fnmatch
if not isinstance(patterns, (list,tuple)) :
patterns = [patterns]
return any([fnmatch(name, p) for p in patterns])
def get_ftp_files(ftp, root, exclusions) :
files = []
ftp.cwd(root)
for k,v in ftp.mlsd() :
if is_exclude(k, exclusions) : continue
path = '/'.join([root,k])
if v['type'] == 'file' :
files.append(path)
elif v['type'] == 'dir' :
files+= get_ftp_files(ftp, path, exclusions)
return sorted(files)
def get_files(root, exclusions) :
'''Recursively get files in passed directory if not in exclusion list'''
files = []
for f in os.scandir(root) :
if is_exclude(f.path, exclusions) : continue
if f.is_file() :
files.append(f.path)
elif f.is_dir() :
files+= get_files(f.path, exclusions)
return sorted(files)
def createDirs(ftp, dirpath):
from ftplib import error_perm
"""
Create dir with subdirs (progressive dir creation).
:param ftp: connected FTP
:param dirpath: path (like 'test/test1/test2')
:type ftp: FTP
:type dirpath: str
:rtype: None
"""
dirpath = dirpath.replace('\\', '/')
tmp = dirpath.split('/')
dirs = []
for _ in tmp:
if len(dirs) == 0:
dirs.append(_)
continue
dirs.append(dirs[-1] + '/' + _)
for _ in dirs:
try:
ftp.mkd(_)
except error_perm as e:
e_str = str(e)
if '550' in e_str and 'File exists' in e_str:
continue
def exist_ftp_file(ftp, filepath) :
filepath = filepath.replace('\\', '/')
#ftp.cwd('/')
if filepath.startswith('/') : filepath = filepath[1:]
split_path = filepath.split('/')
for i,path in enumerate(split_path) :
list_dir = {k:v['type'] for k,v in ftp.mlsd()}
#print(i,path, list(list_dir.keys()))
if path in list_dir.keys() :
if i == len(split_path)-1 :
return True
elif list_dir[path] == 'dir' :
ftp.cwd(path)
else :
return False
else :
return False
class SendToFtp(Operator):
"""Send To FTP"""
bl_idname = "ftpsync.send"
bl_label = "Send to FTP"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
prefs = context.preferences.addons[__name__].preferences
ftp = connect_to_ftp(prefs.domain, prefs.login, prefs.password, prefs.active_connection)
print('\nStart Uploading to the FTP')
print('Source Folder :', prefs.local_folder)
ftp_folder = prefs.ftp_folder.replace('\\','/').strip('/')
if not ftp_folder.startswith('/') : ftp_folder = '/'+ftp_folder
local_folder = prefs.local_folder.replace('\\','/')
for f in get_files(local_folder, EXCLUSIONS) :
'''recursively get files in directory if not in exclusion list'''
ftp.cwd(ftp_folder)
dst_file = re.sub(local_folder, '', f.replace('\\','/')).strip('/')
if prefs.sync_type == 'ONLY_NEW' and exist_ftp_file(ftp,dst_file) :
continue
ftp.cwd(ftp_folder)
createDirs(ftp,dirname(dst_file))
try :
with open(f, 'rb') as fp:
ftp.storbinary('STOR '+dst_file, fp)
print('New file :', dst_file)
except :
print('Impossible to copy %s'%fp)
ftp.close()
return {'FINISHED'}
class GetFromFtp(Operator,):
"""Create a new Mesh Object"""
bl_idname = "ftpsync.get"
bl_label = "Get from FTP"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
prefs = context.preferences.addons[__name__].preferences
ftp = connect_to_ftp(prefs.domain, prefs.login, prefs.password, prefs.active_connection)
if not prefs.local_folder :
print('You have to set the local folder in your prefs')
return {'FINISHED'}
print('\nStart Downloading from FTP')
print('Destination Folder :', prefs.local_folder)
ftp_folder = prefs.ftp_folder.replace('\\','/').strip('/')
if not ftp_folder.startswith('/') : ftp_folder = '/'+ftp_folder
local_folder = prefs.local_folder.replace('\\','/')
for f in get_ftp_files(ftp, ftp_folder, EXCLUSIONS) :
dst_file = join(local_folder, re.sub(ftp_folder,'',f.replace('\\','/')).strip('/'))
if prefs.sync_type == 'ONLY_NEW' and exists(dst_file) :
continue
if not exists(dirname(dst_file)) :
os.makedirs(dirname(dst_file))
with open(dst_file, 'wb') as fp:
ftp.retrbinary('RETR '+f, fp.write)
print('New file :', dst_file)
ftp.close()
return {'FINISHED'}
class Menu(bpy.types.Menu):
bl_label = "ADM"
bl_idname = "ADM_MT_menu"
def draw(self, context):
layout = self.layout
prefs = context.preferences.addons[__name__].preferences
layout.operator("ftpsync.get", icon='IMPORT')
layout.operator("ftpsync.send", icon='EXPORT')
layout.prop(prefs, "sync_type", text = '')
def menu_draw(self,context):
self.layout.menu("ADM_MT_menu")
cls = [Preferences, SendToFtp, GetFromFtp,Menu]
def register():
bpy.types.TOPBAR_MT_editor_menus.append(menu_draw)
for c in cls :
bpy.utils.register_class(c)
def unregister():
bpy.types.TOPBAR_MT_editor_menus.remove(menu_draw)
for c in cls :
bpy.utils.unregister_class(c)
if __name__ == "__main__":
register()

View File

@ -0,0 +1,51 @@
# coding: utf-8
import bpy
import bmesh
from math import degrees, radians
ob = bpy.context.object
me = ob.data
if ob.type != 'MESH':
print('ERROR : not a mesh')
mode = bpy.context.mode
if mode == 'EDIT_MESH':
#me = bpy.context.edit_object.data
bm = bmesh.from_edit_mesh(me) #get Bmesh from edit
elif mode == 'OBJECT':
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(me) # fill it in from a Mesh
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
#deselect everything first
for f in bm.edges:f.select = False
for e in bm.edges:e.select = False
for v in bm.verts:v.select = False
#keep vertex that mark angle above this degree tolerance
degree_tolerance = 20
tol = radians(degree_tolerance)
#Checker
ct = 0
for v in bm.verts:
if len(v.link_edges) == 2:#star/end of chain
if v.calc_edge_angle() < tol:
#v.select_set(True)#full select
v.select_set(ct%2)#checker select
else:
ct = 0#reset counter
ct+=1
if mode == 'EDIT_MESH':
bmesh.update_edit_mesh(me, True)
elif mode == 'OBJECT':
bm.to_mesh(me)
bm.free()

View File

@ -0,0 +1,17 @@
vl = [10,11,12,13,14,15,16,17,18,19,20,21,22,23]
vl = [10,11,12,13,14]
print()
divider = 0.8
vnum = len(vl)
#limit = int(vnum/divider)
limit = int(vnum*divider)
rest = vnum - limit
print("vnum", vnum)#Dbg
print("limit", limit)#Dbg
print("rest", rest)#Dbg
for i, vid in enumerate(vl):
total = (vnum-limit-1)
w = (i - limit)/total
print(i, f'{w:.2f}')#Dbg

182
utils.py Normal file
View File

@ -0,0 +1,182 @@
import bpy
import os
from os import listdir,mkdir,scandir
from os.path import join,dirname,splitext,isdir,exists,basename
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import *
import subprocess
import json
id_type = {"Action" : "actions", "Armature":"armatures", "Brush":"brushes", "CacheFile":"cache_files", "Camera":"cameras",
"Curve":"curves", "FreestyleLineStyle":"linestyles", "GreasePencil":"grease_pencil", "Group":"groups",
"Image":"images", "Key":"shape_keys", "Light":"lights", "Lattice":"lattices", "Library":"librairies", "Mask":"masks",
"Material":"materials", "Mesh":"meshes", "MetaBall":"metaballs", "MovieClip":"movieclips", "NodeTree":"node_groups",
"Object":"objects", "PaintCurve":"paint_curves", "Palette":"palettes", "ParticleSettings":"particles",
"Scene":"scenes", "Screen":"screens", "Sound":"sounds", "Speaker":"speakers", "Text":"texts", "Texture":"textures",
"VectorFont":"fonts", "WindowManager":"window_managers", "World":"worlds"}
def report(var,message,type='INFO') :
print([a for a in locals()])
print([a for a in globals()])
if 'self' in var :
var['self'].report({type}, message)
else :
print('')
print(type)
print(message)
def read_json(path):
jsonFile = path
if os.path.exists(jsonFile):
try :
with open(jsonFile) as data_file:
return (json.load(data_file))
except :
print("the file %s not json readable"%jsonFile)
return
def search_filter(search,str) :
return search.lower() in str.lower()
def title(string) :
return string.replace('_',' ').replace('-',' ').title()
def arg_name(string) :
return string.replace(' ','_').replace('-','_').lower().strip('_')
def open_folder(path) :
try:
os.startfile(path)
except:
subprocess.Popen(['xdg-open', path])
def dic_to_args(dic):
import collections
args = collections.OrderedDict()
for k,v in dic.items() :
if k in ('icon','description') or not isinstance(k,str):
continue
if isinstance(v,str) :
if '/' in v or '\\' in v :
args[k] = StringProperty(default=v,subtype = "FILE_PATH")
else :
args[k] = StringProperty(default=v)
elif isinstance(v,bool) :
args[k] = BoolProperty(default=v)
elif isinstance(v,float) :
args[k] = FloatProperty(default=v,precision=3)
elif isinstance(v,int) :
args[k] = IntProperty(default=v)
elif isinstance(v,dict) :
if v.get("items") and isinstance(v["items"],(tuple,list)) :
args[k] = EnumProperty(items = [(i,i,"") for i in v["items"]])
elif v.get("collection") and isinstance(v["collection"],str) :
if v["collection"] not in id_type :
print("Collection %s not supported must be in %s"%(v["collection"],id_type))
#args[k] = PointerProperty(type = getattr(bpy.types,v["collection"]))
else :
args[k] = PointerProperty(type = getattr(bpy.types,v["collection"]),poll = v.get('poll'))
return args
def read_info(script_path) :
import collections
if isinstance(script_path,list) :
lines =script_path
else :
with open(script_path,encoding="utf-8") as f:
lines = f.read().splitlines()
info = {}
for i,l in enumerate(lines) :
if i >= 10 :
return info,lines
if l.startswith("info") :
info_start = i
info_str = l
while info_str.endswith(',') or not info_str.endswith('}') or i == len(lines)-1:
i+=1
info_str+= lines[i]
info_end = i
opening_brace_index = info_str.find('{')
try :
info = eval(info_str[opening_brace_index:])
except :
print('The info header in the file %s cannot be read'%script_path)
break
info = [(k,v) for k,v in info.items()]
info.sort(key = lambda x : info_str.find(x[0]))
info = collections.OrderedDict(info)
del lines[info_start:info_end+1]
break
# remove backspace
for i in range(len(lines)) :
if lines[0] :
break
else :
lines.pop(0)
for i in range(len(lines)) :
if lines[-1] :
break
else :
lines.pop(-1)
return info,lines
def dic_to_str(dic,keys= None,spaces = 4) :
if not keys :
keys = sorted(dic)
line = '{\n'
for k in keys :
v = dic[k]
line += ' '*spaces
if isinstance(k,str) :
line +="'%s'"%k
else :
line += "%s"%k
line += ' : '
if isinstance(v,str) :
line +="'%s'"%v
else :
line += "%s"%v
line += ',\n'
line += '}\n\n'
return line