Files
workinf_Blender_Wasm/tools/web/compare-blend-id-fields.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

68 lines
4.6 KiB
Python

"""Compare serialized ID fields using Blender's reader; not a Main-equivalence verdict."""
import argparse
import collections
import hashlib
import json
import sys
from pathlib import Path
def read_ids(blend):
result = {}
for block in blend.blocks:
if block.code in (b'DATA', b'DNA1', b'ENDB'): continue
struct = blend.structs[block.sdna_index]
if not any(f.dna_name.name_only == b'id' and f.dna_type.dna_type_id == b'ID' for f in struct.fields): continue
name = block.get((b'id', b'name'))
if not isinstance(name, str): raise TypeError('ID name must be decoded text')
key = block.code.decode('ascii') + ':' + name
if key in result: raise ValueError('Duplicate ID identity needs library disambiguation: ' + key)
blend.handle.seek(block.file_offset); result[key] = (struct, blend.handle.read(block.size))
return result
def changed_fields(struct, before, after, prefix='', base=0):
for field in struct.fields:
if not hasattr(field.dna_type, 'fields'): raise TypeError('DNA field type must be a DNAStruct')
lo, hi = base + field.dna_offset, base + field.dna_offset + field.dna_size
if not (0 <= lo <= hi <= len(before) and hi <= len(after)): raise ValueError('DNA field outside block')
if before[lo:hi] == after[lo:hi]: continue
name = prefix + field.dna_name.name_full.decode('ascii')
if field.dna_type.fields and not field.dna_name.is_pointer and field.dna_name.array_size == 1:
yield from changed_fields(field.dna_type, before, after, name + '.', lo)
else:
yield {'field': name, 'dnaType': field.dna_type.dna_type_id.decode('ascii'), 'bytes': field.dna_size,
'beforeHex': before[lo:hi][:32].hex(), 'afterHex': after[lo:hi][:32].hex(), 'previewTruncated': field.dna_size > 32}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('before', type=Path); parser.add_argument('after', type=Path)
parser.add_argument('--output', required=True, type=Path); parser.add_argument('--dependency-path', type=Path)
args = parser.parse_args()
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'blender-5.2.0/tools/modules'))
if args.dependency_path: sys.path.insert(0, str(args.dependency_path.resolve()))
try: import blendfile
except ModuleNotFoundError as error: raise SystemExit(f'Reader dependency missing: {error.name}; provide --dependency-path (python3-zstandard).')
with blendfile.open_blend(str(args.before)) as before, blendfile.open_blend(str(args.after)) as after:
def dna_hash(blend):
block = next(block for block in blend.blocks if block.code == b'DNA1')
blend.handle.seek(block.file_offset); return hashlib.sha256(blend.handle.read(block.size)).hexdigest()
if dna_hash(before) != dna_hash(after): raise ValueError('DNA schemas differ; use snapshots from one candidate')
left, right = read_ids(before), read_ids(after)
changes = []
for key in sorted(left.keys() & right.keys()):
struct, old = left[key]; other, new = right[key]
if struct.dna_type_id != other.dna_type_id or len(old) != len(new): raise ValueError('ID layout changed: ' + key)
fields = list(changed_fields(struct, old, new))
if fields: changes.append({'id': key, 'fields': fields})
inventory = lambda blend: dict(sorted(collections.Counter(block.code.decode('ascii') + ':' + blend.structs[block.sdna_index].dna_type_id.decode('ascii') for block in blend.blocks if block.code != b'ENDB').items()))
result = {'schemaVersion': 1, 'scope': 'Serialized ID fields and block counts; DATA contents are not a semantic equality verdict',
'changedIds': changes, 'addedIds': sorted(right.keys() - left.keys()), 'removedIds': sorted(left.keys() - right.keys()),
'blockCountsBefore': inventory(before), 'blockCountsAfter': inventory(after), 'dnaSha256': dna_hash(before)}
for name in ['before', 'after']:
file = getattr(args, name)
with file.open('rb') as stream: result[name] = {'path': str(file), 'sha256': hashlib.file_digest(stream, 'sha256').hexdigest()}
args.output.parent.mkdir(parents=True, exist_ok=True)
temporary = args.output.with_name(args.output.name + '.tmp'); temporary.write_text(json.dumps(result, indent=2) + '\n'); temporary.replace(args.output)
print(json.dumps({'output': str(args.output), 'changedIds': len(changes), 'addedIds': len(result['addedIds']), 'removedIds': len(result['removedIds'])}))
if __name__ == '__main__': main()