From db64eb4afa9fb55799891d727edff4d124cb6623 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 19 Jan 2026 15:00:46 +0100 Subject: [PATCH] feat: add 1.0.0 version for blender 4.4.3 --- __init__.py | 177 ++++++++++++++++++++++++++++++++++++++++++ blender_manifest.toml | 11 +++ 2 files changed, 188 insertions(+) create mode 100644 __init__.py create mode 100644 blender_manifest.toml diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..fc66a3d --- /dev/null +++ b/__init__.py @@ -0,0 +1,177 @@ +import bpy +import os +import shutil + + +def get_external_datablocks(): + """Return a list of external data-blocks""" + out = [] + for attr in dir(bpy.data): + collections = getattr(bpy.data, attr) + if isinstance(collections, type(bpy.data.objects)): + for data_block in collections: + if (isinstance(data_block, bpy.types.Image) or isinstance(data_block, bpy.types.MovieClip)) and hasattr(data_block, "filepath"): + if hasattr(data_block, "packed_file"): + print(f"[{'P' if data_block.packed_file is not None else ' '}] [{data_block.__class__}] {data_block.source}: {data_block.filepath}") + else: + print(f" [{data_block.__class__}] {data_block.source}: {data_block.filepath}") + out.append(data_block) + + return out + +def collect_data(data_block, dirpath, sequence_in_subfolder=True): + """Copy data into a given folder. Only work for 'MOVIE' and 'SEQUENCE' data-blocks""" + if data_block.source == "MOVIE": + source_path = os.path.normpath(bpy.path.abspath(data_block.filepath)) + dest_path = os.path.join(dirpath, os.path.basename(source_path)) + print(f"Copying FROM '{source_path}' TO '{dest_path}'") + shutil.copyfile(source_path, dest_path) + + elif data_block.source == "SEQUENCE": + source_dirpath = os.path.dirname(os.path.normpath(bpy.path.abspath(data_block.filepath))) + if sequence_in_subfolder: + dest_dirpath = os.path.join(dirpath, os.path.basename(source_dirpath)) + else: + dest_dirpath = dirpath + if not os.path.isdir(dest_dirpath): + os.mkdir(dest_dirpath) + print(f"Copying FROM '{source_dirpath}' TO '{dest_dirpath}'") + for filename in os.listdir(source_dirpath): + if os.path.splitext(filename)[1] != ".db": + shutil.copyfile(os.path.join(source_dirpath, filename), os.path.join(dest_dirpath, filename)) + +def get_source_datablocks(datablocks, source): + """Return the list data-blocks which are of specific sources""" + out = [] + for data_block in datablocks: + if data_block.source == source: + out.append(data_block) + return out + +def main(): + external_datablocks = get_external_datablocks() + print(f"{len(external_datablocks)} external data-blocks") + + bl_dirpath = os.path.dirname(bpy.data.filepath) + bl_filename = os.path.splitext(os.path.basename(bpy.data.filepath))[0] + if len(external_datablocks) > 0: + # Create a subfolder per blend file + external_data_dirpath = os.path.join(bl_dirpath, f"{bl_filename}-external_data") + if not os.path.isdir(external_data_dirpath): + os.mkdir(external_data_dirpath) + + for data_block in external_datablocks: + collect_data(data_block, external_data_dirpath) + + +class WM_OT_ConsolidateProject(bpy.types.Operator): + """Open the consolidation dialog box""" + bl_label = "Consolidate Project" + bl_idname = "wm.consolidate_project" + + my_purge_unused_data: bpy.props.BoolProperty(name="Purge Unused Data", default=True) + my_pack_ressources: bpy.props.BoolProperty(name="Pack External Ressources", default=True) + my_collect_movies: bpy.props.BoolProperty(name="Collect Movies", default=True) + my_collect_sequences: bpy.props.BoolProperty(name="Collect Image Sequences", default=True) + my_sequences_in_subfolders: bpy.props.BoolProperty(name="Save Image Sequences in Sub-Folders", default=False) + + my_external_datablocks = [] + my_external_movies = [] + my_external_sequences = [] + + my_dirpath = None + my_filename = None + my_external_data_dirpath = None + + def execute(self, context): + if self.my_purge_unused_data: + bpy.ops.outliner.orphans_purge() + if self.my_pack_ressources: + bpy.ops.file.pack_all() + if self.my_collect_movies: + for data_block in self.my_external_movies: + collect_data(data_block, self.my_external_data_dirpath) + if self.my_collect_sequences: + for data_block in self.my_external_sequences: + collect_data(data_block, self.my_external_data_dirpath, self.my_sequences_in_subfolders) + self.report({'INFO'}, "Consolidation complete") + return {"FINISHED"} + + def invoke(self, context, event): + self.my_external_datablocks = get_external_datablocks() + self.my_external_movies = get_source_datablocks(self.my_external_datablocks, source='MOVIE') + self.my_external_sequences = get_source_datablocks(self.my_external_datablocks, source='SEQUENCE') + + self.my_dirpath = os.path.dirname(bpy.data.filepath) + self.my_filename = os.path.splitext(os.path.basename(bpy.data.filepath))[0] + self.my_external_data_dirpath = os.path.join(self.my_dirpath, f"{self.my_filename}-external_data") + + if not os.path.isdir(self.my_external_data_dirpath): + os.mkdir(self.my_external_data_dirpath) + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + + col = layout.column() + col.label(text="Cleaning and Packing Ressources", icon="PACKAGE") + row = col.row() + row.prop(self, "my_purge_unused_data") + row = col.row() + row.prop(self, "my_pack_ressources") + + layout.separator() + + col = layout.column() + col.label(text="Collect movies and image sequences", icon="FILE_MOVIE") + + row = col.row() + row.prop(self, "my_collect_movies") + row = col.row() + row.prop(self, "my_collect_sequences") + row = col.row() + row.prop(self, "my_sequences_in_subfolders", icon="NEWFOLDER") + row.enabled = self.my_collect_sequences + + box = layout.box() + box.label(text="External data-blocks") + for data_block in self.my_external_datablocks: + row = box.row() + display_data_block(data_block, row) + + +def display_data_block(data_block, layout): + split = layout.split(factor=.9) + if data_block.source in ["SEQUENCE", "MOVIE"]: + split.label(text=f"{data_block.name}", icon="FILE_MOVIE") + #split.label(text="", icon="WARNING_LARGE") + else: + split.label(text=f"{data_block.name}", icon="FILE_IMAGE") + if hasattr(data_block, "packed_file"): + if data_block.packed_file is not None: + split.label(text=f"", icon="PACKAGE") + else: + split.label(text=f"", icon="UGLYPACKAGE") + +def menu_func_import(self, context): + self.layout.operator(WM_OT_ConsolidateProject.bl_idname, text="ETNCL - Consolidate", icon="PACKAGE") + self.layout.separator() + +blender_classes = [ + WM_OT_ConsolidateProject +] + +def register(): + for cls in blender_classes: + bpy.utils.register_class(cls) + bpy.types.TOPBAR_MT_file_external_data.prepend(menu_func_import) + +def unregister(): + for cls in blender_classes: + bpy.utils.unregister_class(cls) + bpy.types.TOPBAR_MT_file_external_data.remove(menu_func_import) + + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/blender_manifest.toml b/blender_manifest.toml new file mode 100644 index 0000000..da95fee --- /dev/null +++ b/blender_manifest.toml @@ -0,0 +1,11 @@ +schema_version = "1.0.0" +id = "etncl_consolidation_process" +name = "ETNCL - Consolidation Process" +version = "1.0.0" +tagline = "Fully consolidate a Blender file by packaging its external data" +maintainer = "Florian Pineau " +type = "add-on" +tags = ["Pipeline"] +blender_version_min = "4.4.3" +license = ["SPDX:GPL-3.0-or-later"] +copyright = ["2025 ETINCELLE POST-PRODUCTION"] \ No newline at end of file