Skip to content

Commit 70f9100

Browse files
author
Kevin
committed
feat: Add support for mat backgrounds with image and PDF options in GUI
1 parent 4200a20 commit 70f9100

5 files changed

Lines changed: 146 additions & 10 deletions

File tree

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,57 @@ fll-sim-gui --exit-after 5
249249

250250
Use `--headless` (or env `FLL_SIM_HEADLESS=1`) for CI and container runs.
251251

252+
### Mat Backgrounds: Images & PDFs
253+
254+
You can provide a mat image or PDF for the GUI background.
255+
256+
- Direct image URL (PNG/JPG):
257+
- `python launch_gui_enhanced.py --season latest --mat-url https://example.com/mat.png`
258+
- Direct PDF URL with rasterization:
259+
- `python launch_gui_enhanced.py --season latest --mat-pdf-url https://example.com/mat.pdf --mat-pdf-page 0 --mat-pdf-dpi 300`
260+
- Optional selectors:
261+
- `--mat-pdf-page-label <label>` (uses PDF page label, if present)
262+
- `--mat-pdf-toc-title <title>` (uses PDF Table of Contents title, if present)
263+
264+
Downloaded mats are cached under `assets/mats/<season>/mat.png`.
265+
266+
### Batch Download via Manifest
267+
268+
Use the manifest downloader to grab multiple seasons into `assets/mats/`:
269+
270+
```yaml
271+
# manifest.yml
272+
- season: 2024-submerged
273+
url: https://example.com/2024-mat.pdf
274+
type: pdf
275+
page: 0
276+
dpi: 300
277+
# Optional selectors (if available in the PDF):
278+
# page_label: "Mat"
279+
# toc_title: "Field Mat"
280+
- season: 2023-masterpiece
281+
url: https://example.com/2023-mat.png
282+
type: image
283+
```
284+
285+
Run:
286+
287+
```bash
288+
python -m fll_sim.scripts.fetch_all_mats --manifest manifest.yml
289+
```
290+
291+
### Season Mat Sizes
292+
293+
Default physical size is used when exact season sizes aren’t provided. You can override sizes in `configs/profiles/defaults.yaml`:
294+
295+
```yaml
296+
mat_sizes:
297+
2024-submerged: [2362.0, 1143.0]
298+
2023-masterpiece: [2362.0, 1143.0]
299+
```
300+
301+
These values are in millimeters: `[width_mm, height_mm]`.
302+
252303
## 🤝 Contributing
253304

254305
1. Fork the repository

configs/profiles/defaults.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ advanced:
3636
guided_tutorials: false
3737
competition_timer: true
3838
scoring_penalties: true
39+
40+
# Optional mat size overrides (in mm). Uncomment and edit as needed.
41+
# mat_sizes:
42+
# 2024-submerged: [2362.0, 1143.0]
43+
# 2023-masterpiece: [2362.0, 1143.0]

launch_gui_enhanced.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from fll_sim.assets.mats import get_mat_for_season # noqa: E402
2323
from fll_sim.environment.game_map import GameMap # noqa: E402
2424
from fll_sim.scripts.fetch_mat import fetch_mat_image # noqa: E402
25+
from fll_sim.scripts.fetch_mat import fetch_mat_pdf
2526
from fll_sim.visualization.simulator_view import SimulatorView # noqa: E402
2627

2728

