Refactor Blender addon structure and add explicit --zip CLI flag
Move operators and preferences out of __init__.py into dedicated modules. Fix cyclic import by using proper AddonPreferences pattern. Replace implicit .zip extension detection in CLI with explicit -z/--zip flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
12e6e2643b
commit
bb1de717c8
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
# <pep8 compliant>
|
# <pep8 compliant>
|
||||||
|
|
||||||
__version__ = '1.21'
|
__version__ = "1.21"
|
||||||
|
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Blender Asset Tracer",
|
"name": "Blender Asset Tracer",
|
||||||
@ -37,192 +37,40 @@ bl_info = {
|
|||||||
# Reset root module name if folder has an unexpected name
|
# Reset root module name if folder has an unexpected name
|
||||||
# (like "blender_asset_tracer-main" from zip-dl)
|
# (like "blender_asset_tracer-main" from zip-dl)
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ != "blender_asset_tracer":
|
if __name__ != "blender_asset_tracer":
|
||||||
sys.modules["blender_asset_tracer"] = sys.modules[__name__]
|
sys.modules["blender_asset_tracer"] = sys.modules[__name__]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator
|
|
||||||
from bpy_extras.io_utils import ExportHelper
|
|
||||||
_HAS_BPY = True
|
_HAS_BPY = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_HAS_BPY = False
|
_HAS_BPY = False
|
||||||
|
|
||||||
if _HAS_BPY:
|
if _HAS_BPY:
|
||||||
import zipfile
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path, PurePath
|
|
||||||
|
|
||||||
from blender_asset_tracer.pack import zipped
|
|
||||||
from blender_asset_tracer import blendfile
|
from blender_asset_tracer import blendfile
|
||||||
from . import preferences
|
from . import preferences, operators
|
||||||
|
|
||||||
# Match the CLI's default: skip dangling pointers gracefully instead of crashing.
|
# Match the CLI's default: skip dangling pointers gracefully instead of crashing.
|
||||||
# Production blend files often have references to missing linked libraries.
|
# Production blend files often have references to missing linked libraries.
|
||||||
blendfile.set_strict_pointer_mode(False)
|
blendfile.set_strict_pointer_mode(False)
|
||||||
|
|
||||||
class ExportBatPack(Operator, ExportHelper):
|
|
||||||
bl_idname = "export_bat.pack"
|
|
||||||
bl_label = "Export to Archive using BAT"
|
|
||||||
filename_ext = ".zip"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return bpy.data.is_saved
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
outfname = bpy.path.ensure_ext(self.filepath, ".zip")
|
|
||||||
self.report({'INFO'}, 'Executing ZipPacker ...')
|
|
||||||
|
|
||||||
with zipped.ZipPacker(
|
|
||||||
Path(bpy.data.filepath),
|
|
||||||
Path(bpy.data.filepath).parent,
|
|
||||||
str(self.filepath)) as packer:
|
|
||||||
packer.strategise()
|
|
||||||
packer.execute()
|
|
||||||
self.report({'INFO'}, 'Packing successful!')
|
|
||||||
|
|
||||||
with zipfile.ZipFile(str(self.filepath)) as inzip:
|
|
||||||
inzip.testzip()
|
|
||||||
|
|
||||||
self.report({'INFO'}, 'Written to %s' % outfname)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def open_folder(folderpath):
|
|
||||||
"""Open the folder at the path given with cmd relative to user's OS."""
|
|
||||||
from shutil import which
|
|
||||||
my_os = sys.platform
|
|
||||||
if my_os.startswith(('linux', 'freebsd')):
|
|
||||||
cmd = 'xdg-open'
|
|
||||||
elif my_os.startswith('win'):
|
|
||||||
cmd = 'explorer'
|
|
||||||
if not folderpath:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
cmd = 'open'
|
|
||||||
|
|
||||||
if not folderpath:
|
|
||||||
return
|
|
||||||
|
|
||||||
folderpath = str(folderpath)
|
|
||||||
if os.path.isfile(folderpath):
|
|
||||||
select = False
|
|
||||||
if my_os.startswith('win'):
|
|
||||||
cmd = 'explorer /select,'
|
|
||||||
select = True
|
|
||||||
elif my_os.startswith(('linux', 'freebsd')):
|
|
||||||
if which('nemo'):
|
|
||||||
cmd = 'nemo --no-desktop'
|
|
||||||
select = True
|
|
||||||
elif which('nautilus'):
|
|
||||||
cmd = 'nautilus --no-desktop'
|
|
||||||
select = True
|
|
||||||
if not select:
|
|
||||||
folderpath = os.path.dirname(folderpath)
|
|
||||||
|
|
||||||
folderpath = os.path.normpath(folderpath)
|
|
||||||
fullcmd = cmd.split() + [folderpath]
|
|
||||||
subprocess.Popen(fullcmd)
|
|
||||||
|
|
||||||
class BAT_OT_export_zip(Operator, ExportHelper):
|
|
||||||
"""Export current blendfile with hierarchy preservation"""
|
|
||||||
bl_label = "BAT Pack - keep hierarchy"
|
|
||||||
bl_idname = "bat.export_zip"
|
|
||||||
|
|
||||||
filename_ext = '.zip'
|
|
||||||
|
|
||||||
root_dir: bpy.props.StringProperty(
|
|
||||||
name="Root",
|
|
||||||
description='Top Level Folder of your project.'
|
|
||||||
'\nFor now Copy/Paste correct folder by hand if default is incorrect.'
|
|
||||||
'\n!!! Everything outside won\'t be zipped !!!',
|
|
||||||
)
|
|
||||||
|
|
||||||
use_zip: bpy.props.BoolProperty(
|
|
||||||
name="Output as ZIP",
|
|
||||||
description="If enabled, pack into a ZIP archive. If disabled, copy to a directory.",
|
|
||||||
default=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return bpy.data.is_saved
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
from blender_asset_tracer.pack import Packer
|
|
||||||
from blender_asset_tracer.pack.zipped import ZipPacker
|
|
||||||
|
|
||||||
bfile = Path(bpy.data.filepath)
|
|
||||||
project = Path(self.root_dir) if self.root_dir else bfile.parent
|
|
||||||
target = str(self.filepath)
|
|
||||||
|
|
||||||
if self.use_zip:
|
|
||||||
target = bpy.path.ensure_ext(target, ".zip")
|
|
||||||
packer_cls = ZipPacker
|
|
||||||
else:
|
|
||||||
if target.lower().endswith('.zip'):
|
|
||||||
target = target[:-4]
|
|
||||||
packer_cls = Packer
|
|
||||||
|
|
||||||
self.report({'INFO'}, 'Packing with hierarchy...')
|
|
||||||
|
|
||||||
with packer_cls(bfile, project, target,
|
|
||||||
keep_hierarchy=True) as packer:
|
|
||||||
packer.strategise()
|
|
||||||
packer.execute()
|
|
||||||
|
|
||||||
if self.use_zip:
|
|
||||||
with zipfile.ZipFile(target) as inzip:
|
|
||||||
inzip.testzip()
|
|
||||||
log_output = Path(tempfile.gettempdir(), 'README.txt')
|
|
||||||
with open(log_output, 'w') as log:
|
|
||||||
log.write("Packed with BAT (keep-hierarchy mode)")
|
|
||||||
log.write(f"\nBlend file: {bpy.data.filepath}")
|
|
||||||
with zipfile.ZipFile(target, 'a') as zipObj:
|
|
||||||
zipObj.write(log_output, log_output.name)
|
|
||||||
|
|
||||||
self.report({'INFO'}, 'Written to %s' % target)
|
|
||||||
open_folder(Path(target).parent)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def menu_func(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.separator()
|
|
||||||
layout.operator(ExportBatPack.bl_idname)
|
|
||||||
filepath = layout.operator(BAT_OT_export_zip.bl_idname)
|
|
||||||
|
|
||||||
try:
|
|
||||||
prefs = preferences.get_addon_prefs()
|
|
||||||
root_dir_env = None
|
|
||||||
if prefs.use_env_root:
|
|
||||||
root_dir_env = os.getenv('ZIP_ROOT')
|
|
||||||
if not root_dir_env:
|
|
||||||
root_dir_env = os.getenv('PROJECT_ROOT')
|
|
||||||
if not root_dir_env:
|
|
||||||
root_dir_env = prefs.root_default
|
|
||||||
filepath.root_dir = '' if root_dir_env is None else root_dir_env
|
|
||||||
except Exception:
|
|
||||||
filepath.root_dir = os.getenv('ZIP_ROOT') or os.getenv('PROJECT_ROOT') or ''
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
ExportBatPack,
|
preferences.BATPreferences,
|
||||||
BAT_OT_export_zip,
|
operators.ExportBatPack,
|
||||||
|
operators.BAT_OT_export_zip,
|
||||||
)
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
preferences.register()
|
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.register_class(cls)
|
bpy.utils.register_class(cls)
|
||||||
bpy.types.TOPBAR_MT_file_external_data.append(menu_func)
|
bpy.types.TOPBAR_MT_file_external_data.append(operators.menu_func)
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
bpy.types.TOPBAR_MT_file_external_data.remove(operators.menu_func)
|
||||||
for cls in classes:
|
for cls in classes:
|
||||||
bpy.utils.unregister_class(cls)
|
bpy.utils.unregister_class(cls)
|
||||||
preferences.unregister()
|
|
||||||
bpy.types.TOPBAR_MT_file_external_data.remove(menu_func)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
register()
|
register()
|
||||||
|
|||||||
@ -97,6 +97,14 @@ def add_parser(subparsers):
|
|||||||
"path structure under the target directory. Paths in blend files are "
|
"path structure under the target directory. Paths in blend files are "
|
||||||
"rewritten to relative paths within this structure.",
|
"rewritten to relative paths within this structure.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-z",
|
||||||
|
"--zip",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Force output as a ZIP archive, even when the target path does "
|
||||||
|
"not end in '.zip'. The '.zip' extension is appended automatically.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def cli_pack(args):
|
def cli_pack(args):
|
||||||
@ -156,12 +164,15 @@ def create_packer(
|
|||||||
|
|
||||||
packer = create_shamanpacker(bpath, ppath, target)
|
packer = create_shamanpacker(bpath, ppath, target)
|
||||||
|
|
||||||
elif target.lower().endswith(".zip"):
|
elif args.zip:
|
||||||
from blender_asset_tracer.pack import zipped
|
from blender_asset_tracer.pack import zipped
|
||||||
|
|
||||||
if args.compress:
|
if args.compress:
|
||||||
raise ValueError("ZIP packer does not support on-the-fly compression")
|
raise ValueError("ZIP packer does not support on-the-fly compression")
|
||||||
|
|
||||||
|
if not target.lower().endswith(".zip"):
|
||||||
|
target += ".zip"
|
||||||
|
|
||||||
packer = zipped.ZipPacker(
|
packer = zipped.ZipPacker(
|
||||||
bpath, ppath, target, noop=args.noop, relative_only=args.relative_only,
|
bpath, ppath, target, noop=args.noop, relative_only=args.relative_only,
|
||||||
keep_hierarchy=args.keep_hierarchy,
|
keep_hierarchy=args.keep_hierarchy,
|
||||||
|
|||||||
163
blender_asset_tracer/operators.py
Normal file
163
blender_asset_tracer/operators.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
|
||||||
|
from blender_asset_tracer.pack import zipped
|
||||||
|
|
||||||
|
|
||||||
|
class ExportBatPack(Operator, ExportHelper):
|
||||||
|
bl_idname = "export_bat.pack"
|
||||||
|
bl_label = "BAT - Zip pack (flat)"
|
||||||
|
filename_ext = ".zip"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return bpy.data.is_saved
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
outfname = bpy.path.ensure_ext(self.filepath, ".zip")
|
||||||
|
self.report({"INFO"}, "Executing ZipPacker ...")
|
||||||
|
|
||||||
|
with zipped.ZipPacker(
|
||||||
|
Path(bpy.data.filepath),
|
||||||
|
Path(bpy.data.filepath).parent,
|
||||||
|
str(self.filepath),
|
||||||
|
) as packer:
|
||||||
|
packer.strategise()
|
||||||
|
packer.execute()
|
||||||
|
self.report({"INFO"}, "Packing successful!")
|
||||||
|
|
||||||
|
with zipfile.ZipFile(str(self.filepath)) as inzip:
|
||||||
|
inzip.testzip()
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Written to %s" % outfname)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def open_folder(folderpath):
|
||||||
|
"""Open the folder at the path given with cmd relative to user's OS."""
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
|
my_os = sys.platform
|
||||||
|
if my_os.startswith(("linux", "freebsd")):
|
||||||
|
cmd = "xdg-open"
|
||||||
|
elif my_os.startswith("win"):
|
||||||
|
cmd = "explorer"
|
||||||
|
if not folderpath:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
cmd = "open"
|
||||||
|
|
||||||
|
if not folderpath:
|
||||||
|
return
|
||||||
|
|
||||||
|
folderpath = str(folderpath)
|
||||||
|
if os.path.isfile(folderpath):
|
||||||
|
select = False
|
||||||
|
if my_os.startswith("win"):
|
||||||
|
cmd = "explorer /select,"
|
||||||
|
select = True
|
||||||
|
elif my_os.startswith(("linux", "freebsd")):
|
||||||
|
if which("nemo"):
|
||||||
|
cmd = "nemo --no-desktop"
|
||||||
|
select = True
|
||||||
|
elif which("nautilus"):
|
||||||
|
cmd = "nautilus --no-desktop"
|
||||||
|
select = True
|
||||||
|
if not select:
|
||||||
|
folderpath = os.path.dirname(folderpath)
|
||||||
|
|
||||||
|
folderpath = os.path.normpath(folderpath)
|
||||||
|
fullcmd = cmd.split() + [folderpath]
|
||||||
|
subprocess.Popen(fullcmd)
|
||||||
|
|
||||||
|
|
||||||
|
class BAT_OT_export_zip(Operator, ExportHelper):
|
||||||
|
"""Export current blendfile with hierarchy preservation"""
|
||||||
|
|
||||||
|
bl_label = "BAT - Zip pack (keep hierarchy)"
|
||||||
|
bl_idname = "bat.export_zip"
|
||||||
|
|
||||||
|
filename_ext = ".zip"
|
||||||
|
|
||||||
|
root_dir: bpy.props.StringProperty(
|
||||||
|
name="Root",
|
||||||
|
description="Top Level Folder of your project."
|
||||||
|
"\nFor now Copy/Paste correct folder by hand if default is incorrect."
|
||||||
|
"\n!!! Everything outside won't be zipped !!!",
|
||||||
|
)
|
||||||
|
|
||||||
|
use_zip: bpy.props.BoolProperty(
|
||||||
|
name="Output as ZIP",
|
||||||
|
description="If enabled, pack into a ZIP archive. If disabled, copy to a directory.",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return bpy.data.is_saved
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
from blender_asset_tracer.pack import Packer
|
||||||
|
from blender_asset_tracer.pack.zipped import ZipPacker
|
||||||
|
|
||||||
|
bfile = Path(bpy.data.filepath)
|
||||||
|
project = Path(self.root_dir) if self.root_dir else bfile.parent
|
||||||
|
target = str(self.filepath)
|
||||||
|
|
||||||
|
if self.use_zip:
|
||||||
|
target = bpy.path.ensure_ext(target, ".zip")
|
||||||
|
packer_cls = ZipPacker
|
||||||
|
else:
|
||||||
|
if target.lower().endswith(".zip"):
|
||||||
|
target = target[:-4]
|
||||||
|
packer_cls = Packer
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Packing with hierarchy...")
|
||||||
|
|
||||||
|
with packer_cls(bfile, project, target, keep_hierarchy=True) as packer:
|
||||||
|
packer.strategise()
|
||||||
|
packer.execute()
|
||||||
|
|
||||||
|
if self.use_zip:
|
||||||
|
with zipfile.ZipFile(target) as inzip:
|
||||||
|
inzip.testzip()
|
||||||
|
log_output = Path(tempfile.gettempdir(), "README.txt")
|
||||||
|
with open(log_output, "w") as log:
|
||||||
|
log.write("Packed with BAT (keep-hierarchy mode)")
|
||||||
|
log.write(f"\nBlend file: {bpy.data.filepath}")
|
||||||
|
with zipfile.ZipFile(target, "a") as zipObj:
|
||||||
|
zipObj.write(log_output, log_output.name)
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Written to %s" % target)
|
||||||
|
open_folder(Path(target).parent)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def menu_func(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.separator()
|
||||||
|
layout.operator(ExportBatPack.bl_idname)
|
||||||
|
filepath = layout.operator(BAT_OT_export_zip.bl_idname)
|
||||||
|
|
||||||
|
try:
|
||||||
|
prefs = bpy.context.preferences.addons["blender_asset_tracer"].preferences
|
||||||
|
root_dir_env = None
|
||||||
|
if prefs.use_env_root:
|
||||||
|
root_dir_env = os.getenv("ZIP_ROOT")
|
||||||
|
if not root_dir_env:
|
||||||
|
root_dir_env = os.getenv("PROJECT_ROOT")
|
||||||
|
if not root_dir_env:
|
||||||
|
root_dir_env = prefs.root_default
|
||||||
|
filepath.root_dir = "" if root_dir_env is None else root_dir_env
|
||||||
|
except Exception:
|
||||||
|
filepath.root_dir = os.getenv("ZIP_ROOT") or os.getenv("PROJECT_ROOT") or ""
|
||||||
|
|
||||||
|
|
||||||
27
blender_asset_tracer/preferences.py
Normal file
27
blender_asset_tracer/preferences.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import AddonPreferences
|
||||||
|
from bpy.props import BoolProperty, StringProperty
|
||||||
|
|
||||||
|
|
||||||
|
class BATPreferences(AddonPreferences):
|
||||||
|
bl_idname = "blender_asset_tracer"
|
||||||
|
|
||||||
|
use_env_root: BoolProperty(
|
||||||
|
name="Use Environment Variable for Root",
|
||||||
|
description="Read the project root from ZIP_ROOT or PROJECT_ROOT environment variables",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
root_default: StringProperty(
|
||||||
|
name="Default Root",
|
||||||
|
description="Fallback project root when the environment variable is not set",
|
||||||
|
default="",
|
||||||
|
subtype="DIR_PATH",
|
||||||
|
)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.prop(self, "use_env_root")
|
||||||
|
layout.prop(self, "root_default")
|
||||||
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user