new_gui: data-collection bookmarks (mark / go / collect-from)
Mark smargon positions during data collection (numbered markers on the camera, projected via smargon_to_picture and tracked through rotation). Per-bookmark: Go (move_smargon + set_omega) and Collect (rotation scan with start = bookmark, i.e. launch data collection from that point). Cleared on unmount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8a5bdf142a
commit
05421bf944
@@ -78,6 +78,10 @@ Backend contract is documented in the `new-gui-backend-paths` memory.
|
||||
- **Preconditions**: data-collection actions are gated by a ring/shutter/hutch
|
||||
warning (1-hour snooze); mounting is hard-blocked by the hutch PSS;
|
||||
`sample_missing` shows a modal (suppressed during automation).
|
||||
- **Bookmarks** (mounted): + Bookmark captures the current smargon position
|
||||
(shown as numbered markers on the camera, projected via `smargon_to_picture`);
|
||||
**Go** returns there (`move_smargon` + `set_omega`); **Collect** launches a
|
||||
rotation scan with `start` = the bookmark (data collection *from* that point).
|
||||
- **Mount / Unmount** → `mount(SampleShortInfo)` / `unmount()`.
|
||||
- **Motors** → `set_omega_rel`, `move_smargon`, `zoom`, `front_light`, `back_light`,
|
||||
and a staff-only **Energy** control → `change_energy` (keV).
|
||||
|
||||
@@ -201,6 +201,7 @@ class MainWindow(QWidget):
|
||||
self.manual.smart_params_requested.connect(self._on_smart_params)
|
||||
self.manual.xrf_scan_requested.connect(self._do_xrf)
|
||||
self.manual.raster_goto_requested.connect(self.daq.move_smargon)
|
||||
self.manual.bookmark_goto_requested.connect(self._on_bookmark_goto)
|
||||
self.manual.status_message.connect(self._note)
|
||||
if hasattr(self.daq, "run_number_incremented"):
|
||||
self.daq.run_number_incremented.connect(
|
||||
@@ -620,6 +621,12 @@ class MainWindow(QWidget):
|
||||
return
|
||||
self.daq.move_smargon(SmargonCoordinate(sh_mm=sh))
|
||||
|
||||
def _on_bookmark_goto(self, coord, omega_deg: float) -> None:
|
||||
"""Return to a bookmarked position (move smargon, then omega)."""
|
||||
self.daq.move_smargon(coord)
|
||||
if hasattr(self.daq, "set_omega"):
|
||||
self.daq.set_omega(float(omega_deg))
|
||||
|
||||
def _on_smargon_angles(self, chi_deg: float, phi_deg: float) -> None:
|
||||
"""Move to absolute chi/phi, preserving the current sample-holder position."""
|
||||
from aare.common.coordinate import SmargonCoordinate
|
||||
|
||||
@@ -37,6 +37,7 @@ class ManualView(QWidget):
|
||||
smart_params_requested = Signal(object) # SimpleScanParameters (automate mode)
|
||||
xrf_scan_requested = Signal(object) # FluorescenceSpectrumParameterModel
|
||||
raster_goto_requested = Signal(object) # SmargonCoordinate (goto a grid)
|
||||
bookmark_goto_requested = Signal(object, float) # SmargonCoordinate, omega_deg
|
||||
status_message = Signal(str) # transient note to surface in UI
|
||||
|
||||
def __init__(self, state: AppState, palette: Palette, defaults: dict, parent=None):
|
||||
@@ -48,6 +49,7 @@ class ManualView(QWidget):
|
||||
self._grid_rect_img: tuple[float, float, float, float] | None = None
|
||||
self._raster_grids: list = []
|
||||
self._grids_dialog = None
|
||||
self._bookmarks: list = [] # [{coord: SmargonCoordinate, omega: float}]
|
||||
|
||||
root = QHBoxLayout(self)
|
||||
root.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -159,6 +161,13 @@ class ManualView(QWidget):
|
||||
t.end.connect(s.end)
|
||||
t.unmount.connect(s.unmount)
|
||||
|
||||
# bookmarks
|
||||
bm = self.pipeline.bookmarks
|
||||
bm.bookmark_add.connect(self._on_bookmark_add)
|
||||
bm.bookmark_goto.connect(self._on_bookmark_goto)
|
||||
bm.bookmark_collect.connect(self._on_bookmark_collect)
|
||||
bm.bookmark_remove.connect(self._on_bookmark_remove)
|
||||
|
||||
# ------------------------------------------------------- run handlers
|
||||
def _on_run_center(self, method: str) -> None:
|
||||
# Only the automatic loop-centering method remains a button; click-to-
|
||||
@@ -297,6 +306,65 @@ class ManualView(QWidget):
|
||||
else:
|
||||
self.status_message.emit("Grid has no stored position to go to.")
|
||||
|
||||
# ----------------------------------------------------- bookmarks
|
||||
def _on_bookmark_add(self) -> None:
|
||||
from copy import deepcopy
|
||||
geom = getattr(self._last_status, "geom", None)
|
||||
smg = getattr(geom, "smargon", None) if geom else None
|
||||
if smg is None or getattr(smg, "sh_mm", None) is None:
|
||||
self.status_message.emit("No smargon position yet; cannot bookmark.")
|
||||
return
|
||||
omega = getattr(geom, "omega_deg", 0.0) or 0.0
|
||||
self._bookmarks.append({"coord": deepcopy(smg), "omega": float(omega)})
|
||||
self.pipeline.bookmarks.set_bookmarks(len(self._bookmarks))
|
||||
self._project_bookmarks()
|
||||
|
||||
def _on_bookmark_goto(self, index: int) -> None:
|
||||
if 0 <= index < len(self._bookmarks):
|
||||
bm = self._bookmarks[index]
|
||||
self.bookmark_goto_requested.emit(bm["coord"], bm["omega"])
|
||||
|
||||
def _on_bookmark_collect(self, index: int) -> None:
|
||||
if not (0 <= index < len(self._bookmarks)):
|
||||
return
|
||||
bm = self._bookmarks[index]
|
||||
cs = self.pipeline.settings
|
||||
req = requests.build_rotation_request(
|
||||
collect_params=cs.collect_params(),
|
||||
status=self._last_status,
|
||||
exp_time_s=float(self._defaults.get("rotation_exp_time_s",
|
||||
self._defaults.get("exp_time_s", 0.02))),
|
||||
start_omega_deg=bm["omega"],
|
||||
file_prefix=self.pipeline.filename.file_prefix("data"),
|
||||
start=bm["coord"],
|
||||
)
|
||||
self._state.run_now("collect")
|
||||
self.rotation_scan_requested.emit(req)
|
||||
|
||||
def _on_bookmark_remove(self, index: int) -> None:
|
||||
if 0 <= index < len(self._bookmarks):
|
||||
del self._bookmarks[index]
|
||||
self.pipeline.bookmarks.set_bookmarks(len(self._bookmarks))
|
||||
self._project_bookmarks()
|
||||
|
||||
def _project_bookmarks(self) -> None:
|
||||
from aare.gui.new_gui.widgets.bookmarks import BOOKMARK_COLORS
|
||||
geom = getattr(self._last_status, "geom", None)
|
||||
if geom is None or not hasattr(geom, "smargon_to_picture"):
|
||||
self.camera.set_bookmarks([])
|
||||
return
|
||||
items = []
|
||||
for i, bm in enumerate(self._bookmarks):
|
||||
sh = getattr(bm["coord"], "sh_mm", None)
|
||||
if sh is None:
|
||||
continue
|
||||
try:
|
||||
px = geom.smargon_to_picture(sh)
|
||||
except Exception:
|
||||
continue
|
||||
items.append((px.x, px.y, i + 1, BOOKMARK_COLORS[i % len(BOOKMARK_COLORS)]))
|
||||
self.camera.set_bookmarks(items)
|
||||
|
||||
# ------------------------------------------------------- refreshers
|
||||
def _refresh_mount(self) -> None:
|
||||
phase = self._state.mount_phase
|
||||
@@ -316,6 +384,9 @@ class ManualView(QWidget):
|
||||
if phase != "mounted":
|
||||
self.camera.clear_grid()
|
||||
self._grid_rect_img = None
|
||||
self._bookmarks = [] # bookmarks are per-sample
|
||||
self.pipeline.bookmarks.set_bookmarks(0)
|
||||
self.camera.set_bookmarks([])
|
||||
self.pipeline.update_from_state(self._state)
|
||||
|
||||
def _refresh_pipe(self) -> None:
|
||||
@@ -346,6 +417,8 @@ class ManualView(QWidget):
|
||||
self._last_status = s
|
||||
self.motors.update_daq_status(s)
|
||||
self.camera_controls.update_daq_status(s)
|
||||
if self._bookmarks:
|
||||
self._project_bookmarks()
|
||||
bl = getattr(s, "bl", None)
|
||||
if bl is not None and getattr(bl, "zoom", None) is not None:
|
||||
self.camera.set_zoom(bl.zoom)
|
||||
|
||||
@@ -117,7 +117,10 @@ def build_rotation_request(
|
||||
exp_time_s: float,
|
||||
start_omega_deg: float,
|
||||
file_prefix: str | None = None,
|
||||
start=None,
|
||||
) -> RotationScanRequest:
|
||||
"""``start`` (SmargonCoordinate | None) lets a bookmark launch collection from
|
||||
that position; None starts where the goniometer currently is."""
|
||||
return RotationScanRequest(
|
||||
exp_time_s=exp_time_s,
|
||||
start_omega_deg=float(start_omega_deg),
|
||||
@@ -125,7 +128,7 @@ def build_rotation_request(
|
||||
steps=int(collect_params["steps"]),
|
||||
dtz=collect_params.get("dtz"),
|
||||
transmission=collect_params.get("transmission"),
|
||||
start=None,
|
||||
start=start,
|
||||
end=None,
|
||||
screening=False,
|
||||
file_prefix=file_prefix,
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Data-collection bookmarks: mark smargon positions, return to them or collect.
|
||||
|
||||
A compact bar: "+ Bookmark" captures the current position; each saved bookmark
|
||||
is a chip with Go (move there), Collect (launch data collection from there) and
|
||||
remove. Markers are drawn on the camera by the Manual view.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QWidget
|
||||
|
||||
# Bookmark colours (match the old SmargonBookmark palette).
|
||||
BOOKMARK_COLORS = ("#cf222e", "#1a7f37", "#4d8bff", "#6d4bd1", "#9bcc3f")
|
||||
|
||||
|
||||
class BookmarksBar(QWidget):
|
||||
bookmark_add = Signal()
|
||||
bookmark_goto = Signal(int)
|
||||
bookmark_collect = Signal(int)
|
||||
bookmark_remove = Signal(int)
|
||||
|
||||
def __init__(self, palette, parent=None):
|
||||
super().__init__(parent)
|
||||
self._p = palette
|
||||
self._lay = QHBoxLayout(self)
|
||||
self._lay.setContentsMargins(0, 0, 0, 0)
|
||||
self._lay.setSpacing(8)
|
||||
|
||||
from aare.gui.new_gui.widgets.common import micro_label
|
||||
self._lay.addWidget(micro_label("BOOKMARKS"))
|
||||
add = QPushButton("+ Bookmark")
|
||||
add.setCursor(Qt.PointingHandCursor)
|
||||
add.setStyleSheet(
|
||||
f"QPushButton {{ background:{palette.surface};"
|
||||
f" border:1px solid {palette.border_control}; border-radius:8px;"
|
||||
f" padding:6px 12px; font-size:12px; color:{palette.text_secondary}; }}")
|
||||
add.clicked.connect(self.bookmark_add)
|
||||
self._lay.addWidget(add)
|
||||
|
||||
self._chips_from = self._lay.count() # where chips start
|
||||
self._lay.addStretch(1)
|
||||
|
||||
def set_bookmarks(self, count: int) -> None:
|
||||
# remove existing chips (keep label + add button + trailing stretch)
|
||||
while self._lay.count() > self._chips_from + 1:
|
||||
item = self._lay.takeAt(self._chips_from)
|
||||
w = item.widget()
|
||||
if w:
|
||||
w.setParent(None)
|
||||
w.deleteLater()
|
||||
for i in range(count):
|
||||
self._lay.insertWidget(self._lay.count() - 1, self._make_chip(i))
|
||||
|
||||
def _make_chip(self, i: int) -> QWidget:
|
||||
p = self._p
|
||||
color = BOOKMARK_COLORS[i % len(BOOKMARK_COLORS)]
|
||||
chip = QWidget()
|
||||
chip.setStyleSheet(
|
||||
f"background:{p.surface}; border:1px solid {p.border_control};"
|
||||
f" border-radius:9px;")
|
||||
cl = QHBoxLayout(chip)
|
||||
cl.setContentsMargins(8, 3, 6, 3)
|
||||
cl.setSpacing(6)
|
||||
dot = QLabel(str(i + 1))
|
||||
dot.setFixedSize(16, 16)
|
||||
dot.setAlignment(Qt.AlignCenter)
|
||||
dot.setStyleSheet(
|
||||
f"background:{color}; color:#fff; border-radius:8px; font-size:10px;"
|
||||
f" font-weight:700;")
|
||||
cl.addWidget(dot)
|
||||
go = self._mini("Go", p)
|
||||
go.clicked.connect(lambda _=False, ix=i: self.bookmark_goto.emit(ix))
|
||||
col = self._mini("Collect", p, accent=True)
|
||||
col.clicked.connect(lambda _=False, ix=i: self.bookmark_collect.emit(ix))
|
||||
rm = self._mini("✕", p, danger=True)
|
||||
rm.clicked.connect(lambda _=False, ix=i: self.bookmark_remove.emit(ix))
|
||||
cl.addWidget(go)
|
||||
cl.addWidget(col)
|
||||
cl.addWidget(rm)
|
||||
return chip
|
||||
|
||||
def _mini(self, text: str, p, accent: bool = False, danger: bool = False) -> QPushButton:
|
||||
b = QPushButton(text)
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
if accent:
|
||||
css = f"background:{p.accent}; color:#fff; border:none;"
|
||||
elif danger:
|
||||
css = f"background:transparent; color:{p.danger}; border:none;"
|
||||
else:
|
||||
css = (f"background:{p.surface}; border:1px solid {p.border_control};"
|
||||
f" color:{p.text_secondary};")
|
||||
b.setStyleSheet(
|
||||
f"QPushButton {{ {css} border-radius:6px; padding:3px 8px; font-size:11px; }}")
|
||||
return b
|
||||
@@ -61,6 +61,7 @@ class CameraViewport(QWidget):
|
||||
self._detections: list = [] # raw boxes from prediction
|
||||
self._det_shape = None # [h, w] of the inference image
|
||||
self._show_detections = True
|
||||
self._bookmarks: list = [] # [(x_px, y_px, label, color)]
|
||||
|
||||
# Raster grid-draw state
|
||||
self._grid_draw_enabled = False
|
||||
@@ -254,6 +255,11 @@ class CameraViewport(QWidget):
|
||||
self._beam_busy = bool(busy)
|
||||
self.update()
|
||||
|
||||
def set_bookmarks(self, items: list) -> None:
|
||||
"""items: list of (x_px, y_px, label, color_hex) in image coords."""
|
||||
self._bookmarks = items or []
|
||||
self.update()
|
||||
|
||||
def update_predictions(self, payload: dict) -> None:
|
||||
if not isinstance(payload, dict):
|
||||
return
|
||||
@@ -387,8 +393,26 @@ class CameraViewport(QWidget):
|
||||
if self._show_detections and self._mode == "mounted":
|
||||
self._paint_detections(painter)
|
||||
|
||||
self._paint_bookmarks(painter)
|
||||
self._paint_grid(painter)
|
||||
|
||||
def _paint_bookmarks(self, painter: QPainter) -> None:
|
||||
if not self._bookmarks or self._pixmap is None or self._pixmap.isNull():
|
||||
return
|
||||
f = QFont()
|
||||
f.setPixelSize(10)
|
||||
f.setBold(True)
|
||||
painter.setFont(f)
|
||||
for x_px, y_px, label, color in self._bookmarks:
|
||||
c = self._image_to_widget(QPoint(int(x_px), int(y_px)))
|
||||
col = QColor(color)
|
||||
painter.setBrush(col)
|
||||
painter.setPen(QPen(QColor("#ffffff"), 1))
|
||||
painter.drawEllipse(c, 7, 7)
|
||||
painter.setPen(QColor("#ffffff"))
|
||||
painter.drawText(QRect(c.x() - 7, c.y() - 6, 14, 13),
|
||||
Qt.AlignCenter, str(label))
|
||||
|
||||
def _paint_detections(self, painter: QPainter) -> None:
|
||||
if not self._detections or self._pixmap is None or self._pixmap.isNull():
|
||||
return
|
||||
|
||||
@@ -662,11 +662,17 @@ class PipelinePanel(QWidget):
|
||||
self.transport = TransportRow(palette)
|
||||
lay.addWidget(self.transport)
|
||||
|
||||
# data-collection bookmarks
|
||||
from aare.gui.new_gui.widgets.bookmarks import BookmarksBar
|
||||
self.bookmarks = BookmarksBar(palette)
|
||||
lay.addWidget(self.bookmarks)
|
||||
|
||||
self.set_mounted(False)
|
||||
|
||||
def set_mounted(self, mounted: bool) -> None:
|
||||
self._mounted = mounted
|
||||
self._badge.setVisible(mounted)
|
||||
self.bookmarks.setVisible(mounted)
|
||||
self.transport.setVisible(mounted)
|
||||
self.settings.set_run_enabled(mounted)
|
||||
self.tracker.set_preview(not mounted)
|
||||
|
||||
Reference in New Issue
Block a user