Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
307 changes: 236 additions & 71 deletions frontend/src/components/scanInterface/ParameterCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import {
MenuItem,
Select,
TextField,
ToggleButton,
ToggleButtonGroup,
} from "@mui/material";
import { useContext, useMemo } from "react";
import { useParameter } from "../../hooks/useParameter";
import { ParameterStoreContext } from "../../contexts/ParameterStoreContext";
import { DeviceInfoContext } from "../../contexts/DeviceInfoContext";
import { ExperimentsContext } from "../../contexts/ExperimentsContext";
import { ParameterDisplayGroupsContext } from "../../contexts/ParameterDisplayGroupsContext";
Expand All @@ -17,6 +21,7 @@ import {
experimentIdToNamespace,
} from "../../utils/experimentUtils";
import {
ScanInputMode,
ScanParameterInfo,
ScanPattern,
scanPatterns,
Expand Down Expand Up @@ -89,6 +94,7 @@ export const ParameterCard = ({
);
const deviceInfo = useContext(DeviceInfoContext);
const experiments = useContext(ExperimentsContext);
const parameterStore = useContext(ParameterStoreContext);

// Create a mapping from namespace to experiment display name
const namespaceToDisplayName: Record<string, string> = Object.fromEntries(
Expand Down Expand Up @@ -166,6 +172,51 @@ export const ParameterCard = ({
}, [param, parameterDisplayGroups, deviceInfo]);

const pattern = param.generation.pattern ?? "linear";
const inputMode: ScanInputMode = param.generation.inputMode ?? "startStop";

// Live value of the currently selected parameter — used to seed "Center" when
// switching to span/center mode.
const [currentParamValue] = useParameter(param.id);
const currentNumericValue =
typeof currentParamValue === "number" ? currentParamValue : null;

// Derived quantities for span/center mode.
const center = (param.generation.start + param.generation.stop) / 2;
const span = param.generation.stop - param.generation.start;

const handleInputModeChange = (_: React.MouseEvent, newMode: ScanInputMode | null) => {
if (!newMode || newMode === inputMode) return;

if (newMode === "spanCenter") {
// Seed center from the live parameter value when available, otherwise keep the
// midpoint of the existing range.
const newCenter =
currentNumericValue !== null ? currentNumericValue : center;
const newSpan = Math.abs(span) || 1; // keep existing span (>0 guard)
const newStart = newCenter - newSpan / 2;
const newStop = newCenter + newSpan / 2;
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: {
generation: {
...param.generation,
inputMode: "spanCenter",
start: newStart,
stop: newStop,
},
values: generateScanValues(newStart, newStop, param.generation.points, pattern),
},
});
} else {
// Switching back to start/stop: just flip the mode flag; start/stop are already correct.
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: { generation: { ...param.generation, inputMode: "startStop" } },
});
}
};

