- make test-tooling: planet-gen determinism guard + import_economics
--dry-run, wired into pre-push on TOOLING_CHANGED; ruff widened to
E4/E7/E9/F/W (90 safe auto-fixes applied; E402/E702/F841 ignored with
documented counts)
- one-generator reality fixed in DEVOPS.md, asset-pipeline rule, CLAUDE.md
(import_economics sole generator since #951/D-223); dead check-protocol
target deleted; DEVOPS hook/config sections rewritten from the actual
hook sources; team-patterns gate description updated (client+tooling)
- project.yaml: 0.2.0 → 0.4.0 per the 0.{phase}.{n} scheme, description
refreshed from the v0.1 Sova narration to cascade reality
- stale comment sweep: voxel.rs stub claims (all 8 families implemented),
cascade.rs TODO recited to T-1044, main.rs D-192 handshake claim,
relationships.rs/chunk_streaming.rs version targets → phase language
- gitignore: client/settings.db* e2e-run artifacts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
194 lines
6.2 KiB
Python
194 lines
6.2 KiB
Python
"""
|
|
setup_clothing_metadata.py
|
|
|
|
Usage: python3 tooling/setup_clothing_metadata.py <clothing_dir>
|
|
|
|
Creates coverage.json and reference_mask.png for all 5 initial clothing items.
|
|
This is a pure-Python script — no Blender required.
|
|
|
|
Arguments:
|
|
clothing_dir Root directory for clothing output (e.g. client/assets/characters/clothing/).
|
|
Each item subdirectory must already exist.
|
|
|
|
Files created per item:
|
|
coverage.json -- segment hides + torso_variant (compositor input)
|
|
reference_mask.png -- greyscale recolor mask (all-white v0.2 placeholder: fully tintable)
|
|
|
|
coverage.json format:
|
|
{
|
|
"hides": ["torso", "arm_upper_l", ...],
|
|
"torso_variant": "full"
|
|
}
|
|
|
|
hides entries use short segment names (without "seg_" prefix).
|
|
torso_variant: "full" = clothing hides seg_torso (full torso segment)
|
|
: "upper" = clothing only hides seg_torso_upper (upper chest)
|
|
|
|
reference_mask.png:
|
|
64x64 grayscale PNG. White (255) = fully tintable. v0.2 placeholder — all white.
|
|
Final art will have partial masks (black areas = preserve material texture color).
|
|
|
|
Decisions: D-162 (clothing pre-baked per body type via Surface Deform)
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import zlib
|
|
import struct
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Clothing catalogue metadata
|
|
# -------------------------------------------------------------------------
|
|
|
|
CLOTHING_ITEMS = {
|
|
"coveralls_basic": {
|
|
"description": "Full-body work suit",
|
|
"hides": [
|
|
"torso",
|
|
"arm_upper_l", "arm_upper_r",
|
|
"arm_lower_l", "arm_lower_r",
|
|
"hand_l", "hand_r",
|
|
"leg_upper_l", "leg_upper_r",
|
|
"leg_lower_l", "leg_lower_r",
|
|
"foot_l", "foot_r",
|
|
],
|
|
"torso_variant": "full",
|
|
},
|
|
"jacket_utility": {
|
|
"description": "Upper body outerwear",
|
|
"hides": [
|
|
"torso",
|
|
"arm_upper_l", "arm_upper_r",
|
|
"arm_lower_l", "arm_lower_r",
|
|
],
|
|
"torso_variant": "full",
|
|
},
|
|
"pants_cargo": {
|
|
# Note: pants hide leg segments but NOT foot segments. Boots (boots_work)
|
|
# hide feet. This distinction matters for the compositor — a character
|
|
# wearing pants + no boots shows bare feet via seg_foot_l/r.
|
|
"description": "Lower body cargo trousers",
|
|
"hides": [
|
|
"leg_upper_l", "leg_upper_r",
|
|
"leg_lower_l", "leg_lower_r",
|
|
],
|
|
"torso_variant": "full",
|
|
},
|
|
"shirt_henley": {
|
|
"description": "Upper body inner shirt",
|
|
"hides": [
|
|
"torso",
|
|
"arm_upper_l", "arm_upper_r",
|
|
],
|
|
"torso_variant": "full",
|
|
},
|
|
"boots_work": {
|
|
"description": "Work boots (foot slot + lower leg shaft)",
|
|
"hides": [
|
|
"foot_l", "foot_r",
|
|
"leg_lower_l", "leg_lower_r",
|
|
],
|
|
"torso_variant": "full",
|
|
},
|
|
}
|
|
|
|
MASK_WIDTH = 64
|
|
MASK_HEIGHT = 64
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Minimal PNG writer (no external dependencies)
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _make_png_chunk(chunk_type, data):
|
|
"""Construct a PNG chunk with CRC."""
|
|
payload = chunk_type + data
|
|
crc = zlib.crc32(payload) & 0xFFFFFFFF
|
|
return struct.pack(">I", len(data)) + payload + struct.pack(">I", crc)
|
|
|
|
|
|
def write_white_grayscale_png(path, width=64, height=64):
|
|
"""
|
|
Write a minimal all-white grayscale PNG to `path`.
|
|
Uses only Python built-ins (zlib, struct) — no Pillow required.
|
|
"""
|
|
# PNG signature
|
|
signature = b'\x89PNG\r\n\x1a\n'
|
|
|
|
# IHDR: width, height, bit_depth=8, colortype=0 (grayscale),
|
|
# compression=0, filter=0, interlace=0
|
|
ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 0, 0, 0, 0)
|
|
ihdr = _make_png_chunk(b'IHDR', ihdr_data)
|
|
|
|
# IDAT: raw scanlines — filter byte 0x00 (None) + width bytes of 0xFF (white)
|
|
raw_rows = b''.join(b'\x00' + b'\xff' * width for _ in range(height))
|
|
compressed = zlib.compress(raw_rows, 9)
|
|
idat = _make_png_chunk(b'IDAT', compressed)
|
|
|
|
# IEND
|
|
iend = _make_png_chunk(b'IEND', b'')
|
|
|
|
with open(path, 'wb') as f:
|
|
f.write(signature + ihdr + idat + iend)
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Per-item setup
|
|
# -------------------------------------------------------------------------
|
|
|
|
def setup_item(item_id, config, clothing_dir):
|
|
"""
|
|
Write coverage.json and reference_mask.png for one clothing item.
|
|
Returns (coverage_ok, mask_ok).
|
|
"""
|
|
item_dir = os.path.join(clothing_dir, item_id)
|
|
os.makedirs(item_dir, exist_ok=True)
|
|
|
|
# coverage.json
|
|
coverage = {
|
|
"hides": config["hides"],
|
|
"torso_variant": config["torso_variant"],
|
|
}
|
|
coverage_path = os.path.join(item_dir, "coverage.json")
|
|
with open(coverage_path, 'w') as f:
|
|
json.dump(coverage, f, indent=2)
|
|
coverage_ok = True
|
|
|
|
# reference_mask.png (all-white placeholder)
|
|
mask_path = os.path.join(item_dir, "reference_mask.png")
|
|
write_white_grayscale_png(mask_path, MASK_WIDTH, MASK_HEIGHT)
|
|
mask_ok = True
|
|
|
|
return coverage_ok, mask_ok
|
|
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Entry point
|
|
# -------------------------------------------------------------------------
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python3 tooling/setup_clothing_metadata.py <clothing_dir>")
|
|
sys.exit(1)
|
|
|
|
clothing_dir = sys.argv[1]
|
|
|
|
print("\nClothing Metadata Setup")
|
|
print(f" Output dir: {clothing_dir}")
|
|
print(f" Items: {len(CLOTHING_ITEMS)}")
|
|
|
|
all_ok = True
|
|
for item_id, config in CLOTHING_ITEMS.items():
|
|
coverage_ok, mask_ok = setup_item(item_id, config, clothing_dir)
|
|
status = "OK" if (coverage_ok and mask_ok) else "FAILED"
|
|
print(f" {item_id:20s} {status} "
|
|
f"coverage={'ok' if coverage_ok else 'FAIL'} "
|
|
f"mask={'ok' if mask_ok else 'FAIL'}")
|
|
if not (coverage_ok and mask_ok):
|
|
all_ok = False
|
|
|
|
print(f"\n Done. {'All OK.' if all_ok else 'Some items failed.'}")
|
|
if not all_ok:
|
|
sys.exit(1)
|