"""CLI: stt — statement to TikZ.""" from __future__ import annotations import json from pathlib import Path from typing import Optional import typer from statement_to_tikz.pipeline import load_ir, run_pipeline app = typer.Typer( add_completion=False, help="Convert geometry statements into constraint-solved TikZ diagrams.", ) def _split_out(out: Path) -> tuple[Path, str]: """Interpret --out out/fig as directory out, basename fig.""" if out.suffix in {".tex", ".pdf", ".json"}: return out.parent or Path("."), out.stem parent = out.parent name = out.name or "fig" if parent == Path(""): parent = Path(".") return parent, name @app.command() def main( statement: Optional[str] = typer.Argument( None, help="Geometry statement text, or path to a .txt file", ), out: Path = typer.Option( Path("out/fig"), "--out", "-o", help="Output path prefix (directory/basename without extension)", ), ir_path: Optional[Path] = typer.Option( None, "--ir", help="Load GeometryIR JSON instead of formalizing a statement", ), emit_only: bool = typer.Option( False, "--emit-only", help="With --ir: solve and emit without calling the LLM", ), no_compile: bool = typer.Option( False, "--no-compile", help="Skip pdflatex/latexmk", ), schematic: bool = typer.Option( False, "--schematic", help="Force schematic layout", ), no_offline: bool = typer.Option( False, "--no-offline", help="Disable offline pattern formalizer; require LLM", ), tol: float = typer.Option(1e-4, "--tol", help="Exact-mode residual tolerance"), ) -> None: """Generate TikZ from a statement or IR file.""" if statement is None and ir_path is None: typer.echo( "Usage: stt STATEMENT [-o out/fig] | stt --ir scene.ir.json --emit-only" ) raise typer.Exit(code=1) text: str | None = None ir = None if ir_path is not None: ir = load_ir(ir_path) if not emit_only and statement: text = statement else: text = ir.statement or None elif statement is not None: p = Path(statement) if p.is_file(): text = p.read_text(encoding="utf-8").strip() else: text = statement out_dir, basename = _split_out(out) result = run_pipeline( text, ir=ir, out_dir=out_dir, basename=basename, tol=tol, compile_pdf=not no_compile, allow_offline_formalizer=not no_offline, force_schematic=schematic, ) typer.echo(f"mode: {result['mode']}") typer.echo(f"max_residual: {result['max_residual']:.6g}") if result.get("paths"): for k, v in result["paths"].items(): if v: typer.echo(f"{k}: {v}") compile_info = (result.get("report") or {}).get("compile") if compile_info: if compile_info.get("skipped"): typer.echo(f"compile skipped: {compile_info.get('error')}") elif compile_info.get("ok"): typer.echo("compile: ok") else: typer.echo( f"compile: failed " f"({compile_info.get('error') or compile_info.get('returncode')})" ) raise typer.Exit(code=2) summary = { "mode": result["mode"], "max_residual": result["max_residual"], "paths": result.get("paths"), } typer.echo(json.dumps(summary)) if __name__ == "__main__": app()