bl_info = { "name": "Data Replacer", "author": "VICUBE Animation", "version": (1, 0), "blender": (4, 0, 0), "location": "3D View ▸ Sidebar ▸ Data Replacer", "description": "Replaces materials, objects, and collections with the same name in multiple .blend files from the current scene.", "category": "Object", } import bpy import os import tempfile import subprocess from bpy.types import Operator, Panel, PropertyGroup, UIList from bpy.props import StringProperty, CollectionProperty, PointerProperty, IntProperty class MaterialEntry(PropertyGroup): name: StringProperty(name="Material") class ObjectEntry(PropertyGroup): name: StringProperty(name="Object") class CollectionEntry(PropertyGroup): name: StringProperty(name="Collection") class BlendFileEntry(PropertyGroup): path: StringProperty(name="Blend File", subtype='FILE_PATH') class MaterialReplacerProperties(PropertyGroup): materials: CollectionProperty(type=MaterialEntry) material_index: IntProperty() objects: CollectionProperty(type=ObjectEntry) object_index: IntProperty() collections: CollectionProperty(type=CollectionEntry) collection_index: IntProperty() blend_files: CollectionProperty(type=BlendFileEntry) blend_index: IntProperty() class MATERIAL_UL_List(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): layout.label(text=item.name) class OBJECT_UL_List(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): layout.label(text=item.name) class COLLECTION_UL_List(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): layout.label(text=item.name) class BLEND_UL_List(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): layout.label(text=os.path.basename(item.path)) class ADD_OT_AddMaterial(Operator): bl_idname = "datareplacer.add_material" bl_label = "Add Material" def execute(self, context): props = context.scene.material_replacer_props mat = context.object.active_material if mat and mat.name not in [m.name for m in props.materials]: props.materials.add().name = mat.name return {"FINISHED"} class REMOVE_OT_RemoveMaterial(Operator): bl_idname = "datareplacer.remove_material" bl_label = "Remove Material" def execute(self, context): props = context.scene.material_replacer_props if 0 <= props.material_index < len(props.materials): props.materials.remove(props.material_index) props.material_index = max(0, props.material_index - 1) return {"FINISHED"} class ADD_OT_AddObject(Operator): bl_idname = "datareplacer.add_object" bl_label = "Add Object" def execute(self, context): props = context.scene.material_replacer_props obj = context.object if obj and obj.name not in [o.name for o in props.objects]: props.objects.add().name = obj.name return {"FINISHED"} class REMOVE_OT_RemoveObject(Operator): bl_idname = "datareplacer.remove_object" bl_label = "Remove Object" def execute(self, context): props = context.scene.material_replacer_props if 0 <= props.object_index < len(props.objects): props.objects.remove(props.object_index) props.object_index = max(0, props.object_index - 1) return {"FINISHED"} class ADD_OT_AddCollection(Operator): bl_idname = "datareplacer.add_collection" bl_label = "Add Collection" def execute(self, context): props = context.scene.material_replacer_props coll = context.collection or (context.object.users_collection[0] if context.object and context.object.users_collection else None) if coll and coll.name not in [c.name for c in props.collections]: props.collections.add().name = coll.name return {"FINISHED"} class REMOVE_OT_RemoveCollection(Operator): bl_idname = "datareplacer.remove_collection" bl_label = "Remove Collection" def execute(self, context): props = context.scene.material_replacer_props if 0 <= props.collection_index < len(props.collections): props.collections.remove(props.collection_index) props.collection_index = max(0, props.collection_index - 1) return {"FINISHED"} class ADD_OT_AddBlendFiles(Operator): bl_idname = "datareplacer.add_blend_files" bl_label = "Select .blend Files" filepath: StringProperty(subtype="FILE_PATH") files: CollectionProperty(type=bpy.types.OperatorFileListElement) def invoke(self, context, _event): context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} def execute(self, context): props = context.scene.material_replacer_props base = os.path.dirname(self.filepath) for f in self.files: path = os.path.join(base, f.name) if path not in [b.path for b in props.blend_files]: props.blend_files.add().path = path return {"FINISHED"} class REMOVE_OT_RemoveBlendFile(Operator): bl_idname = "datareplacer.remove_blend" bl_label = "Remove .blend File" def execute(self, context): props = context.scene.material_replacer_props if 0 <= props.blend_index < len(props.blend_files): props.blend_files.remove(props.blend_index) props.blend_index = max(0, props.blend_index - 1) return {"FINISHED"} class DATA_OT_ReplaceInBlendFiles(bpy.types.Operator): bl_idname = "datareplacer.replace_data" bl_label = "Replace" def execute(self, context): props = context.scene.material_replacer_props source_file = bpy.data.filepath if not source_file: self.report({'ERROR'}, "Save the current file first.") return {'CANCELLED'} targets = [b.path for b in props.blend_files if os.path.exists(b.path)] mat_names = [m.name for m in props.materials] obj_names = [o.name for o in props.objects] coll_names = [c.name for c in props.collections] if not targets or (not mat_names and not obj_names and not coll_names): self.report({'ERROR'}, "Select at least one collection/object/material and one target file.") return {'CANCELLED'} blender_bin = bpy.app.binary_path with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as tf: tf.write(f''' import bpy, os source_file = r"""{source_file}""" material_names = {mat_names!r} object_names = {obj_names!r} collection_names = {coll_names!r} target_paths = {targets!r} def replace(): parent_map = {{}} def _get_parent_and_index(coll): root = bpy.context.scene.collection for i, c in enumerate(root.children): if c == coll: return root, i for p in bpy.data.collections: for i, c in enumerate(p.children): if c == coll: return p, i return None, None for n in collection_names: coll = bpy.data.collections.get(n) if coll: parent_map[n] = _get_parent_and_index(coll) for obj in list(coll.all_objects): d = obj.data bpy.data.objects.remove(obj, do_unlink=True) if d and d.users == 0 and hasattr(d, "users") and d.__class__.__name__ == "Mesh": bpy.data.meshes.remove(d) bpy.data.collections.remove(coll) for n in object_names: o = bpy.data.objects.get(n) if o: d = o.data bpy.data.objects.remove(o, do_unlink=True) if d and d.users == 0 and hasattr(d, "users") and d.__class__.__name__ == "Mesh": bpy.data.meshes.remove(d) for n in material_names: m = bpy.data.materials.get(n) if m: m.name = n + "_OLD_REPLACE" bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=False, do_recursive=True) with bpy.data.libraries.load(source_file, link=False) as (src, dst): dst.collections = [n for n in collection_names if n in src.collections] dst.objects = [n for n in object_names if n in src.objects] dst.materials = [n for n in material_names if n in src.materials] for n in material_names: new_mat = bpy.data.materials.get(n) old_mat = bpy.data.materials.get(n + "_OLD_REPLACE") if new_mat and old_mat: for obj in bpy.data.objects: for slot in obj.material_slots: if slot.material == old_mat: slot.material = new_mat bpy.data.materials.remove(old_mat, do_unlink=True) for n in collection_names: coll = bpy.data.collections.get(n) parent, tgt_idx = parent_map.get(n, (None, None)) parent = parent or bpy.context.scene.collection if coll and coll.name not in parent.children: parent.children.link(coll) if coll and tgt_idx is not None: cur_idx = parent.children.find(coll.name) if cur_idx != -1: if hasattr(parent.children, "move"): parent.children.move(cur_idx, tgt_idx) else: ordered = list(parent.children) ordered.pop(cur_idx) ordered.insert(tgt_idx, coll) for ctmp in ordered: parent.children.unlink(ctmp) for ctmp in ordered: parent.children.link(ctmp) for o in [o for o in dst.objects if o and o.users == 0]: bpy.context.collection.objects.link(o) for path in target_paths: bpy.ops.wm.open_mainfile(filepath=path) replace() bpy.ops.wm.save_mainfile() ''') script_path = tf.name subprocess.Popen([blender_bin, "--factory-startup", "--background", "--python", script_path]) self.report({'INFO'}, "Replacement is running in the background.") return {'FINISHED'} class MATERIAL_PT_ReplacerPanel(Panel): bl_label = "Data Replacer" bl_idname = "DATA_PT_replacer_panel" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Data Replacer" def draw(self, context): props = context.scene.material_replacer_props layout = self.layout box = layout.box() box.label(text="Source Data", icon='ASSET_MANAGER') box.label(text="Collections:") row = box.row() row.template_list("COLLECTION_UL_List", "", props, "collections", props, "collection_index") col = row.column(align=True) col.operator("datareplacer.add_collection", icon="ADD", text="") col.operator("datareplacer.remove_collection", icon="REMOVE", text="") box.label(text="Objects:") row = box.row() row.template_list("OBJECT_UL_List", "", props, "objects", props, "object_index") col = row.column(align=True) col.operator("datareplacer.add_object", icon="ADD", text="") col.operator("datareplacer.remove_object", icon="REMOVE", text="") box.label(text="Materials:") row = box.row() row.template_list("MATERIAL_UL_List", "", props, "materials", props, "material_index") col = row.column(align=True) col.operator("datareplacer.add_material", icon="ADD", text="") col.operator("datareplacer.remove_material", icon="REMOVE", text="") box2 = layout.box() box2.label(text="Target Files", icon='FILE_BLEND') row = box2.row() row.template_list("BLEND_UL_List", "", props, "blend_files", props, "blend_index") col = row.column(align=True) col.operator("datareplacer.add_blend_files", icon="ADD", text="") col.operator("datareplacer.remove_blend", icon="REMOVE", text="") box2.operator("datareplacer.replace_data", icon="FILE_REFRESH") classes = ( MaterialEntry, ObjectEntry, CollectionEntry, BlendFileEntry, MaterialReplacerProperties, MATERIAL_UL_List, OBJECT_UL_List, COLLECTION_UL_List, BLEND_UL_List, ADD_OT_AddMaterial, REMOVE_OT_RemoveMaterial, ADD_OT_AddObject, REMOVE_OT_RemoveObject, ADD_OT_AddCollection, REMOVE_OT_RemoveCollection, ADD_OT_AddBlendFiles, REMOVE_OT_RemoveBlendFile, DATA_OT_ReplaceInBlendFiles, MATERIAL_PT_ReplacerPanel, ) def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.material_replacer_props = PointerProperty(type=MaterialReplacerProperties) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) del bpy.types.Scene.material_replacer_props if __name__ == "__main__": register()