disable (currently unused) addons, update

This commit is contained in:
Nathan
2026-07-16 12:41:53 -06:00
parent e3310263f3
commit 0f26db3345
76 changed files with 19896 additions and 6197 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -1,12 +0,0 @@
schema_version = "1.0.0"
id = "looptools"
name = "LoopTools"
version = "4.7.7"
tagline = "Mesh modelling toolkit. Several tools to aid modelling"
maintainer = "Community"
type = "add-on"
tags = ["Mesh"]
blender_version_min = "4.2.0"
license = ["SPDX:GPL-2.0-or-later"]
website = "https://projects.blender.org/extensions/mesh_looptools"
copyright = ["2024 Bart Crouch", "2024 Vladimir Spivak (cwolf3d)"]
@@ -54,13 +54,13 @@
"id": "rainclouds_bulk_scene_tools",
"name": "Raincloud's Bulk Scene Tools",
"tagline": "Bulk utilities for optimizing scene data",
"version": "0.17.0",
"version": "0.18.0",
"type": "add-on",
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
"license": [
"GPL-3.0-or-later"
],
"blender_version_min": "4.2.0",
"blender_version_min": "4.5.0",
"website": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools",
"permissions": {
"files": "Read and write external resources referenced by scenes"
@@ -70,9 +70,9 @@
"Workflow",
"Materials"
],
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v0.17.0/Rainys_Bulk_Scene_Tools.v0.17.0.zip",
"archive_size": 80981,
"archive_hash": "sha256:419433069465b45ea903bd7bb46d89aa28b9c96c541d587d5f3be651a762811f"
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/0.18.0/Rainys_Bulk_Scene_Tools.v0.18.0.zip",
"archive_size": 75599,
"archive_hash": "sha256:6653a01b9ac5822e51113609b5a2970102715ec5f7816a9dccd80ecb4c755ea5"
},
{
"schema_version": "1.0.0",
@@ -92,7 +92,7 @@
"submission",
"utility"
],
"archive_url": "https://github.com/RaincloudTheDragon/sheepit_project_submitter/releases/download/v0.0.8/SheepIt_Project_Submitter.v0.0.8.zip",
"archive_url": "https://github.com/RaincloudTheDragon/based-blendfile-packer/releases/download/v0.0.8/SheepIt_Project_Submitter.v0.0.8.zip",
"archive_size": 47667,
"archive_hash": "sha256:93cd8f18456079130c48c66cfd40235f7fe6414f929f59f90670e7a864821110"
}
@@ -8,7 +8,6 @@ from .panels import bulk_scene_general
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
@@ -77,7 +76,6 @@ def register():
bulk_viewport_display.register()
bulk_data_remap.register()
bulk_path_management.register()
ghost_buster.register()
# Add keybind for Free GPU (global context)
wm = bpy.context.window_manager
@@ -106,10 +104,6 @@ def unregister():
delattr(bpy.types.Scene, '_bst_keymaps')
# Unregister modules
try:
ghost_buster.unregister()
except Exception:
pass
try:
bulk_path_management.unregister()
except Exception:
@@ -3,12 +3,12 @@ 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.17.0"
version = "0.18.0"
type = "add-on"
maintainer = "RaincloudTheDragon <raincloudthedragon@gmail.com>"
license = ["GPL-3.0-or-later"]
blender_version_min = "4.2.0"
blender_version_min = "4.5.0"
website = "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools"
@@ -1,3 +1,9 @@
# v0.18.0 2026-07-14
- Official support: Blender 4.5 and 5.2 LTS only; minimum version raised to 4.5 (#9)
- Bulk Data Remap: native node group rename/merge for numbered duplicates (#16)
- Ghost Buster removed; use Atomic instead (#17)
- Remapper UI: larger Purge Unused button, fixed post-Ghost-Buster spacing
# v0.17.0 2026-03-13
- Remove Action FU: new operator in Animation Data section—clears fake users from all actions so only used animations are kept
@@ -4,7 +4,7 @@ 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)."""
"""Return (total_keyframes, keyframe_frames_set). Compatible with 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):
@@ -1,688 +0,0 @@
import bpy
from ..utils import compat
def safe_wgt_removal():
"""Safely remove only WGT widget objects that are clearly ghosts"""
print("="*80)
print("CONSERVATIVE WGT GHOST REMOVAL")
print("="*80)
# Find all WGT objects
wgt_objects = []
for obj in bpy.data.objects:
if obj.name.startswith('WGT-'):
wgt_objects.append(obj)
print(f"Found {len(wgt_objects)} WGT objects")
# Check which ones are actually being used by armatures
used_wgts = set()
for armature in bpy.data.armatures:
for bone in armature.bones:
if bone.use_deform and hasattr(bone, 'custom_shape') and bone.custom_shape:
used_wgts.add(bone.custom_shape.name)
print(f"Found {len(used_wgts)} WGT objects actually used by armatures")
# Remove unused WGT objects
removed_wgts = 0
for obj in wgt_objects:
if obj.name not in used_wgts:
try:
# Skip linked objects (they're legitimate library content)
if hasattr(obj, 'library') and obj.library is not None:
print(f" Skipping linked WGT: {obj.name} (from {obj.library.name})")
continue
# Check if it's in the WGTS collection (typical ghost pattern)
in_wgts_collection = False
for collection in bpy.data.collections:
if 'WGTS' in collection.name and obj in collection.objects.values():
in_wgts_collection = True
break
if in_wgts_collection:
print(f" Removing unused WGT: {obj.name}")
bpy.data.objects.remove(obj, do_unlink=True)
removed_wgts += 1
except Exception as e:
print(f" Failed to remove {obj.name}: {e}")
print(f"Removed {removed_wgts} unused WGT objects")
return removed_wgts
def is_collection_in_scene_hierarchy(collection, scene_collection):
"""Recursively check if a collection exists anywhere in the scene collection hierarchy"""
if collection == scene_collection:
return True
for child_collection in scene_collection.children:
if child_collection == collection:
return True
if is_collection_in_scene_hierarchy(collection, child_collection):
return True
return False
def clean_empty_collections():
"""Remove empty collections that are not linked to scenes"""
print("\n" + "="*80)
print("CLEANING EMPTY COLLECTIONS")
print("="*80)
removed_collections = 0
collections_to_remove = []
for collection in bpy.data.collections:
# Check if collection is empty
if len(collection.objects) == 0 and len(collection.children) == 0:
# Skip linked collections (they're legitimate library content)
if hasattr(collection, 'library') and collection.library is not None:
print(f" Skipping linked empty collection: {collection.name}")
continue
# Check if it's anywhere in any scene's collection hierarchy
linked_to_scene = False
for scene in bpy.data.scenes:
if is_collection_in_scene_hierarchy(collection, scene.collection):
linked_to_scene = True
print(f" Preserving empty collection: {collection.name} (in scene '{scene.name}')")
break
if not linked_to_scene:
collections_to_remove.append(collection)
for collection in collections_to_remove:
try:
print(f" Removing empty collection: {collection.name}")
bpy.data.collections.remove(collection)
removed_collections += 1
except Exception as e:
print(f" Failed to remove collection {collection.name}: {e}")
print(f"Removed {removed_collections} empty collections")
return removed_collections
def is_object_used_by_scene_instance_collections(obj):
"""Check if object is in a collection that's being instanced by objects in scenes"""
# Find all collections that contain this object
obj_collections = []
for collection in bpy.data.collections:
if obj in collection.objects.values():
obj_collections.append(collection)
if not obj_collections:
return False
# Check if any of these collections are being instanced by objects in scenes
for collection in obj_collections:
# Find objects that instance this collection
for other_obj in bpy.data.objects:
if (other_obj.instance_type == 'COLLECTION' and
other_obj.instance_collection == collection):
# Check if the instancing object is in any scene
for scene in bpy.data.scenes:
if other_obj in scene.objects.values():
return True
return False
def is_object_legitimate_outside_scene(obj):
"""Check if an object has legitimate reasons to exist outside scenes"""
# WGT objects (rig widgets) are legitimate outside scenes
if obj.name.startswith('WGT-'):
return True
# Collection instance objects (linked collection references) are legitimate
if obj.instance_type == 'COLLECTION' and obj.instance_collection is not None:
return True
# Objects that are being used by instance collections in scenes are legitimate
if is_object_used_by_scene_instance_collections(obj):
return True
# Objects used as curve modifiers, constraints targets, etc.
# Check if object is used by modifiers on other objects that are in scenes
for other_obj in bpy.data.objects:
# Check if the other object is in any scene
in_scene = False
for scene in bpy.data.scenes:
if other_obj in scene.objects.values():
in_scene = True
break
if in_scene:
for modifier in other_obj.modifiers:
if hasattr(modifier, 'object') and modifier.object == obj:
return True
if hasattr(modifier, 'target') and modifier.target == obj:
return True
# Check if object is used by constraints on other objects that are in scenes
for other_obj in bpy.data.objects:
# Check if the other object is in any scene
in_scene = False
for scene in bpy.data.scenes:
if other_obj in scene.objects.values():
in_scene = True
break
if in_scene:
for constraint in other_obj.constraints:
if hasattr(constraint, 'target') and constraint.target == obj:
return True
if hasattr(constraint, 'subtarget') and constraint.subtarget == obj.name:
return True
# Check if object is used in particle systems on objects that are in scenes
for other_obj in bpy.data.objects:
# Check if the other object is in any scene
in_scene = False
for scene in bpy.data.scenes:
if other_obj in scene.objects.values():
in_scene = True
break
if in_scene:
for modifier in other_obj.modifiers:
if modifier.type == 'PARTICLE_SYSTEM':
settings = modifier.particle_system.settings
if hasattr(settings, 'object') and settings.object == obj:
return True
if hasattr(settings, 'instance_object') and settings.instance_object == obj:
return True
return False
def clean_object_ghosts(delete_low_priority=False):
"""Remove objects that are not in any scene and have no legitimate purpose (potential ghosts)"""
print("\n" + "="*80)
print("OBJECT GHOST CLEANUP")
print("="*80)
# Get all objects, excluding cameras and lights by default (they're often not in scenes for good reasons)
candidate_objects = [obj for obj in bpy.data.objects if obj.type not in ['CAMERA', 'LIGHT']]
if not candidate_objects:
print("No candidate objects found")
return 0
print(f"Found {len(candidate_objects)} candidate objects")
removed_objects = 0
ghosts_to_remove = []
for obj in candidate_objects:
# Skip linked objects (they're legitimate library content)
if hasattr(obj, 'library') and obj.library is not None:
continue
# Check which scenes contain it
in_scenes = []
for scene in bpy.data.scenes:
if obj in scene.objects.values():
in_scenes.append(scene.name)
# If not in any scene, check if it has legitimate reasons to exist
if len(in_scenes) == 0:
if is_object_legitimate_outside_scene(obj):
print(f" Preserving object: {obj.name} (legitimate use outside scene)")
continue
# If not legitimate, it's a ghost - but be conservative with low user count objects
should_remove = False
removal_reason = ""
if obj.users >= 2:
# Higher user count ghosts are definitely safe to remove
should_remove = True
removal_reason = "ghost (users >= 2, no legitimate use found)"
elif obj.users < 2 and delete_low_priority:
# Low user count ghosts only if user enables the option
should_remove = True
removal_reason = "low priority ghost (users < 2, no legitimate use found)"
elif obj.users < 2:
print(f" Skipping low priority object: {obj.name} (users < 2, enable 'Delete Low Priority' to remove)")
if should_remove:
ghosts_to_remove.append(obj)
print(f" Marking ghost for removal: {obj.name} (type: {obj.type}) - {removal_reason}")
# Remove the ghost objects
for obj in ghosts_to_remove:
try:
print(f" Removing object ghost: {obj.name}")
bpy.data.objects.remove(obj, do_unlink=True)
removed_objects += 1
except Exception as e:
print(f" Failed to remove object {obj.name}: {e}")
print(f"Removed {removed_objects} ghost objects")
return removed_objects
def manual_object_analysis():
"""Manual analysis of objects - show info but don't auto-remove"""
print("\n" + "="*80)
print("OBJECT GHOST ANALYSIS (MANUAL REVIEW)")
print("="*80)
# Get all objects, excluding cameras and lights (they're often legitimately not in scenes)
candidate_objects = [obj for obj in bpy.data.objects if obj.type not in ['CAMERA', 'LIGHT']]
# Filter to only objects not in scenes for analysis
objects_not_in_scenes = []
for obj in candidate_objects:
# Skip linked objects for analysis
if hasattr(obj, 'library') and obj.library is not None:
continue
# Check which scenes contain it
in_scenes = []
for scene in bpy.data.scenes:
if obj in scene.objects.values():
in_scenes.append(scene.name)
if len(in_scenes) == 0:
objects_not_in_scenes.append(obj)
if not objects_not_in_scenes:
print("No local objects found outside scenes")
return
print(f"Found {len(objects_not_in_scenes)} local objects not in any scene:")
for obj in objects_not_in_scenes:
print(f"\n Object: {obj.name} (type: {obj.type})")
print(f" Users: {obj.users}")
print(f" Parent: {obj.parent.name if obj.parent else 'None'}")
# Check collections
in_collections = []
for collection in bpy.data.collections:
if obj in collection.objects.values():
in_collections.append(collection.name)
print(f" In collections: {in_collections}")
# Show recommendation
if is_object_legitimate_outside_scene(obj):
print(f" -> LEGITIMATE: Has valid use outside scenes")
elif obj.users >= 2:
print(f" -> GHOST: No legitimate use found, users >= 2 (will be removed)")
elif obj.users < 2:
print(f" -> LOW PRIORITY: No legitimate use found, users < 2 (needs option enabled)")
else:
print(f" -> UNCLEAR: Manual review needed")
def main(delete_low_priority=False):
"""Main conservative cleanup function"""
print("CONSERVATIVE GHOST DATA CLEANUP")
print("="*80)
print("This script removes:")
print("1. Unused local WGT widget objects")
print("2. Empty unlinked collections")
print("3. Objects not in any scene with no legitimate use")
if delete_low_priority:
print(" - Including low priority ghosts (no legitimate use, users < 2)")
else:
print(" - Excluding low priority ghosts (no legitimate use, users < 2)")
print("="*80)
initial_objects = len(list(bpy.data.objects))
initial_collections = len(list(bpy.data.collections))
# Safe operations only
wgts_removed = safe_wgt_removal()
collections_removed = clean_empty_collections()
object_ghosts_removed = clean_object_ghosts(delete_low_priority)
# Show remaining object analysis
manual_object_analysis()
# Final purge
print("\n" + "="*80)
print("FINAL SAFE PURGE")
print("="*80)
try:
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
print("Safe purge completed")
except:
print("Purge had issues")
final_objects = len(list(bpy.data.objects))
final_collections = len(list(bpy.data.collections))
print(f"\n" + "="*80)
print("CONSERVATIVE CLEANUP SUMMARY")
print("="*80)
print(f"Objects: {initial_objects} -> {final_objects} (removed {initial_objects - final_objects})")
print(f"Collections: {initial_collections} -> {final_collections} (removed {collections_removed})")
print(f"WGT objects removed: {wgts_removed}")
print(f"Object ghosts removed: {object_ghosts_removed}")
print("="*80)
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"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
# Get the delete low priority setting from scene properties
delete_low_priority = getattr(context.scene, "ghost_buster_delete_low_priority", False)
# Call the main ghost buster function
main(delete_low_priority)
self.report({'INFO'}, "Ghost data cleanup completed")
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"Ghost buster failed: {str(e)}")
return {'CANCELLED'}
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"
bl_options = {'REGISTER', 'INTERNAL'}
# Properties to store analysis data
total_wgt_objects: bpy.props.IntProperty(default=0)
unused_wgt_objects: bpy.props.IntProperty(default=0)
used_wgt_objects: bpy.props.IntProperty(default=0)
empty_collections: bpy.props.IntProperty(default=0)
ghost_objects: bpy.props.IntProperty(default=0)
ghost_potential: bpy.props.IntProperty(default=0)
ghost_legitimate: bpy.props.IntProperty(default=0)
ghost_low_priority: bpy.props.IntProperty(default=0)
wgt_details: bpy.props.StringProperty(default="")
collection_details: bpy.props.StringProperty(default="")
ghost_details: bpy.props.StringProperty(default="")
def analyze_ghost_data(self):
"""Analyze ghost data similar to ghost_buster functions"""
# Analyze WGT objects
wgt_objects = []
for obj in bpy.data.objects:
if obj.name.startswith('WGT-'):
wgt_objects.append(obj)
self.total_wgt_objects = len(wgt_objects)
# Check which WGT objects are used by armatures
used_wgts = set()
for armature in bpy.data.armatures:
for bone in armature.bones:
if bone.use_deform and hasattr(bone, 'custom_shape') and bone.custom_shape:
used_wgts.add(bone.custom_shape.name)
self.used_wgt_objects = len(used_wgts)
# Count unused WGT objects
unused_wgts = []
wgt_details_list = []
for obj in wgt_objects:
if obj.name not in used_wgts:
# Skip linked objects (they're legitimate library content)
if hasattr(obj, 'library') and obj.library is not None:
continue
# Check if it's in the WGTS collection (typical ghost pattern)
in_wgts_collection = False
for collection in bpy.data.collections:
if 'WGTS' in collection.name and obj in collection.objects.values():
in_wgts_collection = True
break
if in_wgts_collection:
unused_wgts.append(obj)
wgt_details_list.append(f"{obj.name} (in WGTS collection)")
self.unused_wgt_objects = len(unused_wgts)
self.wgt_details = "\n".join(wgt_details_list[:10]) # Limit to first 10
if len(unused_wgts) > 10:
self.wgt_details += f"\n... and {len(unused_wgts) - 10} more"
# Analyze empty collections
empty_collections = []
collection_details_list = []
for collection in bpy.data.collections:
if len(collection.objects) == 0 and len(collection.children) == 0:
# Skip linked collections (they're legitimate library content)
if hasattr(collection, 'library') and collection.library is not None:
continue
# Check if it's anywhere in any scene's collection hierarchy
linked_to_scene = False
for scene in bpy.data.scenes:
if is_collection_in_scene_hierarchy(collection, scene.collection):
linked_to_scene = True
break
if not linked_to_scene:
empty_collections.append(collection)
collection_details_list.append(f"{collection.name}")
self.empty_collections = len(empty_collections)
self.collection_details = "\n".join(collection_details_list[:10]) # Limit to first 10
if len(empty_collections) > 10:
self.collection_details += f"\n... and {len(empty_collections) - 10} more"
# Analyze ghost objects (objects not in scenes)
candidate_objects = [obj for obj in bpy.data.objects if obj.type not in ['CAMERA', 'LIGHT']]
potential_ghosts = 0
legitimate = 0
low_priority = 0
ghost_details_list = []
for obj in candidate_objects:
# Skip linked objects (they're legitimate library content)
if hasattr(obj, 'library') and obj.library is not None:
continue
# Check which scenes contain it
in_scenes = []
for scene in bpy.data.scenes:
if obj in scene.objects.values():
in_scenes.append(scene.name)
# Only analyze objects not in scenes
if len(in_scenes) == 0:
# Classify object
status = ""
if is_object_legitimate_outside_scene(obj):
legitimate += 1
status = "LEGITIMATE (has valid use outside scenes)"
elif obj.users >= 2:
potential_ghosts += 1
status = "GHOST (no legitimate use found, users >= 2)"
elif obj.users < 2:
low_priority += 1
status = "LOW PRIORITY (no legitimate use found, users < 2)"
else:
status = "UNCLEAR"
ghost_details_list.append(f"{obj.name} ({obj.type}): {status}")
self.ghost_objects = len([obj for obj in candidate_objects if len([s for s in bpy.data.scenes if obj in s.objects.values()]) == 0 and not (hasattr(obj, 'library') and obj.library is not None)])
self.ghost_potential = potential_ghosts
self.ghost_legitimate = legitimate
self.ghost_low_priority = low_priority
self.ghost_details = "\n".join(ghost_details_list[:10]) # Limit to first 10
if len(ghost_details_list) > 10:
self.ghost_details += f"\n... and {len(ghost_details_list) - 10} more"
def draw(self, context):
layout = self.layout
# Title
layout.label(text="Ghost Data Analysis", icon='GHOST_ENABLED')
layout.separator()
# WGT Objects section
box = layout.box()
box.label(text="WGT Widget Objects", icon='ARMATURE_DATA')
col = box.column(align=True)
col.label(text=f"Total WGT objects: {self.total_wgt_objects}")
col.label(text=f"Used by armatures: {self.used_wgt_objects}", icon='CHECKMARK')
if self.unused_wgt_objects > 0:
col.label(text=f"Unused (potential ghosts): {self.unused_wgt_objects}", icon='ERROR')
if self.wgt_details:
box.separator()
details_col = box.column(align=True)
for line in self.wgt_details.split('\n'):
if line.strip():
details_col.label(text=line)
else:
col.label(text="No unused WGT objects found", icon='CHECKMARK')
# Empty Collections section
box = layout.box()
box.label(text="Empty Collections", icon='OUTLINER_COLLECTION')
col = box.column(align=True)
if self.empty_collections > 0:
col.label(text=f"Empty unlinked collections: {self.empty_collections}", icon='ERROR')
if self.collection_details:
box.separator()
details_col = box.column(align=True)
for line in self.collection_details.split('\n'):
if line.strip():
details_col.label(text=line)
else:
col.label(text="No empty unlinked collections found", icon='CHECKMARK')
# Ghost Objects section
box = layout.box()
box.label(text="Ghost Objects Analysis", icon='OBJECT_DATA')
col = box.column(align=True)
col.label(text=f"Objects not in scenes: {self.ghost_objects}")
if self.ghost_objects > 0:
if self.ghost_potential > 0:
col.label(text=f"Ghosts (users >= 2): {self.ghost_potential}", icon='ERROR')
if self.ghost_legitimate > 0:
col.label(text=f"Legitimate objects: {self.ghost_legitimate}", icon='CHECKMARK')
if self.ghost_low_priority > 0:
col.label(text=f"Low priority (users < 2): {self.ghost_low_priority}", icon='QUESTION')
if self.ghost_details:
box.separator()
details_col = box.column(align=True)
for line in self.ghost_details.split('\n'):
if line.strip():
details_col.label(text=line)
else:
col.label(text="No ghost objects found", icon='CHECKMARK')
# Summary
layout.separator()
summary_box = layout.box()
summary_box.label(text="Summary", icon='INFO')
total_issues = self.unused_wgt_objects + self.empty_collections + self.ghost_potential
if total_issues > 0:
summary_box.label(text=f"Found {total_issues} ghost data issues that will be removed", icon='ERROR')
if self.ghost_low_priority > 0:
summary_box.label(text=f"+ {self.ghost_low_priority} low priority issues (optional)", icon='QUESTION')
summary_box.label(text="Use Ghost Buster to clean up safely")
else:
summary_box.label(text="No ghost data issues detected!", icon='CHECKMARK')
if self.ghost_low_priority > 0:
summary_box.label(text=f"({self.ghost_low_priority} low priority issues available)", icon='INFO')
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
# Analyze the ghost data before showing the dialog
self.analyze_ghost_data()
return context.window_manager.invoke_popup(self, width=500)
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"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
# Only available if there are selected objects
return context.selected_objects
def execute(self, context):
# Get selected objects
selected_objects = context.selected_objects.copy()
if not selected_objects:
self.report({'WARNING'}, "No objects selected for resync enforce")
return {'CANCELLED'}
# Count library override objects
override_objects = []
for obj in selected_objects:
if obj.override_library:
override_objects.append(obj)
if not override_objects:
self.report({'WARNING'}, "No library override objects found in selection")
return {'CANCELLED'}
try:
# Store the current selection
original_selection = set(context.selected_objects)
# Select only the override objects
bpy.ops.object.select_all(action='DESELECT')
for obj in override_objects:
obj.select_set(True)
# Call Blender's resync enforce operation
result = bpy.ops.object.library_override_operation(
'INVOKE_DEFAULT',
type='OVERRIDE_LIBRARY_RESYNC_HIERARCHY_ENFORCE',
selection_set='SELECTED'
)
if result == {'FINISHED'}:
self.report({'INFO'}, f"Resync enforce completed on {len(override_objects)} override objects")
return_code = {'FINISHED'}
else:
self.report({'WARNING'}, "Resync enforce operation was cancelled or failed")
return_code = {'CANCELLED'}
# Restore original selection
bpy.ops.object.select_all(action='DESELECT')
for obj in original_selection:
if obj.name in bpy.data.objects: # Check if object still exists
obj.select_set(True)
return return_code
except Exception as e:
self.report({'ERROR'}, f"Resync enforce failed: {str(e)}")
return {'CANCELLED'}
# Note: main() is called by the operator, not automatically
# List of classes to register
classes = (
RBST_Bustin_OT_GhostBuster,
RBST_Bustin_OT_GhostDetector,
RBST_Bustin_OT_ResyncEnforce,
)
def register():
for cls in classes:
compat.safe_register_class(cls)
def unregister():
for cls in reversed(classes):
compat.safe_unregister_class(cls)
@@ -4,8 +4,6 @@ import os
import sys
import subprocess
# Import ghost buster functionality
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.
@@ -45,6 +43,12 @@ def RBST_DatRem_register_properties():
default=True
)
bpy.types.Scene.dataremap_node_groups = bpy.props.BoolProperty( # type: ignore
name="Node Groups",
description="Find and remap duplicate node groups",
default=True
)
# Add properties for showing duplicate lists
bpy.types.Scene.show_image_duplicates = bpy.props.BoolProperty( # type: ignore
name="Show Image Duplicates",
@@ -70,6 +74,12 @@ def RBST_DatRem_register_properties():
default=False
)
bpy.types.Scene.show_node_group_duplicates = bpy.props.BoolProperty( # type: ignore
name="Show Node Group Duplicates",
description="Show list of duplicate node groups",
default=False
)
# Sort by selected properties for each data type
bpy.types.Scene.dataremap_sort_images = bpy.props.BoolProperty( # type: ignore
name="Sort Images by Selected",
@@ -91,6 +101,11 @@ def RBST_DatRem_register_properties():
description="Show selected world groups at the top of the list",
default=False
)
bpy.types.Scene.dataremap_sort_node_groups = bpy.props.BoolProperty( # type: ignore
name="Sort Node Groups by Selected",
description="Show selected node group groups at the top of the list",
default=False
)
# Search filter properties for each data type
bpy.types.Scene.dataremap_search_images = bpy.props.StringProperty( # type: ignore
@@ -117,6 +132,12 @@ def RBST_DatRem_register_properties():
default=""
)
bpy.types.Scene.dataremap_search_node_groups = bpy.props.StringProperty( # type: ignore
name="Search Node Groups",
description="Filter node groups 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 = {}
@@ -128,23 +149,18 @@ def RBST_DatRem_register_properties():
# Store the last clicked group for shift-click range selection
if not hasattr(bpy.types.Scene, "last_clicked_group"):
bpy.types.Scene.last_clicked_group = {}
# Ghost Buster properties
bpy.types.Scene.ghost_buster_delete_low_priority = bpy.props.BoolProperty( # type: ignore
name="Delete Low Priority Ghosts",
description="Delete objects not in scenes with no legitimate use and users < 2",
default=False
)
def RBST_DatRem_unregister_properties():
del bpy.types.Scene.dataremap_images
del bpy.types.Scene.dataremap_materials
del bpy.types.Scene.dataremap_fonts
del bpy.types.Scene.dataremap_worlds
del bpy.types.Scene.dataremap_node_groups
del bpy.types.Scene.show_image_duplicates
del bpy.types.Scene.show_material_duplicates
del bpy.types.Scene.show_font_duplicates
del bpy.types.Scene.show_world_duplicates
del bpy.types.Scene.show_node_group_duplicates
if hasattr(bpy.types.Scene, "excluded_remap_groups"):
del bpy.types.Scene.excluded_remap_groups
if hasattr(bpy.types.Scene, "expanded_remap_groups"):
@@ -157,10 +173,7 @@ def RBST_DatRem_unregister_properties():
del bpy.types.Scene.dataremap_sort_materials
del bpy.types.Scene.dataremap_sort_fonts
del bpy.types.Scene.dataremap_sort_worlds
# Delete ghost buster properties
if hasattr(bpy.types.Scene, "ghost_buster_delete_low_priority"):
del bpy.types.Scene.ghost_buster_delete_low_priority
del bpy.types.Scene.dataremap_sort_node_groups
def RBST_DatRem_get_base_name(name):
"""Extract the base name without numbered suffix"""
@@ -194,6 +207,56 @@ def RBST_DatRem_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 RBST_DatRem_get_node_group_tree_label(node_group):
"""Return a short label for a node group's tree type."""
tree_id = getattr(node_group, 'bl_idname', '') or ''
if tree_id.endswith('NodeTree'):
return tree_id[:-8]
return tree_id or 'Unknown'
def RBST_DatRem_format_node_group_group_key(group_key):
"""Format a node group group key for display."""
if ':' not in group_key:
return group_key
tree_id, base_name = group_key.split(':', 1)
tree_label = tree_id.replace('NodeTree', '') if tree_id.endswith('NodeTree') else tree_id
return f"{base_name} ({tree_label})"
def RBST_DatRem_find_node_group_data_groups():
"""Group node groups by tree type and base name."""
groups = {}
for node_group in bpy.data.node_groups:
if node_group.users == 0:
continue
if hasattr(node_group, 'library') and node_group.library is not None:
continue
base_name = RBST_DatRem_get_base_name(node_group.name)
tree_id = getattr(node_group, 'bl_idname', 'UnknownNodeTree')
group_key = f"{tree_id}:{base_name}"
if group_key not in groups:
groups[group_key] = []
groups[group_key].append(node_group)
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 RBST_DatRem_get_duplicate_groups(data_type):
"""Return duplicate groups for a remapper data type."""
if data_type == "images":
return RBST_DatRem_find_data_groups(bpy.data.images)
if data_type == "materials":
return RBST_DatRem_find_data_groups(bpy.data.materials)
if data_type == "fonts":
return RBST_DatRem_find_data_groups(bpy.data.fonts)
if data_type == "worlds":
return RBST_DatRem_find_data_groups(bpy.data.worlds)
if data_type == "node_groups":
return RBST_DatRem_find_node_group_data_groups()
return {}
def RBST_DatRem_find_target_data(data_group):
"""Find the target data block to remap to"""
# Filter out linked datablocks
@@ -242,7 +305,7 @@ def RBST_DatRem_clean_data_names(data_collection):
return cleaned_count
def RBST_DatRem_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_node_groups):
"""Remap redundant data blocks to their base versions like Blender's Remap Users function, and clean up names."""
remapped_count = 0
cleaned_count = 0
@@ -587,6 +650,35 @@ def RBST_DatRem_remap_data_blocks(context, remap_images, remap_materials, remap_
# Then clean up any remaining numbered suffixes
cleaned_count += RBST_DatRem_clean_data_names(bpy.data.worlds)
# Process node groups
if remap_node_groups:
node_group_groups = RBST_DatRem_find_node_group_data_groups()
for group_key, node_groups in node_group_groups.items():
if f"node_groups:{group_key}" in context.scene.excluded_remap_groups:
continue
target_group = RBST_DatRem_find_target_data(node_groups)
if RBST_DatRem_get_base_name(target_group.name) != target_group.name:
try:
target_group.name = RBST_DatRem_get_base_name(target_group.name)
except AttributeError:
print(f"Warning: Cannot rename linked node group {target_group.name}")
continue
try:
for node_group in node_groups:
if node_group != target_group:
try:
node_group.user_remap(target_group)
remapped_count += 1
except AttributeError:
print(f"Warning: Cannot remap linked node group {node_group.name}")
except Exception as e:
print(f"Error using built-in remap for node groups: {e}")
cleaned_count += RBST_DatRem_clean_data_names(bpy.data.node_groups)
# Force an update of the dependency graph to ensure all users are properly updated
if context.view_layer:
context.view_layer.update()
@@ -605,14 +697,20 @@ class RBST_DatRem_OT_RemapData(bpy.types.Operator):
remap_materials = context.scene.dataremap_materials
remap_fonts = context.scene.dataremap_fonts
remap_worlds = context.scene.dataremap_worlds
remap_node_groups = context.scene.dataremap_node_groups
# Count duplicates before remapping (only for local datablocks)
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 {}
node_group_groups = RBST_DatRem_find_node_group_data_groups() if remap_node_groups else {}
total_duplicates = sum(len(group) - 1 for groups in [image_groups, material_groups, font_groups, world_groups] for group in groups.values())
total_duplicates = sum(
len(group) - 1
for groups in [image_groups, material_groups, font_groups, world_groups, node_group_groups]
for group in groups.values()
)
# Count data blocks with numbered suffixes (only for local datablocks)
total_numbered = 0
@@ -636,6 +734,11 @@ class RBST_DatRem_OT_RemapData(bpy.types.Operator):
if world.users > 0
and not (hasattr(world, 'library') and world.library is not None)
and RBST_DatRem_get_base_name(world.name) != world.name)
if remap_node_groups:
total_numbered += sum(1 for node_group in bpy.data.node_groups
if node_group.users > 0
and not (hasattr(node_group, 'library') and node_group.library is not None)
and RBST_DatRem_get_base_name(node_group.name) != node_group.name)
if total_duplicates == 0 and total_numbered == 0:
self.report({'INFO'}, "No local data blocks to process")
@@ -647,7 +750,8 @@ class RBST_DatRem_OT_RemapData(bpy.types.Operator):
remap_images,
remap_materials,
remap_fonts,
remap_worlds
remap_worlds,
remap_node_groups,
)
# Report results
@@ -808,15 +912,7 @@ class RBST_DatRem_OT_SelectAllGroups(bpy.types.Operator):
context.scene.excluded_remap_groups = {}
# Get the appropriate data groups based on data_type
data_groups = {}
if self.data_type == "images":
data_groups = RBST_DatRem_find_data_groups(bpy.data.images)
elif self.data_type == "materials":
data_groups = RBST_DatRem_find_data_groups(bpy.data.materials)
elif self.data_type == "fonts":
data_groups = RBST_DatRem_find_data_groups(bpy.data.fonts)
elif self.data_type == "worlds":
data_groups = RBST_DatRem_find_data_groups(bpy.data.worlds)
data_groups = RBST_DatRem_get_duplicate_groups(self.data_type)
# Process only groups with more than one item
for base_name, items in data_groups.items():
@@ -873,15 +969,7 @@ class RBST_DatRem_OT_ToggleGroupSelection(bpy.types.Operator):
last_group = context.scene.last_clicked_group[self.data_type]
# Get all data groups for this data type
data_groups = []
if self.data_type == "images":
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.images).keys())
elif self.data_type == "materials":
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.materials).keys())
elif self.data_type == "fonts":
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.fonts).keys())
elif self.data_type == "worlds":
data_groups = list(RBST_DatRem_find_data_groups(bpy.data.worlds).keys())
data_groups = list(RBST_DatRem_get_duplicate_groups(self.data_type).keys())
# Find the indices of the last clicked group and the current group
try:
@@ -966,6 +1054,10 @@ def RBST_DatRem_search_matches_group(group, search_string):
# Check base name
if search_lower in base_name.lower():
return True
if ':' in base_name:
formatted_name = RBST_DatRem_format_node_group_group_key(base_name)
if search_lower in formatted_name.lower():
return True
# Check all item names in group
for item in items:
if search_lower in item.name.lower():
@@ -1075,9 +1167,13 @@ def RBST_DatRem_draw_data_duplicates(layout, context, data_type, data_groups):
row.template_icon(icon_value=target_item.preview.icon_id, scale=1.0)
else:
row.label(text="", icon='WORLD')
elif data_type == "node_groups":
row.label(text="", icon='NODETREE')
display_name = RBST_DatRem_format_node_group_group_key(base_name) if data_type == "node_groups" else base_name
# Add rename operator for the group name
rename_op = row.operator("bst.rename_datablock_remap", text=f"{base_name}: {len(items)} versions", emboss=False)
rename_op = row.operator("bst.rename_datablock_remap", text=f"{display_name}: {len(items)} versions", emboss=False)
rename_op.data_type = data_type
rename_op.old_name = target_item.name
@@ -1112,9 +1208,15 @@ def RBST_DatRem_draw_data_duplicates(layout, context, data_type, data_groups):
sub_row.template_icon(icon_value=item.preview.icon_id, scale=1.0)
else:
sub_row.label(text="", icon='WORLD')
elif data_type == "node_groups":
sub_row.label(text="", icon='NODETREE')
item_label = item.name
if data_type == "node_groups":
item_label = f"{item.name} ({RBST_DatRem_get_node_group_tree_label(item)})"
# Add rename operator for each item
rename_op = sub_row.operator("bst.rename_datablock_remap", text=f"{item.name}", emboss=False)
rename_op = sub_row.operator("bst.rename_datablock_remap", text=item_label, emboss=False)
rename_op.data_type = data_type
rename_op.old_name = item.name
@@ -1160,9 +1262,14 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
linked_types.append("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 context.scene.dataremap_node_groups and RBST_DatRem_has_linked_datablocks(bpy.data.node_groups):
linked_datablocks_found = True
linked_types.append("node groups")
linked_paths.update(RBST_DatRem_get_linked_file_paths(bpy.data.node_groups))
# Display warning about linked datablocks inside the remapper box
if linked_datablocks_found:
warning_box = layout.box()
warning_box = box.box()
warning_box.alert = True
warning_box.label(text="Warning: Linked datablocks detected", icon='ERROR')
warning_box.label(text=f"Types: {', '.join(linked_types)}")
@@ -1192,16 +1299,19 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
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)
node_group_groups = RBST_DatRem_find_node_group_data_groups()
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())
node_group_duplicates = sum(len(group) - 1 for group in node_group_groups.values())
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)
node_group_numbered = sum(1 for node_group in bpy.data.node_groups if node_group.users > 0 and RBST_DatRem_get_base_name(node_group.name) != node_group.name)
# Initialize excluded_remap_groups if it doesn't exist
if not hasattr(context.scene, "excluded_remap_groups"):
@@ -1332,6 +1442,33 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
if context.scene.show_world_duplicates and world_duplicates > 0 and context.scene.dataremap_worlds:
RBST_DatRem_draw_data_duplicates(col, context, "worlds", world_groups)
# Node Groups
row = col.row()
split = row.split(factor=0.6)
sub_row = split.row()
op = sub_row.operator("bst.toggle_data_type", text="", icon='NODETREE', depress=context.scene.dataremap_node_groups)
op.data_type = "node_groups"
if context.scene.dataremap_node_groups:
sub_row.label(text="Node Groups")
else:
sub_row.label(text="Node Groups", icon='RADIOBUT_OFF')
sub_row = split.row()
if node_group_duplicates > 0:
sub_row.prop(context.scene, "show_node_group_duplicates",
text=f"{node_group_duplicates} duplicates",
icon='DISCLOSURE_TRI_DOWN' if context.scene.show_node_group_duplicates else 'DISCLOSURE_TRI_RIGHT',
emboss=False)
elif node_group_numbered > 0:
sub_row.label(text=f"{node_group_numbered} numbered")
else:
sub_row.label(text="0 duplicates")
if context.scene.show_node_group_duplicates and node_group_duplicates > 0 and context.scene.dataremap_node_groups:
RBST_DatRem_draw_data_duplicates(col, context, "node_groups", node_group_groups)
# Add the operator button
row = box.row()
row.scale_y = 1.5
@@ -1355,8 +1492,8 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
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
total_duplicates = image_duplicates + material_duplicates + font_duplicates + world_duplicates + node_group_duplicates
total_numbered = image_numbered + material_numbered + font_numbered + world_numbered + node_group_numbered
if total_duplicates > 0 or total_numbered > 0:
box.separator()
@@ -1367,37 +1504,9 @@ class RBST_DatRem_PT_BulkDataRemap(bpy.types.Panel):
else:
box.label(text="No data blocks to process")
# Add a separator and purge button
box.separator()
row = box.row()
row.operator("bst.purge_unused_data", icon='TRASH')
# Ghost Buster section
layout.separator()
ghost_box = layout.box()
ghost_box.label(text="Ghost Buster - Experimental")
col = ghost_box.column()
col.label(text="Ghost data cleanup & library override fixes:")
col.label(text="• Unused local WGT widget objects")
col.label(text="• Empty unlinked collections")
col.label(text="• Objects not in scenes with no legitimate use")
col.label(text="• Fix broken library override hierarchies")
# Two button layout
row = ghost_box.row(align=True)
row = layout.row()
row.scale_y = 1.5
row.operator("bst.ghost_detector", text="Ghost Detector", icon='ZOOM_IN')
row.operator("bst.ghost_buster", text="Ghost Buster", icon='GHOST_ENABLED')
# Ghost Buster option
ghost_box.prop(context.scene, "ghost_buster_delete_low_priority", text="Delete Low Priority Ghosts")
# Resync Enforce button
ghost_box.separator()
row = ghost_box.row()
row.scale_y = 1.2
row.operator("bst.resync_enforce", text="Resync Enforce", icon='FILE_REFRESH')
row.operator("bst.purge_unused_data", icon='TRASH')
# Add a new operator for toggling data types
class RBST_DatRem_OT_ToggleDataType(bpy.types.Operator):
@@ -1421,6 +1530,8 @@ class RBST_DatRem_OT_ToggleDataType(bpy.types.Operator):
context.scene.dataremap_fonts = not context.scene.dataremap_fonts
elif self.data_type == "worlds":
context.scene.dataremap_worlds = not context.scene.dataremap_worlds
elif self.data_type == "node_groups":
context.scene.dataremap_node_groups = not context.scene.dataremap_node_groups
return {'FINISHED'}
@@ -1543,6 +1654,8 @@ class RBST_DatRem_OT_RenameDatablock(bpy.types.Operator):
data_collection = bpy.data.fonts
elif self.data_type == "worlds":
data_collection = bpy.data.worlds
elif self.data_type == "node_groups":
data_collection = bpy.data.node_groups
if not data_collection:
self.report({'ERROR'}, "Invalid data type")
@@ -395,7 +395,7 @@ class RBST_ViewDisp_OT_RefreshMaterialPreviews(bpy.types.Operator):
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 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:
@@ -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.2, 4.4, 4.5, and 5.0.
Officially supports Blender 4.5 and 5.2.
## Installation
@@ -1,6 +1,6 @@
"""
This module provides API compatibility functions for handling differences
between Blender 4.2, 4.4, 4.5, and 5.0.
between Blender 4.5 and 5.2.
"""
@@ -1,16 +1,14 @@
"""
This module provides version detection and comparison utilities for
multi-version Blender support (4.2, 4.4, 4.5, and 5.0).
multi-version Blender support (4.5 and 5.2).
"""
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)
VERSION_5_2 = (5, 2, 0)
def get_blender_version():
@@ -25,7 +23,7 @@ def get_blender_version():
def get_version_string():
"""
Returns the current Blender version as a string (e.g., "4.2.0").
Returns the current Blender version as a string (e.g., "4.5.0").
Returns:
str: Version string in format "major.minor.patch"
@@ -76,35 +74,16 @@ 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
str: '4.5' or '5.2' for officially supported versions; otherwise 'major.minor'
"""
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
if major == 4 and minor >= 5:
return '4.5'
if major >= 5 and is_version_at_least(5, 2, 0):
return '5.2'
return f"{major}.{minor}"
def is_version_4_5():
@@ -116,3 +95,7 @@ def is_version_5_0():
"""Check if running Blender 5.0 or later."""
return is_version_at_least(5, 0, 0)
def is_version_5_2():
"""Check if running Blender 5.2 LTS or later."""
return is_version_at_least(5, 2, 0)
File diff suppressed because one or more lines are too long