Compare commits

...

40 Commits
v1.18 ... 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
Sybren A. Stüvel
2489e4cbbc Upgrade Twine 4.0 → 5.0
There was an error running `twine check` (`KeyError: 'license'`) that was
fixed by upgrading to 5.0. This version was chosen as it's the one in
the current Ubuntu LTS release.
2025-06-16 12:38:13 +02:00
Sybren A. Stüvel
9223a4d7b9 Bumped version to 1.19 2025-06-16 12:29:45 +02:00
Sybren A. Stüvel
896c4a3ca4 README: update development environment setup instructions 2025-06-16 11:59:51 +02:00
Sybren A. Stüvel
85cf257d2c Upgrade Sphinx to the version bundled with Ubuntu 24.04 LTS
The old version of Sphinx depended on the `pathtools` library, which had
issues installing on my macbook.
2025-06-16 11:52:52 +02:00
Sybren A. Stüvel
03fa3f2d18 Drop support for Python 3.8
It is EOL and no longer supported by the Python foundation.

Also it is getting in the way of upgrading Sphinx.
2025-06-16 11:51:09 +02:00
Sybren A. Stüvel
b1d49627b1 Add Python 3.12 and 3.13 to the Tox environments 2025-06-16 11:41:00 +02:00
Sybren A. Stüvel
876dddb964 CHANGELOG: update log for 1.19 and mark it as released today 2025-06-16 11:26:00 +02:00
Sybren A. Stüvel
12ce4bfc6d pyproject.toml: modernize use of deprecated tool.poetry.dev-dependencies
Use `[tool.poetry.group.dev.dependencies]` instead of the deprecated
`[tool.poetry.dev-dependencies]` section.
2025-06-16 11:24:13 +02:00
Jacques Lucke
f1ee7980b2 Support .blend files saved with large bhead
This is mostly the same as blender/blender!140195. The header parsing code has
been updated to be able to read old and new .blend file headers.

There is a new test file which is the same as the existing `basic_file.blend`,
but saved with the new header format. A new unit test has been added to check
that this file is read correctly as well.

Pull Request: https://projects.blender.org/blender/blender-asset-tracer/pulls/92893
2025-06-13 12:25:51 +02:00
Sybren A. Stüvel
eb69ca5632 Add EndianIO.parse_pointer function
This is to parse in-memory pointer data bytes into an actual pointer value.
2025-01-24 15:34:46 +01:00
Sybren A. Stüvel
f2e28edc05 Implement reader & writer for int8_t type 2024-09-02 18:20:51 +02:00
Sybren A. Stüvel
e3d3d988b7 Re-lock package dependencies
Re-ran `poetry lock` to regenerate the `poetry.lock` file.
2024-09-02 18:20:51 +02:00
Sybren A. Stüvel
073bc8140a Update CHANGELOG 2024-09-02 18:20:51 +02:00
jonasdichelle
16a092ddf1 Add Support for Dynamic Paint Modifier
Add support for the Dynamic Paint modifier point cache.

I added a walker to iterate through all surfaces on a canvas to get each surface's point cache.

Reviewed-on: https://projects.blender.org/blender/blender-asset-tracer/pulls/92889
Reviewed-by: Sybren A. Stüvel <sybren@blender.org>
2024-09-02 18:19:07 +02:00
Bastien Montagne
fe0b3e8f5e Add a simple, direct way to use BAT, without requiring to setup a venv etc.
Allows to use `python3 path/to/cli.py my args` just from source code,
without requiring to set up a whole venv for that. Much simpler when
only 'regular' debuging in code logic itself is needed.
2024-08-13 18:01:38 +02:00
68 changed files with 2486 additions and 1130 deletions

View File

@ -3,6 +3,30 @@
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)).
- 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)
- 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)).

100
README.md
View File

