Skip to content

Commit 59bd0b6

Browse files
authored
Script updated to be an addon.
You can now activate the script via the menu having installed this addon from file: File/External Data/Gather Resources
1 parent c222197 commit 59bd0b6

File tree

2 files changed

+119
-72
lines changed

2 files changed

+119
-72
lines changed

Gather_Resources.PY

Lines changed: 0 additions & 72 deletions
This file was deleted.

__init__.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
bl_info = {
2+
"name": "Gather Resources",
3+
"blender": (4, 2, 0),
4+
"category": "File",
5+
"version": (0, 2),
6+
"author": "Simon Heggie",
7+
"description": "Gathers all resources used in the project and copies them to a local textures folder.",
8+
"location": "File > External Data",
9+
"warning": "Pre-alpha testing",
10+
"wiki_url": "https://github.com/SimonHeggie/Blender-GatherResources/blob/main/README.md",
11+
"tracker_url": "",
12+
}
13+
14+
import bpy
15+
import shutil
16+
from pathlib import Path
17+
from concurrent.futures import ThreadPoolExecutor, as_completed
18+
19+
class GatherResourcesOperator(bpy.types.Operator):
20+
"""Gather resources and copy to the 'textures/' folder within the .blend file's directory"""
21+
bl_idname = "file.gather_resources"
22+
bl_label = "Gather Resources"
23+
bl_options = {'REGISTER', 'UNDO'}
24+
25+
def copy_file(self, src, dest):
26+
"""Helper function to copy files with error handling."""
27+
try:
28+
if not dest.exists() or src.stat().st_mtime > dest.stat().st_mtime:
29+
shutil.copy2(src, dest)
30+
self.report({'INFO'}, f"Collected: {src} -> {dest}")
31+
return src.name, True
32+
except PermissionError as e:
33+
self.report({'ERROR'}, f"Error copying {src}: {e}")
34+
return src.name, False
35+
36+
def get_destination_folder(self, src, textures_dir):
37+
"""Determine destination folder to prevent naming conflicts."""
38+
if textures_dir in src.parents:
39+
return textures_dir # Place in root if already inside textures/
40+
41+
folder_name = src.parent.name
42+
folder_path = textures_dir / folder_name
43+
folder_path.mkdir(parents=True, exist_ok=True)
44+
return folder_path
45+
46+
def execute(self, context):
47+
# Check if the blend file has been saved
48+
blend_filepath = bpy.data.filepath
49+
if not blend_filepath:
50+
self.report({'ERROR'}, "Please save your blend file before gathering resources.")
51+
return {'CANCELLED'}
52+
53+
# Set destination directory relative to the saved .blend file
54+
blend_dir = Path(bpy.path.abspath("//"))
55+
textures_dir = blend_dir / "textures"
56+
textures_dir.mkdir(parents=True, exist_ok=True)
57+
58+
tasks = []
59+
60+
def add_task_for_path(file_path, strip=None):
61+
"""Add a copy task for a given file path, checking strip type if needed."""
62+
if not file_path: # Ensure the path is valid
63+
return
64+
65+
# Check if it's a SoundSequence and use strip.sound.filepath if available
66+
if strip and hasattr(strip, 'sound'):
67+
file_path = strip.sound.filepath
68+
69+
src = Path(bpy.path.abspath(file_path))
70+
if src.exists(): # Ensure the file exists
71+
dest = self.get_destination_folder(src, textures_dir) / src.name
72+
tasks.append(executor.submit(self.copy_file, src, dest))
73+
else:
74+
self.report({'WARNING'}, f"File not found: {src}")
75+
76+
with ThreadPoolExecutor() as executor:
77+
# Process all images used in shaders and textures
78+
for image in bpy.data.images:
79+
add_task_for_path(image.filepath)
80+
81+
# Process all media files in VSE
82+
for scene in bpy.data.scenes:
83+
if scene.sequence_editor:
84+
for strip in scene.sequence_editor.sequences_all:
85+
add_task_for_path(strip.filepath if hasattr(strip, 'filepath') else None, strip=strip)
86+
87+
# Process all cache files in object modifiers
88+
for obj in bpy.data.objects:
89+
for mod in obj.modifiers:
90+
if mod.type == 'MESH_SEQUENCE_CACHE' and mod.cache_file:
91+
add_task_for_path(mod.cache_file.filepath)
92+
93+
# Process results and print summary
94+
collected_files = {"TOTAL": 0}
95+
for future in as_completed(tasks):
96+
filename, success = future.result()
97+
if success:
98+
collected_files["TOTAL"] += 1
99+
100+
self.report({'INFO'}, f"Gathering Complete: {collected_files['TOTAL']} files collected.")
101+
return {'FINISHED'}
102+
103+
104+
def menu_func(self, context):
105+
self.layout.operator(GatherResourcesOperator.bl_idname, text="Gather Resources")
106+
107+
108+
def register():
109+
bpy.utils.register_class(GatherResourcesOperator)
110+
bpy.types.TOPBAR_MT_file_external_data.append(menu_func)
111+
112+
113+
def unregister():
114+
bpy.utils.unregister_class(GatherResourcesOperator)
115+
bpy.types.TOPBAR_MT_file_external_data.remove(menu_func)
116+
117+
118+
if __name__ == "__main__":
119+
register()

0 commit comments

Comments
 (0)