Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f80ecfd
add sliceViewer file and slider component
Abigail-Yates Jun 9, 2026
cfcdbe0
change displayed test image using slider
Abigail-Yates Jun 10, 2026
00b7e96
rearrange tomography views
Abigail-Yates Jun 11, 2026
af7f8d1
prettify volume viewer files
Abigail-Yates Jun 18, 2026
aa84e50
starting point for tomography slice viewer
tomkane-dls Jun 18, 2026
692ecec
wip - slice viewer creating url for img src
Abigail-Yates Jun 18, 2026
3d8dac8
Merge branch 'tomo-slice-viewer' of github.com:DiamondLightSource/atl…
Abigail-Yates Jun 19, 2026
721193a
wip - view one static slice in z-direction
Abigail-Yates Jun 19, 2026
8035b45
slice through z-axis working
Abigail-Yates Jun 19, 2026
e201da4
move slider slice controls into Controls.txk
Abigail-Yates Jun 19, 2026
ce378a1
wip - slice view in X direction
Abigail-Yates Jun 19, 2026
d0cdc90
added x direction slice view
Abigail-Yates Jun 22, 2026
0083626
add y-direction slices, change views to side-by-side
Abigail-Yates Jun 22, 2026
fedd9bb
tidy up
Abigail-Yates Jun 23, 2026
766fdf7
store states in local storage to persist across tabs
Abigail-Yates Jun 23, 2026
94df57e
use Plane enum instead of string
Abigail-Yates Jun 23, 2026
cc8bf7d
generalise x and y directions from volumeShape in SliceViewer
Abigail-Yates Jun 23, 2026
6d0cbcd
fix bug changing to Z-plane in two tabs
Abigail-Yates Jun 24, 2026
30adf14
fix slice direction bug and load test data in TomographyView.tsx
Abigail-Yates Jun 24, 2026
3b9b36e
move loading test data to TomographyView for Volume view
Abigail-Yates Jun 24, 2026
0479c28
generalise plane and fix some linting issues
Abigail-Yates Jun 24, 2026
d69eb15
wip - use Davidia heatmap to show slices
Abigail-Yates Jun 25, 2026
2679598
use Davidia to show slices
Abigail-Yates Jun 26, 2026
fe7b78c
remove axes labels
Abigail-Yates Jun 26, 2026
e3b710f
use ndarray.pick to find slice of data. install ndarray-ops.
Abigail-Yates Jul 8, 2026
7d2ee36
tidy up
Abigail-Yates Jul 8, 2026
4051476
update pnpm-lock.yaml
Abigail-Yates Jul 8, 2026
ba4761b
fix linting errors
Abigail-Yates Jul 8, 2026
5c7fbde
hide slice view tool bar
Abigail-Yates Jul 9, 2026
c0cbfd4
tidy up
Abigail-Yates Jul 9, 2026
5add2fe
remove sliceRenderer, test data. refactoring.
Abigail-Yates Jul 23, 2026
d3e7628
Merge branch 'main' into tomo-slice-viewer
Abigail-Yates Jul 23, 2026
4646be6
move createArrayFromView func into utils and start to add tests for it
Abigail-Yates Jul 28, 2026
febff8e
fix linting
Abigail-Yates Jul 28, 2026
dd11bab
address comments (minWidth and check volumeShape)
Abigail-Yates Jul 29, 2026
77a93bb
Merge branch 'main' of github.com:DiamondLightSource/atlas into tomo-…
Abigail-Yates Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/visr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@react-three/fiber": "^8.16.8",
"@tanstack/react-query": "^5.90.21",
"ndarray": "^1.0.19",
"ndarray-ops": "^1.2.2",
"react-error-boundary": "^6.0.0",
"react-router-dom": "^7.7.1",
"three": "^0.164.1"
Expand All @@ -42,6 +43,8 @@
"@mui/material": "^6.5.0",
"@mui/x-date-pickers": "^7.29.4",
"@types/ndarray": "^1.0.14",
"@types/ndarray-ops": "^1.2.7",
"@types/node": "^25.9.2",
"@types/react-relay": "^18.2.1",
"@types/relay-runtime": "^19.0.2",
"@types/three": "^0.164.0",
Expand Down
3 changes: 1 addition & 2 deletions apps/visr/src/components/tomography/CameraViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Typography } from "@mui/material";
export default function CameraViewer() {

export default function CameraViewer() {
// static image showing camera view/projection feed
return (
<Box
Expand All @@ -27,7 +27,6 @@ export default function CameraViewer() {
<Typography variant="overline" color="primary">
Camera View
</Typography>

</Box>

<Box
Expand Down
53 changes: 49 additions & 4 deletions apps/visr/src/components/tomography/Controls.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
import {
Box,
Button,
FormControlLabel,
LinearProgress,
Radio,
RadioGroup,
Slider,
Stack,
TextField,
Typography,
} from "@mui/material";
import { Plane } from "./PlaneEnum";

interface Props {
onRun: () => void;
onReset: () => void;
onSlide: (event: Event, newValue: number | number[]) => void;
onSetDirection: (event: React.ChangeEvent<HTMLInputElement>) => void;
plane: Plane;
progress: number;
slice: number;
volumeShape: [number, number, number];
}

export default function Controls({ onRun, onReset, progress }: Props) {
export default function Controls({
onRun,
onReset,
onSlide,
onSetDirection,
plane,
progress,
slice,
volumeShape,
}: Props) {
return (
<Box
sx={{
Expand All @@ -27,8 +47,6 @@ export default function Controls({ onRun, onReset, progress }: Props) {
}}
>
<Stack direction="row" spacing={1} alignItems="center">


<TextField
label="angles"
type="number"
Expand All @@ -55,14 +73,41 @@ export default function Controls({ onRun, onReset, progress }: Props) {
>
Revolve
</ToggleButton> */}
<Box sx={{ flex: 1 }}>
<Typography variant="body1" color="primary">
Slice
</Typography>
<Slider
shiftStep={1}
step={1}
min={0}
max={
plane === Plane.Z
? volumeShape[0] - 1
: plane === Plane.Y
? volumeShape[1] - 1
: volumeShape[2] - 1
}
marks
onChange={onSlide}
value={slice}
></Slider>{" "}
<Typography variant="body1" color="primary">
Axis
</Typography>
<RadioGroup row value={plane} onChange={onSetDirection}>
<FormControlLabel value={Plane.X} control={<Radio />} label="X" />
<FormControlLabel value={Plane.Y} control={<Radio />} label="Y" />
<FormControlLabel value={Plane.Z} control={<Radio />} label="Z" />
</RadioGroup>
</Box>
</Stack>

<LinearProgress
variant="determinate"
value={progress}
sx={{ height: 8, borderRadius: 1 }}
/>

</Box>
);
}
5 changes: 5 additions & 0 deletions apps/visr/src/components/tomography/PlaneEnum.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum Plane {
X,
Y,
Z,
}
74 changes: 74 additions & 0 deletions apps/visr/src/components/tomography/SliceViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Box, Typography } from "@mui/material";
import { Plane } from "./PlaneEnum";
import { HeatmapPlot } from "@diamondlightsource/davidia";
import ndarray from "ndarray";
import createArrayFromView from "../../utils/createArrayFromView";

interface Props {
volumeData: Uint8Array;
volumeShape: [number, number, number];
plane: Plane;
slice: number;
}
export default function SliceViewer({
volumeData,
volumeShape,
plane,
slice,
}: Props) {
if (volumeData == undefined || volumeShape == undefined) {
return <Box />;
}

const volume = ndarray(volumeData, volumeShape);
let sliceNdarray: ndarray.NdArray<Uint8Array>;

switch (plane) {
case Plane.Z: {
sliceNdarray = createArrayFromView(volume.pick(slice, null, null));
break;
}
case Plane.Y: {
sliceNdarray = createArrayFromView(volume.pick(null, slice, null));
break;
}
case Plane.X: {
sliceNdarray = createArrayFromView(volume.pick(null, null, slice));
break;
}
}

return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
bgcolor: "background.paper",
}}
>
<Box
sx={{
px: 2,
py: 1.25,
borderBottom: 1,
borderColor: "divider",
// height: "100%",
}}
>
<Typography variant="overline" color="primary">
Slice View
</Typography>
</Box>
<div style={{ display: "grid", height: "100%" }}>
<HeatmapPlot
aspect="auto"
plotConfig={{}}
values={sliceNdarray}
domain={[0, 255]}
customToolbarChildren={null}
/>
</div>
</Box>
);
}
133 changes: 120 additions & 13 deletions apps/visr/src/components/tomography/TomographyView.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,81 @@
import { Box } from "@mui/material";
import { useRef, useState } from "react";
import { Box, Divider, Stack } from "@mui/material";
import { useEffect, useRef, useState } from "react";
import CameraViewer from "./CameraViewer";
import VolumeViewer from "./VolumeViewer";
import Controls from "./Controls";
import SliceViewer from "./SliceViewer";
import { Plane } from "./PlaneEnum";

interface Volume {
volumeData: Uint8Array;
volumeShape: [number, number, number];
}

const SCAN_DURATION_MS = 3000;

function TomographyView() {
const [volume, setVolume] = useState<Volume | null>(null);
const [volumeVisible, setVolumeVisible] = useState(false);
const [progress, setProgress] = useState(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// const [revolve, setRevolve] = useState(false);
const [slice, setSlice] = useState<number>(0);
const [plane, setPlane] = useState<Plane>(Plane.Z);

useEffect(() => {
async function loadTestVolume() {
const [metaRes, rawRes] = await Promise.all([
fetch("/test-data/volume.json"),
fetch("/test-data/volume.raw"),
]);
const meta = (await metaRes.json()) as {
shape: [number, number, number];
};
const buffer = await rawRes.arrayBuffer();
setVolume({
volumeData: new Uint8Array(buffer),
volumeShape: meta.shape,
});
}

loadTestVolume();
}, []);

//use local storage to persist values across multiple open tabs
useEffect(() => {
localStorage.setItem("plane", plane.toString());
localStorage.setItem("volumeVisible", volumeVisible.toString());
localStorage.setItem("slice", slice.toString());
localStorage.setItem("progress", progress.toString());
}, [plane, volumeVisible, slice, progress]);

useEffect(() => {
const onReceiveMessage = (e: StorageEvent) => {
const { key, newValue } = e;
switch (key) {
case "plane": {
setPlane(Number(newValue));
break;
}
case "volumeVisible": {
setVolumeVisible(Boolean(newValue));
break;
}
case "slice": {
setSlice(Number(newValue));
break;
}
case "progress": {
setProgress(Number(newValue));
break;
}
}
};
window.addEventListener("storage", onReceiveMessage);
return () => {
window.removeEventListener("storage", onReceiveMessage);
};
}, []);

// run waits 3 seconds, updating progress bar then allows mock volume to be seen
const handleRun = () => {
Expand All @@ -37,8 +102,18 @@ function TomographyView() {
setProgress(0);
};

// revolve to be implemented
const handleSlider = (event: Event, newValue: number | number[]) => {
const slice = typeof newValue == "number" ? newValue : newValue[0];
setSlice(slice);
};

const handlePlane = (event: React.ChangeEvent<HTMLInputElement>) => {
const radioValue = Number((event.target as HTMLInputElement).value);
setPlane(radioValue);
};

if (!volume) return <Box />;
// revolve to be implemented
return (
<Box
sx={{
Expand All @@ -50,12 +125,10 @@ function TomographyView() {
bgcolor: "background.default",
}}
>
<Box sx={{ display: "flex", flex: 1, overflow: "hidden" }}>

{/* Left panel */}
<Stack direction="row" divider={<Divider orientation="vertical" />}>
<Box
sx={{
width: "38%",
width: "30%",
minWidth: 260,
display: "flex",
flexDirection: "column",
Expand All @@ -64,13 +137,47 @@ function TomographyView() {
<CameraViewer />
</Box>

{/* Right panel */}
<Box sx={{ flex: 1, display: "flex", flexDirection: "column" }}>
<VolumeViewer visible={volumeVisible} />
<Box
sx={{
flex: 1,
width: "30%",
Comment thread
EmsArnold marked this conversation as resolved.
display: "flex",
flexDirection: "column",
}}
>
<VolumeViewer
volumeData={volume.volumeData}
volumeShape={volume.volumeShape}
visible={volumeVisible}
/>
</Box>
</Box>

<Controls onRun={handleRun} onReset={handleReset} progress={progress} />
<Box
sx={{
flex: 1,
width: "30%",
Comment thread
EmsArnold marked this conversation as resolved.
display: "flex",
flexDirection: "column",
bgcolor: "red",
}}
>
<SliceViewer
volumeData={volume.volumeData}
volumeShape={volume.volumeShape}
slice={slice}
plane={plane}
/>
</Box>
</Stack>
<Controls
onRun={handleRun}
onReset={handleReset}
onSlide={handleSlider}
onSetDirection={handlePlane}
plane={plane}
progress={progress}
slice={slice}
volumeShape={volume ? volume.volumeShape : [0, 0, 0]}
/>
</Box>
);
}
Expand Down
Loading
Loading