diff --git a/frontend/src/components/RugnuxPage.tsx b/frontend/src/components/RugnuxPage.tsx
index 4230cfa2d..abf3d164a 100644
--- a/frontend/src/components/RugnuxPage.tsx
+++ b/frontend/src/components/RugnuxPage.tsx
@@ -8,6 +8,7 @@ import EmptyState from "./EmptyState";
import RugnuxTimeline from "./RugnuxTimeline";
import RugnuxSettings from "./RugnuxSettings";
import RugnuxHero from "./RugnuxHero";
+import RugnuxStills from "./RugnuxStills";
import {CutAlert, lastImageTime, ModelScores, ProbeAlert, RunMessage, StateChip, StepTable, VerdictAlert} from "./RugnuxParts";
import BoltIcon from "@mui/icons-material/Bolt";
import {lazy, Suspense} from "react";
@@ -152,25 +153,27 @@ function RugnuxPage({measuring}: { measuring: boolean }) {
text="Dataset token required (key button in the top bar)"/>;
else if (isError || s === undefined || s.state === 'Idle')
live = }
- text="No rugnux run yet - start a rotation collection with live processing enabled"/>;
+ text="No rugnux run yet - start a collection with live processing enabled"/>;
else
live = (
-
+ {s.stills
+ ?
+ : }
{s.model && s.state !== 'Running' && }
-
-
+ {!s.stills &&
{s.verdict
?
:
@@ -180,7 +183,7 @@ function RugnuxPage({measuring}: { measuring: boolean }) {
0 ? 2 : 0}}>
{s.warnings.map((w, i) => {w})}
-
+ }
);
diff --git a/frontend/src/components/RugnuxStills.tsx b/frontend/src/components/RugnuxStills.tsx
new file mode 100644
index 000000000..9d36fcdc4
--- /dev/null
+++ b/frontend/src/components/RugnuxStills.tsx
@@ -0,0 +1,99 @@
+import {Box, Chip, LinearProgress, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography} from "@mui/material";
+import {grey, indigo} from "@mui/material/colors";
+import {rugnux_live_stills} from "../client";
+
+// Serial frames converted by the FPGA and analysed on the GPU: the pre-pass on the first images - the beam
+// centre, distance and mask the whole collection is analysed with - and the analysis since.
+
+const PREPASS_COLOUR = {Holding: "default", Running: "primary", Done: "success", Failed: "error"} as const;
+
+function Tile({label, value, sub}: { label: string, value: string, sub?: string }) {
+ return (
+
+
+ {label}
+
+ {value}
+ {sub && {sub}}
+
+ );
+}
+
+function signed(x: number, digits: number) {
+ return (x >= 0 ? "+" : "") + x.toFixed(digits);
+}
+
+function RugnuxStills({st}: { st: rugnux_live_stills }) {
+ const decided = st.beam_x_pxl !== undefined && st.beam_y_pxl !== undefined;
+ const shift = decided ? Math.hypot(st.beam_x_pxl! - st.file_beam_x_pxl, st.beam_y_pxl! - st.file_beam_y_pxl) : undefined;
+ const indexed = st.images_analysed > 0 ? 100 * st.images_indexed / st.images_analysed : undefined;
+ const fmt = (n?: number) => n === undefined ? "—" : n.toLocaleString();
+
+ return (
+
+
+ Pre-pass
+
+
+ {st.prepass === "Holding" && `holding ${st.images_held} / ${st.images_needed} images`}
+ {st.prepass === "Running" && `on the first ${st.images_held} images`}
+ {st.prepass_s !== undefined && `on ${st.images_held} images in ${st.prepass_s.toFixed(1)} s`}
+ {st.prepass === "Failed" && " · the stated beam centre and mask are used"}
+
+
+ {st.prepass === "Holding" &&
+ }
+ {st.prepass === "Running" && }
+
+
+
+
+
+ Stated
+ Pre-pass
+ Shift
+
+
+
+
+ Beam centre (px)
+ {st.file_beam_x_pxl.toFixed(2)}, {st.file_beam_y_pxl.toFixed(2)}
+
+ {decided ? `${st.beam_x_pxl!.toFixed(2)}, ${st.beam_y_pxl!.toFixed(2)}` : "—"}
+
+
+ {decided ? `${signed(st.beam_x_pxl! - st.file_beam_x_pxl, 2)}, ${signed(st.beam_y_pxl! - st.file_beam_y_pxl, 2)} (${shift!.toFixed(2)} px)` : "—"}
+
+
+
+ Detector distance (mm)
+ {st.file_distance_mm.toFixed(2)}
+
+ {st.distance_mm !== undefined ? st.distance_mm.toFixed(2) : "—"}
+
+
+ {st.distance_mm !== undefined ? `${signed(st.distance_mm - st.file_distance_mm, 2)} mm` : "—"}
+
+
+
+
+
+
+
+
+
+
+
+
+ Every image is analysed with the pre-pass's beam centre, distance and mask; images arriving
+ during the pre-pass are held and analysed after it.
+
+
+ );
+}
+
+export default RugnuxStills;