feat(visual): implement square wafer (X) template, inward label alignment, and density scaling
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user