Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4085f0015f | |||
| cf31dbc5ec | |||
| bb1de717c8 | |||
| 12e6e2643b | |||
| cbbe8be0d8 | |||
|
|
6af9567a44 | ||
|
|
74860b3107 | ||
|
|
5c0edf4c07 | ||
|
|
a973637896 | ||
|
|
7322c0772a | ||
|
|
3b61399f7f | ||
|
|
aceea0d823 | ||
|
|
2c0fe87fba | ||
|
|
47c3e8d77f | ||
|
|
c69612b264 | ||
|
|
b1c4f5e116 | ||
|
|
5690c8e7cb | ||
|
|
50578ac62a | ||
|
|
94486a3218 | ||
|
|
95165a0330 | ||
|
|
8a17495566 | ||
|
|
08b37a35f8 | ||
|
|
6c42d06f05 | ||
|
|
16c208bc8e | ||
|
|
4c429e9212 | ||
|
|
2489e4cbbc | ||
|
|
9223a4d7b9 | ||
|
|
896c4a3ca4 | ||
|
|
85cf257d2c | ||
|
|
03fa3f2d18 | ||
|
|
b1d49627b1 | ||
|
|
876dddb964 | ||
|
|
12ce4bfc6d | ||
|
|
f1ee7980b2 | ||
|
|
eb69ca5632 | ||
|
|
f2e28edc05 | ||
|
|
e3d3d988b7 | ||
|
|
073bc8140a | ||
|
|
16a092ddf1 | ||
|
|
fe0b3e8f5e |
24
CHANGELOG.md
24
CHANGELOG.md
@ -3,6 +3,30 @@
|
|||||||
This file logs the changes that are actually interesting to users (new features,
|
This file logs the changes that are actually interesting to users (new features,
|
||||||
changed functionality, fixed bugs).
|
changed functionality, fixed bugs).
|
||||||
|
|
||||||
|
# Version 1.21 (2025-11-24)
|
||||||
|
|
||||||
|
- Require Python version 3.11 or newer. Versions up to Python 3.14 are supported.
|
||||||
|
- Skip packed blend files. BAT will assume that the packed file is self-contained, i.e. any asset used by a packed blend file should also be packed.
|
||||||
|
- Support Geometry Node simulation caches ([#92890](https://projects.blender.org/blender/blender-asset-tracer/pulls/92890)).
|
||||||
|
- Add a `py.typed` marker file, such that tooling like mypy know BAT has type annotations ([#92895](https://projects.blender.org/blender/blender-asset-tracer/pulls/92895)).
|
||||||
|
- Add support for getting individual array items ([#92898](https://projects.blender.org/blender/blender-asset-tracer/pulls/92898)) and iterating dynamic arrays (part of [#92890](https://projects.blender.org/blender/blender-asset-tracer/pulls/92890)).
|
||||||
|
- Support setting sub-properties via `block.field_set()` ([#92899](https://projects.blender.org/blender/blender-asset-tracer/pulls/92899)).
|
||||||
|
- Make `bat blocks` print the biggest block memory address as hexadecimal ([#92900](https://projects.blender.org/blender/blender-asset-tracer/pulls/92900)).
|
||||||
|
|
||||||
|
|
||||||
|
# Version 1.20 (2025-07-11)
|
||||||
|
|
||||||
|
- Add support for Blender 5.0 compositor node trees ([16c208bc8e13](https://projects.blender.org/blender/blender-asset-tracer/commit/16c208bc8e130c8b1233bdb411ecabdab19af3c5)).
|
||||||
|
- Make it possible to run BAT with `python -m blender_asset_tracer` ([6c42d06f0590](https://projects.blender.org/blender/blender-asset-tracer/commit/6c42d06f05909d4ac2096e84557d19dd93382f3a)).
|
||||||
|
- Add support for loading the file sub-version ([4c429e921228](https://projects.blender.org/blender/blender-asset-tracer/commit/4c429e921228259f47795f8ad913ad3eff8fac71)).
|
||||||
|
|
||||||
|
|
||||||
|
# Version 1.19 (2025-06-16)
|
||||||
|
|
||||||
|
- Add support for tracing dynamic paint caches ([#92889](https://projects.blender.org/blender/blender-asset-tracer/pulls/92889)).
|
||||||
|
- Add support for the large blendfile header blocks ([#92893](https://projects.blender.org/blender/blender-asset-tracer/pulls/92893)). This is necessary for compatibility with Blender 5.0.
|
||||||
|
- Drop support for Python 3.8.
|
||||||
|
|
||||||
# Version 1.18 (2024-01-11)
|
# Version 1.18 (2024-01-11)
|
||||||
|
|
||||||
- When logging that there is no reader implemented for a certain data-block type, the filepath of the blend file that contains that data-block is now included in the message ([#92885](https://projects.blender.org/blender/blender-asset-tracer/pulls/92885)).
|
- When logging that there is no reader implemented for a certain data-block type, the filepath of the blend file that contains that data-block is now included in the message ([#92885](https://projects.blender.org/blender/blender-asset-tracer/pulls/92885)).
|
||||||
|
|||||||
82
README.md
82
README.md
@ -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.
|
||||||
|
|
||||||
@ -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).
|
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
|
python3 path/to/repo/cli.py list path/to/blendfile.blend
|
||||||
. ./.venv/bin/activate
|
```
|
||||||
pip install -U pip
|
|
||||||
pip install poetry black
|
## Setting up development environment
|
||||||
poetry install
|
|
||||||
mypy --install-types
|
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
|
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:
|
||||||
|
|
||||||
#!/usr/bin/env python3.7
|
```python
|
||||||
import json
|
#!/usr/bin/env python3.7
|
||||||
import sys
|
import json
|
||||||
from pathlib import Path
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from blender_asset_tracer import blendfile
|
from blender_asset_tracer import blendfile
|
||||||
from blender_asset_tracer.blendfile import iterators
|
from blender_asset_tracer.blendfile import iterators
|
||||||
|
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
print(f'Usage: {sys.argv[0]} somefile.blend', file=sys.stderr)
|
print(f'Usage: {sys.argv[0]} somefile.blend', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
bf_path = Path(sys.argv[1])
|
bf_path = Path(sys.argv[1])
|
||||||
bf = blendfile.open_cached(bf_path)
|
bf = blendfile.open_cached(bf_path)
|
||||||
|
|
||||||
# Get the first window manager (there is probably exactly one).
|
# Get the first window manager (there is probably exactly one).
|
||||||
window_managers = bf.find_blocks_from_code(b'WM')
|
window_managers = bf.find_blocks_from_code(b'WM')
|
||||||
assert window_managers, 'The Blend file has no window manager'
|
assert window_managers, 'The Blend file has no window manager'
|
||||||
window_manager = window_managers[0]
|
window_manager = window_managers[0]
|
||||||
|
|
||||||
# Get the scene from the first window.
|
# Get the scene from the first window.
|
||||||
windows = window_manager.get_pointer((b'windows', b'first'))
|
windows = window_manager.get_pointer((b'windows', b'first'))
|
||||||
for window in iterators.listbase(windows):
|
for window in iterators.listbase(windows):
|
||||||
scene = window.get_pointer(b'scene')
|
scene = window.get_pointer(b'scene')
|
||||||
break
|
break
|
||||||
|
|
||||||
# BAT can only return simple values, so it can't return the embedded
|
# BAT can only return simple values, so it can't return the embedded
|
||||||
# struct 'r'. 'r.engine' is a simple string, though.
|
# struct 'r'. 'r.engine' is a simple string, though.
|
||||||
engine = scene[b'r', b'engine'].decode('utf8')
|
engine = scene[b'r', b'engine'].decode('utf8')
|
||||||
xsch = scene[b'r', b'xsch']
|
xsch = scene[b'r', b'xsch']
|
||||||
ysch = scene[b'r', b'ysch']
|
ysch = scene[b'r', b'ysch']
|
||||||
size = scene[b'r', b'size'] / 100.0
|
size = scene[b'r', b'size'] / 100.0
|
||||||
|
|
||||||
render_info = {
|
render_info = {
|
||||||
'engine': engine,
|
'engine': engine,
|
||||||
'frame_pixels': {
|
'frame_pixels': {
|
||||||
'x': int(xsch * size),
|
'x': int(xsch * size),
|
||||||
'y': int(ysch * size),
|
'y': int(ysch * size),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -163,6 +173,6 @@ index-servers =
|
|||||||
pip install twine
|
pip install twine
|
||||||
|
|
||||||
poetry build
|
poetry build
|
||||||
twine check 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
|
||||||
twine upload -r bat dist/blender_asset_tracer-1.18.tar.gz dist/blender_asset_tracer-1.18-*.whl
|
poetry run twine upload -r bat dist/blender_asset_tracer-1.21.tar.gz dist/blender_asset_tracer-1.21-*.whl
|
||||||
```
|
```
|
||||||
|
|||||||
@ -20,4 +20,57 @@
|
|||||||
|
|
||||||
# <pep8 compliant>
|
# <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()
|
||||||
|
|||||||
3
blender_asset_tracer/__main__.py
Normal file
3
blender_asset_tracer/__main__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from blender_asset_tracer import cli
|
||||||
|
|
||||||
|
cli.cli_main()
|
||||||
@ -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
|
||||||
@ -135,7 +136,7 @@ class BlendFile:
|
|||||||
self.block_from_addr = {} # type: typing.Dict[int, BlendFileBlock]
|
self.block_from_addr = {} # type: typing.Dict[int, BlendFileBlock]
|
||||||
|
|
||||||
self.header = header.BlendFileHeader(self.fileobj, self.raw_filepath)
|
self.header = header.BlendFileHeader(self.fileobj, self.raw_filepath)
|
||||||
self.block_header_struct = self.header.create_block_header_struct()
|
self.block_header_struct, self.block_header_fields = self.header.create_block_header_struct()
|
||||||
self._load_blocks()
|
self._load_blocks()
|
||||||
|
|
||||||
def _open_file(self, path: pathlib.Path, mode: str) -> typing.IO[bytes]:
|
def _open_file(self, path: pathlib.Path, mode: str) -> typing.IO[bytes]:
|
||||||
@ -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
|
||||||
|
|
||||||
@ -455,23 +483,13 @@ class BlendFileBlock:
|
|||||||
self.code = b"ENDB"
|
self.code = b"ENDB"
|
||||||
return
|
return
|
||||||
|
|
||||||
# header size can be 8, 20, or 24 bytes long
|
blockheader = bfile.block_header_fields(*header_struct.unpack(data))
|
||||||
# 8: old blend files ENDB block (exception)
|
self.code = self.endian.read_data0(blockheader.code)
|
||||||
# 20: normal headers 32 bit platform
|
|
||||||
# 24: normal headers 64 bit platform
|
|
||||||
if len(data) <= 15:
|
|
||||||
self.log.debug("interpreting block as old-style ENB block")
|
|
||||||
blockheader = self.old_structure.unpack(data)
|
|
||||||
self.code = self.endian.read_data0(blockheader[0])
|
|
||||||
return
|
|
||||||
|
|
||||||
blockheader = header_struct.unpack(data)
|
|
||||||
self.code = self.endian.read_data0(blockheader[0])
|
|
||||||
if self.code != b"ENDB":
|
if self.code != b"ENDB":
|
||||||
self.size = blockheader[1]
|
self.size = blockheader.len
|
||||||
self.addr_old = blockheader[2]
|
self.addr_old = blockheader.old
|
||||||
self.sdna_index = blockheader[3]
|
self.sdna_index = blockheader.SDNAnr
|
||||||
self.count = blockheader[4]
|
self.count = blockheader.nr
|
||||||
self.file_offset = bfile.fileobj.tell()
|
self.file_offset = bfile.fileobj.tell()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@ -578,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.
|
||||||
|
|
||||||
@ -594,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(
|
||||||
@ -615,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)
|
||||||
@ -624,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(
|
||||||
@ -690,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)
|
||||||
@ -799,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)
|
||||||
|
|||||||
@ -260,6 +260,7 @@ class Struct:
|
|||||||
b"short": endian.read_short,
|
b"short": endian.read_short,
|
||||||
b"uint64_t": endian.read_ulong,
|
b"uint64_t": endian.read_ulong,
|
||||||
b"float": endian.read_float,
|
b"float": endian.read_float,
|
||||||
|
b"int8_t": endian.read_int8,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
simple_reader = simple_readers[dna_type.dna_type_id]
|
simple_reader = simple_readers[dna_type.dna_type_id]
|
||||||
@ -309,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.
|
||||||
@ -318,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)
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import typing
|
|||||||
class EndianIO:
|
class EndianIO:
|
||||||
# TODO(Sybren): note as UCHAR: struct.Struct = None and move actual structs to LittleEndianTypes
|
# TODO(Sybren): note as UCHAR: struct.Struct = None and move actual structs to LittleEndianTypes
|
||||||
UCHAR = struct.Struct(b"<B")
|
UCHAR = struct.Struct(b"<B")
|
||||||
|
SINT8 = struct.Struct(b"<b")
|
||||||
USHORT = struct.Struct(b"<H")
|
USHORT = struct.Struct(b"<H")
|
||||||
USHORT2 = struct.Struct(b"<HH") # two shorts in a row
|
USHORT2 = struct.Struct(b"<HH") # two shorts in a row
|
||||||
SSHORT = struct.Struct(b"<h")
|
SSHORT = struct.Struct(b"<h")
|
||||||
@ -62,6 +63,14 @@ class EndianIO:
|
|||||||
def write_char(cls, fileobj: typing.IO[bytes], value: int):
|
def write_char(cls, fileobj: typing.IO[bytes], value: int):
|
||||||
return cls._write(fileobj, cls.UCHAR, value)
|
return cls._write(fileobj, cls.UCHAR, value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_int8(cls, fileobj: typing.IO[bytes]):
|
||||||
|
return cls._read(fileobj, cls.SINT8)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write_int8(cls, fileobj: typing.IO[bytes], value: int):
|
||||||
|
return cls._write(fileobj, cls.SINT8, value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_ushort(cls, fileobj: typing.IO[bytes]):
|
def read_ushort(cls, fileobj: typing.IO[bytes]):
|
||||||
return cls._read(fileobj, cls.USHORT)
|
return cls._read(fileobj, cls.USHORT)
|
||||||
@ -120,6 +129,20 @@ class EndianIO:
|
|||||||
return cls.read_ulong(fileobj)
|
return cls.read_ulong(fileobj)
|
||||||
raise ValueError("unsupported pointer size %d" % pointer_size)
|
raise ValueError("unsupported pointer size %d" % pointer_size)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_pointer(cls, pointer_data: bytes):
|
||||||
|
"""Parse bytes as a pointer value."""
|
||||||
|
|
||||||
|
pointer_size = len(pointer_data)
|
||||||
|
try:
|
||||||
|
typestruct = {
|
||||||
|
4: cls.UINT,
|
||||||
|
8: cls.ULONG,
|
||||||
|
}[pointer_size]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError("unsupported pointer size %d" % pointer_size)
|
||||||
|
return typestruct.unpack(pointer_data)[0]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_pointer(cls, fileobj: typing.IO[bytes], pointer_size: int, value: int):
|
def write_pointer(cls, fileobj: typing.IO[bytes], pointer_size: int, value: int):
|
||||||
"""Write a pointer to a file."""
|
"""Write a pointer to a file."""
|
||||||
@ -181,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
|
||||||
@ -207,6 +230,7 @@ class EndianIO:
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
b"char": cls.write_char,
|
b"char": cls.write_char,
|
||||||
|
b"int8": cls.write_int8,
|
||||||
b"ushort": cls.write_ushort,
|
b"ushort": cls.write_ushort,
|
||||||
b"short": cls.write_short,
|
b"short": cls.write_short,
|
||||||
b"uint": cls.write_uint,
|
b"uint": cls.write_uint,
|
||||||
@ -222,6 +246,7 @@ class LittleEndianTypes(EndianIO):
|
|||||||
|
|
||||||
class BigEndianTypes(LittleEndianTypes):
|
class BigEndianTypes(LittleEndianTypes):
|
||||||
UCHAR = struct.Struct(b">B")
|
UCHAR = struct.Struct(b">B")
|
||||||
|
SINT8 = struct.Struct(b">b")
|
||||||
USHORT = struct.Struct(b">H")
|
USHORT = struct.Struct(b">H")
|
||||||
USHORT2 = struct.Struct(b">HH") # two shorts in a row
|
USHORT2 = struct.Struct(b">HH") # two shorts in a row
|
||||||
SSHORT = struct.Struct(b">h")
|
SSHORT = struct.Struct(b">h")
|
||||||
|
|||||||
@ -19,6 +19,8 @@
|
|||||||
# (c) 2009, At Mind B.V. - Jeroen Bakker
|
# (c) 2009, At Mind B.V. - Jeroen Bakker
|
||||||
# (c) 2014, Blender Foundation - Campbell Barton
|
# (c) 2014, Blender Foundation - Campbell Barton
|
||||||
# (c) 2018, Blender Foundation - Sybren A. Stüvel
|
# (c) 2018, Blender Foundation - Sybren A. Stüvel
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
@ -30,58 +32,151 @@ from . import dna_io, exceptions
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BHead4:
|
||||||
|
code: bytes
|
||||||
|
len: int
|
||||||
|
old: int
|
||||||
|
SDNAnr: int
|
||||||
|
nr: int
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SmallBHead8:
|
||||||
|
code: bytes
|
||||||
|
len: int
|
||||||
|
old: int
|
||||||
|
SDNAnr: int
|
||||||
|
nr: int
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LargeBHead8:
|
||||||
|
code: bytes
|
||||||
|
SDNAnr: int
|
||||||
|
old: int
|
||||||
|
len: int
|
||||||
|
nr: int
|
||||||
|
|
||||||
class BlendFileHeader:
|
class BlendFileHeader:
|
||||||
"""
|
"""
|
||||||
BlendFileHeader represents the first 12 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
|
It contains information about the hardware architecture, which is relevant
|
||||||
to the structure of the rest of the file.
|
to the structure of the rest of the file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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:
|
def __init__(self, fileobj: typing.IO[bytes], path: pathlib.Path) -> None:
|
||||||
log.debug("reading blend-file-header %s", path)
|
log.debug("reading blend-file-header %s", path)
|
||||||
fileobj.seek(0, os.SEEK_SET)
|
fileobj.seek(0, os.SEEK_SET)
|
||||||
header = fileobj.read(self.structure.size)
|
|
||||||
values = self.structure.unpack(header)
|
|
||||||
|
|
||||||
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]
|
byte_7 = fileobj.read(1)
|
||||||
if pointer_size_id == b"-":
|
is_legacy_header = byte_7 in (b'_', b'-')
|
||||||
self.pointer_size = 8
|
if is_legacy_header:
|
||||||
elif pointer_size_id == b"_":
|
self.file_format_version = 0
|
||||||
|
if byte_7 == b'_':
|
||||||
self.pointer_size = 4
|
self.pointer_size = 4
|
||||||
|
elif byte_7 == b'-':
|
||||||
|
self.pointer_size = 8
|
||||||
else:
|
else:
|
||||||
raise exceptions.BlendFileError(
|
raise exceptions.BlendFileError("invalid pointer size %r" % byte_7, path)
|
||||||
"invalid pointer size %r" % pointer_size_id, 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
|
||||||
|
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 self.is_little_endian:
|
||||||
if endian_id == b"v":
|
self.endian_str = b'<'
|
||||||
self.endian = dna_io.LittleEndianTypes
|
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:
|
else:
|
||||||
raise exceptions.BlendFileError(
|
self.endian_str = b'>'
|
||||||
"invalid endian indicator %r" % endian_id, path
|
self.endian = dna_io.BigEndianTypes
|
||||||
)
|
|
||||||
|
|
||||||
version_id = values[3]
|
def create_block_header_struct(self) -> typing.Tuple[struct.Struct, typing.Type[typing.Union[BHead4, SmallBHead8, LargeBHead8]]]:
|
||||||
self.version = int(version_id)
|
"""
|
||||||
|
Returns a Struct instance for parsing data block headers and a corresponding
|
||||||
def create_block_header_struct(self) -> struct.Struct:
|
Python class for accessing the right members. Ddepending on the .blend file,
|
||||||
"""Create a Struct instance for parsing data block headers."""
|
the order of the data members in the block header may be different.
|
||||||
return struct.Struct(
|
"""
|
||||||
b"".join(
|
assert self.file_format_version in (0, 1)
|
||||||
(
|
if self.file_format_version == 1:
|
||||||
|
header_struct = struct.Struct(b''.join((
|
||||||
self.endian_str,
|
self.endian_str,
|
||||||
b"4sI",
|
# LargeBHead8.code
|
||||||
b"I" if self.pointer_size == 4 else b"Q",
|
b'4s',
|
||||||
b"II",
|
# LargeBHead8.SDNAnr
|
||||||
)
|
b'i',
|
||||||
)
|
# LargeBHead8.old
|
||||||
)
|
b'Q',
|
||||||
|
# LargeBHead8.len
|
||||||
|
b'q',
|
||||||
|
# LargeBHead8.nr
|
||||||
|
b'q',
|
||||||
|
)))
|
||||||
|
return header_struct, LargeBHead8
|
||||||
|
|
||||||
|
if self.pointer_size == 4:
|
||||||
|
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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -46,11 +46,20 @@ eModifierType_WeightVGEdit = 36
|
|||||||
eModifierType_WeightVGMix = 37
|
eModifierType_WeightVGMix = 37
|
||||||
eModifierType_WeightVGProximity = 38
|
eModifierType_WeightVGProximity = 38
|
||||||
eModifierType_Ocean = 39
|
eModifierType_Ocean = 39
|
||||||
|
eModifierType_DynamicPaint = 40
|
||||||
eModifierType_MeshCache = 46
|
eModifierType_MeshCache = 46
|
||||||
eModifierType_MeshSequenceCache = 52
|
eModifierType_MeshSequenceCache = 52
|
||||||
eModifierType_Fluid = 56
|
eModifierType_Fluid = 56
|
||||||
eModifierType_Nodes = 57
|
eModifierType_Nodes = 57
|
||||||
|
|
||||||
|
# NodesModifierBakeFlag
|
||||||
|
NODES_MODIFIER_BAKE_CUSTOM_PATH = 1 << 1
|
||||||
|
|
||||||
|
# NodesModifierBakeTarget
|
||||||
|
NODES_MODIFIER_BAKE_TARGET_INHERIT = 0
|
||||||
|
NODES_MODIFIER_BAKE_TARGET_PACKED = 1
|
||||||
|
NODES_MODIFIER_BAKE_TARGET_DISK = 2
|
||||||
|
|
||||||
# DNA_particle_types.h
|
# DNA_particle_types.h
|
||||||
PART_DRAW_OB = 7
|
PART_DRAW_OB = 7
|
||||||
PART_DRAW_GR = 8
|
PART_DRAW_GR = 8
|
||||||
|
|||||||
@ -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
21
blender_asset_tracer/cli/pack.py
Normal file → Executable 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:
|
||||||
|
|||||||
163
blender_asset_tracer/operators.py
Normal file
163
blender_asset_tracer/operators.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.types import Operator
|
||||||
|
from bpy_extras.io_utils import ExportHelper
|
||||||
|
|
||||||
|
from blender_asset_tracer.pack import zipped
|
||||||
|
|
||||||
|
|
||||||
|
class ExportBatPack(Operator, ExportHelper):
|
||||||
|
bl_idname = "export_bat.pack"
|
||||||
|
bl_label = "BAT - Zip pack (flat)"
|
||||||
|
filename_ext = ".zip"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return bpy.data.is_saved
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
outfname = bpy.path.ensure_ext(self.filepath, ".zip")
|
||||||
|
self.report({"INFO"}, "Executing ZipPacker ...")
|
||||||
|
|
||||||
|
with zipped.ZipPacker(
|
||||||
|
Path(bpy.data.filepath),
|
||||||
|
Path(bpy.data.filepath).parent,
|
||||||
|
str(self.filepath),
|
||||||
|
) as packer:
|
||||||
|
packer.strategise()
|
||||||
|
packer.execute()
|
||||||
|
self.report({"INFO"}, "Packing successful!")
|
||||||
|
|
||||||
|
with zipfile.ZipFile(str(self.filepath)) as inzip:
|
||||||
|
inzip.testzip()
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Written to %s" % outfname)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def open_folder(folderpath):
|
||||||
|
"""Open the folder at the path given with cmd relative to user's OS."""
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
|
my_os = sys.platform
|
||||||
|
if my_os.startswith(("linux", "freebsd")):
|
||||||
|
cmd = "xdg-open"
|
||||||
|
elif my_os.startswith("win"):
|
||||||
|
cmd = "explorer"
|
||||||
|
if not folderpath:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
cmd = "open"
|
||||||
|
|
||||||
|
if not folderpath:
|
||||||
|
return
|
||||||
|
|
||||||
|
folderpath = str(folderpath)
|
||||||
|
if os.path.isfile(folderpath):
|
||||||
|
select = False
|
||||||
|
if my_os.startswith("win"):
|
||||||
|
cmd = "explorer /select,"
|
||||||
|
select = True
|
||||||
|
elif my_os.startswith(("linux", "freebsd")):
|
||||||
|
if which("nemo"):
|
||||||
|
cmd = "nemo --no-desktop"
|
||||||
|
select = True
|
||||||
|
elif which("nautilus"):
|
||||||
|
cmd = "nautilus --no-desktop"
|
||||||
|
select = True
|
||||||
|
if not select:
|
||||||
|
folderpath = os.path.dirname(folderpath)
|
||||||
|
|
||||||
|
folderpath = os.path.normpath(folderpath)
|
||||||
|
fullcmd = cmd.split() + [folderpath]
|
||||||
|
subprocess.Popen(fullcmd)
|
||||||
|
|
||||||
|
|
||||||
|
class BAT_OT_export_zip(Operator, ExportHelper):
|
||||||
|
"""Export current blendfile with hierarchy preservation"""
|
||||||
|
|
||||||
|
bl_label = "BAT - Zip pack (keep hierarchy)"
|
||||||
|
bl_idname = "bat.export_zip"
|
||||||
|
|
||||||
|
filename_ext = ".zip"
|
||||||
|
|
||||||
|
root_dir: bpy.props.StringProperty(
|
||||||
|
name="Root",
|
||||||
|
description="Top Level Folder of your project."
|
||||||
|
"\nFor now Copy/Paste correct folder by hand if default is incorrect."
|
||||||
|
"\n!!! Everything outside won't be zipped !!!",
|
||||||
|
)
|
||||||
|
|
||||||
|
use_zip: bpy.props.BoolProperty(
|
||||||
|
name="Output as ZIP",
|
||||||
|
description="If enabled, pack into a ZIP archive. If disabled, copy to a directory.",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return bpy.data.is_saved
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
from blender_asset_tracer.pack import Packer
|
||||||
|
from blender_asset_tracer.pack.zipped import ZipPacker
|
||||||
|
|
||||||
|
bfile = Path(bpy.data.filepath)
|
||||||
|
project = Path(self.root_dir) if self.root_dir else bfile.parent
|
||||||
|
target = str(self.filepath)
|
||||||
|
|
||||||
|
if self.use_zip:
|
||||||
|
target = bpy.path.ensure_ext(target, ".zip")
|
||||||
|
packer_cls = ZipPacker
|
||||||
|
else:
|
||||||
|
if target.lower().endswith(".zip"):
|
||||||
|
target = target[:-4]
|
||||||
|
packer_cls = Packer
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Packing with hierarchy...")
|
||||||
|
|
||||||
|
with packer_cls(bfile, project, target, keep_hierarchy=True) as packer:
|
||||||
|
packer.strategise()
|
||||||
|
packer.execute()
|
||||||
|
|
||||||
|
if self.use_zip:
|
||||||
|
with zipfile.ZipFile(target) as inzip:
|
||||||
|
inzip.testzip()
|
||||||
|
log_output = Path(tempfile.gettempdir(), "README.txt")
|
||||||
|
with open(log_output, "w") as log:
|
||||||
|
log.write("Packed with BAT (keep-hierarchy mode)")
|
||||||
|
log.write(f"\nBlend file: {bpy.data.filepath}")
|
||||||
|
with zipfile.ZipFile(target, "a") as zipObj:
|
||||||
|
zipObj.write(log_output, log_output.name)
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Written to %s" % target)
|
||||||
|
open_folder(Path(target).parent)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def menu_func(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.separator()
|
||||||
|
layout.operator(ExportBatPack.bl_idname)
|
||||||
|
filepath = layout.operator(BAT_OT_export_zip.bl_idname)
|
||||||
|
|
||||||
|
try:
|
||||||
|
prefs = bpy.context.preferences.addons["blender_asset_tracer"].preferences
|
||||||
|
root_dir_env = None
|
||||||
|
if prefs.use_env_root:
|
||||||
|
root_dir_env = os.getenv("ZIP_ROOT")
|
||||||
|
if not root_dir_env:
|
||||||
|
root_dir_env = os.getenv("PROJECT_ROOT")
|
||||||
|
if not root_dir_env:
|
||||||
|
root_dir_env = prefs.root_default
|
||||||
|
filepath.root_dir = "" if root_dir_env is None else root_dir_env
|
||||||
|
except Exception:
|
||||||
|
filepath.root_dir = os.getenv("ZIP_ROOT") or os.getenv("PROJECT_ROOT") or ""
|
||||||
|
|
||||||
|
|
||||||
11
blender_asset_tracer/pack/__init__.py
Normal file → Executable file
11
blender_asset_tracer/pack/__init__.py
Normal file → Executable 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:
|
||||||
|
|||||||
27
blender_asset_tracer/preferences.py
Normal file
27
blender_asset_tracer/preferences.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import AddonPreferences
|
||||||
|
from bpy.props import BoolProperty, StringProperty
|
||||||
|
|
||||||
|
|
||||||
|
class BATPreferences(AddonPreferences):
|
||||||
|
bl_idname = "blender_asset_tracer"
|
||||||
|
|
||||||
|
use_env_root: BoolProperty(
|
||||||
|
name="Use Environment Variable for Root",
|
||||||
|
description="Read the project root from ZIP_ROOT or PROJECT_ROOT environment variables",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
root_default: StringProperty(
|
||||||
|
name="Default Root",
|
||||||
|
description="Fallback project root when the environment variable is not set",
|
||||||
|
default="",
|
||||||
|
subtype="DIR_PATH",
|
||||||
|
)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.prop(self, "use_env_root")
|
||||||
|
layout.prop(self, "root_default")
|
||||||
|
|
||||||
|
|
||||||
0
blender_asset_tracer/py.typed
Normal file
0
blender_asset_tracer/py.typed
Normal 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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -22,10 +22,12 @@
|
|||||||
The modifier_xxx() functions all yield result.BlockUsage objects for external
|
The modifier_xxx() functions all yield result.BlockUsage objects for external
|
||||||
files used by the modifiers.
|
files used by the modifiers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from blender_asset_tracer import blendfile, bpathlib, cdefs
|
from blender_asset_tracer import blendfile, bpathlib, cdefs
|
||||||
|
from blender_asset_tracer.blendfile import iterators
|
||||||
from . import result
|
from . import result
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -317,3 +319,95 @@ def modifier_cloth(
|
|||||||
yield from _walk_point_cache(
|
yield from _walk_point_cache(
|
||||||
ctx, block_name, modifier.bfile, pointcache, cdefs.PTCACHE_EXT
|
ctx, block_name, modifier.bfile, pointcache, cdefs.PTCACHE_EXT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mod_handler(cdefs.eModifierType_DynamicPaint)
|
||||||
|
def modifier_dynamic_paint(
|
||||||
|
ctx: ModifierContext, modifier: blendfile.BlendFileBlock, block_name: bytes
|
||||||
|
) -> typing.Iterator[result.BlockUsage]:
|
||||||
|
my_log = log.getChild("modifier_dynamic_paint")
|
||||||
|
|
||||||
|
canvas_settings = modifier.get_pointer(b"canvas")
|
||||||
|
if canvas_settings is None:
|
||||||
|
my_log.debug(
|
||||||
|
"Modifier %r (%r) has no canvas_settings",
|
||||||
|
modifier[b"modifier", b"name"],
|
||||||
|
block_name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
surfaces = canvas_settings.get_pointer((b"surfaces", b"first"))
|
||||||
|
|
||||||
|
for surf_idx, surface in enumerate(iterators.listbase(surfaces)):
|
||||||
|
surface_block_name = block_name + b".canvas_settings.surfaces[%d]" % (surf_idx)
|
||||||
|
point_cache = surface.get_pointer(b"pointcache")
|
||||||
|
if point_cache is None:
|
||||||
|
my_log.debug(
|
||||||
|
"Surface %r (%r) has no pointcache",
|
||||||
|
surface[b"surface", b"name"],
|
||||||
|
surface_block_name,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield from _walk_point_cache(
|
||||||
|
ctx, surface_block_name, modifier.bfile, point_cache, cdefs.PTCACHE_EXT
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mod_handler(cdefs.eModifierType_Nodes)
|
||||||
|
def modifier_nodes(
|
||||||
|
ctx: ModifierContext, modifier: blendfile.BlendFileBlock, block_name: bytes
|
||||||
|
) -> typing.Iterator[result.BlockUsage]:
|
||||||
|
if not modifier.has_field(b"simulation_bake_directory"):
|
||||||
|
return
|
||||||
|
|
||||||
|
mod_directory_ptr, mod_directory_field = modifier.get(
|
||||||
|
b"simulation_bake_directory", return_field=True
|
||||||
|
)
|
||||||
|
|
||||||
|
bakes = modifier.get_pointer(b"bakes")
|
||||||
|
if not bakes:
|
||||||
|
return
|
||||||
|
|
||||||
|
mod_bake_target = modifier.get(b"bake_target")
|
||||||
|
|
||||||
|
for bake_idx, bake in enumerate(iterators.dynamic_array(bakes)):
|
||||||
|
# Check for packed data.
|
||||||
|
bake_target = bake.get(b"bake_target")
|
||||||
|
if bake_target == cdefs.NODES_MODIFIER_BAKE_TARGET_INHERIT:
|
||||||
|
bake_target = mod_bake_target
|
||||||
|
if bake_target == cdefs.NODES_MODIFIER_BAKE_TARGET_PACKED:
|
||||||
|
# This data is packed in the blend file, it's not a dependency to trace.
|
||||||
|
continue
|
||||||
|
|
||||||
|
flag = bake.get(b"flag")
|
||||||
|
use_custom_directory = bool(flag & cdefs.NODES_MODIFIER_BAKE_CUSTOM_PATH)
|
||||||
|
|
||||||
|
if use_custom_directory:
|
||||||
|
bake_directory_ptr, bake_directory_field = bake.get(
|
||||||
|
b"directory", return_field=True
|
||||||
|
)
|
||||||
|
directory_ptr = bake_directory_ptr
|
||||||
|
field = bake_directory_field
|
||||||
|
block = bake
|
||||||
|
else:
|
||||||
|
directory_ptr = mod_directory_ptr
|
||||||
|
field = mod_directory_field
|
||||||
|
block = modifier
|
||||||
|
|
||||||
|
if not directory_ptr:
|
||||||
|
continue
|
||||||
|
directory = bake.bfile.dereference_pointer(directory_ptr)
|
||||||
|
if not directory:
|
||||||
|
continue
|
||||||
|
|
||||||
|
bpath = bpathlib.BlendPath(directory.as_bytes_string())
|
||||||
|
bake_block_name = block_name + b".bakes[%d]" % bake_idx
|
||||||
|
|
||||||
|
yield result.BlockUsage(
|
||||||
|
block,
|
||||||
|
bpath,
|
||||||
|
block_name=bake_block_name,
|
||||||
|
path_full_field=field,
|
||||||
|
is_sequence=True,
|
||||||
|
)
|
||||||
|
|||||||
29
cli.py
Normal file
29
cli.py
Normal 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()
|
||||||
|
|
||||||
@ -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.18'
|
version = '1.21'
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = '1.18'
|
release = '1.21'
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|||||||
2378
poetry.lock
generated
2378
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "blender-asset-tracer"
|
name = "blender-asset-tracer"
|
||||||
version = "1.18"
|
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,29 +24,29 @@ s3 = ["boto3"]
|
|||||||
zstandard = ["zstandard"]
|
zstandard = ["zstandard"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
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.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"
|
||||||
# for the 'radon cc' command
|
# for the 'radon cc' command
|
||||||
radon = "^3.0"
|
radon = "^3.0"
|
||||||
# for converting profiler output to KCacheGrind input
|
# for converting profiler output to KCacheGrind input
|
||||||
"pyprof2calltree" = "*"
|
"pyprof2calltree" = "*"
|
||||||
# For building documentation
|
# For building documentation
|
||||||
sphinx = "^2.1"
|
sphinx = "^7.2"
|
||||||
sphinx-autobuild = "^0.7"
|
sphinx-autobuild = "^2024.3"
|
||||||
sphinx-rtd-theme = "^0.4"
|
sphinx-rtd-theme = "^2.0"
|
||||||
responses = "^0.10"
|
responses = "^0.10"
|
||||||
pathlib2 = { version = "^2.3", python = "<3.6" }
|
|
||||||
tox = "^3.12"
|
tox = "^3.12"
|
||||||
types-requests = "^2.25.0"
|
types-requests = "^2.25.0"
|
||||||
|
|
||||||
@ -54,9 +54,6 @@ types-requests = "^2.25.0"
|
|||||||
bat = 'blender_asset_tracer.cli:cli_main'
|
bat = 'blender_asset_tracer.cli:cli_main'
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
twine = "^4.0.1"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
build-backend = "poetry.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|||||||
@ -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.8
|
python_version = 3.11
|
||||||
|
|
||||||
warn_redundant_casts = True
|
warn_redundant_casts = True
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|||||||
BIN
tests/blendfiles/74871-packed-libraries.blend
Normal file
BIN
tests/blendfiles/74871-packed-libraries.blend
Normal file
Binary file not shown.
BIN
tests/blendfiles/basic_file_large_bhead8.blend
Normal file
BIN
tests/blendfiles/basic_file_large_bhead8.blend
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
BIN
tests/blendfiles/geometry-nodes-sim/geonodes-sim-cache.blend
Normal file
BIN
tests/blendfiles/geometry-nodes-sim/geonodes-sim-cache.blend
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
@ -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}}]}}}}}
|
||||||
BIN
tests/blendfiles/multiple_geometry_nodes_bakes.blend
Normal file
BIN
tests/blendfiles/multiple_geometry_nodes_bakes.blend
Normal file
Binary file not shown.
@ -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)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class BlendFileBlockTest(AbstractBlendFileTest):
|
|||||||
|
|
||||||
def test_loading(self):
|
def test_loading(self):
|
||||||
self.assertFalse(self.bf.is_compressed)
|
self.assertFalse(self.bf.is_compressed)
|
||||||
|
self.assertEqual(0, self.bf.header.file_format_version)
|
||||||
|
|
||||||
def test_some_properties(self):
|
def test_some_properties(self):
|
||||||
ob = self.bf.code_index[b"OB"][0]
|
ob = self.bf.code_index[b"OB"][0]
|
||||||
@ -45,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)
|
||||||
@ -154,6 +161,23 @@ class BlendFileBlockTest(AbstractBlendFileTest):
|
|||||||
self.assertEqual("OBümlaut", ob.id_name.decode())
|
self.assertEqual("OBümlaut", ob.id_name.decode())
|
||||||
|
|
||||||
|
|
||||||
|
class BlendFileLargeBhead8Test(AbstractBlendFileTest):
|
||||||
|
def setUp(self):
|
||||||
|
self.bf = blendfile.BlendFile(self.blendfiles / "basic_file_large_bhead8.blend")
|
||||||
|
|
||||||
|
def test_loading(self):
|
||||||
|
self.assertFalse(self.bf.is_compressed)
|
||||||
|
self.assertEqual(1, self.bf.header.file_format_version)
|
||||||
|
|
||||||
|
def test_some_properties(self):
|
||||||
|
ob = self.bf.code_index[b"OB"][0]
|
||||||
|
self.assertEqual("Object", ob.dna_type_name)
|
||||||
|
|
||||||
|
# Try high level operation to read the object location.
|
||||||
|
loc = ob.get(b"loc")
|
||||||
|
self.assertEqual([2.0, 3.0, 5.0], loc)
|
||||||
|
|
||||||
|
|
||||||
class PointerTest(AbstractBlendFileTest):
|
class PointerTest(AbstractBlendFileTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.bf = blendfile.BlendFile(self.blendfiles / "with_sequencer.blend")
|
self.bf = blendfile.BlendFile(self.blendfiles / "with_sequencer.blend")
|
||||||
@ -279,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
|
||||||
@ -459,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
146
tests/test_pack.py
Normal file → Executable 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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|
||||||
|
|||||||
2
tox.ini
2
tox.ini
@ -1,6 +1,6 @@
|
|||||||
[tox]
|
[tox]
|
||||||
isolated_build = true
|
isolated_build = true
|
||||||
envlist = py38, py39, py310, py311
|
envlist = py311, py312, py313, py314
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
whitelist_externals = poetry
|
whitelist_externals = poetry
|
||||||
|
|||||||
@ -19,5 +19,5 @@ echo git tag -a v$1 -m \'Tagged version $1\'
|
|||||||
echo
|
echo
|
||||||
echo "Build the package & upload to PyPi using:"
|
echo "Build the package & upload to PyPi using:"
|
||||||
echo "poetry build"
|
echo "poetry build"
|
||||||
echo "twine check 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 "twine upload -r bat 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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user