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>
This commit is contained in:
67
tools/web/compare-blend-id-fields.py
Normal file
67
tools/web/compare-blend-id-fields.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user