Files
orisys-app/apps/autocal/tools/layout_mockup.py
T
2026-09-27 19:20:43 +08:00

1495 lines
70 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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, QPointF, QRect, Qt # noqa: E402
from PySide6.QtGui import ( # noqa: E402
QColor,
QFont,
QPainter,
QPainterPath,
QPen,
QPixmap,
QPolygon,
)
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
#: Matches ``MAIN_SPLITTER_SIZES`` in the window: the rail narrowed when the two
#: start-up panels moved into the device-setup dialog, and the mockup has to show the
#: same proportions or it stops being a preview of anything.
RAIL_LEFT = 300
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_idle_hint": "尚未生成标定点",
"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) ▶",
"setup_open": "设备设置",
"setup_title": "设备设置",
"setup_hint": "连接与总线初始化每次开机只需确认一次,因此收进这里;顶栏状态条持续显示结果,设置窗口可以一直开着。",
"setup_tabs": ["连接", "总线初始化", "轴映射"],
"setup_rows": [
["X(工作台)", "驱动器 0"],
["Y(工作台)", "驱动器 1"],
["Z(压轴)", "驱动器 4"],
["yaw(回转)", "驱动器 3"],
["pitch(俯仰)", "驱动器 2"],
],
"setup_connect": "连接",
"setup_disconnect": "断开连接",
"setup_close": "关闭",
"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_name": "序列名称",
"seq_note": "备注",
"seq_summary": "6 点(6 启用) · X 0.000–10.000 (10.000) · Y 0.000 · Z 12.400–13.400 (1.000) · 预计 2:24(估计值)",
"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), ("导出序列…", False), ("导入序列…", False)],
"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": "坐标由网格生成并逐点校验,不接受手工输入:手工输入的坐标没有任何检查,一个错数字会毁掉整条序列。",
"finger_bundle_label": "导出包体",
"finger_bundle_value": "D:\\orisys_ws\\orisys-sensor-design\\build\\export\\finger\\r01",
"finger_load": "载入包体…",
"finger_initial_view": "初始视角",
"finger_reset": "复位视角",
"finger_textured_note": "UV 贴图:patch_texture_compute.png",
"finger_bundle_summary": "1032 顶点 · 1974 三角面 · 15.00 × 7.50 × 15.81 mm · 0.018388 mm/单位",
"finger_flat_title": "展平图像(点击取点)",
"finger_pick_hint": "点击传感面上的像素即可取点;落在 UV 缝隙上的像素会被拒绝。",
"finger_readout_title": "读数",
"finger_readout_rows": [
["传感器 (mm)", "2.841, 3.512, 0.940", None],
["法向", "-0.18, -0.88, 0.43", None],
["平台 (mm)", "2.841, 0.940, -3.512", None],
["位姿 X / Y / yaw / pitch", "-2.841 0.940 -78.42 23.10", None],
["工作台移动 X / Y", "+2.841 -0.940 mm", None],
["对轴残差", "0.03° 可对轴", "#12b76a"],
["坐标系映射", "x→x, y→z, z→-y", None],
],
"finger_tool_label": "刀具 X / Y",
"finger_tool_values": ["0.000", "0.000"],
"finger_sensor_xy_label": "传感器 X / Y",
"finger_sensor_xy_values": ["0.000", "0.000"],
"finger_sensor_zy_label": "传感器 Z / yaw",
"finger_sensor_zy_values": ["0.000", "0.000"],
"finger_preview_title": "三维预览",
"finger_preview_checks": ["显示运动轴", "显示转台原点平面", "显示刀具路径"],
"finger_play": "播放动作",
"move_execute": "执行动作",
"move_stop": "停止",
"move_return": "回初始位",
"move_confirm": "移动前确认",
"move_status": "1/4 yaw → +36.8160 °(回初始位:先退压轴 Z)",
"finger_preview_notes": [
"颜色含义:绿→红 = 表面对齐残差;红/绿/蓝三色轴 = 工作台 X/Y/Z(位于原点);橙点 = 工作台原点。",
"黄色直线 = 刀具轴(+Z,位置即刀具 X/Y);黄色方框 = 传感器安装基面(高度 = 传感器 Z,含安装板)。",
"青色线 = 规划路径;紫圈 = 转台当前姿态、浅紫圈 = 结束姿态,与固定方格网对比即看出整个变换;白点+蓝线 = 接触点及法向。",
],
"finger_point_cols": ["sensor x", "sensor y", "sensor z", "nx", "ny", "nz", "X", "Y", "Z", "yaw", "pitch", "残差"],
"finger_point_rows": [
["2.841", "3.512", "0.940", "-0.18", "-0.88", "0.43", "2.841", "0.940", "7.958", "-78.42", "23.10", "0.03"],
["5.113", "2.204", "-1.870", "0.21", "0.55", "-0.81", "5.113", "-1.870", "7.958", "12.05", "8.44", "0.02"],
["3.902", "6.118", "2.405", "0.55", "0.19", "0.81", "3.902", "2.405", "7.958", "-90.00", "51.66", "0.02"],
],
"finger_table_buttons": [("删除所选", False), ("上移", False), ("下移", False), ("清空标定点", False)],
"finger_bad_word": "撞 yaw 行程限位",
"finger_point_count": "3 个取点 · 展平 1024 × 1024 · 有效像素 72.9%",
"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_idle_hint": "No points generated",
"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) ▶",
"setup_open": "Device setup",
"setup_title": "Device setup",
"setup_hint": "Connecting and bringing the bus up are checked once per machine start-up, so they live here. The status strip keeps reporting the result.",
"setup_tabs": ["Connection", "Bus initialisation", "Axis mapping"],
"setup_rows": [
["X (table)", "Drive 0"],
["Y (table)", "Drive 1"],
["Z (press)", "Drive 4"],
["yaw (rotation)", "Drive 3"],
["pitch (tilt)", "Drive 2"],
],
"setup_connect": "Connect",
"setup_disconnect": "Disconnect",
"setup_close": "Close",
"tabs": ["Axis debug", "Test sequence", "Finger debug", "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_name": "Sequence name",
"seq_note": "Note",
"seq_summary": "6 points (6 enabled) · X 0.000-10.000 (10.000) · Y 0.000 · Z 12.400-13.400 (1.000) · about 2:24 (estimate)",
"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), ("Export sequence…", False), ("Import sequence…", False)],
"point_cols": ["#", "X (mm)", "Y (mm)", "Z (mm)", "Status", "Sensitivity (last)", "Samples"],
"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.",
"finger_bundle_label": "Export bundle",
"finger_bundle_value": "D:\\orisys_ws\\orisys-sensor-design\\build\\export\\finger\\r01",
"finger_load": "Load bundle…",
"finger_initial_view": "Initial view",
"finger_reset": "Reset view",
"finger_textured_note": "UV texture: patch_texture_compute.png",
"finger_bundle_summary": "1032 vertices · 1974 triangles · 15.00 × 7.50 × 15.81 mm · 0.018388 mm per unit",
"finger_flat_title": "Flat image (click to pick)",
"finger_pick_hint": "Click a pixel on the sensing surface to pick a point; pixels in a UV gap are refused.",
"finger_readout_title": "Readouts",
"finger_readout_rows": [
["Sensor (mm)", "2.841, 3.512, 0.940", None],
["Normal", "-0.18, -0.88, 0.43", None],
["Platform (mm)", "2.841, 0.940, -3.512", None],
["Pose X / Y / yaw / pitch", "-2.841 0.940 -78.42 23.10", None],
["Table move X / Y", "+2.841 -0.940 mm", None],
["Residual", "0.03° can align", "#12b76a"],
["Frame map", "x→x, y→z, z→-y", None],
],
"finger_tool_label": "Tool X / Y",
"finger_tool_values": ["0.000", "0.000"],
"finger_sensor_xy_label": "Sensor X / Y",
"finger_sensor_xy_values": ["0.000", "0.000"],
"finger_sensor_zy_label": "Sensor Z / yaw",
"finger_sensor_zy_values": ["0.000", "0.000"],
"finger_preview_title": "3D preview",
"finger_preview_checks": ["Show motion axes", "Show table origin plane", "Show tool path"],
"finger_play": "Play motion",
"move_execute": "Execute",
"move_stop": "Stop",
"move_return": "Return to zero",
"move_confirm": "Confirm before moving",
"move_status": "1/4 yaw → +36.8160° (return to zero retracts Z first)",
"finger_preview_notes": [
"What the colours mean: green→red = the surface's alignment residual; the red/green/blue triad is the table's X/Y/Z at the origin; the orange dot is the origin.",
"Yellow line = the tool axis (+Z, at the tool's own X/Y); the amber outline = the sensor base plane, at the sensor's Z offset (the plate).",
"Cyan line = the planned path; the violet ring is the table's pose now and the faint one where it finishes, against the fixed grid that is where it started; white dot with blue line = the contact and its normal.",
],
"finger_preview_legend": "Surface colour = alignment residual (green aligned, red not) · yellow is the Z axis",
"finger_point_cols": ["sensor x", "sensor y", "sensor z", "nx", "ny", "nz", "X", "Y", "Z", "yaw", "pitch", "residual"],
"finger_point_rows": [
["2.841", "3.512", "0.940", "-0.18", "-0.88", "0.43", "2.841", "0.940", "7.958", "-78.42", "23.10", "0.03"],
["5.113", "2.204", "-1.870", "0.21", "0.55", "-0.81", "5.113", "-1.870", "7.958", "12.05", "8.44", "0.02"],
["3.902", "6.118", "2.405", "0.55", "0.19", "0.81", "3.902", "2.405", "7.958", "-90.00", "51.66", "0.02"],
],
"finger_table_buttons": [("Remove selected", False), ("Move up", False), ("Move down", False), ("Clear points", False)],
"finger_bad_word": "hits yaw travel limit",
"finger_point_count": "3 picked points · flat 1024 × 1024 · 72.9% valid pixels",
"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 _arrowhead(painter: QPainter, path: QPainterPath, share: float, colour: QColor, size: int) -> None:
"""An arrowhead on ``path`` at ``share``, pointing the way the path runs.
The direction comes from two nearby points on the path rather than from
``angleAtPercent``: that angle is measured counter-clockwise from +x while Qt's
y axis grows downward, and every attempt to reconcile the two by hand has
produced an arrow pointing backwards.
"""
tip = path.pointAtPercent(share)
ahead = path.pointAtPercent(min(1.0, share + 0.01))
dx, dy = ahead.x() - tip.x(), ahead.y() - tip.y()
length = math.hypot(dx, dy) or 1.0
ux, uy = dx / length, dy / length
painter.setPen(QPen(colour, 2))
for sign in (1.0, -1.0):
barb = QPointF(
tip.x() - ux * size + sign * -uy * size * 0.6,
tip.y() - uy * size + sign * ux * size * 0.6,
)
painter.drawLine(tip, barb)
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, 300, STRIP_H),
"上次动作: 已使能轴0 / last action: enable axis 0",
palette,
size=10,
muted=True,
align=Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
)
# The device-setup button, before the run-screen buttons: connection and bus
# initialisation live behind it now instead of in the rail.
_button(painter, QRect(PAGE_W - 316, top + 10, 100, 24), s["setup_open"], palette)
_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:
"""The rail as it is now: only what is read while working.
Connection and bus initialisation are in the device-setup dialog, so the rail
starts with the axis readout instead of burying it under ~460 px of start-up
controls.
"""
readout = QRect(rect.x(), rect.y(), 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)
# Returning every drive to zero is a machine-level action: here, in the rail that
# is on screen in every tab, rather than repeated inside a tab.
_button(painter, QRect(inner.x(), y + 76, inner.width(), 28), s["move_return"], palette)
_button(painter, QRect(inner.x(), y + 112, inner.width(), 28), s["clear_alarms"], palette)
group = QRect(rect.x(), rect.y() + 280, 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)
def draw_setup_dialog(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None:
"""The device-setup dialog: connection on one tab, bus initialisation on the other.
Drawn as its own panel so the proposal can be reviewed in the same image as the
page it came out of.
"""
dialog = QRect(rect.x(), rect.y(), rect.width(), 400)
inner = _panel(painter, dialog, palette)
_box_title(painter, dialog, s["setup_title"], palette)
_text(
painter,
QRect(inner.x(), inner.y(), inner.width(), 34),
s["setup_hint"],
palette,
size=9,
muted=True,
)
tabs = QRect(inner.x(), inner.y() + 38, inner.width(), inner.height() - 74)
tab_w = 120
for index, label in enumerate(s["setup_tabs"]):
tab = QRect(tabs.x() + index * tab_w, tabs.y(), tab_w, 26)
# The axis mapping is the newest page, and the one worth reviewing.
active = label == s["setup_tabs"][-1]
painter.fillRect(tab, palette.window if active else palette.panel)
painter.setPen(QPen(palette.border, 1))
painter.drawRect(tab.adjusted(0, 0, -1, -1))
if active:
painter.setPen(QPen(palette.accent, 2))
painter.drawLine(tab.bottomLeft(), tab.bottomRight())
_text(painter, tab, label, palette, size=10, bold=active, align=Qt.AlignmentFlag.AlignCenter)
# The mapping page: one row per platform direction, each showing the drive that
# moves it. The values are the bench's own wiring.
body = _panel(painter, tabs.adjusted(0, 26, 0, 0), palette)
y = body.y() + 6
for label, value in s["setup_rows"]:
_text(painter, QRect(body.x(), y, 150, 22), label, palette, size=10, muted=True)
_text(painter, QRect(body.x() + 156, y, body.width() - 156, 22), value, palette, size=10)
y += 26
_button(painter, QRect(dialog.right() - 110, dialog.bottom() - 34, 96, 26), s["setup_close"], 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)
# the name and note the document carries
y = p_inner.y() + 4
_text(painter, QRect(p_inner.x(), y, 70, 22), s["seq_name"], palette, size=9, muted=True)
_field(painter, QRect(p_inner.x() + 72, y, p_inner.width() - 78, 22), "5 点灵敏度扫描", palette)
y += 26
_text(painter, QRect(p_inner.x(), y, 70, 22), s["seq_note"], palette, size=9, muted=True)
_field(painter, QRect(p_inner.x() + 72, y, p_inner.width() - 78, 22), "", palette)
y += 28
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
row_y = y + row * 26
_text(painter, QRect(x, row_y, 130, 22), label, palette, size=9, muted=True)
_field(painter, QRect(x + 128, row_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)
# Seven columns: no empty actions column, and the summary line carries the
# count and span the operator reasons with.
widths = (60, 150, 150, 150, 140, 190, 110)
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 + 8
_text(painter, QRect(t_inner.x(), y, t_inner.width(), 22), s["seq_summary"], palette, size=10)
y += 24
_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)
def draw_finger_tab(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None:
"""The finger debugger: pick on the flat image, read the pose, check the 3D preview."""
inner = rect.adjusted(10, 8, -10, -8)
zh = s["axis_label"] == "轴号"
y = inner.y()
_text(painter, QRect(inner.x(), y, 90, 24), s["finger_bundle_label"], palette, size=10, muted=True)
_field(painter, QRect(inner.x() + 92, y, 300, 24), s["finger_bundle_value"], palette)
_button(painter, QRect(inner.x() + 400, y, 100, 24), s["finger_load"], palette)
_button(painter, QRect(inner.x() + 508, y, 100, 24), s["finger_initial_view"], palette)
_button(painter, QRect(inner.x() + 616, y, 100, 24), s["finger_reset"], palette)
y += 34
_text(painter, QRect(inner.x(), y, inner.width(), 18), s["finger_bundle_summary"], palette, size=9, muted=True)
y += 24
body_h = int((inner.height() - (y - inner.y())) * 0.62)
table_h = inner.bottom() - (y + body_h) - 8
gap = 10
usable = inner.width() - 2 * gap
picker_w = int(usable * 28 / 100)
readout_w = int(usable * 32 / 100)
preview_w = usable - picker_w - readout_w
picker = QRect(inner.x(), y, picker_w, body_h)
readouts = QRect(picker.right() + gap, y, readout_w, body_h)
preview = QRect(readouts.right() + gap, y, preview_w, body_h)
# Flat unrolled image.
inner_picker = _panel(painter, picker, palette)
_text(painter, QRect(inner_picker.x(), inner_picker.y(), inner_picker.width(), 18), s["finger_flat_title"], palette, size=10, bold=True)
flat = QRect(inner_picker.x(), inner_picker.y() + 22, inner_picker.width(), inner_picker.height() - 22 - 40)
# The textured UV space: black outside the island, artwork inside it.
painter.fillRect(flat, QColor("#000000"))
island = flat.adjusted(int(flat.width() * 0.06), int(flat.height() * 0.16), -int(flat.width() * 0.06), -int(flat.height() * 0.16))
painter.setPen(QPen(QColor("#000000"), 0))
columns, rows = 22, 14
for row in range(rows):
for column in range(columns):
cell = QRect(
island.x() + column * island.width() // columns,
island.y() + row * island.height() // rows,
island.width() // columns + 1,
island.height() // rows + 1,
)
shade = ((row * 7 + column * 13) % 5) / 4.0
painter.fillRect(
cell,
QColor(int(60 + 150 * shade), int(40 + 120 * ((row + column) % 3) / 2), int(90 + 120 * (1 - shade))),
)
for index, (fx, fy) in enumerate(((0.35, 0.4), (0.62, 0.55), (0.5, 0.72))):
mark = QPoint(int(flat.x() + fx * flat.width()), int(flat.y() + fy * flat.height()))
painter.setPen(QPen(QColor("#ffffff"), 2))
painter.setBrush(QColor("#d92d20") if index == 2 else QColor("#12b76a"))
painter.drawEllipse(mark, 4, 4)
# Leave no brush behind: a later panel fills its background, and a leaked
# brush turns that fill into the marker colour.
painter.setBrush(Qt.BrushStyle.NoBrush)
_text(painter, QRect(inner_picker.x(), flat.bottom() + 2, inner_picker.width(), 18), s["finger_textured_note"], palette, size=9, muted=True)
_text(painter, QRect(inner_picker.x(), flat.bottom() + 20, inner_picker.width(), 18), s["finger_pick_hint"], palette, size=9, muted=True)
# Readouts.
inner_read = _panel(painter, readouts, palette)
_text(painter, QRect(inner_read.x(), inner_read.y(), inner_read.width(), 18), s["finger_readout_title"], palette, size=10, bold=True)
row_y = inner_read.y() + 24
label_w = int(inner_read.width() * 0.42)
for label, value, colour in s["finger_readout_rows"]:
_text(painter, QRect(inner_read.x(), row_y, label_w, 20), label, palette, size=9, muted=True)
_text(painter, QRect(inner_read.x() + label_w, row_y, inner_read.width() - label_w, 20), value, palette, size=9, colour=QColor(colour) if colour else None)
row_y += 20
# The setup and tool fields: pairs of numbers, as in the app, where they are
# spin boxes two to a row because they get dialled in against the real machine.
pairs = (
("finger_tool_label", "finger_tool_values"),
("finger_sensor_xy_label", "finger_sensor_xy_values"),
("finger_sensor_zy_label", "finger_sensor_zy_values"),
)
pair_y = row_y + 4
for label_key, values_key in pairs:
_text(painter, QRect(inner_read.x(), pair_y, label_w, 20), s[label_key], palette, size=9, muted=True)
field_w = int((inner_read.width() - label_w - 6) / 2)
for index, value in enumerate(s[values_key][:2]):
_field(
painter,
QRect(inner_read.x() + label_w + index * (field_w + 6), pair_y, field_w, 20),
value,
palette,
)
pair_y += 22
# 3D preview: the machine's frame at the table origin, the sensor on it, and the
# planned path. Schematic, but the parts and their colours are the ones the real
# view draws — a proposal that disagrees with the app is worse than no proposal.
inner_preview = _panel(painter, preview, palette)
_text(painter, QRect(inner_preview.x(), inner_preview.y(), inner_preview.width(), 18), s["finger_preview_title"], palette, size=10, bold=True)
checks_h = 20
play_h = 28
notes_h = 3 * 14 + 4
view = QRect(
inner_preview.x(),
inner_preview.y() + 22,
inner_preview.width(),
inner_preview.height() - 22 - checks_h - play_h - notes_h,
)
painter.fillRect(view, QColor("#12161c"))
cx, cy = view.center().x(), view.center().y() + int(view.height() * 0.12)
# The table origin's plane: a grid, not a filled surface.
half = int(view.width() * 0.30)
floor = QRect(cx - half, cy - int(half * 0.26), 2 * half, int(half * 0.52))
painter.setPen(QPen(QColor("#8c95ab"), 1))
for index in range(5):
fx = floor.x() + index * floor.width() // 4
fy = floor.y() + index * floor.height() // 4
painter.drawLine(fx, floor.y(), fx, floor.bottom())
painter.drawLine(floor.x(), fy, floor.right(), fy)
# The sensor: the sensing face as an arc, the shell below it as a wireframe-ish
# outline. Grey, because the surface carries the residual colour instead.
dome_w = int(view.width() * 0.34)
dome_h = int(view.height() * 0.34)
body = QRect(cx - dome_w // 2, cy - dome_h, dome_w, dome_h + int(dome_h * 0.45))
painter.setPen(QPen(QColor("#5b6273"), 1))
painter.setBrush(QColor("#26b847"))
painter.drawChord(QRect(body.x(), body.y(), body.width(), body.height() * 2 // 3), 0, 180 * 16)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawRect(QRect(body.x(), body.y() + body.height() // 4, body.width(), body.height() // 2))
# The base plane, lifted off the origin by the sensor's z setup offset (the
# mounting plate), and the table's own frame: a circle at the pose it ends in,
# against the square grid that is where it started.
base = QRect(body.x() - 8, body.bottom() - 6, body.width() + 16, int(half * 0.30))
painter.setPen(QPen(QColor("#fab829"), 2))
painter.drawRect(base)
ring = QRect(0, 0, int(half * 1.5), int(half * 0.78))
ring.moveCenter(QPoint(cx + int(half * 0.30), cy + int(half * 0.05)))
painter.setPen(QPen(QColor("#8c6ff7"), 2))
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawEllipse(ring)
painter.drawLine(ring.center().x() - ring.width() // 2, ring.center().y(),
ring.center().x() + ring.width() // 2, ring.center().y())
painter.drawLine(ring.center().x(), ring.center().y() - ring.height() // 2,
ring.center().x(), ring.center().y() + ring.height() // 2)
painter.setPen(QPen(QColor(140, 111, 247, 130), 2))
painter.drawEllipse(ring.adjusted(-14, -7, -14, -7))
# The planned path: the stages swing the contact, then the table slides it in.
contact = QPoint(body.x() + 10, body.y() + int(body.height() * 0.28))
landed = QPoint(cx + 6, cy - int(half * 0.30))
path = QPainterPath(contact)
path.quadTo(QPoint(contact.x() + int(half * 0.75), contact.y() - int(half * 0.35)), landed)
painter.setPen(QPen(QColor("#40d1f2"), 2))
painter.drawPath(path)
for share in (0.35, 0.7):
_arrowhead(painter, path, share, QColor("#ccf4ff"), 7)
painter.setBrush(QColor("#40d1f2"))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(landed, 5, 5)
# The table triad at the origin: the only full-strength one in the picture.
origin = QPoint(floor.center().x(), floor.center().y() + int(half * 0.12))
for colour, end in (
("#e64c4c", QPoint(origin.x(), origin.y() + int(half * 0.62))),
("#59c761", QPoint(origin.x() + int(half * 0.85), origin.y())),
("#5787f2", QPoint(origin.x(), origin.y() - int(half * 0.42))),
):
painter.setPen(QPen(QColor(colour), 2))
painter.drawLine(origin, end)
painter.setBrush(QColor("#fa9933"))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(origin, 5, 5)
painter.setBrush(Qt.BrushStyle.NoBrush)
# The tool axis, through the point the tool touches.
painter.setPen(QPen(QColor("#f2e04c"), 2))
painter.drawLine(QPoint(landed.x(), view.y() + 16), QPoint(landed.x(), landed.y()))
_text(painter, QRect(landed.x() - 8, view.y() + 2, 90, 16), "+Z", palette, size=8, colour=QColor("#f2e04c"))
# The picked contact and its normal, and the point selected in the table.
painter.setPen(QPen(QColor("#59a6ff"), 2))
painter.drawLine(contact, QPoint(contact.x() - int(half * 0.30), contact.y() - int(half * 0.34)))
painter.setPen(QPen(QColor("#ffffff"), 2))
painter.setBrush(QColor("#ffffff"))
painter.drawEllipse(contact, 4, 4)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QColor("#eb2133"))
painter.drawEllipse(QPoint(contact.x() + 14, contact.y() + 4), 5, 5)
painter.setBrush(Qt.BrushStyle.NoBrush)
checks = s["finger_preview_checks"]
check_w = inner_preview.width() // max(1, len(checks))
for index, label in enumerate(checks):
box = QRect(inner_preview.x() + index * check_w, view.bottom() + 2, check_w, checks_h)
painter.setPen(QPen(palette.accent, 2))
painter.drawLine(box.x() + 2, box.y() + 9, box.x() + 5, box.y() + 13)
painter.drawLine(box.x() + 5, box.y() + 13, box.x() + 10, box.y() + 5)
_text(painter, QRect(box.x() + 15, box.y(), check_w - 15, checks_h), label, palette, size=9, muted=True)
row_y = view.bottom() + checks_h + 2
play = QRect(inner_preview.x(), row_y, 110, play_h - 4)
_button(painter, play, s["finger_play"], palette, primary=True)
# The pose move: execute the selected point's solved pose, stop, and the policy
# that decides whether either asks first.
execute = QRect(play.right() + 8, row_y, 100, play_h - 4)
_button(painter, execute, s["move_execute"], palette)
stop = QRect(execute.right() + 8, row_y, 84, play_h - 4)
_button(painter, stop, s["move_stop"], palette, danger=True)
confirm = QRect(stop.right() + 10, row_y, inner_preview.width() - stop.right() - 10, play_h - 4)
_text(painter, confirm, "☑ " + s["move_confirm"], palette, size=9, muted=True)
_text(
painter,
QRect(inner_preview.x(), row_y + play_h, inner_preview.width(), 16),
s["move_status"],
palette,
size=9,
)
notes_y = row_y + play_h + 18
for text in s["finger_preview_notes"]:
_text(painter, QRect(inner_preview.x(), notes_y, inner_preview.width(), 14), text, palette, size=8, muted=True)
notes_y += 14
# Picked points.
table = QRect(inner.x(), y + body_h + 8, inner.width(), max(60, table_h))
painter.fillRect(table, palette.window)
painter.setPen(QPen(palette.border, 1))
painter.drawRect(table.adjusted(0, 0, -1, -1))
header_h = 24
painter.fillRect(QRect(table.x(), table.y(), table.width(), header_h), palette.panel)
# Leading select column, then the data columns: the real table has a checkbox
# per row so points can be picked out, edited, reordered or removed.
select_w = 34
painter.setPen(QPen(palette.border, 1))
painter.drawLine(table.x() + select_w, table.y(), table.x() + select_w, table.bottom())
columns = s["finger_point_cols"]
widths = [int((table.width() - select_w) * share)
for share in (0.09, 0.1, 0.09, 0.09, 0.08, 0.08, 0.08, 0.11,
0.07, 0.07, 0.07, 0.07)]
x = table.x() + select_w
for name, width in zip(columns, widths):
_text(painter, QRect(x, table.y(), width, header_h), name, palette, size=9, muted=True, align=Qt.AlignmentFlag.AlignCenter)
x += width
for index, row in enumerate(s["finger_point_rows"]):
row_y = table.y() + header_h + index * 26
if index % 2 == 0:
painter.fillRect(QRect(table.x() + 1, row_y, table.width() - 2, 25), palette.panel)
# Tick the first row, as the selected point being tested.
box = QRect(table.x() + select_w // 2 - 6, row_y + 8, 12, 12)
painter.setPen(QPen(palette.muted, 1))
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawRect(box)
if index == 0:
painter.setPen(QPen(palette.accent, 2))
painter.drawLine(box.x() + 2, box.y() + 6, box.x() + 5, box.y() + 10)
painter.drawLine(box.x() + 5, box.y() + 10, box.x() + 10, box.y() + 3)
x = table.x() + select_w
for value, width in zip(row, widths):
colour = palette.danger if value == s["finger_bad_word"] else None
_text(painter, QRect(x, row_y, width, 26), value, palette, size=10, align=Qt.AlignmentFlag.AlignCenter, colour=colour)
x += width
_text(painter, QRect(table.x() + 8, table.bottom() + 2, table.width(), 18), s["finger_point_count"], palette, size=9, muted=True)
buttons = s["finger_table_buttons"]
bx = table.x()
by = table.bottom() + 26
for label, primary in buttons:
width = 30 + 8 * len(label)
_button(painter, QRect(bx, by, width, 26), label, palette, primary=primary)
bx += width + 8
TAB_PAINTERS = {
"axes": draw_axis_tab,
"sequence": draw_sequence_tab,
"finger": draw_finger_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", "finger", "sensor", "surface", "points")
#: Pages that are not workspace tabs. The device setup is a dialog, so it is drawn as
#: a page of its own: it has to be reviewable like everything else.
EXTRA_PAGES = ("setup",)
def draw_setup_page(painter: QPainter, palette: Palette, s: dict, rect: QRect) -> None:
"""The device-setup dialog over the page it was opened from."""
painter.fillRect(rect, palette.window)
draw_page(painter, palette, s, rect, active="axes", with_chrome=True, with_rail=True)
draw_setup_dialog(
painter,
palette,
s,
QRect(rect.center().x() - 300, rect.center().y() - 200, 600, 400),
)
def draw_page(
painter: QPainter,
palette: Palette,
s: dict,
rect: QRect,
*,
active: str = "axes",
with_chrome: bool = True,
with_rail: bool = True,
) -> None:
"""The whole page: chrome, strip, run bar, rail, workspace, status bar.
``with_rail`` only exists for the setup page, which draws this page as the
background behind the dialog and does not want to draw the rail twice.
"""
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
if with_rail:
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_full_page(painter: QPainter, palette: Palette, s: dict, rect: QRect, key: str) -> None:
"""One page of the proposal, by key — a workspace tab or the setup dialog."""
if key in EXTRA_PAGES:
draw_setup_page(painter, palette, s, rect)
return
draw_page(painter, palette, s, rect, active=key)
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, 420, RUNBAR_H), s["run_idle_hint"], palette, size=9, 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=TAB_KEYS[0],
# Derived, not listed: a literal here silently rejects any tab added to
# TAB_KEYS, which is how the finger page first went missing.
choices=(*TAB_KEYS, *EXTRA_PAGES, "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, *EXTRA_PAGES) 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_full_page(painter, palette, s, QRect(0, 0, args.width, args.height), 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())