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>
This commit is contained in:
@@ -18,4 +18,7 @@ them would lose the provenance; keeping them live would imply they still work
|
||||
| `wiki-bootstrap/backfill_cultural_corridor.py` | copied `geographic_sector` into `cultural_corridor` | a raw systems.db patch script, the kind `.claude/rules/asset-pipeline.md` forbids; absent from the D-262 flow |
|
||||
| `wiki-bootstrap/process-wiki-system-changes` | wiki → systems.db → star-map.json → re-rendered system pages | **destructive today** — its last step is `wiki_sync.generate_wiki()`, which no longer reproduces the committed pages (T-1290 finding, see the wiki domain's router) |
|
||||
|
||||
| `character-bootstrap/setup_clothing_metadata.py` | wrote `coverage.json` + a placeholder mask for the first five clothing items | all five (`coveralls_basic`, `jacket_utility`, `pants_cargo`, `shirt_henley`, `boots_work`) are gone from today's 24-garment wardrobe; re-running would recreate them |
|
||||
| `atlas-bootstrap/wipe-bodies.sh` | `DELETE FROM bodies; DELETE FROM stations;` on systems.db | **destructive** — a raw DB patch of the kind `.claude/rules/asset-pipeline.md` forbids, against the atlas-CLI-owned bodies catalog |
|
||||
|
||||
Archived in T-1250 / T-1290 (2026-09-23).
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wipe all bodies and stations from systems.db (not star_systems!)
|
||||
set -euo pipefail
|
||||
|
||||
DB="$(cd "$(dirname "$0")/.." && pwd)/server/data/systems.db"
|
||||
|
||||
echo "Deleting all stations..."
|
||||
sqlite3 "$DB" "DELETE FROM stations;"
|
||||
echo "Deleting all bodies..."
|
||||
sqlite3 "$DB" "DELETE FROM bodies;"
|
||||
echo "Done."
|
||||
sqlite3 "$DB" "SELECT 'bodies: ' || COUNT(*) FROM bodies; SELECT 'stations: ' || COUNT(*) FROM stations;"
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user