Add alignment state indicator and pluggable data sources
- AlignmentThresholds dataclass + VisionEngine.__init__ for injection - alignment_state computed per snapshot (ready/aligning/fault) - MonitorPlot circle/vector/legend driven by state color - Status dot + label moved into POSITION VISUALIZATION panel header - CsvSessionSource + DemoSource pluggable DataSource layer - Synthetic DATA/ready/ and DATA/aligning/ sessions for visual testing - Fix CAM 3 label placement (below circle, not above)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Reusable Qt widgets for the wafer monitor demo."""
|
||||
"""Reusable Qt widgets"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,6 +10,30 @@ from .qt import require_qt
|
||||
|
||||
QtCore, QtGui, QtWidgets = require_qt()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TODO P3.1: Add _STATE_COLORS and _DEFAULT_COLOR module-level constants here
|
||||
#
|
||||
# THINKING:
|
||||
# Module-level avoids reconstructing the dict on every paintEvent repaint.
|
||||
# Single source of truth for colors used in two places: drawing calls (P3.2)
|
||||
# and the legend swatch (P3.3). _DEFAULT_COLOR retains the original orange
|
||||
# so any snapshot predating P1.2 renders with pre-existing orange rather than
|
||||
# silently going red. Cross-reference: used by P3.2 and P3.3.
|
||||
#
|
||||
# _STATE_COLORS: dict[str, str] = {
|
||||
# "ready": "#22c55e",
|
||||
# "aligning": "#f59e0b",
|
||||
# "fault": "#ef4444",
|
||||
# }
|
||||
# _DEFAULT_COLOR = "#f97316" # defensive fallback; unexpected alignment_state only
|
||||
# ---------------------------------------------------------------------------
|
||||
_STATE_COLORS: dict[str, str] = {
|
||||
"ready": "#22c55e",
|
||||
"aligning": "#f59e0b",
|
||||
"fault": "#ef4444",
|
||||
}
|
||||
_DEFAULT_COLOR = "#f97316"
|
||||
|
||||
|
||||
class MonitorPlot(QtWidgets.QWidget):
|
||||
"""Operator monitor plot driven by a UI snapshot."""
|
||||
@@ -35,10 +59,17 @@ class MonitorPlot(QtWidgets.QWidget):
|
||||
center = QtCore.QPointF(plot_rect.center())
|
||||
radius = min(plot_rect.width(), plot_rect.height()) * 0.34
|
||||
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor("#93c5fd"), 2, QtCore.Qt.PenStyle.DashLine))
|
||||
painter.setPen(
|
||||
QtGui.QPen(QtGui.QColor("#93c5fd"), 2, QtCore.Qt.PenStyle.DashLine)
|
||||
)
|
||||
painter.drawEllipse(center, radius, radius)
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor("#64748b"), 1))
|
||||
for label, angle in [("CAM 1", -90), ("CAM 2", 0), ("CAM 3", 90), ("CAM 4", 180)]:
|
||||
for label, angle in [
|
||||
("CAM 1", -90),
|
||||
("CAM 2", 0),
|
||||
("CAM 3", 90),
|
||||
("CAM 4", 180),
|
||||
]:
|
||||
point = _polar(center, radius * 1.18, angle)
|
||||
painter.drawText(
|
||||
_camera_label_rect(point, angle),
|
||||
@@ -55,18 +86,33 @@ class MonitorPlot(QtWidgets.QWidget):
|
||||
center.x() + float(detected_center_mm[0]) * scale,
|
||||
center.y() + float(detected_center_mm[1]) * scale,
|
||||
)
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor("#f97316"), 2))
|
||||
painter.drawEllipse(detected_center, detected_radius * scale, detected_radius * scale)
|
||||
color = QtGui.QColor(
|
||||
_STATE_COLORS.get(self._snapshot.alignment_state, _DEFAULT_COLOR)
|
||||
)
|
||||
painter.setPen(QtGui.QPen(color, 2))
|
||||
painter.drawEllipse(
|
||||
detected_center, detected_radius * scale, detected_radius * scale
|
||||
)
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor("#2563eb"), 7))
|
||||
painter.drawPoint(center)
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor("#f97316"), 7))
|
||||
painter.setPen(QtGui.QPen(color, 7))
|
||||
painter.drawLine(center, detected_center)
|
||||
painter.drawPoint(detected_center)
|
||||
else:
|
||||
painter.setPen(QtGui.QColor("#64748b"))
|
||||
painter.drawText(plot_rect, QtCore.Qt.AlignmentFlag.AlignCenter, "No wafer read")
|
||||
painter.drawText(
|
||||
plot_rect, QtCore.Qt.AlignmentFlag.AlignCenter, "No wafer read"
|
||||
)
|
||||
|
||||
_draw_plot_legend(painter, rect, rect.bottom() - legend_height // 2)
|
||||
_draw_plot_legend(
|
||||
painter,
|
||||
rect,
|
||||
rect.bottom() - legend_height // 2,
|
||||
_STATE_COLORS.get(
|
||||
self._snapshot.alignment_state if self._snapshot else "",
|
||||
_DEFAULT_COLOR,
|
||||
),
|
||||
)
|
||||
painter.end()
|
||||
|
||||
|
||||
@@ -83,8 +129,8 @@ class OffsetCard(QtWidgets.QFrame):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("MetricCard")
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 17, 20, 17)
|
||||
layout.setSpacing(8)
|
||||
layout.setContentsMargins(16, 10, 16, 10)
|
||||
layout.setSpacing(4)
|
||||
label_widget = QtWidgets.QLabel(label)
|
||||
label_widget.setObjectName("Metric")
|
||||
row = QtWidgets.QHBoxLayout()
|
||||
@@ -158,22 +204,27 @@ def _polar(center: QtCore.QPointF, radius: float, angle_deg: float) -> QtCore.QP
|
||||
|
||||
|
||||
def _draw_plot_legend(
|
||||
painter: QtGui.QPainter, rect: QtCore.QRect, legend_y: int
|
||||
painter: QtGui.QPainter,
|
||||
rect: QtCore.QRect,
|
||||
legend_y: int,
|
||||
detected_color: str = _DEFAULT_COLOR,
|
||||
) -> None:
|
||||
"""Draw Reference / Detected legend centered with measured spacing."""
|
||||
line_width = 36
|
||||
text_gap = 8
|
||||
entry_gap = 28
|
||||
entries = [
|
||||
(QtGui.QPen(QtGui.QColor("#93c5fd"), 2, QtCore.Qt.PenStyle.DashLine), "Reference"),
|
||||
(QtGui.QPen(QtGui.QColor("#f97316"), 2), "Detected"),
|
||||
(
|
||||
QtGui.QPen(QtGui.QColor("#93c5fd"), 2, QtCore.Qt.PenStyle.DashLine),
|
||||
"Reference",
|
||||
),
|
||||
(QtGui.QPen(QtGui.QColor(detected_color), 2), "Detected"),
|
||||
]
|
||||
|
||||
metrics = QtGui.QFontMetrics(painter.font())
|
||||
text_y = legend_y + 5
|
||||
entry_widths = [
|
||||
line_width + text_gap + metrics.horizontalAdvance(label)
|
||||
for _, label in entries
|
||||
line_width + text_gap + metrics.horizontalAdvance(label) for _, label in entries
|
||||
]
|
||||
total_width = sum(entry_widths) + entry_gap * (len(entries) - 1)
|
||||
x = rect.center().x() - total_width / 2
|
||||
@@ -190,9 +241,11 @@ def _draw_plot_legend(
|
||||
def _camera_label_rect(point: QtCore.QPointF, angle_deg: float) -> QtCore.QRectF:
|
||||
width, height = 56.0, 18.0
|
||||
if angle_deg == -90:
|
||||
return QtCore.QRectF(point.x() - width / 2, point.y() - height - 6, width, height)
|
||||
return QtCore.QRectF(
|
||||
point.x() - width / 2, point.y() - height - 6, width, height
|
||||
)
|
||||
if angle_deg == 90:
|
||||
return QtCore.QRectF(point.x() - width / 2, point.y() - height - 4, width, height)
|
||||
return QtCore.QRectF(point.x() - width / 2, point.y() + 6, width, height)
|
||||
if angle_deg == 0:
|
||||
return QtCore.QRectF(point.x() + 8, point.y() - height / 2, width, height)
|
||||
return QtCore.QRectF(point.x() - width - 8, point.y() - height / 2, width, height)
|
||||
|
||||
Reference in New Issue
Block a user