File size: 3,674 Bytes
eab734a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""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()