Files
orisys-app/apps/autocal/tools/panel_snapshot.py
T
2026-09-20 21:15:39 +08:00

145 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Render the AutoCal window offscreen to a PNG, for review.
python apps/autocal/tools/panel_snapshot.py --theme light --out shot.png
Optionally drives a simulated session first, so the shot shows live values:
python apps/autocal/tools/panel_snapshot.py --demo --language en_US --out shot.png
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
APP_ROOT = Path(__file__).resolve().parents[1]
for path in (APP_ROOT / "src", APP_ROOT.parents[1] / "shared" / "src"):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
os.environ.setdefault("ORISYS_APP_ID", "autocal")
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
if sys.platform == "win32" and "QT_QPA_FONTDIR" not in os.environ:
_fonts = Path(os.environ.get("WINDIR", r"C:\Windows")) / "Fonts"
if _fonts.is_dir():
os.environ["QT_QPA_FONTDIR"] = str(_fonts)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--theme", default="dark", choices=("dark", "light"))
parser.add_argument("--language", default="zh_CN", choices=("zh_CN", "en_US"))
parser.add_argument("--size", default="1600x900", help="WIDTHxHEIGHT")
parser.add_argument("--out", default="autocal.png")
parser.add_argument(
"--tab",
default="axes",
help="workspace tab to show: axes, sequence, sensor, surface, points",
)
parser.add_argument(
"--compare",
action="store_true",
help="render every tab, named <out stem>_<tab>.<ext>, so the layout can be "
"reviewed in one pass",
)
parser.add_argument(
"--demo",
action="store_true",
help="drive a simulated session so the shot shows live values",
)
args = parser.parse_args()
width, height = (int(part) for part in args.size.lower().split("x"))
from PySide6.QtWidgets import QApplication
from orisys_app_shared.i18n import set_locale
from orisys_app_shared.theme import build_qss
from orisys_autocal.motion import ConnectionSpec, DemoTransport
from orisys_autocal.preferences import load_preferences
from orisys_autocal.ui.bus_panel import default_bas_dir
from orisys_autocal.ui.window import AutoCalMainWindow
set_locale(args.language)
app = QApplication.instance() or QApplication([])
app.setStyleSheet(build_qss(args.theme))
prefs = load_preferences()
prefs["theme"] = args.theme
prefs["language"] = args.language
prefs["show_diagnostics"] = False
window = AutoCalMainWindow(prefs=prefs)
window.poller.service._transport = DemoTransport() # noqa: SLF001
window.resize(width, height)
window.show()
app.processEvents()
if args.demo:
window.connect_controller(ConnectionSpec(mode="demo"), label="connect")
_wait_for(app, lambda: window._last_snapshot.connected, 6.0)
window.bus_panel.set_bas_path(str(default_bas_dir() / "ECAT.bas"))
window._on_download_bas()
_wait_for(app, lambda: window._last_snapshot.bus.init_status == 1, 8.0)
window.axis_panel.set_selected_axis(0)
window.axis_panel.target_edit.setText("1000")
window._on_move_abs(0, 1000.0)
_wait_for(app, lambda: window._last_snapshot.axis(0).dpos != 0, 4.0)
app.processEvents()
if args.compare:
return _render_every_tab(window, app, args, width, height)
window.workspace.set_current_key(args.tab)
app.processEvents()
output = Path(args.out).resolve()
if not window.grab().save(str(output)):
print(f"could not write {output}", file=sys.stderr)
return 1
print(
f"wrote {output} ({width}x{height}, theme={args.theme}, "
f"lang={args.language}, tab={args.tab})"
)
window.close()
app.processEvents()
return 0
def _render_every_tab(window, app, args, width: int, height: int) -> int:
"""One image per workspace tab, so the whole layout can be reviewed at once."""
base = Path(args.out).resolve()
failures = 0
for key in window.workspace.page_keys():
window.workspace.set_current_key(key)
app.processEvents()
target = base.with_name(f"{base.stem}_{key}{base.suffix or '.png'}")
if window.grab().save(str(target)):
print(f"wrote {target} ({width}x{height}, tab={key})")
else:
print(f"could not write {target}", file=sys.stderr)
failures += 1
window.close()
app.processEvents()
return 1 if failures else 0
def _wait_for(app, predicate, timeout: float) -> bool:
import time
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
app.processEvents()
if predicate():
return True
time.sleep(0.02)
return False
if __name__ == "__main__":
raise SystemExit(main())