window_texture.py 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243
  1. """Blender-free contract for an estimated shared compact window-frame image."""
  2. import hashlib
  3. import json
  4. import math
  5. from pathlib import Path
  6. import struct
  7. def load_window_texture(manifest_path):
  8. manifest_path = Path(manifest_path).resolve()
  9. record = json.loads(manifest_path.read_text(encoding='utf-8'))
  10. if record.get('kind') != 'estimated-window-frame' or record.get('version') not in (1, 2):
  11. raise ValueError('unsupported compact window texture manifest')
  12. path = (manifest_path.parent / record['image']).resolve()
  13. data = path.read_bytes()
  14. if len(data) > 8192 or len(data) < 33 or data[:8] != b'\x89PNG\r\n\x1a\n':
  15. raise ValueError('window texture must be a bounded PNG')
  16. width, height = struct.unpack('>II', data[16:24])
  17. size_valid = (width, height) == (64, 64) if record['version'] == 1 else (
  18. width == height and width in (32, 64) and record.get('dimensions') == [width, height])
  19. if not size_valid or data[24:26] != bytes((8, 2)):
  20. raise ValueError('window texture must be bounded opaque RGB8; v1=64px, v2=declared 32/64px')
  21. if hashlib.sha256(data).hexdigest() != record['sha256']:
  22. raise ValueError('window texture source hash mismatch')
  23. mip_bytes = sum(max(1, width >> level) * max(1, height >> level) * 4
  24. for level in range(width.bit_length()))
  25. return {**record, 'path': str(path), 'rasterBytes': len(data),
  26. 'width': width, 'height': height, 'rgba8FullMipEstimate': mip_bytes}
  27. def window_quad_uv(points):
  28. """Map a vertical window quad to one tile; never reuse geographic roof UVs."""
  29. if len(points) != 4:
  30. raise ValueError('window face is not a quad')
  31. pairs = [(b[0]-a[0], b[1]-a[1]) for a in points for b in points]
  32. dx, dy = max(pairs, key=lambda p: math.hypot(*p))
  33. length = math.hypot(dx, dy)
  34. low_z, high_z = min(p[2] for p in points), max(p[2] for p in points)
  35. if length < 1e-6 or high_z-low_z < 1e-6:
  36. raise ValueError('window quad has no width or height')
  37. along = [(p[0]-points[0][0])*dx/length + (p[1]-points[0][1])*dy/length for p in points]
  38. low, high = min(along), max(along)
  39. return [((u-low)/(high-low), (p[2]-low_z)/(high_z-low_z)) for u,p in zip(along,points)]