@ -1,4 +1,4 @@
# Blender Asset Tracer BAT🦇
# Blender Asset Tracer BAT🦇 (ADV fork)
Script to manage assets with Blender.
@ -8,15 +8,23 @@ Blender Asset Tracer, a.k.a. BAT🦇, is the replacement of
Development is driven by choices explained in [T54125](https://developer.blender.org/T54125).
## Setting up development environment
## 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.9 -m venv .venv
. ./.venv/bin/activate
pip install -U pip
pip install poetry black
poetry install
mypy --install-types
python3 path/to/repo/cli.py list path/to/blendfile.blend
```
## 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
```
@ -67,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
@ -163,6 +173,6 @@ index-servers =
pip install twine
poetry build
twine check dist/blender_asset_tracer-1.18.tar.gz dist/blender_asset_tracer-1.18-*.whl
twine upload -r bat dist/blender_asset_tracer-1.18.tar.gz dist/blender_asset_tracer-1.18-*.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.18"
__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
@ -135,7 +136,7 @@ class BlendFile:
self.block_from_addr = {} # type: typing.Dict[int, BlendFileBlock]
self.header = header.BlendFileHeader(self.fileobj, self.raw_filepath)
self.block_header_struct = self.header.create_block_header_struct()
self.block_header_struct, self.block_header_fields = self.header.create_block_header_struct()
self._load_blocks()
def _open_file(self, path: pathlib.Path, mode: str) -> typing.IO[bytes]:
@ -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
@ -455,23 +483,13 @@ class BlendFileBlock:
self.code = b"ENDB"
return
# header size can be 8, 20, or 24 bytes long
# 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])
blockheader = bfile.block_header_fields(*header_struct.unpack(data))
self.code = self.endian.read_data0(blockheader.code)
if self.code != b"ENDB":
self.size = blockheader[1]
self.addr_old = blockheader[2]
self.sdna_index = blockheader[3]
self.count = blockheader[4]
self.size = blockheader.len
self.addr_old = blockheader.old
self.sdna_index = blockheader.SDNAnr
self.count = blockheader.nr
self.file_offset = bfile.fileobj.tell()
def __repr__(self) -> str:
@ -578,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.
@ -594,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(
@ -615,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)
@ -624,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(
@ -690,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)
@ -799,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

@ -260,6 +260,7 @@ class Struct:
b"short": endian.read_short,
b"uint64_t": endian.read_ulong,
b"float": endian.read_float,
b"int8_t": endian.read_int8,
}
try:
simple_reader = simple_readers[dna_type.dna_type_id]
@ -309,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.
@ -318,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