@@ -77,6 +78,36 @@ def parse_args() -> argparse.Namespace:
7778
"assets/mats/<season>/mat.png"
7879
),
7980
)
81+
p.add_argument(
82+
"--mat-pdf-url",
83+
default=None,
84+
help=(
85+
"Remote URL to mat PDF. Rasterizes to PNG and caches under "
86+
"assets/mats/<season>/mat.png"
87+
),
88+
)
89+
p.add_argument(
90+
"--mat-pdf-page",
91+
type=int,
92+
default=0,
93+
help="PDF page index to rasterize (default 0)",
94+
)
95+
p.add_argument(
96+
"--mat-pdf-page-label",
97+
default=None,
98+
help="PDF page label to select (overrides index if found)",
99+
)
100+
p.add_argument(
101+
"--mat-pdf-toc-title",
102+
default=None,
103+
help="PDF TOC title to select (overrides index if found)",
104+
)
105+
p.add_argument(
106+
"--mat-pdf-dpi",
107+
type=int,
108+
default=300,
109+
help="DPI for PDF rasterization (default 300)",
110+
)
80111
p.add_argument(
81112
"--px-per-mm",
82113
type=float,
@@ -115,7 +146,7 @@ def main() -> None:
115146
# Resolve mat image
116147
mat_arg: Optional[str] = args.mat_path
117148
mat_path = Path(mat_arg) if mat_arg else None
118-
if not mat_path and args.mat_url:
149+
if not mat_path and (args.mat_url or args.mat_pdf_url):
119150
# Cache under assets/mats/<season>/mat.png
120151
cache_dir = (
121152
project_root
@@ -126,8 +157,22 @@ def main() -> None:
126157
cache_dir.mkdir(parents=True, exist_ok=True)
127158
mat_path = cache_dir / "mat.png"
128159
try:
129-
print(f"Downloading mat from {args.mat_url}...")
130-
fetch_mat_image(url=args.mat_url, out_path=mat_path)
160+
if args.mat_pdf_url:
161+
print(
162+
f"Downloading mat PDF from {args.mat_pdf_url} "
163+
f"(page={args.mat_pdf_page}, dpi={args.mat_pdf_dpi})..."
164+
)
165+
fetch_mat_pdf(
166+
url=args.mat_pdf_url,
167+
out_path=mat_path,
168+
page=args.mat_pdf_page,
169+
dpi=args.mat_pdf_dpi,
170+
page_label=args.mat_pdf_page_label,
171+
toc_title=args.mat_pdf_toc_title,
172+
)
173+
else:
174+
print(f"Downloading mat from {args.mat_url}...")
175+
fetch_mat_image(url=args.mat_url, out_path=mat_path)
131176
print(f"Mat cached to {mat_path}")
132177
except Exception as e: # noqa: BLE001
133178
print(f"Warning: Failed to download mat: {e}")

src/fll_sim/scripts/fetch_all_mats.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ def main() -> None:
5656
kind = entry.get("type", entry.get("format", "image")).lower()
5757
page = int(entry.get("page", 0))
5858
dpi = int(entry.get("dpi", 300))
59+
page_label = entry.get("page_label")
60+
toc_title = entry.get("toc_title")
5961
if not season or not url:
6062
print(f"Skipping invalid entry: {entry}")
6163
continue
@@ -64,7 +66,14 @@ def main() -> None:
6466
out_file = out_dir / "mat.png"
6567
print(f"Downloading {season} from {url} (type={kind}) ...")
6668
if kind == "pdf":
67-
fetch_mat_pdf(url, out_file, page=page, dpi=dpi)
69+
fetch_mat_pdf(
70+
url,
71+
out_file,
72+
page=page,
73+
dpi=dpi,
74+
page_label=page_label,
75+
toc_title=toc_title,
76+
)
6877
else:
6978
fetch_mat_image(url, out_file)
7079
count += 1
@@ -74,4 +83,3 @@ def main() -> None:
7483

7584
if __name__ == "__main__": # pragma: no cover
7685
main()
77-
main()

src/fll_sim/scripts/fetch_mat.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ def fetch_mat_pdf(
8080
*,
8181
page: int = 0,
8282
dpi: int = 300,
83+
page_label: str | None = None,
84+
toc_title: str | None = None,
8385
timeout: float = 30.0,
8486
) -> Path:
8587
"""
@@ -116,17 +118,42 @@ def fetch_mat_pdf(
116118

117119
# Open with PyMuPDF
118120
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
119-
if page < 0 or page >= doc.page_count:
121+
122+
# Resolve page number
123+
target_page = page
124+
if page_label is not None:
125+
# Find page with matching label (if available)
126+
for i in range(doc.page_count):
127+
try:
128+
lbl = doc.load_page(i).label # type: ignore[attr-defined]
129+
except Exception:
130+
lbl = None
131+
if lbl and str(lbl).strip() == str(page_label).strip():
132+
target_page = i
133+
break
134+
elif toc_title is not None:
135+
# Search table of contents; fall back silently if unavailable
136+
toc = None
137+
try:
138+
toc = doc.getToC(simple=True) # type: ignore[attr-defined]
139+
except Exception:
140+
toc = None
141+
if toc:
142+
for _lvl, title, pgnum in toc:
143+
if str(title).strip() == str(toc_title).strip():
144+
target_page = max(0, int(pgnum) - 1)
145+
break
146+
147+
if target_page < 0 or target_page >= doc.page_count:
120148
raise ValueError(
121-
"Page index {page} out of range for PDF with "
122-
f"{doc.page_count} pages"
149+
"Page index out of range for PDF with " f"{doc.page_count} pages"
123150
)
124-
pg = doc.load_page(page)
151+
pg = doc.load_page(target_page)
125152

126153
# Compute matrix for DPI
127154
zoom = dpi / 72.0 # 72 DPI is base
128155
mat = fitz.Matrix(zoom, zoom)
129-
pix = pg.get_pixmap(matrix=mat, alpha=False)
156+
pix = pg.get_pixmap(matrix=mat, alpha=False) # type: ignore[attr-defined]
130157

131158
# Convert pixmap to PIL Image and save as PNG
132159
img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)

0 commit comments

Comments
 (0)