2026-02-16
This commit is contained in:
@@ -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"
|
||||
|
||||
+4
-4
@@ -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():
|
||||
|
||||
+30
-7
@@ -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'}
|
||||
+192
-102
@@ -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
|
||||
+52
-52
@@ -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(
|
||||
|
||||
+8
-18
@@ -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
|
||||
+153
-67
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user