2021-01-10 16:47:17 +01:00
|
|
|
import bpy
|
|
|
|
import os
|
|
|
|
from pathlib import Path
|
2023-02-07 12:55:48 +01:00
|
|
|
import numpy as np
|
2024-12-16 17:29:27 +01:00
|
|
|
|
2021-10-20 20:54:59 +02:00
|
|
|
from . import utils
|
2021-01-10 16:47:17 +01:00
|
|
|
|
2024-12-03 14:26:43 +01:00
|
|
|
from bpy.props import (BoolProperty,
|
|
|
|
PointerProperty,
|
|
|
|
CollectionProperty,
|
|
|
|
StringProperty)
|
|
|
|
|
2023-02-07 12:55:48 +01:00
|
|
|
def remove_stroke_exact_duplications(apply=True):
|
|
|
|
'''Remove accidental stroke duplication (points exactly in the same place)
|
|
|
|
:apply: Remove the duplication instead of just listing dupes
|
|
|
|
return number of duplication found/deleted
|
|
|
|
'''
|
|
|
|
# TODO: add additional check of material (even if unlikely to happen)
|
|
|
|
ct = 0
|
|
|
|
gp_datas = [gp for gp in bpy.data.grease_pencils]
|
|
|
|
for gp in gp_datas:
|
|
|
|
for l in gp.layers:
|
|
|
|
for f in l.frames:
|
|
|
|
stroke_list = []
|
2024-11-11 17:48:11 +01:00
|
|
|
for s in reversed(f.drawing.strokes):
|
2023-02-07 12:55:48 +01:00
|
|
|
|
2024-11-11 16:23:11 +01:00
|
|
|
point_list = [p.position for p in s.points]
|
2023-02-07 12:55:48 +01:00
|
|
|
|
|
|
|
if point_list in stroke_list:
|
|
|
|
ct += 1
|
|
|
|
if apply:
|
|
|
|
# Remove redundancy
|
2024-11-11 17:48:11 +01:00
|
|
|
f.drawing.strokes.remove(s)
|
2023-02-07 12:55:48 +01:00
|
|
|
else:
|
|
|
|
stroke_list.append(point_list)
|
|
|
|
return ct
|
2021-01-10 16:47:17 +01:00
|
|
|
class GPTB_OT_file_checker(bpy.types.Operator):
|
|
|
|
bl_idname = "gp.file_checker"
|
2021-10-20 20:54:59 +02:00
|
|
|
bl_label = "Check File"
|
2021-01-10 16:47:17 +01:00
|
|
|
bl_description = "Check / correct some aspect of the file, properties and such and report"
|
|
|
|
bl_options = {"REGISTER"}
|
|
|
|
|
2022-02-02 17:08:41 +01:00
|
|
|
## list of actions :
|
|
|
|
# Lock main cam
|
2021-01-10 16:47:17 +01:00
|
|
|
# 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
|
2022-02-27 22:47:48 +01:00
|
|
|
# Objects visibility conflict
|
|
|
|
# Objects modifiers visibility conflict
|
2022-02-02 17:08:41 +01:00
|
|
|
# GP modifiers broken target check
|
2021-01-10 16:47:17 +01:00
|
|
|
# Set onion skin filter to 'All type'
|
2021-07-16 18:14:31 +02:00
|
|
|
# Set filepath type
|
|
|
|
# Set Lock object mode state
|
2022-01-18 22:53:08 +01:00
|
|
|
# Disable use light on all object
|
2023-02-07 12:55:48 +01:00
|
|
|
# Remove redundant strokes in frames
|
2021-07-16 18:14:31 +02:00
|
|
|
|
2024-12-03 14:26:43 +01:00
|
|
|
apply_fixes : bpy.props.BoolProperty(name="Apply Fixes", default=False,
|
|
|
|
description="Apply possible fixes instead of just listing (pop the list again in fix mode)",
|
|
|
|
options={'SKIP_SAVE'})
|
|
|
|
|
2021-07-16 18:14:31 +02:00
|
|
|
def invoke(self, context, event):
|
2021-10-20 20:54:59 +02:00
|
|
|
# need some self-control (I had to...)
|
2021-07-16 18:14:31 +02:00
|
|
|
self.ctrl = event.ctrl
|
|
|
|
return self.execute(context)
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
def execute(self, context):
|
2021-10-20 20:54:59 +02:00
|
|
|
prefs = utils.get_addon_prefs()
|
2021-06-17 16:21:27 +02:00
|
|
|
fix = prefs.fixprops
|
2021-01-10 16:47:17 +01:00
|
|
|
problems = []
|
|
|
|
|
2024-12-03 14:26:43 +01:00
|
|
|
## Old method : Apply fixes based on pref (inverted by ctrl key)
|
|
|
|
# # If Ctrl is pressed, invert behavior (invert boolean)
|
|
|
|
# apply ^= self.ctrl
|
2021-07-16 18:14:31 +02:00
|
|
|
|
2024-12-03 14:26:43 +01:00
|
|
|
apply = self.apply_fixes
|
|
|
|
if self.ctrl:
|
|
|
|
apply = True
|
2021-07-16 18:14:31 +02:00
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
## Lock main cam:
|
2021-06-17 16:21:27 +02:00
|
|
|
if fix.lock_main_cam:
|
2021-07-16 18:14:31 +02:00
|
|
|
if not 'layout' in Path(bpy.data.filepath).stem.lower(): # dont touch layout cameras
|
2021-06-17 16:21:27 +02:00
|
|
|
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')
|
2021-07-16 18:14:31 +02:00
|
|
|
if apply:
|
|
|
|
cam.lock_location = cam.lock_rotation = triple
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
## set scene res at pref res according to addon pref
|
2021-06-17 16:21:27 +02:00
|
|
|
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}')
|
2021-07-16 18:14:31 +02:00
|
|
|
if apply:
|
|
|
|
context.scene.render.resolution_x, context.scene.render.resolution_y = rx, ry
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
## set scene percentage at 100:
|
2021-06-17 16:21:27 +02:00
|
|
|
if fix.set_res_percentage:
|
|
|
|
if context.scene.render.resolution_percentage != 100:
|
|
|
|
problems.append('Resolution output to 100%')
|
2021-07-16 18:14:31 +02:00
|
|
|
if apply:
|
|
|
|
context.scene.render.resolution_percentage = 100
|
2021-06-17 16:21:27 +02:00
|
|
|
|
|
|
|
## 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') )
|
2021-07-16 18:14:31 +02:00
|
|
|
if apply:
|
|
|
|
context.scene.render.fps = prefs.fps
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
## set show slider and sync range
|
2021-06-17 16:21:27 +02:00
|
|
|
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)
|
2021-01-10 16:47:17 +01:00
|
|
|
|
2021-06-24 16:40:22 +02:00
|
|
|
## set cursor type
|
2024-11-11 15:47:33 +01:00
|
|
|
if context.mode in ("EDIT_GREASE_PENCIL", "SCULPT_GREASE_PENCIL"):
|
2021-06-24 16:40:22 +02:00
|
|
|
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]}')
|
2021-07-16 18:14:31 +02:00
|
|
|
if apply:
|
|
|
|
bpy.ops.wm.tool_set_by_id(name=tool) # Tweaktoolcode
|
2021-06-17 16:21:27 +02:00
|
|
|
|
|
|
|
# ## 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
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
## GP stroke placement/projection check
|
2021-06-17 16:21:27 +02:00
|
|
|
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)')
|
2021-01-10 16:47:17 +01:00
|
|
|
|
2021-06-17 16:21:27 +02:00
|
|
|
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)')
|
|
|
|
|
2022-01-18 22:53:08 +01:00
|
|
|
## GP Use light disable
|
|
|
|
if fix.set_gp_use_lights_off:
|
2024-11-11 15:35:39 +01:00
|
|
|
gp_with_lights = [o for o in context.scene.objects if o.type == 'GREASEPENCIL' and o.use_grease_pencil_lights]
|
2022-01-18 22:53:08 +01:00
|
|
|
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
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
## Disabled animation
|
2021-06-17 16:21:27 +02:00
|
|
|
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:
|
2021-10-20 20:54:59 +02:00
|
|
|
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:
|
2024-12-03 14:26:43 +01:00
|
|
|
if not (o.hide_get() == o.hide_viewport == o.hide_render):
|
|
|
|
hv = 'No' if o.hide_get() else 'Yes'
|
2021-10-20 20:54:59 +02:00
|
|
|
vp = 'No' if o.hide_viewport else 'Yes'
|
|
|
|
rd = 'No' if o.hide_render else 'Yes'
|
|
|
|
viz_ct += 1
|
2024-12-03 14:26:43 +01:00
|
|
|
print(f'{o.name} : viewlayer {hv} - viewport {vp} - render {rd}')
|
2021-10-20 20:54:59 +02:00
|
|
|
if viz_ct:
|
2024-12-03 14:26:43 +01:00
|
|
|
problems.append(['gp.list_object_visibility_conflicts', f'{viz_ct} objects visibility conflicts (details in console)', 'OBJECT_DATAMODE'])
|
2021-10-20 20:54:59 +02:00
|
|
|
|
|
|
|
## GP modifiers visibility conflict
|
|
|
|
if fix.list_gp_mod_vis_conflict:
|
|
|
|
mod_viz_ct = 0
|
|
|
|
for o in context.scene.objects:
|
2024-11-11 18:44:35 +01:00
|
|
|
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}')
|
2022-02-27 22:47:48 +01:00
|
|
|
|
2021-10-20 20:54:59 +02:00
|
|
|
if mod_viz_ct:
|
2022-02-27 22:47:48 +01:00
|
|
|
problems.append(['gp.list_modifier_visibility', f'{mod_viz_ct} modifiers visibility conflicts (details in console)', 'MODIFIER_DATA'])
|
2021-10-20 20:54:59 +02:00
|
|
|
|
2022-02-02 17:08:41 +01:00
|
|
|
## check if GP modifier have broken layer targets
|
|
|
|
if fix.list_broken_mod_targets:
|
2024-11-11 15:35:39 +01:00
|
|
|
for o in [o for o in bpy.context.scene.objects if o.type == 'GREASEPENCIL']:
|
2024-11-11 15:56:43 +01:00
|
|
|
lay_name_list = [l.name for l in o.data.layers]
|
2024-11-11 18:44:35 +01:00
|
|
|
for m in o.modifiers:
|
|
|
|
if not hasattr(m, 'layer_filter'):
|
2022-02-02 17:08:41 +01:00
|
|
|
continue
|
2024-11-11 18:44:35 +01:00
|
|
|
if m.layer_filter != '' and not m.layer_filter in lay_name_list:
|
|
|
|
mess = f'Broken modifier layer target: {o.name} > {m.name} > {m.layer_filter}'
|
2022-02-02 17:08:41 +01:00
|
|
|
print(mess)
|
|
|
|
problems.append(mess)
|
2021-06-17 16:21:27 +02:00
|
|
|
|
|
|
|
## 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'")
|
2021-07-16 18:14:31 +02:00
|
|
|
if apply:
|
|
|
|
context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT'
|
2021-06-17 16:21:27 +02:00
|
|
|
|
|
|
|
if fix.disable_guide:
|
|
|
|
if context.scene.tool_settings.gpencil_sculpt.guide.use_guide == True:
|
|
|
|
problems.append(f"Disabled Draw Guide")
|
2021-07-16 18:14:31 +02:00
|
|
|
if apply:
|
|
|
|
context.scene.tool_settings.gpencil_sculpt.guide.use_guide = False
|
2021-06-17 16:21:27 +02:00
|
|
|
|
|
|
|
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'")
|
2021-07-16 18:14:31 +02:00
|
|
|
if apply:
|
|
|
|
context.scene.tool_settings.auto_keying_mode = 'ADD_REPLACE_KEYS'
|
2021-06-17 16:21:27 +02:00
|
|
|
|
2021-06-28 12:41:56 +02:00
|
|
|
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('-')
|
2021-07-16 18:14:31 +02:00
|
|
|
|
|
|
|
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
|
2021-06-28 12:41:56 +02:00
|
|
|
|
2021-07-16 18:14:31 +02:00
|
|
|
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
|
|
|
|
|
2023-02-07 12:55:48 +01:00
|
|
|
if fix.remove_redundant_strokes:
|
|
|
|
ct = remove_stroke_exact_duplications(apply=apply)
|
|
|
|
if ct > 0:
|
|
|
|
mess = f'Removed {ct} strokes duplications' if apply else f'Found {ct} strokes duplications'
|
|
|
|
problems.append(mess)
|
|
|
|
|
2021-06-17 16:21:27 +02:00
|
|
|
# ## 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'")
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
# for ob in context.scene.objects:#from object
|
2024-11-11 15:35:39 +01:00
|
|
|
# if ob.type == 'GREASEPENCIL':
|
2021-01-10 16:47:17 +01:00
|
|
|
# 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])
|
2024-12-03 14:26:43 +01:00
|
|
|
|
|
|
|
if not self.apply_fixes:
|
|
|
|
## button to call the operator again with apply_fixes set to True
|
|
|
|
problems.append(['OPERATOR', 'gp.file_checker', 'Apply Fixes', 'FORWARD', {'apply_fixes': True}])
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
# Show in viewport
|
2024-12-03 14:26:43 +01:00
|
|
|
title = "Changed Settings" if apply else "Checked Settings (nothing changed)"
|
2021-10-20 20:54:59 +02:00
|
|
|
utils.show_message_box(problems, _title = title, _icon = 'INFO')
|
2021-01-10 16:47:17 +01:00
|
|
|
else:
|
|
|
|
self.report({'INFO'}, 'All good')
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
|
2021-11-18 21:14:58 +01:00
|
|
|
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"}
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
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()
|
2021-11-18 21:14:58 +01:00
|
|
|
# layout = layout.column() # thinner linespace
|
2021-01-10 16:47:17 +01:00
|
|
|
for l in self.all_lnks:
|
2021-11-18 21:14:58 +01:00
|
|
|
if l[1] == 'CANCEL':
|
2021-01-10 16:47:17 +01:00
|
|
|
layout.label(text=l[0], icon=l[1])
|
2021-11-18 21:14:58 +01:00
|
|
|
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])
|
2021-01-10 16:47:17 +01:00
|
|
|
else:
|
2021-12-04 13:57:32 +01:00
|
|
|
split=layout.split(factor=0.70, align=True)
|
2021-01-10 16:47:17 +01:00
|
|
|
split.label(text=l[0], icon=l[1])
|
2021-11-18 21:14:58 +01:00
|
|
|
## 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
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
def invoke(self, context, event):
|
|
|
|
self.all_lnks = []
|
|
|
|
self.title = ''
|
|
|
|
self.broke_ct = 0
|
|
|
|
abs_ct = 0
|
|
|
|
rel_ct = 0
|
|
|
|
## check for broken links
|
2021-11-18 21:14:58 +01:00
|
|
|
viewed = []
|
2021-01-10 16:47:17 +01:00
|
|
|
for current, lib in zip(bpy.utils.blend_paths(local=True), bpy.utils.blend_paths(absolute=True, local=True)):
|
2021-11-18 21:14:58 +01:00
|
|
|
# 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') )
|
2021-01-10 16:47:17 +01:00
|
|
|
else:
|
2021-11-18 21:14:58 +01:00
|
|
|
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
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
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
|
2021-11-18 21:14:58 +01:00
|
|
|
|
|
|
|
maxlen = max(len(x) for x in viewed)
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
# if broke_ct == 0:
|
|
|
|
# show_message_box(self.all_lnks, _title = self.title, _icon = 'INFO')# Links
|
|
|
|
# return {"FINISHED"}
|
2021-11-18 21:14:58 +01:00
|
|
|
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)
|
|
|
|
|
2021-01-10 16:47:17 +01:00
|
|
|
|
2021-10-20 20:54:59 +02:00
|
|
|
|
2024-12-03 14:26:43 +01:00
|
|
|
class GPTB_OT_list_viewport_render_visibility(bpy.types.Operator):
|
|
|
|
bl_idname = "gp.list_viewport_render_visibility"
|
|
|
|
bl_label = "List Viewport And Render Visibility Conflicts"
|
2021-10-20 20:54:59 +02:00
|
|
|
bl_description = "List objects visibility conflicts, when viewport and render have different values"
|
|
|
|
bl_options = {"REGISTER"}
|
2024-12-03 14:26:43 +01:00
|
|
|
|
2021-10-20 20:54:59 +02:00
|
|
|
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):
|
2023-02-07 12:55:48 +01:00
|
|
|
# TODO: Add visibility check with viewlayer visibility as well
|
2021-10-20 20:54:59 +02:00
|
|
|
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'}
|
2024-12-03 14:26:43 +01:00
|
|
|
|
|
|
|
### -- Sync visibility ops (Could be fused in one ops, but having 3 different operators allow to call from search menu)
|
|
|
|
class GPTB_OT_sync_visibility_from_viewlayer(bpy.types.Operator):
|
|
|
|
bl_idname = "gp.sync_visibility_from_viewlayer"
|
|
|
|
bl_label = "Sync Visibility From Viewlayer"
|
|
|
|
bl_description = "Set viewport and render visibility to match viewlayer visibility"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
for obj in context.scene.objects:
|
|
|
|
is_hidden = obj.hide_get() # Get viewlayer visibility
|
|
|
|
obj.hide_viewport = is_hidden
|
|
|
|
obj.hide_render = is_hidden
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
class GPTB_OT_sync_visibility_from_viewport(bpy.types.Operator):
|
|
|
|
bl_idname = "gp.sync_visibility_from_viewport"
|
|
|
|
bl_label = "Sync Visibility From Viewport"
|
|
|
|
bl_description = "Set viewlayer and render visibility to match viewport visibility"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
for obj in context.scene.objects:
|
|
|
|
is_hidden = obj.hide_viewport
|
|
|
|
obj.hide_set(is_hidden)
|
|
|
|
obj.hide_render = is_hidden
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
class GPTB_OT_sync_visibility_from_render(bpy.types.Operator):
|
|
|
|
bl_idname = "gp.sync_visibility_from_render"
|
|
|
|
bl_label = "Sync Visibility From Render"
|
|
|
|
bl_description = "Set viewlayer and viewport visibility to match render visibility"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
for obj in context.scene.objects:
|
|
|
|
is_hidden = obj.hide_render
|
|
|
|
obj.hide_set(is_hidden)
|
|
|
|
obj.hide_viewport = is_hidden
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
class GPTB_OT_sync_visibible_to_render(bpy.types.Operator):
|
|
|
|
bl_idname = "gp.sync_visibible_to_render"
|
|
|
|
bl_label = "Sync Overall Viewport Visibility To Render"
|
|
|
|
bl_description = "Set render visibility from"
|
|
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
for obj in context.scene.objects:
|
|
|
|
## visible_get is the current visibility status combination of hide_viewport and viewlayer hide (eye)
|
|
|
|
obj.hide_render = not obj.visible_get()
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
class GPTB_PG_object_visibility(bpy.types.PropertyGroup):
|
|
|
|
"""Property group to handle object visibility"""
|
|
|
|
is_hidden: BoolProperty(
|
|
|
|
name="Hide in Viewport",
|
|
|
|
description="Toggle object visibility in viewport",
|
|
|
|
get=lambda self: self.get("is_hidden", False),
|
|
|
|
set=lambda self, value: self.set_visibility(value)
|
|
|
|
)
|
|
|
|
|
|
|
|
object_name: StringProperty(name="Object Name")
|
|
|
|
|
|
|
|
def set_visibility(self, value):
|
|
|
|
"""Set the visibility using hide_set()"""
|
|
|
|
obj = bpy.context.view_layer.objects.get(self.object_name)
|
|
|
|
if obj:
|
|
|
|
obj.hide_set(value)
|
|
|
|
self["is_hidden"] = value
|
|
|
|
|
|
|
|
class GPTB_OT_list_object_visibility_conflicts(bpy.types.Operator):
|
|
|
|
bl_idname = "gp.list_object_visibility_conflicts"
|
|
|
|
bl_label = "List Object Visibility Conflicts"
|
|
|
|
bl_description = "List objects visibility conflicts, when viewport and render have different values"
|
|
|
|
bl_options = {"REGISTER"}
|
|
|
|
|
|
|
|
visibility_items: CollectionProperty(type=GPTB_PG_object_visibility) # type: ignore[valid-type]
|
|
|
|
|
|
|
|
def invoke(self, context, event):
|
|
|
|
# Clear and rebuild both collections
|
|
|
|
self.visibility_items.clear()
|
|
|
|
|
|
|
|
# Store objects with conflicts
|
2024-12-03 15:36:55 +01:00
|
|
|
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ?
|
|
|
|
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
|
2024-12-03 14:26:43 +01:00
|
|
|
|
|
|
|
# Create visibility items in same order
|
2024-12-03 15:36:55 +01:00
|
|
|
for obj in objects_with_conflicts:
|
2024-12-03 14:26:43 +01:00
|
|
|
item = self.visibility_items.add()
|
|
|
|
item.object_name = obj.name
|
|
|
|
item["is_hidden"] = obj.hide_get()
|
|
|
|
|
|
|
|
return context.window_manager.invoke_props_dialog(self, width=250)
|
|
|
|
|
|
|
|
def draw(self, context):
|
|
|
|
layout = self.layout
|
|
|
|
|
|
|
|
# Add sync buttons at the top
|
|
|
|
row = layout.row(align=False)
|
|
|
|
row.label(text="Sync All Visibility From:")
|
|
|
|
row.operator("gp.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF')
|
|
|
|
row.operator("gp.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF')
|
|
|
|
row.operator("gp.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF')
|
|
|
|
layout.separator()
|
|
|
|
|
|
|
|
col = layout.column()
|
|
|
|
# We can safely iterate over visibility_items since objects are stored in same order
|
|
|
|
for vis_item in self.visibility_items:
|
|
|
|
obj = context.view_layer.objects.get(vis_item.object_name)
|
|
|
|
if not obj:
|
|
|
|
continue
|
|
|
|
|
|
|
|
row = col.row(align=False)
|
|
|
|
row.label(text=obj.name)
|
|
|
|
|
|
|
|
## Viewlayer visibility "as prop" to allow slide toggle
|
|
|
|
# hide_icon='HIDE_ON' if vis_item.is_hidden else 'HIDE_OFF'
|
2024-12-03 15:36:55 +01:00
|
|
|
hide_icon='HIDE_ON' if obj.hide_get() else 'HIDE_OFF' # based on object state
|
2024-12-03 14:26:43 +01:00
|
|
|
row.prop(vis_item, "is_hidden", text="", icon=hide_icon, emboss=False)
|
|
|
|
|
|
|
|
# Direct object properties
|
|
|
|
row.prop(obj, 'hide_viewport', text='', emboss=False)
|
|
|
|
row.prop(obj, 'hide_render', text='', emboss=False)
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
return {'FINISHED'}
|
2021-10-20 20:54:59 +02:00
|
|
|
|
2024-11-11 18:44:35 +01:00
|
|
|
## not exposed in UI, Check is performed in Check file (can be called in popped menu)
|
2021-10-20 20:54:59 +02:00
|
|
|
class GPTB_OT_list_modifier_visibility(bpy.types.Operator):
|
|
|
|
bl_idname = "gp.list_modifier_visibility"
|
2024-11-11 18:44:35 +01:00
|
|
|
bl_label = "List Objects Modifiers Visibility Conflicts"
|
2021-10-20 20:54:59 +02:00
|
|
|
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:
|
2024-11-11 18:44:35 +01:00
|
|
|
if not len(o.modifiers):
|
2021-10-20 20:54:59 +02:00
|
|
|
continue
|
|
|
|
mods = []
|
2024-11-11 18:44:35 +01:00
|
|
|
for m in o.modifiers:
|
2021-10-20 20:54:59 +02:00
|
|
|
if m.show_viewport != m.show_render:
|
|
|
|
if not mods:
|
2024-11-11 18:44:35 +01:00
|
|
|
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
|
2021-10-20 20:54:59 +02:00
|
|
|
mods.append(m)
|
2022-02-27 22:47:48 +01:00
|
|
|
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)
|
2021-01-10 16:47:17 +01:00
|
|
|
|
2022-02-27 22:47:48 +01:00
|
|
|
def draw(self, context):
|
|
|
|
layout = self.layout
|
|
|
|
if not self.ob_list:
|
|
|
|
layout.label(text='No modifier visibility conflict found', icon='CHECKMARK')
|
|
|
|
return
|
2021-01-10 16:47:17 +01:00
|
|
|
|
2022-02-27 22:47:48 +01:00
|
|
|
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
|
2021-01-10 16:47:17 +01:00
|
|
|
|
2022-02-27 22:47:48 +01:00
|
|
|
def execute(self, context):
|
|
|
|
return {'FINISHED'}
|
2021-01-10 16:47:17 +01:00
|
|
|
|
|
|
|
classes = (
|
2024-12-03 14:26:43 +01:00
|
|
|
GPTB_OT_list_viewport_render_visibility, # Only viewport and render
|
|
|
|
GPTB_OT_sync_visibility_from_viewlayer,
|
|
|
|
GPTB_OT_sync_visibility_from_viewport,
|
|
|
|
GPTB_OT_sync_visibility_from_render,
|
|
|
|
GPTB_OT_sync_visibible_to_render,
|
|
|
|
GPTB_PG_object_visibility,
|
|
|
|
GPTB_OT_list_object_visibility_conflicts,
|
2021-10-20 20:54:59 +02:00
|
|
|
GPTB_OT_list_modifier_visibility,
|
2021-11-18 21:14:58 +01:00
|
|
|
GPTB_OT_copy_string_to_clipboard,
|
|
|
|
GPTB_OT_copy_multipath_clipboard,
|
2021-01-10 16:47:17 +01:00
|
|
|
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)
|