"""Transport for the `character` domain — args in, delegate, format out.""" from __future__ import annotations import json import os from pathlib import Path import typer from tooling.core import cli, console from tooling.core.command import command from tooling.core.errors import ReachError app = cli.domain("character", "Bodies, garments, GLBs — logos, the GLB strip, garment QA.") @app.callback() def _domain() -> None: """Keeps `character` a group (Typer collapses a single-command app).""" @app.command("logo") @command def logo( text: str = typer.Argument(..., help="The wordmark, e.g. thrds (brands are lowercase)."), out_png: Path = typer.Argument(..., help="Where to write the PNG."), size: int = typer.Option(256, "--size", help="Square edge in px."), ) -> None: """Render a flat white-on-transparent brand wordmark decal (placeholder supply).""" from tooling.domains.character import logo as service result = service.make_logo(text, str(out_png), size) console.out(json.dumps(result)) @app.command("strip-glb") @command def strip_glb( input: Path = typer.Argument(None, help="A .glb to strip."), output: Path = typer.Argument(None, help="Output .glb (default: overwrite the input)."), dir: Path = typer.Option(None, "--dir", help="Strip every .glb under a directory, in place."), ) -> None: """Remove utility nodes (Icosphere, WGT-, DEF-, ORG-, empty meshes) from GLB scene graphs.""" from tooling.domains.character import glb_strip if dir: result = glb_strip.process_directory(str(dir)) elif input: result = glb_strip.process_file(str(input), str(output or input)) else: raise ReachError("nothing to strip", fix="pass a .glb, or --dir ") console.out(json.dumps(result)) console.verdict(f"strip-glb: {result['stripped']} utility node(s) removed") @app.command("qa") @command def qa( config: str = typer.Argument(None, help="Config name in qa_configs/ (e.g. peasant) or a path."), min_pixels: int = typer.Option(8, "--min-pixels", help="Smallest clip blob that fails a capture."), ) -> None: """Capture in Godot, then analyze — the garment acceptance gate. Fails on clip-through.""" from tooling.domains.character import qa as capture_service from tooling.domains.character import qa_analyze plan = capture_service.capture_plan(config, dict(os.environ), Path.home()) capture_service.capture(plan) report, path = qa_analyze.run(str(plan.config), None, min_pixels) _report(report, path, min_pixels) @app.command("qa-analyze") @command def qa_analyze_cmd( config: Path = typer.Option(None, "--config", help="Capture config (reads its out_dir)."), dir: Path = typer.Option(None, "--dir", help="Capture directory (overrides --config)."), min_pixels: int = typer.Option(8, "--min-pixels", help="Smallest clip blob that fails a capture."), report: Path = typer.Option(None, "--report", help="Report path (default: /report.json)."), ) -> None: """Re-analyze existing captures without re-rendering. Fails on clip-through.""" from tooling.domains.character import qa_analyze result, path = qa_analyze.run( str(config) if config else None, str(dir) if dir else None, min_pixels, str(report) if report else None ) _report(result, path, min_pixels) def _report(report: dict, path: Path, min_pixels: int) -> None: """Summary to stdout; a clip-through failure fails the command. The analyzer used to exit 0 whatever it found, although its own README says clip-through "is the real defect and it gates" (T-1290). """ from tooling.domains.character import qa_analyze console.out("\n".join(qa_analyze.summary_lines(report))) console.out(f" report : {path}") failures = report["clip_through_failures"] if failures: out_dir = Path(report["out_dir"]) console.out(f" highlighted frames : {out_dir / 'failures'}") raise ReachError( f"garment-qa: {failures} of {report['total_captures']} captures clip through " f"(largest blob >= {min_pixels}px)", fix=f"inspect {out_dir / 'failures'}/*_HL.png; fix the garment, or re-run with a " "deliberately looser --min-pixels (Wave 1/2 were accepted at 150)", ) console.verdict(f"garment-qa: {report['total_captures']} captures, no clip-through")