1011 lines
45 KiB
Python
1011 lines
45 KiB
Python
#!/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())
|