`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>
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
"""
|
|
make_logo.py (T-1089 gap G4 — brand-logo supply stub)
|
|
|
|
Generate a flat 2D wordmark PNG for a clothing brand decal (D-244: flat 2D
|
|
artwork on 3D surfaces). White-on-transparent, ~256x256, drawn with its own
|
|
crisp edge so the inverted-hull character outline (which never samples the
|
|
decal) leaves it untouched. Must stay legible at 16-32 px gameplay zoom (D-044).
|
|
|
|
This is a PLACEHOLDER supply stub. The production path is /image-gen (Gemini)
|
|
per the feasibility §4 pipeline; this script gives the engine a real decal to
|
|
render now, and doubles as the deterministic fallback generator.
|
|
|
|
Run:
|
|
reach character logo <text> <out_png> [--size 256]
|
|
|
|
Example (the canonical Braemar fiber co-op, wiki/corporations/thrds.md — always
|
|
lowercase):
|
|
reach character logo thrds client/assets/characters/logos/thrds.png
|
|
|
|
Formerly tooling/garment-fit/make_logo.py (T-1290); the drawing is unchanged
|
|
and renders the same PNG bytes.
|
|
"""
|
|
import os
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
from tooling.core import console
|
|
|
|
FONT_CANDIDATES = [
|
|
"/usr/share/fonts/fira-code/FiraCode-Bold.ttf",
|
|
"/usr/share/fonts/adwaita-mono-fonts/AdwaitaMono-Bold.ttf",
|
|
"/usr/share/fonts/dejavu-sans-fonts/DejaVuSans-Bold.ttf",
|
|
]
|
|
|
|
|
|
def load_font(px):
|
|
for path in FONT_CANDIDATES:
|
|
if os.path.isfile(path):
|
|
return ImageFont.truetype(path, px), os.path.basename(path)
|
|
return ImageFont.load_default(), "PIL-default"
|
|
|
|
|
|
def make_logo(text, out_png, size=256):
|
|
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Grow the font until the wordmark fills ~82% of the width.
|
|
target_w = int(size * 0.82)
|
|
px = size
|
|
font, font_name = load_font(px)
|
|
for px in range(size, 8, -2):
|
|
font, font_name = load_font(px)
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
if (bbox[2] - bbox[0]) <= target_w and (bbox[3] - bbox[1]) <= int(size * 0.5):
|
|
break
|
|
|
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
x = (size - tw) / 2 - bbox[0]
|
|
y = (size - th) / 2 - bbox[1]
|
|
# White wordmark, fully opaque.
|
|
draw.text((x, y), text, font=font, fill=(255, 255, 255, 255))
|
|
# A thin underscore bar under the wordmark — reads as a modern corporate mark
|
|
# and gives the decal a stable baseline anchor at low zoom.
|
|
bar_y = y + bbox[3] + int(size * 0.03)
|
|
bar_h = max(2, int(size * 0.02))
|
|
draw.rectangle(
|
|
[(size * 0.12, bar_y), (size * 0.88, bar_y + bar_h)],
|
|
fill=(255, 255, 255, 255),
|
|
)
|
|
|
|
os.makedirs(os.path.dirname(os.path.abspath(out_png)), exist_ok=True)
|
|
img.save(out_png)
|
|
console.event(f"wrote {out_png} ({size}x{size}, font={font_name}, glyph_px={px})")
|
|
return {"file": out_png, "size": size, "font": font_name, "glyph_px": px}
|
|
|