#!/usr/bin/env python3 """Draw the *proposed* AutoCal console as a picture. This is a wireframe, not the application. It exists so the layout can be reviewed and marked up before (or independently of) the real panels being built — there is no controller, no sensor and no 3D behind it, and it imports nothing from ``orisys_autocal`` except the shared theme tokens. python apps/autocal/tools/layout_mockup.py --out layout.png python apps/autocal/tools/layout_mockup.py --theme light --language en_US Canvas contents: * the **full page** at the design resolution (menu bar, permanent status strip, three columns, status bar) with the *Axis debug* tab active; * a **sequence design page**, drawn at the same size, because that interface is a design question in its own right (workflow P5); * **previews** of the remaining three tabs, so the whole plan is in one image. Numbers are pixels and follow the implementation's measured minimums (setup rail 326, readout rail 220, workspace the remainder). """ from __future__ import annotations import argparse import math 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)) # No display and no bundled fonts: the offscreen platform needs both told. 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) from PySide6.QtCore import QPoint, QRect, Qt # noqa: E402 from PySide6.QtGui import QColor, QFont, QPainter, QPen, QPixmap, QPolygon # noqa: E402 from PySide6.QtWidgets import QApplication # noqa: E402 # ---------------------------------------------------------------- geometry --- PAGE_W, PAGE_H = 1920, 1080 MARGIN = 8 MENU_H = 24 BRAND_H = 36 STRIP_H = 44 RUNBAR_H = 40 STATUS_H = 26 RAIL_LEFT = 330 WORKSPACE_W = PAGE_W - RAIL_LEFT - 3 * MARGIN PREVIEW_W, PREVIEW_H = 640, 340 #: Column widths of the axis table: seven data columns plus the actions group. AXIS_COL_W = (46, 96, 100, 100, 130, 130, 130) AXIS_ACTIONS_W = 166 STRINGS = { "zh_CN": { "menu": ["文件", "视图", "帮助"], "brand_detail": "叠动科技", "chip_conn": "已连接", "chip_bus": "EtherCAT", "chip_init": "初始化成功", "chip_axes": "已使能 5/5 轴", "chip_health": "刷新 12 ms 前", "strip_title": "运动控制", "pause": "暂停刷新", "stop_all": "全部停止", "run_title": "运行", "run_phase": "空闲", "run_step": "第 0/0 点", "run_time": "已用 0:00 · 预计剩余 —", "run_run": "运行", "run_pause": "暂停", "run_abort": "中止", "conn_title": "连接控制器", "conn_mode": "连接方式", "conn_mode_value": "以太网", "conn_ip": "IP", "scan": "IP 扫描", "connect": "连接", "disconnect": "断开连接", "connected": "已连接", "bus_title": "总线初始化", "bus_file": "BAS 文件", "choose": "选择 BAS…", "download": "下载并初始化", "reinit": "重新初始化", "bus_state": "初始化状态", "bus_ok": "初始化成功", "bus_type": "总线类型", "nodes": "节点数量", "axes_count": "轴数量", "busdata_group": "总线数据 (SDO / RTEX) ▶", "tabs": ["轴调试", "测试序列", "传感器", "表面分布", "标定点"], "readout_title": "轴状态", "enabled": "已使能", "disabled": "未使能", "position": "0.000 (0.000 mm)", "enable_all": "全部关闭使能", "clear_alarms": "清除全部报警", "status_bar": "状态刷新正常 | 刷新间隔 100 ms | 初始化成功", "axis_label": "轴号", "axis_params": ["脉冲当量", "速度", "加速度"], "axis_values": ["10.000", "1000.0", "10000.0"], "home_fields": ["回零模式", "回零高速", "回零低速", "回零偏移"], "home_values": ["0", "50.0", "10.0", "0.000"], "apply": "应用参数", "home": "启动回零", "clear_alarm": "清除报警", "target": "目标位置", "move_abs": "绝对定位", "move_rel": "相对运动", "mm_hint": "毫米值由脉冲当量换算得出(显示值,非标定长度)", "cols": ["轴号", "使能", "脉冲当量", "速度", "命令位置", "位置 (mm)", "状态"], "row_actions": ["正转", "反转", "停止"], "actions_header": "操作", "axis_detail": "选中轴 0: 轴类型 65 (位置) · 加速度 10000.0 · 反馈 999.000 (1.998 mm) · 轴状态 0 · 回零完成", "seq_profile": "标定点生成", "seq_preview": "预览", "seq_validation": "校验", "seq_points": "标定点列表", "seq_fields": [ ("曲面模式", "平面"), ("点距 (mm)", "2.000"), ("边界余量 (mm)", "1.000"), ("曲面半径 (mm)", "—"), ("安全高度 (mm)", "5.000"), ("进给速度 (mm/s)", "2.000"), ("下压速度 (mm/s)", "0.500"), ("下压深度 (mm)", "0.200"), ("保压 (s)", "0.500"), ("稳定窗口 (s)", "0.500"), ("重复次数", "3"), ("遍历顺序", "蛇形"), ], "seq_preview_placeholder": "三维预览:传感器表面 · 标定点 · 遍历顺序", "seq_preview_note": "(仿真阶段实现)", "seq_preview_hint": "生成后可在此检查点是否落在曲面上、是否在行程内、接触能否被确认。", "seq_checks": ["在曲面范围内", "在行程内", "接触可确认"], "seq_checks_pending": "尚未生成标定点。", "seq_buttons": [("生成标定点", True), ("仿真", True), ("干跑(不接触)", True), ("保存序列…", True), ("载入序列…", True)], "point_cols": ["#", "X (mm)", "Y (mm)", "Z (mm)", "状态", "灵敏度(上次)", "样本", "操作"], "point_rows": [ ["1", "0.000", "0.000", "12.400", "待测", "—", "—", ""], ["2", "2.000", "0.000", "12.400", "待测", "—", "—", ""], ["3", "4.000", "0.000", "12.400", "待测", "—", "—", ""], ], "points_hint": "坐标由网格生成并逐点校验,不接受手工输入:手工输入的坐标没有任何检查,一个错数字会毁掉整条序列。", "sensor_note": "设备与信号源、实时图像、力曲线", "sensor_curve": "力曲线 (FN / FX / FY)", "surface_note": "多点标定灵敏度 → 三维表面色图", "legend": "灵敏度 (N/单位)", "surface_footer": "12 个标定点 · 几何 finger-1.0.0 · 拖动旋转 · 滚轮缩放", "points_page_note": "标定点列表:平台坐标 · 参考载荷 · 采样统计 · 灵敏度", "previews_caption": "其余工作区标签页", }, "en_US": { "menu": ["File", "View", "Help"], "brand_detail": "Orisys Technology", "chip_conn": "Connected", "chip_bus": "EtherCAT", "chip_init": "Initialised", "chip_axes": "5/5 axes enabled", "chip_health": "Read 12 ms ago", "strip_title": "Motion", "pause": "Pause refresh", "stop_all": "Stop all", "run_title": "Run", "run_phase": "Idle", "run_step": "Point 0/0", "run_time": "Elapsed 0:00 · about — left", "run_run": "Run", "run_pause": "Pause", "run_abort": "Abort", "conn_title": "Controller connection", "conn_mode": "Connection", "conn_mode_value": "Ethernet", "conn_ip": "IP", "scan": "Scan IP", "connect": "Connect", "disconnect": "Disconnect", "connected": "Connected", "bus_title": "Bus initialisation", "bus_file": "BAS file", "choose": "Choose BAS…", "download": "Download and initialise", "reinit": "Re-initialise", "bus_state": "Init state", "bus_ok": "Initialised", "bus_type": "Bus type", "nodes": "Nodes", "axes_count": "Axes", "busdata_group": "Bus data (SDO / RTEX) ▶", "tabs": ["Axis debug", "Test sequence", "Sensor", "Surface map", "Calibration points"], "readout_title": "Axis state", "enabled": "Enabled", "disabled": "Disabled", "position": "0.000 (0.000 mm)", "enable_all": "Disable all", "clear_alarms": "Clear all alarms", "status_bar": "State refresh healthy | Refresh every 100 ms | Initialised", "axis_label": "Axis", "axis_params": ["Units", "Speed", "Accel"], "axis_values": ["10.000", "1000.0", "10000.0"], "home_fields": ["Home mode", "Home speed", "Home creep", "Home offset"], "home_values": ["0", "50.0", "10.0", "0.000"], "apply": "Apply", "home": "Home", "clear_alarm": "Clear alarm", "target": "Target", "move_abs": "Move abs", "move_rel": "Move rel", "mm_hint": "Millimetres are derived from the pulse equivalent (display only)", "cols": ["Axis", "Enable", "Units", "Speed", "DPOS", "DPOS(mm)", "Status"], "row_actions": ["Fwd", "Rev", "Stop"], "actions_header": "Actions", "axis_detail": "Axis 0: type 65 (position) · accel 10000.0 · feedback 999.000 (1.998 mm) · status 0 · homed", "seq_profile": "Point generation", "seq_preview": "Preview", "seq_validation": "Checks", "seq_points": "Calibration points", "seq_fields": [ ("Surface mode", "Flat"), ("Pitch (mm)", "2.000"), ("Margin (mm)", "1.000"), ("Radius (mm)", "—"), ("Clear height (mm)", "5.000"), ("Approach (mm/s)", "2.000"), ("Press speed (mm/s)", "0.500"), ("Press depth (mm)", "0.200"), ("Dwell (s)", "0.500"), ("Settle window (s)", "0.500"), ("Repeats", "3"), ("Point order", "Serpentine"), ], "seq_preview_placeholder": "3D preview: sensor surface · points · visiting order", "seq_preview_note": "(implemented in the simulation phase)", "seq_preview_hint": "After generation, check each point is on the surface, inside travel and confirmable by contact.", "seq_checks": ["On the surface", "Within travel", "Contact confirmable"], "seq_checks_pending": "No points generated yet.", "seq_buttons": [("Generate points", True), ("Simulate", True), ("Dry run (no contact)", True), ("Save sequence…", True), ("Load sequence…", True)], "point_cols": ["#", "X (mm)", "Y (mm)", "Z (mm)", "Status", "Sensitivity (last)", "Samples", "Actions"], "point_rows": [ ["1", "0.000", "0.000", "12.400", "Pending", "—", "—", ""], ["2", "2.000", "0.000", "12.400", "Pending", "—", "—", ""], ["3", "4.000", "0.000", "12.400", "Pending", "—", "—", ""], ], "points_hint": "Coordinates are generated and checked per point; they are not typed. A typed coordinate is one nothing has checked.", "sensor_note": "Device and source, live image, force curves", "sensor_curve": "Force curves (FN / FX / FY)", "surface_note": "Multi-point calibration sensitivity → 3D surface colour map", "legend": "Sensitivity (N/unit)", "surface_footer": "12 points · geometry finger-1.0.0 · drag to rotate · wheel to zoom", "points_page_note": "Calibration points: platform coords · reference load · statistics · sensitivity", "previews_caption": "Remaining workspace tabs", }, } class Palette: """Colours from the shared theme, so the mockup matches the product.""" def __init__(self, theme: str) -> None: from orisys_app_shared.theme import load_colors self.raw = load_colors(theme) self.window = self._c("window_bg", "#f4f5f7") self.panel = self._c("panel_bg", "#ffffff") self.border = self._c("panel_border", "#d0d5dd") self.text = self._c("text_primary", "#101828") self.muted = self._c("text_muted", "#667085") self.accent = self._c("accent", "#2f6fed") self.warn = self._c("warn", "#b7791f") self.danger = self._c("danger", "#d92d20") self.bar = self._c("brand_bar_bg", "#ffffff") def _c(self, key: str, fallback: str) -> QColor: return QColor(self.raw.get(key) or fallback) # ------------------------------------------------------------------ helpers --- def _font(size: int = 11, *, bold: bool = False) -> QFont: font = QFont() font.setPointSize(size) font.setBold(bold) return font def _panel(painter: QPainter, rect: QRect, palette: Palette) -> QRect: painter.fillRect(rect, palette.panel) painter.setPen(QPen(palette.border, 1)) painter.drawRect(rect.adjusted(0, 0, -1, -1)) return rect.adjusted(10, 8, -10, -10) def _box_title(painter: QPainter, rect: QRect, text: str, palette: Palette) -> None: """A group-box title, drawn on the frame like Qt does.""" painter.setFont(_font(10, bold=True)) painter.setPen(QPen(palette.text)) metrics = painter.fontMetrics() width = metrics.horizontalAdvance(text) + 12 painter.fillRect(QRect(rect.x() + 10, rect.y() - metrics.height() // 2, width, metrics.height()), palette.panel) painter.drawText( QRect(rect.x() + 14, rect.y() - metrics.height() // 2, width, metrics.height()), Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, text, ) def _text( painter: QPainter, point_or_rect: QPoint | QRect, text: str, palette: Palette, *, size: int = 11, bold: bool = False, muted: bool = False, colour: QColor | None = None, align=Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, ) -> None: painter.setFont(_font(size, bold=bold)) painter.setPen(QPen(colour or (palette.muted if muted else palette.text))) if isinstance(point_or_rect, QRect): painter.drawText(point_or_rect, align | Qt.TextFlag.TextWordWrap if text.count("\n") else align, text) return painter.drawText(point_or_rect, text) def _chip(painter: QPainter, pos: QPoint, text: str, palette: Palette, colour: QColor) -> int: painter.setFont(_font(10)) width = painter.fontMetrics().horizontalAdvance(text) + 20 rect = QRect(pos.x(), pos.y(), width, 22) painter.setPen(QPen(colour, 1)) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawRoundedRect(rect, 10, 10) painter.setPen(QPen(colour)) painter.setFont(_font(10)) painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, text) return width + 8 def _button( painter: QPainter, rect: QRect, text: str, palette: Palette, *, primary: bool = False, danger: bool = False, disabled: bool = False, ) -> None: if disabled: painter.fillRect(rect, palette.window) label = palette.muted elif primary: painter.fillRect(rect, palette.accent) label = QColor("#ffffff") elif danger: painter.fillRect(rect, palette.danger) label = QColor("#ffffff") else: painter.fillRect(rect, palette.panel) label = palette.text painter.setPen(QPen(palette.border if not (primary or danger) else (palette.accent if primary else palette.danger), 1)) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawRect(rect.adjusted(0, 0, -1, -1)) painter.setPen(QPen(label)) painter.setFont(_font(10)) painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, text) def _field(painter: QPainter, rect: QRect, text: str, palette: Palette, *, disabled: bool = False) -> None: painter.fillRect(rect, palette.panel if not disabled else palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(rect.adjusted(0, 0, -1, -1)) painter.setFont(_font(10)) painter.setPen(QPen(palette.muted if disabled else palette.text)) painter.drawText( rect.adjusted(6, 0, -6, 0), Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, text ) # --------------------------------------------------------------- the pieces --- def draw_chrome(painter: QPainter, palette: Palette, s: dict) -> None: painter.fillRect(QRect(0, 0, PAGE_W, MENU_H), palette.bar) x = 12 for item in s["menu"]: _text(painter, QPoint(x, MENU_H - 8), item, palette, size=10) x += painter.fontMetrics().horizontalAdvance(item) + 24 painter.fillRect(QRect(0, MENU_H, PAGE_W, BRAND_H), palette.bar) painter.setPen(QPen(palette.border, 1)) painter.drawLine(0, MENU_H + BRAND_H - 1, PAGE_W, MENU_H + BRAND_H - 1) painter.setPen(QPen(palette.accent, 2)) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawRect(QRect(14, MENU_H + 9, 18, 18)) _text(painter, QPoint(42, MENU_H + BRAND_H - 13), "ORISYS", palette, size=13, bold=True) _text(painter, QPoint(112, MENU_H + BRAND_H - 13), s["brand_detail"], palette, size=10, muted=True) _text( painter, QRect(PAGE_W - 700, MENU_H, 560, BRAND_H), "Orisys AutoCal — 标定台控制 / Platform Control", palette, size=11, colour=palette.text, align=Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, ) _chip(painter, QPoint(PAGE_W - 128, MENU_H + 8), s["chip_conn"], palette, palette.accent) def draw_status_strip(painter: QPainter, palette: Palette, s: dict, top: int) -> None: painter.fillRect(QRect(0, top, PAGE_W, STRIP_H), palette.panel) painter.setPen(QPen(palette.border, 1)) painter.drawLine(0, top, PAGE_W, top) painter.drawLine(0, top + STRIP_H - 1, PAGE_W, top + STRIP_H - 1) x = 16 _text(painter, QPoint(x, top + 28), s["strip_title"], palette, size=11, bold=True) x += painter.fontMetrics().horizontalAdvance(s["strip_title"]) + 20 for label in (s["chip_conn"], s["chip_bus"], s["chip_init"], s["chip_axes"], s["chip_health"]): x += _chip(painter, QPoint(x, top + 11), label, palette, palette.accent) _text( painter, QRect(PAGE_W - 600, top, 380, STRIP_H), "上次动作: 已使能轴0 / last action: enable axis 0", palette, size=10, muted=True, align=Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, ) _button(painter, QRect(PAGE_W - 210, top + 10, 96, 24), s["pause"], palette) _button(painter, QRect(PAGE_W - 106, top + 10, 92, 24), s["stop_all"], palette, danger=True) def draw_status_bar(painter: QPainter, palette: Palette, s: dict, top: int) -> None: """The footer, using its own token rather than the header's navy.""" background = QColor(palette.raw.get("status_bar_bg") or "#EDF1F8") foreground = QColor(palette.raw.get("status_bar_text") or "#141A24") painter.fillRect(QRect(0, top, PAGE_W, STATUS_H), background) painter.setPen(QPen(palette.border, 1)) painter.drawLine(0, top, PAGE_W, top) _text(painter, QPoint(12, top + 17), s["status_bar"], palette, size=10, colour=foreground) def draw_left_rail(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None: conn = QRect(rect.x(), rect.y(), rect.width(), 210) inner = _panel(painter, conn, palette) _box_title(painter, conn, s["conn_title"], palette) y = inner.y() + 6 _text(painter, QRect(inner.x(), y, 76, 22), s["conn_mode"], palette, size=10, muted=True) _field(painter, QRect(inner.x() + 78, y, inner.width() - 78, 22), s["conn_mode_value"] + " ▾", palette) y += 30 _text(painter, QRect(inner.x(), y, 30, 22), s["conn_ip"], palette, size=10, muted=True) _field(painter, QRect(inner.x() + 32, y, 140, 22), "192.168.0.11", palette) _button(painter, QRect(inner.x() + 180, y, 88, 22), s["scan"], palette) y += 32 _button(painter, QRect(inner.x(), y, 104, 26), s["connect"], palette, primary=True) _button(painter, QRect(inner.x() + 112, y, 108, 26), s["disconnect"], palette) y += 36 _text(painter, QRect(inner.x(), y, inner.width(), 20), s["connected"], palette, size=10, muted=True) y += 24 _text(painter, QRect(inner.x(), y, inner.width(), 20), "demo (simulated platform)", palette, size=9, muted=True) bus = QRect(rect.x(), rect.y() + 222, rect.width(), 250) inner = _panel(painter, bus, palette) _box_title(painter, bus, s["bus_title"], palette) y = inner.y() + 6 _text(painter, QRect(inner.x(), y, 60, 22), s["bus_file"], palette, size=10, muted=True) _field(painter, QRect(inner.x() + 62, y, 140, 22), "…\\bas\\ECAT.bas", palette) _button(painter, QRect(inner.x() + 208, y, 66, 22), s["choose"], palette) y += 30 _text(painter, QRect(inner.x(), y, 74, 20), s["bus_state"], palette, size=10, muted=True) _text(painter, QRect(inner.x() + 76, y, 180, 20), s["bus_ok"], palette, size=10) y += 24 _text(painter, QRect(inner.x(), y, 74, 20), s["bus_type"], palette, size=10, muted=True) _text(painter, QRect(inner.x() + 76, y, 180, 20), s["chip_bus"], palette, size=10) y += 24 _text(painter, QRect(inner.x(), y, 90, 20), s["nodes"], palette, size=10, muted=True) _text(painter, QRect(inner.x() + 92, y, 40, 20), "5", palette, size=10) _text(painter, QRect(inner.x() + 146, y, 70, 20), s["axes_count"], palette, size=10, muted=True) _text(painter, QRect(inner.x() + 218, y, 40, 20), "5", palette, size=10) y += 32 _button(painter, QRect(inner.x(), y, 146, 26), s["download"], palette, primary=True) _button(painter, QRect(inner.x() + 154, y, 104, 26), s["reinit"], palette) group = QRect(rect.x(), rect.y() + 484, rect.width(), 32) _panel(painter, group.adjusted(0, 0, 0, -8), palette) _text(painter, QRect(group.x() + 12, group.y(), group.width() - 24, 30), s["busdata_group"], palette, size=10, muted=True) # The axis readout lives in this rail now, not in a second one on the right: # two thin rails cost the workspace 240 px at every window size. readout = QRect(rect.x(), rect.y() + 526, rect.width(), 268) inner = _panel(painter, readout, palette) _box_title(painter, readout, s["readout_title"], palette) y = inner.y() + 6 for axis in range(5): _text(painter, QRect(inner.x(), y, 16, 22), str(axis), palette, size=10, muted=True) _text(painter, QRect(inner.x() + 20, y, 62, 22), s["enabled"], palette, size=9, muted=True) _text( painter, QRect(inner.x() + 84, y, inner.width() - 88, 22), s["position"], palette, size=10, align=Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, ) y += 24 _button(painter, QRect(inner.x(), y + 4, inner.width(), 28), s["enable_all"], palette) _button(painter, QRect(inner.x(), y + 40, inner.width(), 28), s["stop_all"], palette, danger=True) _button(painter, QRect(inner.x(), y + 76, inner.width(), 28), s["clear_alarms"], palette) def draw_tabs(painter: QPainter, palette: Palette, s: dict, workspace: QRect, active: str) -> QRect: tab_h = 30 tab_w = workspace.width() // len(s["tabs"]) for index, name in enumerate(s["tabs"]): tab = QRect(workspace.x() + index * tab_w, workspace.y(), tab_w, tab_h) is_active = name == active painter.fillRect(tab, palette.panel if not is_active else palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(tab.adjusted(0, 0, -1, -1)) if is_active: painter.setPen(QPen(palette.accent, 2)) painter.drawLine(tab.bottomLeft(), tab.bottomRight()) _text( painter, tab, name, palette, size=10, bold=is_active, colour=palette.text if is_active else palette.muted, align=Qt.AlignmentFlag.AlignCenter, ) return QRect( workspace.x() + 1, workspace.y() + tab_h + 1, workspace.width() - 2, workspace.height() - tab_h - 2, ) def draw_axis_tab(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None: inner = rect.adjusted(10, 8, -10, -8) y = inner.y() _text(painter, QRect(inner.x(), y, 44, 22), s["axis_label"], palette, size=10, muted=True) _text(painter, QRect(inner.x() + 46, y, 30, 22), "0", palette, size=10) x = inner.x() + 96 for label, value in zip(s["axis_params"], s["axis_values"]): _text(painter, QRect(x, y, 90, 22), label, palette, size=10, muted=True) x += painter.fontMetrics().horizontalAdvance(label) + 8 _field(painter, QRect(x, y, 92, 22), value, palette) x += 108 y += 32 x = inner.x() for label, value in zip(s["home_fields"], s["home_values"]): _text(painter, QRect(x, y, 80, 22), label, palette, size=10, muted=True) x += painter.fontMetrics().horizontalAdvance(label) + 8 _field(painter, QRect(x, y, 66, 22), value, palette) x += 82 y += 34 _button(painter, QRect(inner.x(), y, 116, 26), s["apply"], palette, primary=True) _button(painter, QRect(inner.x() + 126, y, 106, 26), s["home"], palette) _button(painter, QRect(inner.x() + 242, y, 106, 26), s["clear_alarm"], palette) _text( painter, QRect(inner.x() + 380, y, inner.width() - 384, 26), s["mm_hint"], palette, size=9, muted=True, align=Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, ) y += 36 _text(painter, QRect(inner.x(), y, 64, 22), s["target"], palette, size=10, muted=True) _field(painter, QRect(inner.x() + 66, y, 120, 22), "1000", palette) _button(painter, QRect(inner.x() + 196, y, 96, 24), s["move_abs"], palette) _button(painter, QRect(inner.x() + 300, y, 96, 24), s["move_rel"], palette) y += 34 table_h = rect.height() - (y - rect.y()) - 96 table = QRect(inner.x(), y, inner.width(), table_h) painter.fillRect(table, palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(table.adjusted(0, 0, -1, -1)) actions_w = AXIS_ACTIONS_W data_w = sum(AXIS_COL_W) header_h = 26 painter.fillRect(QRect(table.x(), table.y(), data_w, header_h), palette.panel) x = table.x() for name, width in zip(s["cols"], AXIS_COL_W): _text(painter, QRect(x, table.y(), width, header_h), name, palette, size=9, muted=True, align=Qt.AlignmentFlag.AlignCenter) x += width _text( painter, QRect(table.x() + data_w, table.y(), actions_w, header_h), s["actions_header"], palette, size=9, muted=True, align=Qt.AlignmentFlag.AlignCenter, ) row_h = 40 values = ["0", "已使能", "10.000", "1000.0", "1000.000", "2.000", "停止"] en_values = ["0", "Enabled", "10.000", "1000.0", "1000.000", "2.000", "Stopped"] row_values = values if s["axis_label"] == "轴号" else en_values for row in range(5): row_y = table.y() + header_h + row * row_h if row_y + row_h > table.bottom(): break if row % 2 == 0: painter.fillRect(QRect(table.x() + 1, row_y, data_w + actions_w - 2, row_h - 1), palette.panel) x = table.x() for column, (name, width) in enumerate(zip(s["cols"], AXIS_COL_W)): value = str(row) if column == 0 else row_values[column] _text(painter, QRect(x, row_y, width, row_h), value, palette, size=10, align=Qt.AlignmentFlag.AlignCenter if column else Qt.AlignmentFlag.AlignLeft) x += width offset = table.x() + data_w + 8 for label in s["row_actions"]: _button(painter, QRect(offset, row_y + 8, 46, 24), label, palette) offset += 50 y = table.bottom() + 8 _text(painter, QRect(inner.x(), y, inner.width(), 20), s["axis_detail"], palette, size=9, muted=True) y += 26 _button(painter, QRect(inner.x(), y, 116, 28), s["stop_all"], palette, danger=True) def draw_sequence_tab(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None: inner = rect.adjusted(10, 8, -10, -8) profile_w, validation_w = 420, 280 gap = 12 preview_w = inner.width() - profile_w - validation_w - 2 * gap # The zones get what they need; the point list takes the rest, because it is # the part that grows. top_h = 260 # --- profile: two columns of six fields, so the whole set is visible at once profile = QRect(inner.x(), inner.y(), profile_w, top_h) p_inner = _panel(painter, profile, palette) _box_title(painter, profile, s["seq_profile"], palette) column_w = p_inner.width() // 2 for index, (label, value) in enumerate(s["seq_fields"]): row, side = divmod(index, 2) x = p_inner.x() + side * column_w y = p_inner.y() + 20 + row * 26 _text(painter, QRect(x, y, 130, 22), label, palette, size=9, muted=True) _field(painter, QRect(x + 128, y, column_w - 140, 22), value, palette) # --- preview preview = QRect(inner.x() + profile_w + gap, inner.y(), preview_w, top_h) q_inner = _panel(painter, preview, palette) _box_title(painter, preview, s["seq_preview"], palette) view = QRect(q_inner.x(), q_inner.y() + 4, q_inner.width(), q_inner.height() - 42) painter.fillRect(view, palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(view.adjusted(0, 0, -1, -1)) # a stand-in surface with grid dots and the visiting path cx, cy = view.center().x(), view.center().y() + 6 rx, ry = view.width() // 4, view.height() // 3 for row in range(6): for column in range(6): u = (column - 2.5) / 2.5 v = (row - 2.5) / 2.5 if u * u + v * v > 1.25: continue px = int(cx + u * rx) py = int(cy + v * ry) painter.setPen(QPen(palette.accent, 1)) painter.setBrush(palette.accent) painter.drawEllipse(QPoint(px, py), 3, 3) painter.setPen(QPen(palette.warn, 1, Qt.PenStyle.DashLine)) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawEllipse(QPoint(cx, cy), rx, ry) _text(painter, QRect(view.x() + 10, view.y() + 8, view.width(), 20), s["seq_preview_placeholder"], palette, size=10, bold=True) _text(painter, QRect(view.x() + 10, view.y() + 28, view.width(), 20), s["seq_preview_note"], palette, size=9, muted=True) _text(painter, QRect(q_inner.x(), view.bottom() + 4, q_inner.width(), 34), s["seq_preview_hint"], palette, size=9, muted=True) # --- validation validation = QRect(inner.x() + profile_w + preview_w + 2 * gap, inner.y(), validation_w, top_h) v_inner = _panel(painter, validation, palette) _box_title(painter, validation, s["seq_validation"], palette) y = v_inner.y() + 6 for label in s["seq_checks"]: _text(painter, QRect(v_inner.x(), y, v_inner.width(), 22), "○ " + label + ": —", palette, size=10, muted=True) y += 24 _text(painter, QRect(v_inner.x(), y + 6, v_inner.width(), 40), s["seq_checks_pending"], palette, size=9, muted=True) y = validation.bottom() - len(s["seq_buttons"]) * 31 - 6 for label, _disabled in s["seq_buttons"]: _button(painter, QRect(v_inner.x(), y, v_inner.width(), 26), label, palette, disabled=True) y += 31 # --- point list, full width points = QRect(inner.x(), inner.y() + top_h + gap, inner.width(), inner.height() - top_h - gap) t_inner = _panel(painter, points, palette) _box_title(painter, points, s["seq_points"], palette) widths = (50, 150, 150, 150, 130, 190, 100) widths = widths + (t_inner.width() - sum(widths),) header_h = 28 header = QRect(t_inner.x(), t_inner.y() + 4, t_inner.width(), header_h) painter.fillRect(header, palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(header.adjusted(0, 0, -1, -1)) x = t_inner.x() for name, width in zip(s["point_cols"], widths): _text(painter, QRect(x, header.y(), width, header_h), name, palette, size=9, muted=True, align=Qt.AlignmentFlag.AlignCenter) x += width row_h = 32 for index, row in enumerate(s["point_rows"]): row_y = header.bottom() + 1 + index * row_h if index % 2 == 1: painter.fillRect(QRect(t_inner.x(), row_y, t_inner.width(), row_h - 1), palette.window) x = t_inner.x() for value, width in zip(row, widths): _text(painter, QRect(x, row_y, width, row_h), value, palette, size=10, align=Qt.AlignmentFlag.AlignCenter) x += width y = header.bottom() + 1 + len(s["point_rows"]) * row_h + 10 _text(painter, QRect(t_inner.x(), y, t_inner.width(), 34), s["points_hint"], palette, size=9, muted=True) def draw_sensor_tab(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None: inner = rect.adjusted(10, 8, -10, -8) y = inner.y() _text(painter, QRect(inner.x(), y, 60, 22), "设备" if s["axis_label"] == "轴号" else "Device", palette, size=10, muted=True) _field(painter, QRect(inner.x() + 62, y, 180, 22), "finger ▾", palette) _button(painter, QRect(inner.x() + 252, y, 70, 22), "预览" if s["axis_label"] == "轴号" else "Preview", palette) y += 30 _text(painter, QRect(inner.x(), y, 400, 20), s["sensor_note"], palette, size=9, muted=True) y += 26 image = QRect(inner.x(), y, int(inner.width() * 0.34), inner.height() - (y - rect.y()) - 10) painter.fillRect(image, palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(image.adjusted(0, 0, -1, -1)) for i in range(1, 4): painter.drawLine(image.x() + i * image.width() // 4, image.y() + 6, image.x() + i * image.width() // 4, image.bottom() - 6) curve = QRect(image.right() + 12, y - 56, inner.right() - image.right() - 12, image.height() + 56) painter.fillRect(curve, palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(curve.adjusted(0, 0, -1, -1)) _text(painter, QRect(curve.x() + 10, curve.y() + 8, curve.width(), 20), s["sensor_curve"], palette, size=10, bold=True) plot = curve.adjusted(14, 40, -14, -28) painter.setPen(QPen(palette.border, 1)) painter.drawLine(plot.bottomLeft(), plot.bottomRight()) painter.drawLine(plot.bottomLeft(), plot.topLeft()) for index, colour in enumerate((palette.accent, palette.warn, palette.danger)): painter.setPen(QPen(colour, 2)) points = [] for step in range(80): t = step / 79 value = (1 - t) ** 2 * (0.2 + 0.22 * index) + 0.16 * (1 + index) * (1 - abs(0.5 - t) * 2) points.append((plot.x() + int(t * plot.width()), plot.bottom() - int(min(1.0, value) * plot.height()))) for first, second in zip(points, points[1:]): painter.drawLine(first[0], first[1], second[0], second[1]) def draw_surface_tab(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None: inner = rect.adjusted(10, 8, -10, -8) view = QRect(inner.x(), inner.y(), inner.width() - 220, inner.height() - 30) painter.fillRect(view, palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(view.adjusted(0, 0, -1, -1)) cx, cy = view.center().x(), view.center().y() + 10 rx, ry = view.width() // 3, view.height() // 3 steps = 26 grid = [] for row in range(steps + 1): v = row / steps line = [] for column in range(steps + 1): u = column / steps angle = 2 * math.pi * u line.append((cx + int(rx * math.cos(angle) * math.sin(math.pi * v)), cy - int(ry * math.cos(math.pi * v)))) grid.append(line) for row in range(steps): for column in range(steps): intensity = max(0.0, min(1.0, 0.5 + 0.45 * (column / steps - 0.5) + 0.35 * (row / steps - 0.5))) colour = QColor(int(40 + 200 * intensity), int(110 + 60 * (1 - abs(intensity - 0.5) * 2)), int(235 - 195 * intensity)) painter.setPen(QPen(colour, 1)) painter.setBrush(colour) painter.drawPolygon( QPolygon( [ QPoint(*grid[row][column]), QPoint(*grid[row + 1][column]), QPoint(*grid[row + 1][column + 1]), QPoint(*grid[row][column + 1]), ] ) ) _text(painter, QRect(view.x() + 12, view.y() + 8, view.width(), 22), s["surface_note"], palette, size=10, bold=True) legend = QRect(view.right() + 12, inner.y(), 200, view.height()) _text(painter, QRect(legend.x(), legend.y(), legend.width(), 20), s["legend"], palette, size=10, bold=True) # A legend, not a wall of colour: fixed height so it reads as a scale. bar_h = min(300, legend.height() - 120) bar = QRect(legend.x() + 12, legend.y() + 34, 30, bar_h) for step in range(bar.height()): t = step / max(1, bar.height() - 1) colour = QColor(int(40 + 200 * t), int(110 + 60 * (1 - abs(t - 0.5) * 2)), int(235 - 195 * t)) painter.setPen(QPen(colour, 1)) painter.drawLine(bar.x(), bar.y() + step, bar.x() + bar.width(), bar.y() + step) painter.setPen(QPen(palette.border, 1)) painter.drawRect(bar) for label, offset in (("0.21", 6), ("0.60", bar.height() // 2), ("0.95", bar.height() - 6)): _text(painter, QPoint(bar.right() + 10, bar.y() + offset), label, palette, size=9) _text( painter, QRect(legend.x(), bar.bottom() + 12, legend.width(), 48), s["surface_footer"], palette, size=9, muted=True, ) def draw_points_tab(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None: inner = rect.adjusted(10, 8, -10, -8) _text(painter, QRect(inner.x(), inner.y(), inner.width(), 20), s["points_page_note"], palette, size=10, muted=True) y = inner.y() + 28 table = QRect(inner.x(), y, inner.width(), inner.height() - 28 - 46) painter.fillRect(table, palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawRect(table.adjusted(0, 0, -1, -1)) widths = (50, 140, 140, 140, 150, 190, 110, 150) header_h = 26 painter.fillRect(QRect(table.x(), table.y(), sum(widths), header_h), palette.panel) x = table.x() headers = s["point_cols"] for name, width in zip(headers, widths): _text(painter, QRect(x, table.y(), width, header_h), name, palette, size=9, muted=True, align=Qt.AlignmentFlag.AlignCenter) x += width rows = [ ["1", "0.00", "0.00", "0.00", "2.0", "0.94", "40", "ok"], ["2", "2.00", "0.00", "0.00", "2.0", "0.88", "40", "ok"], ["3", "4.00", "0.00", "0.00", "2.0", "0.81", "40", "偏软" if s["axis_label"] == "轴号" else "soft"], ] for index, row in enumerate(rows): row_y = table.y() + header_h + index * 32 if index % 2 == 0: painter.fillRect(QRect(table.x() + 1, row_y, sum(widths) - 2, 31), palette.panel) x = table.x() for value, width in zip(row, widths): _text(painter, QRect(x, row_y, width, 32), value, palette, size=10, align=Qt.AlignmentFlag.AlignCenter) x += width y = table.bottom() + 10 _button(painter, QRect(inner.x(), y, 130, 28), "采集当前点" if s["axis_label"] == "轴号" else "Capture point", palette, primary=True) _button(painter, QRect(inner.x() + 140, y, 100, 28), "重测" if s["axis_label"] == "轴号" else "Re-measure", palette) _button(painter, QRect(inner.x() + 250, y, 100, 28), "删除" if s["axis_label"] == "轴号" else "Remove", palette) TAB_PAINTERS = { "axes": draw_axis_tab, "sequence": draw_sequence_tab, "sensor": draw_sensor_tab, "surface": draw_surface_tab, "points": draw_points_tab, } #: Tab index -> painter key, for the tab strip labels. TAB_KEYS = ("axes", "sequence", "sensor", "surface", "points") def draw_page( painter: QPainter, palette: Palette, s: dict, rect: QRect, *, active: str = "axes", with_chrome: bool = True, ) -> None: """The whole page: chrome, strip, run bar, rail, workspace, status bar.""" painter.fillRect(rect, palette.window) body_top = rect.y() + MENU_H + BRAND_H + STRIP_H + RUNBAR_H body_h = rect.height() - (MENU_H + BRAND_H + STRIP_H + RUNBAR_H) - STATUS_H draw_left_rail( painter, palette, s, QRect(rect.x() + MARGIN, body_top + MARGIN, RAIL_LEFT, body_h - 2 * MARGIN) ) workspace = QRect( rect.x() + 2 * MARGIN + RAIL_LEFT, body_top + MARGIN, rect.width() - RAIL_LEFT - 3 * MARGIN, body_h - 2 * MARGIN, ) painter.fillRect(workspace, palette.panel) painter.setPen(QPen(palette.border, 1)) painter.drawRect(workspace.adjusted(0, 0, -1, -1)) content = draw_tabs(painter, palette, s, workspace, s["tabs"][TAB_KEYS.index(active)]) TAB_PAINTERS[active](painter, palette, s, content) if with_chrome: draw_chrome(painter, palette, s) draw_status_strip(painter, palette, s, rect.y() + MENU_H + BRAND_H) draw_run_bar(painter, palette, s, rect.y() + MENU_H + BRAND_H + STRIP_H) draw_status_bar(painter, palette, s, rect.bottom() - STATUS_H) def draw_run_bar(painter: QPainter, palette: Palette, s: dict, top: int) -> None: """The run strip: phase, progress and the three controls.""" painter.fillRect(QRect(0, top, PAGE_W, RUNBAR_H), palette.window) painter.setPen(QPen(palette.border, 1)) painter.drawLine(0, top, PAGE_W, top) painter.drawLine(0, top + RUNBAR_H - 1, PAGE_W, top + RUNBAR_H - 1) x = 16 _text(painter, QPoint(x, top + 25), s["run_title"], palette, size=11, bold=True) x += painter.fontMetrics().horizontalAdvance(s["run_title"]) + 20 _text(painter, QRect(x, top, 90, RUNBAR_H), s["run_phase"], palette, size=10, muted=True) x += 96 _text(painter, QRect(x, top, 150, RUNBAR_H), s["run_step"], palette, size=10) x += 158 bar = QRect(x, top + 14, 300, 12) painter.fillRect(bar, palette.border) painter.fillRect(QRect(bar.x(), bar.y(), 3 * bar.width() // 4, bar.height()), palette.accent) _text(painter, QRect(bar.right() + 12, top, 260, RUNBAR_H), s["run_time"], palette, size=10, muted=True) _button(painter, QRect(PAGE_W - 330, top + 7, 96, 26), s["run_run"], palette, disabled=True) _button(painter, QRect(PAGE_W - 228, top + 7, 96, 26), s["run_pause"], palette, disabled=True) _button(painter, QRect(PAGE_W - 126, top + 7, 110, 26), s["run_abort"], palette, danger=True, disabled=True) def draw_previews(painter: QPainter, palette: Palette, s: dict, origin: QPoint, keys: tuple[str, ...]) -> None: _text(painter, QRect(origin.x(), origin.y(), 900, 24), s["previews_caption"], palette, size=12, bold=True) row_y = origin.y() + 34 for index, key in enumerate(keys): box = QRect(origin.x() + index * (PREVIEW_W + 16), row_y, PREVIEW_W, PREVIEW_H) painter.fillRect(box, palette.panel) painter.setPen(QPen(palette.border, 1)) painter.drawRect(box.adjusted(0, 0, -1, -1)) header = QRect(box.x(), box.y(), box.width(), 26) painter.fillRect(header, palette.bar) painter.setPen(QPen(palette.border, 1)) painter.drawLine(header.bottomLeft(), header.bottomRight()) _text( painter, QRect(header.x() + 10, header.y(), header.width() - 20, 26), s["tabs"][TAB_KEYS.index(key)], palette, size=10, bold=True, ) content = QRect(box.x() + 6, box.y() + 30, box.width() - 12, box.height() - 36) painter.save() painter.setClipRect(content) scale = content.width() / WORKSPACE_W painter.translate(content.x(), content.y()) painter.scale(scale, scale) virtual = QRect(0, 0, int(content.width() / scale), int(content.height() / scale)) TAB_PAINTERS[key](painter, palette, s, virtual) painter.restore() def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--theme", default="light", choices=("dark", "light")) parser.add_argument("--language", default="zh_CN", choices=("zh_CN", "en_US")) parser.add_argument("--out", default="layout.png") parser.add_argument("--width", type=int, default=PAGE_W) parser.add_argument("--height", type=int, default=PAGE_H) parser.add_argument( "--page", default="axes", choices=("axes", "sequence", "sensor", "surface", "points", "all"), help="which page to draw; 'all' writes one full-size image per page", ) args = parser.parse_args() app = QApplication.instance() or QApplication([]) palette = Palette(args.theme) s = STRINGS[args.language] base = Path(args.out).resolve() base.parent.mkdir(parents=True, exist_ok=True) pages = TAB_KEYS if args.page == "all" else (args.page,) failures = 0 for key in pages: target = base if args.page != "all" else base.with_name(f"{base.stem}_{key}{base.suffix or '.png'}") pixmap = QPixmap(args.width, args.height) pixmap.fill(palette.window) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) try: draw_page(painter, palette, s, QRect(0, 0, args.width, args.height), active=key) finally: painter.end() if pixmap.save(str(target)): print( f"wrote {target} ({args.width}x{args.height}, theme={args.theme}, " f"lang={args.language}, page={key})" ) else: print(f"could not write {target}", file=sys.stderr) failures += 1 del app return 1 if failures else 0 if __name__ == "__main__": raise SystemExit(main())