2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -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,