From cd5eaf62a6466adbd5d83a804b5834978d89d330 Mon Sep 17 00:00:00 2001 From: Florian Paproth Date: Thu, 2 Jul 2026 22:49:58 +0200 Subject: [PATCH 1/4] Refactor objective switching logic to use backend slot configuration and dispatch notifications for unconfigured slots --- .../src/components/ObjectiveController.js | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/ObjectiveController.js b/frontend/src/components/ObjectiveController.js index c3780c8e..2fdc7f1b 100644 --- a/frontend/src/components/ObjectiveController.js +++ b/frontend/src/components/ObjectiveController.js @@ -16,6 +16,7 @@ import * as objectiveSlice from "../state/slices/ObjectiveSlice.js"; import * as laserSlice from "../state/slices/LaserSlice.js"; import * as stormSlice from "../state/slices/STORMSlice.js"; import * as detectorParametersSlice from "../state/slices/DetectorParametersSlice.js"; +import { setNotification } from "../state/slices/NotificationSlice.js"; import * as positionSlice from "../state/slices/PositionSlice.js"; import { getConnectionSettingsState } from "../state/slices/ConnectionSettingsSlice"; import { useTheme } from "@mui/material/styles"; @@ -23,9 +24,7 @@ import { useTheme } from "@mui/material/styles"; import apiPositionerControllerMovePositioner from "../backendapi/apiPositionerControllerMovePositioner.js"; import apiObjectiveControllerSetPositions from "../backendapi/apiObjectiveControllerSetPositions.js"; import apiObjectiveControllerCalibrateObjective from "../backendapi/apiObjectiveControllerCalibrateObjective.js"; -import apiObjectiveControllerGetCurrentObjective from "../backendapi/apiObjectiveControllerGetCurrentObjective.js"; import apiObjectiveControllerMoveToObjective from "../backendapi/apiObjectiveControllerMoveToObjective.js"; -import apiObjectiveControllerGetStatus from "../backendapi/apiObjectiveControllerGetStatus.js"; import apiPositionerControllerGetPositions from "../backendapi/apiPositionerControllerGetPositions.js"; import apiSettingsControllerGetDetectorNames from "../backendapi/apiSettingsControllerGetDetectorNames.js"; import apiObjectiveControllerSetObjectiveParameters from "../backendapi/apiObjectiveControllerSetObjectiveParameters.js"; @@ -122,22 +121,18 @@ const ExtendedObjectiveController = () => { // Switch objective (slot should be 0 or 1) const handleSwitchObjective = async (slot, skipZ) => { - // Warn user if the target positions have not been configured yet - const x0 = objectiveState.posX0; - const x1 = objectiveState.posX1; - const positionsConfigured = - x0 !== null && - x0 !== undefined && - x0 !== 0 && - x1 !== null && - x1 !== undefined && - x1 !== 0; - if (!positionsConfigured) { - alert( - "Objective positions (X0 / X1) are not configured yet.\n" + - "Please use the Calibration Wizard to set them before switching.", + // Use backend-derived slotConfigured as the source of truth. + const isTargetSlotConfigured = + objectiveState.slotConfigured?.[slot] !== false; + if (!isTargetSlotConfigured) { + dispatch( + setNotification({ + message: + "Target objective slot is not configured yet. Please set objective name and magnification (or use the calibration flow) before switching.", + type: "warning", + }), ); - // return; + return; } try { await rememberObjectiveIllumination({ From 0290ad5cdc4f778ab209ec12fd3d2017260025e3 Mon Sep 17 00:00:00 2001 From: Florian Paproth Date: Thu, 2 Jul 2026 22:50:33 +0200 Subject: [PATCH 2/4] Add API imports for current objective retrieval and status checking --- frontend/src/components/ObjectiveController.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/ObjectiveController.js b/frontend/src/components/ObjectiveController.js index 2fdc7f1b..670505fc 100644 --- a/frontend/src/components/ObjectiveController.js +++ b/frontend/src/components/ObjectiveController.js @@ -24,7 +24,9 @@ import { useTheme } from "@mui/material/styles"; import apiPositionerControllerMovePositioner from "../backendapi/apiPositionerControllerMovePositioner.js"; import apiObjectiveControllerSetPositions from "../backendapi/apiObjectiveControllerSetPositions.js"; import apiObjectiveControllerCalibrateObjective from "../backendapi/apiObjectiveControllerCalibrateObjective.js"; +import apiObjectiveControllerGetCurrentObjective from "../backendapi/apiObjectiveControllerGetCurrentObjective.js"; import apiObjectiveControllerMoveToObjective from "../backendapi/apiObjectiveControllerMoveToObjective.js"; +import apiObjectiveControllerGetStatus from "../backendapi/apiObjectiveControllerGetStatus.js"; import apiPositionerControllerGetPositions from "../backendapi/apiPositionerControllerGetPositions.js"; import apiSettingsControllerGetDetectorNames from "../backendapi/apiSettingsControllerGetDetectorNames.js"; import apiObjectiveControllerSetObjectiveParameters from "../backendapi/apiObjectiveControllerSetObjectiveParameters.js"; From 61419db429db94535fcc38f29de7665318e6da81 Mon Sep 17 00:00:00 2001 From: Florian Paproth Date: Thu, 2 Jul 2026 23:09:04 +0200 Subject: [PATCH 3/4] Refactor WellSelectorComponent and WellSelectorUtils for improved readability and functionality - Cleaned up code formatting and removed unnecessary whitespace in WellSelectorComponent.js. - Updated well layout default name from "Default" to "Blank" in WellSelectorUtils.js. - Enhanced the cloneLayout function in ExperimentSlice.js to ensure deep cloning of well layouts. - Initialized the experiment state with a cloned version of the wellLayoutDevelopment. - Improved error handling and logging for point creation in ExperimentSlice.js. - Adjusted comments for clarity and consistency across the codebase. --- frontend/src/axon/WellSelectorComponent.js | 255 +++++++++++++------ frontend/src/axon/WellSelectorUtils.js | 190 +++++++------- frontend/src/state/slices/ExperimentSlice.js | 136 +++++----- 3 files changed, 347 insertions(+), 234 deletions(-) diff --git a/frontend/src/axon/WellSelectorComponent.js b/frontend/src/axon/WellSelectorComponent.js index a2a2525c..176edf01 100644 --- a/frontend/src/axon/WellSelectorComponent.js +++ b/frontend/src/axon/WellSelectorComponent.js @@ -7,7 +7,6 @@ import * as wsUtils from "./WellSelectorUtils.js"; import InfoPopup from "./InfoPopup.js"; - import * as wellSelectorSlice from "../state/slices/WellSelectorSlice.js"; import * as experimentSlice from "../state/slices/ExperimentSlice.js"; import * as positionSlice from "../state/slices/PositionSlice.js"; @@ -47,15 +46,15 @@ import GpsFixedIcon from "@mui/icons-material/GpsFixed"; const WellSelectorComponent = () => { //local state const [wellLayoutFileList] = useState([ - "image/test.json",//TODO remove test - "image/test1.json",//TODO remove test + "image/test.json", //TODO remove test + "image/test1.json", //TODO remove test ]); // Pending layout switch awaiting user confirmation when pointList is non-empty const [pendingLayoutEvent, setPendingLayoutEvent] = useState(null); //child ref - const childRef = useRef();//canvas + const childRef = useRef(); //canvas const infoPopupRef = useRef(); //redux dispatcher @@ -95,7 +94,6 @@ const WellSelectorComponent = () => { fetchObjectiveControllerGetStatus(dispatch); }, [dispatch]); - // Convert the current freehand polygon (drawn on the canvas) into // experiment scan points using the current FOV and area-scan overlap. const handleConvertFreehandToPoints = () => { @@ -110,7 +108,7 @@ const WellSelectorComponent = () => { if (!positions || positions.length === 0) { if (infoPopupRef.current) { infoPopupRef.current.showMessage( - "Draw a closed freehand region first (FREEHAND mode, click+drag)." + "Draw a closed freehand region first (FREEHAND mode, click+drag).", ); } return; @@ -139,17 +137,16 @@ const WellSelectorComponent = () => { iX: 0, iY: 0, })), - }) + }), ); childRef.current.clearFreehand && childRef.current.clearFreehand(); if (infoPopupRef.current) { infoPopupRef.current.showMessage( - `Created 1 freehand region with ${positions.length} scan position(s).` + `Created 1 freehand region with ${positions.length} scan position(s).`, ); } }; - //################################################################################## const handleModeChange = (mode) => { // Update Redux state @@ -216,24 +213,24 @@ const WellSelectorComponent = () => { const offsetY = wellSelectorState.layoutOffsetY || 0; //check defaults - if (event.target.value === "Default") { + if (event.target.value === "Blank" || event.target.value === "Default") { wellLayout = wsUtils.wellLayoutDefault; } else if (event.target.value === "Heidstar 4x Histosample") { wellLayout = wsUtils.wellLayoutDevelopment; - } else if (event.target.value === "Wellplate 384") { + } else if (event.target.value === "Wellplate 384") { // Generate 384 layout with offsets wellLayout = wsUtils.generateWellLayout384({ offsetX: offsetX, - offsetY: offsetY + offsetY: offsetY, }); } else if (event.target.value === "DEP Chip") { // Generate DEP Chip layout with offsets wellLayout = wsUtils.generateWellLayoutDEPChip({ offsetX: offsetX, - offsetY: offsetY + offsetY: offsetY, }); } else if (event.target.value === "Ropod") { - wellLayout = wsUtils.ropodLayout; + wellLayout = wsUtils.ropodLayout; } else { //donwload layout apiDownloadJson(event.target.value) // Pass the JSON file path @@ -241,11 +238,13 @@ const WellSelectorComponent = () => { console.log("apiDownloadJson", data); //handle layout //TODO - //set popup - if (infoPopupRef.current) { - infoPopupRef.current.showMessage("TODO impl me"); - } - console.error("-----------------------------------------------TODO impl me------------------------------------------------------------"); + //set popup + if (infoPopupRef.current) { + infoPopupRef.current.showMessage("TODO impl me"); + } + console.error( + "-----------------------------------------------TODO impl me------------------------------------------------------------", + ); }) .catch((err) => { //handle error if needed @@ -258,7 +257,6 @@ const WellSelectorComponent = () => { // Apply offsets to the layout wellLayout = wsUtils.applyLayoutOffset(wellLayout, offsetX, offsetY); - //set new layout dispatch(experimentSlice.setWellLayout(wellLayout)); }; @@ -270,17 +268,21 @@ const WellSelectorComponent = () => { const currentY = positionState.y; const currentZ = positionState.z; // Create a new point with current position - dispatch(experimentSlice.createPoint({ - x: currentX, - y: currentY, - z: currentZ, - name: `Position ${experimentState.pointList.length + 1}`, - shape: "" - })); - + dispatch( + experimentSlice.createPoint({ + x: currentX, + y: currentY, + z: currentZ, + name: `Position ${experimentState.pointList.length + 1}`, + shape: "", + }), + ); + // Show confirmation message if (infoPopupRef.current) { - infoPopupRef.current.showMessage(`Added position: X=${currentX}, Y=${currentY}`); + infoPopupRef.current.showMessage( + `Added position: X=${currentX}, Y=${currentY}`, + ); } }; @@ -291,9 +293,11 @@ const WellSelectorComponent = () => { // This uses the clicked position as the known position and transmits it to the backend // via the setStageOffsetAxis API (single source of truth for offsets). if (infoPopupRef.current) { - infoPopupRef.current.showMessage("Right-click on the map where you are and select 'We are here' to calibrate the stage offset."); + infoPopupRef.current.showMessage( + "Right-click on the map where you are and select 'We are here' to calibrate the stage offset.", + ); } - } + }; //################################################################################## const handleMoveCameraSpeedXYChange = (event) => { @@ -314,22 +318,21 @@ const WellSelectorComponent = () => { //################################################################################## return (
+ {/* Opentrons-style labware selection (loadName + well chips + condition labels) */} + - {/* Opentrons-style labware selection (loadName + well chips + condition labels) */} - + {/* LAYOUT */} + {/* WellSelectorComponent with mode passed as prop width: "100%", height: "100%", display: "block"*/} + - {/* LAYOUT */} - {/* WellSelectorComponent with mode passed as prop width: "100%", height: "100%", display: "block"*/} - + {/* PARAMETER*/} + {/* Add a little spacer between the wellselector and layout */} +
-{/* PARAMETER*/} -{/* Add a little spacer between the wellselector and layout */} -
- -{/* PARAMETER */} -{/* Add a little spacer between the wellselector and layout */} -
-
+ {/* PARAMETER */} + {/* Add a little spacer between the wellselector and layout */} +
+
Layout {/* VIEW - All controls in one row */} - - - - -
{/* MODE — selection tools + stage actions, with explanatory icons + tooltips */} @@ -410,17 +428,44 @@ const WellSelectorComponent = () => { {/* Selection / interaction modes (active one is filled) */} {[ - { mode: Mode.SINGLE_SELECT, label: "Single", icon: , tip: "Single point — click the map to add one imaging position." }, - { mode: Mode.AREA_SELECT, label: "Area", icon: , tip: "Area — drag a rectangle to tile-scan a whole region." }, - { mode: Mode.CUP_SELECT, label: "Well", icon: , tip: "Well — click wells to image the entire well." }, - { mode: Mode.FREEHAND_DRAW, label: "Freehand", icon: , tip: "Freehand — draw a closed region, then press Convert to fill it with scan points." }, - { mode: Mode.MOVE_CAMERA, label: "Move", icon: , tip: "Move camera — click the map to drive the stage to that point." }, + { + mode: Mode.SINGLE_SELECT, + label: "Single", + icon: , + tip: "Single point — click the map to add one imaging position.", + }, + { + mode: Mode.AREA_SELECT, + label: "Area", + icon: , + tip: "Area — drag a rectangle to tile-scan a whole region.", + }, + { + mode: Mode.CUP_SELECT, + label: "Well", + icon: , + tip: "Well — click wells to image the entire well.", + }, + { + mode: Mode.FREEHAND_DRAW, + label: "Freehand", + icon: , + tip: "Freehand — draw a closed region, then press Convert to fill it with scan points.", + }, + { + mode: Mode.MOVE_CAMERA, + label: "Move", + icon: , + tip: "Move camera — click the map to drive the stage to that point.", + }, ].map((b) => ( )} - - - - + - diff --git a/frontend/src/axon/WellSelectorUtils.js b/frontend/src/axon/WellSelectorUtils.js index bf9caf78..d54f0a95 100644 --- a/frontend/src/axon/WellSelectorUtils.js +++ b/frontend/src/axon/WellSelectorUtils.js @@ -12,7 +12,7 @@ export function calculateRasterRect( rectPlusX, rectMinusX, rectPlusY, - rectMinusY + rectMinusY, ) { if (rasterWidth <= 0 || rasterHeight <= 0) { throw new Error("Raster width and height must be greater than 0."); @@ -70,7 +70,7 @@ export function calculateRasterRect( rasterLeft, rasterTop, rasterRight, - rasterBottom + rasterBottom, ) ) { rasters.push({ x, y, iX: ix, iY: iy }); @@ -95,7 +95,7 @@ export function calculateRasterOval( rasterWidth, rasterHeight, radiusX, - radiusY + radiusY, ) { const rasters = []; @@ -117,10 +117,10 @@ export function calculateRasterOval( // Queue für die Expansion, Startpunkt ist das Zentrum (index 0,0) const queue = [{ ix: 0, iy: 0 }]; const directions = [ - { dix: 1, diy: 0 }, // Rechts expandieren - { dix: -1, diy: 0 }, // Links expandieren - { dix: 0, diy: 1 }, // Oben expandieren - { dix: 0, diy: -1 }, // Unten expandieren + { dix: 1, diy: 0 }, // Rechts expandieren + { dix: -1, diy: 0 }, // Links expandieren + { dix: 0, diy: 1 }, // Oben expandieren + { dix: 0, diy: -1 }, // Unten expandieren ]; while (queue.length > 0) { @@ -159,7 +159,7 @@ export function calculateNeighborPointsSquare( squareWidth, squareHeight, numNeighborsX, - numNeighborsY + numNeighborsY, ) { const points = []; @@ -185,7 +185,7 @@ export function calculateNeighborPointsCircle( squareY, squareWidth, squareHeight, - numNeighbors + numNeighbors, ) { const points = []; @@ -229,7 +229,7 @@ export function generateCenterPointsInRect( rectStartPoint, rectEndPoint, wellWidth, - wellHeight + wellHeight, ) { // Ensure starting and ending positions are correctly ordered const startX = Math.min(rectStartPoint.x, rectEndPoint.x); @@ -270,7 +270,7 @@ export function isRectInsideRect(x1, y1, width, height, rx1, ry1, rx2, ry2) { corner.x >= left && corner.x <= right && corner.y >= top && - corner.y <= bottom + corner.y <= bottom, ); } @@ -314,7 +314,7 @@ export function isPointInsideRect( point, squareCenter, squareWidth, - squareHeight + squareHeight, ) { const squareX = squareCenter.x - squareWidth / 2; const squareY = squareCenter.y - squareHeight / 2; @@ -388,7 +388,7 @@ export function isLineIntersectingSquare( y2, squareX, squareY, - size + size, ) { // Define the square's corners const topLeft = { x: squareX, y: squareY }; @@ -452,13 +452,13 @@ export function isLineIntersectingSquare( { x: x1, y: y1 }, { x: x2, y: y2 }, topRight, - bottomRight + bottomRight, ) || doLinesIntersect( { x: x1, y: y1 }, { x: x2, y: y2 }, bottomRight, - bottomLeft + bottomLeft, ) || doLinesIntersect({ x: x1, y: y1 }, { x: x2, y: y2 }, bottomLeft, topLeft) ) { @@ -470,7 +470,7 @@ export function isLineIntersectingSquare( //################################################################################## export const wellLayoutDefault = { - name: "Default", + name: "Blank", unit: "um", width: 120000, // Standard width for 32-well plate height: 80000, // Standard height for 32-well plate @@ -487,7 +487,7 @@ export const wellLayoutDevelopment = { wells: [ { x: 18400, - y: 12600+37000-9000, + y: 12600 + 37000 - 9000, shape: "rectangle", width: 27000, height: 74000, @@ -495,7 +495,7 @@ export const wellLayoutDevelopment = { }, { x: 18400 + 30000, - y: 12600+37000-9000, + y: 12600 + 37000 - 9000, shape: "rectangle", width: 27000, height: 74000, @@ -503,7 +503,7 @@ export const wellLayoutDevelopment = { }, { x: 18400 + 2 * 30000, - y: 12600+37000-9000, + y: 12600 + 37000 - 9000, shape: "rectangle", width: 27000, height: 74000, @@ -511,15 +511,15 @@ export const wellLayoutDevelopment = { }, { x: 18400 + 3 * 30000, - y: 12600+37000-9000, + y: 12600 + 37000 - 9000, shape: "rectangle", width: 27000, height: 74000, name: "Slide4", - }, + }, { x: 18400, - y: 12600+37000-9000, + y: 12600 + 37000 - 9000, shape: "circle", radius: 1000, name: "calibration", @@ -559,20 +559,19 @@ export const ropodLayout = { ], }; - // src/utils/generateWellLayout384.js export function generateWellLayout384({ cols = 24, rows = 16, - pitch = 4500, // - radius = 2000, // 3 mm radius + pitch = 4500, // + radius = 2000, // 3 mm radius startX = 12000, startY = 9000, width = 127000, height = 84000, - offsetX = 0, // Global offset in X direction (micrometers) - offsetY = 0, // Global offset in Y direction (micrometers) + offsetX = 0, // Global offset in X direction (micrometers) + offsetY = 0, // Global offset in Y direction (micrometers) } = {}) { const wells = []; @@ -607,14 +606,14 @@ export const wellLayout384 = generateWellLayout384(); export function generateWellLayoutDEPChip({ cols = 8, rows = 6, - pitch = 2250, // 2.25 mm spacing between centers - radius = 750, // 1.5 mm diameter = 0.75 mm radius - startX = 127000/2-4*2250, // Start position X (can be adjusted) - startY = 84000/2-3*2250, // Start position Y (can be adjusted) - width = 127000, // Total width of chip area - height = 84000, // Total height of chip area - offsetX = 0, // Global offset in X direction (micrometers) - offsetY = 0, // Global offset in Y direction (micrometers) + pitch = 2250, // 2.25 mm spacing between centers + radius = 750, // 1.5 mm diameter = 0.75 mm radius + startX = 127000 / 2 - 4 * 2250, // Start position X (can be adjusted) + startY = 84000 / 2 - 3 * 2250, // Start position Y (can be adjusted) + width = 127000, // Total width of chip area + height = 84000, // Total height of chip area + offsetX = 0, // Global offset in X direction (micrometers) + offsetY = 0, // Global offset in Y direction (micrometers) /* startX = 12000, startY = 9000, @@ -663,11 +662,11 @@ export function applyLayoutOffset(layout, offsetX = 0, offsetY = 0) { return { ...layout, - wells: layout.wells.map(well => ({ + wells: layout.wells.map((well) => ({ ...well, x: well.x + offsetX, - y: well.y + offsetY - })) + y: well.y + offsetY, + })), }; } @@ -683,7 +682,7 @@ export function applyLayoutOffset(layout, offsetX = 0, offsetY = 0) { */ export function generateWellCenterPositions(selectedWells, wellLayout) { const positions = []; - + selectedWells.forEach((well, index) => { positions.push({ x: well.x, @@ -691,10 +690,10 @@ export function generateWellCenterPositions(selectedWells, wellLayout) { name: `Well_${index + 1}_Center`, wellId: well.id || index, iX: 0, - iY: 0 + iY: 0, }); }); - + return positions; } @@ -706,25 +705,30 @@ export function generateWellCenterPositions(selectedWells, wellLayout) { * @param {number} stepSize - Base step size between positions in micrometers * @returns {Array} Array of position objects */ -export function generateWellCirclePattern(selectedWells, patternRadius, overlap, stepSize = 10) { +export function generateWellCirclePattern( + selectedWells, + patternRadius, + overlap, + stepSize = 10, +) { const positions = []; - + // Calculate actual step size based on overlap const actualStepSize = stepSize * (1 - overlap); - + selectedWells.forEach((well, wellIndex) => { const wellCenterX = well.x; const wellCenterY = well.y; - + // Generate circular pattern using the existing calculateRasterOval function const circlePositions = calculateRasterOval( { x: wellCenterX, y: wellCenterY }, actualStepSize, actualStepSize, patternRadius, - patternRadius + patternRadius, ); - + // Add each position with well-specific naming circlePositions.forEach((pos, posIndex) => { positions.push({ @@ -734,11 +738,11 @@ export function generateWellCirclePattern(selectedWells, patternRadius, overlap, wellId: well.id || wellIndex, iX: pos.iX, iY: pos.iY, - localIndex: posIndex + localIndex: posIndex, }); }); }); - + return positions; } @@ -751,27 +755,33 @@ export function generateWellCirclePattern(selectedWells, patternRadius, overlap, * @param {number} stepSize - Base step size between positions in micrometers * @returns {Array} Array of position objects */ -export function generateWellRectanglePattern(selectedWells, patternWidth, patternHeight, overlap, stepSize = 10) { +export function generateWellRectanglePattern( + selectedWells, + patternWidth, + patternHeight, + overlap, + stepSize = 10, +) { const positions = []; - + // Calculate actual step size based on overlap const actualStepSize = stepSize * (1 - overlap); - + selectedWells.forEach((well, wellIndex) => { const wellCenterX = well.x; const wellCenterY = well.y; - + // Generate rectangular pattern using the existing calculateRasterRect function const rectPositions = calculateRasterRect( { x: wellCenterX, y: wellCenterY }, actualStepSize, actualStepSize, - patternWidth / 2, // rectPlusX - patternWidth / 2, // rectMinusX + patternWidth / 2, // rectPlusX + patternWidth / 2, // rectMinusX patternHeight / 2, // rectPlusY - patternHeight / 2 // rectMinusY + patternHeight / 2, // rectMinusY ); - + // Add each position with well-specific naming rectPositions.forEach((pos, posIndex) => { positions.push({ @@ -781,11 +791,11 @@ export function generateWellRectanglePattern(selectedWells, patternWidth, patter wellId: well.id || wellIndex, iX: pos.iX, iY: pos.iY, - localIndex: posIndex + localIndex: posIndex, }); }); }); - + return positions; } @@ -797,7 +807,7 @@ export function generateWellRectanglePattern(selectedWells, patternWidth, patter */ export function generatePositionsFromPointList(pointList, wellLayout) { const allPositions = []; - + pointList.forEach((point, pointIndex) => { if (!point.shape || point.shape === "off") { // No pattern, just add the original point @@ -806,39 +816,39 @@ export function generatePositionsFromPointList(pointList, wellLayout) { y: point.y, name: point.name || `Point_${pointIndex + 1}`, iX: 0, - iY: 0 + iY: 0, }); return; } - + if (point.wellMode === "center_only") { // Find wells that intersect with this point and return their centers const intersectingWells = findWellsAtPosition(point, wellLayout); - const centerPositions = generateWellCenterPositions(intersectingWells, wellLayout); + const centerPositions = generateWellCenterPositions( + intersectingWells, + wellLayout, + ); allPositions.push(...centerPositions); - } else if (point.wellMode === "pattern") { // Generate pattern within wells const intersectingWells = findWellsAtPosition(point, wellLayout); - + if (point.patternType === "circle") { const patternPositions = generateWellCirclePattern( intersectingWells, point.patternRadius || 50, - point.patternOverlap || 0.1 + point.patternOverlap || 0.1, ); allPositions.push(...patternPositions); - } else if (point.patternType === "rectangle") { const patternPositions = generateWellRectanglePattern( intersectingWells, point.patternWidth || 100, point.patternHeight || 100, - point.patternOverlap || 0.1 + point.patternOverlap || 0.1, ); allPositions.push(...patternPositions); } - } else { // Traditional rectangle or circle patterns (existing functionality) if (point.shape === "rectangle") { @@ -849,29 +859,32 @@ export function generatePositionsFromPointList(pointList, wellLayout) { point.rectPlusX || 0, point.rectMinusX || 0, point.rectPlusY || 0, - point.rectMinusY || 0 + point.rectMinusY || 0, + ); + allPositions.push( + ...rectPositions.map((pos, i) => ({ + ...pos, + name: `${point.name || `Point_${pointIndex + 1}`}_${i + 1}`, + })), ); - allPositions.push(...rectPositions.map((pos, i) => ({ - ...pos, - name: `${point.name || `Point_${pointIndex + 1}`}_${i + 1}` - }))); - } else if (point.shape === "circle") { const circlePositions = calculateRasterOval( { x: point.x, y: point.y }, 10, // rasterWidth - this should come from experiment settings 10, // rasterHeight - this should come from experiment settings point.circleRadiusX || 0, - point.circleRadiusY || 0 + point.circleRadiusY || 0, + ); + allPositions.push( + ...circlePositions.map((pos, i) => ({ + ...pos, + name: `${point.name || `Point_${pointIndex + 1}`}_${i + 1}`, + })), ); - allPositions.push(...circlePositions.map((pos, i) => ({ - ...pos, - name: `${point.name || `Point_${pointIndex + 1}`}_${i + 1}` - }))); } } }); - + return allPositions; } @@ -885,19 +898,18 @@ export function findWellsAtPosition(point, wellLayout) { if (!wellLayout || !wellLayout.wells) { return []; } - + const intersectingWells = []; - + wellLayout.wells.forEach((well, index) => { let isIntersecting = false; - + if (well.shape === "circle") { // Check if point is within circle const dx = point.x - well.x; const dy = point.y - well.y; const distance = Math.sqrt(dx * dx + dy * dy); isIntersecting = distance <= (well.radius || 0); - } else if (well.shape === "rectangle") { // Check if point is within rectangle const halfWidth = (well.width || 0) / 2; @@ -906,17 +918,17 @@ export function findWellsAtPosition(point, wellLayout) { point, { x: well.x, y: well.y }, well.width || 0, - well.height || 0 + well.height || 0, ); } - + if (isIntersecting) { intersectingWells.push({ ...well, - id: well.id || index + id: well.id || index, }); } }); - + return intersectingWells; } diff --git a/frontend/src/state/slices/ExperimentSlice.js b/frontend/src/state/slices/ExperimentSlice.js index 07c04a44..98a8b9f7 100644 --- a/frontend/src/state/slices/ExperimentSlice.js +++ b/frontend/src/state/slices/ExperimentSlice.js @@ -1,27 +1,18 @@ import { createSlice } from "@reduxjs/toolkit"; import { v4 as uuidv4 } from "uuid"; +import { wellLayoutDevelopment } from "../../axon/WellSelectorUtils.js"; + +const cloneLayout = (layout) => ({ + ...layout, + wells: Array.isArray(layout?.wells) + ? layout.wells.map((well) => ({ ...well })) + : [], +}); // Define the initial state const initialExperimentState = { name: "experiment", - wellLayout: { - name: "Default", - unit: "um", - width: 1000000, - height: 600000, - wells: [ - /* Example well - { x: 200000, y: 200000, shape: "circle", radius: 50000 }, - { x: 400000, y: 200000, shape: "circle", radius: 90000 }, - { x: 600000, y: 200000, shape: "circle", radius: 90000 }, - { x: 800000, y: 200000, shape: "circle", radius: 90000 }, - { x: 200000, y: 400000, shape: "circle", radius: 90000 }, - { x: 400000, y: 400000, shape: "circle", radius: 90000, name: "A1" }, - { x: 600000, y: 400000, shape: "rectangle", width: 90000, height: 180000, }, - { x: 800000, y: 400000, shape: "rectangle", width: 180000, height: 180000, }, - */ - ], - }, + wellLayout: cloneLayout(wellLayoutDevelopment), pointList: [ /* Example item: { @@ -41,7 +32,7 @@ const initialExperimentState = { ], parameterValue: { illumination: [], - channelEnabledForExperiment: [], // Per-channel boolean: include this channel in experiment (separate from physical laser on/off) + channelEnabledForExperiment: [], // Per-channel boolean: include this channel in experiment (separate from physical laser on/off) darkfield: false, illuIntensities: 0, differentialPhaseContrast: false, @@ -62,10 +53,10 @@ const initialExperimentState = { // Software autofocus method: "scan" (Z-sweep) or "hillClimbing" (gradient search) autoFocusSoftwareMethod: "scan", // Hill-climbing autofocus parameters - autoFocusHillClimbingInitialStep: 20, // Starting step size (µm) - autoFocusHillClimbingMinStep: 1, // Convergence criterion (µm) + autoFocusHillClimbingInitialStep: 20, // Starting step size (µm) + autoFocusHillClimbingMinStep: 1, // Convergence criterion (µm) autoFocusHillClimbingStepReduction: 0.5, // Step reduction factor on reversal - autoFocusHillClimbingMaxIterations: 50, // Safety iteration limit + autoFocusHillClimbingMaxIterations: 50, // Safety iteration limit // Hardware autofocus (FocusLock-based one-shot) parameters autoFocusMode: "software", // "software" (Z-sweep/hill-climbing) or "hardware" (one-shot using FocusLock) autofocus_max_attempts: 3, // Max attempts for hardware autofocus @@ -77,31 +68,31 @@ const initialExperimentState = { speed: 20000, z_speed: 5000, // Tiling behaviour toggles (Tiling tab). Default off. - returnToOrigin: false, // move stage back to pre-scan XYZ when the scan ends + returnToOrigin: false, // move stage back to pre-scan XYZ when the scan ends overrideZWithCurrentZ: false, // use current stage Z for every position (ignored if focus map active) gains: 0, exposureTimes: 0, performanceMode: false, // Performance mode advanced settings performanceTriggerMode: "hardware", // "hardware" (external trigger) or "software" (callback-based) - performanceTPreMs: 90, // Pre-exposure settle time in milliseconds - performanceTPostMs: 50, // Post-exposure time (exposure) in milliseconds + performanceTPreMs: 90, // Pre-exposure settle time in milliseconds + performanceTPostMs: 50, // Post-exposure time (exposure) in milliseconds ome_write_tiff: false, ome_write_zarr: false, ome_write_stitched_tiff: true, ome_write_individual_tiffs: false, ome_write_ashlar_stitch: false, - ashlar_pixel_size: 1.0, // µm/pixel — physical pixel size for Ashlar alignment + ashlar_pixel_size: 1.0, // µm/pixel — physical pixel size for Ashlar alignment ashlar_pixel_size_user_set: false, // true once the user has manually edited the field - ashlar_maximum_shift: 50.0, // µm — max per-tile corrective shift allowed by Ashlar - ashlar_align_channel: 0, // zero-based channel index used for tile alignment + ashlar_maximum_shift: 50.0, // µm — max per-tile corrective shift allowed by Ashlar + ashlar_align_channel: 0, // zero-based channel index used for tile alignment // Tile overlap parameters (moved from WellSelectorSlice) - overlapWidth: 0.0, // 0.0 = no overlap (100% spacing), 0.1 = 10% overlap (90% spacing) - overlapHeight: 0.0, // 0.0 = no overlap (100% spacing), 0.1 = 10% overlap (90% spacing) + overlapWidth: 0.0, // 0.0 = no overlap (100% spacing), 0.1 = 10% overlap (90% spacing) + overlapHeight: 0.0, // 0.0 = no overlap (100% spacing), 0.1 = 10% overlap (90% spacing) // Snakescan pattern for tiling - is_snakescan: false, // Enable snakescan pattern (alternating row directions) + is_snakescan: false, // Enable snakescan pattern (alternating row directions) // Illumination mode - keepIlluminationOn: "auto", // "auto" = on for single channel, off for multi | "on" = always keep on | "off" = per-frame toggle + keepIlluminationOn: "auto", // "auto" = on for single channel, off for multi | "on" = always keep on | "off" = per-frame toggle // Per-channel kind-specific params (radius/RGB) keyed by channel name. // Drives LED-matrix synthetic channels ("ring" / "dpc"); ignored for // "default" channels. Example shape: @@ -252,12 +243,12 @@ const experimentSlice = createSlice({ state.parameterValue.zStackStepSize = action.payload; }, setSpeed: (state, action) => { - console.log("setSpeed"); - state.parameterValue.speed = action.payload; + console.log("setSpeed"); + state.parameterValue.speed = action.payload; }, setZSpeed: (state, action) => { - console.log("setZSpeed"); - state.parameterValue.z_speed = action.payload; + console.log("setZSpeed"); + state.parameterValue.z_speed = action.payload; }, setGains: (state, action) => { console.log("setGains"); @@ -329,11 +320,17 @@ const experimentSlice = createSlice({ //------------------------ overlap parameters setOverlapWidth: (state, action) => { console.log("setOverlapWidth", action.payload); - state.parameterValue.overlapWidth = Math.max(-15, Math.min(0.5, action.payload)); // Clamp between -0.5 and 0.5 (-50% to 50%) + state.parameterValue.overlapWidth = Math.max( + -15, + Math.min(0.5, action.payload), + ); // Clamp between -0.5 and 0.5 (-50% to 50%) }, setOverlapHeight: (state, action) => { console.log("setOverlapHeight", action.payload); - state.parameterValue.overlapHeight = Math.max(-15, Math.min(0.5, action.payload)); // Clamp between -0.5 and 0.5 (-50% to 50%) + state.parameterValue.overlapHeight = Math.max( + -15, + Math.min(0.5, action.payload), + ); // Clamp between -0.5 and 0.5 (-50% to 50%) }, setIsSnakescan: (state, action) => { console.log("setIsSnakescan", action.payload); @@ -354,7 +351,9 @@ const experimentSlice = createSlice({ setIlluminationParams: (state, action) => { console.log("setIlluminationParams", action.payload); state.parameterValue.illuminationParams = - action.payload && typeof action.payload === "object" ? action.payload : {}; + action.payload && typeof action.payload === "object" + ? action.payload + : {}; }, // Merge a single channel's params dict. Payload shape: // { channelName: "LED Matrix Ring", params: { radius: 8, intensityG: 255 } } @@ -375,15 +374,25 @@ const experimentSlice = createSlice({ id: uuidv4(), x: action.payload.x, y: action.payload.y, - z: (action.payload.z != null) ? (action.payload.z) : (0), - name: (action.payload.name != null) ? (action.payload.name) : (""), - shape: (action.payload.shape != null) ? (action.payload.shape) : (""), - rectPlusX: (action.payload.rectPlusX != null) ? (action.payload.rectPlusX) : (0), - rectPlusY: (action.payload.rectPlusY != null) ? (action.payload.rectPlusY) : (0), - rectMinusX: (action.payload.rectMinusX != null) ? (action.payload.rectMinusX) : (0), - rectMinusY: (action.payload.rectMinusY != null) ? (action.payload.rectMinusY) : (0), - circleRadiusX: (action.payload.circleRadiusX != null) ? (action.payload.circleRadiusX) : (0), - circleRadiusY: (action.payload.circleRadiusY != null) ? (action.payload.circleRadiusY) : (0), + z: action.payload.z != null ? action.payload.z : 0, + name: action.payload.name != null ? action.payload.name : "", + shape: action.payload.shape != null ? action.payload.shape : "", + rectPlusX: + action.payload.rectPlusX != null ? action.payload.rectPlusX : 0, + rectPlusY: + action.payload.rectPlusY != null ? action.payload.rectPlusY : 0, + rectMinusX: + action.payload.rectMinusX != null ? action.payload.rectMinusX : 0, + rectMinusY: + action.payload.rectMinusY != null ? action.payload.rectMinusY : 0, + circleRadiusX: + action.payload.circleRadiusX != null + ? action.payload.circleRadiusX + : 0, + circleRadiusY: + action.payload.circleRadiusY != null + ? action.payload.circleRadiusY + : 0, // Optional grouping metadata. Points sharing the same ``areaId`` / // ``groupId`` belong to a single logical scan area (e.g. freehand // polygon, area-select rectangle, per-well subpoints) and downstream @@ -398,24 +407,29 @@ const experimentSlice = createSlice({ ? action.payload.neighborPointList : [], }; - + console.log("createPoint newPoint", newPoint); // Prevent duplicate points with the same physical position and shape - const isDuplicate = state.pointList.some((existing) => - existing.x === newPoint.x && - existing.y === newPoint.y && - existing.shape === newPoint.shape && - existing.rectPlusX === newPoint.rectPlusX && - existing.rectPlusY === newPoint.rectPlusY && - existing.rectMinusX === newPoint.rectMinusX && - existing.rectMinusY === newPoint.rectMinusY && - existing.circleRadiusX === newPoint.circleRadiusX && - existing.circleRadiusY === newPoint.circleRadiusY + const isDuplicate = state.pointList.some( + (existing) => + existing.x === newPoint.x && + existing.y === newPoint.y && + existing.shape === newPoint.shape && + existing.rectPlusX === newPoint.rectPlusX && + existing.rectPlusY === newPoint.rectPlusY && + existing.rectMinusX === newPoint.rectMinusX && + existing.rectMinusY === newPoint.rectMinusY && + existing.circleRadiusX === newPoint.circleRadiusX && + existing.circleRadiusY === newPoint.circleRadiusY, ); if (!isDuplicate) { state.pointList.push(newPoint); } else { - console.warn("[ExperimentSlice] Skipped duplicate point at", newPoint.x, newPoint.y); + console.warn( + "[ExperimentSlice] Skipped duplicate point at", + newPoint.x, + newPoint.y, + ); } }, addPoint: (state, action) => { @@ -428,7 +442,7 @@ const experimentSlice = createSlice({ // the same well twice. const incoming = Array.isArray(action.payload) ? action.payload : []; const seen = new Set( - state.pointList.map((p) => `${p.x}|${p.y}|${p.name || ""}`) + state.pointList.map((p) => `${p.x}|${p.y}|${p.name || ""}`), ); for (const p of incoming) { const newPoint = { From 3614d2bc30941a9cf1ab66464620ba44c13e2aff Mon Sep 17 00:00:00 2001 From: Florian Paproth Date: Thu, 2 Jul 2026 23:12:40 +0200 Subject: [PATCH 4/4] Refactor ContextMenu styles for improved consistency and readability --- .../components/ContextMenu/ContextMenu.scss | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/frontend/src/FileManager/components/ContextMenu/ContextMenu.scss b/frontend/src/FileManager/components/ContextMenu/ContextMenu.scss index 3018335e..70fce17b 100644 --- a/frontend/src/FileManager/components/ContextMenu/ContextMenu.scss +++ b/frontend/src/FileManager/components/ContextMenu/ContextMenu.scss @@ -2,12 +2,13 @@ .fm-context-menu { position: absolute; - background-color: rgb(175, 62, 62); - border: 1px solid #c6c6c6; + background-color: #1e1e1e; + border: 1px solid #424242; border-radius: 6px; padding: 4px; z-index: 1; transition: opacity 0.1s linear; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45); .file-context-menu-list { font-size: 1.1em; @@ -30,12 +31,12 @@ &:hover { cursor: pointer; - background-color: rgb(0, 0, 0, 0.07); + background-color: rgba(255, 255, 255, 0.08); } } li.active { - background-color: rgb(0, 0, 0, 0.07); + background-color: rgba(255, 255, 255, 0.08); } li.disable-paste { @@ -49,34 +50,34 @@ } .divider { - border-bottom: 1px solid #c6c6c6; + border-bottom: 1px solid #424242; margin: 5px 0 3px 0; } .list-expand-icon { margin-left: auto; - color: #444444; + color: #aaaaaa; } .sub-menu { position: absolute; left: calc(100% - 2px); top: 0; - background-color: white; - border: 1px solid #c6c6c6; + background-color: #1e1e1e; + border: 1px solid #424242; border-radius: 6px; padding: 4px; z-index: 1; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45); .item-selected { width: 13px; - color: #444444; + color: #aaaaaa; } li { - &:hover { - background-color: rgb(0, 0, 0, 0.07) !important; + background-color: rgba(255, 255, 255, 0.08) !important; } } } @@ -93,4 +94,4 @@ opacity: 1; pointer-events: all; visibility: visible; -} \ No newline at end of file +}