2025-12-01
This commit is contained in:
@@ -102,6 +102,13 @@ def register_dataremap_properties():
|
||||
# Store the last clicked group for shift-click range selection
|
||||
if not hasattr(bpy.types.Scene, "last_clicked_group"):
|
||||
bpy.types.Scene.last_clicked_group = {}
|
||||
|
||||
# Ghost Buster properties
|
||||
bpy.types.Scene.ghost_buster_delete_low_priority = bpy.props.BoolProperty( # type: ignore
|
||||
name="Delete Low Priority Ghosts",
|
||||
description="Delete objects not in scenes with no legitimate use and users < 2",
|
||||
default=False
|
||||
)
|
||||
|
||||
def unregister_dataremap_properties():
|
||||
del bpy.types.Scene.dataremap_images
|
||||
@@ -124,6 +131,10 @@ def unregister_dataremap_properties():
|
||||
del bpy.types.Scene.dataremap_sort_materials
|
||||
del bpy.types.Scene.dataremap_sort_fonts
|
||||
del bpy.types.Scene.dataremap_sort_worlds
|
||||
|
||||
# Delete ghost buster properties
|
||||
if hasattr(bpy.types.Scene, "ghost_buster_delete_low_priority"):
|
||||
del bpy.types.Scene.ghost_buster_delete_low_priority
|
||||
|
||||
def get_base_name(name):
|
||||
"""Extract the base name without numbered suffix"""
|
||||
@@ -1224,7 +1235,7 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
|
||||
col.label(text="Ghost data cleanup & library override fixes:")
|
||||
col.label(text="• Unused local WGT widget objects")
|
||||
col.label(text="• Empty unlinked collections")
|
||||
col.label(text="• CC objects not in any scene")
|
||||
col.label(text="• Objects not in scenes with no legitimate use")
|
||||
col.label(text="• Fix broken library override hierarchies")
|
||||
|
||||
# Two button layout
|
||||
@@ -1233,6 +1244,9 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
|
||||
row.operator("bst.ghost_detector", text="Ghost Detector", icon='ZOOM_IN')
|
||||
row.operator("bst.ghost_buster", text="Ghost Buster", icon='GHOST_ENABLED')
|
||||
|
||||
# Ghost Buster option
|
||||
ghost_box.prop(context.scene, "ghost_buster_delete_low_priority", text="Delete Low Priority Ghosts")
|
||||
|
||||
# Resync Enforce button
|
||||
ghost_box.separator()
|
||||
row = ghost_box.row()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import bpy # type: ignore
|
||||
from bpy.types import Panel, Operator, PropertyGroup # type: ignore
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty, PointerProperty, CollectionProperty # type: ignore
|
||||
import os.path
|
||||
import os
|
||||
import re
|
||||
|
||||
class REMOVE_EXT_OT_summary_dialog(bpy.types.Operator):
|
||||
@@ -72,6 +72,18 @@ def get_image_paths(image_name):
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
def ensure_directory_for_path(path):
|
||||
"""
|
||||
Ensure the directory for the provided path exists.
|
||||
Handles Blender-style relative paths as well.
|
||||
"""
|
||||
if not path:
|
||||
return
|
||||
abs_path = bpy.path.abspath(path)
|
||||
directory = os.path.dirname(abs_path)
|
||||
if directory and not os.path.exists(directory):
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
def get_image_extension(image):
|
||||
"""
|
||||
Get the file extension from an image
|
||||
@@ -109,7 +121,7 @@ def get_image_extension(image):
|
||||
print(f"DEBUG: No matching format found, returning empty extension")
|
||||
return ''
|
||||
|
||||
def set_image_paths(image_name, new_path):
|
||||
def set_image_paths(image_name, new_path, tile_paths=None):
|
||||
"""
|
||||
Set filepath and filepath_raw for an image using its datablock name
|
||||
Also update packed_file.filepath if the file is packed
|
||||
@@ -118,11 +130,15 @@ def set_image_paths(image_name, new_path):
|
||||
image_name (str): The name of the image datablock
|
||||
new_path (str): The new path to assign
|
||||
|
||||
Args:
|
||||
tile_paths (dict, optional): Mapping of UDIM tile numbers to filepaths
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False if image not found
|
||||
"""
|
||||
if image_name in bpy.data.images:
|
||||
img = bpy.data.images[image_name]
|
||||
ensure_directory_for_path(new_path)
|
||||
|
||||
# Set the filepath properties
|
||||
img.filepath = new_path
|
||||
@@ -141,6 +157,24 @@ def set_image_paths(image_name, new_path):
|
||||
# If it fails, the original filepaths (img.filepath and img.filepath_raw)
|
||||
# are still set, which is better than nothing
|
||||
pass
|
||||
|
||||
# Support UDIM/tiled images
|
||||
if tile_paths and hasattr(img, "tiles"):
|
||||
for tile in img.tiles:
|
||||
tile_number = str(getattr(tile, "number", "1001"))
|
||||
tile_path = tile_paths.get(tile_number)
|
||||
if not tile_path:
|
||||
continue
|
||||
if not hasattr(tile, "filepath"):
|
||||
# Blender versions prior to 4.0 don't expose per-tile filepaths;
|
||||
# rely on the UDIM template instead.
|
||||
continue
|
||||
ensure_directory_for_path(tile_path)
|
||||
try:
|
||||
tile.filepath = tile_path
|
||||
except AttributeError:
|
||||
# Some builds still expose the attribute but keep it read-only.
|
||||
pass
|
||||
|
||||
return True
|
||||
else:
|
||||
@@ -760,14 +794,21 @@ class BST_OT_pack_images(Operator):
|
||||
selected_images = list(bpy.data.images)
|
||||
|
||||
for img in selected_images:
|
||||
if not img.packed_file and not img.is_generated:
|
||||
try:
|
||||
print(f"DEBUG: Packing image: {img.name}")
|
||||
img.pack()
|
||||
packed_count += 1
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Failed to pack {img.name}: {str(e)}")
|
||||
failed_count += 1
|
||||
# Skip images that can't or shouldn't be packed
|
||||
if (img.packed_file or # Already packed
|
||||
img.source == 'GENERATED' or # Procedurally generated
|
||||
img.source == 'VIEWER' or # Render Result, Viewer Node, etc.
|
||||
not img.filepath or # No file path
|
||||
img.name in ['Render Result', 'Viewer Node']): # Special Blender images
|
||||
continue
|
||||
|
||||
try:
|
||||
print(f"DEBUG: Packing image: {img.name}")
|
||||
img.pack()
|
||||
packed_count += 1
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Failed to pack {img.name}: {str(e)}")
|
||||
failed_count += 1
|
||||
|
||||
if packed_count > 0:
|
||||
self.report({'INFO'}, f"Successfully packed {packed_count} images" +
|
||||
@@ -1261,12 +1302,6 @@ class BST_OT_cancel_operation(Operator):
|
||||
props.cancel_operation = True
|
||||
props.operation_status = "Cancelling operation..."
|
||||
|
||||
# Also set internal cancellation flags for any running operations
|
||||
# This helps with operations that might be in restricted contexts
|
||||
for timer in bpy.app.timers:
|
||||
if hasattr(timer, '_cancelled'):
|
||||
timer._cancelled = True
|
||||
|
||||
self.report({'INFO'}, "Operation cancellation requested")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ from ..ops.NoSubdiv import NoSubdiv
|
||||
from ..ops.remove_custom_split_normals import RemoveCustomSplitNormals
|
||||
from ..ops.create_ortho_camera import CreateOrthoCamera
|
||||
from ..ops.spawn_scene_structure import SpawnSceneStructure
|
||||
from ..ops.delete_single_keyframe_actions import DeleteSingleKeyframeActions
|
||||
from ..ops.find_material_users import FindMaterialUsers, MATERIAL_USERS_OT_summary_dialog
|
||||
from ..ops.remove_unused_material_slots import RemoveUnusedMaterialSlots
|
||||
from ..ops.convert_relations_to_constraint import ConvertRelationsToConstraint
|
||||
|
||||
class BulkSceneGeneral(bpy.types.Panel):
|
||||
"""Bulk Scene General Panel"""
|
||||
@@ -39,6 +43,22 @@ class BulkSceneGeneral(bpy.types.Panel):
|
||||
row = box.row(align=True)
|
||||
row.operator("bst.free_gpu", text="Free GPU", icon='MEMORY')
|
||||
|
||||
# Materials section
|
||||
box = layout.box()
|
||||
box.label(text="Materials")
|
||||
row = box.row(align=True)
|
||||
row.operator("bst.remove_unused_material_slots", text="Remove Unused Material Slots", icon='MATERIAL')
|
||||
row = box.row(align=True)
|
||||
row.operator("bst.find_material_users", text="Find Material Users", icon='VIEWZOOM')
|
||||
|
||||
# Animation Data section
|
||||
box = layout.box()
|
||||
box.label(text="Animation Data")
|
||||
row = box.row(align=True)
|
||||
row.operator("bst.delete_single_keyframe_actions", text="Delete Single Keyframe Actions", icon='ANIM_DATA')
|
||||
row = box.row(align=True)
|
||||
row.operator("bst.convert_relations_to_constraint", text="Convert Relations to Constraint", icon_value=405)
|
||||
|
||||
# List of all classes in this module
|
||||
classes = (
|
||||
BulkSceneGeneral,
|
||||
@@ -46,6 +66,11 @@ classes = (
|
||||
RemoveCustomSplitNormals,
|
||||
CreateOrthoCamera,
|
||||
SpawnSceneStructure,
|
||||
DeleteSingleKeyframeActions,
|
||||
FindMaterialUsers,
|
||||
MATERIAL_USERS_OT_summary_dialog,
|
||||
RemoveUnusedMaterialSlots,
|
||||
ConvertRelationsToConstraint,
|
||||
)
|
||||
|
||||
# Registration
|
||||
@@ -58,6 +83,12 @@ def register():
|
||||
description="Apply only to selected objects",
|
||||
default=True
|
||||
)
|
||||
# Register temporary material property for Find Material Users operator
|
||||
bpy.types.Scene.bst_temp_material = bpy.props.PointerProperty(
|
||||
name="Temporary Material",
|
||||
description="Temporary material selection for Find Material Users operator",
|
||||
type=bpy.types.Material
|
||||
)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
@@ -67,4 +98,7 @@ def unregister():
|
||||
pass
|
||||
# Unregister the window manager property
|
||||
if hasattr(bpy.types.WindowManager, "bst_no_subdiv_only_selected"):
|
||||
del bpy.types.WindowManager.bst_no_subdiv_only_selected
|
||||
del bpy.types.WindowManager.bst_no_subdiv_only_selected
|
||||
# Unregister temporary material property
|
||||
if hasattr(bpy.types.Scene, "bst_temp_material"):
|
||||
del bpy.types.Scene.bst_temp_material
|
||||
@@ -266,6 +266,74 @@ class VIEWPORT_OT_SetViewportColors(bpy.types.Operator):
|
||||
|
||||
bpy.context.window_manager.popup_menu(draw_popup, title="Processing Complete", icon='INFO')
|
||||
|
||||
|
||||
class VIEWPORT_OT_RefreshMaterialPreviews(bpy.types.Operator):
|
||||
"""Regenerate material previews to avoid stale thumbnails"""
|
||||
bl_idname = "bst.refresh_material_previews"
|
||||
bl_label = "Refresh Material Previews"
|
||||
bl_options = {'REGISTER'}
|
||||
|
||||
def execute(self, context):
|
||||
forced_count = 0
|
||||
try:
|
||||
bpy.ops.wm.previews_clear()
|
||||
bpy.ops.wm.previews_batch_generate()
|
||||
bpy.ops.wm.previews_ensure()
|
||||
except Exception as exc:
|
||||
self.report({'WARNING'}, f"Pre-clearing previews failed: {exc}")
|
||||
|
||||
temp_obj = self._create_preview_object(context)
|
||||
|
||||
try:
|
||||
for material in bpy.data.materials:
|
||||
if not material or material.is_grease_pencil:
|
||||
continue
|
||||
|
||||
try:
|
||||
self._force_preview(material, temp_obj)
|
||||
forced_count += 1
|
||||
except Exception as exc:
|
||||
print(f"BST preview refresh: failed for {material.name}: {exc}")
|
||||
finally:
|
||||
self._cleanup_preview_object(temp_obj)
|
||||
|
||||
message = f"Material previews refreshed ({forced_count} materials)"
|
||||
self.report({'INFO'}, message)
|
||||
return {'FINISHED'}
|
||||
|
||||
def _create_preview_object(self, context):
|
||||
mesh = bpy.data.meshes.new("BST_PreviewMesh")
|
||||
mesh.from_pydata(
|
||||
[(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)],
|
||||
[],
|
||||
[(0, 1, 2), (0, 2, 3), (0, 3, 1), (1, 3, 2)]
|
||||
)
|
||||
obj = bpy.data.objects.new("BST_PreviewObject", mesh)
|
||||
obj.hide_viewport = True
|
||||
obj.hide_render = True
|
||||
context.scene.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
def _cleanup_preview_object(self, obj):
|
||||
if not obj:
|
||||
return
|
||||
mesh = obj.data
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
if mesh:
|
||||
bpy.data.meshes.remove(mesh, do_unlink=True)
|
||||
|
||||
def _force_preview(self, material, temp_obj):
|
||||
if temp_obj.data.materials:
|
||||
temp_obj.data.materials[0] = material
|
||||
else:
|
||||
temp_obj.data.materials.append(material)
|
||||
material.preview_render_type = 'SPHERE'
|
||||
preview = material.preview_ensure()
|
||||
if preview:
|
||||
# Touch icon id to ensure generation
|
||||
_ = preview.icon_id
|
||||
|
||||
|
||||
def correct_viewport_color(color):
|
||||
"""Adjust viewport colors by color intensity and saturation"""
|
||||
r, g, b = color
|
||||
@@ -702,6 +770,7 @@ class VIEW3D_PT_BulkViewportDisplay(bpy.types.Panel):
|
||||
# Add primary settings
|
||||
col = box.column(align=True)
|
||||
col.prop(context.scene, "viewport_colors_selected_only")
|
||||
col.operator("bst.refresh_material_previews", icon='FILE_REFRESH')
|
||||
|
||||
# Add advanced options in a collapsible section
|
||||
row = box.row()
|
||||
@@ -936,6 +1005,7 @@ class VIEWPORT_OT_SelectDiffuseNodes(bpy.types.Operator):
|
||||
# List of all classes in this module
|
||||
classes = (
|
||||
VIEWPORT_OT_SetViewportColors,
|
||||
VIEWPORT_OT_RefreshMaterialPreviews,
|
||||
VIEW3D_PT_BulkViewportDisplay,
|
||||
MATERIAL_OT_SelectInEditor,
|
||||
VIEWPORT_OT_SelectDiffuseNodes,
|
||||
|
||||
Reference in New Issue
Block a user