2026-02-16

This commit is contained in:
2026-03-17 15:25:32 -06:00
parent d5dd373de0
commit 60100fbab2
560 changed files with 33397 additions and 20776 deletions
@@ -5,14 +5,14 @@ import sys
import subprocess
# Import ghost buster functionality
from ..ops.ghost_buster import GhostBuster, GhostDetector, ResyncEnforce
from ..ops.ghost_buster import RBST_Bustin_OT_GhostBuster, RBST_Bustin_OT_GhostDetector, RBST_Bustin_OT_ResyncEnforce
from ..utils import compat
# Regular expression to match numbered suffixes like .001, .002, _001, _0001, etc.
NUMBERED_SUFFIX_PATTERN = re.compile(r'(.*?)[._](\d{3,})$')
RBST_DatRem_NUMBERED_SUFFIX_PATTERN = re.compile(r'(.*?)[._](\d{3,})$')
# Function to check if any datablocks in a collection are linked from a library
def has_linked_datablocks(data_collection):
def RBST_DatRem_has_linked_datablocks(data_collection):
"""Check if any datablocks in the collection are linked from a library"""
for data in data_collection:
if data.users > 0 and hasattr(data, 'library') and data.library is not None:
@@ -20,7 +20,7 @@ def has_linked_datablocks(data_collection):
return False
# Register properties for data remap settings
def register_dataremap_properties():
def RBST_DatRem_register_properties():
bpy.types.Scene.dataremap_images = bpy.props.BoolProperty( # type: ignore
name="Images",
description="Find and remap duplicate images",
@@ -136,7 +136,7 @@ def register_dataremap_properties():
default=False
)
def unregister_dataremap_properties():
def RBST_DatRem_unregister_properties():
del bpy.types.Scene.dataremap_images
del bpy.types.Scene.dataremap_materials
del bpy.types.Scene.dataremap_fonts
@@ -162,14 +162,14 @@ def unregister_dataremap_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):
def RBST_DatRem_get_base_name(name):
"""Extract the base name without numbered suffix"""
match = NUMBERED_SUFFIX_PATTERN.match(name)
match = RBST_DatRem_NUMBERED_SUFFIX_PATTERN.match(name)
if match:
return match.group(1) # Return the base name
return name
def find_data_groups(data_collection):
def RBST_DatRem_find_data_groups(data_collection):
"""Group data blocks by their base name, excluding those with no users or linked from libraries"""
groups = {}
@@ -182,7 +182,7 @@ def find_data_groups(data_collection):
if hasattr(data, 'library') and data.library is not None:
continue
base_name = get_base_name(data.name)
base_name = RBST_DatRem_get_base_name(data.name)
# Only group local datablocks
if base_name not in groups:
@@ -194,7 +194,7 @@ def find_data_groups(data_collection):
return {name: items for name, items in groups.items()
if len(items) > 1 and any(not (hasattr(item, 'library') and item.library is not None) for item in items)}
def find_target_data(data_group):
def RBST_DatRem_find_target_data(data_group):
"""Find the target data block to remap to"""
# Filter out linked datablocks
local_data_group = [data for data in data_group if not (hasattr(data, 'library') and data.library is not None)]
@@ -205,7 +205,7 @@ def find_target_data(data_group):
# First, try to find a data block without a numbered suffix
for data in local_data_group:
if get_base_name(data.name) == data.name:
if RBST_DatRem_get_base_name(data.name) == data.name:
return data
# If no unnumbered version exists, find the "youngest" version (highest number)
@@ -213,7 +213,7 @@ def find_target_data(data_group):
highest_suffix = 0
for data in local_data_group:
match = NUMBERED_SUFFIX_PATTERN.match(data.name)
match = RBST_DatRem_NUMBERED_SUFFIX_PATTERN.match(data.name)
if match:
suffix_num = int(match.group(2))
if suffix_num > highest_suffix:
@@ -222,7 +222,7 @@ def find_target_data(data_group):
return youngest
def clean_data_names(data_collection):
def RBST_DatRem_clean_data_names(data_collection):
"""Remove numbered suffixes from all data blocks with users"""
cleaned_count = 0
@@ -235,14 +235,14 @@ def clean_data_names(data_collection):
if hasattr(data, 'library') and data.library is not None:
continue
base_name = get_base_name(data.name)
base_name = RBST_DatRem_get_base_name(data.name)
if base_name != data.name:
data.name = base_name
cleaned_count += 1
return cleaned_count
def remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap_worlds):
def RBST_DatRem_remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap_worlds):
"""Remap redundant data blocks to their base versions like Blender's Remap Users function, and clean up names."""
remapped_count = 0
cleaned_count = 0
@@ -250,18 +250,18 @@ def remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap
# Process images
if remap_images:
# First remap duplicates
image_groups = find_data_groups(bpy.data.images)
image_groups = RBST_DatRem_find_data_groups(bpy.data.images)
for base_name, images in image_groups.items():
# Skip excluded groups
if f"images:{base_name}" in context.scene.excluded_remap_groups:
continue
target_image = find_target_data(images)
target_image = RBST_DatRem_find_target_data(images)
# Rename the target if it has a numbered suffix and is the youngest
if get_base_name(target_image.name) != target_image.name:
if RBST_DatRem_get_base_name(target_image.name) != target_image.name:
try:
target_image.name = get_base_name(target_image.name)
target_image.name = RBST_DatRem_get_base_name(target_image.name)
except AttributeError:
# Skip if the target is linked and can't be renamed
print(f"Warning: Cannot rename linked image {target_image.name}")
@@ -323,23 +323,23 @@ def remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap
# This matches Blender's Remap Users behavior
# Then clean up any remaining numbered suffixes
cleaned_count += clean_data_names(bpy.data.images)
cleaned_count += RBST_DatRem_clean_data_names(bpy.data.images)
# Process materials
if remap_materials:
# First remap duplicates
material_groups = find_data_groups(bpy.data.materials)
material_groups = RBST_DatRem_find_data_groups(bpy.data.materials)
for base_name, materials in material_groups.items():
# Skip excluded groups
if f"materials:{base_name}" in context.scene.excluded_remap_groups:
continue
target_material = find_target_data(materials)
target_material = RBST_DatRem_find_target_data(materials)
# Rename the target if it has a numbered suffix and is the youngest
if get_base_name(target_material.name) != target_material.name:
if RBST_DatRem_get_base_name(target_material.name) != target_material.name:
try:
target_material.name = get_base_name(target_material.name)
target_material.name = RBST_DatRem_get_base_name(target_material.name)
except AttributeError:
# Skip if the target is linked and can't be renamed
print(f"Warning: Cannot rename linked material {target_material.name}")
@@ -443,23 +443,23 @@ def remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap
# This matches Blender's Remap Users behavior
# Then clean up any remaining numbered suffixes
cleaned_count += clean_data_names(bpy.data.materials)
cleaned_count += RBST_DatRem_clean_data_names(bpy.data.materials)
# Process fonts
if remap_fonts:
# First remap duplicates
font_groups = find_data_groups(bpy.data.fonts)
font_groups = RBST_DatRem_find_data_groups(bpy.data.fonts)
for base_name, fonts in font_groups.items():
# Skip excluded groups
if f"fonts:{base_name}" in context.scene.excluded_remap_groups:
continue
target_font = find_target_data(fonts)
target_font = RBST_DatRem_find_target_data(fonts)
# Rename the target if it has a numbered suffix and is the youngest
if get_base_name(target_font.name) != target_font.name:
if RBST_DatRem_get_base_name(target_font.name) != target_font.name:
try:
target_font.name = get_base_name(target_font.name)
target_font.name = RBST_DatRem_get_base_name(target_font.name)
except AttributeError:
# Skip if the target is linked and can't be renamed
print(f"Warning: Cannot rename linked font {target_font.name}")
@@ -519,23 +519,23 @@ def remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap
# This matches Blender's Remap Users behavior
# Then clean up any remaining numbered suffixes
cleaned_count += clean_data_names(bpy.data.fonts)
cleaned_count += RBST_DatRem_clean_data_names(bpy.data.fonts)
# Process worlds
if remap_worlds:
# First remap duplicates
world_groups = find_data_groups(bpy.data.worlds)
world_groups = RBST_DatRem_find_data_groups(bpy.data.worlds)
for base_name, worlds in world_groups.items():
# Skip excluded groups
if f"worlds:{base_name}" in context.scene.excluded_remap_groups:
continue
target_world = find_target_data(worlds)
target_world = RBST_DatRem_find_target_data(worlds)
# Rename the target if it has a numbered suffix and is the youngest
if get_base_name(target_world.name) != target_world.name:
if RBST_DatRem_get_base_name(target_world.name) != target_world.name:
try:
target_world.name = get_base_name(target_world.name)
target_world.name = RBST_DatRem_get_base_name(target_world.name)
except AttributeError:
# Skip if the target is linked and can't be renamed
print(f"Warning: Cannot rename linked world {target_world.name}")
@@ -585,7 +585,7 @@ def remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap
# This matches Blender's Remap Users behavior
# Then clean up any remaining numbered suffixes
cleaned_count += clean_data_names(bpy.data.worlds)
cleaned_count += RBST_DatRem_clean_data_names(bpy.data.worlds)
# Force an update of the dependency graph to ensure all users are properly updated
if context.view_layer:
@@ -593,7 +593,7 @@ def remap_data_blocks(context, remap_images, remap_materials, remap_fonts, remap
return remapped_count, cleaned_count
class DATAREMAP_OT_RemapData(bpy.types.Operator):
class RBST_DatRem_OT_RemapData(bpy.types.Operator):
"""Remap redundant data blocks to reduce duplicates"""
bl_idname = "bst.bulk_data_remap"
bl_label = "Remap Data"
@@ -607,10 +607,10 @@ class DATAREMAP_OT_RemapData(bpy.types.Operator):
remap_worlds = context.scene.dataremap_worlds
# Count duplicates before remapping (only for local datablocks)
image_groups = find_data_groups(bpy.data.images) if remap_images else {}
material_groups = find_data_groups(bpy.data.materials) if remap_materials else {}
font_groups = find_data_groups(bpy.data.fonts) if remap_fonts else {}
world_groups = find_data_groups(bpy.data.worlds) if remap_worlds else {}
image_groups = RBST_DatRem_find_data_groups(bpy.data.images) if remap_images else {}
material_groups = RBST_DatRem_find_data_groups(bpy.data.materials) if remap_materials else {}
font_groups = RBST_DatRem_find_data_groups(bpy.data.fonts) if remap_fonts else {}
world_groups = RBST_DatRem_find_data_groups(bpy.data.worlds) if remap_worlds else {}
total_duplicates = sum(len(group) - 1 for groups in [image_groups, material_groups, font_groups, world_groups] for group in groups.values())
@@ -620,29 +620,29 @@ class DATAREMAP_OT_RemapData(bpy.types.Operator):
total_numbered += sum(1 for img in bpy.data.images
if img.users > 0
and not (hasattr(img, 'library') and img.library is not None)
and get_base_name(img.name) != img.name)
and RBST_DatRem_get_base_name(img.name) != img.name)
if remap_materials:
total_numbered += sum(1 for mat in bpy.data.materials
if mat.users > 0
and not (hasattr(mat, 'library') and mat.library is not None)
and get_base_name(mat.name) != mat.name)
and RBST_DatRem_get_base_name(mat.name) != mat.name)
if remap_fonts:
total_numbered += sum(1 for font in bpy.data.fonts
if font.users > 0
and not (hasattr(font, 'library') and font.library is not None)
and get_base_name(font.name) != font.name)
and RBST_DatRem_get_base_name(font.name) != font.name)
if remap_worlds:
total_numbered += sum(1 for world in bpy.data.worlds
if world.users > 0
and not (hasattr(world, 'library') and world.library is not None)
and get_base_name(world.name) != world.name)
and RBST_DatRem_get_base_name(world.name) != world.name)
if total_duplicates == 0 and total_numbered == 0:
self.report({'INFO'}, "No local data blocks to process")
return {'CANCELLED'}
# Perform the remapping and cleaning
remapped_count, cleaned_count = remap_data_blocks(
remapped_count, cleaned_count = RBST_DatRem_remap_data_blocks(
context,
remap_images,
remap_materials,
@@ -662,8 +662,80 @@ class DATAREMAP_OT_RemapData(bpy.types.Operator):
return {'FINISHED'}
# Add a new operator for merging duplicates using data-block utilities
class RBST_DatRem_OT_MergeDuplicatesWithDBU(bpy.types.Operator):
"""Merge duplicates using data-block utilities addon for all supported data types"""
bl_idname = "bst.merge_duplicates_dbu"
bl_label = "Merge Duplicates (DBU)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
# Check if data-block utilities addon is installed
if not hasattr(context.scene, 'dbu_similar_settings'):
self.report({'ERROR'}, "Data-block utilities addon is not installed or enabled")
return {'CANCELLED'}
# Data types to process in order
data_types = ['NODETREE', 'MATERIAL', 'LIGHT', 'IMAGE', 'MESH']
type_labels = {
'NODETREE': 'Node Groups',
'MATERIAL': 'Materials',
'LIGHT': 'Lights',
'IMAGE': 'Images',
'MESH': 'Meshes'
}
total_merged = 0
processed_types = []
try:
settings = context.scene.dbu_similar_settings
for id_type in data_types:
# Set the id_type
settings.id_type = id_type
# Find similar and duplicates
try:
bpy.ops.scene.dbu_find_similar_and_duplicates()
except Exception as e:
self.report({'WARNING'}, f"Failed to find duplicates for {type_labels[id_type]}: {str(e)}")
continue
# Check if any duplicates were found
if not settings.duplicates:
continue
# Count items to be merged (each group has duplicates, so count items - groups)
# Each group keeps one item, so we count (total items - number of groups)
total_items = sum(len(group.group) for group in settings.duplicates)
num_groups = len(settings.duplicates)
items_to_remove = total_items - num_groups # One item per group is kept
# Merge duplicates
try:
bpy.ops.scene.dbu_merge_duplicates()
total_merged += items_to_remove
processed_types.append(f"{type_labels[id_type]} ({items_to_remove})")
except Exception as e:
self.report({'WARNING'}, f"Failed to merge duplicates for {type_labels[id_type]}: {str(e)}")
continue
# Report results
if total_merged > 0:
types_str = ", ".join(processed_types)
self.report({'INFO'}, f"Merged {total_merged} duplicate(s) across: {types_str}")
else:
self.report({'INFO'}, "No duplicates found to merge")
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Error during merge operation: {str(e)}")
return {'CANCELLED'}
# Add a new operator for purging unused data
class DATAREMAP_OT_PurgeUnused(bpy.types.Operator):
class RBST_DatRem_OT_PurgeUnused(bpy.types.Operator):
"""Purge all unused data-blocks from the file (equivalent to File > Clean Up > Purge Unused Data)"""
bl_idname = "bst.purge_unused_data"
bl_label = "Purge Unused Data"
@@ -678,7 +750,7 @@ class DATAREMAP_OT_PurgeUnused(bpy.types.Operator):
return {'FINISHED'}
# Add a new operator for toggling group exclusion
class DATAREMAP_OT_ToggleGroupExclusion(bpy.types.Operator):
class RBST_DatRem_OT_ToggleGroupExclusion(bpy.types.Operator):
"""Toggle whether this group should be included in remapping"""
bl_idname = "bst.toggle_group_exclusion"
bl_label = "Toggle Group"
@@ -712,7 +784,7 @@ class DATAREMAP_OT_ToggleGroupExclusion(bpy.types.Operator):
return {'FINISHED'}
class DATAREMAP_OT_SelectAllGroups(bpy.types.Operator):
class RBST_DatRem_OT_SelectAllGroups(bpy.types.Operator):
"""Select or deselect all groups of a specific data type"""
bl_idname = "bst.select_all_data_groups"
bl_label = "Select All Groups"
@@ -738,13 +810,13 @@ class DATAREMAP_OT_SelectAllGroups(bpy.types.Operator):
# Get the appropriate data groups based on data_type
data_groups = {}
if self.data_type == "images":
data_groups = find_data_groups(bpy.data.images)
data_groups = RBST_DatRem_find_data_groups(bpy.data.images)
elif self.data_type == "materials":
data_groups = find_data_groups(bpy.data.materials)
data_groups = RBST_DatRem_find_data_groups(bpy.data.materials)
elif self.data_type == "fonts":
data_groups = find_data_groups(bpy.data.fonts)
data_groups = RBST_DatRem_find_data_groups(bpy.data.fonts)
elif self.data_type == "worlds":
data_groups = find_data_groups(bpy.data.worlds)
data_groups = RBST_DatRem_find_data_groups(bpy.data.worlds)
# Process only groups with more than one item
for base_name, items in data_groups.items():
@@ -762,7 +834,7 @@ class DATAREMAP_OT_SelectAllGroups(bpy.types.Operator):
return {'FINISHED'}
# Update the toggle group selection operator to handle shift-click range selection
class DATAREMAP_OT_ToggleGroupSelection(bpy.types.Operator):
class RBST_DatRem_OT_ToggleGroupSelection(bpy.types.Operator):
"""Toggle whether this group should be included in remapping"""
bl_idname = "bst.toggle_group_selection"
bl_label = "Toggle Group Selection"
@@ -803,13 +875,13 @@ class DATAREMAP_OT_ToggleGroupSelection(bpy.types.Operator):
# Get all data groups for this data type
data_groups = []
if self.data_type == "images":
data_groups = list(find_data_groups(bpy.data.images).keys())
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.images).keys())
elif self.data_type == "materials":
data_groups = list(find_data_groups(bpy.data.materials).keys())
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.materials).keys())
elif self.data_type == "fonts":
data_groups = list(find_data_groups(bpy.data.fonts).keys())
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.fonts).keys())
elif self.data_type == "worlds":
data_groups = list(find_data_groups(bpy.data.worlds).keys())
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.worlds).keys())
# Find the indices of the last clicked group and the current group
try:
@@ -869,7 +941,7 @@ class DATAREMAP_OT_ToggleGroupSelection(bpy.types.Operator):
return {'FINISHED'}
# Add a custom draw function for checkboxes that supports drag selection
def draw_drag_selectable_checkbox(layout, context, data_type, group_key):
def RBST_DatRem_draw_drag_selectable_checkbox(layout, context, data_type, group_key):
"""Draw a checkbox that supports drag selection"""
# Create a unique key for this group
key = f"{data_type}:{group_key}"
@@ -885,7 +957,7 @@ def draw_drag_selectable_checkbox(layout, context, data_type, group_key):
op.group_key = group_key
op.data_type = data_type
def search_matches_group(group, search_string):
def RBST_DatRem_search_matches_group(group, search_string):
"""Check if search string matches group base name or any item in group"""
if not search_string:
return True
@@ -901,7 +973,7 @@ def search_matches_group(group, search_string):
return False
# Update the UI code to use the custom draw function
def draw_data_duplicates(layout, context, data_type, data_groups):
def RBST_DatRem_draw_data_duplicates(layout, context, data_type, data_groups):
"""Draw the list of duplicate data items with drag-selectable checkboxes and click to rename"""
box_dup = layout.box()
@@ -945,7 +1017,7 @@ def draw_data_duplicates(layout, context, data_type, data_groups):
search_string = getattr(context.scene, search_prop_name)
if search_string:
group_items = [group for group in group_items if search_matches_group(group, search_string)]
group_items = [group for group in group_items if RBST_DatRem_search_matches_group(group, search_string)]
# Sort by selection if enabled
sort_prop_name = f"dataremap_sort_{data_type}"
@@ -965,7 +1037,7 @@ def draw_data_duplicates(layout, context, data_type, data_groups):
row = box_dup.row()
# Add checkbox to include/exclude this group using the custom draw function
draw_drag_selectable_checkbox(row, context, data_type, base_name)
RBST_DatRem_draw_drag_selectable_checkbox(row, context, data_type, base_name)
# Add dropdown toggle
group_key = f"{data_type}:{base_name}"
@@ -979,7 +1051,7 @@ def draw_data_duplicates(layout, context, data_type, data_groups):
exp_op.data_type = data_type
# Find the original data item (target)
target_item = find_target_data(items)
target_item = RBST_DatRem_find_target_data(items)
# Add icon based on data type
if data_type == "images":
@@ -1046,10 +1118,10 @@ def draw_data_duplicates(layout, context, data_type, data_groups):
rename_op.data_type = data_type
rename_op.old_name = item.name
class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
"""Bulk Data Remap Panel"""
bl_label = "Bulk Data Remap"
bl_idname = "VIEW3D_PT_bulk_data_remap"
bl_idname = "RBST_DatRem_PT_bulk_data_remap"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Edit'
@@ -1068,25 +1140,25 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
linked_types = []
linked_paths = set()
if context.scene.dataremap_images and has_linked_datablocks(bpy.data.images):
if context.scene.dataremap_images and RBST_DatRem_has_linked_datablocks(bpy.data.images):
linked_datablocks_found = True
linked_types.append("images")
linked_paths.update(get_linked_file_paths(bpy.data.images))
linked_paths.update(RBST_DatRem_get_linked_file_paths(bpy.data.images))
if context.scene.dataremap_materials and has_linked_datablocks(bpy.data.materials):
if context.scene.dataremap_materials and RBST_DatRem_has_linked_datablocks(bpy.data.materials):
linked_datablocks_found = True
linked_types.append("materials")
linked_paths.update(get_linked_file_paths(bpy.data.materials))
linked_paths.update(RBST_DatRem_get_linked_file_paths(bpy.data.materials))
if context.scene.dataremap_fonts and has_linked_datablocks(bpy.data.fonts):
if context.scene.dataremap_fonts and RBST_DatRem_has_linked_datablocks(bpy.data.fonts):
linked_datablocks_found = True
linked_types.append("fonts")
linked_paths.update(get_linked_file_paths(bpy.data.fonts))
linked_paths.update(RBST_DatRem_get_linked_file_paths(bpy.data.fonts))
if context.scene.dataremap_worlds and has_linked_datablocks(bpy.data.worlds):
if context.scene.dataremap_worlds and RBST_DatRem_has_linked_datablocks(bpy.data.worlds):
linked_datablocks_found = True
linked_types.append("worlds")
linked_paths.update(get_linked_file_paths(bpy.data.worlds))
linked_paths.update(RBST_DatRem_get_linked_file_paths(bpy.data.worlds))
# Display warning about linked datablocks in a separate section if found
if linked_datablocks_found:
@@ -1116,20 +1188,20 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
col = box.column(align=True)
# Count duplicates and numbered suffixes for each type
image_groups = find_data_groups(bpy.data.images)
material_groups = find_data_groups(bpy.data.materials)
font_groups = find_data_groups(bpy.data.fonts)
world_groups = find_data_groups(bpy.data.worlds)
image_groups = RBST_DatRem_find_data_groups(bpy.data.images)
material_groups = RBST_DatRem_find_data_groups(bpy.data.materials)
font_groups = RBST_DatRem_find_data_groups(bpy.data.fonts)
world_groups = RBST_DatRem_find_data_groups(bpy.data.worlds)
image_duplicates = sum(len(group) - 1 for group in image_groups.values())
material_duplicates = sum(len(group) - 1 for group in material_groups.values())
font_duplicates = sum(len(group) - 1 for group in font_groups.values())
world_duplicates = sum(len(group) - 1 for group in world_groups.values())
image_numbered = sum(1 for img in bpy.data.images if img.users > 0 and get_base_name(img.name) != img.name)
material_numbered = sum(1 for mat in bpy.data.materials if mat.users > 0 and get_base_name(mat.name) != mat.name)
font_numbered = sum(1 for font in bpy.data.fonts if font.users > 0 and get_base_name(font.name) != font.name)
world_numbered = sum(1 for world in bpy.data.worlds if world.users > 0 and get_base_name(world.name) != world.name)
image_numbered = sum(1 for img in bpy.data.images if img.users > 0 and RBST_DatRem_get_base_name(img.name) != img.name)
material_numbered = sum(1 for mat in bpy.data.materials if mat.users > 0 and RBST_DatRem_get_base_name(mat.name) != mat.name)
font_numbered = sum(1 for font in bpy.data.fonts if font.users > 0 and RBST_DatRem_get_base_name(font.name) != font.name)
world_numbered = sum(1 for world in bpy.data.worlds if world.users > 0 and RBST_DatRem_get_base_name(world.name) != world.name)
# Initialize excluded_remap_groups if it doesn't exist
if not hasattr(context.scene, "excluded_remap_groups"):
@@ -1165,7 +1237,7 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
# Show image duplicates if enabled
if context.scene.show_image_duplicates and image_duplicates > 0 and context.scene.dataremap_images:
draw_data_duplicates(col, context, "images", image_groups)
RBST_DatRem_draw_data_duplicates(col, context, "images", image_groups)
# Materials
row = col.row()
@@ -1196,7 +1268,7 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
# Show material duplicates if enabled
if context.scene.show_material_duplicates and material_duplicates > 0 and context.scene.dataremap_materials:
draw_data_duplicates(col, context, "materials", material_groups)
RBST_DatRem_draw_data_duplicates(col, context, "materials", material_groups)
# Fonts
row = col.row()
@@ -1227,7 +1299,7 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
# Show font duplicates if enabled
if context.scene.show_font_duplicates and font_duplicates > 0 and context.scene.dataremap_fonts:
draw_data_duplicates(col, context, "fonts", font_groups)
RBST_DatRem_draw_data_duplicates(col, context, "fonts", font_groups)
# World
row = col.row()
@@ -1258,13 +1330,30 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
# Show world duplicates if enabled
if context.scene.show_world_duplicates and world_duplicates > 0 and context.scene.dataremap_worlds:
draw_data_duplicates(col, context, "worlds", world_groups)
RBST_DatRem_draw_data_duplicates(col, context, "worlds", world_groups)
# Add the operator button
row = box.row()
row.scale_y = 1.5
row.operator("bst.bulk_data_remap")
# Add Data-Block Utils Integration section
box.separator()
dbu_box = box.box()
dbu_box.label(text="Data-Block Utils Integration")
dbu_col = dbu_box.column()
dbu_col.label(text="Merge duplicates using data-block utilities addon")
dbu_col.label(text="Processes: Node Groups, Materials, Lights, Images, Meshes")
# Check if data-block utilities addon is available
if hasattr(context.scene, 'dbu_similar_settings'):
dbu_row = dbu_box.row()
dbu_row.scale_y = 1.5
dbu_row.operator("bst.merge_duplicates_dbu", text="Merge Duplicates (DBU)", icon='FILE_PARENT')
else:
dbu_box.alert = True
dbu_box.label(text="Data-block utilities addon not installed", icon='ERROR')
# Show total counts
total_duplicates = image_duplicates + material_duplicates + font_duplicates + world_duplicates
total_numbered = image_numbered + material_numbered + font_numbered + world_numbered
@@ -1311,7 +1400,7 @@ class VIEW3D_PT_BulkDataRemap(bpy.types.Panel):
row.operator("bst.resync_enforce", text="Resync Enforce", icon='FILE_REFRESH')
# Add a new operator for toggling data types
class DATAREMAP_OT_ToggleDataType(bpy.types.Operator):
class RBST_DatRem_OT_ToggleDataType(bpy.types.Operator):
"""Toggle whether this data type should be included in remapping"""
bl_idname = "bst.toggle_data_type"
bl_label = "Toggle Data Type"
@@ -1336,7 +1425,7 @@ class DATAREMAP_OT_ToggleDataType(bpy.types.Operator):
return {'FINISHED'}
# Add a new operator for toggling group expansion
class DATAREMAP_OT_ToggleGroupExpansion(bpy.types.Operator):
class RBST_DatRem_OT_ToggleGroupExpansion(bpy.types.Operator):
"""Toggle whether this group should be expanded to show details"""
bl_idname = "bst.toggle_group_expansion"
bl_label = "Toggle Group Expansion"
@@ -1371,7 +1460,7 @@ class DATAREMAP_OT_ToggleGroupExpansion(bpy.types.Operator):
return {'FINISHED'}
# Function to get unique linked file paths from datablocks
def get_linked_file_paths(data_collection):
def RBST_DatRem_get_linked_file_paths(data_collection):
"""Get unique file paths of linked libraries from datablocks"""
linked_paths = set()
@@ -1382,7 +1471,7 @@ def get_linked_file_paths(data_collection):
return linked_paths
class DATAREMAP_OT_OpenLinkedFile(bpy.types.Operator):
class RBST_DatRem_OT_OpenLinkedFile(bpy.types.Operator):
"""Open the linked file in a new Blender instance"""
bl_idname = "bst.open_linked_file"
bl_label = "Open Linked File"
@@ -1411,7 +1500,7 @@ class DATAREMAP_OT_OpenLinkedFile(bpy.types.Operator):
return {'FINISHED'}
# Add a new operator for renaming datablocks
class DATAREMAP_OT_RenameDatablock(bpy.types.Operator):
class RBST_DatRem_OT_RenameDatablock(bpy.types.Operator):
"""Click to rename datablock"""
bl_idname = "bst.rename_datablock_remap"
bl_label = "Rename Datablock"
@@ -1483,21 +1572,22 @@ class DATAREMAP_OT_RenameDatablock(bpy.types.Operator):
# List of all classes in this module
classes = (
DATAREMAP_OT_RemapData,
DATAREMAP_OT_PurgeUnused,
DATAREMAP_OT_ToggleDataType,
DATAREMAP_OT_ToggleGroupExclusion,
DATAREMAP_OT_SelectAllGroups,
VIEW3D_PT_BulkDataRemap,
DATAREMAP_OT_ToggleGroupExpansion,
DATAREMAP_OT_ToggleGroupSelection,
DATAREMAP_OT_OpenLinkedFile,
DATAREMAP_OT_RenameDatablock,
RBST_DatRem_OT_RemapData,
RBST_DatRem_OT_MergeDuplicatesWithDBU,
RBST_DatRem_OT_PurgeUnused,
RBST_DatRem_OT_ToggleDataType,
RBST_DatRem_OT_ToggleGroupExclusion,
RBST_DatRem_OT_SelectAllGroups,
RBST_DatRem_PT_BulkDataRemap,
RBST_DatRem_OT_ToggleGroupExpansion,
RBST_DatRem_OT_ToggleGroupSelection,
RBST_DatRem_OT_OpenLinkedFile,
RBST_DatRem_OT_RenameDatablock,
)
# Registration
def register():
register_dataremap_properties()
RBST_DatRem_register_properties()
for cls in classes:
compat.safe_register_class(cls)
@@ -1507,6 +1597,6 @@ def unregister():
compat.safe_unregister_class(cls)
# Unregister properties
try:
unregister_dataremap_properties()
RBST_DatRem_unregister_properties()
except Exception:
pass
@@ -5,7 +5,7 @@ import os
import re
from ..utils import compat
class REMOVE_EXT_OT_summary_dialog(bpy.types.Operator):
class RBST_PathMan_OT_summary_dialog(bpy.types.Operator):
"""Show remove extensions operation summary"""
bl_idname = "remove_ext.summary_dialog"
bl_label = "Remove Extensions Summary"
@@ -204,7 +204,7 @@ def bulk_remap_paths(mapping_dict):
return (success_count, failed_list)
# Properties for path management
class BST_PathProperties(PropertyGroup):
class RBST_PathMan_PG_PathProperties(PropertyGroup):
# Active image pointer
active_image: PointerProperty(
name="Image",
@@ -328,7 +328,7 @@ class BST_PathProperties(PropertyGroup):
)
# Operator to remap a single datablock path
class BST_OT_remap_path(Operator):
class RBST_PathMan_OT_remap_path(Operator):
bl_idname = "bst.remap_path"
bl_label = "Remap Path"
bl_description = "Change the filepath and filepath_raw for a datablock"
@@ -374,7 +374,7 @@ class BST_OT_remap_path(Operator):
return context.window_manager.invoke_props_dialog(self)
# Operator to toggle all image selections
class BST_OT_toggle_select_all(Operator):
class RBST_PathMan_OT_toggle_select_all(Operator):
bl_idname = "bst.toggle_select_all"
bl_label = "Toggle All"
bl_description = "Toggle selection of all datablocks"
@@ -394,7 +394,7 @@ class BST_OT_toggle_select_all(Operator):
return {'FINISHED'}
# Operator to remap multiple paths at once
class BST_OT_bulk_remap(Operator):
class RBST_PathMan_OT_bulk_remap(Operator):
bl_idname = "bst.bulk_remap"
bl_label = "Remap Paths"
bl_description = "Apply the new path to all selected datablocks"
@@ -493,7 +493,7 @@ class BST_OT_bulk_remap(Operator):
return 0.05 # Process next item in 0.05 seconds (50ms) for better stability
# Operator to toggle path editing mode
class BST_OT_toggle_path_edit(Operator):
class RBST_PathMan_OT_toggle_path_edit(Operator):
bl_idname = "bst.toggle_path_edit"
bl_label = "Toggle Path Edit"
bl_description = "Toggle between view and edit mode for paths"
@@ -535,7 +535,7 @@ class BST_OT_toggle_path_edit(Operator):
return {'FINISHED'}
# Operator to select all images used in the current material
class BST_OT_select_material_images(Operator):
class RBST_PathMan_OT_select_material_images(Operator):
bl_idname = "bst.select_material_images"
bl_label = "Select Material Images"
bl_description = "Select all images used in the current material in the node editor"
@@ -569,7 +569,7 @@ class BST_OT_select_material_images(Operator):
return {'FINISHED'}
# Operator to select active/selected image texture nodes
class BST_OT_select_active_images(Operator):
class RBST_PathMan_OT_select_active_images(Operator):
bl_idname = "bst.select_active_images"
bl_label = "Select Active Images"
bl_description = "Select all images from currently selected texture nodes in the node editor"
@@ -603,7 +603,7 @@ class BST_OT_select_active_images(Operator):
return {'FINISHED'}
# Operator to select all images with absolute paths
class BST_OT_select_absolute_images(Operator):
class RBST_PathMan_OT_select_absolute_images(Operator):
bl_idname = "bst.select_absolute_images"
bl_label = "Select Absolute Images"
bl_description = "Select all images with absolute file paths"
@@ -658,7 +658,7 @@ class BST_OT_select_absolute_images(Operator):
return {'FINISHED'}
# Add a class for renaming datablocks
class BST_OT_rename_datablock(Operator):
class RBST_PathMan_OT_rename_datablock(Operator):
"""Click to rename datablock"""
bl_idname = "bst.rename_datablock"
bl_label = "Rename Datablock"
@@ -706,7 +706,7 @@ class BST_OT_rename_datablock(Operator):
return {'CANCELLED'}
# Update class for shift+click selection
class BST_OT_toggle_image_selection(Operator):
class RBST_PathMan_OT_toggle_image_selection(Operator):
"""Toggle whether this image should be included in bulk operations"""
bl_idname = "bst.toggle_image_selection"
bl_label = "Toggle Image Selection"
@@ -759,7 +759,7 @@ class BST_OT_toggle_image_selection(Operator):
return {'FINISHED'}
# Add new operator for reusing material name in path
class BST_OT_reuse_material_path(Operator):
class RBST_PathMan_OT_reuse_material_path(Operator):
"""Use the active material's name in the path"""
bl_idname = "bst.reuse_material_path"
bl_label = "Use Material Path"
@@ -793,7 +793,7 @@ class BST_OT_reuse_material_path(Operator):
return {'CANCELLED'}
# Add new operator for reusing blend file name in path
class BST_OT_reuse_blend_name(Operator):
class RBST_PathMan_OT_reuse_blend_name(Operator):
"""Use the current blend file name in the path"""
bl_idname = "bst.reuse_blend_name"
bl_label = "Use Blend Name"
@@ -817,7 +817,7 @@ class BST_OT_reuse_blend_name(Operator):
return {'CANCELLED'}
# Make Paths Relative Operator
class BST_OT_make_paths_relative(Operator):
class RBST_PathMan_OT_make_paths_relative(Operator):
bl_idname = "bst.make_paths_relative"
bl_label = "Make Paths Relative"
bl_description = "Convert absolute paths to relative paths for all datablocks"
@@ -829,7 +829,7 @@ class BST_OT_make_paths_relative(Operator):
return {'FINISHED'}
# Make Paths Absolute Operator
class BST_OT_make_paths_absolute(Operator):
class RBST_PathMan_OT_make_paths_absolute(Operator):
bl_idname = "bst.make_paths_absolute"
bl_label = "Make Paths Absolute"
bl_description = "Convert relative paths to absolute paths for all datablocks"
@@ -841,7 +841,7 @@ class BST_OT_make_paths_absolute(Operator):
return {'FINISHED'}
# Pack Images Operator
class BST_OT_pack_images(Operator):
class RBST_PathMan_OT_pack_images(Operator):
bl_idname = "bst.pack_images"
bl_label = "Pack Images"
bl_description = "Pack selected images into the .blend file"
@@ -883,7 +883,7 @@ class BST_OT_pack_images(Operator):
return {'FINISHED'}
# Unpack Images Operator
class BST_OT_unpack_images(Operator):
class RBST_PathMan_OT_unpack_images(Operator):
bl_idname = "bst.unpack_images"
bl_label = "Unpack Images (Use Local)"
bl_description = "Unpack selected images to their file paths using the 'USE_LOCAL' option"
@@ -918,7 +918,7 @@ class BST_OT_unpack_images(Operator):
return {'FINISHED'}
# Remove Packed Images Operator
class BST_OT_remove_packed_images(Operator):
class RBST_PathMan_OT_remove_packed_images(Operator):
bl_idname = "bst.remove_packed_images"
bl_label = "Remove Packed Data"
bl_description = "Remove packed image data without saving to disk"
@@ -953,7 +953,7 @@ class BST_OT_remove_packed_images(Operator):
return {'FINISHED'}
# Save All Images Operator
class BST_OT_save_all_images(Operator):
class RBST_PathMan_OT_save_all_images(Operator):
bl_idname = "bst.save_all_images"
bl_label = "Save All Images"
bl_description = "Save all selected images to image paths"
@@ -1059,7 +1059,7 @@ class BST_OT_save_all_images(Operator):
return 0.05 # Process next item in 0.05 seconds (50ms) for better stability
# Remove Extensions Operator
class BST_OT_remove_extensions(Operator):
class RBST_PathMan_OT_remove_extensions(Operator):
bl_idname = "bst.remove_extensions"
bl_label = "Remove Extensions"
bl_description = "Remove common file extensions from selected image datablock names."
@@ -1148,7 +1148,7 @@ class BST_OT_remove_extensions(Operator):
removal_details=details_text.strip())
# Add new operator for flat color texture renaming
class BST_OT_rename_flat_colors(Operator):
class RBST_PathMan_OT_rename_flat_colors(Operator):
"""Rename flat color textures to their hex color values"""
bl_idname = "bst.rename_flat_colors"
bl_label = "Rename Flat Colors"
@@ -1354,7 +1354,7 @@ class BST_OT_rename_flat_colors(Operator):
return 0.05 # Process next item in 0.05 seconds (50ms) for better stability
# Cancel Operation Operator
class BST_OT_cancel_operation(Operator):
class RBST_PathMan_OT_cancel_operation(Operator):
bl_idname = "bst.cancel_operation"
bl_label = "Cancel Operation"
bl_description = "Cancel the currently running operation"
@@ -1433,9 +1433,9 @@ def get_combined_path(context, datablock_name, extension=""):
return path + datablock_name + extension
# Panel for Shader Editor sidebar
class NODE_PT_bulk_path_tools(Panel):
class RBST_PathMan_PT_bulk_path_tools(Panel):
bl_label = "Bulk Pathing"
bl_idname = "NODE_PT_bulk_path_tools"
bl_idname = "RBST_PathMan_PT_bulk_path_tools"
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = 'Node'
@@ -1643,9 +1643,9 @@ class NODE_PT_bulk_path_tools(Panel):
box.label(text="No images in blend file")
# Sub-panel for existing Bulk Scene Tools
class VIEW3D_PT_bulk_path_subpanel(Panel):
class RBST_PathMan_PT_bulk_path_subpanel(Panel):
bl_label = "Bulk Path Management"
bl_idname = "VIEW3D_PT_bulk_path_subpanel"
bl_idname = "RBST_PathMan_PT_bulk_path_subpanel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Edit'
@@ -1655,34 +1655,34 @@ class VIEW3D_PT_bulk_path_subpanel(Panel):
def draw(self, context):
# Use the same draw function as the NODE_EDITOR panel
NODE_PT_bulk_path_tools.draw(self, context)
RBST_PathMan_PT_bulk_path_tools.draw(self, context)
# Registration function for this module
classes = (
REMOVE_EXT_OT_summary_dialog,
BST_PathProperties,
BST_OT_remap_path,
BST_OT_toggle_select_all,
BST_OT_bulk_remap,
BST_OT_toggle_path_edit,
BST_OT_select_material_images,
BST_OT_select_active_images,
BST_OT_select_absolute_images,
BST_OT_rename_datablock,
BST_OT_toggle_image_selection,
BST_OT_reuse_material_path,
BST_OT_reuse_blend_name,
BST_OT_make_paths_relative,
BST_OT_make_paths_absolute,
BST_OT_pack_images,
BST_OT_unpack_images,
BST_OT_remove_packed_images,
BST_OT_save_all_images,
BST_OT_remove_extensions,
BST_OT_rename_flat_colors,
BST_OT_cancel_operation,
NODE_PT_bulk_path_tools,
VIEW3D_PT_bulk_path_subpanel,
RBST_PathMan_OT_summary_dialog,
RBST_PathMan_PG_PathProperties,
RBST_PathMan_OT_remap_path,
RBST_PathMan_OT_toggle_select_all,
RBST_PathMan_OT_bulk_remap,
RBST_PathMan_OT_toggle_path_edit,
RBST_PathMan_OT_select_material_images,
RBST_PathMan_OT_select_active_images,
RBST_PathMan_OT_select_absolute_images,
RBST_PathMan_OT_rename_datablock,
RBST_PathMan_OT_toggle_image_selection,
RBST_PathMan_OT_reuse_material_path,
RBST_PathMan_OT_reuse_blend_name,
RBST_PathMan_OT_make_paths_relative,
RBST_PathMan_OT_make_paths_absolute,
RBST_PathMan_OT_pack_images,
RBST_PathMan_OT_unpack_images,
RBST_PathMan_OT_remove_packed_images,
RBST_PathMan_OT_save_all_images,
RBST_PathMan_OT_remove_extensions,
RBST_PathMan_OT_rename_flat_colors,
RBST_PathMan_OT_cancel_operation,
RBST_PathMan_PT_bulk_path_tools,
RBST_PathMan_PT_bulk_path_subpanel,
)
def register():
@@ -1690,7 +1690,7 @@ def register():
compat.safe_register_class(cls)
# Register properties
bpy.types.Scene.bst_path_props = PointerProperty(type=BST_PathProperties)
bpy.types.Scene.bst_path_props = PointerProperty(type=RBST_PathMan_PG_PathProperties)
# Add custom property to images for selection
bpy.types.Image.bst_selected = BoolProperty(
@@ -4,15 +4,15 @@ 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
from ..ops.white_world import WhiteWorld
from ..utils import compat
class BulkSceneGeneral(bpy.types.Panel):
class RBST_SceneGen_PT_BulkSceneGeneral(bpy.types.Panel):
"""Bulk Scene General Panel"""
bl_label = "Scene General"
bl_idname = "VIEW3D_PT_bulk_scene_general"
bl_idname = "RBST_SceneGen_PT_bulk_scene_general"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Edit'
@@ -28,6 +28,8 @@ class BulkSceneGeneral(bpy.types.Panel):
row = box.row()
row.scale_y = 1.2
row.operator("bst.spawn_scene_structure", text="Spawn Scene Structure", icon='OUTLINER_COLLECTION')
row = box.row(align=True)
row.operator("bst.white_world", text="White World", icon='WORLD')
# Mesh section
box = layout.box()
@@ -49,8 +51,6 @@ class BulkSceneGeneral(bpy.types.Panel):
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()
@@ -62,14 +62,13 @@ class BulkSceneGeneral(bpy.types.Panel):
# List of all classes in this module
classes = (
BulkSceneGeneral,
RBST_SceneGen_PT_BulkSceneGeneral,
NoSubdiv, # Add NoSubdiv operator class
RemoveCustomSplitNormals,
CreateOrthoCamera,
SpawnSceneStructure,
WhiteWorld,
DeleteSingleKeyframeActions,
FindMaterialUsers,
MATERIAL_USERS_OT_summary_dialog,
RemoveUnusedMaterialSlots,
ConvertRelationsToConstraint,
)
@@ -84,19 +83,10 @@ 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):
compat.safe_unregister_class(cls)
# Unregister the window manager property
if hasattr(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
del bpy.types.WindowManager.bst_no_subdiv_only_selected
@@ -6,9 +6,10 @@ from enum import Enum
import colorsys # Add colorsys for RGB to HSV conversion
from ..ops.select_diffuse_nodes import select_diffuse_nodes # Import the specific function
from ..utils import compat
from ..utils import version
# Material processing status enum
class MaterialStatus(Enum):
class RBST_ViewDisp_MaterialStatus(Enum):
PENDING = 0
PROCESSING = 1
COMPLETED = 2
@@ -26,7 +27,7 @@ material_queue = []
current_index = 0
# Scene properties for viewport display settings
def register_viewport_properties():
def RBST_ViewDisp_register_properties():
bpy.types.Scene.viewport_colors_selected_only = bpy.props.BoolProperty( # type: ignore
name="Selected Objects Only",
description="Apply viewport colors only to materials in selected objects",
@@ -104,7 +105,7 @@ def unregister_viewport_properties():
del bpy.types.Scene.viewport_colors_show_advanced
del bpy.types.Scene.show_material_results
class VIEWPORT_OT_SetViewportColors(bpy.types.Operator):
class RBST_ViewDisp_OT_SetViewportColors(bpy.types.Operator):
"""Set Viewport Display colors from BSDF base color or texture"""
bl_idname = "bst.set_viewport_colors"
bl_label = "Set Viewport Colors"
@@ -252,11 +253,11 @@ class VIEWPORT_OT_SetViewportColors(bpy.types.Operator):
failed_count = 0
for _, status in material_results.values():
if status == MaterialStatus.PREVIEW_BASED:
if status == RBST_ViewDisp_MaterialStatus.PREVIEW_BASED:
preview_count += 1
elif status == MaterialStatus.COMPLETED:
elif status == RBST_ViewDisp_MaterialStatus.COMPLETED:
node_count += 1
elif status == MaterialStatus.FAILED:
elif status == RBST_ViewDisp_MaterialStatus.FAILED:
failed_count += 1
# Use a popup menu instead of self.report since this might be called from a timer
@@ -268,7 +269,7 @@ 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):
class RBST_ViewDisp_OT_RefreshMaterialPreviews(bpy.types.Operator):
"""Regenerate material previews to avoid stale thumbnails"""
bl_idname = "bst.refresh_material_previews"
bl_label = "Refresh Material Previews"
@@ -276,22 +277,37 @@ class VIEWPORT_OT_RefreshMaterialPreviews(bpy.types.Operator):
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}")
# Skip the preview clearing operators in Blender 5.0 - they can cause crashes
# Instead, we'll just regenerate previews for each material individually
# This is safer and avoids the EXCEPTION_ACCESS_VIOLATION crashes
if not version.is_version_5_0():
# Only use these operators in older Blender versions
try:
if hasattr(bpy.context, 'temp_override'):
with bpy.context.temp_override():
if hasattr(bpy.ops.wm, 'previews_clear'):
bpy.ops.wm.previews_clear()
else:
if hasattr(bpy.ops.wm, 'previews_clear'):
bpy.ops.wm.previews_clear()
except Exception as exc:
# These operations are optional - continue even if they fail
print(f"BST preview refresh: Pre-clearing previews failed (non-fatal): {exc}")
temp_obj = self._create_preview_object(context)
if not temp_obj:
self.report({'ERROR'}, "Failed to create preview object. Cannot refresh material previews.")
return {'CANCELLED'}
try:
for material in bpy.data.materials:
if not material or material.is_grease_pencil:
continue
try:
self._force_preview(material, temp_obj)
self._force_preview(material, temp_obj, context)
forced_count += 1
except Exception as exc:
print(f"BST preview refresh: failed for {material.name}: {exc}")
@@ -303,36 +319,106 @@ class VIEWPORT_OT_RefreshMaterialPreviews(bpy.types.Operator):
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
"""Create a temporary preview object for material preview generation."""
try:
mesh = bpy.data.meshes.new("RBST_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("RBST_PreviewObject", mesh)
obj.hide_viewport = True
obj.hide_render = True
# Link to scene collection - may fail in some contexts
try:
context.scene.collection.objects.link(obj)
except Exception:
# Fallback: try master collection or view layer
if hasattr(context, 'view_layer') and hasattr(context.view_layer, 'active_layer_collection'):
context.view_layer.active_layer_collection.collection.objects.link(obj)
elif hasattr(bpy.context, 'scene') and hasattr(bpy.context.scene, 'collection'):
bpy.context.scene.collection.objects.link(obj)
return obj
except Exception as exc:
print(f"BST preview refresh: Failed to create preview object: {exc}")
return None
def _cleanup_preview_object(self, obj):
"""Safely cleanup the preview object with proper error handling."""
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)
try:
# Clear any material assignments first to avoid dangling references
if hasattr(obj, 'data') and obj.data and hasattr(obj.data, 'materials'):
try:
obj.data.materials.clear()
except Exception:
pass
# Get mesh reference before removing object
mesh = obj.data if hasattr(obj, 'data') else None
# Remove object
try:
bpy.data.objects.remove(obj, do_unlink=True)
except Exception as exc:
print(f"BST preview refresh: Failed to remove preview object: {exc}")
# Remove mesh if it still exists
if mesh and mesh.name in bpy.data.meshes:
try:
bpy.data.meshes.remove(mesh, do_unlink=True)
except Exception as exc:
print(f"BST preview refresh: Failed to remove preview mesh: {exc}")
except Exception as exc:
print(f"BST preview refresh: Error during cleanup: {exc}")
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 _force_preview(self, material, temp_obj, context):
"""Force preview generation for a material with proper error handling."""
try:
# Assign material to temp object
if temp_obj.data.materials:
temp_obj.data.materials[0] = material
else:
temp_obj.data.materials.append(material)
# Set preview render type if available
if hasattr(material, 'preview_render_type'):
material.preview_render_type = 'SPHERE'
# Ensure preview exists - this may fail in some Blender versions
# In Blender 5.0, accessing icon_id immediately after preview_ensure() can cause crashes
if hasattr(material, 'preview_ensure'):
try:
preview = material.preview_ensure()
# In Blender 4.2/4.5, accessing icon_id is safe and helps ensure generation
# In Blender 5.0+, we skip this to avoid crashes
if not version.is_version_5_0() and preview and hasattr(preview, 'icon_id'):
try:
_ = preview.icon_id
except Exception:
# If icon_id access fails, that's okay - preview_ensure() is enough
pass
except Exception as exc:
# Preview generation may fail for some materials - this is acceptable
print(f"BST preview refresh: Could not generate preview for {material.name}: {exc}")
# Try alternative method: force update by accessing preview property
if hasattr(material, 'preview'):
try:
_ = material.preview
except Exception:
pass
# Clear material assignment to avoid keeping references
if temp_obj.data.materials:
temp_obj.data.materials.clear()
except Exception as exc:
# Re-raise to be caught by caller
raise exc
def correct_viewport_color(color):
@@ -373,11 +459,11 @@ def process_material(material, use_vectorized=True):
"""Process a material to determine its viewport color"""
if not material:
print(f"Material is None, using fallback color")
return (1, 1, 1), MaterialStatus.PREVIEW_BASED
return (1, 1, 1), RBST_ViewDisp_MaterialStatus.PREVIEW_BASED
if material.is_grease_pencil:
print(f"Material {material.name}: is a grease pencil material, using fallback color")
return (1, 1, 1), MaterialStatus.PREVIEW_BASED
return (1, 1, 1), RBST_ViewDisp_MaterialStatus.PREVIEW_BASED
try:
# Get color from material thumbnail
@@ -393,14 +479,14 @@ def process_material(material, use_vectorized=True):
corrected_color = correct_viewport_color(color)
print(f"Material {material.name}: Corrected thumbnail color = {corrected_color}")
return corrected_color, MaterialStatus.PREVIEW_BASED
return corrected_color, RBST_ViewDisp_MaterialStatus.PREVIEW_BASED
else:
print(f"Material {material.name}: Could not extract color from thumbnail, using fallback color")
return (1, 1, 1), MaterialStatus.PREVIEW_BASED
return (1, 1, 1), RBST_ViewDisp_MaterialStatus.PREVIEW_BASED
except Exception as e:
print(f"Error processing material {material.name}: {e}")
return (1, 1, 1), MaterialStatus.FAILED
return (1, 1, 1), RBST_ViewDisp_MaterialStatus.FAILED
def get_average_color(image, use_vectorized=True):
"""Calculate the average color of an image"""
@@ -719,38 +805,38 @@ def find_diffuse_texture(material):
def get_status_icon(status):
"""Get the icon for a material status"""
if status == MaterialStatus.PENDING:
if status == RBST_ViewDisp_MaterialStatus.PENDING:
return 'TRIA_RIGHT'
elif status == MaterialStatus.PROCESSING:
elif status == RBST_ViewDisp_MaterialStatus.PROCESSING:
return 'SORTTIME'
elif status == MaterialStatus.COMPLETED:
elif status == RBST_ViewDisp_MaterialStatus.COMPLETED:
return 'CHECKMARK'
elif status == MaterialStatus.PREVIEW_BASED:
elif status == RBST_ViewDisp_MaterialStatus.PREVIEW_BASED:
return 'IMAGE_DATA'
elif status == MaterialStatus.FAILED:
elif status == RBST_ViewDisp_MaterialStatus.FAILED:
return 'ERROR'
else:
return 'QUESTION'
def get_status_text(status):
"""Get the text for a material status"""
if status == MaterialStatus.PENDING:
if status == RBST_ViewDisp_MaterialStatus.PENDING:
return "Pending"
elif status == MaterialStatus.PROCESSING:
elif status == RBST_ViewDisp_MaterialStatus.PROCESSING:
return "Processing"
elif status == MaterialStatus.COMPLETED:
elif status == RBST_ViewDisp_MaterialStatus.COMPLETED:
return "Node-based"
elif status == MaterialStatus.PREVIEW_BASED:
elif status == RBST_ViewDisp_MaterialStatus.PREVIEW_BASED:
return "Thumbnail-based"
elif status == MaterialStatus.FAILED:
elif status == RBST_ViewDisp_MaterialStatus.FAILED:
return "Failed"
else:
return "Unknown"
class VIEW3D_PT_BulkViewportDisplay(bpy.types.Panel):
class RBST_ViewDisp_PT_BulkViewportDisplay(bpy.types.Panel):
"""Bulk Viewport Display Panel"""
bl_label = "Bulk Viewport Display"
bl_idname = "VIEW3D_PT_bulk_viewport_display"
bl_idname = "RBST_ViewDisp_PT_bulk_viewport_display"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Edit'
@@ -827,9 +913,9 @@ class VIEW3D_PT_BulkViewportDisplay(bpy.types.Panel):
color, status = material_results[material_name]
# Update counts
if status == MaterialStatus.PREVIEW_BASED:
if status == RBST_ViewDisp_MaterialStatus.PREVIEW_BASED:
preview_count += 1
elif status == MaterialStatus.FAILED:
elif status == RBST_ViewDisp_MaterialStatus.FAILED:
failed_count += 1
row = col.row(align=True)
@@ -868,7 +954,7 @@ class VIEW3D_PT_BulkViewportDisplay(bpy.types.Panel):
layout.separator()
layout.operator("bst.select_diffuse_nodes", icon='NODE_TEXTURE')
class MATERIAL_OT_SelectInEditor(bpy.types.Operator):
class RBST_ViewDisp_OT_SelectInEditor(bpy.types.Operator):
"""Select this material in the editor"""
bl_idname = "bst.select_in_editor"
bl_label = "Select Material"
@@ -989,7 +1075,7 @@ def get_color_from_preview(material, use_vectorized=True):
else:
return None
class VIEWPORT_OT_SelectDiffuseNodes(bpy.types.Operator):
class RBST_ViewDisp_OT_SelectDiffuseNodes(bpy.types.Operator):
bl_idname = "bst.select_diffuse_nodes"
bl_label = "Set Texture Display"
bl_description = "Select the most relevant diffuse/base color image texture node in each material"
@@ -1005,11 +1091,11 @@ 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,
RBST_ViewDisp_OT_SetViewportColors,
RBST_ViewDisp_OT_RefreshMaterialPreviews,
RBST_ViewDisp_PT_BulkViewportDisplay,
RBST_ViewDisp_OT_SelectInEditor,
RBST_ViewDisp_OT_SelectDiffuseNodes,
)
# Registration
@@ -1018,12 +1104,12 @@ def register():
compat.safe_register_class(cls)
# Register properties
register_viewport_properties()
RBST_ViewDisp_register_properties()
def unregister():
# Unregister properties
try:
unregister_viewport_properties()
RBST_ViewDisp_unregister_properties()
except Exception:
pass
# Unregister classes