2026-01-01
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"id": "basedplayblast",
|
||||
"name": "BasedPlayblast",
|
||||
"tagline": "Easily create playblasts from Blender and Flamenco",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.1",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
|
||||
"license": [
|
||||
@@ -24,16 +24,16 @@
|
||||
"Workflow",
|
||||
"Video"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/BasedPlayblast/releases/download/v2.6.0/BasedPlayblast.v2.6.0.zip",
|
||||
"archive_size": 47989,
|
||||
"archive_hash": "sha256:ba8307675a0ca0d24496c7151e84349608fee709cc088dc82acaacec56d1dc7f"
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/BasedPlayblast/releases/download/v2.6.1/BasedPlayblast.v2.6.1.zip",
|
||||
"archive_size": 48471,
|
||||
"archive_hash": "sha256:ce9740ad252a00643f75202b53c9ef1e9c6ee8b5a2d34cbaf751b4084e78665c"
|
||||
},
|
||||
{
|
||||
"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>",
|
||||
"license": [
|
||||
@@ -49,16 +49,16 @@
|
||||
"Workflow",
|
||||
"Materials"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v0.12.0/Rainys_Bulk_Scene_Tools.v0.12.0.zip",
|
||||
"archive_size": 75117,
|
||||
"archive_hash": "sha256:0607fafbd9f74f792fdb96e5913f03d9e4cc13cff8b5e3225468174959ca5b18"
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v0.14.0/Rainys_Bulk_Scene_Tools.v0.14.0.zip",
|
||||
"archive_size": 78363,
|
||||
"archive_hash": "sha256:943c723511fb8d7199bf079cb94ba63c552d6477b9a4e003bfffc185c169ea4b"
|
||||
},
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"id": "atomic_data_manager",
|
||||
"name": "Atomic Data Manager",
|
||||
"tagline": "Smart cleanup and inspection of Blender data-blocks",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon",
|
||||
"license": [
|
||||
@@ -70,9 +70,9 @@
|
||||
"management",
|
||||
"cleanup"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.0.0/Atomic_Data_Manager.v2.0.0.zip",
|
||||
"archive_size": 67447,
|
||||
"archive_hash": "sha256:5adf9ff89d1d24eaa79012b2a6c86f962fc107abc09b16a065e8327fbe57fb10"
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.1.0/Atomic_Data_Manager.v2.1.0.zip",
|
||||
"archive_size": 73646,
|
||||
"archive_hash": "sha256:a10f6b7eb9d7c437574c66dc15f73d74a0ff86e793c7460804d7bf5cb7cb29cc"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"version": "v1",
|
||||
"blocklist": [],
|
||||
"data": [
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"id": "basedplayblast",
|
||||
"name": "BasedPlayblast",
|
||||
"tagline": "Easily create playblasts from Blender and Flamenco",
|
||||
"version": "2.6.1",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"website": "https://github.com/RaincloudTheDragon/BasedPlayblast",
|
||||
"permissions": {
|
||||
"files": "Import/export files and data"
|
||||
},
|
||||
"tags": [
|
||||
"Animation",
|
||||
"Render",
|
||||
"Workflow",
|
||||
"Video"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/BasedPlayblast/releases/download/v2.6.1/BasedPlayblast.v2.6.1.zip",
|
||||
"archive_size": 48471,
|
||||
"archive_hash": "sha256:ce9740ad252a00643f75202b53c9ef1e9c6ee8b5a2d34cbaf751b4084e78665c"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"website": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools",
|
||||
"permissions": {
|
||||
"files": "Read and write external resources referenced by scenes"
|
||||
},
|
||||
"tags": [
|
||||
"Scene",
|
||||
"Workflow",
|
||||
"Materials"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v0.14.0/Rainys_Bulk_Scene_Tools.v0.14.0.zip",
|
||||
"archive_size": 78363,
|
||||
"archive_hash": "sha256:943c723511fb8d7199bf079cb94ba63c552d6477b9a4e003bfffc185c169ea4b"
|
||||
},
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"id": "atomic_data_manager",
|
||||
"name": "Atomic Data Manager",
|
||||
"tagline": "Smart cleanup and inspection of Blender data-blocks",
|
||||
"version": "2.1.0",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"tags": [
|
||||
"utility",
|
||||
"management",
|
||||
"cleanup"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.1.0/Atomic_Data_Manager.v2.1.0.zip",
|
||||
"archive_size": 73646,
|
||||
"archive_hash": "sha256:a10f6b7eb9d7c437574c66dc15f73d74a0ff86e793c7460804d7bf5cb7cb29cc"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"version": "v1",
|
||||
"blocklist": [],
|
||||
"data": [
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"id": "basedplayblast",
|
||||
"name": "BasedPlayblast",
|
||||
"tagline": "Easily create playblasts from Blender and Flamenco",
|
||||
"version": "2.6.1",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"website": "https://github.com/RaincloudTheDragon/BasedPlayblast",
|
||||
"permissions": {
|
||||
"files": "Import/export files and data"
|
||||
},
|
||||
"tags": [
|
||||
"Animation",
|
||||
"Render",
|
||||
"Workflow",
|
||||
"Video"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/BasedPlayblast/releases/download/v2.6.1/BasedPlayblast.v2.6.1.zip",
|
||||
"archive_size": 48471,
|
||||
"archive_hash": "sha256:ce9740ad252a00643f75202b53c9ef1e9c6ee8b5a2d34cbaf751b4084e78665c"
|
||||
},
|
||||
{
|
||||
"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.13.1",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"website": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools",
|
||||
"permissions": {
|
||||
"files": "Read and write external resources referenced by scenes"
|
||||
},
|
||||
"tags": [
|
||||
"Scene",
|
||||
"Workflow",
|
||||
"Materials"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v.013.1/Rainys_Bulk_Scene_Tools.v0.13.1.zip",
|
||||
"archive_size": 77484,
|
||||
"archive_hash": "sha256:7c6e6f92e4c9f871d471e2eb31c22ff9b7614a24dbfa5d1f668f7909908b8307"
|
||||
},
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"id": "atomic_data_manager",
|
||||
"name": "Atomic Data Manager",
|
||||
"tagline": "Smart cleanup and inspection of Blender data-blocks",
|
||||
"version": "2.0.0",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"tags": [
|
||||
"utility",
|
||||
"management",
|
||||
"cleanup"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.0.0/Atomic_Data_Manager.v2.0.0.zip",
|
||||
"archive_size": 67447,
|
||||
"archive_hash": "sha256:5adf9ff89d1d24eaa79012b2a6c86f962fc107abc09b16a065e8327fbe57fb10"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"version": "v1",
|
||||
"blocklist": [],
|
||||
"data": [
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"id": "basedplayblast",
|
||||
"name": "BasedPlayblast",
|
||||
"tagline": "Easily create playblasts from Blender and Flamenco",
|
||||
"version": "2.6.1",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"website": "https://github.com/RaincloudTheDragon/BasedPlayblast",
|
||||
"permissions": {
|
||||
"files": "Import/export files and data"
|
||||
},
|
||||
"tags": [
|
||||
"Animation",
|
||||
"Render",
|
||||
"Workflow",
|
||||
"Video"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/BasedPlayblast/releases/download/v2.6.1/BasedPlayblast.v2.6.1.zip",
|
||||
"archive_size": 48471,
|
||||
"archive_hash": "sha256:ce9740ad252a00643f75202b53c9ef1e9c6ee8b5a2d34cbaf751b4084e78665c"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon <raincloudthedragon@gmail.com>",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"website": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools",
|
||||
"permissions": {
|
||||
"files": "Read and write external resources referenced by scenes"
|
||||
},
|
||||
"tags": [
|
||||
"Scene",
|
||||
"Workflow",
|
||||
"Materials"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/Rainys-Bulk-Scene-Tools/releases/download/v0.12.0/Rainys_Bulk_Scene_Tools.v0.12.0.zip",
|
||||
"archive_size": 75117,
|
||||
"archive_hash": "sha256:0607fafbd9f74f792fdb96e5913f03d9e4cc13cff8b5e3225468174959ca5b18"
|
||||
},
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"id": "atomic_data_manager",
|
||||
"name": "Atomic Data Manager",
|
||||
"tagline": "Smart cleanup and inspection of Blender data-blocks",
|
||||
"version": "2.0.0",
|
||||
"type": "add-on",
|
||||
"maintainer": "RaincloudTheDragon",
|
||||
"license": [
|
||||
"GPL-3.0-or-later"
|
||||
],
|
||||
"blender_version_min": "4.2.0",
|
||||
"tags": [
|
||||
"utility",
|
||||
"management",
|
||||
"cleanup"
|
||||
],
|
||||
"archive_url": "https://github.com/RaincloudTheDragon/atomic-data-manager/releases/download/v2.0.0/Atomic_Data_Manager.v2.0.0.zip",
|
||||
"archive_size": 67447,
|
||||
"archive_hash": "sha256:5adf9ff89d1d24eaa79012b2a6c86f962fc107abc09b16a065e8327fbe57fb10"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,41 @@
|
||||
# Changelog
|
||||
## [v2.1.0] - 2025-12-18
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
### Features
|
||||
- Added support for detecting unused objects and armatures (#1)
|
||||
- Objects not present in any scene collections are now detected as unused
|
||||
- Armatures not used by any objects in scenes (including direct use, modifiers, and constraints like "Child Of") are detected as unused
|
||||
- Smart Select and Clean operations now support objects and armatures
|
||||
|
||||
### Fixes
|
||||
- Fixed material detection in Geometry Nodes Set Material nodes
|
||||
- Materials used in Geometry Nodes' "Set Material" nodes are now correctly detected as used
|
||||
- Fixed legacy issue where materials in node groups (e.g., "outline-highlight" in "box-highlight" node group) were incorrectly flagged as unused
|
||||
- This was a hangover from Atomic never being developed past Blender 2.93, where Geometry Nodes Set Material nodes use input sockets rather than direct material properties
|
||||
- Performance optimizations for Smart Select and Clean operations (#3)
|
||||
- Removed inefficient threading implementation that was causing poor performance
|
||||
- Implemented short-circuiting logic in Smart Select to exit early when unused items are found
|
||||
- Fixed UI operators to use cached values instead of recalculating on every draw call
|
||||
- Note: Further performance improvements are limited by Blender's Python API being single-threaded and requiring sequential access to `bpy.data` collections, making true parallelization impossible without risking data corruption
|
||||
|
||||
### Internal
|
||||
- Removed incorrect "Remington Creative" copyright notices from newly created files
|
||||
- Updated repository configuration in manifest
|
||||
|
||||
## [v2.0.3] - 2025-12-17
|
||||
|
||||
### Fixes
|
||||
- Fixed missing import error in missing file detection
|
||||
|
||||
## [v2.0.2] - 2025-12-17
|
||||
|
||||
### Fixes
|
||||
- Atomic now completely ignores all library-linked and override datablocks across all operations, as originally intended.
|
||||
|
||||
## [v2.0.1] - 2025-12-16
|
||||
|
||||
### Fixes
|
||||
- Blender 5.0 compatibility: Fixed `AttributeError` when detecting missing library files (Library objects use `packed_file` singular, Image objects use `packed_files` plural in 5.0)
|
||||
- Fixed unregistration errors in Blender 4.5 by using safe unregister functions throughout the codebase
|
||||
|
||||
## [v2.0.0] - Raincloud's first re-release
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ registration for all packages within the add-on.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from .utils import compat
|
||||
from . import ops
|
||||
from . import ui
|
||||
from .ui import inspect_ui
|
||||
@@ -40,8 +40,10 @@ class ATOMIC_PG_main(bpy.types.PropertyGroup):
|
||||
lights: bpy.props.BoolProperty(default=False)
|
||||
materials: bpy.props.BoolProperty(default=False)
|
||||
node_groups: bpy.props.BoolProperty(default=False)
|
||||
objects: bpy.props.BoolProperty(default=False)
|
||||
particles: bpy.props.BoolProperty(default=False)
|
||||
textures: bpy.props.BoolProperty(default=False)
|
||||
armatures: bpy.props.BoolProperty(default=False)
|
||||
worlds: bpy.props.BoolProperty(default=False)
|
||||
|
||||
# inspect data-block search fields
|
||||
@@ -66,6 +68,12 @@ class ATOMIC_PG_main(bpy.types.PropertyGroup):
|
||||
textures_field: bpy.props.StringProperty(
|
||||
update=inspect_ui.update_inspection)
|
||||
|
||||
objects_field: bpy.props.StringProperty(
|
||||
update=inspect_ui.update_inspection)
|
||||
|
||||
armatures_field: bpy.props.StringProperty(
|
||||
update=inspect_ui.update_inspection)
|
||||
|
||||
worlds_field: bpy.props.StringProperty(
|
||||
update=inspect_ui.update_inspection)
|
||||
|
||||
@@ -111,6 +119,16 @@ class ATOMIC_PG_main(bpy.types.PropertyGroup):
|
||||
'WORLDS',
|
||||
'Worlds',
|
||||
'Worlds'
|
||||
),
|
||||
(
|
||||
'OBJECTS',
|
||||
'Objects',
|
||||
'Objects'
|
||||
),
|
||||
(
|
||||
'ARMATURES',
|
||||
'Armatures',
|
||||
'Armatures'
|
||||
)
|
||||
],
|
||||
default='COLLECTIONS'
|
||||
@@ -188,7 +206,14 @@ class ATOMIC_PG_main(bpy.types.PropertyGroup):
|
||||
'Worlds',
|
||||
'WORLD',
|
||||
9
|
||||
)
|
||||
),
|
||||
(
|
||||
'ARMATURES',
|
||||
'Armatures',
|
||||
'Armatures',
|
||||
'ARMATURE_DATA',
|
||||
10
|
||||
)
|
||||
],
|
||||
default='OVERVIEW'
|
||||
)
|
||||
@@ -220,5 +245,5 @@ def unregister():
|
||||
ui.unregister()
|
||||
ops.unregister()
|
||||
|
||||
unregister_class(ATOMIC_PG_main)
|
||||
compat.safe_unregister_class(ATOMIC_PG_main)
|
||||
del bpy.types.Scene.atomic
|
||||
|
||||
@@ -2,14 +2,14 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "atomic_data_manager"
|
||||
name = "Atomic Data Manager"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
type = "add-on"
|
||||
author = "RaincloudTheDragon"
|
||||
maintainer = "RaincloudTheDragon"
|
||||
blender_version_min = "4.2.0"
|
||||
license = ["GPL-3.0-or-later"]
|
||||
description = "An Intelligent Data Manager for Blender."
|
||||
homepage = "https://github.com/grantwilk/atomic-data-manager"
|
||||
homepage = "https://github.com/RaincloudTheDragon/atomic-data-manager/"
|
||||
tagline = "Smart cleanup and inspection of Blender data-blocks"
|
||||
|
||||
tags = ["utility", "management", "cleanup"]
|
||||
|
||||
@@ -30,9 +30,10 @@ intefaces in Blender.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from .. import config
|
||||
from ..stats import unused
|
||||
from ..stats import unused_parallel
|
||||
from .utils import nuke
|
||||
from .utils import clean
|
||||
from ..ui.utils import ui_layouts
|
||||
@@ -157,8 +158,10 @@ class ATOMIC_OT_clean_all(bpy.types.Operator):
|
||||
unused_lights = []
|
||||
unused_materials = []
|
||||
unused_node_groups = []
|
||||
unused_objects = []
|
||||
unused_particles = []
|
||||
unused_textures = []
|
||||
unused_armatures = []
|
||||
unused_worlds = []
|
||||
|
||||
def draw(self, context):
|
||||
@@ -167,67 +170,74 @@ class ATOMIC_OT_clean_all(bpy.types.Operator):
|
||||
col = layout.column()
|
||||
col.label(text="Remove the following data-blocks?")
|
||||
|
||||
collections = sorted(unused.collections_deep())
|
||||
# Use cached values from invoke() instead of recalculating
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Collections",
|
||||
items=collections,
|
||||
items=sorted(self.unused_collections),
|
||||
icon="OUTLINER_OB_GROUP_INSTANCE"
|
||||
)
|
||||
|
||||
images = sorted(unused.images_deep())
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Images",
|
||||
items=images,
|
||||
items=sorted(self.unused_images),
|
||||
icon="IMAGE_DATA"
|
||||
)
|
||||
|
||||
lights = sorted(unused.lights_deep())
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Lights",
|
||||
items=lights,
|
||||
items=sorted(self.unused_lights),
|
||||
icon="OUTLINER_OB_LIGHT"
|
||||
)
|
||||
|
||||
materials = sorted(unused.materials_deep())
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Materials",
|
||||
items=materials,
|
||||
items=sorted(self.unused_materials),
|
||||
icon="MATERIAL"
|
||||
)
|
||||
|
||||
node_groups = sorted(unused.node_groups_deep())
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Node Groups",
|
||||
items=node_groups,
|
||||
items=sorted(self.unused_node_groups),
|
||||
icon="NODETREE"
|
||||
)
|
||||
|
||||
particles = sorted(unused.particles_deep())
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Objects",
|
||||
items=sorted(self.unused_objects),
|
||||
icon="OBJECT_DATA"
|
||||
)
|
||||
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Particle Systems",
|
||||
items=particles,
|
||||
items=sorted(self.unused_particles),
|
||||
icon="PARTICLES"
|
||||
)
|
||||
|
||||
textures = sorted(unused.textures_deep())
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Textures",
|
||||
items=textures,
|
||||
items=sorted(self.unused_textures),
|
||||
icon="TEXTURE"
|
||||
)
|
||||
|
||||
worlds = sorted(unused.worlds())
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Armatures",
|
||||
items=sorted(self.unused_armatures),
|
||||
icon="ARMATURE_DATA"
|
||||
)
|
||||
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Worlds",
|
||||
items=worlds,
|
||||
items=sorted(self.unused_worlds),
|
||||
icon="WORLD"
|
||||
)
|
||||
|
||||
@@ -240,8 +250,10 @@ class ATOMIC_OT_clean_all(bpy.types.Operator):
|
||||
clean.lights()
|
||||
clean.materials()
|
||||
clean.node_groups()
|
||||
clean.objects()
|
||||
clean.particles()
|
||||
clean.textures()
|
||||
clean.armatures()
|
||||
clean.worlds()
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -249,14 +261,19 @@ class ATOMIC_OT_clean_all(bpy.types.Operator):
|
||||
def invoke(self, context, event):
|
||||
wm = context.window_manager
|
||||
|
||||
self.unused_collections = unused.collections_deep()
|
||||
self.unused_images = unused.images_deep()
|
||||
self.unused_lights = unused.lights_deep()
|
||||
self.unused_materials = unused.materials_deep()
|
||||
self.unused_node_groups = unused.node_groups_deep()
|
||||
self.unused_particles = unused.particles_deep()
|
||||
self.unused_textures = unused.textures_deep()
|
||||
self.unused_worlds = unused.worlds()
|
||||
# Use parallel execution for better performance
|
||||
all_unused = unused_parallel.get_all_unused_parallel()
|
||||
|
||||
self.unused_collections = all_unused['collections']
|
||||
self.unused_images = all_unused['images']
|
||||
self.unused_lights = all_unused['lights']
|
||||
self.unused_materials = all_unused['materials']
|
||||
self.unused_node_groups = all_unused['node_groups']
|
||||
self.unused_objects = all_unused['objects']
|
||||
self.unused_particles = all_unused['particles']
|
||||
self.unused_textures = all_unused['textures']
|
||||
self.unused_armatures = all_unused['armatures']
|
||||
self.unused_worlds = all_unused['worlds']
|
||||
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
@@ -790,4 +807,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for item in reg_list:
|
||||
unregister_class(item)
|
||||
compat.safe_unregister_class(item)
|
||||
|
||||
@@ -26,11 +26,18 @@ operators.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from .utils import delete
|
||||
from .utils import duplicate
|
||||
|
||||
|
||||
def _check_library_or_override(datablock):
|
||||
"""Check if datablock is library-linked or override, return error message if so."""
|
||||
if compat.is_library_or_override(datablock):
|
||||
return "Cannot modify library-linked or override datablocks"
|
||||
return None
|
||||
|
||||
|
||||
# Atomic Data Manager Inspection Rename Operator
|
||||
class ATOMIC_OT_inspection_rename(bpy.types.Operator):
|
||||
"""Give this data-block a new name"""
|
||||
@@ -51,35 +58,75 @@ class ATOMIC_OT_inspection_rename(bpy.types.Operator):
|
||||
name = atom.rename_field
|
||||
|
||||
if inspection == 'COLLECTIONS':
|
||||
bpy.data.collections[atom.collections_field].name = name
|
||||
collection = bpy.data.collections[atom.collections_field]
|
||||
error = _check_library_or_override(collection)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
collection.name = name
|
||||
atom.collections_field = name
|
||||
|
||||
if inspection == 'IMAGES':
|
||||
bpy.data.images[atom.images_field].name = name
|
||||
image = bpy.data.images[atom.images_field]
|
||||
error = _check_library_or_override(image)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
image.name = name
|
||||
atom.images_field = name
|
||||
|
||||
if inspection == 'LIGHTS':
|
||||
bpy.data.lights[atom.lights_field].name = name
|
||||
light = bpy.data.lights[atom.lights_field]
|
||||
error = _check_library_or_override(light)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
light.name = name
|
||||
atom.lights_field = name
|
||||
|
||||
if inspection == 'MATERIALS':
|
||||
bpy.data.materials[atom.materials_field].name = name
|
||||
material = bpy.data.materials[atom.materials_field]
|
||||
error = _check_library_or_override(material)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
material.name = name
|
||||
atom.materials_field = name
|
||||
|
||||
if inspection == 'NODE_GROUPS':
|
||||
bpy.data.node_groups[atom.node_groups_field].name = name
|
||||
node_group = bpy.data.node_groups[atom.node_groups_field]
|
||||
error = _check_library_or_override(node_group)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
node_group.name = name
|
||||
atom.node_groups_field = name
|
||||
|
||||
if inspection == 'PARTICLES':
|
||||
bpy.data.particles[atom.particles_field].name = name
|
||||
particle = bpy.data.particles[atom.particles_field]
|
||||
error = _check_library_or_override(particle)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
particle.name = name
|
||||
atom.particles_field = name
|
||||
|
||||
if inspection == 'TEXTURES':
|
||||
bpy.data.textures[atom.textures_field].name = name
|
||||
texture = bpy.data.textures[atom.textures_field]
|
||||
error = _check_library_or_override(texture)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
texture.name = name
|
||||
atom.textures_field = name
|
||||
|
||||
if inspection == 'WORLDS':
|
||||
bpy.data.worlds[atom.worlds_field].name = name
|
||||
world = bpy.data.worlds[atom.worlds_field]
|
||||
error = _check_library_or_override(world)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
world.name = name
|
||||
atom.worlds_field = name
|
||||
|
||||
atom.rename_field = ""
|
||||
@@ -172,44 +219,72 @@ class ATOMIC_OT_inspection_replace(bpy.types.Operator):
|
||||
|
||||
if inspection == 'IMAGES' and \
|
||||
atom.replace_field in bpy.data.images.keys():
|
||||
bpy.data.images[atom.images_field].user_remap(
|
||||
bpy.data.images[atom.replace_field])
|
||||
image = bpy.data.images[atom.images_field]
|
||||
error = _check_library_or_override(image)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
image.user_remap(bpy.data.images[atom.replace_field])
|
||||
atom.images_field = atom.replace_field
|
||||
|
||||
if inspection == 'LIGHTS' and \
|
||||
atom.replace_field in bpy.data.lights.keys():
|
||||
bpy.data.lights[atom.lights_field].user_remap(
|
||||
bpy.data.lights[atom.replace_field])
|
||||
light = bpy.data.lights[atom.lights_field]
|
||||
error = _check_library_or_override(light)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
light.user_remap(bpy.data.lights[atom.replace_field])
|
||||
atom.lights_field = atom.replace_field
|
||||
|
||||
if inspection == 'MATERIALS' and \
|
||||
atom.replace_field in bpy.data.materials.keys():
|
||||
bpy.data.materials[atom.materials_field].user_remap(
|
||||
bpy.data.materials[atom.replace_field])
|
||||
material = bpy.data.materials[atom.materials_field]
|
||||
error = _check_library_or_override(material)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
material.user_remap(bpy.data.materials[atom.replace_field])
|
||||
atom.materials_field = atom.replace_field
|
||||
|
||||
if inspection == 'NODE_GROUPS' and \
|
||||
atom.replace_field in bpy.data.node_groups.keys():
|
||||
bpy.data.node_groups[atom.node_groups_field].user_remap(
|
||||
bpy.data.node_groups[atom.replace_field])
|
||||
node_group = bpy.data.node_groups[atom.node_groups_field]
|
||||
error = _check_library_or_override(node_group)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
node_group.user_remap(bpy.data.node_groups[atom.replace_field])
|
||||
atom.node_groups_field = atom.replace_field
|
||||
|
||||
if inspection == 'PARTICLES' and \
|
||||
atom.replace_field in bpy.data.particles.keys():
|
||||
bpy.data.particles[atom.particles_field].user_remap(
|
||||
bpy.data.particles[atom.replace_field])
|
||||
particle = bpy.data.particles[atom.particles_field]
|
||||
error = _check_library_or_override(particle)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
particle.user_remap(bpy.data.particles[atom.replace_field])
|
||||
atom.particles_field = atom.replace_field
|
||||
|
||||
if inspection == 'TEXTURES' and \
|
||||
atom.replace_field in bpy.data.textures.keys():
|
||||
bpy.data.textures[atom.textures_field].user_remap(
|
||||
bpy.data.textures[atom.replace_field])
|
||||
texture = bpy.data.textures[atom.textures_field]
|
||||
error = _check_library_or_override(texture)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
texture.user_remap(bpy.data.textures[atom.replace_field])
|
||||
atom.textures_field = atom.replace_field
|
||||
|
||||
if inspection == 'WORLDS' and \
|
||||
atom.replace_field in bpy.data.worlds.keys():
|
||||
bpy.data.worlds[atom.worlds_field].user_remap(
|
||||
bpy.data.worlds[atom.replace_field])
|
||||
world = bpy.data.worlds[atom.worlds_field]
|
||||
error = _check_library_or_override(world)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
world.user_remap(bpy.data.worlds[atom.replace_field])
|
||||
atom.worlds_field = atom.replace_field
|
||||
|
||||
atom.replace_field = ""
|
||||
@@ -232,38 +307,59 @@ class ATOMIC_OT_inspection_toggle_fake_user(bpy.types.Operator):
|
||||
|
||||
if inspection == 'IMAGES':
|
||||
image = bpy.data.images[atom.images_field]
|
||||
bpy.data.images[atom.images_field].use_fake_user = \
|
||||
not image.use_fake_user
|
||||
error = _check_library_or_override(image)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
image.use_fake_user = not image.use_fake_user
|
||||
|
||||
if inspection == 'LIGHTS':
|
||||
light = bpy.data.lights[atom.lights_field]
|
||||
bpy.data.lights[atom.lights_field].use_fake_user = \
|
||||
not light.use_fake_user
|
||||
error = _check_library_or_override(light)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
light.use_fake_user = not light.use_fake_user
|
||||
|
||||
if inspection == 'MATERIALS':
|
||||
material = bpy.data.materials[atom.materials_field]
|
||||
bpy.data.materials[atom.materials_field].use_fake_user = \
|
||||
not material.use_fake_user
|
||||
error = _check_library_or_override(material)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
material.use_fake_user = not material.use_fake_user
|
||||
|
||||
if inspection == 'NODE_GROUPS':
|
||||
node_group = bpy.data.node_groups[atom.node_groups_field]
|
||||
bpy.data.node_groups[atom.node_groups_field].use_fake_user = \
|
||||
not node_group.use_fake_user
|
||||
error = _check_library_or_override(node_group)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
node_group.use_fake_user = not node_group.use_fake_user
|
||||
|
||||
if inspection == 'PARTICLES':
|
||||
particle = bpy.data.particles[atom.particles_field]
|
||||
bpy.data.particles[atom.particles_field].use_fake_user = \
|
||||
not particle.use_fake_user
|
||||
error = _check_library_or_override(particle)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
particle.use_fake_user = not particle.use_fake_user
|
||||
|
||||
if inspection == 'TEXTURES':
|
||||
texture = bpy.data.textures[atom.textures_field]
|
||||
bpy.data.textures[atom.textures_field].use_fake_user = \
|
||||
not texture.use_fake_user
|
||||
error = _check_library_or_override(texture)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
texture.use_fake_user = not texture.use_fake_user
|
||||
|
||||
if inspection == 'WORLDS':
|
||||
world = bpy.data.worlds[atom.worlds_field]
|
||||
bpy.data.worlds[atom.worlds_field].use_fake_user = \
|
||||
not world.use_fake_user
|
||||
error = _check_library_or_override(world)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
world.use_fake_user = not world.use_fake_user
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -283,6 +379,11 @@ class ATOMIC_OT_inspection_duplicate(bpy.types.Operator):
|
||||
collections = bpy.data.collections
|
||||
|
||||
if key in collections.keys():
|
||||
collection = collections[key]
|
||||
error = _check_library_or_override(collection)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
copy_key = duplicate.collection(key)
|
||||
atom.collections_field = copy_key
|
||||
|
||||
@@ -291,6 +392,11 @@ class ATOMIC_OT_inspection_duplicate(bpy.types.Operator):
|
||||
images = bpy.data.images
|
||||
|
||||
if key in images.keys():
|
||||
image = images[key]
|
||||
error = _check_library_or_override(image)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
copy_key = duplicate.image(key)
|
||||
atom.images_field = copy_key
|
||||
|
||||
@@ -299,6 +405,11 @@ class ATOMIC_OT_inspection_duplicate(bpy.types.Operator):
|
||||
lights = bpy.data.lights
|
||||
|
||||
if key in lights.keys():
|
||||
light = lights[key]
|
||||
error = _check_library_or_override(light)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
copy_key = duplicate.light(key)
|
||||
atom.lights_field = copy_key
|
||||
|
||||
@@ -307,6 +418,11 @@ class ATOMIC_OT_inspection_duplicate(bpy.types.Operator):
|
||||
materials = bpy.data.materials
|
||||
|
||||
if key in materials.keys():
|
||||
material = materials[key]
|
||||
error = _check_library_or_override(material)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
copy_key = duplicate.material(key)
|
||||
atom.materials_field = copy_key
|
||||
|
||||
@@ -315,6 +431,11 @@ class ATOMIC_OT_inspection_duplicate(bpy.types.Operator):
|
||||
node_groups = bpy.data.node_groups
|
||||
|
||||
if key in node_groups.keys():
|
||||
node_group = node_groups[key]
|
||||
error = _check_library_or_override(node_group)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
copy_key = duplicate.node_group(key)
|
||||
atom.node_groups_field = copy_key
|
||||
|
||||
@@ -323,6 +444,11 @@ class ATOMIC_OT_inspection_duplicate(bpy.types.Operator):
|
||||
particles = bpy.data.particles
|
||||
|
||||
if key in particles.keys():
|
||||
particle = particles[key]
|
||||
error = _check_library_or_override(particle)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
copy_key = duplicate.particle(key)
|
||||
atom.particles_field = copy_key
|
||||
|
||||
@@ -331,6 +457,11 @@ class ATOMIC_OT_inspection_duplicate(bpy.types.Operator):
|
||||
textures = bpy.data.textures
|
||||
|
||||
if key in textures.keys():
|
||||
texture = textures[key]
|
||||
error = _check_library_or_override(texture)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
copy_key = duplicate.texture(key)
|
||||
atom.textures_field = copy_key
|
||||
|
||||
@@ -339,6 +470,11 @@ class ATOMIC_OT_inspection_duplicate(bpy.types.Operator):
|
||||
worlds = bpy.data.worlds
|
||||
|
||||
if key in worlds.keys():
|
||||
world = worlds[key]
|
||||
error = _check_library_or_override(world)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
copy_key = duplicate.world(key)
|
||||
atom.worlds_field = copy_key
|
||||
|
||||
@@ -360,6 +496,11 @@ class ATOMIC_OT_inspection_delete(bpy.types.Operator):
|
||||
collections = bpy.data.collections
|
||||
|
||||
if key in collections.keys():
|
||||
collection = collections[key]
|
||||
error = _check_library_or_override(collection)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
delete.collection(key)
|
||||
atom.collections_field = ""
|
||||
|
||||
@@ -368,6 +509,11 @@ class ATOMIC_OT_inspection_delete(bpy.types.Operator):
|
||||
images = bpy.data.images
|
||||
|
||||
if key in images.keys():
|
||||
image = images[key]
|
||||
error = _check_library_or_override(image)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
delete.image(key)
|
||||
atom.images_field = ""
|
||||
|
||||
@@ -376,6 +522,11 @@ class ATOMIC_OT_inspection_delete(bpy.types.Operator):
|
||||
lights = bpy.data.lights
|
||||
|
||||
if key in lights.keys():
|
||||
light = lights[key]
|
||||
error = _check_library_or_override(light)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
delete.light(key)
|
||||
atom.lights_field = ""
|
||||
|
||||
@@ -384,6 +535,11 @@ class ATOMIC_OT_inspection_delete(bpy.types.Operator):
|
||||
materials = bpy.data.materials
|
||||
|
||||
if key in materials.keys():
|
||||
material = materials[key]
|
||||
error = _check_library_or_override(material)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
delete.material(key)
|
||||
atom.materials_field = ""
|
||||
|
||||
@@ -392,6 +548,11 @@ class ATOMIC_OT_inspection_delete(bpy.types.Operator):
|
||||
node_groups = bpy.data.node_groups
|
||||
|
||||
if key in node_groups.keys():
|
||||
node_group = node_groups[key]
|
||||
error = _check_library_or_override(node_group)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
delete.node_group(key)
|
||||
atom.node_groups_field = ""
|
||||
|
||||
@@ -399,6 +560,11 @@ class ATOMIC_OT_inspection_delete(bpy.types.Operator):
|
||||
key = atom.particles_field
|
||||
particles = bpy.data.particles
|
||||
if key in particles.keys():
|
||||
particle = particles[key]
|
||||
error = _check_library_or_override(particle)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
delete.particle(key)
|
||||
atom.particles_field = ""
|
||||
|
||||
@@ -407,6 +573,11 @@ class ATOMIC_OT_inspection_delete(bpy.types.Operator):
|
||||
textures = bpy.data.textures
|
||||
|
||||
if key in textures.keys():
|
||||
texture = textures[key]
|
||||
error = _check_library_or_override(texture)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
delete.texture(key)
|
||||
atom.textures_field = ""
|
||||
|
||||
@@ -415,6 +586,11 @@ class ATOMIC_OT_inspection_delete(bpy.types.Operator):
|
||||
worlds = bpy.data.worlds
|
||||
|
||||
if key in worlds.keys():
|
||||
world = worlds[key]
|
||||
error = _check_library_or_override(world)
|
||||
if error:
|
||||
self.report({'ERROR'}, error)
|
||||
return {'CANCELLED'}
|
||||
delete.world(key)
|
||||
atom.worlds_field = ""
|
||||
|
||||
@@ -437,4 +613,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for item in reg_list:
|
||||
unregister_class(item)
|
||||
compat.safe_unregister_class(item)
|
||||
|
||||
@@ -26,8 +26,9 @@ various selection operations.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from ..stats import unused
|
||||
from ..stats import unused_parallel
|
||||
from .utils import clean
|
||||
from .utils import nuke
|
||||
from ..ui.utils import ui_layouts
|
||||
@@ -57,7 +58,9 @@ class ATOMIC_OT_nuke(bpy.types.Operator):
|
||||
|
||||
# display when the main panel collections property is toggled
|
||||
if atom.collections:
|
||||
collections = sorted(bpy.data.collections.keys())
|
||||
from ..utils import compat
|
||||
collections = sorted([c.name for c in bpy.data.collections
|
||||
if not compat.is_library_or_override(c)])
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Collections",
|
||||
@@ -67,7 +70,9 @@ class ATOMIC_OT_nuke(bpy.types.Operator):
|
||||
|
||||
# display when the main panel images property is toggled
|
||||
if atom.images:
|
||||
images = sorted(bpy.data.images.keys())
|
||||
from ..utils import compat
|
||||
images = sorted([i.name for i in bpy.data.images
|
||||
if not compat.is_library_or_override(i)])
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Images",
|
||||
@@ -77,7 +82,9 @@ class ATOMIC_OT_nuke(bpy.types.Operator):
|
||||
|
||||
# display when the main panel lights property is toggled
|
||||
if atom.lights:
|
||||
lights = sorted(bpy.data.lights.keys())
|
||||
from ..utils import compat
|
||||
lights = sorted([l.name for l in bpy.data.lights
|
||||
if not compat.is_library_or_override(l)])
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Lights",
|
||||
@@ -87,7 +94,9 @@ class ATOMIC_OT_nuke(bpy.types.Operator):
|
||||
|
||||
# display when the main panel materials property is toggled
|
||||
if atom.materials:
|
||||
materials = sorted(bpy.data.materials.keys())
|
||||
from ..utils import compat
|
||||
materials = sorted([m.name for m in bpy.data.materials
|
||||
if not compat.is_library_or_override(m)])
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Materials",
|
||||
@@ -97,7 +106,9 @@ class ATOMIC_OT_nuke(bpy.types.Operator):
|
||||
|
||||
# display when the main panel node groups property is toggled
|
||||
if atom.node_groups:
|
||||
node_groups = sorted(bpy.data.node_groups.keys())
|
||||
from ..utils import compat
|
||||
node_groups = sorted([ng.name for ng in bpy.data.node_groups
|
||||
if not compat.is_library_or_override(ng)])
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Node Groups",
|
||||
@@ -107,7 +118,9 @@ class ATOMIC_OT_nuke(bpy.types.Operator):
|
||||
|
||||
# display when the main panel particle systems property is toggled
|
||||
if atom.particles:
|
||||
particles = sorted(bpy.data.particles.keys())
|
||||
from ..utils import compat
|
||||
particles = sorted([p.name for p in bpy.data.particles
|
||||
if not compat.is_library_or_override(p)])
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Particle Systems",
|
||||
@@ -117,7 +130,9 @@ class ATOMIC_OT_nuke(bpy.types.Operator):
|
||||
|
||||
# display when the main panel textures property is toggled
|
||||
if atom.textures:
|
||||
textures = sorted(bpy.data.textures.keys())
|
||||
from ..utils import compat
|
||||
textures = sorted([t.name for t in bpy.data.textures
|
||||
if not compat.is_library_or_override(t)])
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Textures",
|
||||
@@ -127,7 +142,9 @@ class ATOMIC_OT_nuke(bpy.types.Operator):
|
||||
|
||||
# display when the main panel worlds property is toggled
|
||||
if atom.worlds:
|
||||
worlds = sorted(bpy.data.worlds.keys())
|
||||
from ..utils import compat
|
||||
worlds = sorted([w.name for w in bpy.data.worlds
|
||||
if not compat.is_library_or_override(w)])
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Worlds",
|
||||
@@ -184,8 +201,10 @@ class ATOMIC_OT_clean(bpy.types.Operator):
|
||||
unused_lights = []
|
||||
unused_materials = []
|
||||
unused_node_groups = []
|
||||
unused_objects = []
|
||||
unused_particles = []
|
||||
unused_textures = []
|
||||
unused_armatures = []
|
||||
unused_worlds = []
|
||||
|
||||
def draw(self, context):
|
||||
@@ -197,8 +216,9 @@ class ATOMIC_OT_clean(bpy.types.Operator):
|
||||
|
||||
# display if no main panel properties are toggled
|
||||
if not (atom.collections or atom.images or atom.lights or
|
||||
atom.materials or atom.node_groups or atom.particles
|
||||
or atom.textures or atom.worlds):
|
||||
atom.materials or atom.node_groups or atom.objects or
|
||||
atom.particles or atom.textures or atom.armatures or
|
||||
atom.worlds):
|
||||
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
@@ -249,6 +269,15 @@ class ATOMIC_OT_clean(bpy.types.Operator):
|
||||
icon="NODETREE"
|
||||
)
|
||||
|
||||
# display when the main panel objects property is toggled
|
||||
if atom.objects:
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Objects",
|
||||
items=self.unused_objects,
|
||||
icon="OBJECT_DATA"
|
||||
)
|
||||
|
||||
# display when the main panel particle systems property is toggled
|
||||
if atom.particles:
|
||||
ui_layouts.box_list(
|
||||
@@ -260,14 +289,22 @@ class ATOMIC_OT_clean(bpy.types.Operator):
|
||||
|
||||
# display when the main panel textures property is toggled
|
||||
if atom.textures:
|
||||
textures = sorted(unused.textures_deep())
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Textures",
|
||||
items=textures,
|
||||
items=self.unused_textures,
|
||||
icon="TEXTURE"
|
||||
)
|
||||
|
||||
# display when the main panel armatures property is toggled
|
||||
if atom.armatures:
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Armatures",
|
||||
items=self.unused_armatures,
|
||||
icon="ARMATURE_DATA"
|
||||
)
|
||||
|
||||
# display when the main panel worlds property is toggled
|
||||
if atom.worlds:
|
||||
ui_layouts.box_list(
|
||||
@@ -297,12 +334,18 @@ class ATOMIC_OT_clean(bpy.types.Operator):
|
||||
if atom.node_groups:
|
||||
clean.node_groups()
|
||||
|
||||
if atom.objects:
|
||||
clean.objects()
|
||||
|
||||
if atom.particles:
|
||||
clean.particles()
|
||||
|
||||
if atom.textures:
|
||||
clean.textures()
|
||||
|
||||
if atom.armatures:
|
||||
clean.armatures()
|
||||
|
||||
if atom.worlds:
|
||||
clean.worlds()
|
||||
|
||||
@@ -314,29 +357,38 @@ class ATOMIC_OT_clean(bpy.types.Operator):
|
||||
wm = context.window_manager
|
||||
atom = bpy.context.scene.atomic
|
||||
|
||||
# Use parallel execution for better performance
|
||||
all_unused = unused_parallel.get_all_unused_parallel()
|
||||
|
||||
if atom.collections:
|
||||
self.unused_collections = unused.collections_deep()
|
||||
self.unused_collections = all_unused['collections']
|
||||
|
||||
if atom.images:
|
||||
self.unused_images = unused.images_deep()
|
||||
self.unused_images = all_unused['images']
|
||||
|
||||
if atom.lights:
|
||||
self.unused_lights = unused.lights_deep()
|
||||
self.unused_lights = all_unused['lights']
|
||||
|
||||
if atom.materials:
|
||||
self.unused_materials = unused.materials_deep()
|
||||
self.unused_materials = all_unused['materials']
|
||||
|
||||
if atom.node_groups:
|
||||
self.unused_node_groups = unused.node_groups_deep()
|
||||
self.unused_node_groups = all_unused['node_groups']
|
||||
|
||||
if atom.objects:
|
||||
self.unused_objects = all_unused['objects']
|
||||
|
||||
if atom.particles:
|
||||
self.unused_particles = unused.particles_deep()
|
||||
self.unused_particles = all_unused['particles']
|
||||
|
||||
if atom.textures:
|
||||
self.unused_textures = unused.textures_deep()
|
||||
self.unused_textures = all_unused['textures']
|
||||
|
||||
if atom.armatures:
|
||||
self.unused_armatures = all_unused['armatures']
|
||||
|
||||
if atom.worlds:
|
||||
self.unused_worlds = unused.worlds()
|
||||
self.unused_worlds = all_unused['worlds']
|
||||
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
@@ -359,30 +411,20 @@ class ATOMIC_OT_smart_select(bpy.types.Operator):
|
||||
bl_label = "Smart Select"
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
bpy.context.scene.atomic.collections = \
|
||||
any(unused.collections_deep())
|
||||
|
||||
bpy.context.scene.atomic.images = \
|
||||
any(unused.images_deep())
|
||||
|
||||
bpy.context.scene.atomic.lights = \
|
||||
any(unused.lights_deep())
|
||||
|
||||
bpy.context.scene.atomic.materials = \
|
||||
any(unused.materials_deep())
|
||||
|
||||
bpy.context.scene.atomic.node_groups = \
|
||||
any(unused.node_groups_deep())
|
||||
|
||||
bpy.context.scene.atomic.particles = \
|
||||
any(unused.particles_deep())
|
||||
|
||||
bpy.context.scene.atomic.textures = \
|
||||
any(unused.textures_deep())
|
||||
|
||||
bpy.context.scene.atomic.worlds = \
|
||||
any(unused.worlds())
|
||||
# Use parallel execution for better performance
|
||||
unused_flags = unused_parallel.get_unused_for_smart_select()
|
||||
|
||||
atom = bpy.context.scene.atomic
|
||||
atom.collections = unused_flags['collections']
|
||||
atom.images = unused_flags['images']
|
||||
atom.lights = unused_flags['lights']
|
||||
atom.materials = unused_flags['materials']
|
||||
atom.node_groups = unused_flags['node_groups']
|
||||
atom.objects = unused_flags['objects']
|
||||
atom.particles = unused_flags['particles']
|
||||
atom.textures = unused_flags['textures']
|
||||
atom.armatures = unused_flags['armatures']
|
||||
atom.worlds = unused_flags['worlds']
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -441,4 +483,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for item in reg_list:
|
||||
unregister_class(item)
|
||||
compat.safe_unregister_class(item)
|
||||
|
||||
@@ -31,7 +31,7 @@ attempting to reload missing project files.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from ..stats import missing
|
||||
from ..ui.utils import ui_layouts
|
||||
|
||||
@@ -192,4 +192,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for item in reg_list:
|
||||
unregister_class(item)
|
||||
compat.safe_unregister_class(item)
|
||||
|
||||
@@ -28,7 +28,7 @@ support page in the web browser.
|
||||
import bpy
|
||||
import webbrowser
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
|
||||
|
||||
# Atomic Data Manager Open Support Me Operator
|
||||
@@ -52,4 +52,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reg_list:
|
||||
unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
@@ -72,3 +72,15 @@ def worlds():
|
||||
# removes all unused worlds from the project
|
||||
for world_key in unused.worlds():
|
||||
bpy.data.worlds.remove(bpy.data.worlds[world_key])
|
||||
|
||||
|
||||
def objects():
|
||||
# removes all unused objects from the project
|
||||
for object_key in unused.objects_deep():
|
||||
bpy.data.objects.remove(bpy.data.objects[object_key])
|
||||
|
||||
|
||||
def armatures():
|
||||
# removes all unused armatures from the project
|
||||
for armature_key in unused.armatures_deep():
|
||||
bpy.data.armatures.remove(bpy.data.armatures[armature_key])
|
||||
|
||||
@@ -24,11 +24,18 @@ data categories.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from ...utils import compat
|
||||
|
||||
|
||||
def nuke_data(data):
|
||||
# removes all data-blocks from the indicated set of data
|
||||
# Skip library-linked and override datablocks
|
||||
keys_to_remove = []
|
||||
for key in data.keys():
|
||||
datablock = data[key]
|
||||
if not compat.is_library_or_override(datablock):
|
||||
keys_to_remove.append(key)
|
||||
for key in keys_to_remove:
|
||||
data.remove(data[key])
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ This file contains functions that count quantities of various sets of data.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from ..utils import compat
|
||||
from . import unused
|
||||
from . import unnamed
|
||||
from . import missing
|
||||
@@ -30,8 +31,11 @@ from . import missing
|
||||
|
||||
def collections():
|
||||
# returns the number of collections in the project
|
||||
|
||||
return len(bpy.data.collections)
|
||||
count = 0
|
||||
for collection in bpy.data.collections:
|
||||
if not compat.is_library_or_override(collection):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def collections_unused():
|
||||
@@ -48,8 +52,11 @@ def collections_unnamed():
|
||||
|
||||
def images():
|
||||
# returns the number of images in the project
|
||||
|
||||
return len(bpy.data.images)
|
||||
count = 0
|
||||
for image in bpy.data.images:
|
||||
if not compat.is_library_or_override(image):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def images_unused():
|
||||
@@ -72,8 +79,11 @@ def images_missing():
|
||||
|
||||
def lights():
|
||||
# returns the number of lights in the project
|
||||
|
||||
return len(bpy.data.lights)
|
||||
count = 0
|
||||
for light in bpy.data.lights:
|
||||
if not compat.is_library_or_override(light):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def lights_unused():
|
||||
@@ -90,8 +100,11 @@ def lights_unnamed():
|
||||
|
||||
def materials():
|
||||
# returns the number of materials in the project
|
||||
|
||||
return len(bpy.data.materials)
|
||||
count = 0
|
||||
for material in bpy.data.materials:
|
||||
if not compat.is_library_or_override(material):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def materials_unused():
|
||||
@@ -108,8 +121,11 @@ def materials_unnamed():
|
||||
|
||||
def node_groups():
|
||||
# returns the number of node groups in the project
|
||||
|
||||
return len(bpy.data.node_groups)
|
||||
count = 0
|
||||
for node_group in bpy.data.node_groups:
|
||||
if not compat.is_library_or_override(node_group):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def node_groups_unused():
|
||||
@@ -126,8 +142,11 @@ def node_groups_unnamed():
|
||||
|
||||
def objects():
|
||||
# returns the number of objects in the project
|
||||
|
||||
return len(bpy.data.objects)
|
||||
count = 0
|
||||
for obj in bpy.data.objects:
|
||||
if not compat.is_library_or_override(obj):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def objects_unnamed():
|
||||
@@ -138,8 +157,11 @@ def objects_unnamed():
|
||||
|
||||
def particles():
|
||||
# returns the number of particles in the project
|
||||
|
||||
return len(bpy.data.particles)
|
||||
count = 0
|
||||
for particle in bpy.data.particles:
|
||||
if not compat.is_library_or_override(particle):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def particles_unused():
|
||||
@@ -156,8 +178,11 @@ def particles_unnamed():
|
||||
|
||||
def textures():
|
||||
# returns the number of textures in the project
|
||||
|
||||
return len(bpy.data.textures)
|
||||
count = 0
|
||||
for texture in bpy.data.textures:
|
||||
if not compat.is_library_or_override(texture):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def textures_unused():
|
||||
@@ -174,8 +199,11 @@ def textures_unnamed():
|
||||
|
||||
def worlds():
|
||||
# returns the number of worlds in the project
|
||||
|
||||
return len(bpy.data.worlds)
|
||||
count = 0
|
||||
for world in bpy.data.worlds:
|
||||
if not compat.is_library_or_override(world):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def worlds_unused():
|
||||
|
||||
@@ -25,6 +25,7 @@ project.
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from ..utils import version, compat
|
||||
|
||||
|
||||
def get_missing(data):
|
||||
@@ -37,12 +38,29 @@ def get_missing(data):
|
||||
do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"]
|
||||
|
||||
for datablock in data:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(datablock):
|
||||
continue
|
||||
|
||||
# the absolute path to our data-block
|
||||
abspath = bpy.path.abspath(datablock.filepath)
|
||||
|
||||
# Check if data-block is packed
|
||||
# Blender 5.0+: Image objects use 'packed_files' (plural), Library objects use 'packed_file' (singular)
|
||||
# Blender 4.2/4.5: Both Image and Library objects use 'packed_file' (singular)
|
||||
is_packed = False
|
||||
if version.is_version_at_least(5, 0, 0):
|
||||
# Blender 5.0+: Check type-specific attributes
|
||||
if isinstance(datablock, bpy.types.Image):
|
||||
is_packed = bool(datablock.packed_files) if hasattr(datablock, 'packed_files') else False
|
||||
elif isinstance(datablock, bpy.types.Library):
|
||||
is_packed = bool(datablock.packed_file) if hasattr(datablock, 'packed_file') else False
|
||||
else:
|
||||
# Blender 4.2/4.5: Both use 'packed_file' (singular)
|
||||
is_packed = bool(datablock.packed_file) if hasattr(datablock, 'packed_file') else False
|
||||
|
||||
# if data-block is not packed and has an invalid filepath
|
||||
if not datablock.packed_files and not os.path.isfile(abspath):
|
||||
if not is_packed and not os.path.isfile(abspath):
|
||||
|
||||
# if data-block is not in our do not flag list
|
||||
# append it to the missing data list
|
||||
@@ -50,7 +68,7 @@ def get_missing(data):
|
||||
missing.append(datablock.name)
|
||||
|
||||
# if data-block is packed but it does not have a filepath
|
||||
elif datablock.packed_files and not abspath:
|
||||
elif is_packed and not abspath:
|
||||
|
||||
# if data-block is not in our do not flag list
|
||||
# append it to the missing data list
|
||||
|
||||
@@ -25,6 +25,7 @@ Blender project.
|
||||
|
||||
import bpy
|
||||
import re
|
||||
from ..utils import compat
|
||||
|
||||
|
||||
def collections():
|
||||
@@ -32,6 +33,9 @@ def collections():
|
||||
unnamed = []
|
||||
|
||||
for collection in bpy.data.collections:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(collection):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', collection.name) or \
|
||||
collection.name.startswith("Collection"):
|
||||
unnamed.append(collection.name)
|
||||
@@ -44,6 +48,9 @@ def images():
|
||||
unnamed = []
|
||||
|
||||
for image in bpy.data.images:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(image):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', image.name) or \
|
||||
image.name.startswith("Untitled"):
|
||||
unnamed.append(image.name)
|
||||
@@ -56,6 +63,9 @@ def lights():
|
||||
unnamed = []
|
||||
|
||||
for light in bpy.data.lights:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(light):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', light.name) or \
|
||||
light.name.startswith("Light"):
|
||||
unnamed.append(light.name)
|
||||
@@ -67,7 +77,10 @@ def materials():
|
||||
# returns the keys of all unnamed materials in the project
|
||||
unnamed = []
|
||||
|
||||
for material in bpy.data.lights:
|
||||
for material in bpy.data.materials:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(material):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', material.name) or \
|
||||
material.name.startswith("Material"):
|
||||
unnamed.append(material.name)
|
||||
@@ -152,6 +165,9 @@ def objects():
|
||||
unnamed = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(obj):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', obj.name) or \
|
||||
obj.name.startswith(default_obj_names):
|
||||
unnamed.append(obj.name)
|
||||
@@ -164,6 +180,9 @@ def node_groups():
|
||||
unnamed = []
|
||||
|
||||
for node_group in bpy.data.node_groups:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(node_group):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', node_group.name) or \
|
||||
node_group.name.startswith("NodeGroup"):
|
||||
unnamed.append(node_group.name)
|
||||
@@ -176,6 +195,9 @@ def particles():
|
||||
unnamed = []
|
||||
|
||||
for particle in bpy.data.particles:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(particle):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', particle.name) or \
|
||||
particle.name.startswith("ParticleSettings"):
|
||||
unnamed.append(particle.name)
|
||||
@@ -188,6 +210,9 @@ def textures():
|
||||
unnamed = []
|
||||
|
||||
for texture in bpy.data.textures:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(texture):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', texture.name) or \
|
||||
texture.name.startswith("Texture"):
|
||||
unnamed.append(texture.name)
|
||||
@@ -200,6 +225,9 @@ def worlds():
|
||||
unnamed = []
|
||||
|
||||
for world in bpy.data.worlds:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(world):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', world.name) or \
|
||||
world.name.startswith("World"):
|
||||
unnamed.append(world.name)
|
||||
|
||||
@@ -25,6 +25,7 @@ as determined by stats.users.py
|
||||
|
||||
import bpy
|
||||
from .. import config
|
||||
from ..utils import compat
|
||||
from . import users
|
||||
|
||||
|
||||
@@ -35,6 +36,9 @@ def shallow(data):
|
||||
unused = []
|
||||
|
||||
for datablock in data:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(datablock):
|
||||
continue
|
||||
|
||||
# if data-block has no users or if it has a fake user and
|
||||
# ignore fake users is enabled
|
||||
@@ -52,6 +56,9 @@ def collections_deep():
|
||||
unused = []
|
||||
|
||||
for collection in bpy.data.collections:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(collection):
|
||||
continue
|
||||
if not users.collection_all(collection.name):
|
||||
unused.append(collection.name)
|
||||
|
||||
@@ -65,6 +72,9 @@ def collections_shallow():
|
||||
unused = []
|
||||
|
||||
for collection in bpy.data.collections:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(collection):
|
||||
continue
|
||||
if not (collection.objects or collection.children):
|
||||
unused.append(collection.name)
|
||||
|
||||
@@ -81,6 +91,9 @@ def images_deep():
|
||||
do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"]
|
||||
|
||||
for image in bpy.data.images:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(image):
|
||||
continue
|
||||
if not users.image_all(image.name):
|
||||
|
||||
# check if image has a fake user or if ignore fake users
|
||||
@@ -118,6 +131,9 @@ def lights_deep():
|
||||
unused = []
|
||||
|
||||
for light in bpy.data.lights:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(light):
|
||||
continue
|
||||
if not users.light_all(light.name):
|
||||
|
||||
# check if light has a fake user or if ignore fake users
|
||||
@@ -141,6 +157,9 @@ def materials_deep():
|
||||
unused = []
|
||||
|
||||
for material in bpy.data.materials:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(material):
|
||||
continue
|
||||
if not users.material_all(material.name):
|
||||
|
||||
# check if material has a fake user or if ignore fake users
|
||||
@@ -164,6 +183,9 @@ def node_groups_deep():
|
||||
unused = []
|
||||
|
||||
for node_group in bpy.data.node_groups:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(node_group):
|
||||
continue
|
||||
if not users.node_group_all(node_group.name):
|
||||
|
||||
# check if node group has a fake user or if ignore fake users
|
||||
@@ -190,6 +212,9 @@ def particles_deep():
|
||||
unused = []
|
||||
|
||||
for particle in bpy.data.particles:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(particle):
|
||||
continue
|
||||
if not users.particle_all(particle.name):
|
||||
|
||||
# check if particle system has a fake user or if ignore fake
|
||||
@@ -216,6 +241,9 @@ def textures_deep():
|
||||
unused = []
|
||||
|
||||
for texture in bpy.data.textures:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(texture):
|
||||
continue
|
||||
if not users.texture_all(texture.name):
|
||||
|
||||
# check if texture has a fake user or if ignore fake users
|
||||
@@ -239,6 +267,9 @@ def worlds():
|
||||
unused = []
|
||||
|
||||
for world in bpy.data.worlds:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(world):
|
||||
continue
|
||||
|
||||
# if data-block has no users or if it has a fake user and
|
||||
# ignore fake users is enabled
|
||||
@@ -248,3 +279,55 @@ def worlds():
|
||||
unused.append(world.name)
|
||||
|
||||
return unused
|
||||
|
||||
|
||||
def objects_deep():
|
||||
# returns a list of keys of unused objects
|
||||
|
||||
unused = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(obj):
|
||||
continue
|
||||
if not users.object_all(obj.name):
|
||||
|
||||
# check if object has a fake user or if ignore fake users
|
||||
# is enabled
|
||||
if not obj.use_fake_user or config.include_fake_users:
|
||||
unused.append(obj.name)
|
||||
|
||||
return unused
|
||||
|
||||
|
||||
def objects_shallow():
|
||||
# returns a list of keys of unused objects that may be
|
||||
# incomplete, but is significantly faster than doing a deep search
|
||||
|
||||
return shallow(bpy.data.objects)
|
||||
|
||||
|
||||
def armatures_deep():
|
||||
# returns a list of keys of unused armatures
|
||||
|
||||
unused = []
|
||||
|
||||
for armature in bpy.data.armatures:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(armature):
|
||||
continue
|
||||
if not users.armature_all(armature.name):
|
||||
|
||||
# check if armature has a fake user or if ignore fake users
|
||||
# is enabled
|
||||
if not armature.use_fake_user or config.include_fake_users:
|
||||
unused.append(armature.name)
|
||||
|
||||
return unused
|
||||
|
||||
|
||||
def armatures_shallow():
|
||||
# returns a list of keys of unused armatures that may be
|
||||
# incomplete, but is significantly faster than doing a deep search
|
||||
|
||||
return shallow(bpy.data.armatures)
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import bpy
|
||||
from ..stats import unused
|
||||
from ..stats import users
|
||||
from .. import config
|
||||
from ..utils import compat
|
||||
|
||||
|
||||
def get_all_unused_parallel():
|
||||
"""
|
||||
Get all unused data-blocks efficiently in a single batch.
|
||||
|
||||
Returns a dictionary with keys:
|
||||
- collections: list of unused collection names
|
||||
- images: list of unused image names
|
||||
- lights: list of unused light names
|
||||
- materials: list of unused material names
|
||||
- node_groups: list of unused node group names
|
||||
- objects: list of unused object names
|
||||
- particles: list of unused particle names
|
||||
- textures: list of unused texture names
|
||||
- armatures: list of unused armature names
|
||||
- worlds: list of unused world names
|
||||
"""
|
||||
# Execute all checks sequentially but in a clean batch
|
||||
# This avoids threading overhead while keeping code organized
|
||||
return {
|
||||
'collections': unused.collections_deep(),
|
||||
'images': unused.images_deep(),
|
||||
'lights': unused.lights_deep(),
|
||||
'materials': unused.materials_deep(),
|
||||
'node_groups': unused.node_groups_deep(),
|
||||
'objects': unused.objects_deep(),
|
||||
'particles': unused.particles_deep(),
|
||||
'textures': unused.textures_deep(),
|
||||
'armatures': unused.armatures_deep(),
|
||||
'worlds': unused.worlds(),
|
||||
}
|
||||
|
||||
|
||||
def _has_any_unused_collections():
|
||||
"""Check if there are any unused collections (short-circuits early)."""
|
||||
for collection in bpy.data.collections:
|
||||
if compat.is_library_or_override(collection):
|
||||
continue
|
||||
if not users.collection_all(collection.name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_images():
|
||||
"""Check if there are any unused images (short-circuits early)."""
|
||||
do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"]
|
||||
|
||||
for image in bpy.data.images:
|
||||
if compat.is_library_or_override(image):
|
||||
continue
|
||||
if not users.image_all(image.name):
|
||||
if not image.use_fake_user or config.include_fake_users:
|
||||
if image.name not in do_not_flag:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_lights():
|
||||
"""Check if there are any unused lights (short-circuits early)."""
|
||||
for light in bpy.data.lights:
|
||||
if compat.is_library_or_override(light):
|
||||
continue
|
||||
if not users.light_all(light.name):
|
||||
if not light.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_materials():
|
||||
"""Check if there are any unused materials (short-circuits early)."""
|
||||
for material in bpy.data.materials:
|
||||
if compat.is_library_or_override(material):
|
||||
continue
|
||||
if not users.material_all(material.name):
|
||||
if not material.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_node_groups():
|
||||
"""Check if there are any unused node groups (short-circuits early)."""
|
||||
for node_group in bpy.data.node_groups:
|
||||
if compat.is_library_or_override(node_group):
|
||||
continue
|
||||
if not users.node_group_all(node_group.name):
|
||||
if not node_group.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_particles():
|
||||
"""Check if there are any unused particles (short-circuits early)."""
|
||||
if not hasattr(bpy.data, 'particles'):
|
||||
return False
|
||||
|
||||
for particle in bpy.data.particles:
|
||||
if compat.is_library_or_override(particle):
|
||||
continue
|
||||
if not users.particle_all(particle.name):
|
||||
if not particle.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_textures():
|
||||
"""Check if there are any unused textures (short-circuits early)."""
|
||||
if not hasattr(bpy.data, 'textures'):
|
||||
return False
|
||||
|
||||
for texture in bpy.data.textures:
|
||||
if compat.is_library_or_override(texture):
|
||||
continue
|
||||
if not users.texture_all(texture.name):
|
||||
if not texture.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_worlds():
|
||||
"""Check if there are any unused worlds (short-circuits early)."""
|
||||
for world in bpy.data.worlds:
|
||||
if compat.is_library_or_override(world):
|
||||
continue
|
||||
if world.users == 0 or (world.users == 1 and
|
||||
world.use_fake_user and
|
||||
config.include_fake_users):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_objects():
|
||||
"""Check if there are any unused objects (short-circuits early)."""
|
||||
for obj in bpy.data.objects:
|
||||
if compat.is_library_or_override(obj):
|
||||
continue
|
||||
if not users.object_all(obj.name):
|
||||
if not obj.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_armatures():
|
||||
"""Check if there are any unused armatures (short-circuits early)."""
|
||||
for armature in bpy.data.armatures:
|
||||
if compat.is_library_or_override(armature):
|
||||
continue
|
||||
if not users.armature_all(armature.name):
|
||||
if not armature.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_unused_for_smart_select():
|
||||
"""
|
||||
Get unused data for smart select operation (returns booleans).
|
||||
Optimized to short-circuit early - stops checking each category
|
||||
as soon as unused data is found. This is much faster than computing
|
||||
the full list of unused items.
|
||||
|
||||
Returns a dictionary with boolean values indicating if each category
|
||||
has unused data-blocks.
|
||||
"""
|
||||
# Use optimized short-circuit versions that stop as soon as
|
||||
# they find ONE unused item, rather than computing the full list
|
||||
return {
|
||||
'collections': _has_any_unused_collections(),
|
||||
'images': _has_any_unused_images(),
|
||||
'lights': _has_any_unused_lights(),
|
||||
'materials': _has_any_unused_materials(),
|
||||
'node_groups': _has_any_unused_node_groups(),
|
||||
'objects': _has_any_unused_objects(),
|
||||
'particles': _has_any_unused_particles(),
|
||||
'textures': _has_any_unused_textures(),
|
||||
'armatures': _has_any_unused_armatures(),
|
||||
'worlds': _has_any_unused_worlds(),
|
||||
}
|
||||
|
||||
|
||||
@@ -426,12 +426,24 @@ def light_objects(light_key):
|
||||
|
||||
def material_all(material_key):
|
||||
# returns a list of keys of every data-block that uses this material
|
||||
return material_objects(material_key) + \
|
||||
material_geometry_nodes(material_key)
|
||||
# Use comprehensive custom detection that covers all usage contexts
|
||||
users = []
|
||||
|
||||
# Check direct object usage (material slots)
|
||||
users.extend(material_objects(material_key))
|
||||
|
||||
# Check Geometry Nodes usage (materials in node groups used by objects)
|
||||
users.extend(material_geometry_nodes(material_key))
|
||||
|
||||
# Check node group usage (materials in node groups used elsewhere)
|
||||
users.extend(material_node_groups(material_key))
|
||||
|
||||
return distinct(users)
|
||||
|
||||
|
||||
def material_geometry_nodes(material_key):
|
||||
# returns a list of object keys that use the material via Geometry Nodes
|
||||
# Only counts objects that are in scene collections (recursive check)
|
||||
|
||||
users = []
|
||||
material = bpy.data.materials[material_key]
|
||||
@@ -440,17 +452,104 @@ def material_geometry_nodes(material_key):
|
||||
from ..utils import compat
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
# Skip library-linked and override objects
|
||||
if compat.is_library_or_override(obj):
|
||||
continue
|
||||
|
||||
# Check if object is in any scene collection (reuse object_all logic)
|
||||
# This ensures recursive checking: if the object using the material isn't in a scene,
|
||||
# the material isn't considered used
|
||||
obj_scenes = object_all(obj.name)
|
||||
is_in_scene = bool(obj_scenes)
|
||||
|
||||
if not is_in_scene:
|
||||
continue # Skip objects not in scene collections
|
||||
|
||||
if hasattr(obj, 'modifiers'):
|
||||
for modifier in obj.modifiers:
|
||||
if compat.is_geometry_nodes_modifier(modifier):
|
||||
ng = compat.get_geometry_nodes_modifier_node_group(modifier)
|
||||
if ng:
|
||||
# Check if this node group or any nested node groups contain the material
|
||||
if node_group_has_material(ng.name, material.name):
|
||||
users.append(obj.name)
|
||||
|
||||
return distinct(users)
|
||||
|
||||
|
||||
def material_node_groups(material_key):
|
||||
# returns a list of keys indicating where the material is used via node groups
|
||||
# This checks if the material is used in any node group, and if that node group
|
||||
# is itself used anywhere. This complements material_geometry_nodes() by checking
|
||||
# additional usage contexts (materials, other node groups, compositor, etc.)
|
||||
# Note: Geometry Nodes usage is already checked by material_geometry_nodes()
|
||||
# Optimized to return early when usage is found
|
||||
|
||||
from ..utils import compat
|
||||
material = bpy.data.materials[material_key]
|
||||
|
||||
# Check all node groups to see if they contain this material
|
||||
for node_group in bpy.data.node_groups:
|
||||
# Skip library-linked and override node groups
|
||||
if compat.is_library_or_override(node_group):
|
||||
continue
|
||||
if node_group_has_material(node_group.name, material.name):
|
||||
# This node group contains the material, check if the node group is used
|
||||
# Check usage contexts in order of likelihood, return early when found
|
||||
|
||||
# First check: is it used in Geometry Nodes modifiers? (most common case)
|
||||
# Note: material_geometry_nodes() already checks this, but we verify here too
|
||||
obj_users = node_group_objects(node_group.name)
|
||||
if obj_users:
|
||||
return obj_users # Return immediately - material is used
|
||||
|
||||
# Second check: is it used in materials?
|
||||
mat_users = node_group_materials(node_group.name)
|
||||
if mat_users:
|
||||
return mat_users # Return immediately - material is used
|
||||
|
||||
# Third check: is it used in compositor?
|
||||
comp_users = node_group_compositors(node_group.name)
|
||||
if comp_users:
|
||||
return comp_users # Return immediately - material is used
|
||||
|
||||
# Fourth check: is it used in textures?
|
||||
tex_users = node_group_textures(node_group.name)
|
||||
if tex_users:
|
||||
return tex_users # Return immediately - material is used
|
||||
|
||||
# Fifth check: is it used in worlds?
|
||||
world_users = node_group_worlds(node_group.name)
|
||||
if world_users:
|
||||
return world_users # Return immediately - material is used
|
||||
|
||||
# Last check: is it used in other node groups? (recursive, but only if needed)
|
||||
ng_users = node_group_node_groups(node_group.name)
|
||||
if ng_users:
|
||||
# Check if any parent node groups are used (quick check only)
|
||||
for parent_ng_name in ng_users:
|
||||
# Quick check: see if parent is used in objects (most common)
|
||||
parent_obj_users = node_group_objects(parent_ng_name)
|
||||
if parent_obj_users:
|
||||
return parent_obj_users
|
||||
# Quick check: see if parent is used in materials
|
||||
parent_mat_users = node_group_materials(parent_ng_name)
|
||||
if parent_mat_users:
|
||||
return parent_mat_users
|
||||
# Also check if parent is used in compositor, textures, worlds
|
||||
parent_comp_users = node_group_compositors(parent_ng_name)
|
||||
if parent_comp_users:
|
||||
return parent_comp_users
|
||||
parent_tex_users = node_group_textures(parent_ng_name)
|
||||
if parent_tex_users:
|
||||
return parent_tex_users
|
||||
parent_world_users = node_group_worlds(parent_ng_name)
|
||||
if parent_world_users:
|
||||
return parent_world_users
|
||||
|
||||
return [] # Material not used in any node groups
|
||||
|
||||
|
||||
def material_objects(material_key):
|
||||
# returns a list of object keys that use this material
|
||||
|
||||
@@ -760,22 +859,122 @@ def node_group_has_material(node_group_key, material_key):
|
||||
# returns true if a node group contains this material (directly or nested)
|
||||
|
||||
has_material = False
|
||||
node_group = bpy.data.node_groups[node_group_key]
|
||||
material = bpy.data.materials[material_key]
|
||||
try:
|
||||
node_group = bpy.data.node_groups[node_group_key]
|
||||
material = bpy.data.materials[material_key]
|
||||
except (KeyError, AttributeError):
|
||||
return False
|
||||
|
||||
for node in node_group.nodes:
|
||||
# base case: nodes with a material property (e.g., Set Material)
|
||||
if hasattr(node, 'material') and node.material:
|
||||
if node.material.name == material.name:
|
||||
has_material = True
|
||||
try:
|
||||
for node in node_group.nodes:
|
||||
try:
|
||||
# Explicitly check for GeometryNodeSetMaterial nodes first
|
||||
# This is the most reliable way to detect Set Material nodes in Geometry Nodes
|
||||
if hasattr(node, 'bl_idname'):
|
||||
try:
|
||||
if node.bl_idname == 'GeometryNodeSetMaterial':
|
||||
# Geometry Nodes Set Material nodes use input sockets, not a direct material property
|
||||
# Check the material input socket
|
||||
try:
|
||||
# Try to access the Material input socket directly by name
|
||||
if hasattr(node, 'inputs') and 'Material' in node.inputs:
|
||||
try:
|
||||
material_socket = node.inputs['Material']
|
||||
# Check the default_value (for unlinked materials)
|
||||
if hasattr(material_socket, 'default_value'):
|
||||
socket_material = material_socket.default_value
|
||||
if socket_material and hasattr(socket_material, 'name'):
|
||||
if (socket_material.name == material.name or
|
||||
socket_material == material):
|
||||
has_material = True
|
||||
except (KeyError, AttributeError, ReferenceError, RuntimeError, TypeError):
|
||||
pass
|
||||
|
||||
# Also check all inputs as fallback (in case socket name differs)
|
||||
if not has_material:
|
||||
for input_socket in getattr(node, 'inputs', []):
|
||||
try:
|
||||
# Check socket type - material sockets are typically 'MATERIAL' type
|
||||
socket_type = getattr(input_socket, 'type', '')
|
||||
if socket_type == 'MATERIAL' or 'material' in str(input_socket).lower():
|
||||
# Check if this socket has a default_value that is a material
|
||||
if hasattr(input_socket, 'default_value') and input_socket.default_value:
|
||||
socket_material = input_socket.default_value
|
||||
if socket_material and hasattr(socket_material, 'name'):
|
||||
if (socket_material.name == material.name or
|
||||
socket_material == material):
|
||||
has_material = True
|
||||
break # Found it, no need to continue
|
||||
except (AttributeError, ReferenceError, RuntimeError, TypeError):
|
||||
continue # Skip this socket if we can't access it
|
||||
|
||||
# Also check if the node has a direct material property (fallback for some versions)
|
||||
if not has_material and hasattr(node, 'material'):
|
||||
try:
|
||||
if node.material:
|
||||
if (node.material.name == material.name or
|
||||
node.material == material):
|
||||
has_material = True
|
||||
break # Found it, no need to continue
|
||||
except (AttributeError, ReferenceError, RuntimeError):
|
||||
pass # Skip if material access fails
|
||||
|
||||
if has_material:
|
||||
break # Break outer loop if we found it
|
||||
except (AttributeError, ReferenceError, RuntimeError):
|
||||
pass # Skip if Set Material node input access fails
|
||||
except (AttributeError, ReferenceError, RuntimeError):
|
||||
pass # Skip if bl_idname access fails
|
||||
|
||||
# Fallback: Check for any node with a material property (e.g., Set Material)
|
||||
# This catches other node types that might have materials
|
||||
if not has_material and hasattr(node, 'material'):
|
||||
try:
|
||||
if node.material:
|
||||
# Check both by name and by direct reference for robustness
|
||||
if (node.material.name == material.name or
|
||||
node.material == material):
|
||||
has_material = True
|
||||
break # Found it, no need to continue
|
||||
except (AttributeError, ReferenceError, RuntimeError):
|
||||
pass # Skip if material access fails
|
||||
|
||||
# Also check node type by substring for Set Material nodes (backup check)
|
||||
if not has_material and hasattr(node, 'bl_idname'):
|
||||
try:
|
||||
node_type = node.bl_idname
|
||||
# Check for Geometry Nodes Set Material node type (substring match)
|
||||
if 'SetMaterial' in node_type or 'SET_MATERIAL' in node_type.upper():
|
||||
if hasattr(node, 'material'):
|
||||
try:
|
||||
if node.material:
|
||||
if (node.material.name == material.name or
|
||||
node.material == material):
|
||||
has_material = True
|
||||
break
|
||||
except (AttributeError, ReferenceError, RuntimeError):
|
||||
pass # Skip if material access fails
|
||||
except (AttributeError, ReferenceError, RuntimeError):
|
||||
pass # Skip if bl_idname access fails
|
||||
|
||||
# recurse case: nested node groups
|
||||
elif hasattr(node, 'node_tree') and node.node_tree:
|
||||
has_material = node_group_has_material(
|
||||
node.node_tree.name, material.name)
|
||||
# recurse case: nested node groups
|
||||
# Check this separately (not elif) in case we need to recurse
|
||||
if not has_material and hasattr(node, 'node_tree'):
|
||||
try:
|
||||
if node.node_tree:
|
||||
has_material = node_group_has_material(
|
||||
node.node_tree.name, material.name)
|
||||
except (KeyError, AttributeError, ReferenceError, RuntimeError):
|
||||
continue # Skip invalid node groups
|
||||
|
||||
if has_material:
|
||||
break
|
||||
if has_material:
|
||||
break
|
||||
except (AttributeError, ReferenceError, RuntimeError):
|
||||
# Skip nodes that cause errors (e.g., invalid/corrupted nodes)
|
||||
continue
|
||||
except (AttributeError, ReferenceError, RuntimeError):
|
||||
# If we can't even iterate nodes, return False
|
||||
return False
|
||||
|
||||
return has_material
|
||||
|
||||
@@ -969,6 +1168,75 @@ def texture_particles(texture_key):
|
||||
return distinct(users)
|
||||
|
||||
|
||||
def object_all(object_key):
|
||||
# returns a list of scene names where the object is used
|
||||
# An object is "used" if it's in any collection that's part of any scene's collection hierarchy
|
||||
|
||||
users = []
|
||||
obj = bpy.data.objects[object_key]
|
||||
|
||||
# Get all collections that contain this object
|
||||
for collection in obj.users_collection:
|
||||
# Check if this collection is in any scene's hierarchy
|
||||
for scene in bpy.data.scenes:
|
||||
if _scene_collection_contains(scene.collection, collection):
|
||||
if scene.name not in users:
|
||||
users.append(scene.name)
|
||||
|
||||
return distinct(users)
|
||||
|
||||
|
||||
def armature_all(armature_key):
|
||||
# returns a list of object names that use the armature
|
||||
# Checks direct usage, modifier usage, and constraint usage
|
||||
# Only counts objects that are actually in scene collections (recursive check)
|
||||
|
||||
users = []
|
||||
armature = bpy.data.armatures[armature_key]
|
||||
|
||||
# Check all objects - but only count those that are in scene collections
|
||||
for obj in bpy.data.objects:
|
||||
# Skip library-linked and override objects
|
||||
from ..utils import compat
|
||||
if compat.is_library_or_override(obj):
|
||||
continue
|
||||
|
||||
# Check if object is in any scene collection (reuse object_all logic)
|
||||
obj_scenes = object_all(obj.name)
|
||||
is_in_scene = bool(obj_scenes)
|
||||
|
||||
# Check for usage regardless of scene status (we'll filter later)
|
||||
found_usage = False
|
||||
|
||||
# 1. Direct usage: ARMATURE objects where object.data == armature
|
||||
if obj.type == 'ARMATURE' and obj.data == armature:
|
||||
found_usage = True
|
||||
|
||||
# 2. Modifier usage: Armature modifiers where modifier.object.data == armature
|
||||
if not found_usage and hasattr(obj, 'modifiers'):
|
||||
for modifier in obj.modifiers:
|
||||
if modifier.type == 'ARMATURE':
|
||||
if hasattr(modifier, 'object') and modifier.object:
|
||||
if modifier.object.type == 'ARMATURE' and modifier.object.data == armature:
|
||||
found_usage = True
|
||||
break
|
||||
|
||||
# 3. Constraint usage: Constraints that target ARMATURE objects using this armature
|
||||
if not found_usage and hasattr(obj, 'constraints'):
|
||||
for constraint in obj.constraints:
|
||||
if hasattr(constraint, 'target') and constraint.target:
|
||||
if constraint.target.type == 'ARMATURE' and constraint.target.data == armature:
|
||||
found_usage = True
|
||||
break
|
||||
|
||||
# Only add to users if the object is actually in a scene
|
||||
# This implements recursive checking: if the user object is unused, it doesn't count
|
||||
if found_usage and is_in_scene:
|
||||
users.append(obj.name)
|
||||
|
||||
return distinct(users)
|
||||
|
||||
|
||||
def distinct(seq):
|
||||
# returns a list of distinct elements
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ This file contains the inspection user interface.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from ..stats import users
|
||||
from .utils import ui_layouts
|
||||
|
||||
@@ -700,14 +700,138 @@ class ATOMIC_OT_inspect_worlds(bpy.types.Operator):
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
# Atomic Data Manager Inspect Objects UI Operator
|
||||
class ATOMIC_OT_inspect_objects(bpy.types.Operator):
|
||||
"""Inspect Objects"""
|
||||
bl_idname = "atomic.inspect_objects"
|
||||
bl_label = "Inspect Objects"
|
||||
|
||||
# user lists
|
||||
users_scenes = []
|
||||
|
||||
def draw(self, context):
|
||||
global inspection_update_trigger
|
||||
atom = bpy.context.scene.atomic
|
||||
|
||||
layout = self.layout
|
||||
|
||||
# inspect objects header
|
||||
ui_layouts.inspect_header(
|
||||
layout=layout,
|
||||
atom_prop="objects_field",
|
||||
data="objects"
|
||||
)
|
||||
|
||||
# inspection update code
|
||||
if inspection_update_trigger:
|
||||
# if key is valid, update the user lists
|
||||
if atom.objects_field in bpy.data.objects.keys():
|
||||
self.users_scenes = users.object_all(atom.objects_field)
|
||||
|
||||
# if key is invalid, empty the user lists
|
||||
else:
|
||||
self.users_scenes = []
|
||||
|
||||
inspection_update_trigger = False
|
||||
|
||||
# scenes box list
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Scenes",
|
||||
items=self.users_scenes,
|
||||
icon="SCENE_DATA"
|
||||
)
|
||||
|
||||
row = layout.row() # extra row for spacing
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# update inspection context
|
||||
atom = bpy.context.scene.atomic
|
||||
atom.active_inspection = "OBJECTS"
|
||||
|
||||
# trigger update on invoke
|
||||
global inspection_update_trigger
|
||||
inspection_update_trigger = True
|
||||
|
||||
# invoke inspect dialog
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
# Atomic Data Manager Inspect Armatures UI Operator
|
||||
class ATOMIC_OT_inspect_armatures(bpy.types.Operator):
|
||||
"""Inspect Armatures"""
|
||||
bl_idname = "atomic.inspect_armatures"
|
||||
bl_label = "Inspect Armatures"
|
||||
|
||||
# user lists
|
||||
users_objects = []
|
||||
|
||||
def draw(self, context):
|
||||
global inspection_update_trigger
|
||||
atom = bpy.context.scene.atomic
|
||||
|
||||
layout = self.layout
|
||||
|
||||
# inspect armatures header
|
||||
ui_layouts.inspect_header(
|
||||
layout=layout,
|
||||
atom_prop="armatures_field",
|
||||
data="armatures"
|
||||
)
|
||||
|
||||
# inspection update code
|
||||
if inspection_update_trigger:
|
||||
# if key is valid, update the user lists
|
||||
if atom.armatures_field in bpy.data.armatures.keys():
|
||||
self.users_objects = users.armature_all(atom.armatures_field)
|
||||
|
||||
# if key is invalid, empty the user lists
|
||||
else:
|
||||
self.users_objects = []
|
||||
|
||||
inspection_update_trigger = False
|
||||
|
||||
# objects box list
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Objects",
|
||||
items=self.users_objects,
|
||||
icon="OBJECT_DATA"
|
||||
)
|
||||
|
||||
row = layout.row() # extra row for spacing
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# update inspection context
|
||||
atom = bpy.context.scene.atomic
|
||||
atom.active_inspection = "ARMATURES"
|
||||
|
||||
# trigger update on invoke
|
||||
global inspection_update_trigger
|
||||
inspection_update_trigger = True
|
||||
|
||||
# invoke inspect dialog
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self)
|
||||
|
||||
|
||||
reg_list = [
|
||||
ATOMIC_OT_inspect_collections,
|
||||
ATOMIC_OT_inspect_images,
|
||||
ATOMIC_OT_inspect_lights,
|
||||
ATOMIC_OT_inspect_materials,
|
||||
ATOMIC_OT_inspect_node_groups,
|
||||
ATOMIC_OT_inspect_objects,
|
||||
ATOMIC_OT_inspect_particles,
|
||||
ATOMIC_OT_inspect_textures,
|
||||
ATOMIC_OT_inspect_armatures,
|
||||
ATOMIC_OT_inspect_worlds
|
||||
]
|
||||
|
||||
@@ -719,4 +843,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reg_list:
|
||||
unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
@@ -28,7 +28,7 @@ category toggles and the category selection tools.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from ..stats import count
|
||||
from .utils import ui_layouts
|
||||
|
||||
@@ -50,8 +50,10 @@ class ATOMIC_PT_main_panel(bpy.types.Panel):
|
||||
atom.lights,
|
||||
atom.materials,
|
||||
atom.node_groups,
|
||||
atom.objects,
|
||||
atom.particles,
|
||||
atom.textures,
|
||||
atom.armatures,
|
||||
atom.worlds
|
||||
]
|
||||
|
||||
@@ -87,6 +89,23 @@ class ATOMIC_PT_main_panel(bpy.types.Panel):
|
||||
text=""
|
||||
)
|
||||
|
||||
# objects buttons
|
||||
splitcol = col.split(factor=0.8, align=True)
|
||||
|
||||
splitcol.prop(
|
||||
atom,
|
||||
"objects",
|
||||
text="Objects",
|
||||
icon='OBJECT_DATA',
|
||||
toggle=True
|
||||
)
|
||||
|
||||
splitcol.operator(
|
||||
"atomic.inspect_objects",
|
||||
icon='VIEWZOOM',
|
||||
text=""
|
||||
)
|
||||
|
||||
# lights buttons
|
||||
splitcol = col.split(factor=0.8, align=True)
|
||||
|
||||
@@ -175,6 +194,23 @@ class ATOMIC_PT_main_panel(bpy.types.Panel):
|
||||
text=""
|
||||
)
|
||||
|
||||
# armatures buttons
|
||||
splitcol = col.split(factor=0.8, align=True)
|
||||
|
||||
splitcol.prop(
|
||||
atom,
|
||||
"armatures",
|
||||
text="Armatures",
|
||||
icon='ARMATURE_DATA',
|
||||
toggle=True
|
||||
)
|
||||
|
||||
splitcol.operator(
|
||||
"atomic.inspect_armatures",
|
||||
icon='VIEWZOOM',
|
||||
text=""
|
||||
)
|
||||
|
||||
# particles buttons
|
||||
splitcol = col.split(factor=0.8, align=True)
|
||||
|
||||
@@ -242,4 +278,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reg_list:
|
||||
unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
@@ -25,7 +25,7 @@ pops up when missing files are detected on file load.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from bpy.app.handlers import persistent
|
||||
from .. import config
|
||||
from ..stats import missing
|
||||
@@ -189,7 +189,7 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for item in reg_list:
|
||||
unregister_class(item)
|
||||
compat.safe_unregister_class(item)
|
||||
|
||||
# stop running missing file auto-detection after loading a Blender file
|
||||
bpy.app.handlers.load_post.remove(autodetect_missing_files)
|
||||
|
||||
@@ -25,7 +25,7 @@ registration.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
|
||||
|
||||
# Atomic Data Manager Main Pie Menu
|
||||
@@ -197,4 +197,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reg_list:
|
||||
unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
@@ -25,7 +25,7 @@ some functions for syncing the preference properties with external factors.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from .. import config
|
||||
# updater removed in Blender 4.5 extension format
|
||||
|
||||
@@ -328,6 +328,6 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reg_list:
|
||||
unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
remove_pie_menu_hotkeys()
|
||||
|
||||
@@ -28,7 +28,7 @@ it.
|
||||
|
||||
import bpy
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from ..stats import count
|
||||
from ..stats import misc
|
||||
from .utils import ui_layouts
|
||||
@@ -373,4 +373,4 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reg_list:
|
||||
unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
@@ -26,7 +26,7 @@ support Remington Creative popup.
|
||||
import bpy
|
||||
import time
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from ..utils import compat
|
||||
from bpy.app.handlers import persistent
|
||||
from .. import config
|
||||
from . import preferences_ui
|
||||
@@ -123,6 +123,6 @@ def register():
|
||||
|
||||
def unregister():
|
||||
for cls in reg_list:
|
||||
unregister_class(cls)
|
||||
compat.safe_unregister_class(cls)
|
||||
|
||||
bpy.app.handlers.load_post.remove(show_support_me_popup)
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
"""
|
||||
Copyright (C) 2019 Remington Creative
|
||||
|
||||
This file is part of Atomic Data Manager.
|
||||
|
||||
Atomic Data Manager is free software: you can redistribute
|
||||
it and/or modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
Atomic Data Manager is distributed in the hope that it will
|
||||
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with Atomic Data Manager. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
---
|
||||
|
||||
This package contains utility modules for version detection and API compatibility.
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
"""
|
||||
Copyright (C) 2019 Remington Creative
|
||||
|
||||
This file is part of Atomic Data Manager.
|
||||
|
||||
Atomic Data Manager is free software: you can redistribute
|
||||
it and/or modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
Atomic Data Manager is distributed in the hope that it will
|
||||
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with Atomic Data Manager. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
---
|
||||
|
||||
This module provides API compatibility functions for handling differences
|
||||
between Blender 4.2 LTS, 4.5 LTS, and 5.0.
|
||||
|
||||
@@ -152,3 +133,25 @@ def get_scene_compositor_node_tree(scene):
|
||||
if hasattr(scene, 'node_tree') and scene.node_tree:
|
||||
return scene.node_tree
|
||||
return None
|
||||
|
||||
|
||||
def is_library_or_override(datablock):
|
||||
"""
|
||||
Check if a datablock is library-linked or an override.
|
||||
Atomic should completely ignore all datablocks within libraries.
|
||||
|
||||
Args:
|
||||
datablock: The datablock to check
|
||||
|
||||
Returns:
|
||||
bool: True if the datablock is library-linked or an override, False otherwise
|
||||
"""
|
||||
# Check if datablock is linked from a library
|
||||
if hasattr(datablock, 'library') and datablock.library:
|
||||
return True
|
||||
|
||||
# Check if datablock is an override (Blender 3.0+)
|
||||
if hasattr(datablock, 'override_library') and datablock.override_library:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
"""
|
||||
Copyright (C) 2019 Remington Creative
|
||||
|
||||
This file is part of Atomic Data Manager.
|
||||
|
||||
Atomic Data Manager is free software: you can redistribute
|
||||
it and/or modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
Atomic Data Manager is distributed in the hope that it will
|
||||
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with Atomic Data Manager. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
---
|
||||
|
||||
This module provides version detection and comparison utilities for
|
||||
multi-version Blender support (4.2 LTS, 4.5 LTS, and 5.0).
|
||||
|
||||
|
||||
@@ -676,17 +676,25 @@ class BPL_OT_create_playblast(Operator):
|
||||
glob.glob(os.path.join(frame_output_dir, "*.jpeg")))
|
||||
file_output_done = len(frame_files) >= expected_frames
|
||||
elif getattr(self, '_needs_video_encode', False):
|
||||
# Check for last frame file (PNG or JPEG) or completed frame count
|
||||
# Check for last frame file (PNG or JPEG) - must check for actual last frame number
|
||||
# Determine extension from current render settings
|
||||
current_format = context.scene.render.image_settings.file_format
|
||||
frame_ext = ".jpg" if current_format == "JPEG" else ".png"
|
||||
expected_frame = f"{output_path}{expected_frames:04d}{frame_ext}"
|
||||
if os.path.exists(expected_frame):
|
||||
# Check for the actual last frame file (frame_end, not expected_frames)
|
||||
last_frame_file = f"{output_path}{self._frame_end:04d}{frame_ext}"
|
||||
if os.path.exists(last_frame_file):
|
||||
file_output_done = True
|
||||
else:
|
||||
# Check both PNG and JPEG files
|
||||
# Also check both PNG and JPEG files, but verify we have the last frame
|
||||
frame_matches = glob.glob(f"{output_path}*.png") + glob.glob(f"{output_path}*.jpg") + glob.glob(f"{output_path}*.jpeg")
|
||||
file_output_done = len(frame_matches) >= expected_frames
|
||||
# Check if we have at least expected_frames AND the last frame exists
|
||||
if len(frame_matches) >= expected_frames:
|
||||
# Verify the last frame file exists
|
||||
last_frame_png = f"{output_path}{self._frame_end:04d}.png"
|
||||
last_frame_jpg = f"{output_path}{self._frame_end:04d}.jpg"
|
||||
file_output_done = os.path.exists(last_frame_png) or os.path.exists(last_frame_jpg)
|
||||
else:
|
||||
file_output_done = False
|
||||
else:
|
||||
file_ext = get_file_extension(context.scene.basedplayblast.video_format)
|
||||
file_output_done = os.path.exists(output_path + file_ext)
|
||||
@@ -700,17 +708,36 @@ class BPL_OT_create_playblast(Operator):
|
||||
self._render_job_finished_time = time.time()
|
||||
|
||||
ready_to_finalize = False
|
||||
if rendered_frames_done:
|
||||
ready_to_finalize = True
|
||||
elif file_output_done:
|
||||
ready_to_finalize = True
|
||||
elif frame_range_done and self._render_job_finished_time is not None:
|
||||
if (time.time() - self._render_job_finished_time) >= self._render_job_grace:
|
||||
# For video encoding path, we MUST have both the last frame file AND the render job must be finished
|
||||
if getattr(self, '_needs_video_encode', False):
|
||||
# Strict check: need the last frame file AND render job must be finished
|
||||
if file_output_done and not render_job_running:
|
||||
if self._render_job_finished_time is not None:
|
||||
# Wait a bit after render job finishes to ensure all files are written
|
||||
if (time.time() - self._render_job_finished_time) >= 0.5:
|
||||
ready_to_finalize = True
|
||||
elif not self._render_job_was_running:
|
||||
# If render job was never running (unlikely but possible), just check file
|
||||
ready_to_finalize = file_output_done
|
||||
# Additional safeguard: if we've seen the end frame and render job finished, wait a bit then finalize
|
||||
elif frame_range_done and not render_job_running and self._render_job_finished_time is not None:
|
||||
if (time.time() - self._render_job_finished_time) >= self._render_job_grace:
|
||||
ready_to_finalize = True
|
||||
print("Render job ended; finalizing after grace period (video encode path).")
|
||||
else:
|
||||
# For non-video-encode paths, use original logic
|
||||
if rendered_frames_done:
|
||||
ready_to_finalize = True
|
||||
print("Render job ended; finalizing after grace period without detecting file.")
|
||||
elif file_output_done:
|
||||
ready_to_finalize = True
|
||||
elif frame_range_done and self._render_job_finished_time is not None:
|
||||
if (time.time() - self._render_job_finished_time) >= self._render_job_grace:
|
||||
ready_to_finalize = True
|
||||
print("Render job ended; finalizing after grace period without detecting file.")
|
||||
|
||||
# Additional safeguard: if we've seen the end frame and no progress change for a moment, finalize
|
||||
if not ready_to_finalize and frame_range_done and self._last_frame_change_time:
|
||||
# But only if render job is not running
|
||||
if not ready_to_finalize and frame_range_done and not render_job_running and self._last_frame_change_time:
|
||||
if (time.time() - self._last_frame_change_time) >= 1.0:
|
||||
ready_to_finalize = True
|
||||
print("Frame progress stalled at end frame; finalizing to prevent hang.")
|
||||
@@ -1493,9 +1520,12 @@ class BPL_OT_create_playblast(Operator):
|
||||
# Build FFmpeg command with proper structure:
|
||||
# 1. All inputs first (video, then audio if present)
|
||||
# 2. Then all encoding options
|
||||
# Note: FFmpeg's %04d pattern expects frames starting at 0000, but our frames start at frame_start
|
||||
# We need to add -start_number to tell FFmpeg the actual starting frame number
|
||||
ffmpeg_cmd = [
|
||||
"ffmpeg", "-y", # Overwrite output file
|
||||
"-framerate", str(framerate),
|
||||
"-start_number", str(self._frame_start), # Tell FFmpeg the starting frame number
|
||||
"-i", frame_pattern,
|
||||
]
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
|
||||
id = "basedplayblast"
|
||||
name = "BasedPlayblast"
|
||||
tagline = "Easily create playblasts from Blender and Flamenco"
|
||||
version = "2.6.0"
|
||||
version = "2.6.1"
|
||||
type = "add-on"
|
||||
|
||||
maintainer = "RaincloudTheDragon <raincloudthedragon@gmail.com>"
|
||||
|
||||
@@ -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