| """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() |
|
|