@ -28,6 +28,7 @@ import typing
class EndianIO:
# TODO(Sybren): note as UCHAR: struct.Struct = None and move actual structs to LittleEndianTypes
UCHAR = struct.Struct(b"<B")
SINT8 = struct.Struct(b"<b")
USHORT = struct.Struct(b"<H")
USHORT2 = struct.Struct(b"<HH") # two shorts in a row
SSHORT = struct.Struct(b"<h")
@ -62,6 +63,14 @@ class EndianIO:
def write_char(cls, fileobj: typing.IO[bytes], value: int):
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
def read_ushort(cls, fileobj: typing.IO[bytes]):
return cls._read(fileobj, cls.USHORT)
@ -120,6 +129,20 @@ class EndianIO:
return cls.read_ulong(fileobj)
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
def write_pointer(cls, fileobj: typing.IO[bytes], pointer_size: int, value: int):
"""Write a pointer to a file."""
@ -181,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
@ -207,6 +230,7 @@ class EndianIO:
"""
return {
b"char": cls.write_char,
b"int8": cls.write_int8,
b"ushort": cls.write_ushort,
b"short": cls.write_short,
b"uint": cls.write_uint,
@ -222,6 +246,7 @@ class LittleEndianTypes(EndianIO):
class BigEndianTypes(LittleEndianTypes):
UCHAR = struct.Struct(b">B")
SINT8 = struct.Struct(b">b")
USHORT = struct.Struct(b">H")
USHORT2 = struct.Struct(b">HH") # two shorts in a row
SSHORT = struct.Struct(b">h")

View File

@ -19,6 +19,8 @@
# (c) 2009, At Mind B.V. - Jeroen Bakker
# (c) 2014, Blender Foundation - Campbell Barton
# (c) 2018, Blender Foundation - Sybren A. Stüvel
from dataclasses import dataclass
import logging
import os
import pathlib
@ -30,58 +32,151 @@ from . import dna_io, exceptions
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:
"""
BlendFileHeader represents the first 12 bytes of a blend file.
BlendFileHeader represents the first 12-17 bytes of a blend file.
It contains information about the hardware architecture, which is relevant
to the structure of the rest of the file.
"""
structure = struct.Struct(b"7s1s1s3s")
magic: bytes
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:
log.debug("reading blend-file-header %s", path)
fileobj.seek(0, os.SEEK_SET)
header = fileobj.read(self.structure.size)
values = self.structure.unpack(header)
self.magic = values[0]
bytes_0_6 = fileobj.read(7)
if bytes_0_6 != b'BLENDER':
raise exceptions.BlendFileError("invalid first bytes %r" % bytes_0_6, path)
self.magic = bytes_0_6
pointer_size_id = values[1]
if pointer_size_id == b"-":
byte_7 = fileobj.read(1)
is_legacy_header = byte_7 in (b'_', b'-')
if is_legacy_header:
self.file_format_version = 0
if byte_7 == b'_':
self.pointer_size = 4
elif byte_7 == b'-':
self.pointer_size = 8
else:
raise exceptions.BlendFileError("invalid pointer size %r" % byte_7, path)
byte_8 = fileobj.read(1)
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
elif pointer_size_id == b"_":
self.pointer_size = 4
else:
raise exceptions.BlendFileError(
"invalid pointer size %r" % pointer_size_id, path
)
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)
endian_id = values[2]
if endian_id == b"v":
if self.is_little_endian:
self.endian_str = b'<'
self.endian = dna_io.LittleEndianTypes
self.endian_str = b"<" # indication for struct.Struct()
elif endian_id == b"V":
self.endian = dna_io.BigEndianTypes
self.endian_str = b">" # indication for struct.Struct()
else:
raise exceptions.BlendFileError(
"invalid endian indicator %r" % endian_id, path
)
self.endian_str = b'>'
self.endian = dna_io.BigEndianTypes
version_id = values[3]
self.version = int(version_id)
def create_block_header_struct(self) -> typing.Tuple[struct.Struct, typing.Type[typing.Union[BHead4, SmallBHead8, LargeBHead8]]]:
"""
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
def create_block_header_struct(self) -> struct.Struct:
"""Create a Struct instance for parsing data block headers."""
return struct.Struct(
b"".join(
(
self.endian_str,
b"4sI",
b"I" if self.pointer_size == 4 else b"Q",
b"II",
)
)
)
if self.pointer_size == 4:
header_struct = struct.Struct(b''.join((
self.endian_str,
# BHead4.code
b'4s',
# BHead4.len
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,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

@ -46,11 +46,20 @@ eModifierType_WeightVGEdit = 36
eModifierType_WeightVGMix = 37
eModifierType_WeightVGProximity = 38
eModifierType_Ocean = 39
eModifierType_DynamicPaint = 40
eModifierType_MeshCache = 46
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

@ -22,10 +22,12 @@
The modifier_xxx() functions all yield result.BlockUsage objects for external
files used by the modifiers.
"""
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__)
@ -317,3 +319,95 @@ def modifier_cloth(
yield from _walk_point_cache(
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 Normal file
View File

@ -0,0 +1,29 @@
# ##### 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'
# The short X.Y version
version = '1.18'
version = '1.21'
# The full version, including alpha/beta/rc tags
release = '1.18'
release = '1.21'
# -- General configuration ---------------------------------------------------

2378
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.18"
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,29 +24,29 @@ s3 = ["boto3"]
zstandard = ["zstandard"]
[tool.poetry.dependencies]
python = "^3.8"
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.dev-dependencies]
[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"
# for converting profiler output to KCacheGrind input
"pyprof2calltree" = "*"
# For building documentation
sphinx = "^2.1"
sphinx-autobuild = "^0.7"
sphinx-rtd-theme = "^0.4"
sphinx = "^7.2"
sphinx-autobuild = "^2024.3"
sphinx-rtd-theme = "^2.0"
responses = "^0.10"
pathlib2 = { version = "^2.3", python = "<3.6" }
tox = "^3.12"
types-requests = "^2.25.0"
@ -54,9 +54,6 @@ types-requests = "^2.25.0"
bat = 'blender_asset_tracer.cli:cli_main'
[tool.poetry.group.dev.dependencies]
twine = "^4.0.1"
[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.8
python_version = 3.11
warn_redundant_casts = True
ignore_missing_imports = True

Binary file not shown.

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

@ -13,6 +13,7 @@ class BlendFileBlockTest(AbstractBlendFileTest):
def test_loading(self):
self.assertFalse(self.bf.is_compressed)
self.assertEqual(0, self.bf.header.file_format_version)
def test_some_properties(self):
ob = self.bf.code_index[b"OB"][0]
@ -45,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)
@ -154,6 +161,23 @@ class BlendFileBlockTest(AbstractBlendFileTest):
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):
def setUp(self):
self.bf = blendfile.BlendFile(self.blendfiles / "with_sequencer.blend")
@ -279,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
@ -459,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 = py38, py39, py310, py311
envlist = py311, py312, py313, py314
[testenv]
whitelist_externals = poetry

View File

@ -19,5 +19,5 @@ echo git tag -a v$1 -m \'Tagged version $1\'
echo
echo "Build the package & upload to PyPi using:"
echo "poetry build"
echo "twine check 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"
echo "poetry run 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"