dsh ui improve: add brand bar
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dependency-free smoke run for the customer UI.
|
||||
|
||||
``pytest`` is the canonical runner (``apps/customer/tests``), but this script
|
||||
needs nothing beyond the app's own runtime dependencies, so it can be used in
|
||||
minimal environments:
|
||||
|
||||
python tools/ui_check.py
|
||||
|
||||
It builds the real main window offscreen and asserts the layout, theming and
|
||||
status-feedback invariants that the UI work depends on.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = APP_ROOT.parents[1]
|
||||
for _path in (APP_ROOT / "src", REPO_ROOT / "shared" / "src"):
|
||||
if _path.is_dir() and str(_path) not in sys.path:
|
||||
sys.path.insert(0, str(_path))
|
||||
|
||||
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)
|
||||
os.chdir(APP_ROOT)
|
||||
|
||||
FAILURES: list[str] = []
|
||||
CHECKS = 0
|
||||
|
||||
|
||||
def check(condition: bool, message: str) -> None:
|
||||
global CHECKS
|
||||
CHECKS += 1
|
||||
if not condition:
|
||||
FAILURES.append(message)
|
||||
|
||||
|
||||
def section(name: str) -> None:
|
||||
print(f"\n== {name} ==")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Palette rules
|
||||
#
|
||||
# These are the WCAG / colour-separation invariants the theme tokens must
|
||||
# satisfy. ``tests/test_ui_smoke.py`` imports :func:`audit_palette` from here so
|
||||
# both runners share one source of truth.
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
TEXT_MIN_RATIO = 4.5
|
||||
GRAPHIC_MIN_RATIO = 3.0
|
||||
VIZ_MIN_HUE_GAP = 90.0
|
||||
VIZ_MIN_LUMINANCE_RATIO = 1.2
|
||||
RAMP_BLEND_STOPS = 2 # the low stops may merge into the canvas
|
||||
|
||||
TEXT_PAIRS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("text_primary", (
|
||||
"window_bg", "panel_bg", "card_bg", "toolbar_bg", "input_bg",
|
||||
"table_header_bg", "table_row_alt", "menu_bg", "button_bg",
|
||||
"table_selected", "scrollbar_bg",
|
||||
)),
|
||||
("text_secondary", (
|
||||
"window_bg", "panel_bg", "card_bg", "toolbar_bg",
|
||||
"table_header_bg", "button_bg", "input_bg", "menu_bg",
|
||||
)),
|
||||
("text_muted", ("window_bg", "panel_bg", "card_bg", "toolbar_bg", "input_bg")),
|
||||
("hint_text", ("card_bg", "panel_bg", "window_bg", "input_bg")),
|
||||
("accent_text", ("accent", "accent_hover", "danger_fill", "danger_fill_hover")),
|
||||
("danger_text", ("danger_fill", "danger_fill_hover")),
|
||||
("danger", ("card_bg", "panel_bg")),
|
||||
("danger_hover", ("card_bg", "panel_bg")),
|
||||
("warning", ("card_bg", "panel_bg")),
|
||||
("info", ("card_bg", "panel_bg")),
|
||||
("success", ("card_bg", "panel_bg")),
|
||||
("status_idle", ("panel_bg",)),
|
||||
("status_connecting", ("panel_bg",)),
|
||||
("status_running", ("panel_bg",)),
|
||||
("status_error", ("panel_bg",)),
|
||||
("brand_bar_fg", ("brand_bar_bg",)),
|
||||
("brand_bar_fg_muted", ("brand_bar_bg",)),
|
||||
)
|
||||
|
||||
GRAPHIC_PAIRS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("plot_fn", ("plot_bg",)),
|
||||
("plot_fx", ("plot_bg",)),
|
||||
("plot_fy", ("plot_bg",)),
|
||||
("scrollbar_handle", ("scrollbar_bg",)),
|
||||
("scrollbar_handle_hover", ("scrollbar_bg",)),
|
||||
("accent", ("panel_bg", "window_bg", "toolbar_bg")),
|
||||
("accent_hover", ("panel_bg", "toolbar_bg")),
|
||||
("focus_ring", ("window_bg", "input_bg", "panel_bg", "card_bg")),
|
||||
("input_border", ("input_bg", "panel_bg", "card_bg", "toolbar_bg")),
|
||||
)
|
||||
|
||||
VIZ_KEYS = ("plot_fn", "plot_fx", "plot_fy")
|
||||
|
||||
# Deliberate, documented exemptions (see apps/customer/docs/ui-palette.md):
|
||||
# decorative 1px container outlines, and disabled states which WCAG 1.4.3
|
||||
# explicitly exempts.
|
||||
DECORATIVE_PAIRS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("card_border", ("card_bg",)),
|
||||
("panel_border", ("panel_bg", "window_bg")),
|
||||
("plot_grid", ("plot_bg",)),
|
||||
("separator", ("toolbar_bg", "panel_bg")),
|
||||
)
|
||||
|
||||
|
||||
def _srgb_channel(value: int) -> float:
|
||||
channel = value / 255.0
|
||||
if channel <= 0.03928:
|
||||
return channel / 12.92
|
||||
return ((channel + 0.055) / 1.055) ** 2.4
|
||||
|
||||
|
||||
def _rgb(hex_color: str) -> tuple[int, int, int]:
|
||||
text = str(hex_color).strip().lstrip("#")
|
||||
return tuple(int(text[i : i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
|
||||
|
||||
|
||||
def relative_luminance(hex_color: str) -> float:
|
||||
red, green, blue = _rgb(hex_color)
|
||||
return (
|
||||
0.2126 * _srgb_channel(red)
|
||||
+ 0.7152 * _srgb_channel(green)
|
||||
+ 0.0722 * _srgb_channel(blue)
|
||||
)
|
||||
|
||||
|
||||
def contrast_ratio(first: str, second: str) -> float:
|
||||
high = relative_luminance(first)
|
||||
low = relative_luminance(second)
|
||||
if high < low:
|
||||
high, low = low, high
|
||||
return (high + 0.05) / (low + 0.05)
|
||||
|
||||
|
||||
def hue_degrees(hex_color: str) -> float:
|
||||
red, green, blue = (value / 255.0 for value in _rgb(hex_color))
|
||||
high, low = max(red, green, blue), min(red, green, blue)
|
||||
delta = high - low
|
||||
if delta == 0:
|
||||
return 0.0
|
||||
if high == red:
|
||||
return (60 * ((green - blue) / delta) + 360) % 360
|
||||
if high == green:
|
||||
return 60 * ((blue - red) / delta) + 120
|
||||
return 60 * ((red - green) / delta) + 240
|
||||
|
||||
|
||||
def _hue_gap(first: str, second: str) -> float:
|
||||
delta = abs(hue_degrees(first) - hue_degrees(second))
|
||||
return min(delta, 360 - delta)
|
||||
|
||||
|
||||
def audit_palette(verbose: bool = False) -> list[str]:
|
||||
"""Return the list of palette-rule violations (empty when compliant)."""
|
||||
from orisys_app_shared.theme import load_colors
|
||||
|
||||
failures: list[str] = []
|
||||
themes = ("dark", "light")
|
||||
palettes = {theme: load_colors(theme) for theme in themes}
|
||||
|
||||
dark_keys, light_keys = set(palettes["dark"]), set(palettes["light"])
|
||||
if dark_keys != light_keys:
|
||||
failures.append(f"token keys differ between themes: {sorted(dark_keys ^ light_keys)}")
|
||||
|
||||
for theme, palette in palettes.items():
|
||||
if verbose:
|
||||
print(f" -- {theme} --")
|
||||
for key, value in sorted(palette.items()):
|
||||
if key == "flow_ramp":
|
||||
stops = [stop.strip() for stop in value.split(",") if stop.strip()]
|
||||
if not stops:
|
||||
failures.append(f"{theme}.flow_ramp is empty")
|
||||
for stop in stops:
|
||||
if not (stop.startswith("#") and len(stop) == 7):
|
||||
failures.append(f"{theme}.flow_ramp stop {stop!r} is not #RRGGBB")
|
||||
elif not (str(value).startswith("#") and len(str(value)) == 7):
|
||||
failures.append(f"{theme}.{key} = {value!r} is not #RRGGBB")
|
||||
|
||||
for key, backgrounds in TEXT_PAIRS:
|
||||
for background in backgrounds:
|
||||
ratio = contrast_ratio(palette[key], palette[background])
|
||||
if verbose:
|
||||
print(f" text {key:19s} on {background:17s} {ratio:5.2f}")
|
||||
if ratio < TEXT_MIN_RATIO:
|
||||
failures.append(
|
||||
f"{theme}: text {key} on {background} = {ratio:.2f} "
|
||||
f"(need {TEXT_MIN_RATIO})"
|
||||
)
|
||||
|
||||
for key, backgrounds in GRAPHIC_PAIRS:
|
||||
for background in backgrounds:
|
||||
ratio = contrast_ratio(palette[key], palette[background])
|
||||
if verbose:
|
||||
print(f" graph {key:19s} on {background:17s} {ratio:5.2f}")
|
||||
if ratio < GRAPHIC_MIN_RATIO:
|
||||
failures.append(
|
||||
f"{theme}: graphic {key} on {background} = {ratio:.2f} "
|
||||
f"(need {GRAPHIC_MIN_RATIO})"
|
||||
)
|
||||
|
||||
for index, first in enumerate(VIZ_KEYS):
|
||||
for second in VIZ_KEYS[index + 1 :]:
|
||||
gap = _hue_gap(palette[first], palette[second])
|
||||
high = max(relative_luminance(palette[first]), relative_luminance(palette[second]))
|
||||
low = min(relative_luminance(palette[first]), relative_luminance(palette[second]))
|
||||
ratio = (high + 0.05) / (low + 0.05)
|
||||
if verbose:
|
||||
print(
|
||||
f" viz {first:19s} vs {second:17s} "
|
||||
f"dHue={gap:5.1f} LrelRatio={ratio:4.2f}"
|
||||
)
|
||||
if gap < VIZ_MIN_HUE_GAP and ratio < VIZ_MIN_LUMINANCE_RATIO:
|
||||
failures.append(
|
||||
f"{theme}: {first}/{second} are too similar "
|
||||
f"(hue gap {gap:.0f} deg, luminance ratio {ratio:.2f})"
|
||||
)
|
||||
|
||||
stops = [stop.strip() for stop in palette["flow_ramp"].split(",") if stop.strip()]
|
||||
if len(stops) >= 2:
|
||||
luminances = [relative_luminance(stop) for stop in stops]
|
||||
if verbose:
|
||||
print(f" ramp luminances {[round(value, 4) for value in luminances]}")
|
||||
if not all(
|
||||
luminances[i] < luminances[i + 1] for i in range(len(luminances) - 1)
|
||||
):
|
||||
failures.append(f"{theme}: flow_ramp is not monotone in luminance")
|
||||
for stop in stops[RAMP_BLEND_STOPS:]:
|
||||
ratio = contrast_ratio(stop, palette["arrow_canvas"])
|
||||
if ratio < GRAPHIC_MIN_RATIO:
|
||||
failures.append(
|
||||
f"{theme}: flow_ramp stop {stop} on arrow_canvas = "
|
||||
f"{ratio:.2f} (need {GRAPHIC_MIN_RATIO})"
|
||||
)
|
||||
|
||||
for theme, palette in palettes.items():
|
||||
if palette["brand_bar_bg"] == palette["window_bg"]:
|
||||
failures.append(f"{theme}: brand_bar_bg collides with window_bg")
|
||||
if verbose:
|
||||
print(f" -- {theme} documented exemptions --")
|
||||
for key, backgrounds in DECORATIVE_PAIRS:
|
||||
for background in backgrounds:
|
||||
print(
|
||||
f" exempt {key:19s} on {background:17s} "
|
||||
f"{contrast_ratio(palette[key], palette[background]):5.2f}"
|
||||
)
|
||||
|
||||
return failures
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.force.calibration_io import CalSample
|
||||
from orisys_app_shared.i18n import set_locale, t
|
||||
from orisys_app_shared.theme import build_qss, load_colors
|
||||
from orisys_customer.app_window import CustomerMainWindow
|
||||
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
set_locale("zh_CN")
|
||||
|
||||
section("theme tokens")
|
||||
dark = set(load_colors("dark"))
|
||||
light = set(load_colors("light"))
|
||||
check(dark == light, f"token keys differ: {sorted(dark ^ light)}")
|
||||
for theme in ("dark", "light"):
|
||||
qss = build_qss(theme)
|
||||
check(len(qss) > 1000, f"{theme} qss suspiciously short")
|
||||
check(
|
||||
load_colors(theme)["arrow_canvas"] != load_colors(theme)["accent"],
|
||||
f"{theme}: arrow canvas collides with accent",
|
||||
)
|
||||
check("QProgressBar::chunk" in qss, f"{theme}: progress bar not styled")
|
||||
check("QHeaderView::section" in qss, f"{theme}: table header not styled")
|
||||
check("QRadioButton::indicator" in qss, f"{theme}: radio not styled")
|
||||
check("QScrollBar::handle" in qss, f"{theme}: scrollbar not styled")
|
||||
check("QFrame#brandBar" in qss, f"{theme}: brand bar not styled")
|
||||
check("brand_bar_bg" not in qss, f"{theme}: qss leaked a token name")
|
||||
|
||||
section("palette rules")
|
||||
palette_failures = audit_palette(verbose=True)
|
||||
for failure in palette_failures:
|
||||
check(False, failure)
|
||||
if not palette_failures:
|
||||
check(True, "palette rules hold for both themes")
|
||||
|
||||
scene_count = 0
|
||||
for theme in ("dark", "light"):
|
||||
for sensor in ("box", "finger"):
|
||||
section(f"window {theme}/{sensor}")
|
||||
scene_count += 1
|
||||
app.setStyleSheet(build_qss(theme))
|
||||
window = CustomerMainWindow(
|
||||
prefs={
|
||||
"language": "zh_CN",
|
||||
"theme": theme,
|
||||
"sensor_type": sensor,
|
||||
"show_cal_panel": True,
|
||||
}
|
||||
)
|
||||
window.resize(1440, 880)
|
||||
window.show()
|
||||
app.processEvents()
|
||||
|
||||
check(window.windowTitle() != "", "window title empty")
|
||||
check(
|
||||
window.status_chip.text() != "",
|
||||
"status chip has no phase text",
|
||||
)
|
||||
check(
|
||||
window.toolbar.btn_run.text().strip() not in ("", "▶"),
|
||||
f"run button is unlabelled: {window.toolbar.btn_run.text()!r}",
|
||||
)
|
||||
|
||||
brand_bar = window.brand_bar
|
||||
check(brand_bar.height() >= 28, "brand bar is too short to read")
|
||||
check(brand_bar.name.text() != "", "brand bar wordmark empty")
|
||||
check(brand_bar.tagline.text() != "", "brand bar tagline empty")
|
||||
check(brand_bar.product.text() == t("app.title"), "brand bar product name wrong")
|
||||
logo = brand_bar.logo.pixmap()
|
||||
if logo is not None and not logo.isNull():
|
||||
image = logo.toImage()
|
||||
pixels = [
|
||||
image.pixelColor(x, y).alpha()
|
||||
for y in range(image.height())
|
||||
for x in range(image.width())
|
||||
]
|
||||
check(any(a < 32 for a in pixels), "brand logo lost its transparency")
|
||||
check(any(a >= 200 for a in pixels), "brand logo renders empty")
|
||||
else:
|
||||
check(not brand_bar.logo.isVisible(), "hidden logo still shown")
|
||||
|
||||
panels = [
|
||||
window.mesh3d_panel,
|
||||
window.arrow_panel,
|
||||
window.force_plots,
|
||||
window.cal_panel,
|
||||
]
|
||||
for panel, width in zip(panels, window.content_splitter.sizes()):
|
||||
if panel.isVisible():
|
||||
check(
|
||||
width >= panel.minimumWidth(),
|
||||
f"{type(panel).__name__} width {width} < min {panel.minimumWidth()}",
|
||||
)
|
||||
|
||||
check(
|
||||
window.force_plots.width() >= window.force_plots.minimumWidth(),
|
||||
"force plots narrower than its minimum",
|
||||
)
|
||||
|
||||
# value cards must keep their text visible
|
||||
for card, label in zip(
|
||||
window.force_plots.value_cards, window.force_plots.value_labels
|
||||
):
|
||||
check(
|
||||
label.height() >= label.fontMetrics().height(),
|
||||
"value card text is clipped",
|
||||
)
|
||||
check(label.text() != "", "value card is empty")
|
||||
|
||||
# demo data flows through the plots and the calibration table
|
||||
for i in range(90):
|
||||
window.force_plots.push(i / 30.0, float(i % 7), -1.0, 1.0, unit="N")
|
||||
app.processEvents()
|
||||
check(
|
||||
window.force_plots.history.sample_count > 0,
|
||||
"plots did not accept samples",
|
||||
)
|
||||
|
||||
window.cal_panel.set_running(True)
|
||||
check(window.cal_panel.btn_record.isEnabled(), "record button stays disabled")
|
||||
window.cal_panel._store.upsert(
|
||||
"fn", CalSample(weight_n=1.0, raw_mean=9.7, n=120, std=0.02)
|
||||
)
|
||||
window.cal_panel._refresh_table()
|
||||
check(window.cal_panel.table.rowCount() == 1, "calibration row missing")
|
||||
check(
|
||||
not window.cal_panel.empty_hint.isVisible(),
|
||||
"empty hint still visible with rows",
|
||||
)
|
||||
|
||||
window._set_phase("running")
|
||||
check("status_running" not in window.status_chip.text(), "chip not localized")
|
||||
window._set_theme("light" if theme == "dark" else "dark")
|
||||
app.processEvents()
|
||||
|
||||
window._persist_prefs()
|
||||
check(
|
||||
len(window.prefs.get("splitter_sizes", [])) == 4,
|
||||
"splitter sizes not persisted",
|
||||
)
|
||||
window.close()
|
||||
app.processEvents()
|
||||
|
||||
section("small window")
|
||||
app.setStyleSheet(build_qss("dark"))
|
||||
window = CustomerMainWindow(
|
||||
prefs={
|
||||
"language": "en_US",
|
||||
"theme": "dark",
|
||||
"sensor_type": "finger",
|
||||
"show_cal_panel": True,
|
||||
"show_finger_3d": True,
|
||||
}
|
||||
)
|
||||
window.resize(1280, 720)
|
||||
window.show()
|
||||
app.processEvents()
|
||||
for panel, width in zip(
|
||||
[
|
||||
window.mesh3d_panel,
|
||||
window.arrow_panel,
|
||||
window.force_plots,
|
||||
window.cal_panel,
|
||||
],
|
||||
window.content_splitter.sizes(),
|
||||
):
|
||||
if panel.isVisible():
|
||||
check(width >= panel.minimumWidth(), f"1280px: {type(panel).__name__} squeezed")
|
||||
oversize = []
|
||||
for child in window.findChildren(object):
|
||||
if not hasattr(child, "isVisible") or not child.isVisible():
|
||||
continue
|
||||
if not hasattr(child, "geometry"):
|
||||
continue
|
||||
parent = child.parentWidget()
|
||||
if parent is None or not parent.isVisible():
|
||||
continue
|
||||
geometry = child.geometry()
|
||||
if geometry.width() > parent.width() + 1 or geometry.height() > parent.height() + 1:
|
||||
oversize.append(type(child).__name__)
|
||||
check(not oversize, f"widgets overflow their parent: {oversize}")
|
||||
window.close()
|
||||
app.processEvents()
|
||||
|
||||
print(f"\nscenes: {scene_count} checks: {CHECKS} failures: {len(FAILURES)}")
|
||||
for failure in FAILURES:
|
||||
print(f" FAIL: {failure}")
|
||||
return 1 if FAILURES else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the customer UI offscreen to PNG files for visual review.
|
||||
|
||||
The tool never opens a camera and never starts acquisition: it builds the real
|
||||
main window, optionally injects deterministic demo data, then grabs the window.
|
||||
|
||||
Examples
|
||||
--------
|
||||
python tools/ui_snapshot.py --theme dark --out out_dark.png
|
||||
python tools/ui_snapshot.py --theme light --sensor finger \
|
||||
--panels cal,3d --demo --size 1600x900 --out out_light.png
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = APP_ROOT.parents[1]
|
||||
for _path in (APP_ROOT / "src", REPO_ROOT / "shared" / "src"):
|
||||
if _path.is_dir() and str(_path) not in sys.path:
|
||||
sys.path.insert(0, str(_path))
|
||||
|
||||
# Silent platform must be selected before QApplication exists.
|
||||
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)
|
||||
os.chdir(APP_ROOT)
|
||||
|
||||
|
||||
def _demo_data(window) -> None:
|
||||
"""Fill the plots, the calibration table and the arrow canvas."""
|
||||
plots = window.force_plots
|
||||
for i in range(240):
|
||||
t = i / 30.0
|
||||
fn = 6.0 + 5.0 * math.sin(t * 1.4)
|
||||
fx = 4.0 * math.sin(t * 2.1 + 0.4)
|
||||
fy = -3.0 + 2.5 * math.cos(t * 1.1)
|
||||
plots.push(t, fn, fx, fy, unit="N")
|
||||
|
||||
cal = window.cal_panel
|
||||
cal.set_running(True)
|
||||
cal.push_raw(9.81, -0.42, 0.10)
|
||||
for weight, raw, std in ((1.0, 9.72, 0.031), (2.0, 19.51, 0.044), (5.0, 48.90, 0.061)):
|
||||
from orisys_app_shared.force.calibration_io import CalSample
|
||||
|
||||
cal._store.upsert("fn", CalSample(weight_n=weight, raw_mean=raw, n=180, std=std))
|
||||
cal._refresh_table()
|
||||
|
||||
# Synthetic outward flow so the branded canvas and flow ramp are reviewable.
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.acquisition.arrows import (
|
||||
draw_arrow_display,
|
||||
flow_colormap,
|
||||
hex_to_bgr,
|
||||
)
|
||||
from orisys_app_shared.theme import color
|
||||
|
||||
from orisys_customer.app_window import _bgr_to_qimage
|
||||
|
||||
size = 320
|
||||
grid_y, grid_x = np.mgrid[0:size, 0:size].astype(np.float32)
|
||||
centre = size / 2.0
|
||||
flow = np.zeros((size, size, 2), np.float32)
|
||||
flow[..., 0] = (grid_x - centre) * 0.10
|
||||
flow[..., 1] = (grid_y - centre) * 0.10
|
||||
blank = np.zeros((size, size, 3), np.uint8)
|
||||
rendered = draw_arrow_display(
|
||||
blank,
|
||||
flow,
|
||||
background="pure_color",
|
||||
canvas_bgr=hex_to_bgr(color(window._theme, "arrow_canvas", "#0B1830")),
|
||||
draw_style="finger" if window.sensor_picker.sensor_type() == "finger" else "box",
|
||||
colormap=flow_colormap(window._theme),
|
||||
)
|
||||
window.arrow_panel.show_frame(_bgr_to_qimage(rendered))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--theme", choices=("dark", "light"), default="dark")
|
||||
parser.add_argument("--lang", choices=("zh_CN", "en_US"), default="zh_CN")
|
||||
parser.add_argument("--sensor", choices=("box", "mini", "finger"), default="box")
|
||||
parser.add_argument(
|
||||
"--panels",
|
||||
default="",
|
||||
help="comma list of optional panels to show: cal,3d",
|
||||
)
|
||||
parser.add_argument("--size", default="1440x880", help="WxH window size")
|
||||
parser.add_argument("--view", default="arrows_pure")
|
||||
parser.add_argument("--demo", action="store_true", help="inject demo plot/table data")
|
||||
parser.add_argument("--out", required=True, help="output PNG path")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
width, height = (int(part) for part in args.size.lower().split("x", 1))
|
||||
except ValueError:
|
||||
parser.error("--size must look like 1440x880")
|
||||
|
||||
panels = {part.strip().lower() for part in args.panels.split(",") if part.strip()}
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.i18n import set_locale
|
||||
from orisys_app_shared.theme import build_qss
|
||||
from orisys_customer.app_window import CustomerMainWindow
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_locale(args.lang)
|
||||
app.setStyleSheet(build_qss(args.theme))
|
||||
|
||||
prefs = {
|
||||
"language": args.lang,
|
||||
"theme": args.theme,
|
||||
"sensor_type": args.sensor,
|
||||
"show_cal_panel": "cal" in panels,
|
||||
"show_finger_3d": "3d" in panels,
|
||||
"show_fps": True,
|
||||
"image_view_mode": args.view,
|
||||
}
|
||||
window = CustomerMainWindow(prefs=prefs)
|
||||
window.resize(width, height)
|
||||
window.show()
|
||||
app.processEvents()
|
||||
|
||||
if args.demo:
|
||||
_demo_data(window)
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
ok = window.grab().save(str(out))
|
||||
print(f"{'saved' if ok else 'FAILED'} {out} ({width}x{height}, theme={args.theme})")
|
||||
window.close()
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user