1225 lines
58 KiB
Python
1225 lines
58 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_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) ▶",
|
||
"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_axes_note": "黄色为工具轴 · X/Y 为工作台,Z 为工具行程 · 绿=对齐,红=不可对齐",
|
||
"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 (mm)", "2.841, 0.940", None],
|
||
["yaw / pitch", "-78.42° / 23.10°", None],
|
||
["对轴残差", "0.03° 可对轴", "#12b76a"],
|
||
["坐标系映射", "x→x, y→z, z→-y", None],
|
||
],
|
||
"finger_plate_label": "安装板厚 (mm)",
|
||
"finger_plate_value": "0.000",
|
||
"finger_preview_title": "三维预览",
|
||
"finger_preview_legend": "曲面颜色 = 对轴残差(绿为对正,红为对不上) · 黄色为 Z 轴",
|
||
"finger_point_cols": ["#", "像素 u, v", "sensor x", "sensor y", "sensor z", "nx", "ny", "nz", "X / Y", "Z", "yaw", "pitch"],
|
||
"finger_point_rows": [
|
||
["1", "0.35, 0.40", "2.841", "3.512", "0.940", "-0.18", "-0.88", "0.43", "2.841 / 0.940", "7.958", "-78.42", "23.10"],
|
||
["2", "0.62, 0.55", "5.113", "2.204", "-1.870", "0.21", "0.55", "-0.81", "5.113 / -1.870", "7.958", "12.05", "8.44"],
|
||
["3", "0.50, 0.72", "3.902", "6.118", "2.405", "0.55", "0.19", "0.81", "3.902 / 2.405", "7.958", "-90.00", "51.66"],
|
||
],
|
||
"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) ▶",
|
||
"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_axes_note": "Yellow is the tool axis · X/Y are the table, Z the tool travel · green aligned, red not",
|
||
"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],
|
||
["X / Y (mm)", "2.841, 0.940", None],
|
||
["yaw / pitch", "-78.42° / 23.10°", None],
|
||
["Residual", "0.03° can align", "#12b76a"],
|
||
["Frame map", "x→x, y→z, z→-y", None],
|
||
],
|
||
"finger_plate_label": "Mounting plate (mm)",
|
||
"finger_plate_value": "0.000",
|
||
"finger_preview_title": "3D preview",
|
||
"finger_preview_legend": "Surface colour = alignment residual (green aligned, red not) · yellow is the Z axis",
|
||
"finger_point_cols": ["#", "pixel u, v", "sensor x", "sensor y", "sensor z", "nx", "ny", "nz", "X / Y", "Z", "yaw", "pitch"],
|
||
"finger_point_rows": [
|
||
["1", "0.35, 0.40", "2.841", "3.512", "0.940", "-0.18", "-0.88", "0.43", "2.841 / 0.940", "7.958", "-78.42", "23.10"],
|
||
["2", "0.62, 0.55", "5.113", "2.204", "-1.870", "0.21", "0.55", "-0.81", "5.113 / -1.870", "7.958", "12.05", "8.44"],
|
||
["3", "0.50, 0.72", "3.902", "6.118", "2.405", "0.55", "0.19", "0.81", "3.902 / 2.405", "7.958", "-90.00", "51.66"],
|
||
],
|
||
"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 _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)
|
||
# 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
|
||
_text(painter, QRect(inner_read.x(), inner_read.bottom() - 36, inner_read.width(), 18), s["finger_plate_label"], palette, size=9, muted=True)
|
||
_field(painter, QRect(inner_read.x(), inner_read.bottom() - 20, inner_read.width(), 20), s["finger_plate_value"], palette)
|
||
|
||
# 3D preview: the tool axis and a normal with its residual.
|
||
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)
|
||
view = QRect(inner_preview.x(), inner_preview.y() + 22, inner_preview.width(), inner_preview.height() - 22 - 34)
|
||
painter.fillRect(view, QColor("#12161c"))
|
||
painter.setPen(QPen(QColor("#2a313c"), 1))
|
||
for i in range(1, 4):
|
||
painter.drawLine(view.x() + i * view.width() // 4, view.y(), view.x() + i * view.width() // 4, view.bottom())
|
||
cx, cy = view.center().x(), view.center().y() + 40
|
||
axis = QPoint(cx, view.y() + 24)
|
||
painter.setPen(QPen(QColor("#f2c94c"), 3))
|
||
painter.drawLine(axis, QPoint(cx, cy))
|
||
painter.setBrush(QColor("#f2c94c"))
|
||
painter.drawEllipse(QPoint(cx, cy), 3, 3)
|
||
# The motion axes: the platform frame, drawn in the sensor frame.
|
||
for colour, end in (
|
||
("#e65454", QPoint(cx - 96, cy + 18)),
|
||
("#61cc61", QPoint(cx + 30, cy + 92)),
|
||
("#5c8cf2", QPoint(cx + 88, cy - 26)),
|
||
):
|
||
painter.setPen(QPen(QColor(colour), 2))
|
||
painter.drawLine(QPoint(cx, cy), end)
|
||
painter.setPen(QPen(QColor("#12b76a"), 3))
|
||
painter.drawLine(QPoint(cx, cy), QPoint(cx + 26, cy - 96))
|
||
painter.setPen(QPen(QColor("#d92d20"), 3))
|
||
painter.drawLine(QPoint(cx, cy), QPoint(cx - 74, cy - 78))
|
||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||
painter.setPen(QPen(QColor("#4a5568"), 1))
|
||
painter.drawLine(QPoint(cx - 90, cy), QPoint(cx + 90, cy))
|
||
painter.setPen(QPen(QColor("#e65454"), 1))
|
||
painter.drawText(QPoint(cx - 108, cy + 24), "X")
|
||
painter.setPen(QPen(QColor("#61cc61"), 1))
|
||
painter.drawText(QPoint(cx + 34, cy + 100), "Y")
|
||
painter.setPen(QPen(QColor("#5c8cf2"), 1))
|
||
painter.drawText(QPoint(cx + 92, cy - 28), "Z")
|
||
_text(painter, QRect(inner_preview.x(), view.bottom() + 2, inner_preview.width(), 30), s["finger_axes_note"], palette, size=8, muted=True)
|
||
|
||
# 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)
|
||
columns = s["finger_point_cols"]
|
||
widths = [int(table.width() * share) for share in (0.06, 0.09, 0.1, 0.09, 0.09, 0.07, 0.07, 0.07, 0.09, 0.09, 0.09, 0.09)]
|
||
x = table.x()
|
||
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)
|
||
x = table.x()
|
||
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)
|
||
|
||
|
||
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")
|
||
|
||
|
||
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, 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, "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())
|