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, This file logs the changes that are actually interesting to users (new features,
changed functionality, fixed bugs). changed functionality, fixed bugs).
# Version 1.21 (2025-11-24)
- Require Python version 3.11 or newer. Versions up to Python 3.14 are supported.
- Skip packed blend files. BAT will assume that the packed file is self-contained, i.e. any asset used by a packed blend file should also be packed.
- Support Geometry Node simulation caches ([#92890](https://projects.blender.org/blender/blender-asset-tracer/pulls/92890)).
- Add a `py.typed` marker file, such that tooling like mypy know BAT has type annotations ([#92895](https://projects.blender.org/blender/blender-asset-tracer/pulls/92895)).
- Add support for getting individual array items ([#92898](https://projects.blender.org/blender/blender-asset-tracer/pulls/92898)) and iterating dynamic arrays (part of [#92890](https://projects.blender.org/blender/blender-asset-tracer/pulls/92890)).
- Support setting sub-properties via `block.field_set()` ([#92899](https://projects.blender.org/blender/blender-asset-tracer/pulls/92899)).
- Make `bat blocks` print the biggest block memory address as hexadecimal ([#92900](https://projects.blender.org/blender/blender-asset-tracer/pulls/92900)).
# Version 1.20 (2025-07-11)
- Add support for Blender 5.0 compositor node trees ([16c208bc8e13](https://projects.blender.org/blender/blender-asset-tracer/commit/16c208bc8e130c8b1233bdb411ecabdab19af3c5)).
- Make it possible to run BAT with `python -m blender_asset_tracer` ([6c42d06f0590](https://projects.blender.org/blender/blender-asset-tracer/commit/6c42d06f05909d4ac2096e84557d19dd93382f3a)).
- Add support for loading the file sub-version ([4c429e921228](https://projects.blender.org/blender/blender-asset-tracer/commit/4c429e921228259f47795f8ad913ad3eff8fac71)).
# Version 1.19 (2025-06-16) # 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 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. Script to manage assets with Blender.
@ -75,6 +75,7 @@ Mypy likes to see the return type of `__init__` methods explicitly declared as `
BAT can be used as a Python library to inspect the contents of blend files, without having to BAT can be used as a Python library to inspect the contents of blend files, without having to
open Blender itself. Here is an example showing how to determine the render engine used: open Blender itself. Here is an example showing how to determine the render engine used:
```python
#!/usr/bin/env python3.7 #!/usr/bin/env python3.7
import json import json
import sys import sys
@ -118,6 +119,7 @@ open Blender itself. Here is an example showing how to determine the render engi
json.dump(render_info, sys.stdout, indent=4, sort_keys=True) json.dump(render_info, sys.stdout, indent=4, sort_keys=True)
print() print()
```
To understand the naming of the properties, look at Blender's `DNA_xxxx.h` files with struct To understand the naming of the properties, look at Blender's `DNA_xxxx.h` files with struct
definitions. It is those names that are accessed via `blender_asset_tracer.blendfile`. The definitions. It is those names that are accessed via `blender_asset_tracer.blendfile`. The
@ -171,6 +173,6 @@ index-servers =
pip install twine pip install twine
poetry build poetry build
poetry run twine check 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.19.tar.gz dist/blender_asset_tracer-1.19-*.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> # <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.filepath = path
self.raw_filepath = path self.raw_filepath = path
self._is_modified = False self._is_modified = False
self.file_subversion = 0
self.fileobj = self._open_file(path, mode) self.fileobj = self._open_file(path, mode)
self.blocks = [] # type: BFBList self.blocks = [] # type: BFBList
@ -169,6 +170,8 @@ class BlendFile:
if block.code == b"DNA1": if block.code == b"DNA1":
self.decode_structs(block) self.decode_structs(block)
elif block.code == b"GLOB":
self.decode_glob(block)
else: else:
self.fileobj.seek(block.size, os.SEEK_CUR) self.fileobj.seek(block.size, os.SEEK_CUR)
@ -356,6 +359,25 @@ class BlendFile:
dna_struct.append_field(field) dna_struct.append_field(field)
dna_offset += dna_size dna_offset += dna_size
def decode_glob(self, block: "BlendFileBlock") -> None:
"""Partially decode the GLOB block to get the file sub-version."""
# Before this, the subversion didn't exist in 'FileGlobal'.
if self.header.version <= 242:
self.file_subversion = 0
return
# GLOB can appear in the file before DNA1, and so we cannot use DNA to
# parse the fields.
# The subversion is always the `short` at offset 4.
# block_data = io.BytesIO(block.raw_data())
endian = self.header.endian
self.fileobj.seek(4, os.SEEK_CUR) # Skip the next 4 bytes.
self.file_subversion = endian.read_short(self.fileobj)
# Skip to the next block.
self.fileobj.seek(block.file_offset + block.size, os.SEEK_SET)
def abspath(self, relpath: bpathlib.BlendPath) -> bpathlib.BlendPath: def abspath(self, relpath: bpathlib.BlendPath) -> bpathlib.BlendPath:
"""Construct an absolute path from a blendfile-relative path.""" """Construct an absolute path from a blendfile-relative path."""
@ -425,6 +447,12 @@ class BlendFileBlock:
old_structure = struct.Struct(b"4sI") old_structure = struct.Struct(b"4sI")
"""old blend files ENDB block structure""" """old blend files ENDB block structure"""
# Explicitly annotate to avoid `Any` from `.unpack()`.
size: int
addr_old: int
sdna_index: int
count: int
def __init__(self, bfile: BlendFile) -> None: def __init__(self, bfile: BlendFile) -> None:
self.bfile = bfile self.bfile = bfile
@ -568,6 +596,7 @@ class BlendFileBlock:
null_terminated=True, null_terminated=True,
as_str=False, as_str=False,
return_field=False, return_field=False,
array_index=0,
) -> typing.Any: ) -> typing.Any:
"""Read a property and return the value. """Read a property and return the value.
@ -584,8 +613,20 @@ class BlendFileBlock:
(assumes UTF-8 encoding). (assumes UTF-8 encoding).
:param return_field: When True, returns tuple (dna.Field, value). :param return_field: When True, returns tuple (dna.Field, value).
Otherwise just returns the value. Otherwise just returns the value.
:param array_index: If the property is an array, this determines the
index of the returned item from that array. Also see
`blendfile.iterators.dynamic_array()` for iterating such arrays.
""" """
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] dna_struct = self.bfile.structs[self.sdna_index]
field, value = dna_struct.field_get( field, value = dna_struct.field_get(
@ -605,8 +646,8 @@ class BlendFileBlock:
self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET) self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
return self.bfile.fileobj.read(self.size) return self.bfile.fileobj.read(self.size)
def as_string(self) -> str: def as_bytes_string(self) -> bytes:
"""Interpret the bytes of this datablock as null-terminated utf8 string.""" """Interpret the bytes of this datablock as null-terminated string of raw bytes."""
the_bytes = self.raw_data() the_bytes = self.raw_data()
try: try:
first_null = the_bytes.index(0) first_null = the_bytes.index(0)
@ -614,6 +655,11 @@ class BlendFileBlock:
pass pass
else: else:
the_bytes = the_bytes[:first_null] the_bytes = the_bytes[:first_null]
return the_bytes
def as_string(self) -> str:
"""Interpret the bytes of this datablock as null-terminated utf8 string."""
the_bytes = self.as_bytes_string()
return the_bytes.decode() return the_bytes.decode()
def get_recursive_iter( def get_recursive_iter(
@ -680,7 +726,7 @@ class BlendFileBlock:
hsh = zlib.adler32(str(value).encode(), hsh) hsh = zlib.adler32(str(value).encode(), hsh)
return hsh return hsh
def set(self, path: bytes, value): def set(self, path: dna.FieldPath, value):
dna_struct = self.bfile.structs[self.sdna_index] dna_struct = self.bfile.structs[self.sdna_index]
self.bfile.mark_modified() self.bfile.mark_modified()
self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET) self.bfile.fileobj.seek(self.file_offset, os.SEEK_SET)
@ -789,9 +835,13 @@ class BlendFileBlock:
def __getitem__(self, path: dna.FieldPath): def __getitem__(self, path: dna.FieldPath):
return self.get(path) return self.get(path)
def __setitem__(self, item: bytes, value) -> None: def __setitem__(self, item: dna.FieldPath, value) -> None:
self.set(item, value) self.set(item, value)
def has_field(self, name: bytes) -> bool:
dna_struct = self.bfile.structs[self.sdna_index]
return dna_struct.has_field(name)
def keys(self) -> typing.Iterator[bytes]: def keys(self) -> typing.Iterator[bytes]:
"""Generator, yields all field names of this block.""" """Generator, yields all field names of this block."""
return (f.name.name_only for f in self.dna_type.fields) return (f.name.name_only for f in self.dna_type.fields)

View File

@ -310,7 +310,7 @@ class Struct:
self, self,
file_header: header.BlendFileHeader, file_header: header.BlendFileHeader,
fileobj: typing.IO[bytes], fileobj: typing.IO[bytes],
path: bytes, path: FieldPath,
value: typing.Any, value: typing.Any,
): ):
"""Write a value to the blend file. """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 struct on disk (e.g. the start of the BlendFileBlock containing the
data). data).
""" """
assert isinstance(path, bytes), "path should be bytes, but is %s" % type(path)
field, offset = self.field_from_path(file_header.pointer_size, path) field, offset = self.field_from_path(file_header.pointer_size, path)

View File

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

View File

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

View File

@ -52,6 +52,14 @@ eModifierType_MeshSequenceCache = 52
eModifierType_Fluid = 56 eModifierType_Fluid = 56
eModifierType_Nodes = 57 eModifierType_Nodes = 57
# NodesModifierBakeFlag
NODES_MODIFIER_BAKE_CUSTOM_PATH = 1 << 1
# NodesModifierBakeTarget
NODES_MODIFIER_BAKE_TARGET_INHERIT = 0
NODES_MODIFIER_BAKE_TARGET_PACKED = 1
NODES_MODIFIER_BAKE_TARGET_DISK = 2
# DNA_particle_types.h # DNA_particle_types.h
PART_DRAW_OB = 7 PART_DRAW_OB = 7
PART_DRAW_GR = 8 PART_DRAW_GR = 8

View File

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

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

View File

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

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

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

View File

@ -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") @dna_code("LI")
@skip_packed
def library(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]: def library(block: blendfile.BlendFileBlock) -> typing.Iterator[result.BlockUsage]:
"""Library data blocks.""" """Library data blocks."""
path, field = block.get(b"name", return_field=True) path, field = block.get(b"name", return_field=True)

View File

@ -90,7 +90,14 @@ def _expand_generic_mtex(block: blendfile.BlendFileBlock):
def _expand_generic_nodetree(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")) nodes = block.get_pointer((b"nodes", b"first"))
@ -145,6 +152,10 @@ def _expand_generic_idprops(block: blendfile.BlendFileBlock):
def _expand_generic_nodetree_id(block: blendfile.BlendFileBlock): def _expand_generic_nodetree_id(block: blendfile.BlendFileBlock):
if block.bfile.header.version >= 500 and block.bfile.file_subversion >= 4:
# Introduced in Blender 5.0, commit bd61e69be5a7c96f1e5da1c86aafc17b839e049f
block_ntree = block.get_pointer(b"compositing_node_group", None)
else:
block_ntree = block.get_pointer(b"nodetree", None) block_ntree = block.get_pointer(b"nodetree", None)
if block_ntree is not None: if block_ntree is not None:
yield from _expand_generic_nodetree(block_ntree) yield from _expand_generic_nodetree(block_ntree)

View File

@ -27,6 +27,7 @@ import logging
import typing import typing
from blender_asset_tracer import blendfile, bpathlib, cdefs from blender_asset_tracer import blendfile, bpathlib, cdefs
from blender_asset_tracer.blendfile import iterators
from . import result from . import result
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -337,7 +338,7 @@ def modifier_dynamic_paint(
surfaces = canvas_settings.get_pointer((b"surfaces", b"first")) 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) surface_block_name = block_name + b".canvas_settings.surfaces[%d]" % (surf_idx)
point_cache = surface.get_pointer(b"pointcache") point_cache = surface.get_pointer(b"pointcache")
if point_cache is None: if point_cache is None:
@ -351,3 +352,62 @@ def modifier_dynamic_paint(
yield from _walk_point_cache( yield from _walk_point_cache(
ctx, surface_block_name, modifier.bfile, point_cache, cdefs.PTCACHE_EXT 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' author = 'Sybren A. Stüvel'
# The short X.Y version # The short X.Y version
version = '1.19' version = '1.21'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '1.19' release = '1.21'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------

1845
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -46,6 +46,12 @@ class BlendFileBlockTest(AbstractBlendFileTest):
mname = mesh.get((b"id", b"name"), as_str=True) mname = mesh.get((b"id", b"name"), as_str=True)
self.assertEqual("MECube³", mname) self.assertEqual("MECube³", mname)
# Try to access different file-block items.
verts_ptr = mesh.get(b"mvert")
verts = self.bf.block_from_addr[verts_ptr]
assert verts.get(b"co") == [-1.0, -1.0, -1.0]
assert verts.get(b"co", array_index=1) == [-1.0, -1.0, 1.0]
def test_get_recursive_iter(self): def test_get_recursive_iter(self):
ob = self.bf.code_index[b"OB"][0] ob = self.bf.code_index[b"OB"][0]
assert isinstance(ob, blendfile.BlendFileBlock) assert isinstance(ob, blendfile.BlendFileBlock)
@ -297,6 +303,29 @@ class ArrayTest(AbstractBlendFileTest):
self.assertEqual(name, tex.id_name) self.assertEqual(name, tex.id_name)
class DynamicArrayTest(AbstractBlendFileTest):
def test_dynamic_array_of_bakes(self):
self.bf = blendfile.BlendFile(self.blendfiles / "multiple_geometry_nodes_bakes.blend")
obj = self.bf.code_index[b"OB"][0]
assert isinstance(obj, blendfile.BlendFileBlock)
modifier = obj.get_pointer((b"modifiers", b"first"))
assert isinstance(modifier, blendfile.BlendFileBlock)
bakes = modifier.get_pointer(b"bakes")
bake_count = bakes.count
self.assertEqual(3, bake_count)
for i, bake in enumerate(blendfile.iterators.dynamic_array(bakes)):
if i == 0:
frame_start = 37
if i == 1:
frame_start = 5
if i == 2:
frame_start = 12
self.assertEqual(frame_start, bake.get(b"frame_start"))
class CompressionRecognitionTest(AbstractBlendFileTest): class CompressionRecognitionTest(AbstractBlendFileTest):
def _find_compression_type(self, filename: str) -> magic_compression.Compression: def _find_compression_type(self, filename: str) -> magic_compression.Compression:
path = self.blendfiles / filename path = self.blendfiles / filename
@ -477,3 +506,15 @@ class BlendFileCacheTest(AbstractBlendFileTest):
self.assertIs(bf, blendfile._cached_bfiles[other]) self.assertIs(bf, blendfile._cached_bfiles[other])
self.assertEqual(str(bf.raw_filepath), bf.fileobj.name) self.assertEqual(str(bf.raw_filepath), bf.fileobj.name)
class BlendFileSubVersionTest(AbstractBlendFileTest):
def test_file_subversion(self) -> None:
self.bf = blendfile.BlendFile(self.blendfiles / "multiple_materials.blend")
self.assertEqual(self.bf.file_subversion, 3)
self.bf = blendfile.BlendFile(
self.blendfiles
/ "compositor_nodes/compositor_nodes_blender500_library.blend"
)
self.assertEqual(self.bf.file_subversion, 36)

146
tests/test_pack.py 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): class AbortTest(AbstractPackTest):
def test_abort_strategise(self): def test_abort_strategise(self):
infile = self.blendfiles / "subdir/doubly_linked_up.blend" infile = self.blendfiles / "subdir/doubly_linked_up.blend"

View File

@ -1,4 +1,5 @@
import collections import collections
import functools
import logging import logging
import sys import sys
import typing 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): def test_deps_recursive(self):
self.assert_deps( self.assert_deps(
"doubly_linked.blend", "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): def test_usage_abspath(self):
deps = [ deps = [
dep 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): def test_recursion_loop(self):
infinite_bfile = self.blendfiles / "recursive_dependency_1.blend" infinite_bfile = self.blendfiles / "recursive_dependency_1.blend"

View File

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