feat(visual): implement square wafer (X) template, inward label alignment, and density scaling

This commit is contained in:
jack
2026-06-15 11:20:55 -07:00
parent cea4fb782e
commit 7e584e08e8
+124 -19
View File
@@ -39,6 +39,8 @@ class WaferMapItem(QQuickPaintedItem):
blendChanged = Signal() blendChanged = Signal()
showLabelsChanged = Signal() showLabelsChanged = Signal()
colorsChanged = Signal() colorsChanged = Signal()
shapeChanged = Signal()
sizeChanged = Signal()
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@@ -49,6 +51,8 @@ class WaferMapItem(QQuickPaintedItem):
self._margin: float = 1.0 self._margin: float = 1.0
self._blend: float = 0.0 self._blend: float = 0.0
self._show_labels: bool = True self._show_labels: bool = True
self._shape: str = "round"
self._size: float = 300.0
# Dark-theme color defaults (match Theme.qml tokens) # Dark-theme color defaults (match Theme.qml tokens)
self._ring_color = QColor("#2A3441") # waferRingColor (toneBorder) self._ring_color = QColor("#2A3441") # waferRingColor (toneBorder)
@@ -74,8 +78,17 @@ class WaferMapItem(QQuickPaintedItem):
@sensors.setter @sensors.setter
def sensors(self, val: list) -> None: def sensors(self, val: list) -> None:
self._sensors = [Sensor(label=d["label"], x=float(d["x"]), y=float(d["y"])) self._sensors = [
for d in (val or [])] Sensor(
label=d["label"],
x=float(d["x"]),
y=float(d["y"]),
side=d.get("side", "right"),
offset_x=float(d.get("offset_x", 0.0)),
offset_y=float(d.get("offset_y", 0.0))
)
for d in (val or [])
]
self._rebuild() self._rebuild()
self.sensorsChanged.emit() self.sensorsChanged.emit()
@@ -144,6 +157,26 @@ class WaferMapItem(QQuickPaintedItem):
self.showLabelsChanged.emit() self.showLabelsChanged.emit()
self.update() self.update()
@Property(str, notify=shapeChanged)
def shape(self) -> str:
return self._shape
@shape.setter
def shape(self, val: str) -> None:
self._shape = str(val).lower()
self._rebuild()
self.shapeChanged.emit()
@Property(float, notify=sizeChanged)
def size(self) -> float:
return self._size
@size.setter
def size(self, val: float) -> None:
self._size = float(val)
self._rebuild()
self.sizeChanged.emit()
# Colour properties — QML can bind these to Theme tokens # Colour properties — QML can bind these to Theme tokens
@Property(QColor, notify=colorsChanged) @Property(QColor, notify=colorsChanged)
def ringColor(self) -> QColor: return self._ring_color def ringColor(self) -> QColor: return self._ring_color
@@ -262,7 +295,7 @@ class WaferMapItem(QQuickPaintedItem):
xs, ys, vs, xs, ys, vs,
width=ds, height=ds, width=ds, height=ds,
extent=(-r_mm, r_mm, -r_mm, r_mm), extent=(-r_mm, r_mm, -r_mm, r_mm),
round_clip=True, round_clip=(self._shape == "round"),
) )
except Exception: except Exception:
self._heatmap = None self._heatmap = None
@@ -328,7 +361,27 @@ class WaferMapItem(QQuickPaintedItem):
r_mm = self._wafer_radius_mm() r_mm = self._wafer_radius_mm()
sc = self._scale(ds, r_mm) sc = self._scale(ds, r_mm)
# Concentric rings at actual sensor group radii (falls back to 25/50/75/100% when no sensors). if self._shape == "square":
# Draw square boundary (thick pen)
border_pen = QPen(self._ring_color, 2, Qt.PenStyle.SolidLine)
painter.setPen(border_pen)
half_size_px = int(self._size / 2 * sc)
painter.drawRect(cx - half_size_px, cy - half_size_px, 2 * half_size_px, 2 * half_size_px)
# Crosshair axes
axis_pen = QPen(self._axis_color, 1, Qt.PenStyle.DashLine)
painter.setPen(axis_pen)
painter.drawLine(cx, cy - half_size_px, cx, cy + half_size_px)
painter.drawLine(cx - half_size_px, cy, cx + half_size_px, cy)
# Draw concentric square guide lines at the distinct radii of sensor rings
grid_pen = QPen(self._ring_color, 1, Qt.PenStyle.SolidLine)
painter.setPen(grid_pen)
for ring_r_mm in self._sensor_ring_radii_mm()[:-1]: # exclude the outermost border
rr = max(1, int(ring_r_mm * sc))
painter.drawRect(cx - rr, cy - rr, 2 * rr, 2 * rr)
else:
# Concentric rings
ring_pen = QPen(self._ring_color, 1, Qt.PenStyle.SolidLine) ring_pen = QPen(self._ring_color, 1, Qt.PenStyle.SolidLine)
painter.setPen(ring_pen) painter.setPen(ring_pen)
for ring_r_mm in self._sensor_ring_radii_mm(): for ring_r_mm in self._sensor_ring_radii_mm():
@@ -356,18 +409,20 @@ class WaferMapItem(QQuickPaintedItem):
def _paint_markers(self, painter: QPainter) -> None: def _paint_markers(self, painter: QPainter) -> None:
r = self._marker_r r = self._marker_r
# Scale font size based on the number of sensors to prevent overlap on dense wafers
num_sensors = len(self._sensors)
font_scale = 1.0
if num_sensors > 60:
font_scale = 0.7 # reduce font size by 30% for dense wafers
elif num_sensors > 40:
font_scale = 0.85
id_font = QFont() id_font = QFont()
id_font.setPointSize(max(5, r)) id_font.setPointSize(max(4, int(r * font_scale)))
id_font.setBold(True) id_font.setBold(True)
temp_font = QFont() temp_font = QFont()
temp_font.setPointSize(max(4, r - 1)) temp_font.setPointSize(max(3, int((r - 1) * font_scale)))
# Pre-compute ID font metrics for vertical centering
painter.setFont(id_font)
id_fm = painter.fontMetrics()
id_line_h = id_fm.height()
id_ascent = id_fm.ascent()
band_color = { band_color = {
"in_range": self._in_range_color, "in_range": self._in_range_color,
@@ -390,17 +445,67 @@ class WaferMapItem(QQuickPaintedItem):
if self._show_labels: if self._show_labels:
has_temp = i < len(self._values) has_temp = i < len(self._values)
lx = px + r + 3
# Two-line block: split the gap at dot center; single-line: original position
y1 = (py - id_line_h // 2) if has_temp else (py + id_ascent // 2)
# Sensor ID — bold, muted text color # Fetch text alignment side and offsets
side = getattr(s, "side", "right").lower()
ox = getattr(s, "offset_x", 0.0) * r
oy = getattr(s, "offset_y", 0.0) * r
# Pre-compute metrics using current scaled fonts
painter.setFont(id_font)
id_fm = painter.fontMetrics()
id_line_h = id_fm.height()
id_ascent = id_fm.ascent()
painter.setFont(temp_font)
temp_fm = painter.fontMetrics()
temp_line_h = temp_fm.height()
temp_ascent = temp_fm.ascent()
id_text = s.label
temp_text = f"{self._values[i]:.2f}" if has_temp else ""
id_w = id_fm.horizontalAdvance(id_text)
temp_w = temp_fm.horizontalAdvance(temp_text) if has_temp else 0
text_w = max(id_w, temp_w)
# Height of the 1 or 2-line text block
text_h = (id_line_h + temp_line_h) if has_temp else id_line_h
# Calculate box top-left (lx, ly) relative to dot center (px, py)
gap = 3
if side == "left":
lx = px - r - gap - text_w + ox
ly = py - text_h // 2 + oy
elif side == "top":
lx = px - text_w // 2 + ox
ly = py - r - gap - text_h + oy
elif side == "bottom":
lx = px - text_w // 2 + ox
ly = py + r + gap + oy
else: # "right" or default
lx = px + r + gap + ox
ly = py - text_h // 2 + oy
# Draw Sensor ID (first line)
painter.setFont(id_font) painter.setFont(id_font)
painter.setPen(QPen(self._text_color)) painter.setPen(QPen(self._text_color))
painter.drawText(lx, y1, s.label) y1 = ly + id_ascent
if side in ("top", "bottom"):
painter.drawText(lx + (text_w - id_w) // 2, y1, id_text)
elif side == "left":
painter.drawText(lx + (text_w - id_w), y1, id_text)
else:
painter.drawText(lx, y1, id_text)
# Temperature — band color, smaller font, below ID # Draw Temperature (second line)
if has_temp: if has_temp:
painter.setFont(temp_font) painter.setFont(temp_font)
painter.setPen(QPen(color)) painter.setPen(QPen(color))
painter.drawText(lx, y1 + id_line_h, f"{self._values[i]:.2f}") y2 = ly + id_line_h + temp_ascent
if side in ("top", "bottom"):
painter.drawText(lx + (text_w - temp_w) // 2, y2, temp_text)
elif side == "left":
painter.drawText(lx + (text_w - temp_w), y2, temp_text)
else:
painter.drawText(lx, y2, temp_text)