Compare commits

..

25 Commits
v1.19 ... main

Author SHA1 Message Date
4085f0015f rollback zip changes 2026-02-20 11:00:12 +01:00
cf31dbc5ec Update README.md 2026-02-20 10:51:45 +01:00
bb1de717c8 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>
2026-02-20 10:50:31 +01:00
12e6e2643b zip operator 2026-02-19 18:06:34 +01:00
cbbe8be0d8 keep hierarchy option 2026-02-19 18:06:25 +01:00
Sybren A. Stüvel
6af9567a44 Mark 1.21 as released today 2025-11-24 16:42:57 +01:00
Sybren A. Stüvel
74860b3107 Bumped version to 1.21 2025-11-24 16:39:34 +01:00
Sybren A. Stüvel
5c0edf4c07 Test Python 3.14 with Tox 2025-11-24 16:02:05 +01:00
Sybren A. Stüvel
a973637896 Upgrade some dependencies
Upgrade `zstandard` to `0.16`, because that is what's used in the oldest
still-supported Blender (4.2-LTS).

This also upgrades Pytest to 7.4.4 and Pytest-cov to 4.1.0. Still a
conservative upgrade, but at least it gets rid of various deprecation
warnings.
2025-11-24 16:02:05 +01:00
Sybren A. Stüvel
7322c0772a Poetry: Bump Python to 3.11 and update build system to poetry-core
The oldest still-supported version of Blender is 4.2-LTS, which uses
Python 3.11, so that's what's now the required Python version for BAT.

Make the build system depend on `poetry-core` instead of `poetry`, to
make package builds faster (especially important when using Tox for
testing in various environments).

This also re-generates the `poetry.lock` file, so updates the
dependencies to their latest version compatible with the `project.toml`
file.
2025-11-24 16:02:05 +01:00
Sybren A. Stüvel
3b61399f7f Update CHANGELOG.md with recent commits 2025-11-24 15:34:45 +01:00
Sybren A. Stüvel
aceea0d823 Skip tracing 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.
2025-11-24 15:30:03 +01:00
Andrej730
2c0fe87fba cli.blocks - print biggest block's address as hex for readibility (#92900)
As addresses typically represented everywhere as hex values.

Before:
```
Biggest ARegion-DATA block is 304 B at address 1585888006560
Finding what points there
     <BlendFileBlock.ScrArea (DATA), size=184 at 0x1713e4acd60> (b'regionbase', b'first')
     <BlendFileBlock.ARegion (DATA), size=304 at 0x1713e4a9020> b'prev'
```

