2026-01-01
This commit is contained in:
@@ -10,6 +10,7 @@ from .ops.Rename_images_by_mat import Rename_images_by_mat, RENAME_OT_summary_di
|
||||
from .ops.FreeGPU import BST_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):
|
||||
@@ -58,7 +59,7 @@ classes = (
|
||||
def register():
|
||||
# Register classes from this module (do this first to ensure preferences are available)
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
# Print debug info about preferences
|
||||
try:
|
||||
@@ -128,10 +129,7 @@ def unregister():
|
||||
rainys_repo_bootstrap.unregister()
|
||||
# Unregister classes from this module
|
||||
for cls in reversed(classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
if __name__ == "__main__":
|
||||
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.12.0"
|
||||
version = "0.14.0"
|
||||
type = "add-on"
|
||||
|
||||
maintainer = "RaincloudTheDragon <raincloudthedragon@gmail.com>"
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
# 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)
|
||||
|
||||
# v0.13.1
|
||||
- Fix github workflow to include new utils folder
|
||||
|
||||
# v0.13.0
|
||||
- Set up compat for #9, still needs bugchecking, but the main setup is complete.
|
||||
- Fixed #10
|
||||
|
||||
# v0.12.0
|
||||
- Integrate Rainy's Extension Repo bootstrapper
|
||||
- Set minimum Blender version to 4.2 for #9
|
||||
|
||||
@@ -7,6 +7,7 @@ from ..panels.bulk_path_management import (
|
||||
set_image_paths,
|
||||
ensure_directory_for_path,
|
||||
)
|
||||
from ..utils import compat
|
||||
|
||||
class AUTOMAT_OT_summary_dialog(bpy.types.Operator):
|
||||
"""Show AutoMat Extractor operation summary"""
|
||||
@@ -70,8 +71,9 @@ class AutoMatExtractor(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
# Get addon preferences
|
||||
addon_name = __package__.split('.')[0]
|
||||
prefs = context.preferences.addons.get(addon_name).preferences
|
||||
common_outside = prefs.automat_common_outside_blend
|
||||
addon_entry = context.preferences.addons.get(addon_name)
|
||||
prefs = addon_entry.preferences if addon_entry else None
|
||||
common_outside = prefs.automat_common_outside_blend if prefs else False
|
||||
|
||||
# Get selected images
|
||||
selected_images = [img for img in bpy.data.images if hasattr(img, "bst_selected") and img.bst_selected]
|
||||
@@ -190,12 +192,23 @@ class AutoMatExtractor(bpy.types.Operator):
|
||||
img = self.selected_images[self.current_index]
|
||||
props.operation_status = f"Building path for {img.name}..."
|
||||
|
||||
# Get blend file name
|
||||
blend_name = bpy.path.basename(bpy.data.filepath)
|
||||
if blend_name:
|
||||
blend_name = os.path.splitext(blend_name)[0]
|
||||
# Get blend file name - respect user preference if set
|
||||
if props.use_blend_subfolder:
|
||||
blend_name = props.blend_subfolder
|
||||
if not blend_name:
|
||||
# Fall back to filename if not specified
|
||||
blend_path = bpy.data.filepath
|
||||
if blend_path:
|
||||
blend_name = os.path.splitext(os.path.basename(blend_path))[0]
|
||||
else:
|
||||
blend_name = "untitled"
|
||||
else:
|
||||
blend_name = "untitled"
|
||||
# Derive from filename
|
||||
blend_path = bpy.data.filepath
|
||||
if blend_path:
|
||||
blend_name = os.path.splitext(os.path.basename(blend_path))[0]
|
||||
else:
|
||||
blend_name = "untitled"
|
||||
blend_name = self.sanitize_filename(blend_name)
|
||||
|
||||
# Determine common path
|
||||
@@ -532,9 +545,9 @@ classes = (
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import bpy
|
||||
import re
|
||||
from ..utils import compat
|
||||
|
||||
class RENAME_OT_summary_dialog(bpy.types.Operator):
|
||||
"""Show rename operation summary"""
|
||||
@@ -505,9 +506,9 @@ classes = (
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from ..utils import compat
|
||||
|
||||
class CreateOrthoCamera(Operator):
|
||||
"""Create an orthographic camera with predefined settings"""
|
||||
@@ -38,10 +39,10 @@ class CreateOrthoCamera(Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(CreateOrthoCamera)
|
||||
compat.safe_register_class(CreateOrthoCamera)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(CreateOrthoCamera)
|
||||
compat.safe_unregister_class(CreateOrthoCamera)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
@@ -1,4 +1,5 @@
|
||||
import bpy
|
||||
from ..utils import compat
|
||||
|
||||
def safe_wgt_removal():
|
||||
"""Safely remove only WGT widget objects that are clearly ghosts"""
|
||||
@@ -680,11 +681,8 @@ classes = (
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
compat.safe_unregister_class(cls)
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
import bpy
|
||||
from ..utils import compat
|
||||
|
||||
class RemoveCustomSplitNormals(bpy.types.Operator):
|
||||
"""Remove custom split normals and apply smooth shading to all accessible mesh objects"""
|
||||
@@ -53,10 +54,10 @@ class RemoveCustomSplitNormals(bpy.types.Operator):
|
||||
|
||||
# Registration
|
||||
def register():
|
||||
bpy.utils.register_class(MESH_OT_RemoveCustomSplitNormals)
|
||||
compat.safe_register_class(RemoveCustomSplitNormals)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(MESH_OT_RemoveCustomSplitNormals)
|
||||
compat.safe_unregister_class(RemoveCustomSplitNormals)
|
||||
|
||||
# Only run if this script is run directly
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -6,6 +6,7 @@ import subprocess
|
||||
|
||||
# Import ghost buster functionality
|
||||
from ..ops.ghost_buster import GhostBuster, GhostDetector, 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,})$')
|
||||
@@ -91,6 +92,31 @@ def register_dataremap_properties():
|
||||
default=False
|
||||
)
|
||||
|
||||
# Search filter properties for each data type
|
||||
bpy.types.Scene.dataremap_search_images = bpy.props.StringProperty( # type: ignore
|
||||
name="Search Images",
|
||||
description="Filter images by name (case-insensitive)",
|
||||
default=""
|
||||
)
|
||||
|
||||
bpy.types.Scene.dataremap_search_materials = bpy.props.StringProperty( # type: ignore
|
||||
name="Search Materials",
|
||||
description="Filter materials by name (case-insensitive)",
|
||||
default=""
|
||||
)
|
||||
|
||||
bpy.types.Scene.dataremap_search_fonts = bpy.props.StringProperty( # type: ignore
|
||||
name="Search Fonts",
|
||||
description="Filter fonts by name (case-insensitive)",
|
||||
default=""
|
||||
)
|
||||
|
||||
bpy.types.Scene.dataremap_search_worlds = bpy.props.StringProperty( # type: ignore
|
||||
name="Search Worlds",
|
||||
description="Filter worlds by name (case-insensitive)",
|
||||
default=""
|
||||
)
|
||||
|
||||
# Dictionary to store excluded groups
|
||||
if not hasattr(bpy.types.Scene, "excluded_remap_groups"):
|
||||
bpy.types.Scene.excluded_remap_groups = {}
|
||||
@@ -859,6 +885,21 @@ 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):
|
||||
"""Check if search string matches group base name or any item in group"""
|
||||
if not search_string:
|
||||
return True
|
||||
search_lower = search_string.lower()
|
||||
base_name, items = group
|
||||
# Check base name
|
||||
if search_lower in base_name.lower():
|
||||
return True
|
||||
# Check all item names in group
|
||||
for item in items:
|
||||
if search_lower in item.name.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
# Update the UI code to use the custom draw function
|
||||
def draw_data_duplicates(layout, context, data_type, data_groups):
|
||||
"""Draw the list of duplicate data items with drag-selectable checkboxes and click to rename"""
|
||||
@@ -881,6 +922,13 @@ def draw_data_duplicates(layout, context, data_type, data_groups):
|
||||
if hasattr(context.scene, sort_prop_name):
|
||||
select_row.prop(context.scene, sort_prop_name, text="Sort by Selected")
|
||||
|
||||
# Add search filter
|
||||
search_row = box_dup.row()
|
||||
search_row.label(text="", icon='VIEWZOOM')
|
||||
search_prop_name = f"dataremap_search_{data_type}"
|
||||
if hasattr(context.scene, search_prop_name):
|
||||
search_row.prop(context.scene, search_prop_name, text="")
|
||||
|
||||
box_dup.separator(factor=0.5)
|
||||
|
||||
# Initialize the expanded groups dictionary if it doesn't exist
|
||||
@@ -890,6 +938,15 @@ def draw_data_duplicates(layout, context, data_type, data_groups):
|
||||
# Get the groups and possibly sort them
|
||||
group_items = list(data_groups.items())
|
||||
|
||||
# Filter by search string if provided
|
||||
search_prop_name = f"dataremap_search_{data_type}"
|
||||
search_string = ""
|
||||
if hasattr(context.scene, search_prop_name):
|
||||
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)]
|
||||
|
||||
# Sort by selection if enabled
|
||||
sort_prop_name = f"dataremap_sort_{data_type}"
|
||||
if hasattr(context.scene, sort_prop_name) and getattr(context.scene, sort_prop_name):
|
||||
@@ -1443,14 +1500,11 @@ def register():
|
||||
register_dataremap_properties()
|
||||
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
compat.safe_unregister_class(cls)
|
||||
# Unregister properties
|
||||
try:
|
||||
unregister_dataremap_properties()
|
||||
|
||||
+82
-4
@@ -3,6 +3,7 @@ from bpy.types import Panel, Operator, PropertyGroup # type: ignore
|
||||
from bpy.props import StringProperty, BoolProperty, EnumProperty, PointerProperty, CollectionProperty # type: ignore
|
||||
import os
|
||||
import re
|
||||
from ..utils import compat
|
||||
|
||||
class REMOVE_EXT_OT_summary_dialog(bpy.types.Operator):
|
||||
"""Show remove extensions operation summary"""
|
||||
@@ -257,6 +258,13 @@ class BST_PathProperties(PropertyGroup):
|
||||
default=True
|
||||
) # type: ignore
|
||||
|
||||
# Search filter for images
|
||||
search_filter: StringProperty(
|
||||
name="Search Filter",
|
||||
description="Filter images by name (case-insensitive)",
|
||||
default=""
|
||||
) # type: ignore
|
||||
|
||||
# Smart pathing properties
|
||||
smart_base_path: StringProperty(
|
||||
name="Base Path",
|
||||
@@ -594,6 +602,61 @@ class BST_OT_select_active_images(Operator):
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
# Operator to select all images with absolute paths
|
||||
class BST_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"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
selected_count = 0
|
||||
|
||||
# Iterate through all images
|
||||
for img in bpy.data.images:
|
||||
# Skip images that shouldn't be checked
|
||||
if (img.source == 'GENERATED' or # Procedurally generated
|
||||
img.source == 'VIEWER' or # Render Result, Viewer Node, etc.
|
||||
img.name in ['Render Result', 'Viewer Node']): # Special Blender images
|
||||
continue
|
||||
|
||||
# Check if image has a file path
|
||||
if not img.filepath and not img.filepath_raw:
|
||||
continue
|
||||
|
||||
# Check both filepath and filepath_raw for absolute paths
|
||||
is_absolute = False
|
||||
|
||||
# Check filepath
|
||||
if img.filepath:
|
||||
# Skip Blender relative paths (starting with //)
|
||||
if not img.filepath.startswith('//'):
|
||||
# Convert to absolute path and check
|
||||
abs_path = bpy.path.abspath(img.filepath)
|
||||
if abs_path and os.path.isabs(abs_path):
|
||||
is_absolute = True
|
||||
|
||||
# Check filepath_raw if filepath wasn't absolute
|
||||
if not is_absolute and img.filepath_raw:
|
||||
# Skip Blender relative paths (starting with //)
|
||||
if not img.filepath_raw.startswith('//'):
|
||||
# Convert to absolute path and check
|
||||
abs_path = bpy.path.abspath(img.filepath_raw)
|
||||
if abs_path and os.path.isabs(abs_path):
|
||||
is_absolute = True
|
||||
|
||||
# Select image if it has an absolute path
|
||||
if is_absolute:
|
||||
img.bst_selected = True
|
||||
selected_count += 1
|
||||
|
||||
if selected_count > 0:
|
||||
self.report({'INFO'}, f"Selected {selected_count} images with absolute paths")
|
||||
else:
|
||||
self.report({'INFO'}, "No images with absolute paths found")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
# Add a class for renaming datablocks
|
||||
class BST_OT_rename_datablock(Operator):
|
||||
"""Click to rename datablock"""
|
||||
@@ -1482,7 +1545,8 @@ class NODE_PT_bulk_path_tools(Panel):
|
||||
|
||||
# Get addon preferences
|
||||
addon_name = __package__.split('.')[0]
|
||||
prefs = context.preferences.addons.get(addon_name).preferences
|
||||
addon_entry = context.preferences.addons.get(addon_name)
|
||||
prefs = addon_entry.preferences if addon_entry else None
|
||||
|
||||
row = box.row(align=True)
|
||||
row.enabled = any_selected
|
||||
@@ -1495,7 +1559,8 @@ class NODE_PT_bulk_path_tools(Panel):
|
||||
|
||||
# Right side: checkbox
|
||||
col = split.column()
|
||||
col.prop(prefs, "automat_common_outside_blend", text="", icon='FOLDER_REDIRECT')
|
||||
if prefs:
|
||||
col.prop(prefs, "automat_common_outside_blend", text="", icon='FOLDER_REDIRECT')
|
||||
|
||||
# Bulk operations section
|
||||
box = layout.box()
|
||||
@@ -1521,10 +1586,16 @@ class NODE_PT_bulk_path_tools(Panel):
|
||||
row = box.row(align=True)
|
||||
row.operator("bst.select_material_images", text="Material Images")
|
||||
row.operator("bst.select_active_images", text="Active Images")
|
||||
row.operator("bst.select_absolute_images", text="Absolute Images", icon='FOLDER_REDIRECT')
|
||||
|
||||
# Sorting option
|
||||
row = box.row()
|
||||
row.prop(path_props, "sort_by_selected", text="Sort by Selected")
|
||||
|
||||
# Search filter
|
||||
row = box.row()
|
||||
row.label(text="", icon='VIEWZOOM')
|
||||
row.prop(path_props, "search_filter", text="")
|
||||
|
||||
box.separator()
|
||||
|
||||
@@ -1539,6 +1610,12 @@ class NODE_PT_bulk_path_tools(Panel):
|
||||
# Use original order
|
||||
sorted_images = bpy.data.images
|
||||
|
||||
# Filter by search string if provided
|
||||
search_filter = path_props.search_filter
|
||||
if search_filter:
|
||||
search_lower = search_filter.lower()
|
||||
sorted_images = [img for img in sorted_images if search_lower in img.name.lower()]
|
||||
|
||||
for img in sorted_images:
|
||||
# Add bst_selected attribute if it doesn't exist
|
||||
if not hasattr(img, "bst_selected"):
|
||||
@@ -1590,6 +1667,7 @@ classes = (
|
||||
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,
|
||||
@@ -1609,7 +1687,7 @@ classes = (
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
# Register properties
|
||||
bpy.types.Scene.bst_path_props = PointerProperty(type=BST_PathProperties)
|
||||
@@ -1633,7 +1711,7 @@ def unregister():
|
||||
|
||||
# Unregister classes
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
+3
-5
@@ -7,6 +7,7 @@ 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 ..utils import compat
|
||||
|
||||
class BulkSceneGeneral(bpy.types.Panel):
|
||||
"""Bulk Scene General Panel"""
|
||||
@@ -76,7 +77,7 @@ classes = (
|
||||
# Registration
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
# Register the window manager property for the checkbox
|
||||
bpy.types.WindowManager.bst_no_subdiv_only_selected = bpy.props.BoolProperty(
|
||||
name="Selected Only",
|
||||
@@ -92,10 +93,7 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
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
|
||||
|
||||
+3
-5
@@ -5,6 +5,7 @@ import os
|
||||
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
|
||||
|
||||
# Material processing status enum
|
||||
class MaterialStatus(Enum):
|
||||
@@ -1014,7 +1015,7 @@ classes = (
|
||||
# Registration
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
compat.safe_register_class(cls)
|
||||
|
||||
# Register properties
|
||||
register_viewport_properties()
|
||||
@@ -1027,7 +1028,4 @@ def unregister():
|
||||
pass
|
||||
# Unregister classes
|
||||
for cls in reversed(classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
compat.safe_unregister_class(cls)
|
||||
@@ -7,7 +7,7 @@ A couple Blender tools to help me automate some tedious tasks in scene optimizat
|
||||
- Bulk Data Remap
|
||||
- Bulk Viewport Display
|
||||
|
||||
Officially supports Blender 4.4.1, but may still work on older versions.
|
||||
Officially supports Blender 4.2, 4.4, 4.5, and 5.0.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
This package contains utility modules for version detection and API compatibility.
|
||||
|
||||
"""
|
||||
|
||||
from . import version
|
||||
from . import compat
|
||||
|
||||
__all__ = ['version', 'compat']
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
This module provides API compatibility functions for handling differences
|
||||
between Blender 4.2, 4.4, 4.5, and 5.0.
|
||||
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class, unregister_class
|
||||
from . import version
|
||||
|
||||
|
||||
def safe_register_class(cls):
|
||||
"""
|
||||
Safely register a class, handling any version-specific registration issues.
|
||||
|
||||
Args:
|
||||
cls: The class to register
|
||||
|
||||
Returns:
|
||||
bool: True if registration succeeded, False otherwise
|
||||
"""
|
||||
try:
|
||||
register_class(cls)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to register {cls.__name__}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def safe_unregister_class(cls):
|
||||
"""
|
||||
Safely unregister a class, handling any version-specific unregistration issues.
|
||||
|
||||
Args:
|
||||
cls: The class to unregister
|
||||
|
||||
Returns:
|
||||
bool: True if unregistration succeeded, False otherwise
|
||||
"""
|
||||
try:
|
||||
unregister_class(cls)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to unregister {cls.__name__}: {e}")
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
This module provides version detection and comparison utilities for
|
||||
multi-version Blender support (4.2, 4.4, 4.5, and 5.0).
|
||||
|
||||
"""
|
||||
|
||||
import bpy
|
||||
|
||||
# Version constants
|
||||
VERSION_4_2 = (4, 2, 0)
|
||||
VERSION_4_4 = (4, 4, 0)
|
||||
VERSION_4_5 = (4, 5, 0)
|
||||
VERSION_5_0 = (5, 0, 0)
|
||||
|
||||
|
||||
def get_blender_version():
|
||||
"""
|
||||
Returns the current Blender version as a tuple (major, minor, patch).
|
||||
|
||||
Returns:
|
||||
tuple: (major, minor, patch) version numbers
|
||||
"""
|
||||
return bpy.app.version
|
||||
|
||||
|
||||
def get_version_string():
|
||||
"""
|
||||
Returns the current Blender version as a string (e.g., "4.2.0").
|
||||
|
||||
Returns:
|
||||
str: Version string in format "major.minor.patch"
|
||||
"""
|
||||
version = get_blender_version()
|
||||
return f"{version[0]}.{version[1]}.{version[2]}"
|
||||
|
||||
|
||||
def is_version_at_least(major, minor=0, patch=0):
|
||||
"""
|
||||
Check if the current Blender version is at least the specified version.
|
||||
|
||||
Args:
|
||||
major (int): Major version number
|
||||
minor (int): Minor version number (default: 0)
|
||||
patch (int): Patch version number (default: 0)
|
||||
|
||||
Returns:
|
||||
bool: True if current version >= specified version
|
||||
"""
|
||||
current = get_blender_version()
|
||||
target = (major, minor, patch)
|
||||
|
||||
if current[0] != target[0]:
|
||||
return current[0] > target[0]
|
||||
if current[1] != target[1]:
|
||||
return current[1] > target[1]
|
||||
return current[2] >= target[2]
|
||||
|
||||
|
||||
def is_version_less_than(major, minor=0, patch=0):
|
||||
"""
|
||||
Check if the current Blender version is less than the specified version.
|
||||
|
||||
Args:
|
||||
major (int): Major version number
|
||||
minor (int): Minor version number (default: 0)
|
||||
patch (int): Patch version number (default: 0)
|
||||
|
||||
Returns:
|
||||
bool: True if current version < specified version
|
||||
"""
|
||||
return not is_version_at_least(major, minor, patch)
|
||||
|
||||
|
||||
def get_version_category():
|
||||
"""
|
||||
Returns the version category string for the current Blender version.
|
||||
|
||||
Returns:
|
||||
str: '4.2', '4.4', '4.5', or '5.0' based on the current version
|
||||
"""
|
||||
version = get_blender_version()
|
||||
major, minor = version[0], version[1]
|
||||
|
||||
if major == 4:
|
||||
if minor < 4:
|
||||
return '4.2'
|
||||
elif minor < 5:
|
||||
return '4.4'
|
||||
else:
|
||||
return '4.5'
|
||||
elif major >= 5:
|
||||
return '5.0'
|
||||
else:
|
||||
# Fallback for older versions
|
||||
return f"{major}.{minor}"
|
||||
|
||||
|
||||
def is_version_4_2():
|
||||
"""Check if running Blender 4.2 (4.2.x only, not 4.3 or 4.4)."""
|
||||
version = get_blender_version()
|
||||
return version[0] == 4 and version[1] == 2
|
||||
|
||||
|
||||
def is_version_4_4():
|
||||
"""Check if running Blender 4.4 (4.4.x only, not 4.5)."""
|
||||
version = get_blender_version()
|
||||
return version[0] == 4 and version[1] == 4
|
||||
|
||||
|
||||
def is_version_4_5():
|
||||
"""Check if running Blender 4.5 LTS."""
|
||||
return is_version_at_least(4, 5, 0) and is_version_less_than(5, 0, 0)
|
||||
|
||||
|
||||
def is_version_5_0():
|
||||
"""Check if running Blender 5.0 or later."""
|
||||
return is_version_at_least(5, 0, 0)
|
||||
|
||||
Reference in New Issue
Block a user