diff --git a/apps/visr/src/components/CamPva.tsx b/apps/visr/src/components/CamPva.tsx new file mode 100644 index 00000000..8f10cf3b --- /dev/null +++ b/apps/visr/src/components/CamPva.tsx @@ -0,0 +1,50 @@ +import { useState, useEffect } from "react"; +import { Box } from "@mui/material"; +import VideocamOffIcon from "@mui/icons-material/VideocamOff"; + +interface CamPvaProps { + src: string; + alt?: string; +} + +function CamPva({ src, alt = "Camera view" }: CamPvaProps) { + const [connected, setConnected] = useState(false); + + // Start disconnected again if a different stream is passed in + useEffect(() => { + setConnected(false); + }, [src]); + + return ( + + {!connected && ( + + + + )} + setConnected(true)} + onError={() => setConnected(false)} + sx={{ + width: "100%", + height: "100%", + objectFit: "contain", + visibility: connected ? "visible" : "hidden", + }} + /> + + ); +} + +export default CamPva; diff --git a/apps/visr/src/components/cell.ts b/apps/visr/src/components/cell.ts new file mode 100644 index 00000000..ba887914 --- /dev/null +++ b/apps/visr/src/components/cell.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from "react"; + +export interface Cell { + key: string; + title: string; + content: ReactNode; +} diff --git a/apps/visr/src/components/spectroscopy/SpectroscopyPlots.tsx b/apps/visr/src/components/spectroscopy/SpectroscopyPlots.tsx index 0bdb6091..031c26f8 100644 --- a/apps/visr/src/components/spectroscopy/SpectroscopyPlots.tsx +++ b/apps/visr/src/components/spectroscopy/SpectroscopyPlots.tsx @@ -30,6 +30,11 @@ import { SCAN_STATUS_TEXT, type ScanStatus, } from "./scanStatus"; +import CamPva from "../CamPva"; +import type { Cell } from "../cell"; + +const OVERVIEW_SRC = + "https://visr-pvws.diamond.ac.uk/mjpg/BL01C-DI-DCAM-04:PVA:OUTPUT"; const CHANNELS = [ { key: "red", label: "Red channel" }, @@ -39,7 +44,8 @@ const CHANNELS = [ type ChannelKey = (typeof CHANNELS)[number]["key"]; type PlotValues = ComponentProps["values"]; -export type SpectroscopyData = Partial>; +type ImageKey = ChannelKey | "composite"; +export type SpectroscopyData = Partial>; // --------------------------------------------------------------------------- // Data source -> channel mapping @@ -47,17 +53,22 @@ export type SpectroscopyData = Partial>; const SOURCE_CHANNELS: Record< DataSource, - Record + Record< + ImageKey, + `${"camera" | "diodes"}${"Red" | "Green" | "Blue" | "Composite"}` + > > = { [DataSource.Camera]: { red: "cameraRed", green: "cameraGreen", blue: "cameraBlue", + composite: "cameraComposite", }, [DataSource.Diodes]: { red: "diodesRed", green: "diodesGreen", blue: "diodesBlue", + composite: "diodesComposite", }, }; @@ -71,6 +82,7 @@ function selectChannels( red: data[mapping.red], green: data[mapping.green], blue: data[mapping.blue], + composite: data[mapping.composite], }; } @@ -220,7 +232,7 @@ function SpectroscopyPlots({ const totalRows = 10; const rowsPerPlot = expanded ? totalRows / 2 : totalRows; - const w = 1; + const cols = expanded ? 2 : 5; const minimumRowHeight = 20; const minimumGridHeight = minimumRowHeight * totalRows; const [containerHeight, setContainerHeight] = useState(0); @@ -246,28 +258,16 @@ function SpectroscopyPlots({ const gridHeight = rowHeight * totalRows; // ------------------------------------------------------------------------- - // Layout: 3x1 collapsed, 2x2 expanded + // Cells // ------------------------------------------------------------------------- - const layout = [ - { i: "0", x: 0, y: 0, w: w, h: rowsPerPlot, static: true }, - { i: "1", x: 1, y: 0, w: w, h: rowsPerPlot, static: true }, - { - i: "2", - x: expanded ? 0 : 2, - y: expanded ? rowsPerPlot : 0, - w: w, - h: rowsPerPlot, - static: true, - }, - ]; + const overviewCell: Cell = { + key: "overview", + title: "Overview", + content: , + }; - // ------------------------------------------------------------------------- - // Views - // ------------------------------------------------------------------------- - const views = CHANNELS.map(({ key }) => { - const values = channels[key]; - if (!values) return null; - return ( + const plot = (values: PlotValues | undefined) => + values ? ( - ); - }); + ) : null; + + const channelCells: Cell[] = CHANNELS.map(({ key, label }) => ({ + key, + title: label, + content: plot(channels[key]), + })); - const titles = CHANNELS.map(({ label }) => label); + // Not computed yet: empty cells on the same axes as the channels + const compositeCell: Cell = { + key: "composite", + title: "Composite image", + content: plot(channels.composite), + }; + + // Drawer open: camera + 3 channels + composite in one row (5x1) + // Drawer closed: 3 channels + composite in a 2x2 + const cells = expanded + ? [...channelCells, compositeCell] + : [overviewCell, ...channelCells, compositeCell]; + + // ------------------------------------------------------------------------- + // Layout + // ------------------------------------------------------------------------- + const layout = cells.map(({ key }, i) => ({ + i: key, + x: i % cols, + y: Math.floor(i / cols) * rowsPerPlot, + w: 1, + h: rowsPerPlot, + static: true, + })); // ------------------------------------------------------------------------- // Plot cells // ------------------------------------------------------------------------- - const plots = views.map((view, i) => ( + const plots = cells.map(({ key, title, content }) => ( - {titles[i]} + {title} div": { height: "100%" }, }} > - {view} + {content} )); @@ -353,7 +381,7 @@ function SpectroscopyPlots({ width={width} style={{ height: gridHeight }} gridConfig={{ - cols: expanded ? 2 : 3, + cols: cols, rowHeight: rowHeight, margin: [0, 0], }} diff --git a/apps/visr/src/components/spectroscopy/useSpectroscopyData.ts b/apps/visr/src/components/spectroscopy/useSpectroscopyData.ts index 4277d605..1cb2785f 100644 --- a/apps/visr/src/components/spectroscopy/useSpectroscopyData.ts +++ b/apps/visr/src/components/spectroscopy/useSpectroscopyData.ts @@ -12,6 +12,9 @@ export interface DataChannels { diodesRed: NDT; diodesGreen: NDT; diodesBlue: NDT; + /** Composite images; not computed yet, so all empty cells for now */ + cameraComposite: NDT; + diodesComposite: NDT; xValues: NDT; yValues: NDT; /** The beam size /render drew with, and where it came from (Gaussian and footprints) */ @@ -326,6 +329,8 @@ export function toChannels( diodesRed: image(ch.DiodesRed, "red"), diodesGreen: image(ch.DiodesGreen, "green"), diodesBlue: image(ch.DiodesBlue, "blue"), + cameraComposite: image(null, "gray"), + diodesComposite: image(null, "gray"), xValues: binEdgesToAxis(pixelEdges(v.x0, v.x1, v.width)), yValues: binEdgesToAxis(pixelEdges(v.y0, v.y1, v.height)), beam: answer.beam, @@ -341,6 +346,8 @@ export function toChannels( diodesRed: empty, diodesGreen: empty, diodesBlue: empty, + cameraComposite: empty, + diodesComposite: empty, xValues: initialAxes, yValues: initialAxes, }; @@ -361,6 +368,8 @@ export function toChannels( diodesRed: image(binned.DiodesRed, "red"), diodesGreen: image(binned.DiodesGreen, "green"), diodesBlue: image(binned.DiodesBlue, "blue"), + cameraComposite: image(null, "gray"), + diodesComposite: image(null, "gray"), xValues: binEdgesToAxis(splitEdges(binned.x_limits, parts)), yValues: binEdgesToAxis(splitEdges(binned.y_limits, parts)), }; diff --git a/apps/visr/src/components/tomography/TomographyPlots.tsx b/apps/visr/src/components/tomography/TomographyPlots.tsx index b45b54b7..c8776ab1 100644 --- a/apps/visr/src/components/tomography/TomographyPlots.tsx +++ b/apps/visr/src/components/tomography/TomographyPlots.tsx @@ -4,6 +4,13 @@ import { useLayoutEffect, useState } from "react"; import SliceViewer from "./SliceViewer"; import VolumeRenderer from "./VolumeRenderer"; import { Plane } from "./plane"; +import CamPva from "../CamPva"; +import type { Cell } from "../cell"; + +const OVERVIEW_SRC = + "https://visr-pvws.diamond.ac.uk/mjpg/BL01C-DI-DCAM-01:PVA:OUTPUT"; +const DETECTOR_SRC = + "https://visr-pvws.diamond.ac.uk/mjpg/BL01C-DI-DCAM-05:PVA:OUTPUT"; // --------------------------------------------------------------------------- // Types @@ -19,26 +26,6 @@ interface Props { threshold: number; } -// --------------------------------------------------------------------------- -// Camera view -// --------------------------------------------------------------------------- - -function CamPva() { - const [imgSrc, setImgSrc] = useState( - "https://visr-pvws.diamond.ac.uk/mjpg/BL01C-DI-DCAM-05:PVA:OUTPUT", - ); - - return ( - setImgSrc("../../../test-data/seal.png")} - alt="Camera view" - /> - ); -} - // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- @@ -57,7 +44,6 @@ function TomographyPlots({ // Grid sizing // ------------------------------------------------------------------------- const h = 10; - const w = 1; const minimumRowHeight = 30; const minimumGridHeight = minimumRowHeight * h; const [containerHeight, setContainerHeight] = useState(0); @@ -82,44 +68,72 @@ function TomographyPlots({ : 50; const gridHeight = rowHeight * h; + if (!volumeData) return ; + // ------------------------------------------------------------------------- - // Layout: 3x1 + // Cells // ------------------------------------------------------------------------- - const layout = [ - { i: "0", x: 0, y: 0, w: w, h: h, static: true }, - { i: "1", x: 1, y: 0, w: w, h: h, static: true }, - { i: "2", x: 2, y: 0, w: w, h: h, static: true }, + const overviewCell: Cell = { + key: "overview", + title: "Overview", + content: , + }; + + const plotCells: Cell[] = [ + { + key: "detector", + title: "Detector", + content: , + }, + { + key: "reconstruction", + title: "Reconstruction", + content: ( + + ), + }, + { + key: "slice", + title: "Slice View", + content: ( + + ), + }, ]; - if (!volumeData) return ; + // Drawer open: overview + 3 plots (4x1) + // Drawer closed: 3 plots only (3x1) + const cells = drawerOpen ? [overviewCell, ...plotCells] : plotCells; + const cols = cells.length; // ------------------------------------------------------------------------- - // Views + // Layout // ------------------------------------------------------------------------- - const views = [ - , - , - , - ]; - - const titles = ["Camera View", "Reconstruction", "Slice View"]; + const layout = cells.map(({ key }, i) => ({ + i: key, + x: i, + y: 0, + w: 1, + h: h, + static: true, + })); // ------------------------------------------------------------------------- // Plot cells // ------------------------------------------------------------------------- - const plots = views.map((view, i) => ( + const plots = cells.map(({ key, title, content }) => ( - {titles[i]} + {title} div": { height: "100%" }, }} > - {view} + {content} )); @@ -178,7 +192,7 @@ function TomographyPlots({ width={width} style={{ height: gridHeight }} gridConfig={{ - cols: 3, + cols: cols, rowHeight: rowHeight, margin: [0, 0], }}