1474 lines
69 KiB
Python
1474 lines
69 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 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": [
|
||
["连接方式", "以太网 ▾"],
|
||
["IP", "192.168.0.11"],
|
||
["状态", "已连接"],
|
||
["BAS 文件", "…\\bas\\ECAT.bas"],
|
||
["初始化状态", "初始化成功"],
|
||
["总线类型", "EtherCAT"],
|
||
["节点数量", "5"],
|
||
["轴数量", "5"],
|
||
],
|
||
"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": "播放动作",
|
||
"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"],
|
||
"setup_rows": [
|
||
["Connection", "Ethernet ▾"],
|
||
["IP", "192.168.0.11"],
|
||
["State", "Connected"],
|
||
["BAS file", "…\\bas\\ECAT.bas"],
|
||
["Init state", "Initialised"],
|
||
["Bus type", "EtherCAT"],
|
||
["Nodes", "5"],
|
||
["Axes", "5"],
|
||
],
|
||
"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",
|
||
"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)
|
||
_button(painter, QRect(inner.x(), y + 76, 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 = 130
|
||
for index, label in enumerate(s["setup_tabs"]):
|
||
tab = QRect(tabs.x() + index * tab_w, tabs.y(), tab_w, 26)
|
||
active = index == 0
|
||
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 rows are both tabs' contents stacked, in order: the connection fields and
|
||
# the bus readouts, which is what the two tabs hold between them.
|
||
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, 96, 22), label, palette, size=10, muted=True)
|
||
_text(painter, QRect(body.x() + 104, y, body.width() - 104, 22), value, palette, size=10)
|
||
y += 24
|
||
_button(painter, QRect(body.x(), y + 6, 110, 26), s["setup_connect"], palette, primary=True)
|
||
_button(painter, QRect(body.x() + 118, y + 6, 118, 26), s["setup_disconnect"], palette)
|
||
_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)
|
||
|
||
play = QRect(inner_preview.x(), view.bottom() + checks_h + 2, 110, play_h - 4)
|
||
_button(painter, play, s["finger_play"], palette, primary=True)
|
||
notes_y = play.bottom() + 4
|
||
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())
|