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,15 +5,15 @@ from .panels import bulk_viewport_display
from .panels import bulk_data_remap
from .panels import bulk_path_management
from .panels import bulk_scene_general
from .ops.AutoMatExtractor import AutoMatExtractor, AUTOMAT_OT_summary_dialog
from .ops.Rename_images_by_mat import Rename_images_by_mat, RENAME_OT_summary_dialog
from .ops.FreeGPU import BST_FreeGPU
from .ops.AutoMatExtractor import RBST_AutoMat_OT_AutoMatExtractor, RBST_AutoMat_OT_summary_dialog
from .ops.Rename_images_by_mat import RBST_RenameImg_OT_Rename_images_by_mat, RBST_RenameImg_OT_summary_dialog
from .ops.FreeGPU import RBST_FreeGPU
from .ops import ghost_buster
from . import rainys_repo_bootstrap
from .utils import compat
# Addon preferences class for update settings
class BST_AddonPreferences(AddonPreferences):
class RBST_AddonPreferences(AddonPreferences):
bl_idname = __package__
# AutoMat Extractor settings
@@ -48,12 +48,12 @@ class VIEW3D_PT_BulkSceneTools(Panel):
# List of all classes in this module
classes = (
VIEW3D_PT_BulkSceneTools,
BST_AddonPreferences,
AutoMatExtractor,
AUTOMAT_OT_summary_dialog,
Rename_images_by_mat,
RENAME_OT_summary_dialog,
BST_FreeGPU,
RBST_AddonPreferences,
RBST_AutoMat_OT_AutoMatExtractor,
RBST_AutoMat_OT_summary_dialog,
RBST_RenameImg_OT_Rename_images_by_mat,
RBST_RenameImg_OT_summary_dialog,
RBST_FreeGPU,
)
def register():
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
id = "rainclouds_bulk_scene_tools"
name = "Raincloud's Bulk Scene Tools"
tagline = "Bulk utilities for optimizing scene data"
version = "0.14.0"
version = "0.16.0"
type = "add-on"
maintainer = "RaincloudTheDragon <raincloudthedragon@gmail.com>"
@@ -1,3 +1,23 @@
# v0.16.0 2026-01-15
- Delete Single Keyframe Actions: Blender 5.0 compatible (action layers/strips/channelbags); skip actions from linked libraries
- Find Material Users: removed (issue #14; use Atomic or DBU)
- White World: new operator in Scene General (#13)—white background world, film transparent, orphans purge
# v0.15.1 2026-01-12
- Fixed AutoMatExtractor MSMNAO Pack texture handling: preserve texture type suffixes (MSMNAO Pack, SSTM Pack, etc.) when sanitizing filenames
- Fixed issue #12: corrected duplicated prefix in RBST_ViewDisp_MaterialStatus enum definition
- Added Props subcollection under Char in SpawnSceneStructure operator
# v0.15.0 2026-01-07
- Refactored all class, function, and constant names to use consistent RBST_ prefix system with module-specific sub-prefixes
- Added Data-Block Utils integration: Merge Duplicates button in Bulk Data Remap panel that automates finding and merging duplicates across Node Groups, Materials, Lights, Images, and Meshes
# v0.14.1
- Fixed crash in Blender 5.0 when refreshing material previews (#11)
- Removed problematic batch preview operators and unsafe icon_id access that caused crashes
- Added version-specific optimizations for 4.2/4.5 vs 5.0+ compatibility
- Improved cleanup and reduced CPU usage
# v0.14.0
- Added operator to select all images with absolute paths (#3)
- Added search functionality to filter datablocks in PathMan and Data Remapper panels (#4)
@@ -9,7 +9,7 @@ from ..panels.bulk_path_management import (
)
from ..utils import compat
class AUTOMAT_OT_summary_dialog(bpy.types.Operator):
class RBST_AutoMat_OT_summary_dialog(bpy.types.Operator):
"""Show AutoMat Extractor operation summary"""
bl_idname = "bst.automat_summary_dialog"
bl_label = "AutoMat Extractor Summary"
@@ -62,7 +62,7 @@ class AUTOMAT_OT_summary_dialog(bpy.types.Operator):
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=500)
class AutoMatExtractor(bpy.types.Operator):
class RBST_AutoMat_OT_AutoMatExtractor(bpy.types.Operator):
bl_idname = "bst.automatextractor"
bl_label = "AutoMatExtractor"
bl_description = "Pack selected images and extract them with organized paths by blend file and material"
@@ -405,8 +405,10 @@ class AutoMatExtractor(bpy.types.Operator):
def sanitize_filename(self, filename):
"""Sanitize filename/folder name for filesystem compatibility"""
# First, remove potential file extensions, including numerical ones like .001
base_name = re.sub(r'\.\d{3}$', '', filename) # Remove .001, .002 etc.
base_name = os.path.splitext(base_name)[0] # Remove standard extensions
# Remove .001, .002 etc. when followed by _ or space (for CC/iC Pack textures)
base_name = re.sub(r'\.\d{3}(?=[_\s])', '', filename) # Remove .001, .002 etc. when followed by _ or space
base_name = re.sub(r'\.\d{3}$', '', base_name) # Also remove if at the end
base_name = os.path.splitext(base_name)[0] # Remove standard extensions
# Remove or replace invalid characters for Windows/Mac/Linux
sanitized = re.sub(r'[<>:"/\\|?*]', '_', base_name)
@@ -539,8 +541,8 @@ class AutoMatExtractor(bpy.types.Operator):
# Must register the new dialog class as well
classes = (
AUTOMAT_OT_summary_dialog,
AutoMatExtractor,
RBST_AutoMat_OT_summary_dialog,
RBST_AutoMat_OT_AutoMatExtractor,
)
def register():
@@ -1,6 +1,6 @@
import bpy
class BST_FreeGPU(bpy.types.Operator):
class RBST_FreeGPU(bpy.types.Operator):
bl_idname = "bst.free_gpu"
bl_label = "Free VRAM"
bl_description = "Unallocate all material images from VRAM"
@@ -2,7 +2,7 @@ import bpy
import re
from ..utils import compat
class RENAME_OT_summary_dialog(bpy.types.Operator):
class RBST_RenameImg_OT_summary_dialog(bpy.types.Operator):
"""Show rename operation summary"""
bl_idname = "bst.rename_summary_dialog"
bl_label = "Rename Summary"
@@ -66,7 +66,7 @@ class RENAME_OT_summary_dialog(bpy.types.Operator):
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=500)
class Rename_images_by_mat(bpy.types.Operator):
class RBST_RenameImg_OT_Rename_images_by_mat(bpy.types.Operator):
bl_idname = "bst.rename_images_by_mat"
bl_label = "Rename Images by Material"
bl_description = "Rename selected images based on their material usage, preserving texture type suffixes"
@@ -500,8 +500,8 @@ class Rename_images_by_mat(bpy.types.Operator):
# Registration classes - need to register both operators
classes = (
RENAME_OT_summary_dialog,
Rename_images_by_mat,
RBST_RenameImg_OT_summary_dialog,
RBST_RenameImg_OT_Rename_images_by_mat,
)
def register():
@@ -1,5 +1,32 @@
import bpy
from ..utils import version
def _collect_keyframe_stats(action):
"""Return (total_keyframes, keyframe_frames_set). Compatible with 4.2/4.5 (action.fcurves) and 5.0 (layers/strips/channelbags)."""
keyframe_frames = set()
total_keyframes = 0
if version.is_version_less_than(5, 0, 0):
for fcurve in action.fcurves:
for kf in fcurve.keyframe_points:
keyframe_frames.add(kf.co[0])
total_keyframes += 1
else:
# Blender 5.0+: legacy action.fcurves removed; use layers → strips → channelbag(slot).fcurves
for layer in action.layers:
for strip in layer.strips:
for slot in action.slots:
channelbag = strip.channelbag(slot, ensure=False)
if channelbag is None:
continue
for fcurve in channelbag.fcurves:
for kf in fcurve.keyframe_points:
keyframe_frames.add(kf.co[0])
total_keyframes += 1
return total_keyframes, keyframe_frames
class DeleteSingleKeyframeActions(bpy.types.Operator):
"""Delete actions that have no keyframes, only one keyframe, or all keyframes on the same frame"""
bl_idname = "bst.delete_single_keyframe_actions"
@@ -12,13 +39,9 @@ class DeleteSingleKeyframeActions(bpy.types.Operator):
actions_to_delete = []
for action in actions:
keyframe_frames = set()
total_keyframes = 0
for fcurve in action.fcurves:
for kf in fcurve.keyframe_points:
keyframe_frames.add(kf.co[0])
total_keyframes += 1
if getattr(action, "library", None) is not None:
continue
total_keyframes, keyframe_frames = _collect_keyframe_stats(action)
# No keyframes
if total_keyframes == 0:
actions_to_delete.append(action)
@@ -1,157 +0,0 @@
import bpy
class MATERIAL_USERS_OT_summary_dialog(bpy.types.Operator):
"""Show material users analysis in a popup dialog"""
bl_idname = "bst.material_users_summary_dialog"
bl_label = "Material Users Summary"
bl_options = {'REGISTER', 'INTERNAL'}
# Properties to store summary data
material_name: bpy.props.StringProperty(default="")
users_count: bpy.props.IntProperty(default=0)
fake_user: bpy.props.BoolProperty(default=False)
object_users: bpy.props.StringProperty(default="")
node_users: bpy.props.StringProperty(default="")
material_node_users: bpy.props.StringProperty(default="")
total_user_count: bpy.props.IntProperty(default=0)
def draw(self, context):
layout = self.layout
# Title
layout.label(text=f"Material Users - '{self.material_name}'", icon='MATERIAL')
layout.separator()
# Basic info box
box = layout.box()
col = box.column(align=True)
col.label(text=f"Blender Users Count: {self.users_count}")
col.label(text=f"Fake User: {'Yes' if self.fake_user else 'No'}")
col.label(text=f"Total Found Users: {self.total_user_count}")
layout.separator()
# Object users section
if self.object_users:
layout.label(text="Object Users:", icon='OBJECT_DATA')
objects_box = layout.box()
objects_col = objects_box.column(align=True)
for obj_name in self.object_users.split('|'):
if obj_name.strip():
objects_col.label(text=f"{obj_name}", icon='RIGHTARROW_THIN')
else:
layout.label(text="Object Users: None", icon='OBJECT_DATA')
# Node tree users section
if self.node_users:
layout.separator()
layout.label(text="Node Tree Users:", icon='NODETREE')
nodes_box = layout.box()
nodes_col = nodes_box.column(align=True)
for node_ref in self.node_users.split('|'):
if node_ref.strip():
nodes_col.label(text=f"{node_ref}", icon='RIGHTARROW_THIN')
# Material node tree users section
if self.material_node_users:
layout.separator()
layout.label(text="Material Node Tree Users:", icon='MATERIAL')
mat_nodes_box = layout.box()
mat_nodes_col = mat_nodes_box.column(align=True)
for mat_node_ref in self.material_node_users.split('|'):
if mat_node_ref.strip():
mat_nodes_col.label(text=f"{mat_node_ref}", icon='RIGHTARROW_THIN')
layout.separator()
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_popup(self, width=500)
class FindMaterialUsers(bpy.types.Operator):
"""Find all users of a specified material and display detailed information"""
bl_idname = "bst.find_material_users"
bl_label = "Find Material Users"
bl_description = "Find and display all users of a specified material"
bl_options = {'REGISTER'}
material_name: bpy.props.StringProperty(
name="Material",
description="Name of the material to analyze",
default="",
)
def draw(self, context):
layout = self.layout
# Set the material if we have a name
if self.material_name and self.material_name in bpy.data.materials:
context.scene.bst_temp_material = bpy.data.materials[self.material_name]
# Use template_ID to get the proper material selector (without new button)
layout.template_ID(context.scene, "bst_temp_material", text="Material")
def execute(self, context):
# Get the material from the temp property
material = getattr(context.scene, 'bst_temp_material', None)
if not material:
self.report({'ERROR'}, "No material selected")
return {'CANCELLED'}
# Update our material_name property
self.material_name = material.name
# Check objects
object_users = []
for obj in bpy.data.objects:
if obj.material_slots:
for slot in obj.material_slots:
if slot.material == material:
object_users.append(obj.name)
break
# Check node groups more thoroughly
node_users = []
for node_tree in bpy.data.node_groups:
for node in node_tree.nodes:
# Check material nodes
if hasattr(node, 'material') and node.material == material:
node_users.append(f"{node_tree.name}.{node.name}")
# Check material input sockets
for input_socket in node.inputs:
if hasattr(input_socket, 'default_value') and hasattr(input_socket.default_value, 'name'):
if input_socket.default_value.name == material.name:
node_users.append(f"{node_tree.name}.{node.name}.{input_socket.name}")
# Check material node trees
material_node_users = []
for mat in bpy.data.materials:
if mat.node_tree:
for node in mat.node_tree.nodes:
if hasattr(node, 'material') and node.material == material:
material_node_users.append(f"{mat.name}.{node.name}")
# Show summary dialog
self.show_summary_dialog(context, material, object_users, node_users, material_node_users)
return {'FINISHED'}
def show_summary_dialog(self, context, material, object_users, node_users, material_node_users):
"""Show the material users summary in a popup dialog"""
total_user_count = len(object_users) + len(node_users) + len(material_node_users)
# Create and configure the summary dialog
dialog_op = bpy.ops.bst.material_users_summary_dialog
dialog_op('INVOKE_DEFAULT',
material_name=material.name,
users_count=material.users,
fake_user=material.use_fake_user,
object_users='|'.join(object_users),
node_users='|'.join(node_users),
material_node_users='|'.join(material_node_users),
total_user_count=total_user_count)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -369,7 +369,7 @@ def main(delete_low_priority=False):
print(f"Object ghosts removed: {object_ghosts_removed}")
print("="*80)
class GhostBuster(bpy.types.Operator):
class RBST_Bustin_OT_GhostBuster(bpy.types.Operator):
"""Conservative cleanup of ghost data (unused WGT objects, empty collections)"""
bl_idname = "bst.ghost_buster"
bl_label = "Ghost Buster"
@@ -388,7 +388,7 @@ class GhostBuster(bpy.types.Operator):
self.report({'ERROR'}, f"Ghost buster failed: {str(e)}")
return {'CANCELLED'}
class GhostDetector(bpy.types.Operator):
class RBST_Bustin_OT_GhostDetector(bpy.types.Operator):
"""Detect and analyze ghost data without removing it"""
bl_idname = "bst.ghost_detector"
bl_label = "Ghost Detector"
@@ -606,7 +606,7 @@ class GhostDetector(bpy.types.Operator):
self.analyze_ghost_data()
return context.window_manager.invoke_popup(self, width=500)
class ResyncEnforce(bpy.types.Operator):
class RBST_Bustin_OT_ResyncEnforce(bpy.types.Operator):
"""Resync Enforce: Fix broken library override hierarchies by rebuilding from linked references"""
bl_idname = "bst.resync_enforce"
bl_label = "Resync Enforce"
@@ -674,9 +674,9 @@ class ResyncEnforce(bpy.types.Operator):
# List of classes to register
classes = (
GhostBuster,
GhostDetector,
ResyncEnforce,
RBST_Bustin_OT_GhostBuster,
RBST_Bustin_OT_GhostDetector,
RBST_Bustin_OT_ResyncEnforce,
)
def register():
@@ -81,6 +81,23 @@ class SpawnSceneStructure(bpy.types.Operator):
layer_collection = self.find_layer_collection(view_layer.layer_collection, subcollection_name)
if layer_collection:
layer_collection.exclude = True
# Create Props collection under Char
if subcollection_name == "Char":
char_collection = existing_subcollection if subcollection_exists else subcollection
props_exists = False
existing_props = None
for child in char_collection.children:
if child.name == "Props":
props_exists = True
existing_props = child
skipped_collections.append(f"{main_collection_name}/{subcollection_name}/Props")
break
if not props_exists:
props_collection = bpy.data.collections.new("Props")
char_collection.children.link(props_collection)
created_collections.append(f"{main_collection_name}/{subcollection_name}/Props")
# Report results
if created_collections:
@@ -0,0 +1,28 @@
import bpy
class WhiteWorld(bpy.types.Operator):
"""Create a pure-white world and set it active; remove 'Dual Node Background' if present, enable transparent film"""
bl_idname = "bst.white_world"
bl_label = "White World"
bl_description = "Create white background world, set film transparent, purge orphans"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if "Dual Node Background" in bpy.data.worlds:
w = bpy.data.worlds["Dual Node Background"]
bpy.data.worlds.remove(w, do_unlink=True)
new_world = bpy.data.worlds.new(name="World")
new_world.use_nodes = True
nodes = new_world.node_tree.nodes
links = new_world.node_tree.links
nodes.clear()
bg = nodes.new(type="ShaderNodeBackground")
bg.inputs[0].default_value = (1.0, 1.0, 1.0, 1.0)
out = nodes.new(type="ShaderNodeOutputWorld")
links.new(bg.outputs[0], out.inputs[0])
context.scene.world = new_world
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
context.scene.render.film_transparent = True
self.report({'INFO'}, "White world set, film transparent, orphans purged")
return {'FINISHED'}
@@ -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