Files
settled-reach/tooling/archive/character-bootstrap/setup_clothing_metadata.py
T
jpmschweitzerandClaude Opus 5.5 26cc8de7f3 refactor(tooling): T-1290 — the character domain, and six payloads the map misfiled
`reach character {logo, strip-glb, qa, qa-analyze}` replaces make_logo.py,
glb_strip_utility_nodes.py, analyze_captures.py and the run-garment-qa bash
driver. The QA configs and method doc move beside the domain (qa_configs/,
GARMENT_QA.md), and `qa` takes a config name (`reach character qa hoodie_modern`)
or a path.

Parity, from baselines taken before anything moved:

- the logo PNG is byte-identical
- a synthetic GLB with three real utility nodes strips to identical bytes
  (the committed bodies strip 0 nodes, so they proved nothing)
- re-analyzing a cached capture set gives a byte-identical report.json and
  summary

run-garment-qa is rewritten, not wrapped (D-263). Its decisions — which config,
which Godot ($GODOT, then ~/bin/godot4, then PATH), and whether xvfb-run is
needed — are capture_plan(), pinned by tooling/test_character.py without
launching Godot. The bash exit codes are kept: 2 for a missing config, 3 for
no Godot.

The T-1271 domain map was wrong about this domain. Six of its ten files import
bpy: convert_outfit, inspect_glb, check_hair_symmetry, check_icosphere,
render_quaternius_test and test_quaternius_raw. They are Blender payloads and
joined the carve-out as blender_* (41 payloads now). The 22 existing payloads'
docstrings still cited tooling/garment-fit/ from before T-1273; fixed.

Archived, with reasons in tooling/archive/README.md:

- setup_clothing_metadata.py wrote coverage data for five garments that no
  longer exist in the 24-garment wardrobe
- wipe-bodies.sh ran raw DELETEs on systems.db

segment_reference_distribution.md moved to docs/assets/visual/.

Behaviour changes:

- The QA analyzer exited 0 whatever it found, though its own README says
  clip-through "is the real defect and it gates". qa and qa-analyze now exit 1
  on clip-through, and the remedy names --min-pixels (Wave 1/2 were accepted
  at 150). The cached peasant set has 33 failures at the default 8 px.
- glb strip re-reported the same nodes as stripped on every re-run and
  rewrote an unchanged file: it left them as orphans and then found them
  again. Only nodes still linked into the graph count now, and a first pass
  writes the same bytes as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:55:46 +02:00

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)