gp_toolbox/OP_file_checker.py

636 lines
26 KiB
Python
Executable File

import bpy
import os
from pathlib import Path
from . import utils
class GPTB_OT_file_checker(bpy.types.Operator):
bl_idname = "gp.file_checker"
bl_label = "Check File"
bl_description = "Check / correct some aspect of the file, properties and such and report"
bl_options = {"REGISTER"}
## list of actions :
# Lock main cam
# set scene res
# set scene percentage at 100:
# set show slider and sync range
# set fps
# set cursor type
# GP use additive drawing (else creating a frame in dopesheet makes it blank...)
# GP stroke placement/projection check
# Disabled animation
# Objects visibility conflict
# Objects modifiers visibility conflict
# GP modifiers broken target check
# Set onion skin filter to 'All type'
# Set filepath type
# Set Lock object mode state
# Disable use light on all object
def invoke(self, context, event):
# need some self-control (I had to...)
self.ctrl = event.ctrl
return self.execute(context)
def execute(self, context):
prefs = utils.get_addon_prefs()
fix = prefs.fixprops
problems = []
apply = not fix.check_only
# If Ctrl is pressed, invert behavior (invert boolean)
apply ^= self.ctrl
## Lock main cam:
if fix.lock_main_cam:
if not 'layout' in Path(bpy.data.filepath).stem.lower(): # dont touch layout cameras
if context.scene.camera:
cam = context.scene.camera
if cam.name == 'draw_cam' and cam.parent:
if cam.parent.type == 'CAMERA':
cam = cam.parent
else:
cam = None
if cam:
triple = (True,True,True)
if cam.lock_location[:] != triple or cam.lock_rotation[:] != triple:
problems.append('Lock main camera')
if apply:
cam.lock_location = cam.lock_rotation = triple
## set scene res at pref res according to addon pref
if fix.set_scene_res:
rx, ry = prefs.render_res_x, prefs.render_res_y
# TODO set (rx, ry) to camera resolution if specified in camera name
if context.scene.render.resolution_x != rx or context.scene.render.resolution_y != ry:
problems.append(f'Resolution {context.scene.render.resolution_x}x{context.scene.render.resolution_y} >> {rx}x{ry}')
if apply:
context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
## set scene percentage at 100:
if fix.set_res_percentage:
if context.scene.render.resolution_percentage != 100:
problems.append('Resolution output to 100%')
if apply:
context.scene.render.resolution_percentage = 100
## set fps according to preferences settings
if fix.set_fps:
if context.scene.render.fps != prefs.fps:
problems.append( (f"framerate corrected {context.scene.render.fps} >> {prefs.fps}", 'ERROR') )
if apply:
context.scene.render.fps = prefs.fps
## set show slider and sync range
if fix.set_slider_n_sync:
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'DOPESHEET_EDITOR':
if hasattr(area.spaces[0], 'show_sliders'):
setattr(area.spaces[0], 'show_sliders', True)
if hasattr(area.spaces[0], 'show_locked_time'):
setattr(area.spaces[0], 'show_locked_time', True)
## set cursor type
if context.mode in ("EDIT_GPENCIL", "SCULPT_GPENCIL"):
tool = fix.select_active_tool
if tool != 'none':
if bpy.context.workspace.tools.from_space_view3d_mode(bpy.context.mode, create=False).idname != tool:
problems.append(f'tool changed to {tool.split(".")[1]}')
if apply:
bpy.ops.wm.tool_set_by_id(name=tool) # Tweaktoolcode
# ## GP use additive drawing (else creating a frame in dopesheet makes it blank...)
# if not context.scene.tool_settings.use_gpencil_draw_additive:
# problems.append(f'Activated Gp additive drawing mode (snowflake)')
# context.scene.tool_settings.use_gpencil_draw_additive = True
## GP stroke placement/projection check
if fix.check_front_axis:
if context.scene.tool_settings.gpencil_sculpt.lock_axis != 'AXIS_Y':
problems.append('/!\\ Draw axis not "Front" (Need Manual change if not Ok)')
if fix.check_placement:
if bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d != 'ORIGIN':
problems.append('/!\\ Draw placement not "Origin" (Need Manual change if not Ok)')
## GP Use light disable
if fix.set_gp_use_lights_off:
gp_with_lights = [o for o in context.scene.objects if o.type == 'GPENCIL' and o.use_grease_pencil_lights]
if gp_with_lights:
problems.append(f'Disable "Use Lights" on {len(gp_with_lights)} Gpencil objects')
if apply:
for o in gp_with_lights:
o.use_grease_pencil_lights = False
## Disabled animation
if fix.list_disabled_anim:
fcu_ct = 0
for act in bpy.data.actions:
if not act.users:
continue
for fcu in act.fcurves:
if fcu.mute:
fcu_ct += 1
print(f"muted: {act.name} > {fcu.data_path}")
if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
## Object visibility conflict
if fix.list_obj_vis_conflict:
viz_ct = 0
for o in context.scene.objects:
if o.hide_viewport != o.hide_render:
vp = 'No' if o.hide_viewport else 'Yes'
rd = 'No' if o.hide_render else 'Yes'
viz_ct += 1
print(f'{o.name} : viewport {vp} != render {rd}')
if viz_ct:
problems.append(['gp.list_object_visibility', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
## GP modifiers visibility conflict
if fix.list_gp_mod_vis_conflict:
mod_viz_ct = 0
for o in context.scene.objects:
if o.type == 'GPENCIL':
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
vp = 'Yes' if m.show_viewport else 'No'
rd = 'Yes' if m.show_render else 'No'
mod_viz_ct += 1
print(f'{o.name} - GP modifier {m.name}: viewport {vp} != render {rd}')
else:
for m in o.modifiers:
if m.show_viewport != m.show_render:
vp = 'Yes' if m.show_viewport else 'No'
rd = 'Yes' if m.show_render else 'No'
mod_viz_ct += 1
print(f'{o.name} - modifier {m.name}: viewport {vp} != render {rd}')
if mod_viz_ct:
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA'])
## check if GP modifier have broken layer targets
if fix.list_broken_mod_targets:
for o in [o for o in bpy.context.scene.objects if o.type == 'GPENCIL']:
lay_name_list = [l.info for l in o.data.layers]
for m in o.grease_pencil_modifiers:
if not hasattr(m, 'layer'):
continue
if not m.layer in lay_name_list:
mess = f'Broken modifier target: {o.name} > {m.name} > {m.layer}'
print(mess)
problems.append(mess)
## Use median point
if fix.set_pivot_median_point:
if context.scene.tool_settings.transform_pivot_point != 'MEDIAN_POINT':
problems.append(f"Pivot changed from '{context.scene.tool_settings.transform_pivot_point}' to 'MEDIAN_POINT'")
if apply:
context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
if fix.disable_guide:
if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True:
problems.append(f"Disabled Draw Guide")
if apply:
context.scene.tool_settings.gpencil_sculpt.guide.use_guide = False
if fix.autokey_add_n_replace:
if context.scene.tool_settings.auto_keying_mode != 'ADD_REPLACE_KEYS':
problems.append(f"Autokey mode reset to 'Add & Replace'")
if apply:
context.scene.tool_settings.auto_keying_mode = 'ADD_REPLACE_KEYS'
if fix.file_path_type != 'none':
pathes = []
for p in bpy.utils.blend_paths():
if fix.file_path_type == 'RELATIVE':
if not p.startswith('//'):
pathes.append(p)
elif fix.file_path_type == 'ABSOLUTE':
if p.startswith('//'):
pathes.append(p)
if pathes:
mess = f'{len(pathes)}/{len(bpy.utils.blend_paths())} paths not {fix.file_path_type.lower()} (see console)'
problems.append(mess)
print(mess)
print('\n'.join(pathes))
print('-')
if fix.lock_object_mode != 'none':
lockmode = bpy.context.scene.tool_settings.lock_object_mode
if fix.lock_object_mode == 'LOCK':
if not lockmode:
problems.append(f"Lock object mode toggled On")
if apply:
bpy.context.scene.tool_settings.lock_object_mode = True
elif fix.lock_object_mode == 'UNLOCK':
if lockmode:
problems.append(f"Lock object mode toggled Off")
if apply:
bpy.context.scene.tool_settings.lock_object_mode = False
# ## Set onion skin filter to 'All type'
# fix_kf_type = 0
# for gp in bpy.data.grease_pencils:#from data
# if not gp.is_annotation:
# if gp.onion_keyframe_type != 'ALL':
# gp.onion_keyframe_type = 'ALL'
# fix_kf_type += 1
# if fix_kf_type:
# problems.append(f"{fix_kf_type} GP onion skin filter to 'All type'")
# for ob in context.scene.objects:#from object
# if ob.type == 'GPENCIL':
# ob.data.onion_keyframe_type = 'ALL'
#### --- print fix/problems report
if problems:
print('===File check===')
for p in problems:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
title = "Changed Settings" if apply else "Checked Settings (dry run, nothing changed)"
utils.show_message_box(problems, _title = title, _icon = 'INFO')
else:
self.report({'INFO'}, 'All good')
return {"FINISHED"}
class GPTB_OT_copy_string_to_clipboard(bpy.types.Operator):
bl_idname = "gp.copy_string_to_clipboard"
bl_label = "Copy String"
bl_description = "Copy passed string to clipboard"
bl_options = {"REGISTER"}
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
def execute(self, context):
if not self.string:
# self.report({'ERROR'}, 'Nothing to copy')
return {"CANCELLED"}
bpy.context.window_manager.clipboard = self.string
self.report({'INFO'}, f'Copied: {self.string}')
return {"FINISHED"}
class GPTB_OT_copy_multipath_clipboard(bpy.types.Operator):
bl_idname = "gp.copy_multipath_clipboard"
bl_label = "Choose Path to Copy"
bl_description = "Copy Chosen Path"
bl_options = {"REGISTER"}
string : bpy.props.StringProperty(options={'SKIP_SAVE'})
def invoke(self, context, event):
if not self.string:
return {"CANCELLED"}
self.pathes = []
try:
absolute = os.path.abspath(bpy.path.abspath(self.string))
abs_parent = os.path.dirname(os.path.abspath(bpy.path.abspath(self.string)))
path_abs = str(Path(bpy.path.abspath(self.string)).resolve())
except:
# case of invalid / non-accessable path
bpy.context.window_manager.clipboard = self.string
return context.window_manager.invoke_props_dialog(self, width=800)
self.pathes.append(('Raw Path', self.string))
self.pathes.append(('Parent', os.path.dirname(self.string)))
if absolute != self.string:
self.pathes.append(('Absolute', absolute))
if absolute != self.string:
self.pathes.append(('Absolute Parent', abs_parent))
if absolute != path_abs:
self.pathes.append(('Resolved',path_abs))
self.pathes.append(('File name', os.path.basename(self.string)))
maxlen = max(len(l[1]) for l in self.pathes)
popup_width = 800
if maxlen < 50:
popup_width = 500
elif maxlen > 100:
popup_width = 1000
return context.window_manager.invoke_props_dialog(self, width=popup_width)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.separator()
col = layout.column()
for l in self.pathes:
split=col.split(factor=0.2, align=True)
split.operator('gp.copy_string_to_clipboard', text=l[0], icon='COPYDOWN').string = l[1]
split.label(text=l[1])
def execute(self, context):
return {"FINISHED"}
class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
bl_options = {"REGISTER"}
def execute(self, context):
return {"FINISHED"}
def draw(self, context):
layout = self.layout
layout.label(text=self.title)
if self.broke_ct:
layout.label(text="You can try to scan for missing files:")
## How to launch directly without filebrowser ?
# in Shot folder
layout.operator('file.find_missing_files', text='in parent hierarchy').directory = Path(bpy.data.filepath).parents[1].as_posix()
if self.proj:
# In Library
layout.operator('file.find_missing_files', text='in library').directory = (Path(self.proj)/'library').as_posix()
# In all project
layout.operator('file.find_missing_files', text='in all project (last resort)').directory = self.proj
layout.separator()
# layout = layout.column() # thinner linespace
for l in self.all_lnks:
if l[1] == 'CANCEL':
layout.label(text=l[0], icon=l[1])
continue
if l[1] == 'LIBRARY_DATA_BROKEN':
split=layout.split(factor=0.85)
split.label(text=l[0], icon=l[1])
# layout.label(text=l[0], icon=l[1])
else:
split=layout.split(factor=0.70, align=True)
split.label(text=l[0], icon=l[1])
## resolve() return somethin different than os.path.abspath.
# split.operator('wm.path_open', text='Open folder', icon='FILE_FOLDER').filepath = Path(bpy.path.abspath(l[0])).resolve().parent.as_posix()
# split.operator('wm.path_open', text='Open file', icon='FILE_TICK').filepath = Path(bpy.path.abspath(l[0])).resolve().as_posix()
split.operator('wm.path_open', text='Open Folder', icon='FILE_FOLDER').filepath = Path(os.path.abspath(bpy.path.abspath(l[0]))).parent.as_posix()
split.operator('wm.path_open', text='Open File', icon='FILE_TICK').filepath = Path(os.path.abspath(bpy.path.abspath(l[0]))).as_posix()
split.operator('gp.copy_multipath_clipboard', text='Copy Path', icon='COPYDOWN').string = l[0]
# split.operator('gp.copy_string_to_clipboard', text='Copy Path', icon='COPYDOWN').string = l[0] # copy blend path directly
def invoke(self, context, event):
self.all_lnks = []
self.title = ''
self.broke_ct = 0
abs_ct = 0
rel_ct = 0
## check for broken links
viewed = []
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
# avoid relisting same path mutliple times
if current in viewed:
continue
# TODO find a proper way to show the number of user of this path...
viewed.append(current)
realib = Path(current) # path as-is
lfp = Path(lib) # absolute path
try: # Try because some path may fail parsing
if not lfp.exists():
self.broke_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )
else:
if realib.as_posix().startswith('//'):
rel_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LINKED') )
else:
abs_ct += 1
self.all_lnks.append( (f"{realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )
except:
self.broke_ct += 1
self.all_lnks.append( (f"{current}" , 'CANCEL') ) # error accessing file
if not self.all_lnks:
self.report({'INFO'}, 'No external links in files')
return {"FINISHED"}
bct = f"{self.broke_ct} broken " if self.broke_ct else ''
act = f"{abs_ct} absolute " if abs_ct else ''
rct = f"{rel_ct} clean " if rel_ct else ''
self.title = f"{bct}{act}{rct}"
self.all_lnks.sort(key=lambda x: x[1], reverse=True)
if self.all_lnks:
print('===File check===')
for p in self.all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
maxlen = max(len(x) for x in viewed)
# if broke_ct == 0:
# show_message_box(self.all_lnks, _title = self.title, _icon = 'INFO')# Links
# return {"FINISHED"}
popup_width = 800
if maxlen < 50:
popup_width = 500
elif maxlen > 100:
popup_width = 1000
self.proj = os.environ.get('PROJECT_ROOT')
return context.window_manager.invoke_props_dialog(self, width=popup_width)
""" OLD links checker with show_message_box
class GPTB_OT_links_checker(bpy.types.Operator):
bl_idname = "gp.links_checker"
bl_label = "Links check"
bl_description = "Check states of file direct links"
bl_options = {"REGISTER"}
def execute(self, context):
all_lnks = []
has_broken_link = False
## check for broken links
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
lfp = Path(lib)
realib = Path(current)
if not lfp.exists():
has_broken_link = True
all_lnks.append( (f"Broken link: {realib.as_posix()}", 'LIBRARY_DATA_BROKEN') )#lfp.as_posix()
else:
if realib.as_posix().startswith('//'):
all_lnks.append( (f"Link: {realib.as_posix()}", 'LINKED') )#lfp.as_posix()
else:
all_lnks.append( (f"Link: {realib.as_posix()}", 'LIBRARY_DATA_INDIRECT') )#lfp.as_posix()
all_lnks.sort(key=lambda x: x[1], reverse=True)
if all_lnks:
print('===File check===')
for p in all_lnks:
if isinstance(p, str):
print(p)
else:
print(p[0])
# Show in viewport
utils.show_message_box(all_lnks, _title = "Links", _icon = 'INFO')
return {"FINISHED"} """
class GPTB_OT_list_object_visibility(bpy.types.Operator):
bl_idname = "gp.list_object_visibility"
bl_label = "List Object Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
def invoke(self, context, event):
self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render]
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
for o in self.ob_list:
row = layout.row()
row.label(text=o.name)
row.prop(o, 'hide_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(o, 'hide_render', text='', emboss=False) # invert_checkbox=True
def execute(self, context):
return {'FINISHED'}
## basic listing as message box # all in invoke now
# li = []
# viz_ct = 0
# for o in context.scene.objects:
# if o.hide_viewport != o.hide_render:
# vp = 'No' if o.hide_viewport else 'Yes'
# rd = 'No' if o.hide_render else 'Yes'
# viz_ct += 1
# li.append(f'{o.name} : viewport {vp} != render {rd}')
# if li:
# utils.show_message_box(_message=li, _title=f'{viz_ct} visibility conflicts found')
# else:
# self.report({'INFO'}, f"No Object visibility conflict on current scene")
# return {'FINISHED'}
## Only GP modifier
'''
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
bl_idname = "gp.list_modifier_visibility"
bl_label = "List GP Modifiers Visibility Conflicts"
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
def invoke(self, context, event):
self.ob_list = []
for o in context.scene.objects:
if o.type != 'GPENCIL':
continue
if not len(o.grease_pencil_modifiers):
continue
mods = []
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods])
mods.append(m)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
for o in self.ob_list:
layout.label(text=o[0].name, icon='OUTLINER_OB_GREASEPENCIL')
for m in o[1]:
row = layout.row()
row.label(text='')
row.label(text=m.name, icon='MODIFIER_ON')
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
def execute(self, context):
return {'FINISHED'}
'''
## not exposed in UI, Check is performed in Check file (can be called in popped menu)
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
bl_idname = "gp.list_modifier_visibility"
bl_label = "List Objects Modifiers Visibility Conflicts"
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
def invoke(self, context, event):
self.ob_list = []
for o in context.scene.objects:
if o.type == 'GPENCIL':
if not len(o.grease_pencil_modifiers):
continue
mods = []
for m in o.grease_pencil_modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, 'OUTLINER_OB_GREASEPENCIL'])
mods.append(m)
else:
if not len(o.modifiers):
continue
mods = []
for m in o.modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
mods.append(m)
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
if not self.ob_list:
layout.label(text='No modifier visibility conflict found', icon='CHECKMARK')
return
for o in self.ob_list:
layout.label(text=o[0].name, icon=o[2])
for m in o[1]:
row = layout.row()
row.label(text='')
row.label(text=m.name, icon='MODIFIER_ON')
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
def execute(self, context):
return {'FINISHED'}
classes = (
GPTB_OT_list_object_visibility,
GPTB_OT_list_modifier_visibility,
GPTB_OT_copy_string_to_clipboard,
GPTB_OT_copy_multipath_clipboard,
GPTB_OT_file_checker,
GPTB_OT_links_checker,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)