return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
Expand Down Expand Up @@ -272,11 +323,37 @@ export const ParameterCard = ({
value={param.id}
title={param.id}
onChange={(e) => {
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: { id: e.target.value },
});
const newParamId = e.target.value;

if (inputMode === "spanCenter") {
// Re-centre on the new parameter's live value (if numeric); keep the span.
const rawValue = parameterStore?.get(newParamId);
const newCenter =
typeof rawValue === "number" ? rawValue : center;
const currentSpan = Math.abs(span) || 1;
const newStart = newCenter - currentSpan / 2;
const newStop = newCenter + currentSpan / 2;
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: {
id: newParamId,
generation: { ...param.generation, start: newStart, stop: newStop },
values: generateScanValues(
newStart,
newStop,
param.generation.points,
pattern,
),
},
});
} else {
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: { id: newParamId },
});
}
}}
renderValue={(value) => {
if (Object.keys(parameterOptions).length === 0) {
Expand All @@ -302,76 +379,164 @@ export const ParameterCard = ({
</FormControl>

<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<TextField
required
label="Start"
<ToggleButtonGroup
value={inputMode}
exclusive
size="small"
type="number"
fullWidth
value={param.generation.start}
onChange={(e) =>
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: {
generation: {
...param.generation,
start: Number(e.target.value),
onChange={handleInputModeChange}
>
<ToggleButton value="startStop" sx={{ textTransform: "none" }}>
Start / Stop
</ToggleButton>
<ToggleButton value="spanCenter" sx={{ textTransform: "none" }}>
Center / Span
</ToggleButton>
</ToggleButtonGroup>

{inputMode === "spanCenter" ? (
<>
<TextField
required
label="Center"
size="small"
type="number"
fullWidth
value={center}
onChange={(e) => {
const newCenter = Number(e.target.value);
const newStart = newCenter - span / 2;
const newStop = newCenter + span / 2;
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: {
generation: { ...param.generation, start: newStart, stop: newStop },
values: generateScanValues(
newStart,
newStop,
param.generation.points,
pattern,
),
},
});
}}
variant="outlined"
slotProps={{
input: {
inputProps: {
min: parameterOptions[param.id]?.min,
max: parameterOptions[param.id]?.max,
},
},
values: generateScanValues(
Number(e.target.value),
param.generation.stop,
param.generation.points,
pattern,
),
},
})
}
variant="outlined"
slotProps={{
input: {
inputProps: {
min: parameterOptions[param.id]?.min,
max: parameterOptions[param.id]?.max,
},
},
}}
/>
<TextField
required
label="Stop"
size="small"
type="number"
fullWidth
value={param.generation.stop}
onChange={(e) =>
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: {
generation: {
...param.generation,
stop: Number(e.target.value),
}}
/>
<TextField
required
label="Span"
size="small"
type="number"
fullWidth
value={Math.abs(span)}
onChange={(e) => {
const newSpan = Math.abs(Number(e.target.value));
const newStart = center - newSpan / 2;
const newStop = center + newSpan / 2;
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: {
generation: { ...param.generation, start: newStart, stop: newStop },
values: generateScanValues(
newStart,
newStop,
param.generation.points,
pattern,
),
},
});
}}
variant="outlined"
slotProps={{
input: { inputProps: { min: 0 } },
}}
/>
</>
) : (
<>
<TextField
required
label="Start"
size="small"
type="number"
fullWidth
value={param.generation.start}
onChange={(e) =>
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: {
generation: {
...param.generation,
start: Number(e.target.value),
},
values: generateScanValues(
Number(e.target.value),
param.generation.stop,
param.generation.points,
pattern,
),
},
})
}
variant="outlined"
slotProps={{
input: {
inputProps: {
min: parameterOptions[param.id]?.min,
max: parameterOptions[param.id]?.max,
},
},
values: generateScanValues(
param.generation.start,
Number(e.target.value),
param.generation.points,
pattern,
),
},
})
}
variant="outlined"
slotProps={{
input: {
inputProps: {
min: parameterOptions[param.id]?.min,
max: parameterOptions[param.id]?.max,
},
},
}}
/>
}}
/>
<TextField
required
label="Stop"
size="small"
type="number"
fullWidth
value={param.generation.stop}
onChange={(e) =>
dispatchScanInfoStateUpdate({
type: "UPDATE_PARAMETER",
index,
payload: {
generation: {
...param.generation,
stop: Number(e.target.value),
},
values: generateScanValues(
param.generation.start,
Number(e.target.value),
param.generation.points,
pattern,
),
},
})
}
variant="outlined"
slotProps={{
input: {
inputProps: {
min: parameterOptions[param.id]?.min,
max: parameterOptions[param.id]?.max,
},
},
}}
/>
</>
)}

<TextField
required
label="Points"
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/types/ScanParameterInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export const scanPatterns = ["linear", "scatter", "centred", "forwardReverse"] as const;
export type ScanPattern = (typeof scanPatterns)[number];

export const scanInputModes = ["startStop", "spanCenter"] as const;
export type ScanInputMode = (typeof scanInputModes)[number];

export interface ScanParameterInfo {
id: string;
deviceNameOrDisplayGroup: string;
Expand All @@ -11,6 +14,8 @@ export interface ScanParameterInfo {
stop: number;
points: number;
pattern: ScanPattern;
/** Controls which pair of fields is shown in the UI. "startStop" is the default. */
inputMode?: ScanInputMode;
};
n_scan_points?: number;
}
Loading
Loading