Compare commits

..

No commits in common. "main" and "v1.18" have entirely different histories.
main ... v1.18

68 changed files with 1154 additions and 2510 deletions

View File

@ -3,30 +3,6 @@
This file logs the changes that are actually interesting to users (new features, This file logs the changes that are actually interesting to users (new features,
changed functionality, fixed bugs). changed functionality, fixed bugs).
# Version 1.21 (2025-11-24)
- Require Python version 3.11 or newer. Versions up to Python 3.14 are supported.
- Skip packed blend files. BAT will assume that the packed file is self-contained, i.e. any asset used by a packed blend file should also be packed.
- Support Geometry Node simulation caches ([#92890](https://projects.blender.org/blender/blender-asset-tracer/pulls/92890)).
- Add a `py.typed` marker file, such that tooling like mypy know BAT has type annotations ([#92895](https://projects.blender.org/blender/blender-asset-tracer/pulls/92895)).
- Add support for getting individual array items ([#92898](https://projects.blender.org/blender/blender-asset-tracer/pulls/92898)) and iterating dynamic arrays (part of [#92890](https://projects.blender.org/blender/blender-asset-tracer/pulls/92890)).
- Support setting sub-properties via `block.field_set()` ([#92899](https://projects.blender.org/blender/blender-asset-tracer/pulls/92899)).
- Make `bat blocks` print the biggest block memory address as hexadecimal ([#92900](https://projects.blender.org/blender/blender-asset-tracer/pulls/92900)).
# Version 1.20 (2025-07-11)
- Add support for Blender 5.0 compositor node trees ([16c208bc8e13](https://projects.blender.org/blender/blender-asset-tracer/commit/16c208bc8e130c8b1233bdb411ecabdab19af3c5)).
- Make it possible to run BAT with `python -m blender_asset_tracer` ([6c42d06f0590](https://projects.blender.org/blender/blender-asset-tracer/commit/6c42d06f05909d4ac2096e84557d19dd93382f3a)).
- Add support for loading the file sub-version ([4c429e921228](https://projects.blender.org/blender/blender-asset-tracer/commit/4c429e921228259f47795f8ad913ad3eff8fac71)).
# Version 1.19 (2025-06-16)
- Add support for tracing dynamic paint caches ([#92889](https://projects.blender.org/blender/blender-asset-tracer/pulls/92889)).
- Add support for the large blendfile header blocks ([#92893](https://projects.blender.org/blender/blender-asset-tracer/pulls/92893)). This is necessary for compatibility with Blender 5.0.
- Drop support for Python 3.8.
# Version 1.18 (2024-01-11) # Version 1.18 (2024-01-11)
- When logging that there is no reader implemented for a certain data-block type, the filepath of the blend file that contains that data-block is now included in the message ([#92885](https://projects.blender.org/blender/blender-asset-tracer/pulls/92885)). - When logging that there is no reader implemented for a certain data-block type, the filepath of the blend file that contains that data-block is now included in the message ([#92885](https://projects.blender.org/blender/blender-asset-tracer/pulls/92885)).

View File

@ -1,4 +1,4 @@
# Blender Asset Tracer BAT🦇 (ADV fork) # Blender Asset Tracer BAT🦇
Script to manage assets with Blender. Script to manage assets with Blender.
@ -8,23 +8,15 @@ Blender Asset Tracer, a.k.a. BAT🦇, is the replacement of
Development is driven by choices explained in [T54125](https://developer.blender.org/T54125). Development is driven by choices explained in [T54125](https://developer.blender.org/T54125).
## Basic access to command line operations
The `cli.py` wrapper at the root of the project can be used to directly access the command line
tools, without requiring any setup involving `venv` and so on:
```
python3 path/to/repo/cli.py list path/to/blendfile.blend
```
## Setting up development environment ## Setting up development environment
First install [Poetry](https://python-poetry.org/). Because BAT has different
requirements than Poetry itself, it is recommended to install Poetry outside the
virtualenv you use for BAT. After that, run:
``` ```
poetry install --all-extras --all-groups python3.9 -m venv .venv
. ./.venv/bin/activate
pip install -U pip
pip install poetry black
poetry install
mypy --install-types
``` ```
@ -75,7 +67,6 @@ Mypy likes to see the return type of `__init__` methods explicitly declared as `
BAT can be used as a Python library to inspect the contents of blend files, without having to BAT can be used as a Python library to inspect the contents of blend files, without having to
open Blender itself. Here is an example showing how to determine the render engine used: open Blender itself. Here is an example showing how to determine the render engine used:
```python
#!/usr/bin/env python3.7 #!/usr/bin/env python3.7
import json import json
import sys import sys
@ -119,7 +110,6 @@ render_info = {
json.dump(render_info, sys.stdout, indent=4, sort_keys=True) json.dump(render_info, sys.stdout, indent=4, sort_keys=True)
print() print()
```
To understand the naming of the properties, look at Blender's `DNA_xxxx.h` files with struct To understand the naming of the properties, look at Blender's `DNA_xxxx.h` files with struct
definitions. It is those names that are accessed via `blender_asset_tracer.blendfile`. The definitions. It is those names that are accessed via `blender_asset_tracer.blendfile`. The
@ -173,6 +163,6 @@ index-servers =
pip install twine pip install twine
poetry build poetry build
poetry run twine check dist/blender_asset_tracer-1.21.tar.gz dist/blender_asset_tracer-1.21-*.whl twine check dist/blender_asset_tracer-1.18.tar.gz dist/blender_asset_tracer-1.18-*.whl
poetry run twine upload -r bat dist/blender_asset_tracer-1.21.tar.gz dist/blender_asset_tracer-1.21-*.whl twine upload -r bat dist/blender_asset_tracer-1.18.tar.gz dist/blender_asset_tracer-1.18-*.whl
``` ```

View File

@ -20,57 +20,4 @@
# <pep8 compliant> # <pep8 compliant>
__version__ = "1.21" __version__ = "1.18"
bl_info = {
"name": "Blender Asset Tracer",
"author": "Campbell Barton, Sybren A. St\u00fcvel, Lo\u00efc Charri\u00e8re, Cl\u00e9ment Ducarteron, Mario Hawat, Joseph Henry",
"version": (1, 21, 0),
"blender": (2, 80, 0),
"location": "File > External Data > BAT",
"description": "Utility for packing blend files",
"warning": "",
"wiki_url": "https://developer.blender.org/project/profile/79/",
"category": "Import-Export",
}
# Reset root module name if folder has an unexpected name
# (like "blender_asset_tracer-main" from zip-dl)
import sys
if __name__ != "blender_asset_tracer":
sys.modules["blender_asset_tracer"] = sys.modules[__name__]
try:
import bpy
_HAS_BPY = True
except ImportError:
_HAS_BPY = False
if _HAS_BPY:
from blender_asset_tracer import blendfile
from . import preferences, operators
# Match the CLI's default: skip dangling pointers gracefully instead of crashing.
# Production blend files often have references to missing linked libraries.
blendfile.set_strict_pointer_mode(False)
classes = (
preferences.BATPreferences,
operators.ExportBatPack,
operators.BAT_OT_export_zip,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_external_data.append(operators.menu_func)
def unregister():
bpy.types.TOPBAR_MT_file_external_data.remove(operators.menu_func)
for cls in classes:
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()

View File

@ -1,3 +0,0 @@
from blender_asset_tracer import cli
cli.cli_main()

View File

@ -122,7 +122,6 @@ class BlendFile:
self.filepath = path self.filepath = path
self.raw_filepath = path self.raw_filepath = path
self._is_modified = False self._is_modified = False
self.file_subversion = 0
self.fileobj = self._open_file(path, mode) self.fileobj = self._open_file(path, mode)
self.blocks = [] # type: BFBList self.blocks = [] # type: BFBList
@ -136,7 +135,7 @@ class BlendFile:
self.block_from_addr = {} # type: typing.Dict[int, BlendFileBlock] self.block_from_addr = {} # type: typing.Dict[int, BlendFileBlock]
self.header = header.BlendFileHeader(self.fileobj, self.raw_filepath) self.header = header.BlendFileHeader(self.fileobj, self.raw_filepath)
self.block_header_struct, self.block_header_fields = self.header.create_block_header_struct() self.block_header_struct = self.header.create_block_header_struct()
self._load_blocks() self._load_blocks()
def _open_file(self, path: pathlib.Path, mode: str) -> typing.IO[bytes]: def _open_file(self, path: pathlib.Path, mode: str) -> typing.IO[bytes]:
@ -170,8 +169,6 @@ class BlendFile:
if block.code == b"DNA1": if block.code == b"DNA1":
self.decode_structs(block) self.decode_structs(block)
elif block.code == b"GLOB":
self.decode_glob(block)
else: else:
self.fileobj.seek(block.size, os.SEEK_CUR) self.fileobj.seek(block.size, os.SEEK_CUR)
@ -359,25 +356,6 @@ class BlendFile:
dna_struct.append_field(field) dna_struct.append_field(field)
dna_offset += dna_size dna_offset += dna_size
def decode_glob(self, block: "BlendFileBlock") -> None:
"""Partially decode the GLOB block to get the file sub-version."""
# Before this, the subversion didn't exist in 'FileGlobal'.
if self.header.version <= 242:
self.file_subversion = 0
return
# GLOB can appear in the file before DNA1, and so we cannot use DNA to
# parse the fields.
# The subversion is always the `short` at offset 4.
# block_data = io.BytesIO(block.raw_data())
endian = self.header.endian
self.fileobj.seek(4, os.SEEK_CUR) # Skip the next 4 bytes.
self.file_subversion = endian.read_short(self.fileobj)
# Skip to the next block.
self.fileobj.seek(block.file_offset + block.size, os.SEEK_SET)
def abspath(self, relpath: bpathlib.BlendPath) -> bpathlib.BlendPath: def abspath(self, relpath: bpathlib.BlendPath) -> bpathlib.BlendPath:
"""Construct an absolute path from a blendfile-relative path.""" """Construct an absolute path from a blendfile-relative path."""
@ -447,12 +425,6 @@ class BlendFileBlock:
old_structure = struct.Struct(b"4sI") old_structure = struct.Struct(b"4sI")
"""old blend files ENDB block structure""" """old blend files ENDB block structure"""
# Explicitly annotate to avoid `Any` from `.unpack()`.
size: int
addr_old: int
sdna_index: int
count: int
def __init__(self, bfile: BlendFile) -> None: def __init__(self, bfile: BlendFile) -> None:
self.bfile = bfile self.bfile = bfile
@ -483,13 +455,23 @@ class BlendFileBlock:
self.code = b"ENDB" self.code = b"ENDB"
return return
blockheader = bfile.block_header_fields(*header_struct.unpack(data)) # header size can be 8, 20, or 24 bytes long
self.code = self.endian.read_data0(blockheader.code) # 8: old blend files ENDB block (exception)
# 20: normal headers 32 bit platform
# 24: normal headers 64 bit platform
if len(data) <= 15:
self.log.debug("interpreting block as old-style ENB block")
blockheader = self.old_structure.unpack(data)
self.code = self.endian.read_data0(blockheader[0])
return
blockheader = header_struct.unpack(data)
self.code = self.endian.read_data0(blockheader[0])
if self.code != b"ENDB": if self.code != b"ENDB":
self.size = blockheader.len self.size = blockheader[1]
self.addr_old = blockheader.old self.addr_old = blockheader[2]
self.sdna_index = blockheader.SDNAnr self.sdna_index = blockheader[3]
self.count = blockheader.nr self.count = blockheader[4]
self.file_offset = bfile.fileobj.tell() self.file_offset = bfile.fileobj.tell()
def __repr__(self) -> str: def __repr__(self) -> str:
@ -596,7 +578,6 @@ class BlendFileBlock:
null_terminated=True, null_terminated=True,
as_str=False, as_str=False,
return_field=False, return_field=False,
array_index=0,
) -> typing.Any: ) -> typing.Any:
"""Read a property and return the value. """Read a property and return the value.
@ -613,20 +594,8 @@ class BlendFileBlock:
(assumes UTF-8 encoding). (assumes UTF-8 encoding).
:param return_field: When True, returns tuple (dna.Field, value). :param return_field: When True, returns tuple (dna.Field, value).
Otherwise just returns the value. Otherwise just returns the value.
:param array_index: If the property is an array, this determines the
index of the returned item from that array. Also see
`blendfile.iterators.dynamic_array()` for iterating such arrays.
""" """
file_offset = self.file_offset self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
if array_index:
if not (0 <= array_index < self.count):
raise IndexError(
"Invalid 'array_index' for file-block. "
f"Expected int value in range 0-{self.count - 1}, got {array_index}."
)
file_offset += array_index * self.dna_type.size
self.bfile.fileobj.seek(file_offset, os.SEEK_SET)
dna_struct = self.bfile.structs[self.sdna_index] dna_struct = self.bfile.structs[self.sdna_index]
field, value = dna_struct.field_get( field, value = dna_struct.field_get(
@ -646,8 +615,8 @@ class BlendFileBlock:
self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET) self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
return self.bfile.fileobj.read(self.size) return self.bfile.fileobj.read(self.size)
def as_bytes_string(self) -> bytes: def as_string(self) -> str:
"""Interpret the bytes of this datablock as null-terminated string of raw bytes.""" """Interpret the bytes of this datablock as null-terminated utf8 string."""
the_bytes = self.raw_data() the_bytes = self.raw_data()
try: try:
first_null = the_bytes.index(0) first_null = the_bytes.index(0)
@ -655,11 +624,6 @@ class BlendFileBlock:
pass pass
else: else:
the_bytes = the_bytes[:first_null] the_bytes = the_bytes[:first_null]
return the_bytes
def as_string(self) -> str:
"""Interpret the bytes of this datablock as null-terminated utf8 string."""
the_bytes = self.as_bytes_string()
return the_bytes.decode() return the_bytes.decode()
def get_recursive_iter( def get_recursive_iter(
@ -726,7 +690,7 @@ class BlendFileBlock:
hsh = zlib.adler32(str(value).encode(), hsh) hsh = zlib.adler32(str(value).encode(), hsh)
return hsh return hsh
def set(self, path: dna.FieldPath, value): def set(self, path: bytes, value):
dna_struct = self.bfile.structs[self.sdna_index] dna_struct = self.bfile.structs[self.sdna_index]
self.bfile.mark_modified() self.bfile.mark_modified()
self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET) self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
@ -835,13 +799,9 @@ class BlendFileBlock:
def __getitem__(self, path: dna.FieldPath): def __getitem__(self, path: dna.FieldPath):
return self.get(path) return self.get(path)
def __setitem__(self, item: dna.FieldPath, value) -> None: def __setitem__(self, item: bytes, value) -> None:
self.set(item, value) self.set(item, value)
def has_field(self, name: bytes) -> bool:
dna_struct = self.bfile.structs[self.sdna_index]
return dna_struct.has_field(name)
def keys(self) -> typing.Iterator[bytes]: def keys(self) -> typing.Iterator[bytes]:
"""Generator, yields all field names of this block.""" """Generator, yields all field names of this block."""
return (f.name.name_only for f in self.dna_type.fields) return (f.name.name_only for f in self.dna_type.fields)

View File

@ -260,7 +260,6 @@ class Struct:
b"short": endian.read_short, b"short": endian.read_short,
b"uint64_t": endian.read_ulong, b"uint64_t": endian.read_ulong,
b"float": endian.read_float, b"float": endian.read_float,
b"int8_t": endian.read_int8,
} }
try: try:
simple_reader = simple_readers[dna_type.dna_type_id] simple_reader = simple_readers[dna_type.dna_type_id]
@ -310,7 +309,7 @@ class Struct:
self, self,
file_header: header.BlendFileHeader, file_header: header.BlendFileHeader,
fileobj: typing.IO[bytes], fileobj: typing.IO[bytes],
path: FieldPath, path: bytes,
value: typing.Any, value: typing.Any,
): ):
"""Write a value to the blend file. """Write a value to the blend file.
@ -319,6 +318,7 @@ class Struct:
struct on disk (e.g. the start of the BlendFileBlock containing the struct on disk (e.g. the start of the BlendFileBlock containing the
data). data).
""" """
assert isinstance(path, bytes), "path should be bytes, but is %s" % type(path)
field, offset = self.field_from_path(file_header.pointer_size, path) field, offset = self.field_from_path(file_header.pointer_size, path)

View File

@ -28,7 +28,6 @@ import typing
class EndianIO: class EndianIO:
# TODO(Sybren): note as UCHAR: struct.Struct = None and move actual structs to LittleEndianTypes # TODO(Sybren): note as UCHAR: struct.Struct = None and move actual structs to LittleEndianTypes
UCHAR = struct.Struct(b"<B") UCHAR = struct.Struct(b"<B")
SINT8 = struct.Struct(b"<b")
USHORT = struct.Struct(b"<H") USHORT = struct.Struct(b"<H")
USHORT2 = struct.Struct(b"<HH") # two shorts in a row USHORT2 = struct.Struct(b"<HH") # two shorts in a row
SSHORT = struct.Struct(b"<h") SSHORT = struct.Struct(b"<h")
@ -63,14 +62,6 @@ class EndianIO:
def write_char(cls, fileobj: typing.IO[bytes], value: int): def write_char(cls, fileobj: typing.IO[bytes], value: int):
return cls._write(fileobj, cls.UCHAR, value) return cls._write(fileobj, cls.UCHAR, value)
@classmethod
def read_int8(cls, fileobj: typing.IO[bytes]):
return cls._read(fileobj, cls.SINT8)
@classmethod
def write_int8(cls, fileobj: typing.IO[bytes], value: int):
return cls._write(fileobj, cls.SINT8, value)
@classmethod @classmethod
def read_ushort(cls, fileobj: typing.IO[bytes]): def read_ushort(cls, fileobj: typing.IO[bytes]):
return cls._read(fileobj, cls.USHORT) return cls._read(fileobj, cls.USHORT)
@ -129,20 +120,6 @@ class EndianIO:
return cls.read_ulong(fileobj) return cls.read_ulong(fileobj)
raise ValueError("unsupported pointer size %d" % pointer_size) raise ValueError("unsupported pointer size %d" % pointer_size)
@classmethod
def parse_pointer(cls, pointer_data: bytes):
"""Parse bytes as a pointer value."""
pointer_size = len(pointer_data)
try:
typestruct = {
4: cls.UINT,
8: cls.ULONG,
}[pointer_size]
except KeyError:
raise ValueError("unsupported pointer size %d" % pointer_size)
return typestruct.unpack(pointer_data)[0]
@classmethod @classmethod
def write_pointer(cls, fileobj: typing.IO[bytes], pointer_size: int, value: int): def write_pointer(cls, fileobj: typing.IO[bytes], pointer_size: int, value: int):
"""Write a pointer to a file.""" """Write a pointer to a file."""
@ -204,17 +181,17 @@ class EndianIO:
return fileobj.write(to_write) return fileobj.write(to_write)
@classmethod @classmethod
def read_bytes0(cls, fileobj: typing.IO[bytes], length: int) -> bytes: def read_bytes0(cls, fileobj, length):
data = fileobj.read(length) data = fileobj.read(length)
return cls.read_data0(data) return cls.read_data0(data)
@classmethod @classmethod
def read_data0_offset(cls, data: bytes, offset: int) -> bytes: def read_data0_offset(cls, data, offset):
add = data.find(b"\0", offset) - offset add = data.find(b"\0", offset) - offset
return data[offset : offset + add] return data[offset : offset + add]
@classmethod @classmethod
def read_data0(cls, data: bytes) -> bytes: def read_data0(cls, data):
add = data.find(b"\0") add = data.find(b"\0")
if add < 0: if add < 0:
return data return data
@ -230,7 +207,6 @@ class EndianIO:
""" """
return { return {
b"char": cls.write_char, b"char": cls.write_char,
b"int8": cls.write_int8,
b"ushort": cls.write_ushort, b"ushort": cls.write_ushort,
b"short": cls.write_short, b"short": cls.write_short,
b"uint": cls.write_uint, b"uint": cls.write_uint,
@ -246,7 +222,6 @@ class LittleEndianTypes(EndianIO):
class BigEndianTypes(LittleEndianTypes): class BigEndianTypes(LittleEndianTypes):
UCHAR = struct.Struct(b">B") UCHAR = struct.Struct(b">B")
SINT8 = struct.Struct(b">b")
USHORT = struct.Struct(b">H") USHORT = struct.Struct(b">H")
USHORT2 = struct.Struct(b">HH") # two shorts in a row USHORT2 = struct.Struct(b">HH") # two shorts in a row
SSHORT = struct.Struct(b">h") SSHORT = struct.Struct(b">h")

View File

@ -19,8 +19,6 @@
# (c) 2009, At Mind B.V. - Jeroen Bakker # (c) 2009, At Mind B.V. - Jeroen Bakker
# (c) 2014, Blender Foundation - Campbell Barton # (c) 2014, Blender Foundation - Campbell Barton
# (c) 2018, Blender Foundation - Sybren A. Stüvel # (c) 2018, Blender Foundation - Sybren A. Stüvel
from dataclasses import dataclass
import logging import logging
import os import os
import pathlib import pathlib
@ -32,151 +30,58 @@ from . import dna_io, exceptions
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@dataclass
class BHead4:
code: bytes
len: int
old: int
SDNAnr: int
nr: int
@dataclass
class SmallBHead8:
code: bytes
len: int
old: int
SDNAnr: int
nr: int
@dataclass
class LargeBHead8:
code: bytes
SDNAnr: int
old: int
len: int
nr: int
class BlendFileHeader: class BlendFileHeader:
""" """
BlendFileHeader represents the first 12-17 bytes of a blend file. BlendFileHeader represents the first 12 bytes of a blend file.
It contains information about the hardware architecture, which is relevant It contains information about the hardware architecture, which is relevant
to the structure of the rest of the file. to the structure of the rest of the file.
""" """
magic: bytes structure = struct.Struct(b"7s1s1s3s")
file_format_version: int
pointer_size: int
is_little_endian: bool
endian: typing.Type[dna_io.EndianIO]
endian_str: bytes
def __init__(self, fileobj: typing.IO[bytes], path: pathlib.Path) -> None: def __init__(self, fileobj: typing.IO[bytes], path: pathlib.Path) -> None:
log.debug("reading blend-file-header %s", path) log.debug("reading blend-file-header %s", path)
fileobj.seek(0, os.SEEK_SET) fileobj.seek(0, os.SEEK_SET)
header = fileobj.read(self.structure.size)
values = self.structure.unpack(header)
bytes_0_6 = fileobj.read(7) self.magic = values[0]
if bytes_0_6 != b'BLENDER':
raise exceptions.BlendFileError("invalid first bytes %r" % bytes_0_6, path)
self.magic = bytes_0_6
byte_7 = fileobj.read(1) pointer_size_id = values[1]
is_legacy_header = byte_7 in (b'_', b'-') if pointer_size_id == b"-":
if is_legacy_header: self.pointer_size = 8
self.file_format_version = 0 elif pointer_size_id == b"_":
if byte_7 == b'_':
self.pointer_size = 4 self.pointer_size = 4
elif byte_7 == b'-':
self.pointer_size = 8
else: else:
raise exceptions.BlendFileError("invalid pointer size %r" % byte_7, path) raise exceptions.BlendFileError(
byte_8 = fileobj.read(1) "invalid pointer size %r" % pointer_size_id, path
if byte_8 == b'v': )
self.is_little_endian = True
elif byte_8 == b'V':
self.is_little_endian = False
else:
raise exceptions.BlendFileError("invalid endian indicator %r" % byte_8, path)
bytes_9_11 = fileobj.read(3)
self.version = int(bytes_9_11)
else:
byte_8 = fileobj.read(1)
header_size = int(byte_7 + byte_8)
if header_size != 17:
raise exceptions.BlendFileError("unknown file header size %d" % header_size, path)
byte_9 = fileobj.read(1)
if byte_9 != b'-':
raise exceptions.BlendFileError("invalid file header", path)
self.pointer_size = 8
byte_10_11 = fileobj.read(2)
self.file_format_version = int(byte_10_11)
if self.file_format_version != 1:
raise exceptions.BlendFileError("unsupported file format version %r" % self.file_format_version, path)
byte_12 = fileobj.read(1)
if byte_12 != b'v':
raise exceptions.BlendFileError("invalid file header", path)
self.is_little_endian = True
byte_13_16 = fileobj.read(4)
self.version = int(byte_13_16)
if self.is_little_endian: endian_id = values[2]
self.endian_str = b'<' if endian_id == b"v":
self.endian = dna_io.LittleEndianTypes self.endian = dna_io.LittleEndianTypes
else: self.endian_str = b"<" # indication for struct.Struct()
self.endian_str = b'>' elif endian_id == b"V":
self.endian = dna_io.BigEndianTypes self.endian = dna_io.BigEndianTypes
self.endian_str = b">" # indication for struct.Struct()
else:
raise exceptions.BlendFileError(
"invalid endian indicator %r" % endian_id, path
)
def create_block_header_struct(self) -> typing.Tuple[struct.Struct, typing.Type[typing.Union[BHead4, SmallBHead8, LargeBHead8]]]: version_id = values[3]
""" self.version = int(version_id)
Returns a Struct instance for parsing data block headers and a corresponding
Python class for accessing the right members. Ddepending on the .blend file,
the order of the data members in the block header may be different.
"""
assert self.file_format_version in (0, 1)
if self.file_format_version == 1:
header_struct = struct.Struct(b''.join((
self.endian_str,
# LargeBHead8.code
b'4s',
# LargeBHead8.SDNAnr
b'i',
# LargeBHead8.old
b'Q',
# LargeBHead8.len
b'q',
# LargeBHead8.nr
b'q',
)))
return header_struct, LargeBHead8
if self.pointer_size == 4: def create_block_header_struct(self) -> struct.Struct:
header_struct = struct.Struct(b''.join(( """Create a Struct instance for parsing data block headers."""
return struct.Struct(
b"".join(
(
self.endian_str, self.endian_str,
# BHead4.code b"4sI",
b'4s', b"I" if self.pointer_size == 4 else b"Q",
# BHead4.len b"II",
b'i', )
# BHead4.old )
b'I', )
# BHead4.SDNAnr
b'i',
# BHead4.nr
b'i',
)))
return header_struct, BHead4
assert self.pointer_size == 8
header_struct = struct.Struct(b''.join((
self.endian_str,
# SmallBHead8.code
b'4s',
# SmallBHead8.len
b'i',
# SmallBHead8.old
b'Q',
# SmallBHead8.SDNAnr
b'i',
# SmallBHead8.nr
b'i',
)))
return header_struct, SmallBHead8

View File

@ -20,7 +20,6 @@
# (c) 2014, Blender Foundation - Campbell Barton # (c) 2014, Blender Foundation - Campbell Barton
# (c) 2018, Blender Foundation - Sybren A. Stüvel # (c) 2018, Blender Foundation - Sybren A. Stüvel
import typing import typing
import copy
from blender_asset_tracer import cdefs from blender_asset_tracer import cdefs
from . import BlendFileBlock from . import BlendFileBlock
@ -71,27 +70,3 @@ def modifiers(object_block: BlendFileBlock) -> typing.Iterator[BlendFileBlock]:
# 'ob->modifiers[...]' # 'ob->modifiers[...]'
mods = object_block.get_pointer((b"modifiers", b"first")) mods = object_block.get_pointer((b"modifiers", b"first"))
yield from listbase(mods, next_path=(b"modifier", b"next")) yield from listbase(mods, next_path=(b"modifier", b"next"))
def dynamic_array(block: BlendFileBlock) -> typing.Iterator[BlendFileBlock]:
"""
Generator that yields each element of a dynamic array as a separate block.
Dynamic arrays are multiple contiguous elements accessed via a single
pointer. BAT interprets these as a single data block, making it hard to
access individual elements. This function divides the array into individual
blocks by creating modified copies of the original block.
See `some_block.get(b'name', array_index)` if you want to access elements by
index (instead of iterating).
"""
element_size = block.dna_type.size
sub_block = copy.copy(block)
sub_block.size = element_size
for i in range(block.count):
# When sub_block's data is read, it'll be read from this offset in the blend file.
sub_block.file_offset = block.file_offset + i * element_size
yield sub_block

View File

@ -46,20 +46,11 @@ eModifierType_WeightVGEdit = 36
eModifierType_WeightVGMix = 37 eModifierType_WeightVGMix = 37
eModifierType_WeightVGProximity = 38 eModifierType_WeightVGProximity = 38
eModifierType_Ocean = 39 eModifierType_Ocean = 39
eModifierType_DynamicPaint = 40
eModifierType_MeshCache = 46 eModifierType_MeshCache = 46
eModifierType_MeshSequenceCache = 52 eModifierType_MeshSequenceCache = 52
eModifierType_Fluid = 56 eModifierType_Fluid = 56
eModifierType_Nodes = 57 eModifierType_Nodes = 57
# NodesModifierBakeFlag
NODES_MODIFIER_BAKE_CUSTOM_PATH = 1 << 1
# NodesModifierBakeTarget
NODES_MODIFIER_BAKE_TARGET_INHERIT = 0
NODES_MODIFIER_BAKE_TARGET_PACKED = 1
NODES_MODIFIER_BAKE_TARGET_DISK = 2
# DNA_particle_types.h # DNA_particle_types.h
PART_DRAW_OB = 7 PART_DRAW_OB = 7
PART_DRAW_GR = 8 PART_DRAW_GR = 8

View File

@ -112,7 +112,7 @@ def cli_blocks(args):
# From the blocks of the most space-using category, the biggest block. # From the blocks of the most space-using category, the biggest block.
biggest_block = sorted(infos[0].blocks, key=lambda blck: blck.size, reverse=True)[0] biggest_block = sorted(infos[0].blocks, key=lambda blck: blck.size, reverse=True)[0]
print( print(
"Biggest %s block is %s at address 0x%x" "Biggest %s block is %s at address %s"
% ( % (
block_key(biggest_block), block_key(biggest_block),
common.humanize_bytes(biggest_block.size), common.humanize_bytes(biggest_block.size),

21
blender_asset_tracer/cli/pack.py Executable file → Normal file
View File

@ -88,15 +88,6 @@ def add_parser(subparsers):
help="Only pack assets that are referred to with a relative path (e.g. " help="Only pack assets that are referred to with a relative path (e.g. "
"starting with `//`.", "starting with `//`.",
) )
parser.add_argument(
"--keep-hierarchy",
default=False,
action="store_true",
help="Preserve the full filesystem directory hierarchy in the pack. "
"All files (including the blend file) are placed at their absolute "
"path structure under the target directory. Paths in blend files are "
"rewritten to relative paths within this structure.",
)
def cli_pack(args): def cli_pack(args):
@ -128,9 +119,6 @@ def create_packer(
if args.relative_only: if args.relative_only:
raise ValueError("S3 uploader does not support the --relative-only option") raise ValueError("S3 uploader does not support the --relative-only option")
if args.keep_hierarchy:
raise ValueError("S3 uploader does not support the --keep-hierarchy option")
packer = create_s3packer(bpath, ppath, pathlib.PurePosixPath(target)) packer = create_s3packer(bpath, ppath, pathlib.PurePosixPath(target))
elif ( elif (
@ -149,11 +137,6 @@ def create_packer(
"Shaman uploader does not support the --relative-only option" "Shaman uploader does not support the --relative-only option"
) )
if args.keep_hierarchy:
raise ValueError(
"Shaman uploader does not support the --keep-hierarchy option"
)
packer = create_shamanpacker(bpath, ppath, target) packer = create_shamanpacker(bpath, ppath, target)
elif target.lower().endswith(".zip"): elif target.lower().endswith(".zip"):
@ -163,8 +146,7 @@ def create_packer(
raise ValueError("ZIP packer does not support on-the-fly compression") raise ValueError("ZIP packer does not support on-the-fly compression")
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,
) )
else: else:
packer = pack.Packer( packer = pack.Packer(
@ -174,7 +156,6 @@ def create_packer(
noop=args.noop, noop=args.noop,
compress=args.compress, compress=args.compress,
relative_only=args.relative_only, relative_only=args.relative_only,
keep_hierarchy=args.keep_hierarchy,
) )
if args.exclude: if args.exclude:

View File

@ -1,163 +0,0 @@
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 ""

11
blender_asset_tracer/pack/__init__.py Executable file → Normal file
View File

@ -103,7 +103,6 @@ class Packer:
noop=False, noop=False,
compress=False, compress=False,
relative_only=False, relative_only=False,
keep_hierarchy=False,
) -> None: ) -> None:
self.blendfile = bfile self.blendfile = bfile
self.project = project self.project = project
@ -112,7 +111,6 @@ class Packer:
self.noop = noop self.noop = noop
self.compress = compress self.compress = compress
self.relative_only = relative_only self.relative_only = relative_only
self.keep_hierarchy = keep_hierarchy
self._aborted = threading.Event() self._aborted = threading.Event()
self._abort_lock = threading.RLock() self._abort_lock = threading.RLock()
self._abort_reason = "" self._abort_reason = ""
@ -243,9 +241,6 @@ class Packer:
# network shares mapped to Windows drive letters back to their UNC # network shares mapped to Windows drive letters back to their UNC
# notation. Only resolving one but not the other (which can happen # notation. Only resolving one but not the other (which can happen
# with the abosolute() call above) can cause errors. # with the abosolute() call above) can cause errors.
if self.keep_hierarchy:
bfile_pp = self._target_path / bpathlib.strip_root(bfile_path)
else:
bfile_pp = self._target_path / bfile_path.relative_to( bfile_pp = self._target_path / bfile_path.relative_to(
bpathlib.make_absolute(self.project) bpathlib.make_absolute(self.project)
) )
@ -340,9 +335,6 @@ class Packer:
self._new_location_paths.add(asset_path) self._new_location_paths.add(asset_path)
else: else:
log.debug("%s can keep using %s", bfile_path, usage.asset_path) log.debug("%s can keep using %s", bfile_path, usage.asset_path)
if self.keep_hierarchy:
asset_pp = self._target_path / bpathlib.strip_root(asset_path)
else:
asset_pp = self._target_path / asset_path.relative_to(self.project) asset_pp = self._target_path / asset_path.relative_to(self.project)
act.new_path = asset_pp act.new_path = asset_pp
@ -354,9 +346,6 @@ class Packer:
assert isinstance(act, AssetAction) assert isinstance(act, AssetAction)
relpath = bpathlib.strip_root(path) relpath = bpathlib.strip_root(path)
if self.keep_hierarchy:
act.new_path = pathlib.Path(self._target_path, relpath)
else:
act.new_path = pathlib.Path(self._target_path, "_outside_project", relpath) act.new_path = pathlib.Path(self._target_path, "_outside_project", relpath)
def _group_rewrites(self) -> None: def _group_rewrites(self) -> None:

View File

@ -1,27 +0,0 @@
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")

View File

@ -119,7 +119,6 @@ def image(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]
@dna_code("LI") @dna_code("LI")
@skip_packed
def library(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]: def library(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Library data blocks.""" """Library data blocks."""
path, field = block.get(b"name", return_field=True) path, field = block.get(b"name", return_field=True)

View File

@ -90,14 +90,7 @@ def _expand_generic_mtex(block: blendfile.BlendFileBlock):
def _expand_generic_nodetree(block: blendfile.BlendFileBlock): def _expand_generic_nodetree(block: blendfile.BlendFileBlock):
if block.dna_type.dna_type_id == b"ID": assert block.dna_type.dna_type_id == b"bNodeTree"
# This is a placeholder for a linked node tree.
yield block
return
assert (
block.dna_type.dna_type_id == b"bNodeTree"
), f"Expected bNodeTree, got {block.dna_type.dna_type_id.decode()})"
nodes = block.get_pointer((b"nodes", b"first")) nodes = block.get_pointer((b"nodes", b"first"))
@ -152,10 +145,6 @@ def _expand_generic_idprops(block: blendfile.BlendFileBlock):
def _expand_generic_nodetree_id(block: blendfile.BlendFileBlock): def _expand_generic_nodetree_id(block: blendfile.BlendFileBlock):
if block.bfile.header.version >= 500 and block.bfile.file_subversion >= 4:
# Introduced in Blender 5.0, commit bd61e69be5a7c96f1e5da1c86aafc17b839e049f
block_ntree = block.get_pointer(b"compositing_node_group", None)
else:
block_ntree = block.get_pointer(b"nodetree", None) block_ntree = block.get_pointer(b"nodetree", None)
if block_ntree is not None: if block_ntree is not None:
yield from _expand_generic_nodetree(block_ntree) yield from _expand_generic_nodetree(block_ntree)

View File

@ -22,12 +22,10 @@
The modifier_xxx() functions all yield result.BlockUsage objects for external The modifier_xxx() functions all yield result.BlockUsage objects for external
files used by the modifiers. files used by the modifiers.
""" """
import logging import logging
import typing import typing
from blender_asset_tracer import blendfile, bpathlib, cdefs from blender_asset_tracer import blendfile, bpathlib, cdefs
from blender_asset_tracer.blendfile import iterators
from . import result from . import result
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -319,95 +317,3 @@ def modifier_cloth(
yield from _walk_point_cache( yield from _walk_point_cache(
ctx, block_name, modifier.bfile, pointcache, cdefs.PTCACHE_EXT ctx, block_name, modifier.bfile, pointcache, cdefs.PTCACHE_EXT
) )
@mod_handler(cdefs.eModifierType_DynamicPaint)
def modifier_dynamic_paint(
ctx: ModifierContext, modifier: blendfile.BlendFileBlock, block_name: bytes
) -> typing.Iterator[result.BlockUsage]:
my_log = log.getChild("modifier_dynamic_paint")
canvas_settings = modifier.get_pointer(b"canvas")
if canvas_settings is None:
my_log.debug(
"Modifier %r (%r) has no canvas_settings",
modifier[b"modifier", b"name"],
block_name,
)
return
surfaces = canvas_settings.get_pointer((b"surfaces", b"first"))
for surf_idx, surface in enumerate(iterators.listbase(surfaces)):
surface_block_name = block_name + b".canvas_settings.surfaces[%d]" % (surf_idx)
point_cache = surface.get_pointer(b"pointcache")
if point_cache is None:
my_log.debug(
"Surface %r (%r) has no pointcache",
surface[b"surface", b"name"],
surface_block_name,
)
continue
yield from _walk_point_cache(
ctx, surface_block_name, modifier.bfile, point_cache, cdefs.PTCACHE_EXT
)
@mod_handler(cdefs.eModifierType_Nodes)
def modifier_nodes(
ctx: ModifierContext, modifier: blendfile.BlendFileBlock, block_name: bytes
) -> typing.Iterator[result.BlockUsage]:
if not modifier.has_field(b"simulation_bake_directory"):
return
mod_directory_ptr, mod_directory_field = modifier.get(
b"simulation_bake_directory", return_field=True
)
bakes = modifier.get_pointer(b"bakes")
if not bakes:
return
mod_bake_target = modifier.get(b"bake_target")
for bake_idx, bake in enumerate(iterators.dynamic_array(bakes)):
# Check for packed data.
bake_target = bake.get(b"bake_target")
if bake_target == cdefs.NODES_MODIFIER_BAKE_TARGET_INHERIT:
bake_target = mod_bake_target
if bake_target == cdefs.NODES_MODIFIER_BAKE_TARGET_PACKED:
# This data is packed in the blend file, it's not a dependency to trace.
continue
flag = bake.get(b"flag")
use_custom_directory = bool(flag & cdefs.NODES_MODIFIER_BAKE_CUSTOM_PATH)
if use_custom_directory:
bake_directory_ptr, bake_directory_field = bake.get(
b"directory", return_field=True
)
directory_ptr = bake_directory_ptr
field = bake_directory_field
block = bake
else:
directory_ptr = mod_directory_ptr
field = mod_directory_field
block = modifier
if not directory_ptr:
continue
directory = bake.bfile.dereference_pointer(directory_ptr)
if not directory:
continue
bpath = bpathlib.BlendPath(directory.as_bytes_string())
bake_block_name = block_name + b".bakes[%d]" % bake_idx
yield result.BlockUsage(
block,
bpath,
block_name=bake_block_name,
path_full_field=field,
is_sequence=True,
)

29
cli.py
View File

@ -1,29 +0,0 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Copyright (C) 2014-2018 Blender Foundation
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
import blender_asset_tracer.cli
if __name__ == "__main__":
print("\n\n *** Running {} *** \n".format(__file__))
blender_asset_tracer.cli.cli_main()

View File

@ -24,9 +24,9 @@ copyright = '2018, Sybren A. Stüvel'
author = 'Sybren A. Stüvel' author = 'Sybren A. Stüvel'
# The short X.Y version # The short X.Y version
version = '1.21' version = '1.18'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '1.21' release = '1.18'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------

2426
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "blender-asset-tracer" name = "blender-asset-tracer"
version = "1.21" version = "1.18"
homepage = 'https://developer.blender.org/project/profile/79/' homepage = 'https://developer.blender.org/project/profile/79/'
description = "BAT parses Blend files and produces dependency information. After installation run `bat --help`" description = "BAT parses Blend files and produces dependency information. After installation run `bat --help`"
@ -24,29 +24,29 @@ s3 = ["boto3"]
zstandard = ["zstandard"] zstandard = ["zstandard"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.11,<4.0" python = "^3.8"
requests = "^2.11" requests = "^2.11"
# For S3 storage support: # For S3 storage support:
boto3 = { version = "^1.9", optional = true } boto3 = { version = "^1.9", optional = true }
# For Blender 3.0+ compressed file support. # For Blender 3.0+ compressed file support.
zstandard = { version = "^0.16", optional = true } zstandard = { version = "^0.15", optional = true }
[tool.poetry.group.dev.dependencies] [tool.poetry.dev-dependencies]
mypy = ">=0.942" mypy = ">=0.942"
pytest = "^7.4.4" pytest = "^6.2"
pytest-cov = "^4.1.0" pytest-cov = "^3.0.0"
twine = "^5.0.0"
# for the 'radon cc' command # for the 'radon cc' command
radon = "^3.0" radon = "^3.0"
# for converting profiler output to KCacheGrind input # for converting profiler output to KCacheGrind input
"pyprof2calltree" = "*" "pyprof2calltree" = "*"
# For building documentation # For building documentation
sphinx = "^7.2" sphinx = "^2.1"
sphinx-autobuild = "^2024.3" sphinx-autobuild = "^0.7"
sphinx-rtd-theme = "^2.0" sphinx-rtd-theme = "^0.4"
responses = "^0.10" responses = "^0.10"
pathlib2 = { version = "^2.3", python = "<3.6" }
tox = "^3.12" tox = "^3.12"
types-requests = "^2.25.0" types-requests = "^2.25.0"
@ -54,6 +54,9 @@ types-requests = "^2.25.0"
bat = 'blender_asset_tracer.cli:cli_main' bat = 'blender_asset_tracer.cli:cli_main'
[tool.poetry.group.dev.dependencies]
twine = "^4.0.1"
[build-system] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"] requires = ["poetry>=0.12"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.masonry.api"

View File

@ -1,12 +1,12 @@
[tool:pytest] [tool:pytest]
addopts = -p pytest_cov -v --cov blender_asset_tracer --cov-report term-missing addopts = -v --cov blender_asset_tracer --cov-report term-missing
[pep8] [pep8]
max-line-length = 100 max-line-length = 100
[mypy] [mypy]
# This should match pyproject.toml # This should match pyproject.toml
python_version = 3.11 python_version = 3.8
warn_redundant_casts = True warn_redundant_casts = True
ignore_missing_imports = True ignore_missing_imports = True

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00001_00000.blob","start":39364,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00002_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00003_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00004_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00001_00000.blob","start":39364,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00002_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00003_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00004_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00001_00000.blob","start":39364,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00002_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00003_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00004_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00001_00000.blob","start":39364,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00002_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00003_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -1 +0,0 @@
{"version":3,"items":{"0":{"name":"Geometry","type":"GEOMETRY","data":{"mesh":{"num_vertices":2012,"num_edges":3978,"num_polygons":1968,"num_corners":7872,"poly_offsets":{"name":"00001_00000.blob","start":0,"size":7876},"materials":[],"attributes":[{"name":".corner_edge","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":7876,"size":31488}},{"name":"position","domain":"POINT","type":"FLOAT_VECTOR","data":{"name":"00004_00000.blob","start":0,"size":24144}},{"name":".edge_verts","domain":"EDGE","type":"INT32_2D","data":{"name":"00001_00000.blob","start":63508,"size":31824}},{"name":".corner_vert","domain":"CORNER","type":"INT","data":{"name":"00001_00000.blob","start":95332,"size":31488}},{"name":"UVMap","domain":"CORNER","type":"FLOAT2","data":{"name":"00001_00000.blob","start":126820,"size":62976}},{"name":".uv_select_vert","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".uv_select_edge","domain":"CORNER","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":189796,"size":7872}},{"name":".select_vert","domain":"POINT","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":197668,"size":2012}},{"name":".select_edge","domain":"EDGE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":199680,"size":3978}},{"name":".select_poly","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":203658,"size":1968}},{"name":".uv_select_face","domain":"FACE","type":"BOOLEAN","data":{"name":"00001_00000.blob","start":205626,"size":1968}}]}}}}}

View File

@ -93,10 +93,6 @@ class StructTest(unittest.TestCase):
self.s_ulong = dna.Struct(b"ulong", 8) self.s_ulong = dna.Struct(b"ulong", 8)
self.s_uint64 = dna.Struct(b"uint64_t", 8) self.s_uint64 = dna.Struct(b"uint64_t", 8)
self.s_uint128 = dna.Struct(b"uint128_t", 16) # non-supported type self.s_uint128 = dna.Struct(b"uint128_t", 16) # non-supported type
self.s_substruct = dna.Struct(b"substruct")
self.f_substruct_name = dna.Field(self.s_char, dna.Name(b"name[10]"), 10, 0)
self.s_substruct.append_field(self.f_substruct_name)
self.f_next = dna.Field(self.s, dna.Name(b"*next"), 8, 0) self.f_next = dna.Field(self.s, dna.Name(b"*next"), 8, 0)
self.f_prev = dna.Field(self.s, dna.Name(b"*prev"), 8, 8) self.f_prev = dna.Field(self.s, dna.Name(b"*prev"), 8, 8)
@ -113,7 +109,6 @@ class StructTest(unittest.TestCase):
self.f_testint = dna.Field(self.s_int, dna.Name(b"testint"), 4, 4178) self.f_testint = dna.Field(self.s_int, dna.Name(b"testint"), 4, 4178)
self.f_testfloat = dna.Field(self.s_float, dna.Name(b"testfloat"), 4, 4182) self.f_testfloat = dna.Field(self.s_float, dna.Name(b"testfloat"), 4, 4182)
self.f_testulong = dna.Field(self.s_ulong, dna.Name(b"testulong"), 8, 4186) self.f_testulong = dna.Field(self.s_ulong, dna.Name(b"testulong"), 8, 4186)
self.f_substruct = dna.Field(self.s_substruct, dna.Name(b"testsubstruct"), 10, 4194)
self.s.append_field(self.f_next) self.s.append_field(self.f_next)
self.s.append_field(self.f_prev) self.s.append_field(self.f_prev)
@ -130,7 +125,6 @@ class StructTest(unittest.TestCase):
self.s.append_field(self.f_testint) self.s.append_field(self.f_testint)
self.s.append_field(self.f_testfloat) self.s.append_field(self.f_testfloat)
self.s.append_field(self.f_testulong) self.s.append_field(self.f_testulong)
self.s.append_field(self.f_substruct)
def test_autosize(self): def test_autosize(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@ -269,19 +263,6 @@ class StructTest(unittest.TestCase):
self.assertAlmostEqual(2.79, val[1]) self.assertAlmostEqual(2.79, val[1])
fileobj.seek.assert_called_with(4144, os.SEEK_CUR) fileobj.seek.assert_called_with(4144, os.SEEK_CUR)
def test_struct_field_get_subproperty(self):
fileobj = mock.MagicMock(io.BufferedReader)
fileobj.read.return_value = b"my_name"
_, val = self.s.field_get(
self.FakeHeader(),
fileobj,
(b"testsubstruct", b"name"),
as_str=True,
)
self.assertEqual("my_name", val)
fileobj.seek.assert_called_with(4194, os.SEEK_CUR)
def test_char_field_set(self): def test_char_field_set(self):
fileobj = mock.MagicMock(io.BufferedReader) fileobj = mock.MagicMock(io.BufferedReader)
value = 255 value = 255
@ -363,10 +344,3 @@ class StructTest(unittest.TestCase):
expected = struct.pack(b">f", value) expected = struct.pack(b">f", value)
self.s.field_set(self.FakeHeader(), fileobj, b"testfloat", value) self.s.field_set(self.FakeHeader(), fileobj, b"testfloat", value)
fileobj.write.assert_called_with(expected) fileobj.write.assert_called_with(expected)
def test_struct_field_set_subproperty(self):
fileobj = mock.MagicMock(io.BufferedReader)
expected = b"new_name\x00"
self.s.field_set(self.FakeHeader(), fileobj, (b"testsubstruct", b"name"), "new_name")
fileobj.write.assert_called_with(expected)

View File

@ -13,7 +13,6 @@ class BlendFileBlockTest(AbstractBlendFileTest):
def test_loading(self): def test_loading(self):
self.assertFalse(self.bf.is_compressed) self.assertFalse(self.bf.is_compressed)
self.assertEqual(0, self.bf.header.file_format_version)
def test_some_properties(self): def test_some_properties(self):
ob = self.bf.code_index[b"OB"][0] ob = self.bf.code_index[b"OB"][0]
@ -46,12 +45,6 @@ class BlendFileBlockTest(AbstractBlendFileTest):
mname = mesh.get((b"id", b"name"), as_str=True) mname = mesh.get((b"id", b"name"), as_str=True)
self.assertEqual("MECube³", mname) self.assertEqual("MECube³", mname)
# Try to access different file-block items.
verts_ptr = mesh.get(b"mvert")
verts = self.bf.block_from_addr[verts_ptr]
assert verts.get(b"co") == [-1.0, -1.0, -1.0]
assert verts.get(b"co", array_index=1) == [-1.0, -1.0, 1.0]
def test_get_recursive_iter(self): def test_get_recursive_iter(self):
ob = self.bf.code_index[b"OB"][0] ob = self.bf.code_index[b"OB"][0]
assert isinstance(ob, blendfile.BlendFileBlock) assert isinstance(ob, blendfile.BlendFileBlock)
@ -161,23 +154,6 @@ class BlendFileBlockTest(AbstractBlendFileTest):
self.assertEqual("OBümlaut", ob.id_name.decode()) self.assertEqual("OBümlaut", ob.id_name.decode())
class BlendFileLargeBhead8Test(AbstractBlendFileTest):
def setUp(self):
self.bf = blendfile.BlendFile(self.blendfiles / "basic_file_large_bhead8.blend")
def test_loading(self):
self.assertFalse(self.bf.is_compressed)
self.assertEqual(1, self.bf.header.file_format_version)
def test_some_properties(self):
ob = self.bf.code_index[b"OB"][0]
self.assertEqual("Object", ob.dna_type_name)
# Try high level operation to read the object location.
loc = ob.get(b"loc")
self.assertEqual([2.0, 3.0, 5.0], loc)
class PointerTest(AbstractBlendFileTest): class PointerTest(AbstractBlendFileTest):
def setUp(self): def setUp(self):
self.bf = blendfile.BlendFile(self.blendfiles / "with_sequencer.blend") self.bf = blendfile.BlendFile(self.blendfiles / "with_sequencer.blend")
@ -303,29 +279,6 @@ class ArrayTest(AbstractBlendFileTest):
self.assertEqual(name, tex.id_name) self.assertEqual(name, tex.id_name)
class DynamicArrayTest(AbstractBlendFileTest):
def test_dynamic_array_of_bakes(self):
self.bf = blendfile.BlendFile(self.blendfiles / "multiple_geometry_nodes_bakes.blend")
obj = self.bf.code_index[b"OB"][0]
assert isinstance(obj, blendfile.BlendFileBlock)
modifier = obj.get_pointer((b"modifiers", b"first"))
assert isinstance(modifier, blendfile.BlendFileBlock)
bakes = modifier.get_pointer(b"bakes")
bake_count = bakes.count
self.assertEqual(3, bake_count)
for i, bake in enumerate(blendfile.iterators.dynamic_array(bakes)):
if i == 0:
frame_start = 37
if i == 1:
frame_start = 5
if i == 2:
frame_start = 12
self.assertEqual(frame_start, bake.get(b"frame_start"))
class CompressionRecognitionTest(AbstractBlendFileTest): class CompressionRecognitionTest(AbstractBlendFileTest):
def _find_compression_type(self, filename: str) -> magic_compression.Compression: def _find_compression_type(self, filename: str) -> magic_compression.Compression:
path = self.blendfiles / filename path = self.blendfiles / filename
@ -506,15 +459,3 @@ class BlendFileCacheTest(AbstractBlendFileTest):
self.assertIs(bf, blendfile._cached_bfiles[other]) self.assertIs(bf, blendfile._cached_bfiles[other])
self.assertEqual(str(bf.raw_filepath), bf.fileobj.name) self.assertEqual(str(bf.raw_filepath), bf.fileobj.name)
class BlendFileSubVersionTest(AbstractBlendFileTest):
def test_file_subversion(self) -> None:
self.bf = blendfile.BlendFile(self.blendfiles / "multiple_materials.blend")
self.assertEqual(self.bf.file_subversion, 3)
self.bf = blendfile.BlendFile(
self.blendfiles
/ "compositor_nodes/compositor_nodes_blender500_library.blend"
)
self.assertEqual(self.bf.file_subversion, 36)

146
tests/test_pack.py Executable file → Normal file
View File

@ -694,152 +694,6 @@ class ProgressTest(AbstractPackTest):
) )
class KeepHierarchyPackTest(AbstractPackTest):
def hierarchy_path(self, filepath) -> Path:
"""Return the keep-hierarchy path for a file: target / strip_root(abs_path)."""
return Path(self.tpath, bpathlib.strip_root(filepath))
def test_strategise_keep_hierarchy_no_rewrite(self):
"""When all deps are in-project with relative paths, no rewriting is needed."""
infile = self.blendfiles / "doubly_linked.blend"
packer = pack.Packer(
infile, self.blendfiles, self.tpath, keep_hierarchy=True
)
packer.strategise()
packed_files = (
"doubly_linked.blend",
"linked_cube.blend",
"basic_file.blend",
"material_textures.blend",
"textures/Bricks/brick_dotted_04-bump.jpg",
"textures/Bricks/brick_dotted_04-color.jpg",
)
for pf in packed_files:
path = self.blendfiles / pf
act = packer._actions[path]
self.assertEqual(
pack.PathAction.KEEP_PATH, act.path_action, "for %s" % pf
)
# In keep_hierarchy mode, paths use strip_root(abs_path) instead of
# relative_to(project).
self.assertEqual(
self.hierarchy_path(path), act.new_path, "for %s" % pf
)
self.assertEqual({}, self.rewrites(packer))
self.assertEqual(len(packed_files), len(packer._actions))
def test_strategise_keep_hierarchy_rewrite(self):
"""Deps outside the project go to target/strip_root(path), not _outside_project/."""
ppath = self.blendfiles / "subdir"
infile = ppath / "doubly_linked_up.blend"
packer = pack.Packer(infile, ppath, self.tpath, keep_hierarchy=True)
packer.strategise()
# The blendfile itself should be at target / strip_root(abs_path)
act = packer._actions[infile]
self.assertEqual(pack.PathAction.KEEP_PATH, act.path_action)
self.assertEqual(self.hierarchy_path(infile), act.new_path)
# External files should NOT be under _outside_project/
external_files = (
"linked_cube.blend",
"basic_file.blend",
"material_textures.blend",
"textures/Bricks/brick_dotted_04-bump.jpg",
"textures/Bricks/brick_dotted_04-color.jpg",
)
for fn in external_files:
path = self.blendfiles / fn
act = packer._actions[path]
self.assertEqual(
pack.PathAction.FIND_NEW_LOCATION, act.path_action, "for %s" % fn
)
# Should be at target / strip_root(abs_path), NOT target/_outside_project/...
expected = self.hierarchy_path(path)
self.assertEqual(
expected,
act.new_path,
f"\nEXPECT: {expected}\nACTUAL: {act.new_path}\nfor {fn}",
)
# There should be no _outside_project in any new_path
for path, action in packer._actions.items():
self.assertNotIn(
"_outside_project",
str(action.new_path),
f"_outside_project should not appear in keep_hierarchy mode for {path}",
)
def test_execute_keep_hierarchy(self):
"""Verify files are copied to correct hierarchy and paths are rewritten."""
ppath = self.blendfiles / "subdir"
infile = ppath / "doubly_linked_up.blend"
with pack.Packer(infile, ppath, self.tpath, keep_hierarchy=True) as packer:
packer.strategise()
packer.execute()
# The blendfile should be at its hierarchy position
packed_blend = self.hierarchy_path(infile)
self.assertTrue(packed_blend.exists(), "Blendfile should be in hierarchy")
# There should be NO _outside_project directory
self.assertFalse(
(self.tpath / "_outside_project").exists(),
"_outside_project should not exist in keep_hierarchy mode",
)
# Dependencies should be at their hierarchy positions
for fn in ("linked_cube.blend", "basic_file.blend", "material_textures.blend"):
dep_path = self.hierarchy_path(self.blendfiles / fn)
self.assertTrue(dep_path.exists(), "%s should be in hierarchy" % fn)
# Verify paths were rewritten correctly in the packed blend file.
# The rewritten paths should be relative from the packed blend to
# the packed dependencies.
bfile = blendfile.open_cached(packed_blend, assert_cached=False)
libs = sorted(bfile.code_index[b"LI"])
# Since keep_hierarchy preserves relative positions, the relative paths
# from subdir/doubly_linked_up.blend to the parent blendfiles should
# be the same as the originals (//../linked_cube.blend etc.)
self.assertEqual(b"LILib", libs[0].id_name)
self.assertEqual(b"//../linked_cube.blend", libs[0][b"name"])
self.assertEqual(b"LILib.002", libs[1].id_name)
self.assertEqual(b"//../material_textures.blend", libs[1][b"name"])
def test_execute_keep_hierarchy_no_touch_origs(self):
"""Original files should not be modified."""
ppath = self.blendfiles / "subdir"
infile = ppath / "doubly_linked_up.blend"
with pack.Packer(infile, ppath, self.tpath, keep_hierarchy=True) as packer:
packer.strategise()
packer.execute()
# The original file shouldn't be touched.
bfile = blendfile.open_cached(infile, assert_cached=False)
libs = sorted(bfile.code_index[b"LI"])
self.assertEqual(b"LILib", libs[0].id_name)
self.assertEqual(b"//../linked_cube.blend", libs[0][b"name"])
self.assertEqual(b"LILib.002", libs[1].id_name)
self.assertEqual(b"//../material_textures.blend", libs[1][b"name"])
def test_keep_hierarchy_output_path(self):
"""output_path should use the full hierarchy path."""
infile = self.blendfiles / "basic_file.blend"
packer = pack.Packer(
infile, self.blendfiles, self.tpath, keep_hierarchy=True
)
packer.strategise()
self.assertEqual(self.hierarchy_path(infile), packer.output_path)
class AbortTest(AbstractPackTest): class AbortTest(AbstractPackTest):
def test_abort_strategise(self): def test_abort_strategise(self):
infile = self.blendfiles / "subdir/doubly_linked_up.blend" infile = self.blendfiles / "subdir/doubly_linked_up.blend"

View File

@ -1,5 +1,4 @@
import collections import collections
import functools
import logging import logging
import sys import sys
import typing import typing
@ -370,10 +369,6 @@ class DepsTest(AbstractTracerTest):
}, },
) )
def test_block_li_packed(self):
# Packed libraries should not be traced.
self.assert_deps("74871-packed-libraries.blend", {})
def test_deps_recursive(self): def test_deps_recursive(self):
self.assert_deps( self.assert_deps(
"doubly_linked.blend", "doubly_linked.blend",
@ -511,26 +506,6 @@ class DepsTest(AbstractTracerTest):
}, },
) )
def test_compositor_nodes(self) -> None:
"""Test compositor node trees.
Since Blender 5.0 these use a different DNA field, and can also be
linked from other files.
"""
self.assert_deps(
"compositor_nodes/compositor_nodes_blender500_workfile.blend",
{
b"LIcompositor_nodes_blender500_library.blend": Expect(
type="Library",
full_field="name[1024]",
dirname_field=None,
basename_field=None,
asset_path=b"//compositor_nodes_blender500_library.blend",
is_sequence=False,
),
},
)
def test_usage_abspath(self): def test_usage_abspath(self):
deps = [ deps = [
dep dep
@ -557,56 +532,6 @@ class DepsTest(AbstractTracerTest):
}, },
) )
def test_geonodes_sim_data(self) -> None:
# Simplify the rest of the code by putting the values that are the same of all cases here:
expect_bake = functools.partial(
Expect,
dirname_field=None,
basename_field=None,
is_sequence=True,
)
expects = {
# Two objects that use "Inherit from Modifer":
b"OBCustom Bake Path.modifiers[0].bakes[0]": [
# Custom path set on the sim node, so this is sim node data.
expect_bake(
type="NodesModifierBake",
full_field="*directory",
asset_path=b"//bakePath",
),
],
b"OBDefault Bake Path.modifiers[0].bakes[0]": [
# NO custom path set on the sim node, so this follows the modifier data.
expect_bake(
type="NodesModifierData",
full_field="*simulation_bake_directory",
asset_path=b"//config-on-sim-node",
),
],
# Two objects that have the config only on the node itself:
b"OBCustom Bake Path.001.modifiers[0].bakes[0]": [
expect_bake(
type="NodesModifierBake",
full_field="*directory",
asset_path=b"//set-on-node",
),
],
b"OBDefault Bake Path.001.modifiers[0].bakes[0]": [
expect_bake(
type="NodesModifierData",
full_field="*simulation_bake_directory",
asset_path=b"//only-set-on-modifier",
),
],
}
# NOTE: there are two more objects in the scene, 'Packed Bake' and
# 'Packed Bake.001'. But, because those use packed data (on the modifier
# resp. bake level), they should not be listed as dependencies.
self.maxDiff = None
self.assert_deps("geometry-nodes-sim/geonodes-sim-cache.blend", expects)
def test_recursion_loop(self): def test_recursion_loop(self):
infinite_bfile = self.blendfiles / "recursive_dependency_1.blend" infinite_bfile = self.blendfiles / "recursive_dependency_1.blend"

View File

@ -1,6 +1,6 @@
[tox] [tox]
isolated_build = true isolated_build = true
envlist = py311, py312, py313, py314 envlist = py38, py39, py310, py311
[testenv] [testenv]
whitelist_externals = poetry whitelist_externals = poetry

View File

@ -19,5 +19,5 @@ echo git tag -a v$1 -m \'Tagged version $1\'
echo echo
echo "Build the package & upload to PyPi using:" echo "Build the package & upload to PyPi using:"
echo "poetry build" echo "poetry build"
echo "poetry run twine check dist/blender_asset_tracer-$1.tar.gz dist/blender_asset_tracer-$1-*.whl" echo "twine check dist/blender_asset_tracer-$1.tar.gz dist/blender_asset_tracer-$1-*.whl"
echo "poetry run twine upload -r bat dist/blender_asset_tracer-$1.tar.gz dist/blender_asset_tracer-$1-*.whl" echo "twine upload -r bat dist/blender_asset_tracer-$1.tar.gz dist/blender_asset_tracer-$1-*.whl"