After:
```
Biggest ARegion-DATA block is 304 B at address 0x1713e4a91a0
Finding what points there
     <BlendFileBlock.ScrArea (DATA), size=184 at 0x1713e4acd60> (b'regionbase', b'first')
     <BlendFileBlock.ARegion (DATA), size=304 at 0x1713e4a9020> b'prev'
```

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92900
2025-11-24 15:08:39 +01:00
Andrej730
47c3e8d77f Struct.field_set - support setting subproperties (#92899)
Extend `Struct.field_set()` so that the `path` type can be a
`dna.FieldPath`, to mirror `Struct.field_get`'s `path` type, and allow
users to set file-block's subproperties. For example this allows
setting `object_block[(b'id', 'name')]`.

Also, along the way, added a test for getting subproperty value.

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92899
2025-11-24 15:07:46 +01:00
Andrej730
c69612b264 BlendFileBlock.get to support accessing different items in the file-block (#92898)
Add an `array_index` parameter to `block.get(property_name)` to get a
specific item from an array.

Example:
```python
verts = self.bf.block_from_addr[verts_ptr]
assert verts.get(b"co") == [-1.0, -1.0, -1.0]  # index 0
assert verts.get(b"co", array_index=1) == [-1.0, -1.0, 1.0]
```

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92898
2025-11-24 14:53:15 +01:00
Andrej730
b1c4f5e116 setup.cfg - set enabled pytest_cov explicitly (#92896)
To support running tests if `PYTEST_DISABLE_PLUGIN_AUTOLOAD` is set
(to avoid conflicts with other plugins that might be available on the
system).

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92896
2025-11-24 14:40:35 +01:00
Andrej730
5690c8e7cb Add py.typed marker file (#92895)
PEP 561 added a requirement for packages that do have typing
information, to include empty py.typed file:
https://peps.python.org/pep-0561/#packaging-type-information

Example warnings from pyright/pylance:
```python
# Stub file not found for "blender_asset_tracer" Pylance: reportMissingTypeStubs
from blender_asset_tracer import blendfile
```

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92895
2025-11-24 14:38:15 +01:00
Jonas Dichelle
50578ac62a Add Support for Geometry Node Cache (#92890)
Add support for geometry node simulation cache files.

This also adds support for dealing with dynamic arrays in Blender's
DNA, because `modifier.bakes` is a pointer to such an array.

Co-authored-by: Sybren A. Stüvel <sybren@blender.org>
Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92890
2025-11-24 13:06:38 +01:00
Andrej730
94486a3218 Add additional annotations to avoid typing issues (#92897)
BlendFileBlock attributes:

Explicit annotation on BlendFileBlock are needed because otherwise
e.g. `block.add_old` type was imprecisely inferred from the
assignments as `int | Any`, where `Any` comes from `.unpack` returning
`tuple[Any, ...]`.

Ideally unpack should be somehow connected to the returned types, but
this solution should work for now just to avoid typing errors.

dna_io - add some missing annotations:

Some annotations were needed to ensure `block.code` will be inferred
as `bytes` and not `bytes | Unknown`.

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92897
Reviewed-by: Sybren A. Stüvel <sybren@blender.org>
2025-08-25 11:51:25 +02:00
Andrej730
95165a0330 README - code example to use Python syntax highlighting (#92894)
Added explicit code block  for Python syntax highlighting.

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92894
2025-08-25 11:51:25 +02:00
Sybren A. Stüvel
8a17495566 Bump version to 1.20 and mark it as released today 2025-08-25 11:51:25 +02:00
Sybren A. Stüvel
08b37a35f8 Update CHANGELOG.md 2025-07-11 15:45:24 +02:00
Sybren A. Stüvel
6c42d06f05 Add a __main__.py file
Add a `__main__.py` file, so that BAT can be run with
`python3 -m blender_asset_tracer`. This makes it easier to start BAt in
a debugger.
2025-07-11 15:42:39 +02:00
Sybren A. Stüvel
16c208bc8e Support compositor node trees in Blender 5.0
Since Blender 5.0 these use a different DNA field. BAT now supports
this too, and supports linked node trees as well.

See blender/blender@bd61e69be5
2025-07-11 15:42:07 +02:00
Sybren A. Stüvel
4c429e9212 Read blendfile sub-version
Add support for reading the blend file's sub-version.
2025-07-11 15:30:58 +02:00
64 changed files with 1766 additions and 975 deletions

View File

@ -3,6 +3,24 @@
This file logs the changes that are actually interesting to users (new features,
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)).

View File

@ -1,4 +1,4 @@
# Blender Asset Tracer BAT🦇
# Blender Asset Tracer BAT🦇 (ADV fork)
Script to manage assets with Blender.
@ -75,49 +75,51 @@ 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
open Blender itself. Here is an example showing how to determine the render engine used:
#!/usr/bin/env python3.7
import json
import sys
from pathlib import Path
```python
#!/usr/bin/env python3.7
import json
import sys
from pathlib import Path
from blender_asset_tracer import blendfile
from blender_asset_tracer.blendfile import iterators
from blender_asset_tracer import blendfile
from blender_asset_tracer.blendfile import iterators
if len(sys.argv) != 2:
print(f'Usage: {sys.argv[0]} somefile.blend', file=sys.stderr)
sys.exit(1)
if len(sys.argv) != 2:
print(f'Usage: {sys.argv[0]} somefile.blend', file=sys.stderr)
sys.exit(1)
bf_path = Path(sys.argv[1])
bf = blendfile.open_cached(bf_path)
bf_path = Path(sys.argv[1])
bf = blendfile.open_cached(bf_path)
# Get the first window manager (there is probably exactly one).
window_managers = bf.find_blocks_from_code(b'WM')
assert window_managers, 'The Blend file has no window manager'
window_manager = window_managers[0]
# Get the first window manager (there is probably exactly one).
window_managers = bf.find_blocks_from_code(b'WM')
assert window_managers, 'The Blend file has no window manager'
window_manager = window_managers[0]
# Get the scene from the first window.
windows = window_manager.get_pointer((b'windows', b'first'))
for window in iterators.listbase(windows):
scene = window.get_pointer(b'scene')
break
# Get the scene from the first window.
windows = window_manager.get_pointer((b'windows', b'first'))
for window in iterators.listbase(windows):
scene = window.get_pointer(b'scene')
break
# BAT can only return simple values, so it can't return the embedded
# struct 'r'. 'r.engine' is a simple string, though.
engine = scene[b'r', b'engine'].decode('utf8')
xsch = scene[b'r', b'xsch']
ysch = scene[b'r', b'ysch']
size = scene[b'r', b'size'] / 100.0
# BAT can only return simple values, so it can't return the embedded
# struct 'r'. 'r.engine' is a simple string, though.
engine = scene[b'r', b'engine'].decode('utf8')
xsch = scene[b'r', b'xsch']
ysch = scene[b'r', b'ysch']
size = scene[b'r', b'size'] / 100.0
render_info = {
'engine': engine,
'frame_pixels': {
'x': int(xsch * size),
'y': int(ysch * size),
},
}
render_info = {
'engine': engine,
'frame_pixels': {
'x': int(xsch * size),
'y': int(ysch * size),
},
}
json.dump(render_info, sys.stdout, indent=4, sort_keys=True)
print()
json.dump(render_info, sys.stdout, indent=4, sort_keys=True)
print()
```
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
@ -171,6 +173,6 @@ index-servers =
pip install twine
poetry build
poetry run twine check dist/blender_asset_tracer-1.19.tar.gz dist/blender_asset_tracer-1.19-*.whl
poetry run twine upload -r bat dist/blender_asset_tracer-1.19.tar.gz dist/blender_asset_tracer-1.19-*.whl
poetry run twine check dist/blender_asset_tracer-1.21.tar.gz dist/blender_asset_tracer-1.21-*.whl
poetry run twine upload -r bat dist/blender_asset_tracer-1.21.tar.gz dist/blender_asset_tracer-1.21-*.whl
```

View File

@ -20,4 +20,57 @@
# <pep8 compliant>
__version__ = "1.19"
__version__ = "1.21"
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

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

View File

@ -122,6 +122,7 @@ class BlendFile:
self.filepath = path
self.raw_filepath = path
self._is_modified = False
self.file_subversion = 0
self.fileobj = self._open_file(path, mode)
self.blocks = [] # type: BFBList
@ -169,6 +170,8 @@ class BlendFile:
if block.code == b"DNA1":
self.decode_structs(block)
elif block.code == b"GLOB":
self.decode_glob(block)
else:
self.fileobj.seek(block.size, os.SEEK_CUR)
@ -356,6 +359,25 @@ class BlendFile:
dna_struct.append_field(field)
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:
"""Construct an absolute path from a blendfile-relative path."""
@ -425,6 +447,12 @@ class BlendFileBlock:
old_structure = struct.Struct(b"4sI")
"""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:
self.bfile = bfile
@ -568,6 +596,7 @@ class BlendFileBlock:
null_terminated=True,
as_str=False,
return_field=False,
array_index=0,
) -> typing.Any:
"""Read a property and return the value.
@ -584,8 +613,20 @@ class BlendFileBlock:
(assumes UTF-8 encoding).
:param return_field: When True, returns tuple (dna.Field, 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.
"""
self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
file_offset = self.file_offset
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]
field, value = dna_struct.field_get(
@ -605,8 +646,8 @@ class BlendFileBlock:
self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
return self.bfile.fileobj.read(self.size)
def as_string(self) -> str:
"""Interpret the bytes of this datablock as null-terminated utf8 string."""
def as_bytes_string(self) -> bytes:
"""Interpret the bytes of this datablock as null-terminated string of raw bytes."""
the_bytes = self.raw_data()
try:
first_null = the_bytes.index(0)
@ -614,6 +655,11 @@ class BlendFileBlock:
pass
else:
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()
def get_recursive_iter(
@ -680,7 +726,7 @@ class BlendFileBlock:
hsh = zlib.adler32(str(value).encode(), hsh)
return hsh
def set(self, path: bytes, value):
def set(self, path: dna.FieldPath, value):
dna_struct = self.bfile.structs[self.sdna_index]
self.bfile.mark_modified()
self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
@ -789,9 +835,13 @@ class BlendFileBlock:
def __getitem__(self, path: dna.FieldPath):
return self.get(path)
def __setitem__(self, item: bytes, value) -> None:
def __setitem__(self, item: dna.FieldPath, value) -> None:
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]:
"""Generator, yields all field names of this block."""
return (f.name.name_only for f in self.dna_type.fields)

View File

@ -310,7 +310,7 @@ class Struct:
self,
file_header: header.BlendFileHeader,
fileobj: typing.IO[bytes],
path: bytes,
path: FieldPath,
value: typing.Any,
):
"""Write a value to the blend file.
@ -319,7 +319,6 @@ class Struct:
struct on disk (e.g. the start of the BlendFileBlock containing the
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)

View File

@ -204,17 +204,17 @@ class EndianIO:
return fileobj.write(to_write)
@classmethod
def read_bytes0(cls, fileobj, length):
def read_bytes0(cls, fileobj: typing.IO[bytes], length: int) -> bytes:
data = fileobj.read(length)
return cls.read_data0(data)
@classmethod
def read_data0_offset(cls, data, offset):
def read_data0_offset(cls, data: bytes, offset: int) -> bytes:
add = data.find(b"\0", offset) - offset
return data[offset : offset + add]
@classmethod
def read_data0(cls, data):
def read_data0(cls, data: bytes) -> bytes:
add = data.find(b"\0")
if add < 0:
return data

View File

@ -20,6 +20,7 @@
# (c) 2014, Blender Foundation - Campbell Barton
# (c) 2018, Blender Foundation - Sybren A. Stüvel
import typing
import copy
from blender_asset_tracer import cdefs
from . import BlendFileBlock
@ -70,3 +71,27 @@ def modifiers(object_block: BlendFileBlock) -> typing.Iterator[BlendFileBlock]:
# 'ob->modifiers[...]'
mods = object_block.get_pointer((b"modifiers", b"first"))
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

@ -52,6 +52,14 @@ eModifierType_MeshSequenceCache = 52
eModifierType_Fluid = 56
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
PART_DRAW_OB = 7
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.
biggest_block = sorted(infos[0].blocks, key=lambda blck: blck.size, reverse=True)[0]
print(
"Biggest %s block is %s at address %s"
"Biggest %s block is %s at address 0x%x"
% (
block_key(biggest_block),
common.humanize_bytes(biggest_block.size),

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

@ -88,6 +88,15 @@ def add_parser(subparsers):
help="Only pack assets that are referred to with a relative path (e.g. "
"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):
@ -119,6 +128,9 @@ def create_packer(
if args.relative_only:
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))
elif (
@ -137,6 +149,11 @@ def create_packer(
"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)
elif target.lower().endswith(".zip"):
@ -146,7 +163,8 @@ def create_packer(
raise ValueError("ZIP packer does not support on-the-fly compression")
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:
packer = pack.Packer(
@ -156,6 +174,7 @@ def create_packer(
noop=args.noop,
compress=args.compress,
relative_only=args.relative_only,
keep_hierarchy=args.keep_hierarchy,
)
if args.exclude:

View 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 ""

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

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

View 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")

View File

View File

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

View File

@ -90,7 +90,14 @@ def _expand_generic_mtex(block: blendfile.BlendFileBlock):
def _expand_generic_nodetree(block: blendfile.BlendFileBlock):
assert block.dna_type.dna_type_id == b"bNodeTree"
if block.dna_type.dna_type_id == b"ID":
# 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"))
@ -145,7 +152,11 @@ def _expand_generic_idprops(block: blendfile.BlendFileBlock):
def _expand_generic_nodetree_id(block: blendfile.BlendFileBlock):
block_ntree = block.get_pointer(b"nodetree", None)
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)
if block_ntree is not None:
yield from _expand_generic_nodetree(block_ntree)

View File

@ -27,6 +27,7 @@ import logging
import typing
from blender_asset_tracer import blendfile, bpathlib, cdefs
from blender_asset_tracer.blendfile import iterators
from . import result
log = logging.getLogger(__name__)
@ -337,7 +338,7 @@ def modifier_dynamic_paint(
surfaces = canvas_settings.get_pointer((b"surfaces", b"first"))
for surf_idx, surface in enumerate(blendfile.iterators.listbase(surfaces)):
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:
@ -351,3 +352,62 @@ def modifier_dynamic_paint(
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,
)

View File

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

1845
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "blender-asset-tracer"
version = "1.19"
version = "1.21"
homepage = 'https://developer.blender.org/project/profile/79/'
description = "BAT parses Blend files and produces dependency information. After installation run `bat --help`"
@ -24,19 +24,19 @@ s3 = ["boto3"]
zstandard = ["zstandard"]
[tool.poetry.dependencies]
python = "^3.9"
python = ">=3.11,<4.0"
requests = "^2.11"
# For S3 storage support:
boto3 = { version = "^1.9", optional = true }
# For Blender 3.0+ compressed file support.
zstandard = { version = "^0.15", optional = true }
zstandard = { version = "^0.16", optional = true }
[tool.poetry.group.dev.dependencies]
mypy = ">=0.942"
pytest = "^6.2"
pytest-cov = "^3.0.0"
pytest = "^7.4.4"
pytest-cov = "^4.1.0"
twine = "^5.0.0"
# for the 'radon cc' command
radon = "^3.0"
@ -55,5 +55,5 @@ bat = 'blender_asset_tracer.cli:cli_main'
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

View File

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

Binary file not shown.

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1 @@
{"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}}]}}}}}

Binary file not shown.

View File

@ -93,6 +93,10 @@ class StructTest(unittest.TestCase):
self.s_ulong = dna.Struct(b"ulong", 8)
self.s_uint64 = dna.Struct(b"uint64_t", 8)
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_prev = dna.Field(self.s, dna.Name(b"*prev"), 8, 8)
@ -109,6 +113,7 @@ class StructTest(unittest.TestCase):
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_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_prev)
@ -125,6 +130,7 @@ class StructTest(unittest.TestCase):
self.s.append_field(self.f_testint)
self.s.append_field(self.f_testfloat)
self.s.append_field(self.f_testulong)
self.s.append_field(self.f_substruct)
def test_autosize(self):
with self.assertRaises(ValueError):
@ -263,6 +269,19 @@ class StructTest(unittest.TestCase):
self.assertAlmostEqual(2.79, val[1])
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):
fileobj = mock.MagicMock(io.BufferedReader)
value = 255
@ -344,3 +363,10 @@ class StructTest(unittest.TestCase):
expected = struct.pack(b">f", value)
self.s.field_set(self.FakeHeader(), fileobj, b"testfloat", value)
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

@ -46,6 +46,12 @@ class BlendFileBlockTest(AbstractBlendFileTest):
mname = mesh.get((b"id", b"name"), as_str=True)
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):
ob = self.bf.code_index[b"OB"][0]
assert isinstance(ob, blendfile.BlendFileBlock)
@ -297,6 +303,29 @@ class ArrayTest(AbstractBlendFileTest):
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):
def _find_compression_type(self, filename: str) -> magic_compression.Compression:
path = self.blendfiles / filename
@ -477,3 +506,15 @@ class BlendFileCacheTest(AbstractBlendFileTest):
self.assertIs(bf, blendfile._cached_bfiles[other])
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 Normal file → Executable file
View File

@ -694,6 +694,152 @@ 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):
def test_abort_strategise(self):
infile = self.blendfiles / "subdir/doubly_linked_up.blend"

View File

@ -1,4 +1,5 @@
import collections
import functools
import logging
import sys
import typing
@ -369,6 +370,10 @@ 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):
self.assert_deps(
"doubly_linked.blend",
@ -506,6 +511,26 @@ 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):
deps = [
dep
@ -532,6 +557,56 @@ 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):
infinite_bfile = self.blendfiles / "recursive_dependency_1.blend"

View File

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